/** * MetonaToast i18n — 国际化管理 * @module i18n * @version 0.5.0 */ import { LOCALES } from './constants.js'; import type { I18nUtils, LocaleInfo } from './types.js'; // 当前语言状态 let currentLocale = 'zh-CN'; const localeListeners: Set<(locale: string) => void> = new Set(); let fallbackLocale = 'zh-CN'; /** * 获取当前语言 */ export const getCurrentLocale = (): string => { return currentLocale; }; /** * 设置当前语言 */ export const setCurrentLocale = (locale: string): void => { if (!LOCALES[locale]) { console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`); locale = fallbackLocale; } currentLocale = locale; notifyLocaleListeners(locale); saveLocale(locale); }; /** * 获取回退语言 */ export const getFallbackLocale = (): string => { return fallbackLocale; }; /** * 设置回退语言 */ export const setFallbackLocale = (locale: string): void => { if (!LOCALES[locale]) { console.warn(`Fallback locale "${locale}" not found`); return; } fallbackLocale = locale; }; /** * 翻译函数 */ export const t = (key: string, params: Record = {}): string => { const currentTranslation = getTranslation(currentLocale, key); if (currentTranslation !== undefined) { return interpolate(currentTranslation, params); } if (currentLocale !== fallbackLocale) { const fallbackTranslation = getTranslation(fallbackLocale, key); if (fallbackTranslation !== undefined) { return interpolate(fallbackTranslation, params); } } console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`); return key; }; /** * 获取翻译 */ const getTranslation = (locale: string, key: string): string | undefined => { const localeData = LOCALES[locale]; if (!localeData) return undefined; const keys = key.split('.'); let result: unknown = localeData; for (const k of keys) { if (result && typeof result === 'object' && k in (result as Record)) { result = (result as Record)[k]; } else { return undefined; } } return typeof result === 'string' ? result : undefined; }; /** * 插值函数 */ const interpolate = (str: string, params: Record): string => { return str.replace(/\{(\w+)\}/g, (_match, key: string) => { return params[key] !== undefined ? String(params[key]) : _match; }); }; /** * 检查翻译是否存在 */ export const hasTranslation = (key: string): boolean => { return getTranslation(currentLocale, key) !== undefined || getTranslation(fallbackLocale, key) !== undefined; }; /** * 获取所有翻译 */ export const getTranslations = (locale: string): Record => { return LOCALES[locale] || {}; }; /** * 添加翻译 */ export const addTranslations = (locale: string, translations: Record): void => { if (!LOCALES[locale]) { LOCALES[locale] = {}; } deepMerge(LOCALES[locale], translations as Record); }; /** * 深度合并对象 */ const deepMerge = (target: Record, source: Record): Record => { for (const key in source) { if (source[key] instanceof Object && key in target && target[key] instanceof Object) { deepMerge(target[key] as Record, source[key] as Record); } else { target[key] = source[key]; } } return target; }; /** * 移除翻译 */ export const removeTranslation = (locale: string, key: string): void => { const localeData = LOCALES[locale]; if (!localeData) return; const keys = key.split('.'); let current: Record = localeData; for (let i = 0; i < keys.length - 1; i++) { if (current[keys[i]] && typeof current[keys[i]] === 'object') { current = current[keys[i]] as Record; } else { return; } } delete current[keys[keys.length - 1]]; }; /** * 清除翻译 */ export const clearTranslations = (locale: string): void => { if (LOCALES[locale]) { LOCALES[locale] = {}; } }; /** * 获取支持的语言列表 */ export const getSupportedLocales = (): string[] => { return Object.keys(LOCALES); }; /** * 检查语言是否支持 */ export const isLocaleSupported = (locale: string): boolean => { return locale in LOCALES; }; /** * 获取语言名称 */ export const getLocaleName = (locale: string): string => { const names: Record = { 'zh-CN': '简体中文', 'zh-TW': '繁體中文', 'en-US': 'English (US)', 'en-GB': 'English (UK)', 'ja': '日本語', 'ko': '한국어', 'fr': 'Français', 'de': 'Deutsch', 'es': 'Español', 'pt': 'Português', 'ru': 'Русский', 'ar': 'العربية', 'hi': 'हिन्दी', 'th': 'ไทย', 'vi': 'Tiếng Việt', 'id': 'Bahasa Indonesia', 'ms': 'Bahasa Melayu', 'tr': 'Türkçe', 'it': 'Italiano', 'nl': 'Nederlands', 'pl': 'Polski', 'uk': 'Українська', 'cs': 'Čeština', 'sv': 'Svenska', 'da': 'Dansk', 'fi': 'Suomi', 'nb': 'Norsk', 'el': 'Ελληνικά', 'he': 'עברית', 'hu': 'Magyar', 'ro': 'Română', 'bg': 'Български', 'hr': 'Hrvatski', 'sk': 'Slovenčina', 'sl': 'Slovenščina', 'et': 'Eesti', 'lv': 'Latviešu', 'lt': 'Lietuvių', 'ca': 'Català', 'gl': 'Galego', 'eu': 'Euskara', 'cy': 'Cymraeg', 'ga': 'Gaeilge', 'mt': 'Malti', 'is': 'Íslenska', 'mk': 'Македонски', 'sq': 'Shqip', 'sr': 'Српски', 'bs': 'Bosanski', 'me': 'Crnogorski', 'ka': 'ქართული', 'hy': 'Հայերեն', 'az': 'Azərbaycan', 'uz': "O'zbek", 'kk': 'Қазақ', 'ky': 'Кыргыз', 'tg': 'Тоҷикӣ', 'tk': 'Türkmen', 'mn': 'Монгол', 'ne': 'नेपाली', 'si': 'සිංහල', 'my': 'မြန်မာ', 'km': 'ខ្មែរ', 'lo': 'ລາວ', 'am': 'አማርኛ', 'sw': 'Kiswahili', 'yo': 'Yorùbá', 'ig': 'Igbo', 'ha': 'Hausa', 'zu': 'isiZulu', 'af': 'Afrikaans', 'xh': 'isiXhosa', 'st': 'Sesotho', 'tn': 'Setswana', 'ts': 'Xitsonga', 'ss': 'siSwati', 've': 'Tshivenda', 'nr': 'isiNdebele', }; return names[locale] || locale; }; /** * 获取语言方向 */ export const getLocaleDirection = (locale: string): 'ltr' | 'rtl' => { const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug']; return rtlLocales.includes(locale) ? 'rtl' : 'ltr'; }; /** * 获取语言信息 */ export const getLocaleInfo = (locale: string): LocaleInfo => { return { code: locale, name: getLocaleName(locale), direction: getLocaleDirection(locale), isSupported: isLocaleSupported(locale), isCurrent: locale === currentLocale, isFallback: locale === fallbackLocale, }; }; /** * 获取所有语言信息 */ export const getAllLocaleInfo = (): LocaleInfo[] => { return getSupportedLocales().map(getLocaleInfo); }; /** * 保存语言到本地存储 */ export const saveLocale = (locale: string): void => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-locale', locale); } catch (e) { console.warn('Failed to save locale:', e); } } }; /** * 从本地存储加载语言 */ export const loadLocale = (): string => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-locale') || getDefaultLocale(); } catch (e) { console.warn('Failed to load locale:', e); } } return getDefaultLocale(); }; /** * 获取默认语言 */ export const getDefaultLocale = (): string => { if (typeof navigator !== 'undefined') { const browserLocale = navigator.language || (navigator as unknown as { userLanguage?: string }).userLanguage; if (browserLocale && isLocaleSupported(browserLocale)) { return browserLocale; } const shortLocale = browserLocale?.split('-')[0]; if (shortLocale && isLocaleSupported(shortLocale)) { return shortLocale; } } return fallbackLocale; }; /** * 初始化国际化系统 */ export const initI18n = (): void => { const savedLocale = loadLocale(); setCurrentLocale(savedLocale); }; /** * 切换语言 */ export const switchLocale = (locale: string): void => { setCurrentLocale(locale); }; /** * 添加语言监听器 */ export const addLocaleListener = (listener: (locale: string) => void): () => void => { localeListeners.add(listener); return () => { localeListeners.delete(listener); }; }; /** * 移除语言监听器 */ export const removeLocaleListener = (listener: (locale: string) => void): void => { localeListeners.delete(listener); }; /** * 通知语言监听器 */ const notifyLocaleListeners = (locale: string): void => { localeListeners.forEach((listener) => { try { listener(locale); } catch (e) { console.error('Locale listener error:', e); } }); }; /** * 清除所有语言监听器 */ export const clearLocaleListeners = (): void => { localeListeners.clear(); }; /** * 格式化数字 */ export const formatNumber = (number: number, options: Intl.NumberFormatOptions = {}): string => { try { return new Intl.NumberFormat(currentLocale, options).format(number); } catch { return number.toString(); } }; /** * 格式化货币 */ export const formatCurrency = (amount: number, currency = 'USD', options: Intl.NumberFormatOptions = {}): string => { try { return new Intl.NumberFormat(currentLocale, { style: 'currency', currency, ...options, }).format(amount); } catch { return amount.toString(); } }; /** * 格式化百分比 */ export const formatPercent = (value: number, options: Intl.NumberFormatOptions = {}): string => { try { return new Intl.NumberFormat(currentLocale, { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2, ...options, }).format(value / 100); } catch { return `${value}%`; } }; /** * 格式化日期 */ export const formatDate = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => { try { const dateObj = date instanceof Date ? date : new Date(date); return new Intl.DateTimeFormat(currentLocale, options).format(dateObj); } catch { return String(date); } }; /** * 格式化时间 */ export const formatTime = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => { return formatDate(date, { hour: 'numeric', minute: 'numeric', second: 'numeric', ...options, }); }; /** * 格式化相对时间 */ export const formatRelativeTime = (date: Date | number | string, options: Intl.RelativeTimeFormatOptions = {}): string => { try { const dateObj = date instanceof Date ? date : new Date(date); const now = new Date(); const diff = dateObj.getTime() - now.getTime(); const rtf = new Intl.RelativeTimeFormat(currentLocale, { numeric: 'auto', ...options, }); const units: Array<{ unit: Intl.RelativeTimeFormatUnit; ms: number }> = [ { unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 }, { unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 }, { unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 }, { unit: 'day', ms: 24 * 60 * 60 * 1000 }, { unit: 'hour', ms: 60 * 60 * 1000 }, { unit: 'minute', ms: 60 * 1000 }, { unit: 'second', ms: 1000 }, ]; for (const { unit, ms } of units) { if (Math.abs(diff) >= ms || unit === 'second') { const value = Math.round(diff / ms); return rtf.format(value, unit); } } return String(date); } catch { return String(date); } }; /** * 格式化列表 — 优先使用 Intl.ListFormat,降级到逗号拼接 */ export const formatList = (list: string[], _options: Record = {}): string => { const intl = Intl as typeof Intl & { ListFormat?: new (locale: string, options?: Record) => { format: (items: string[]) => string } }; if (typeof intl.ListFormat === 'function') { try { return new intl.ListFormat(currentLocale, { style: 'long', type: 'conjunction', }).format(list); } catch (_e) { /* fallback below */ } } return list.join(', '); }; /** * 格式化复数 */ export const formatPlural = (count: number, options: Intl.PluralRulesOptions = {}): string => { try { return new Intl.PluralRules(currentLocale, options).select(count); } catch { return count === 1 ? 'one' : 'other'; } }; /** * 复数翻译 */ export const plural = (key: string, count: number, params: Record = {}): string => { const pluralForm = formatPlural(count); const pluralKey = `${key}.${pluralForm}`; if (hasTranslation(pluralKey)) { return t(pluralKey, { ...params, count }); } if (hasTranslation(key)) { return t(key, { ...params, count }); } return key; }; /** * 日期时间格式化选项 */ export const dateTimeFormats: Record = { short: { year: 'numeric', month: 'short', day: 'numeric' }, medium: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }, long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: 'numeric', minute: 'numeric', second: 'numeric' }, time: { hour: 'numeric', minute: 'numeric', second: 'numeric' }, date: { year: 'numeric', month: 'long', day: 'numeric' }, weekday: { weekday: 'long' }, month: { month: 'long' }, year: { year: 'numeric' }, }; /** * 数字格式化选项 */ export const numberFormats: Record = { integer: { maximumFractionDigits: 0 }, decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 }, percent: { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2 }, currency: { style: 'currency', currency: 'USD' }, }; /** * 国际化工具 — 代理层 */ export const i18nUtils: I18nUtils = { t, plural, getCurrentLocale, setCurrentLocale, switchLocale, getFallbackLocale, setFallbackLocale, hasTranslation, getTranslations, addTranslations, removeTranslation, clearTranslations, getSupportedLocales, isLocaleSupported, getLocaleName, getLocaleDirection, getLocaleInfo, getAllLocaleInfo, formatNumber, formatCurrency, formatPercent, formatDate, formatTime, formatRelativeTime, formatList, formatPlural, addLocaleListener, removeLocaleListener, clearLocaleListeners, initI18n, saveLocale, loadLocale, getDefaultLocale, }; /** * 预设语言包 */ export const presetLocales: Record; }> = { 'zh-CN': { name: '简体中文', nativeName: '简体中文', direction: 'ltr', translations: LOCALES['zh-CN'], }, 'en-US': { name: 'English (US)', nativeName: 'English (US)', direction: 'ltr', translations: LOCALES['en-US'], }, }; export { i18nUtils as default };