107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||
import * as Localization from 'expo-localization';
|
||
import i18n from 'i18next';
|
||
import { initReactI18next } from 'react-i18next';
|
||
|
||
// 用 require 避免 TS 的 json module 配置差异导致无法编译
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||
const all = require('./locales/all.json') as { en: Record<string, unknown>; 'zh-TW': Record<string, unknown> };
|
||
|
||
/**
|
||
* 语言码约定:
|
||
* - 繁体中文:zh-TW
|
||
* - 英语:en
|
||
*/
|
||
export type AppLanguage = 'zh-TW' | 'en';
|
||
|
||
export const SUPPORTED_LANGUAGES: readonly AppLanguage[] = [
|
||
'zh-TW',
|
||
'en',
|
||
] as const;
|
||
|
||
const DEFAULT_FALLBACK_LANGUAGE: AppLanguage = 'en';
|
||
const STORAGE_KEY_LANGUAGE = 'settings.language';
|
||
|
||
function isSupportedLanguage(lang: string): lang is AppLanguage {
|
||
return (SUPPORTED_LANGUAGES as readonly string[]).includes(lang);
|
||
}
|
||
|
||
function normalizeDeviceLanguageTagToAppLanguage(languageTag: string): AppLanguage {
|
||
const tag = languageTag.toLowerCase();
|
||
|
||
// 中文:当前仅支持繁体中文(zh-TW)
|
||
if (tag.startsWith('zh')) {
|
||
return 'zh-TW';
|
||
}
|
||
|
||
// 其他语言:按前缀匹配(当前仅支持英文)
|
||
if (tag.startsWith('en')) return 'en';
|
||
|
||
return DEFAULT_FALLBACK_LANGUAGE;
|
||
}
|
||
|
||
function getDeviceLanguage(): AppLanguage {
|
||
// expo-localization 返回系统 locale 列表,取第一个作为当前设备偏好
|
||
const locales = Localization.getLocales();
|
||
const first = locales?.[0]?.languageTag;
|
||
|
||
if (first) {
|
||
return normalizeDeviceLanguageTagToAppLanguage(first);
|
||
}
|
||
|
||
// 兜底:少数情况下 locales 为空
|
||
return DEFAULT_FALLBACK_LANGUAGE;
|
||
}
|
||
|
||
export async function getLanguagePreference(): Promise<AppLanguage | null> {
|
||
const value = await AsyncStorage.getItem(STORAGE_KEY_LANGUAGE);
|
||
if (!value) return null;
|
||
return isSupportedLanguage(value) ? value : null;
|
||
}
|
||
|
||
export async function setLanguagePreference(lang: AppLanguage): Promise<void> {
|
||
await AsyncStorage.setItem(STORAGE_KEY_LANGUAGE, lang);
|
||
}
|
||
|
||
export async function clearLanguagePreference(): Promise<void> {
|
||
await AsyncStorage.removeItem(STORAGE_KEY_LANGUAGE);
|
||
}
|
||
|
||
/**
|
||
* 初始化 i18n(只会初始化一次)
|
||
*
|
||
* 语言选择优先级:
|
||
* 1) 用户设置(若存在)
|
||
* 2) 设备语言(在支持列表内时生效;否则会被 normalize 到默认回退)
|
||
* 3) 默认回退(en)
|
||
*/
|
||
export async function initI18n(): Promise<void> {
|
||
if (i18n.isInitialized) return;
|
||
|
||
const userLang = await getLanguagePreference();
|
||
const deviceLang = getDeviceLanguage();
|
||
const initialLang = userLang ?? deviceLang;
|
||
|
||
await i18n.use(initReactI18next).init({
|
||
resources: {
|
||
'zh-TW': { translation: all['zh-TW'] as any },
|
||
en: { translation: all.en as any },
|
||
},
|
||
lng: initialLang,
|
||
fallbackLng: DEFAULT_FALLBACK_LANGUAGE,
|
||
interpolation: {
|
||
// React Native 不需要对内容做 HTML 转义
|
||
escapeValue: false,
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 用户手动切换语言:立即生效 + 持久化
|
||
*/
|
||
export async function changeLanguage(lang: AppLanguage): Promise<void> {
|
||
await setLanguagePreference(lang);
|
||
await i18n.changeLanguage(lang);
|
||
}
|
||
|