Files
nanobot/webui/src/i18n/config.ts
T
bill-kopp-ai-devandchengyongru 1e7518a207 feat(webui): add Brazilian Portuguese (pt-BR) locale
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).
2026-07-13 23:28:19 +08:00

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