mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 19:32:27 +00:00
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
import { getSafeLocalStorage } from "../../local-storage.ts";
|
|
import { en } from "../locales/en.ts";
|
|
import {
|
|
DEFAULT_LOCALE,
|
|
SUPPORTED_LOCALES,
|
|
isSupportedLocale,
|
|
loadLazyLocaleTranslation,
|
|
resolveNavigatorLocale,
|
|
} from "./registry.ts";
|
|
import type { Locale, TranslationMap } from "./types.ts";
|
|
|
|
type Subscriber = (locale: Locale) => void;
|
|
|
|
export { SUPPORTED_LOCALES, isSupportedLocale };
|
|
|
|
class I18nManager {
|
|
private locale: Locale = DEFAULT_LOCALE;
|
|
private translations: Partial<Record<Locale, TranslationMap>> = { [DEFAULT_LOCALE]: en };
|
|
private subscribers: Set<Subscriber> = new Set();
|
|
|
|
constructor() {
|
|
this.loadLocale();
|
|
}
|
|
|
|
private readStoredLocale(): string | null {
|
|
const storage = getSafeLocalStorage();
|
|
if (!storage) {
|
|
return null;
|
|
}
|
|
try {
|
|
return storage.getItem("openclaw.i18n.locale");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private persistLocale(locale: Locale) {
|
|
const storage = getSafeLocalStorage();
|
|
if (!storage) {
|
|
return;
|
|
}
|
|
try {
|
|
storage.setItem("openclaw.i18n.locale", locale);
|
|
} catch {
|
|
// Ignore storage write failures in private/blocked contexts.
|
|
}
|
|
}
|
|
|
|
private resolveInitialLocale(): Locale {
|
|
const saved = this.readStoredLocale();
|
|
if (isSupportedLocale(saved)) {
|
|
return saved;
|
|
}
|
|
const language =
|
|
typeof globalThis.navigator?.language === "string" ? globalThis.navigator.language : null;
|
|
return resolveNavigatorLocale(language ?? "");
|
|
}
|
|
|
|
private loadLocale() {
|
|
const initialLocale = this.resolveInitialLocale();
|
|
if (initialLocale === DEFAULT_LOCALE) {
|
|
this.locale = DEFAULT_LOCALE;
|
|
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 = locale !== DEFAULT_LOCALE && !this.translations[locale];
|
|
if (this.locale === locale && !needsTranslationLoad) {
|
|
return;
|
|
}
|
|
|
|
if (needsTranslationLoad) {
|
|
try {
|
|
const translation = await loadLazyLocaleTranslation(locale);
|
|
if (!translation) {
|
|
return;
|
|
}
|
|
this.translations[locale] = translation;
|
|
} catch (e) {
|
|
console.error(`Failed to load locale: ${locale}`, e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.locale = locale;
|
|
this.persistLocale(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[DEFAULT_LOCALE];
|
|
|
|
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 !== DEFAULT_LOCALE) {
|
|
value = this.translations[DEFAULT_LOCALE];
|
|
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);
|