feat(i18n): detect initial locale from user language

This commit is contained in:
t8y2 2026-06-01 16:38:44 +08:00
parent a87b8ece31
commit fb7d9dc847
2 changed files with 63 additions and 3 deletions

View File

@ -17,18 +17,49 @@ const localeLoaders: Record<Exclude<Locale, "zh-CN">, () => Promise<{ default: L
es: () => import("./locales/es"),
};
function normalizeLocale(value: string | null): Locale {
export function normalizeLocale(value: string | null): Locale | null {
if (value && supportedLocales.includes(value as Locale)) {
return value as Locale;
}
return null;
}
export function localeFromLanguageTag(value: string | null | undefined): Locale | null {
if (!value) return null;
const normalized = value.replace("_", "-").toLowerCase();
if (normalized === "zh-cn" || normalized.startsWith("zh-")) return "zh-CN";
if (normalized === "zh") return "zh-CN";
if (normalized === "en" || normalized.startsWith("en-")) return "en";
if (normalized === "es" || normalized.startsWith("es-")) return "es";
return null;
}
export function detectLocaleFromLanguages(languages: readonly string[]): Locale {
for (const language of languages) {
const locale = normalizeLocale(language) ?? localeFromLanguageTag(language);
if (locale) return locale;
}
return defaultLocale;
}
function detectUserLocale(): Locale {
try {
const languages = globalThis.navigator?.languages;
const language = globalThis.navigator?.language;
const candidates = Array.isArray(languages) ? [...languages] : [];
if (language) candidates.push(language);
return detectLocaleFromLanguages(candidates);
} catch {
return defaultLocale;
}
}
const savedLocale = normalizeLocale(safeLocalStorageGet("dbx-locale"));
const initialLocale = savedLocale ?? detectUserLocale();
const i18n = createI18n({
legacy: false,
locale: savedLocale,
locale: initialLocale,
fallbackLocale: defaultLocale,
messages: {
"zh-CN": zhCN,
@ -46,7 +77,7 @@ export async function loadLocaleMessages(locale: Locale) {
}
export async function loadSavedLocale() {
await loadLocaleMessages(savedLocale);
await loadLocaleMessages(initialLocale);
}
export async function setLocale(locale: Locale) {

View File

@ -0,0 +1,29 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import {
detectLocaleFromLanguages,
localeFromLanguageTag,
normalizeLocale,
} from "../../apps/desktop/src/i18n/index.ts";
test("normalizes exact supported locales", () => {
assert.equal(normalizeLocale("en"), "en");
assert.equal(normalizeLocale("es"), "es");
assert.equal(normalizeLocale("zh-CN"), "zh-CN");
assert.equal(normalizeLocale("fr-FR"), null);
assert.equal(normalizeLocale(null), null);
});
test("maps user language tags to supported locales", () => {
assert.equal(localeFromLanguageTag("zh-Hans-CN"), "zh-CN");
assert.equal(localeFromLanguageTag("zh_TW"), "zh-CN");
assert.equal(localeFromLanguageTag("en-US"), "en");
assert.equal(localeFromLanguageTag("es-MX"), "es");
assert.equal(localeFromLanguageTag("fr-FR"), null);
});
test("detects the first supported user locale and falls back to Chinese", () => {
assert.equal(detectLocaleFromLanguages(["fr-FR", "es-MX", "en-US"]), "es");
assert.equal(detectLocaleFromLanguages(["de-DE", "ja-JP"]), "zh-CN");
assert.equal(detectLocaleFromLanguages([]), "zh-CN");
});