chore: vendor client into main repo

This commit is contained in:
吕新雨
2026-01-28 22:54:21 +08:00
parent 7d743e78ea
commit 6a598f0a98
79 changed files with 12952 additions and 1 deletions

123
client/src/i18n/index.ts Normal file
View 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);
}