Files
mindfulness/client/src/i18n/index.ts

107 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}