diff --git a/package.json b/package.json index 4efc034..732d0b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "life-os", - "version": "1.5.3", + "version": "1.5.4", "private": true, "type": "module", "scripts": { diff --git a/public/addons.js b/public/addons.js index 5bc3ab5..d309e97 100644 --- a/public/addons.js +++ b/public/addons.js @@ -3,7 +3,7 @@ const DRAFT_KEY = 'lifeos_today_draft_v1'; const localDay = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }; const escapeHtml = value => String(value || '').replace(/[&<>]/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); - const APP_VERSION = '1.5.3'; + const APP_VERSION = '1.5.4'; window.LifeOSVersion = APP_VERSION; const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system']; const t = (key, values) => LifeI18n.t(key, values); diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js index ddbc66e..1ceca53 100644 --- a/public/i18n-renderers.js +++ b/public/i18n-renderers.js @@ -22,11 +22,6 @@ window.closeModal = () => { rootModal().innerHTML = ''; }; window.applyLanguage = () => { document.documentElement.lang = lang() === 'zh' ? 'zh-CN' : 'en'; document.title = t('shell.pageTitle'); const brand = document.querySelector('#desktop-brand'), launcher = document.querySelector('#life-launcher'), boot = document.querySelector('#boot span'); if (brand) brand.textContent = t('shell.brand'); if (launcher) launcher.textContent = t('shell.life'); if (boot) boot.textContent = t('shell.initializing'); if (settings().wallpaper) applyWallpaper(settings().wallpaper); }; window.applyWallpaper = wallpaper => { if (!wallpaper?.url) return; const desktop = document.querySelector('#desktop'); desktop.style.backgroundImage = `linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${wallpaper.url}')`; desktop.style.backgroundSize = 'cover'; desktop.style.backgroundPosition = 'center'; desktop.title = t('shell.wallpaperTitle', { title: wallpaper.title || t('wallpaper.dailyImage') }); }; - window.setLanguage = value => { - const state = settings(); if (!['auto','en','zh'].includes(value)) return; state.language = value; state.languageMode = value === 'auto' ? 'auto' : 'manual'; save('lifeos_settings', state); - window.renderIcons(); window.renderAll(); window.applyLanguage(); - window.toast(value === 'auto' ? t(lang() === 'zh' ? 'feedback.followingZh' : 'feedback.followingEn') : t(value === 'zh' ? 'feedback.languageZh' : 'feedback.languageEn')); - }; window.today = container => { const entryDate = todayTargetDate || day(), memory = mems().find(item => item.date === entryDate); if (todayDraft === null) { selectedMood = memory?.mood || 'cloudy'; selectedEnergy = memory?.energy || 3; } @@ -77,8 +72,8 @@ }; window.saveProfile = () => { save('lifeos_profile', { name: pname.value, birthday: pbday.value, startDate: pstart.value || day(), motto: pmotto.value }); renderAll(); toast(t('profile.saved')); }; window.systemApp = container => { - const state = settings(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = browserLanguage() === 'zh' ? t('system.detectedZh') : t('system.detectedEn'); - container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${t('system.data')}
${t('system.storage')}

${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}

${t('system.dataStorage')}

${t('system.stored')}
${t('system.backups')}

${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}

${t('system.version')}

${t('system.release',{version:window.LifeOSVersion||'1.5.3'})}

`; + const state = settings(), preference = LifeI18n.preference(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = browserLanguage() === 'zh' ? t('system.detectedZh') : t('system.detectedEn'); + container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${t('system.data')}
${t('system.storage')}

${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}

${t('system.dataStorage')}

${t('system.stored')}
${t('system.backups')}

${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}

${t('system.version')}

${t('system.release',{version:window.LifeOSVersion||'1.5.4'})}

`; }; window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = ``; }; window.replaceConfirm = () => { rootModal().innerHTML = ``; }; diff --git a/public/i18n.js b/public/i18n.js index bdc1a71..cc852a0 100644 --- a/public/i18n.js +++ b/public/i18n.js @@ -3,20 +3,51 @@ const dictionaries = window.LifeOSLocales || {}; const en = dictionaries.en || {}; const zh = dictionaries['zh-CN'] || {}; + const PREFERENCE_KEY = 'lifeos_language_preference_v1'; + const validPreference = value => ['auto', 'en', 'zh'].includes(value); const lookup = (object, path) => path.split('.').reduce((value, part) => value?.[part], object); - const current = () => typeof window.lang === 'function' && window.lang() === 'zh' ? 'zh' : 'en'; + const detectBrowserLanguage = () => { + const preferences = navigator.languages?.length ? navigator.languages : [navigator.language || navigator.userLanguage || 'en']; + return preferences.some(value => String(value).toLowerCase().startsWith('zh')) ? 'zh' : 'en'; + }; + const readPreference = () => { + try { + const saved = localStorage.getItem(PREFERENCE_KEY); + if (validPreference(saved)) return 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' && ['en', 'zh'].includes(oldSettings.language) ? 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 selected = current() === 'zh' ? zh : en; + const selected = language() === 'zh' ? zh : en; const template = lookup(selected, key) ?? lookup(en, key) ?? key; return String(template).replace(/\{(\w+)\}/g, (_, name) => values[name] ?? `{${name}}`); } - function locale() { return current() === 'zh' ? 'zh-CN' : 'en-US'; } + function locale() { return language() === 'zh' ? 'zh-CN' : 'en-US'; } 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); } function number(value, options) { return new Intl.NumberFormat(locale(), options).format(value); } - window.LifeI18n = { t, date, number, locale, dictionaries: { en, zh } }; + function setLanguage(value) { + if (!validPreference(value)) return; + preference = value; + try { localStorage.setItem(PREFERENCE_KEY, value); } catch {} + window.renderIcons?.(); + window.renderAll?.(); + window.applyLanguage?.(); + window.toast?.(value === 'auto' ? (language() === 'zh' ? 'feedback.followingZh' : 'feedback.followingEn') : (value === 'zh' ? 'feedback.languageZh' : 'feedback.languageEn')); + } + + window.lang = language; + window.setLanguage = setLanguage; + window.LifeI18n = { t, date, number, locale, language, preference: () => preference, dictionaries: { en, zh } }; })(); diff --git a/public/index.html b/public/index.html index 3048162..ce9c335 100644 --- a/public/index.html +++ b/public/index.html @@ -10,11 +10,11 @@ document.head.insertAdjacentHTML('beforeend','