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
+10 -8
View File
@@ -3,9 +3,11 @@
import { useRouter } from "next/navigation";
import { useState } from "react";
import { track } from "@/lib/analytics";
import { useI18n } from "@/lib/i18n/client";
export function CustomForm() {
const router = useRouter();
const { t } = useI18n();
const [worldSetting, setWorldSetting] = useState("");
const [styleGuide, setStyleGuide] = useState("");
const [submitting, setSubmitting] = useState(false);
@@ -35,7 +37,7 @@ export function CustomForm() {
<span className="text-clay-400 mr-2 font-serif italic not-italic font-normal">
</span>
World ·
{t("customForm.world")}
</span>
<span className="text-[10px] text-clay-400 num">
{worldSetting.length}
@@ -45,7 +47,7 @@ export function CustomForm() {
value={worldSetting}
onChange={(e) => setWorldSetting(e.target.value)}
rows={6}
placeholder="例:1990 年代末的中国南方县城。主角是高三转学生,在多雨的六月遇到一个总在天台读诗的同学。剧情慢热、含蓄、带点伤感⋯"
placeholder={t("customForm.worldPlaceholder")}
className="w-full bg-transparent border-0 border-b border-clay-900/20 px-0 py-3 text-clay-900 font-serif text-lg leading-[1.7] focus:outline-none focus:border-clay-700 transition-colors resize-none placeholder:font-serif placeholder:italic placeholder:text-base placeholder:leading-[1.7]"
/>
</div>
@@ -56,7 +58,7 @@ export function CustomForm() {
<span className="text-clay-400 mr-2 font-serif italic not-italic font-normal">
</span>
Style ·
{t("customForm.style")}
</span>
<span className="text-[10px] text-clay-400 num">
{styleGuide.length}
@@ -66,7 +68,7 @@ export function CustomForm() {
value={styleGuide}
onChange={(e) => setStyleGuide(e.target.value)}
rows={4}
placeholder="例:水彩柔光,午后暖意,动漫视觉小说画风,传统对话面板⋯"
placeholder={t("customForm.stylePlaceholder")}
className="w-full bg-transparent border-0 border-b border-clay-900/20 px-0 py-3 text-clay-900 font-serif text-lg leading-[1.7] focus:outline-none focus:border-clay-700 transition-colors resize-none placeholder:font-serif placeholder:italic placeholder:text-base placeholder:leading-[1.7]"
/>
</div>
@@ -74,17 +76,17 @@ export function CustomForm() {
<div className="pt-6 flex items-center justify-between">
<span className="text-[10px] smallcaps text-clay-500">
{submitting
? "正在唤起第一帧…"
? t("customForm.status.starting")
: canSubmit
? "准 · 备 · 就 · 绪"
: "两 · 段 · 即 · 可 · 开 · 场"}
? t("customForm.status.ready")
: t("customForm.status.needMore")}
</span>
<button
type="submit"
disabled={!canSubmit}
className="group flex items-center gap-3 text-[10px] smallcaps text-clay-900 disabled:text-clay-300 disabled:cursor-not-allowed enabled:hover:text-ember-500 transition-colors duration-300"
>
{t("customForm.start")}
<span className="w-10 h-px bg-current transition-all duration-300 group-enabled:group-hover:w-16" />
<i className="fa-solid fa-arrow-right text-[9px]" />
</button>