From fb7d9dc8474d991508cd2e451437c33cef0cc585 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 1 Jun 2026 16:38:44 +0800 Subject: [PATCH] feat(i18n): detect initial locale from user language --- apps/desktop/src/i18n/index.ts | 37 ++++++++++++++++++++++++--- packages/app-tests/i18nLocale.test.ts | 29 +++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 packages/app-tests/i18nLocale.test.ts diff --git a/apps/desktop/src/i18n/index.ts b/apps/desktop/src/i18n/index.ts index 611c2c433..fd199a8e3 100644 --- a/apps/desktop/src/i18n/index.ts +++ b/apps/desktop/src/i18n/index.ts @@ -17,18 +17,49 @@ const localeLoaders: Record, () => 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) { diff --git a/packages/app-tests/i18nLocale.test.ts b/packages/app-tests/i18nLocale.test.ts new file mode 100644 index 000000000..dc48cd6e0 --- /dev/null +++ b/packages/app-tests/i18nLocale.test.ts @@ -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"); +});