diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index 0901fbf..236ece7 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -10,6 +10,11 @@ import { type Language, languages, resources } from './resources'; * cycle `utils.LOCAL` is still undefined, so `getString(undefined)` throws and * every route that imports `translate` fails to evaluate (expo-router then * reports "missing default export" for all routes). Read storage directly here. + * + * This initialLanguage() is the single place that maps a device locale to an + * app language — utils.getDefaultLanguage() just reads the resulting + * i18n.language, so the rendered language and the highlighted pill cannot + * disagree. */ function isLanguage(value: string | null | undefined): value is Language { return !!value && (languages as readonly string[]).includes(value); @@ -18,8 +23,19 @@ function isLanguage(value: string | null | undefined): value is Language { function initialLanguage(): Language { const stored = storage.getString('language'); if (isLanguage(stored)) return stored; - const code = getLocales()[0]?.languageCode; - return isLanguage(code) ? code : 'en'; + + const loc = getLocales()[0]; + if (!loc) return 'en'; + // Chinese needs script/region disambiguation — languageCode is 'zh' for + // EVERY Chinese locale, which used to load Traditional on zh-Hans devices. + // Hans script or CN/SG/MY region → Simplified, otherwise Traditional. + const tag = (loc.languageTag ?? '').toLowerCase(); + if (tag.startsWith('zh')) { + return tag.includes('hans') || tag.includes('cn') || tag.includes('sg') || tag.includes('my') + ? 'zh-CN' + : 'zh'; + } + return isLanguage(loc.languageCode) ? loc.languageCode : 'en'; } void i18n.use(initReactI18next).init({ diff --git a/src/lib/i18n/utils.ts b/src/lib/i18n/utils.ts index 6b3b2d4..5d1de25 100644 --- a/src/lib/i18n/utils.ts +++ b/src/lib/i18n/utils.ts @@ -1,5 +1,4 @@ import { storage } from '@/lib/storage'; -import { getLocales } from 'expo-localization'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useMMKVString } from 'react-native-mmkv'; @@ -13,19 +12,14 @@ export function isLanguage(value: string | null | undefined): value is Language return !!value && (languages as readonly string[]).includes(value); } +/** + * The default language — what applies when the user hasn't picked one. + * Reads the i18n instance instead of re-deriving from the device locale, so + * the selector highlights exactly what is rendered (the locale mapping lives + * only in ./index's initialLanguage). + */ export function getDefaultLanguage(): Language { - const loc = getLocales()[0]; - if (!loc) return 'en'; - // Chinese needs script/region disambiguation (languageCode is just 'zh'): - // Hans script or CN/SG/MY region → Simplified, otherwise Traditional. - const tag = (loc.languageTag ?? '').toLowerCase(); - if (tag.startsWith('zh')) { - return tag.includes('hans') || tag.includes('cn') || tag.includes('sg') || tag.includes('my') - ? 'zh-CN' - : 'zh'; - } - const code = loc.languageCode; - return isLanguage(code) ? code : 'en'; + return isLanguage(i18n.language) ? i18n.language : 'en'; } /** Read the selected language synchronously (safe at i18n init time). */