feat(i18n): follow browser language by default

This commit is contained in:
Codex committed 2026-09-25 07:15:08 +07:00
1 parent 75c81ce498
commit bf30001c42
3 files changed
+20 -4

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
- **Timeline**:按时间轴浏览、编辑和删除记忆。
- **Museum**:将重要记忆长期珍藏为馆藏。
- **Insights / Profile**:查看情绪、能量、连续记录与个人系统状态。
- **System**:深浅主题、中文/English 切换、JSON 导入导出与本地存储说明。
- **System**:深浅主题、自动识别浏览器语言(也可固定中文/English)、JSON 导入导出与本地存储说明。
- **Bing 每日一图**:桌面壁纸每日自动更新;接口不可用时保留内置背景。
- **Windows 式操作**:桌面端单击选择、双击打开;右键桌面或 App 图标可使用快捷菜单。
- **快捷键与草稿恢复**:`Ctrl / ⌘ + K` 打开启动器,`Ctrl / ⌘ + F` 搜索记忆,`Ctrl / ⌘ + S` 归档 Today;当天未归档文字会自动恢复。
+17 -1
View File
@@ -26,6 +26,21 @@
restoreDraft(entry);
entry.addEventListener('input', () => saveDraft(entry.value));
}
function wireLanguageControl() {
const settingsPanel = document.querySelector('#content-system .settings');
if (!settingsPanel || settingsPanel.querySelector('#lifeos-browser-language')) return;
const state = settings();
const actual = typeof browserLanguage === 'function' ? browserLanguage() : 'en';
const section = document.createElement('section');
section.id = 'lifeos-browser-language';
section.innerHTML = `<div class="eyebrow">${copy('BROWSER LANGUAGE', '浏览器语言')}</div><p class="sub">${copy(`Detected: ${actual === 'zh' ? 'Chinese' : 'English'}`, `当前识别:${actual === 'zh' ? '中文' : 'English'}`)}</p><div class="choices"><button class="chip ${state.languageMode !== 'manual' ? 'selected' : ''}" data-language="auto">${copy('AUTO', '跟随浏览器')}</button><button class="chip ${state.languageMode === 'manual' && state.language === 'en' ? 'selected' : ''}" data-language="en">ENGLISH</button><button class="chip ${state.languageMode === 'manual' && state.language === 'zh' ? 'selected' : ''}" data-language="zh">中文</button></div>`;
section.querySelectorAll('button').forEach(button => button.addEventListener('click', () => setLanguage(button.dataset.language)));
[...settingsPanel.children].forEach(node => {
const label = node.querySelector?.('.eyebrow')?.textContent.trim();
if (label === 'LANGUAGE' || label === '语言') node.remove();
});
settingsPanel.prepend(section);
}
function clearDraftAfterSave() {
setTimeout(() => {
const today = new Date().toISOString().slice(0, 10);
@@ -62,8 +77,9 @@
const style = document.createElement('style');
style.textContent = `.lifeos-search-backdrop{position:fixed;inset:0;background:#0008;z-index:8000}.lifeos-search-panel{position:fixed;z-index:8001;top:18vh;left:50%;transform:translateX(-50%);width:min(560px,calc(100vw - 28px));background:var(--panel);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);padding:15px}.lifeos-search-head{display:flex;justify-content:space-between;align-items:center;font:500 11px 'DM Mono';letter-spacing:.12em;color:var(--muted);margin-bottom:13px}.lifeos-search-head button{border:0;background:transparent;color:var(--ink);font-size:20px}.lifeos-search-panel input{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:12px;outline:none}.lifeos-search-panel input:focus{border-color:var(--accent)}.lifeos-search-results{margin-top:9px;max-height:360px;overflow:auto}.lifeos-search-results button{display:grid;gap:3px;width:100%;text-align:left;border:0;border-radius:9px;padding:11px;background:transparent;color:var(--ink)}.lifeos-search-results button:hover{background:var(--panel2)}.lifeos-search-results small,.lifeos-search-results span,.lifeos-search-hint{color:var(--muted);font-size:12px}.lifeos-search-results strong{font-size:14px}.lifeos-search-results span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lifeos-search-empty{padding:24px;color:var(--muted);text-align:center;font-size:13px}.lifeos-search-hint{margin:11px 0 0;font:10px 'DM Mono';letter-spacing:.04em}`;
document.head.append(style);
new MutationObserver(wireToday).observe(document.body, { childList: true, subtree: true });
new MutationObserver(() => { wireToday(); wireLanguageControl(); }).observe(document.body, { childList: true, subtree: true });
wireToday();
wireLanguageControl();
document.addEventListener('click', e => { if (e.target.closest('#content-today .primary')) clearDraftAfterSave(); });
document.addEventListener('keydown', e => {
const mod = e.ctrlKey || e.metaKey;
+2 -2
View File
@@ -8,9 +8,9 @@
const APPS={today:['📅','TODAY'],timeline:['🕒','TIMELINE'],museum:['🏛️','MUSEUM'],insights:['📊','INSIGHTS'],profile:['👤','PROFILE'],system:['⚙️','SYSTEM']};let windows={},z=20,selectedMood='cloudy',selectedEnergy=3,importData=null,selectedIcon=null;
const films=[['A Quiet Tuesday','Drama · Slice of Life'],['The Afternoon That Stayed','Independent · Warm'],['Small Light, Long Hallway','Contemplative · Drama'],['Before the Notifications','Minimal · Coming of Age'],['Room With Open Curtains','Slice of Life · Calm'],['The Usual Way Home','Drama · Familiar'],['Notes Left Unfinished','Reflective · Short Film'],['A Weather of Thoughts','Quiet · Personal'],['The Day Between Things','Indie · Observational'],['Slow Hours, Soft Edges','Slice of Life · Dream'],['A Window Still Lit','Drama · Evening'],['Ordinary, Carefully Held','Human · Gentle'],['One More Page Tonight','Study · Slow Burn'],['The Space After Lunch','Memory · Calm'],['Late Bus, Clear Mind','Urban · Reflective'],['Little Things in Focus','Documentary · Life'],['What the Room Remembered','Poetic · Still'],['Blue Hour, No Rush','Evening · Quiet'],['The Map of a Small Day','Journey · Personal'],['Somewhere After Four','Indie · Warm'],['A Day With No Headline','Slice of Life · Soft'],['The Sound of Taking Time','Slow Cinema · Personal'],['Things That Did Not Need Fixing','Drama · Gentle'],['Halfway Through September','Memory · Youth'],['A Small Archive of Us','Personal · Quiet'],['The Night Kept Its Shape','Noir · Calm'],['Unscheduled Sunshine','Light · Personal'],['The Last Open Tab','Modern · Reflective'],['A Place for Today','Drama · Human'],['Nothing Lost to the Day','Slice of Life · Hopeful']];
const lines=['Nothing remarkable happened. Maybe that was the point.','A small day, held carefully enough to remember.','The ordinary parts stayed longer than expected.','Not every chapter needs a dramatic ending.','Some thoughts arrived; some were allowed to pass.','A quiet record of being here.','The day moved at its own reasonable pace.','There was room for a little more softness.','Even the unfinished things belonged here.','The light changed, and so did the mood.','A scene worth keeping, without needing an explanation.','It was enough to notice the day as it was.','Time did not rush; neither did this memory.','The details were small, but they were real.','A pause became part of the story.','Somewhere, this became a day to keep.','The familiar became briefly luminous.','Nothing needed to be solved before nightfall.','A few moments stayed in focus.','The story was quiet, but it was still yours.','Today left a small mark.','A gentle frame around an ordinary hour.','The day asked for attention, not perfection.','This memory can rest here for now.','The world was loud; this page was not.','A few loose threads, honestly kept.','The simplest version of today was enough.','The day settled before it ended.','There was meaning in the in-between.','A private moment, safely archived.'];
function load(k,d){try{return JSON.parse(localStorage.getItem(k))||d}catch{return d}}function save(k,v){localStorage.setItem(k,JSON.stringify(v))}function mems(){return load('lifeos_memories',[])}function museum(){return load('lifeos_museum',[])}function profile(){return load('lifeos_profile',{name:'',birthday:'',startDate:new Date().toISOString().slice(0,10),motto:''})}function settings(){return load('lifeos_settings',{theme:'dark',lastBackup:null,language:'en'})}function sys(){return load('lifeos_system',{schemaVersion:1})}
function load(k,d){try{return JSON.parse(localStorage.getItem(k))||d}catch{return d}}function save(k,v){localStorage.setItem(k,JSON.stringify(v))}function mems(){return load('lifeos_memories',[])}function museum(){return load('lifeos_museum',[])}function profile(){return load('lifeos_profile',{name:'',birthday:'',startDate:new Date().toISOString().slice(0,10),motto:''})}function settings(){return{theme:'dark',lastBackup:null,language:'auto',languageMode:'auto',...load('lifeos_settings',{})}}function sys(){return load('lifeos_system',{schemaVersion:1})}
const ZH={'LIFE OS / PERSONAL DESKTOP':'LIFE OS / 人生操作系统','TODAY':'今日','TIMELINE':'时间线','MUSEUM':'人生博物馆','INSIGHTS':'洞察','PROFILE':'个人资料','SYSTEM':'系统','SYSTEM ONLINE · LIFE OS v0.1':'系统在线 · LIFE OS v0.1','SYSTEM DATE':'系统日期','MEMORY ARCHIVE':'记忆档案','MEMORIES ARCHIVED':'条记忆已归档','MEMORY ARCHIVED':'记忆已归档','How was your day?':'今天过得怎么样?','Write something about today...':'写下今天的片段…',"TODAY'S WEATHER":'今日天气','ENERGY LEVEL':'能量等级',"TODAY'S FILM":'今日电影','ARCHIVE TODAY':'归档今日','UPDATE MEMORY':'更新记忆','ARCHIVING MEMORY...':'正在归档…','MUSEUM':'人生博物馆','Preserved moments':'被珍藏的瞬间','Worth keeping for the long view.':'值得留给漫长回望的片段。','A museum begins with one preserved moment.':'从一份被珍藏的记忆开始。','STATUS: PRESERVED':'状态:已珍藏','LAST 30 DAYS':'最近 30 天','Insights, quietly.':'安静地看见时间。','WEATHER':'天气','AVERAGE ENERGY':'平均能量','MOST ACTIVE DAY':'最活跃日','CURRENT STREAK':'当前连续记录','LONGEST STREAK':'最长连续记录','SYSTEM PROFILE':'系统档案','LIFE OS ONLINE':'LIFE OS 在线','DAYS ONLINE':'系统运行天数','MEMORIES':'记忆','MUSEUM ITEMS':'博物馆藏品','COMMON WEATHER':'常见天气','PERSONAL SYSTEM':'个人系统','Display Name':'显示名称','Birthday (optional)':'生日(选填)','Life OS Start Date':'Life OS 启动日期','Personal Motto':'个人格言','SAVE PROFILE':'保存资料','PROFILE SAVED':'资料已保存','SYSTEM SETTINGS':'系统设置','System, local and yours.':'你的本地个人系统。','APPEARANCE':'外观','DATA':'数据','STORAGE':'存储','DATA STORAGE':'数据存储','Your data is stored locally in this browser.':'你的数据仅存储在此浏览器中。','Create regular backups to prevent data loss.':'定期备份,让记忆一直在。','LAST BACKUP · NEVER':'上次备份 · 从未','BACKUP RECOMMENDED':'建议备份','VERSION':'版本','EXPORT DATA':'导出数据','IMPORT DATA':'导入数据','RESET LIFE OS':'重置 Life OS','LANGUAGE':'语言','ENGLISH':'English','CHINESE':'中文','Dark':'深色','Light':'浅色','Auto':'自动','No memories yet.':'还没有记忆。','Open Today when you are ready to keep one.':'准备好记录时,打开“今日”。','MEMORY DETAIL':'记忆详情','EDIT':'编辑','ADD TO MUSEUM':'加入博物馆','PRESERVED':'已珍藏','DELETE':'删除','CONFIRM DELETE':'确认删除','Remove this memory?':'删除这条记忆?','This cannot be undone.':'此操作无法撤销。','DELETE MEMORY':'删除记忆','CANCEL':'取消','BACKUP CREATED':'备份已创建','INVALID BACKUP FILE':'无效备份文件','SYSTEM RESTORED':'系统已恢复','RESTORING SYSTEM...':'正在恢复系统…','MINIMIZED — TAP THE APP ICON TO RESTORE':'已最小化 — 点击任务栏图标即可恢复'};
function lang(){return settings().language||'en'}function applyLanguage(){let zh=lang()==='zh';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}})}function setLanguage(v){let s=settings();s.language=v;save('lifeos_settings',s);renderAll();applyLanguage();toast(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 applyLanguage(){let zh=lang()==='zh';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}})}function setLanguage(v){let s=settings();s.language=v;s.languageMode=v==='auto'?'auto':'manual';save('lifeos_settings',s);renderAll();applyLanguage();toast(v==='auto'?(lang()==='zh'?'已跟随浏览器语言:中文':'Following browser language: English'):v==='zh'?'语言已切换为中文':'Language set to English')}
function day(){return new Date().toISOString().slice(0,10)}function fmtDate(d){return new Date(d+'T12:00:00').toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric'}).toUpperCase()}function cap(x){return x[0].toUpperCase()+x.slice(1)}function esc(s){return (s||'').replace(/[&<>]/g,x=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[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()){applyWallpaper(s.wallpaper);return}try{let r=await fetch('https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-US'),d=(await r.json()).images?.[0];if(!d)throw Error('No Bing image');s.wallpaper={date:day(),url:'https://www.bing.com'+d.url,title:d.copyright||'Bing daily image'};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.querySelector('.glyph').style.outline='none');e.currentTarget.querySelector('.glyph').style.outline='2px solid var(--accent)'}function renderIcons(){icons.innerHTML=Object.entries(APPS).map(([id,[g,n]])=>`<button class="app-icon" onclick="desktopIconClick('${id}',event)" ondblclick="openApp('${id}')" oncontextmenu="showContext(event,'${id}')"><span class="glyph">${g}</span><span>${n}</span></button>`).join('')}function renderTaskbar(){let ids=innerWidth<650?Object.keys(windows):['today','timeline','museum','insights'];tasks.innerHTML=ids.map(id=>`<button class="taskapp ${windows[id]?'active':''}" title="${APPS[id][1]}" aria-label="Open ${APPS[id][1]}" onclick="openApp('${id}')">${APPS[id][0]}</button>`).join('')}