openclaw/ui/src/i18n/lib/translate.ts
Daniel Reis 84a88b2ace
feat(i18n): add German (de) locale (#28495)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: e418326aaffe4672c2b5423083cbc068a9b1cb6d
Co-authored-by: dsantoreis <220753637+dsantoreis@users.noreply.github.com>
Co-authored-by: Evizero <10854026+Evizero@users.noreply.github.com>
Reviewed-by: @Evizero
2026-02-27 11:44:09 +01:00

140 lines
3.9 KiB
TypeScript

import { en } from "../locales/en.ts";
import type { Locale, TranslationMap } from "./types.ts";
type Subscriber = (locale: Locale) => void;
export const SUPPORTED_LOCALES: ReadonlyArray<Locale> = ["en", "zh-CN", "zh-TW", "pt-BR", "de"];
export function isSupportedLocale(value: string | null | undefined): value is Locale {
return value !== null && value !== undefined && SUPPORTED_LOCALES.includes(value as Locale);
}
class I18nManager {
private locale: Locale = "en";
private translations: Record<Locale, TranslationMap> = { en } as Record<Locale, TranslationMap>;
private subscribers: Set<Subscriber> = new Set();
constructor() {
this.loadLocale();
}
private resolveInitialLocale(): Locale {
const saved = localStorage.getItem("openclaw.i18n.locale");
if (isSupportedLocale(saved)) {
return saved;
}
const navLang = navigator.language;
if (navLang.startsWith("zh")) {
return navLang === "zh-TW" || navLang === "zh-HK" ? "zh-TW" : "zh-CN";
}
if (navLang.startsWith("pt")) {
return "pt-BR";
}
if (navLang.startsWith("de")) {
return "de";
}
return "en";
}
private loadLocale() {
const initialLocale = this.resolveInitialLocale();
if (initialLocale === "en") {
this.locale = "en";
return;
}
// Use the normal locale setter so startup locale loading follows the same
// translation-loading + notify path as manual locale changes.
void this.setLocale(initialLocale);
}
public getLocale(): Locale {
return this.locale;
}
public async setLocale(locale: Locale) {
const needsTranslationLoad = !this.translations[locale];
if (this.locale === locale && !needsTranslationLoad) {
return;
}
// Lazy load translations if needed
if (needsTranslationLoad) {
try {
let module: Record<string, TranslationMap>;
if (locale === "zh-CN") {
module = await import("../locales/zh-CN.ts");
} else if (locale === "zh-TW") {
module = await import("../locales/zh-TW.ts");
} else if (locale === "pt-BR") {
module = await import("../locales/pt-BR.ts");
} else if (locale === "de") {
module = await import("../locales/de.ts");
} else {
return;
}
this.translations[locale] = module[locale.replace("-", "_")];
} catch (e) {
console.error(`Failed to load locale: ${locale}`, e);
return;
}
}
this.locale = locale;
localStorage.setItem("openclaw.i18n.locale", locale);
this.notify();
}
public registerTranslation(locale: Locale, map: TranslationMap) {
this.translations[locale] = map;
}
public subscribe(sub: Subscriber) {
this.subscribers.add(sub);
return () => this.subscribers.delete(sub);
}
private notify() {
this.subscribers.forEach((sub) => sub(this.locale));
}
public t(key: string, params?: Record<string, string>): string {
const keys = key.split(".");
let value: unknown = this.translations[this.locale] || this.translations["en"];
for (const k of keys) {
if (value && typeof value === "object") {
value = (value as Record<string, unknown>)[k];
} else {
value = undefined;
break;
}
}
// Fallback to English
if (value === undefined && this.locale !== "en") {
value = this.translations["en"];
for (const k of keys) {
if (value && typeof value === "object") {
value = (value as Record<string, unknown>)[k];
} else {
value = undefined;
break;
}
}
}
if (typeof value !== "string") {
return key;
}
if (params) {
return value.replace(/\{(\w+)\}/g, (_, k) => params[k] || `{${k}}`);
}
return value;
}
}
export const i18n = new I18nManager();
export const t = (key: string, params?: Record<string, string>) => i18n.t(key, params);