Translate the WebUI common.json to pt-BR, register the locale in config.ts (with normalizeLocale rules for "pt", "pt-BR", "pt-PT", ...) and index.ts resources, and add a Brazilian Portuguese overview assertion in i18n.test.tsx. Notes: - Generic "pt" and any "pt-*" variant resolve to pt-BR. A future pt-PT locale can be added without affecting existing users. - Universally borrowed technical terms (nanobot, BYOK, MCP, JSON, URL, Web, Apps when used as a product name, Skills) are kept in English, matching the pattern used by es/fr/ja/ko/vi/id. - Empty-thread greetings, quick-action prompts, and slash command descriptions are translated; quick-action prompts are intentionally phrased so the agent still receives a clear task spec. Verified with bun run test (536 passing), bun run lint (clean), and bun run build (7.18s).
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
export const LOCALE_STORAGE_KEY = "nanobot.locale";
|
|
|
|
export const supportedLocales = [
|
|
{ code: "en", label: "English", nativeLabel: "English" },
|
|
{ code: "zh-CN", label: "Chinese (Simplified)", nativeLabel: "简体中文" },
|
|
{ code: "zh-TW", label: "Chinese (Traditional)", nativeLabel: "繁體中文" },
|
|
{ code: "fr", label: "French", nativeLabel: "Français" },
|
|
{ code: "ja", label: "Japanese", nativeLabel: "日本語" },
|
|
{ code: "ko", label: "Korean", nativeLabel: "한국어" },
|
|
{ code: "es", label: "Spanish", nativeLabel: "Español" },
|
|
{ code: "pt-BR", label: "Portuguese (Brazil)", nativeLabel: "Português (Brasil)" },
|
|
{ code: "vi", label: "Vietnamese", nativeLabel: "Tiếng Việt" },
|
|
{ code: "id", label: "Indonesian", nativeLabel: "Bahasa Indonesia" },
|
|
] as const;
|
|
|
|
export type SupportedLocale = (typeof supportedLocales)[number]["code"];
|
|
|
|
export const defaultLocale: SupportedLocale = "en";
|
|
export const fallbackLocale: SupportedLocale = "en";
|
|
|
|
export function normalizeLocale(
|
|
input: string | null | undefined,
|
|
): SupportedLocale {
|
|
if (!input) return defaultLocale;
|
|
const trimmed = input.trim();
|
|
if (!trimmed) return defaultLocale;
|
|
|
|
const exact = supportedLocales.find((locale) => locale.code === trimmed);
|
|
if (exact) return exact.code;
|
|
|
|
const lower = trimmed.toLowerCase();
|
|
if (lower === "zh" || lower.startsWith("zh-cn") || lower.startsWith("zh-sg")) {
|
|
return "zh-CN";
|
|
}
|
|
if (
|
|
lower.startsWith("zh-tw") ||
|
|
lower.startsWith("zh-hk") ||
|
|
lower.startsWith("zh-mo") ||
|
|
lower.startsWith("zh-hant")
|
|
) {
|
|
return "zh-TW";
|
|
}
|
|
if (lower === "pt" || lower.startsWith("pt-")) {
|
|
return "pt-BR";
|
|
}
|
|
|
|
const base = lower.split("-")[0];
|
|
const baseMatch = supportedLocales.find(
|
|
(locale) => locale.code.toLowerCase() === base,
|
|
);
|
|
return baseMatch?.code ?? defaultLocale;
|
|
}
|
|
|
|
export function readStoredLocale(): SupportedLocale | null {
|
|
if (typeof window === "undefined") return null;
|
|
try {
|
|
const raw = window.localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
return raw ? normalizeLocale(raw) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function resolveInitialLocale(): SupportedLocale {
|
|
return readStoredLocale() ?? defaultLocale;
|
|
}
|
|
|
|
export function persistLocale(locale: SupportedLocale): void {
|
|
if (typeof window === "undefined") return;
|
|
try {
|
|
window.localStorage.setItem(LOCALE_STORAGE_KEY, locale);
|
|
} catch {
|
|
// ignore storage errors
|
|
}
|
|
}
|
|
|
|
export function applyDocumentLocale(locale: SupportedLocale): void {
|
|
if (typeof document === "undefined") return;
|
|
document.documentElement.lang = locale;
|
|
}
|
|
|
|
export function localeOption(locale: SupportedLocale) {
|
|
return supportedLocales.find((entry) => entry.code === locale) ?? supportedLocales[0];
|
|
}
|