Files
infiplot-web/lib/i18n/server.ts
T
DESKTOP-I1T6TF3\Q 2d35c1d9de 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>
2026-06-18 16:54:35 +08:00

177 lines
5.1 KiB
TypeScript

import type { Locale } from "./config";
import { DEFAULT_LOCALE, getInitialLocale } from "./config";
import { getNestedValue, formatTranslation } from "./utils";
// Server-side translation cache
const translationCache = new Map<Locale, Record<string, unknown>>();
// Get locale from request headers
export function getLocaleFromHeaders(headers: Headers): Locale {
// Check for custom locale header
const customLocale = headers.get("x-locale");
if (customLocale) {
return customLocale as Locale;
}
// Check Accept-Language header
const acceptLanguage = headers.get("accept-language");
if (acceptLanguage) {
const browserLang = acceptLanguage.split(",")[0]?.split("-")[0];
// Map common language codes to our locales
const localeMap: Record<string, Locale> = {
en: "en",
zh: "zh-CN",
ja: "ja",
ko: "ko",
es: "es",
fr: "fr",
de: "de",
pt: "pt",
ru: "ru",
it: "it",
vi: "vi",
th: "th",
id: "id",
tr: "tr",
pl: "pl",
nl: "nl",
uk: "uk",
hi: "hi",
cs: "cs",
};
const browserLangBase = acceptLanguage.split(",")[0]?.split("-")[0];
if (browserLangBase) {
const matched = localeMap[browserLangBase];
if (matched) return matched;
}
}
return DEFAULT_LOCALE;
}
// Load translations for server-side
export async function loadTranslations(locale: Locale): Promise<Record<string, unknown>> {
// Check cache first
if (translationCache.has(locale)) {
return translationCache.get(locale)!;
}
try {
// Dynamic import based on locale
let translations;
switch (locale) {
case "zh-CN":
translations = (await import("./locales/zh-CN")).zhCN;
break;
case "en":
translations = (await import("./locales/en")).en;
break;
case "zh-TW":
translations = (await import("./locales/zh-TW")).zhTW;
break;
case "zh-HK":
translations = (await import("./locales/zh-HK")).zhHK;
break;
case "ja":
translations = (await import("./locales/ja")).ja;
break;
case "ko":
translations = (await import("./locales/ko")).ko;
break;
case "es":
translations = (await import("./locales/es")).es;
break;
case "fr":
translations = (await import("./locales/fr")).fr;
break;
case "de":
translations = (await import("./locales/de")).de;
break;
case "pt-BR":
translations = (await import("./locales/pt-BR")).ptBR;
break;
case "pt":
translations = (await import("./locales/pt")).pt;
break;
case "ru":
translations = (await import("./locales/ru")).ru;
break;
case "it":
translations = (await import("./locales/it")).it;
break;
case "vi":
translations = (await import("./locales/vi")).vi;
break;
case "th":
translations = (await import("./locales/th")).th;
break;
case "id":
translations = (await import("./locales/id")).id;
break;
case "tr":
translations = (await import("./locales/tr")).tr;
break;
case "pl":
translations = (await import("./locales/pl")).pl;
break;
case "nl":
translations = (await import("./locales/nl")).nl;
break;
case "uk":
translations = (await import("./locales/uk")).uk;
break;
case "hi":
translations = (await import("./locales/hi")).hi;
break;
case "cs":
translations = (await import("./locales/cs")).cs;
break;
default:
console.warn(`Translations for ${locale} not found, using English fallback`);
translations = (await import("./locales/en")).en;
break;
}
translationCache.set(locale, translations as Record<string, unknown>);
return translations as Record<string, unknown>;
} catch (error) {
console.error(`Failed to load translations for ${locale}:`, error);
// Fallback to default locale
const fallback = await import("./locales/zh-CN");
return fallback.zhCN as Record<string, unknown>;
}
}
// Server-side translation function
export async function getTranslations(locale: Locale): Promise<Record<string, unknown>> {
return loadTranslations(locale);
}
// Create a translation function for server components
export function createTranslator(translations: Record<string, unknown>) {
return function t(key: string, params: Record<string, string | number | boolean> = {}): string {
const value = getNestedValue(translations, key);
if (value === undefined) {
console.warn(`Translation missing for key: ${key}`);
return key;
}
if (typeof value === "function") {
return (value as (params: Record<string, string | number | boolean>) => string)(params);
}
if (typeof value === "string") {
return formatTranslation(value, params);
}
return String(value);
};
}
// Get initial locale for server components
export function getServerLocale(): Locale {
return DEFAULT_LOCALE; // Will be overridden by middleware in production
}