2d35c1d9de
- 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>
88 lines
2.0 KiB
TypeScript
88 lines
2.0 KiB
TypeScript
import type { Locale } from "./config";
|
|
|
|
/**
|
|
* Get a nested value from an object using a dot-notation path
|
|
* @example getNestedValue({ a: { b: "c" } }, "a.b") // "c"
|
|
*/
|
|
export function getNestedValue<T>(obj: T, path: string): unknown {
|
|
return path.split(".").reduce<unknown>((current, key) => {
|
|
if (current && typeof current === "object" && key in current) {
|
|
return (current as Record<string, unknown>)[key];
|
|
}
|
|
return undefined;
|
|
}, obj);
|
|
}
|
|
|
|
/**
|
|
* Format a translation string with parameters
|
|
* Supports both {{key}} syntax and simple function-based interpolation
|
|
*/
|
|
export function formatTranslation(
|
|
template: string,
|
|
params: Record<string, string | number | boolean>,
|
|
): string {
|
|
if (Object.keys(params).length === 0) return template;
|
|
|
|
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
return params[key]?.toString() ?? `{{${key}}}`;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Deep merge two objects
|
|
*/
|
|
export function deepMerge<T extends Record<string, unknown>>(
|
|
target: T,
|
|
source: Partial<T>,
|
|
): T {
|
|
const result = { ...target };
|
|
|
|
for (const key in source) {
|
|
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
if (result[key] && typeof result[key] === "object" && !Array.isArray(result[key])) {
|
|
result[key] = deepMerge(
|
|
result[key] as Record<string, unknown>,
|
|
source[key] as Record<string, unknown>,
|
|
) as T[Extract<keyof T, string>];
|
|
} else {
|
|
result[key] = source[key] as T[Extract<keyof T, string>];
|
|
}
|
|
} else {
|
|
result[key] = source[key] as T[Extract<keyof T, string>];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Validate locale string
|
|
*/
|
|
export function isValidLocale(locale: string): locale is Locale {
|
|
const validLocales: Locale[] = [
|
|
"en",
|
|
"zh-CN",
|
|
"zh-TW",
|
|
"zh-HK",
|
|
"ja",
|
|
"ko",
|
|
"es",
|
|
"fr",
|
|
"de",
|
|
"pt-BR",
|
|
"pt",
|
|
"ru",
|
|
"it",
|
|
"vi",
|
|
"th",
|
|
"id",
|
|
"tr",
|
|
"pl",
|
|
"nl",
|
|
"uk",
|
|
"hi",
|
|
"cs",
|
|
];
|
|
return validLocales.includes(locale as Locale);
|
|
}
|