chore: vendor client into main repo
This commit is contained in:
123
client/src/i18n/index.ts
Normal file
123
client/src/i18n/index.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as Localization from 'expo-localization';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import en from './locales/en.json';
|
||||
import es from './locales/es.json';
|
||||
import pt from './locales/pt.json';
|
||||
import zhCN from './locales/zh-CN.json';
|
||||
import zhTW from './locales/zh-TW.json';
|
||||
|
||||
/**
|
||||
* 语言码约定:
|
||||
* - 简体中文:zh-CN
|
||||
* - 繁体中文:zh-TW
|
||||
* - 英语:en
|
||||
* - 西班牙语:es
|
||||
* - 葡萄牙语:pt
|
||||
*/
|
||||
export type AppLanguage = 'zh-CN' | 'zh-TW' | 'en' | 'es' | 'pt';
|
||||
|
||||
export const SUPPORTED_LANGUAGES: readonly AppLanguage[] = [
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
'en',
|
||||
'es',
|
||||
'pt',
|
||||
] as const;
|
||||
|
||||
const DEFAULT_FALLBACK_LANGUAGE: AppLanguage = 'zh-CN';
|
||||
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();
|
||||
|
||||
// 中文:优先区分繁简
|
||||
if (tag.startsWith('zh')) {
|
||||
// 常见繁体标记:zh-TW / zh-HK / zh-Hant
|
||||
if (tag.includes('tw') || tag.includes('hk') || tag.includes('hant')) {
|
||||
return 'zh-TW';
|
||||
}
|
||||
return 'zh-CN';
|
||||
}
|
||||
|
||||
// 其他语言:按前缀匹配
|
||||
if (tag.startsWith('en')) return 'en';
|
||||
if (tag.startsWith('es')) return 'es';
|
||||
if (tag.startsWith('pt')) return 'pt';
|
||||
|
||||
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) 默认回退(zh-CN)
|
||||
*/
|
||||
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-CN': { translation: zhCN },
|
||||
'zh-TW': { translation: zhTW },
|
||||
en: { translation: en },
|
||||
es: { translation: es },
|
||||
pt: { translation: pt },
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user