Files
infiplot-web/lib/i18n/utils.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

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);
}