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
+18 -16
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { track } from "@/lib/analytics";
import { useI18n } from "@/lib/i18n/client";
type AuthStep = "pick" | "email-input" | "otp-verify";
@@ -25,6 +26,7 @@ export function AuthModal({
const [otp, setOtp] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const { t } = useI18n();
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
@@ -120,21 +122,21 @@ export function AuthModal({
}}
role="dialog"
aria-modal="true"
aria-label="登录"
aria-label={t("auth.ariaLabel")}
>
{/* header */}
<div className="flex items-center justify-between border-b border-cream-50/10 px-5 py-3.5">
<div className="flex items-center gap-2 text-[11px] smallcaps text-cream-50/70">
<i className="fa-solid fa-right-to-bracket text-[11px]" />
{step === "pick" && "登录以继续"}
{step === "email-input" && "邮箱登录"}
{step === "otp-verify" && "验证码"}
{step === "pick" && t("auth.steps.pick")}
{step === "email-input" && t("auth.steps.email")}
{step === "otp-verify" && t("auth.steps.otp")}
</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="关闭"
aria-label={t("auth.close")}
>
<i className="fa-solid fa-xmark text-[12px]" />
</button>
@@ -154,7 +156,7 @@ export function AuthModal({
className="flex w-full items-center justify-center gap-2.5 rounded-md border border-cream-50/15 bg-cream-50/[0.06] px-4 py-2.5 text-[13px] text-cream-50/90 transition-colors hover:bg-cream-50/[0.12] disabled:opacity-50"
>
<i className="fa-brands fa-google text-[14px]" />
Google
{t("auth.googleLogin")}
</button>
<button
type="button"
@@ -163,11 +165,11 @@ export function AuthModal({
className="flex w-full items-center justify-center gap-2.5 rounded-md border border-cream-50/15 bg-cream-50/[0.06] px-4 py-2.5 text-[13px] text-cream-50/90 transition-colors hover:bg-cream-50/[0.12] disabled:opacity-50"
>
<i className="fa-brands fa-github text-[14px]" />
GitHub
{t("auth.githubLogin")}
</button>
<div className="flex items-center gap-3 py-1">
<div className="h-px flex-1 bg-cream-50/10" />
<span className="text-[10px] text-cream-50/40"></span>
<span className="text-[10px] text-cream-50/40">{t("auth.or")}</span>
<div className="h-px flex-1 bg-cream-50/10" />
</div>
<button
@@ -176,7 +178,7 @@ export function AuthModal({
className="flex w-full items-center justify-center gap-2.5 rounded-md border border-cream-50/15 bg-cream-50/[0.06] px-4 py-2.5 text-[13px] text-cream-50/90 transition-colors hover:bg-cream-50/[0.12]"
>
<i className="fa-solid fa-envelope text-[13px]" />
{t("auth.emailLogin")}
</button>
</>
)}
@@ -188,7 +190,7 @@ export function AuthModal({
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSendOtp()}
placeholder="your@email.com"
placeholder={t("auth.emailPlaceholder")}
autoFocus
className="w-full rounded-md border border-cream-50/15 bg-cream-50/[0.06] px-3.5 py-2.5 text-[13px] text-cream-50/90 placeholder:text-cream-50/30 outline-none focus:border-[rgba(175,138,72,0.6)]"
/>
@@ -198,7 +200,7 @@ export function AuthModal({
onClick={handleSendOtp}
className="w-full rounded-md bg-[rgba(175,138,72,0.85)] px-4 py-2.5 text-[13px] font-medium text-cream-50 transition-colors hover:bg-[rgba(175,138,72,1)] disabled:opacity-50"
>
{loading ? "发送中..." : "发送验证码"}
{loading ? t("auth.sending") : t("auth.sendCode")}
</button>
<button
type="button"
@@ -208,7 +210,7 @@ export function AuthModal({
}}
className="w-full text-center text-[12px] text-cream-50/50 transition-colors hover:text-cream-50/80"
>
{t("auth.back")}
</button>
</>
)}
@@ -216,7 +218,7 @@ export function AuthModal({
{step === "otp-verify" && (
<>
<p className="text-[12px] text-cream-50/60 leading-snug">
<span className="text-cream-50/90">{email.trim()}</span>
{t("auth.codeSent", { email: email.trim() })}
</p>
<input
type="text"
@@ -225,7 +227,7 @@ export function AuthModal({
value={otp}
onChange={(e) => setOtp(e.target.value.replace(/\D/g, ""))}
onKeyDown={(e) => e.key === "Enter" && handleVerifyOtp()}
placeholder="6 位验证码"
placeholder={t("auth.codePlaceholder")}
autoFocus
className="w-full rounded-md border border-cream-50/15 bg-cream-50/[0.06] px-3.5 py-2.5 text-center text-[16px] tracking-[0.35em] text-cream-50/90 placeholder:text-cream-50/30 placeholder:tracking-normal outline-none focus:border-[rgba(175,138,72,0.6)]"
/>
@@ -235,7 +237,7 @@ export function AuthModal({
onClick={handleVerifyOtp}
className="w-full rounded-md bg-[rgba(175,138,72,0.85)] px-4 py-2.5 text-[13px] font-medium text-cream-50 transition-colors hover:bg-[rgba(175,138,72,1)] disabled:opacity-50"
>
{loading ? "验证中..." : "确认"}
{loading ? t("auth.verifying") : t("auth.verify")}
</button>
<button
type="button"
@@ -246,7 +248,7 @@ export function AuthModal({
}}
className="w-full text-center text-[12px] text-cream-50/50 transition-colors hover:text-cream-50/80"
>
{t("auth.resend")}
</button>
</>
)}