58 lines
3.6 KiB
JavaScript
58 lines
3.6 KiB
JavaScript
/* Translation lookup and locale-aware formatting. Copy lives in /locales/*.json. */
|
|
(() => {
|
|
const dictionaries = window.LifeOSLocales || {};
|
|
const supported = ['en', 'zh-CN', 'zh-HK', 'zh-MO', 'zh-TW', 'ar', 'fr', 'ru', 'es', 'ja'];
|
|
const names = { en: 'English', 'zh-CN': '简体中文', 'zh-HK': '繁體中文(香港)', 'zh-MO': '繁體中文(澳門)', 'zh-TW': '繁體中文(臺灣)', ar: 'العربية', fr: 'Français', ru: 'Русский', es: 'Español', ja: '日本語' };
|
|
const PREFERENCE_KEY = 'lifeos_language_preference_v1';
|
|
const normalize = value => {
|
|
const code = String(value || '').replace(/_/g, '-').toLowerCase();
|
|
if (code === 'zh' || code.startsWith('zh-hans') || code.startsWith('zh-cn') || code.startsWith('zh-sg')) return 'zh-CN';
|
|
if (code.startsWith('zh-hk') || code.startsWith('zh-hant-hk')) return 'zh-HK';
|
|
if (code.startsWith('zh-mo') || code.startsWith('zh-hant-mo')) return 'zh-MO';
|
|
if (code.startsWith('zh-tw') || code.startsWith('zh-hant')) return 'zh-TW';
|
|
return supported.find(item => item === code || code.startsWith(item + '-')) || null;
|
|
};
|
|
const validPreference = value => value === 'auto' || Boolean(normalize(value));
|
|
const lookup = (object, path) => path.split('.').reduce((value, part) => value?.[part], object);
|
|
const detectBrowserLanguage = () => {
|
|
const preferences = navigator.languages?.length ? navigator.languages : [navigator.language || navigator.userLanguage || 'en'];
|
|
return preferences.map(normalize).find(Boolean) || 'en';
|
|
};
|
|
const readPreference = () => {
|
|
try {
|
|
const saved = localStorage.getItem(PREFERENCE_KEY);
|
|
if (validPreference(saved)) return saved === 'auto' ? 'auto' : normalize(saved);
|
|
let oldSettings = {};
|
|
try { oldSettings = JSON.parse(localStorage.getItem('lifeos_settings') || '{}'); } catch {}
|
|
const legacyMode = oldSettings.languageMode ?? (['en', 'zh'].includes(oldSettings.language) ? 'manual' : 'auto');
|
|
const migrated = legacyMode === 'manual' && normalize(oldSettings.language) ? normalize(oldSettings.language) : 'auto';
|
|
localStorage.setItem(PREFERENCE_KEY, migrated);
|
|
return migrated;
|
|
} catch { return 'auto'; }
|
|
};
|
|
let preference = readPreference();
|
|
const language = () => preference === 'auto' ? detectBrowserLanguage() : preference;
|
|
function t(key, values = {}) {
|
|
const template = lookup(dictionaries[language()], key) ?? lookup(dictionaries.en, key) ?? key;
|
|
return String(template).replace(/\{(\w+)\}/g, (_, name) => values[name] ?? `{${name}}`);
|
|
}
|
|
const locale = () => language() === 'en' ? 'en-US' : language();
|
|
function date(value, options) {
|
|
const parsed = value instanceof Date ? value : new Date(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T12:00:00` : value);
|
|
return new Intl.DateTimeFormat(locale(), options).format(parsed);
|
|
}
|
|
const number = (value, options) => new Intl.NumberFormat(locale(), options).format(value);
|
|
function setLanguage(value) {
|
|
if (!validPreference(value)) return;
|
|
preference = value === 'auto' ? 'auto' : normalize(value);
|
|
try { localStorage.setItem(PREFERENCE_KEY, preference); } catch {}
|
|
window.renderIcons?.();
|
|
window.renderAll?.();
|
|
window.applyLanguage?.();
|
|
window.toast?.(preference === 'auto' ? t('feedback.followingBrowser', { name: names[language()] }) : t('feedback.languageSelected', { name: names[language()] }));
|
|
}
|
|
window.lang = language;
|
|
window.setLanguage = setLanguage;
|
|
window.LifeI18n = { t, date, number, locale, language, preference: () => preference, browserLanguage: detectBrowserLanguage, names, supported, dictionaries: { ...dictionaries, zh: dictionaries['zh-CN'] } };
|
|
})();
|