- Add expo-localization, i18next, react-i18next (versions aligned with Expo 54 / RN 0.81) - src/lib/i18n/locale-resolve.ts: pure locale-resolution helpers (system tag -> supported catalog, with en fallback), unit-tested via node --test with no RN imports - src/lib/i18n/config.ts: i18next init wired to expo-localization device detection, en.json + zh-Hans.json catalogs - Persist a locale preference (system | en | zh-Hans) in the settings zustand store, applied immediately via i18next.changeLanguage - Wire I18nextProvider in app/_layout.tsx - Localize the Settings screen (~28 strings) as the reference pattern for extracting user-facing strings, with a language picker row and reviewed Simplified Chinese translations Other screens (session/[id], connection/*, index, chat components) are deferred follow-up — issue #68 stays open for that work. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
44 lines
1.7 KiB
TypeScript
44 lines
1.7 KiB
TypeScript
// Pure locale-resolution logic, kept free of expo-localization/i18next imports
|
|
// so it's unit-testable with plain `node --test` (same split as
|
|
// settings-merge.ts / store-review-policy.ts).
|
|
|
|
export const SUPPORTED_LOCALES = ["en", "zh-Hans"] as const
|
|
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number]
|
|
|
|
export const FALLBACK_LOCALE: SupportedLocale = "en"
|
|
|
|
// User-facing preference: "system" defers to the device's locale list.
|
|
export type LocalePreference = "system" | SupportedLocale
|
|
|
|
/**
|
|
* Map one device locale tag (e.g. "zh-Hans-CN", "zh-CN", "en-US", "fr-FR") to
|
|
* one of our supported catalogs, matching by language subtag since we don't
|
|
* ship per-region variants. Returns null if we have no catalog for it.
|
|
*/
|
|
export function matchSupportedLocale(tag: string): SupportedLocale | null {
|
|
const lower = tag.toLowerCase()
|
|
if (lower.startsWith("zh")) return "zh-Hans"
|
|
if (lower.startsWith("en")) return "en"
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Resolve the effective app locale from a user preference plus the device's
|
|
* ranked list of locale tags (as returned by expo-localization's
|
|
* `getLocales().map(l => l.languageTag)`).
|
|
* - An explicit preference (anything but "system") always wins.
|
|
* - "system" walks the device list in priority order and uses the first tag
|
|
* we have a catalog for.
|
|
* - If nothing matches (or the list is empty), falls back to FALLBACK_LOCALE.
|
|
*/
|
|
export function resolveLocale(preference: LocalePreference, deviceTags: readonly string[]): SupportedLocale {
|
|
if (preference !== "system") return preference
|
|
|
|
for (const tag of deviceTags) {
|
|
const match = matchSupportedLocale(tag)
|
|
if (match) return match
|
|
}
|
|
|
|
return FALLBACK_LOCALE
|
|
}
|