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
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useRef } from "react";
import { useI18n } from "@/lib/i18n/client";
export type DialogueHistoryItem = {
id: string;
@@ -23,6 +24,7 @@ export function DialogueHistoryModal({
onClose: () => void;
playerName?: string;
}) {
const { t } = useI18n();
const displaySpeaker = (s: string | undefined) =>
s === "你" && playerName ? playerName : s;
const listRef = useRef<HTMLDivElement>(null);
@@ -63,19 +65,19 @@ export function DialogueHistoryModal({
}}
role="dialog"
aria-modal="true"
aria-label="剧情回溯"
aria-label={t("history.ariaLabel")}
>
<div className="flex items-center justify-between border-b border-cream-50/10 px-4 py-3">
<div className="flex items-center gap-2 text-[10px] smallcaps text-cream-50/70">
<i className="fa-solid fa-clock-rotate-left text-[10px]" />
· · ·
{t("history.title")}
</div>
<button
type="button"
onClick={onClose}
className="flex h-7 w-7 items-center justify-center text-cream-50/60 transition-colors hover:text-cream-50"
aria-label="关闭剧情回溯"
title="关闭"
aria-label={t("history.closeAriaLabel")}
title={t("history.close")}
>
<i className="fa-solid fa-xmark text-[12px]" />
</button>
@@ -89,7 +91,7 @@ export function DialogueHistoryModal({
>
{items.length === 0 ? (
<p className="py-8 text-center font-serif text-[13px] text-cream-50/55">
{t("history.noHistory")}
</p>
) : (
<div className="space-y-3">
@@ -97,7 +99,7 @@ export function DialogueHistoryModal({
<div key={item.id} className="text-left">
<div className="mb-1 flex items-baseline gap-2">
<span className="text-[9px] smallcaps text-cream-50/35">
{String(item.sceneIndex).padStart(3, "0")}
{t("history.scene", { n: String(item.sceneIndex).padStart(3, "0") })}
</span>
{item.speaker && (
<span className="font-serif text-[12px] text-[rgba(205,165,90,0.92)]">
@@ -128,7 +130,7 @@ export function DialogueHistoryModal({
{item.selectedChoice && (
<p className="mt-2 inline-flex max-w-full items-start gap-2 rounded-[5px] border border-[rgba(180,140,80,0.35)] bg-[rgba(180,140,60,0.10)] px-2.5 py-1.5 font-serif text-[12px] leading-snug text-cream-50/85">
<span className="shrink-0 text-[rgba(195,155,75,0.9)]">
{t("history.choice")}
</span>
<span>{item.selectedChoice}</span>
</p>
@@ -136,7 +138,7 @@ export function DialogueHistoryModal({
{item.freeformAction && (
<p className="mt-2 inline-flex max-w-full items-start gap-2 rounded-[5px] border border-ember-500/30 bg-ember-500/10 px-2.5 py-1.5 font-serif text-[12px] leading-snug text-cream-50/85">
<span className="shrink-0 text-ember-300/90">
{t("history.action")}
</span>
<span>{item.freeformAction}</span>
</p>