diff --git a/public/addons.js b/public/addons.js index 6fdbb05..141f691 100644 --- a/public/addons.js +++ b/public/addons.js @@ -4,7 +4,24 @@ const APP_VERSION = '1.1.0'; const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'system']; const zh = () => (typeof lang === 'function' ? lang() : 'en') === 'zh'; - const copy = (en, cn) => (zh() ? cn : en); + // Add-on strings share the same catalogue as the desktop shell. Keeping + // this mapping here makes a new feature impossible to ship with a separate + // ad-hoc language mechanism. + const EXTRA_ZH = { + 'DRAFT RESTORED':'草稿已恢复','BROWSER LANGUAGE':'浏览器语言', + 'Detected: Chinese':'当前识别:中文','Detected: English':'当前识别:英语', + 'AUTO':'跟随浏览器','SEARCH MEMORY ARCHIVE':'搜索记忆档案', + 'Search titles, notes, moods…':'搜索标题、记录或天气…', + 'ESC to close · click a result to open it':'按 Esc 关闭 · 点击结果即可打开', + 'Untitled memory':'未命名记忆','No written note.':'尚未写下记录。', + 'No memories found.':'没有找到记忆。','CLOUD SYNC PAUSED — CHANGES STAY ON THIS DEVICE':'云端同步暂停——更改仍保留在此设备', + 'CLOUD SYNC READY':'云端同步已就绪','Move this browser’s Life OS to the cloud?':'将此浏览器的 Life OS 迁移到云端?', + 'Your existing local records will become the first synced copy for this ChatGPT account.':'现有本地记录将成为这个 ChatGPT 账号的第一份云端同步数据。', + 'MIGRATE & SYNC':'迁移并同步','START FRESH':'从空白开始','LOCAL LIFE OS MIGRATED':'本地 Life OS 已迁移', + 'CLOUD LIFE OS RESTORED':'云端 Life OS 已恢复','CLOUD STORAGE UNAVAILABLE — LOCAL MODE ACTIVE':'云端存储暂不可用——已使用本地模式' + }; + if (typeof ZH !== 'undefined') Object.assign(ZH, EXTRA_ZH); + const copy = (en, cn) => (typeof i18n === 'function' ? i18n(en, en) : (zh() ? cn : en)); function readDraft() { try { return JSON.parse(localStorage.getItem(DRAFT_KEY)); } catch { return null; } @@ -34,7 +51,8 @@ const actual = typeof browserLanguage === 'function' ? browserLanguage() : 'en'; const section = document.createElement('section'); section.id = 'lifeos-browser-language'; - section.innerHTML = `
${copy('BROWSER LANGUAGE', '浏览器语言')}

${copy(`Detected: ${actual === 'zh' ? 'Chinese' : 'English'}`, `当前识别:${actual === 'zh' ? '中文' : 'English'}`)}

`; + const detected = actual === 'zh' ? copy('Detected: Chinese', '当前识别:中文') : copy('Detected: English', '当前识别:英语'); + section.innerHTML = `
${copy('BROWSER LANGUAGE', '浏览器语言')}

${detected}

`; section.querySelectorAll('button').forEach(button => button.addEventListener('click', () => setLanguage(button.dataset.language))); [...settingsPanel.children].forEach(node => { const label = node.querySelector?.('.eyebrow')?.textContent.trim(); @@ -89,6 +107,19 @@ if (next !== node.innerHTML) node.innerHTML = next; }); } + function polishLocaleChrome() { + const panel = document.querySelector('#content-system .settings'); + if (!panel) return; + const appearance = [...panel.querySelectorAll('section')].find(section => { + const label = section.querySelector('.eyebrow')?.textContent.trim(); + return label === 'APPEARANCE' || label === '外观'; + }); + if (!appearance) return; + const labels = zh() ? ['深色', '浅色', '跟随系统'] : ['DARK', 'LIGHT', 'AUTO']; + appearance.querySelectorAll('.chip').forEach((button, index) => { + if (labels[index] && button.textContent !== labels[index]) button.textContent = labels[index]; + }); + } function exportData() { const data = { app: 'Life OS', version: APP_VERSION, schemaVersion: 1, exportedAt: new Date().toISOString(), profile: profile(), memories: mems(), museum: museum(), settings: settings() }; const link = document.createElement('a'); @@ -158,10 +189,11 @@ cloudStyle.textContent = `#lifeos-cloud-migration{position:fixed;inset:0;z-index:9000;background:#071008b8;display:grid;place-items:center;padding:20px}.lifeos-cloud-card{width:min(470px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:25px;box-shadow:var(--shadow)}.lifeos-cloud-card h2{margin:10px 0;font-size:25px;letter-spacing:-.04em}.lifeos-cloud-card p{color:var(--muted);line-height:1.65;margin:0 0 20px}`; document.head.append(cloudStyle); startCloudSync(); - new MutationObserver(() => { wireToday(); wireLanguageControl(); polishSystemChrome(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title'] }); + new MutationObserver(() => { wireToday(); wireLanguageControl(); polishSystemChrome(); polishLocaleChrome(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title'] }); wireToday(); wireLanguageControl(); polishSystemChrome(); + polishLocaleChrome(); document.addEventListener('click', e => { if (e.target.closest('#content-today .primary')) clearDraftAfterSave(); }); document.addEventListener('click', e => { if (e.target.closest('.launchitem')) document.querySelector('#launcher')?.classList.add('hidden'); diff --git a/public/index.html b/public/index.html index 86da415..dd8eb6f 100644 --- a/public/index.html +++ b/public/index.html @@ -14,9 +14,19 @@ const ZH={'LIFE OS / PERSONAL DESKTOP':'LIFE OS / 人生操作系统','TODAY':' Object.assign(ZH,{ 'INITIALIZING PERSONAL SYSTEM...':'正在初始化个人系统…','GOOD MORNING':'早上好','GOOD AFTERNOON':'下午好','GOOD EVENING':'晚上好','Open':'打开','Minimize':'最小化','Maximize':'最大化','Close':'关闭','Refresh desktop':'刷新桌面','New today entry':'新建今日记录','System settings':'系统设置','CLEAR':'晴朗','LIGHT':'微光','CLOUDY':'多云','RAIN':'雨天','STORM':'风暴','Clear':'晴朗','Light':'微光','Cloudy':'多云','Rain':'雨天','Storm':'风暴','DARK':'深色','LIGHT':'浅色','AUTO':'自动','Version 0.1 · Schema v1':'版本 0.1 · 架构 v1','MONDAY':'星期一','TUESDAY':'星期二','WEDNESDAY':'星期三','THURSDAY':'星期四','FRIDAY':'星期五','SATURDAY':'星期六','SUNDAY':'星期日','Monday':'星期一','Tuesday':'星期二','Wednesday':'星期三','Thursday':'星期四','Friday':'星期五','Saturday':'星期六','Sunday':'星期日','No written note.':'尚未写下记录。','EDITING AS TODAY — SAVE TO UPDATE':'正在以今日记录编辑 — 保存即可更新','MEMORY PRESERVED':'记忆已珍藏','MEMORY REMOVED':'记忆已删除','No entries yet':'暂无记录','Longest streak:':'最长连续记录:','days':'天','DAYS':'天','ENERGY':'能量','Local Storage':'本地存储','memories':'条记忆','museum items':'件博物馆藏品','Approx.':'约','KB':'KB','Life OS Backup':'Life OS 备份','Ready to restore':'准备恢复','Exported:':'导出时间:','Memories:':'记忆:','Museum Items:':'博物馆藏品:','MERGE':'合并','REPLACE ALL DATA':'替换全部数据','FINAL CONFIRMATION':'最终确认','Replace all Life OS data?':'替换全部 Life OS 数据?','This will replace all current Life OS data.':'这将覆盖当前所有 Life OS 数据。','REPLACE DATA':'替换数据','Reset all local data?':'重置所有本地数据?','Export a backup first if you want to keep it.':'如需保留数据,请先导出备份。','RESET DATA':'重置数据','Following browser language: English':'已跟随浏览器语言:English' }); +// "LIGHT" is a weather state (微光), while a light colour scheme is 浅色. +// Keep the two meanings separate; translating visible strings by themselves +// cannot distinguish them reliably. ZH.LIGHT='微光'; +Object.assign(ZH,{ + 'THEME_DARK':'深色','THEME_LIGHT':'浅色','THEME_AUTO':'跟随系统', + 'LANGUAGE_AUTO':'跟随浏览器','LANGUAGE_DETECTED_ZH':'当前识别:中文', + 'LANGUAGE_DETECTED_EN':'Detected: English', + 'MINIMIZED_RESTORE':'已最小化 — 点击任务栏图标即可恢复', + 'MINIMIZED_RESTORE_EN':'MINIMIZED — TAP THE APP ICON TO RESTORE' +}); ZH['Restore window']='恢复窗口'; -function browserLanguage(){let ls=navigator.languages?.length?navigator.languages:[navigator.language||'en'];return ls.some(x=>String(x).toLowerCase().startsWith('zh'))?'zh':'en'}function lang(){let s=settings();return s.languageMode==='manual'&&['en','zh'].includes(s.language)?s.language:browserLanguage()}function localizeDynamicText(zh){if(!zh)return;let walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),nodes=[];while(walker.nextNode())nodes.push(walker.currentNode);nodes.forEach(node=>{let v=node.nodeValue.trim(),r,section=node.parentElement?.closest('section')?.querySelector('.eyebrow')?.textContent||'';if(v==='微光'&§ion.includes('外观'))node.nodeValue=node.nodeValue.replace(v,'浅色');else if(v==='Local Storage')node.nodeValue=node.nodeValue.replace(v,'本地存储');else if(v==='Life OS')node.nodeValue=node.nodeValue.replace(v,'人生操作系统');else if(r=v.match(/^(\d+) (?:MEMORY|MEMORIES) ARCHIVED$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆已归档`);else if(r=v.match(/^(\d+) DAYS$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 天`);else if(r=v.match(/^Longest streak: (\d+) days$/))node.nodeValue=node.nodeValue.replace(v,`最长连续记录:${r[1]} 天`);else if(r=v.match(/^(\d+) memories · (\d+) preserved$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆 · ${r[2]} 件已珍藏`);else if(r=v.match(/^Exported: (.+)$/))node.nodeValue=node.nodeValue.replace(v,`导出时间:${r[1]}`);else if(r=v.match(/^Memories: (\d+)$/))node.nodeValue=node.nodeValue.replace(v,`记忆:${r[1]}`);else if(r=v.match(/^Museum Items: (\d+)$/))node.nodeValue=node.nodeValue.replace(v,`博物馆藏品:${r[1]}`);else if(r=v.match(/^LAST BACKUP · (.+)$/))node.nodeValue=node.nodeValue.replace(v,`上次备份 · ${r[1]}`);else if(r=v.match(/^(\d+) memories · (\d+) museum items · Approx\. (\d+) KB$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆 · ${r[2]} 件博物馆藏品 · 约 ${r[3]} KB`)})}function applyLanguage(){let zh=lang()==='zh';document.documentElement.lang=zh?'zh-CN':'en';document.title=zh?'Life OS / 人生操作系统':'Life OS';let map=zh?ZH:Object.fromEntries(Object.entries(ZH).map(([a,b])=>[b,a]));let walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let n=[];while(walker.nextNode())n.push(walker.currentNode);n.forEach(x=>{let v=x.nodeValue.trim();if(map[v])x.nodeValue=x.nodeValue.replace(v,map[v])});document.querySelectorAll('[placeholder]').forEach(x=>{if(zh&&ZH[x.placeholder])x.placeholder=ZH[x.placeholder];if(!zh){let k=Object.entries(ZH).find(([,v])=>v===x.placeholder)?.[0];if(k)x.placeholder=k}});localizeDynamicText(zh)}function setLanguage(v){let s=settings();s.language=v;s.languageMode=v==='auto'?'auto':'manual';save('lifeos_settings',s);renderIcons();renderAll();applyLanguage();toast(v==='auto'?(lang()==='zh'?'已跟随浏览器语言:中文':'Following browser language: English'):v==='zh'?'语言已切换为中文':'Language set to English')} +function browserLanguage(){let ls=navigator.languages?.length?navigator.languages:[navigator.language||'en'];return ls.some(x=>String(x).toLowerCase().startsWith('zh'))?'zh':'en'}function lang(){let s=settings();return s.languageMode==='manual'&&['en','zh'].includes(s.language)?s.language:browserLanguage()}function localizeDynamicText(zh){if(!zh)return;let walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),nodes=[];while(walker.nextNode())nodes.push(walker.currentNode);nodes.forEach(node=>{if(node.parentElement?.closest('textarea,input,.entry,.snippet,.mtitle,.film-title,.film-copy'))return;let v=node.nodeValue.trim(),r,section=node.parentElement?.closest('section')?.querySelector('.eyebrow')?.textContent||'';if(v==='微光'&§ion.includes('外观'))node.nodeValue=node.nodeValue.replace(v,'浅色');else if(v==='Local Storage')node.nodeValue=node.nodeValue.replace(v,'本地存储');else if(v==='Life OS')node.nodeValue=node.nodeValue.replace(v,'人生操作系统');else if(r=v.match(/^(\d+) (?:MEMORY|MEMORIES) ARCHIVED$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆已归档`);else if(r=v.match(/^(\d+) DAYS$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 天`);else if(r=v.match(/^Longest streak: (\d+) days$/))node.nodeValue=node.nodeValue.replace(v,`最长连续记录:${r[1]} 天`);else if(r=v.match(/^(\d+) memories · (\d+) preserved$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆 · ${r[2]} 件已珍藏`);else if(r=v.match(/^Exported: (.+)$/))node.nodeValue=node.nodeValue.replace(v,`导出时间:${r[1]}`);else if(r=v.match(/^Memories: (\d+)$/))node.nodeValue=node.nodeValue.replace(v,`记忆:${r[1]}`);else if(r=v.match(/^Museum Items: (\d+)$/))node.nodeValue=node.nodeValue.replace(v,`博物馆藏品:${r[1]}`);else if(r=v.match(/^LAST BACKUP · (.+)$/))node.nodeValue=node.nodeValue.replace(v,`上次备份 · ${r[1]}`);else if(r=v.match(/^(\d+) memories · (\d+) museum items · Approx\. (\d+) KB$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆 · ${r[2]} 件博物馆藏品 · 约 ${r[3]} KB`)})}function applyLanguage(){let zh=lang()==='zh';document.documentElement.lang=zh?'zh-CN':'en';document.title=zh?'Life OS / 人生操作系统':'Life OS';let map=zh?ZH:Object.fromEntries(Object.entries(ZH).map(([a,b])=>[b,a]));let walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let n=[];while(walker.nextNode())n.push(walker.currentNode);n.forEach(x=>{if(x.parentElement?.closest('textarea,input,.entry,.snippet,.mtitle,.film-title,.film-copy'))return;let v=x.nodeValue.trim();if(map[v])x.nodeValue=x.nodeValue.replace(v,map[v])});document.querySelectorAll('[placeholder]').forEach(x=>{if(zh&&ZH[x.placeholder])x.placeholder=ZH[x.placeholder];if(!zh){let k=Object.entries(ZH).find(([,v])=>v===x.placeholder)?.[0];if(k)x.placeholder=k}});localizeDynamicText(zh)}function setLanguage(v){let s=settings();s.language=v;s.languageMode=v==='auto'?'auto':'manual';save('lifeos_settings',s);renderIcons();renderAll();applyLanguage();toast(v==='auto'?(lang()==='zh'?'已跟随浏览器语言:中文':'Following browser language: English'):v==='zh'?'语言已切换为中文':'Language set to English')} function i18n(key,fallback=key){return lang()==='zh'?(ZH[key]||key):fallback}function appName(id){return i18n(APPS[id][1])}function moodName(m){return i18n(m.toUpperCase(),cap(m))}function day(){return new Date().toISOString().slice(0,10)}function fmtDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function cap(x){return x[0].toUpperCase()+x.slice(1)}function esc(s){return (s||'').replace(/[&<>]/g,x=>({'&':'&','<':'<','>':'>'}[x]))} function applyWallpaper(w){if(!w?.url)return;desktop.style.backgroundImage=`linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${w.url}')`;desktop.style.backgroundSize='cover';desktop.style.backgroundPosition='center';desktop.title=`Bing daily image: ${w.title||'Daily image'}`}async function loadBingWallpaper(){let s=settings();if(s.wallpaper?.date===day()&&s.wallpaper?.source==='lifeos-proxy'){applyWallpaper(s.wallpaper);return}try{let r=await fetch('/api/bing-wallpaper'),d=await r.json();if(!r.ok||!d.url)throw Error(d.error||'No Bing image');s.wallpaper={date:day(),url:'/api/bing-wallpaper-image?date='+day(),title:d.title||'Bing daily image',source:'lifeos-proxy'};save('lifeos_settings',s);applyWallpaper(s.wallpaper)}catch(e){console.info('Bing wallpaper unavailable; using the calm fallback.',e)}}function init(){let s=settings();document.documentElement.dataset.theme=s.theme==='auto'?(matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):s.theme;renderIcons();renderTaskbar();loadBingWallpaper();setInterval(clock,1000);clock();setTimeout(applyLanguage,0);setTimeout(()=>boot.remove(),650)} function desktopIconClick(id,e){if(innerWidth<650){openApp(id);return}selectedIcon=id;document.querySelectorAll('.app-icon').forEach(x=>x.classList.remove('selected'));e.currentTarget.classList.add('selected')}function desktopIconKey(id,e){if(e.key==='Enter'||e.key===' '){e.preventDefault();openApp(id)}}function renderIcons(){icons.innerHTML=Object.entries(APPS).map(([id,[g,n]])=>``).join('')}function taskClick(id){let w=windows[id];if(!w){openApp(id);return}if(w.classList.contains('min')){openApp(id);return}if(activeWindow===id){minWin(id);return}focus(w)}function renderTaskbar(){let ids=innerWidth<650?Object.keys(windows):['today','timeline','museum','insights'];tasks.innerHTML=ids.map(id=>{let w=windows[id],min=w?.classList.contains('min'),fg=activeWindow===id&&!min;return``}).join('')}