feat(i18n): add language switcher with en/ja translations

- New client-side i18n via React Context (useI18n, tArray, I18nProvider)
- Catalog ships 21 locale stubs; only zh-CN/en/ja have reviewed translations
- Header language switcher (globe icon + short label) before settings gear
- All hardcoded Chinese UI text migrated to keys: typewriter, options,
  hints (with embedded gear icon via dangerouslySetInnerHTML), settings
  panel, footer/about, play page hints
- AI output language follows user-selected locale via trailing one-liner
  directive appended to Architect/Writer/CharacterDesigner/InsertBeat
  user messages (preserves system-prompt cacheability)
- Per-locale separator rule: zh uses middot between every glyph; en/ja
  use plain spaces
- Option value → i18n key suffix maps preserve Chinese as the underlying
  identifier so analytics unions and STYLE_MAP keys stay byte-stable

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
DESKTOP-I1T6TF3\Q
2026-06-18 16:54:35 +08:00
parent f1fe7964a2
commit 2d35c1d9de
52 changed files with 6411 additions and 261 deletions
+1
View File
@@ -64,6 +64,7 @@ export async function startSession(
styleReferenceImage: req.styleReferenceImage?.trim() || undefined,
orientation: coerceOrientation(req.orientation),
playerName: req.playerName?.trim() || undefined,
language: req.language?.trim() || undefined,
};
// Stage 0 — Architect (+ optional auto style selection, in parallel).
+71
View File
@@ -9,6 +9,60 @@ import type {
} from "@infiplot/types";
import { formatStepfunCatalogForPrompt } from "@infiplot/tts-client";
// ══════════════════════════════════════════════════════════════════════
// Output-language directive — appended to user messages so the AI's
// GENERATED dialogue, narration, and voice-design text follow the UI
// locale the player picked. Returns "" for zh-CN (the prompts' native
// language) so existing sessions behave byte-identically to before.
//
// We intentionally append this as a TRAILING one-liner rather than
// rewriting the system prompts in the target language — the prompts body
// is the cacheable / reviewed / future-edit-friendly asset, and a single
// trailing directive is enough for modern LLMs to switch their output
// language while still receiving Chinese instructions.
// ══════════════════════════════════════════════════════════════════════
const LANG_LABELS: Record<string, string> = {
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"zh-HK": "繁體中文(香港)",
en: "English",
ja: "日本語",
ko: "한국어",
es: "Español",
fr: "Français",
de: "Deutsch",
"pt-BR": "Português (Brasil)",
pt: "Português",
ru: "Русский",
it: "Italiano",
vi: "Tiếng Việt",
th: "ภาษาไทย",
id: "Bahasa Indonesia",
tr: "Türkçe",
pl: "Polski",
nl: "Nederlands",
uk: "Українська",
hi: "हिन्दी",
cs: "Čeština",
};
/**
* Returns a one-line Chinese instruction telling the LLM to produce its
* free-form output (dialogue, narration, voice-design text) in the player's
* selected UI language. Returns an empty string for zh-CN sessions — those
* are the prompts' native language and need no directive.
*
* Always returns Chinese regardless of session.language because the system
* prompts are Chinese; the directive instructs the model to *output* in the
* target language, not to read prompts in it.
*/
export function buildLanguageDirective(language: string | undefined): string {
if (!language || language === "zh-CN") return "";
const label = LANG_LABELS[language];
if (!label) return "";
return `\n【输出语言】你产出的所有自然语言内容(对白台词 line / 旁白 narration / sceneSummary / storyState 各字段 / voiceDescription / lineDelivery 等)必须使用「${label}」;JSON 字段名、sceneKey、英文 visualDescription / painting prompt 仍按各 agent 既有规则。`;
}
// ══════════════════════════════════════════════════════════════════════
// Multi-agent scene generation pipeline:
// Architect (总编剧) — ONE-TIME at session start: the story bible
@@ -141,6 +195,8 @@ export function buildArchitectUserMessage(session: Session): string {
parts.push(
"\n请据此产出这部交互剧的故事档案(story bible),严格以 JSON 格式返回。",
);
const langDirective = buildLanguageDirective(session.language);
if (langDirective) parts.push(langDirective);
return parts.join("\n");
}
@@ -534,6 +590,8 @@ export function buildWriterPlanUserMessage(session: Session): string {
parts.push(
'\n现在**只规划本场景的骨架**(不要写 beats 台词):给出 sceneSummary(画面感强、含开场钩子)、sceneKey、entryBeatId、本场景会出场的全部角色 cast、以及入口 beat 的 entrySpeaker 与 entryActiveCharacters。严格以 JSON 格式返回。',
);
const langDirective = buildLanguageDirective(session.language);
if (langDirective) parts.push(langDirective);
return parts.join("\n");
}
@@ -577,6 +635,8 @@ export function buildWriterBeatsUserMessage(
parts.push(
"\n把上面的规划展开成完整的 beats[](入口 beat 用规划的 entryBeatId / speaker / 登场角色),写完后更新 storyStatePatch。严格以 JSON 格式返回。",
);
const langDirective = buildLanguageDirective(session.language);
if (langDirective) parts.push(langDirective);
return parts.join("\n");
}
@@ -737,6 +797,15 @@ export function buildCharacterDesignerUserMessage(
parts.push(
"\n请为该角色同时设计 visualDescription(英文,必须覆盖 system 中的 6 大要素清单)和 voiceDescription(中文),严格以 JSON 格式返回。",
);
// When the player picked a non-zh-CN UI language, override the
// system-prompt's "中文" voiceDescription guidance: the description text
// flows into MiMo's voice-design, which gives better prosody when the
// description is written in the target output language. (StepFun's 32
// preset voices are fixed Chinese timbres, but voiceDescription is still
// used as documentation + stepfunVoiceId picking context — keeping it
// in the player's language is consistent.)
const langDirective = buildLanguageDirective(session.language);
if (langDirective) parts.push(langDirective);
return parts.join("\n");
}
@@ -1061,6 +1130,8 @@ export function buildInsertBeatUserMessage(
parts.push(`\n玩家此刻的自由动作:${freeformAction}`);
parts.push("\n请生成一个有实质回应的 beat,严格以 JSON 格式返回。");
const langDirective = buildLanguageDirective(session.language);
if (langDirective) parts.push(langDirective);
return parts.join("\n");
}