diff --git a/public/addons.js b/public/addons.js
index 141f691..35fd590 100644
--- a/public/addons.js
+++ b/public/addons.js
@@ -3,25 +3,7 @@
const DRAFT_KEY = 'lifeos_today_draft_v1';
const APP_VERSION = '1.1.0';
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'system'];
- const zh = () => (typeof lang === 'function' ? lang() : 'en') === 'zh';
- // 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));
+ const t = (key, values) => LifeI18n.t(key, values);
function readDraft() {
try { return JSON.parse(localStorage.getItem(DRAFT_KEY)); } catch { return null; }
@@ -35,7 +17,7 @@
if (!draft || draft.date !== today || !draft.text || entry.value) return;
entry.value = draft.text;
entry.dispatchEvent(new Event('input', { bubbles: true }));
- if (typeof toast === 'function') toast(copy('DRAFT RESTORED', '草稿已恢复'));
+ if (typeof toast === 'function') toast('addon.draftRestored');
}
function wireToday() {
const entry = document.querySelector('#entry');
@@ -44,22 +26,7 @@
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';
- 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();
- if (label === 'LANGUAGE' || label === '语言') node.remove();
- });
- settingsPanel.prepend(section);
- }
+ function wireLanguageControl() { /* The System renderer owns this control. */ }
function clearDraftAfterSave() {
setTimeout(() => {
const today = new Date().toISOString().slice(0, 10);
@@ -75,7 +42,7 @@
closeSearch();
const box = document.createElement('div');
box.id = 'lifeos-search';
- box.innerHTML = `${copy('SEARCH MEMORY ARCHIVE', '搜索记忆档案')}
${copy('ESC to close · click a result to open it', 'ESC 关闭 · 点击结果即可打开')}
`;
+ box.innerHTML = `${t('addon.searchTitle')}
${t('addon.searchHint')}
`;
document.body.append(box);
const input = box.querySelector('input');
const results = box.querySelector('.lifeos-search-results');
@@ -84,7 +51,7 @@
try { memories = JSON.parse(localStorage.getItem('lifeos_memories') || '[]'); } catch {}
const query = input.value.trim().toLowerCase();
const found = memories.filter(m => !query || [m.filmTitle, m.text, m.mood, m.date].join(' ').toLowerCase().includes(query)).slice(0, 8);
- results.innerHTML = found.length ? found.map(m => ``).join('') : `${copy('No memories found.', '没有找到记忆。')}
`;
+ results.innerHTML = found.length ? found.map(m => ``).join('') : `${t('addon.noResults')}
`;
results.querySelectorAll('button').forEach(button => button.addEventListener('click', () => { closeSearch(); openApp('timeline'); setTimeout(() => detail(button.dataset.id), 20); }));
};
input.addEventListener('input', render);
@@ -107,25 +74,12 @@
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');
link.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
link.download = `life-os-backup-${day()}.json`; link.click();
- const state = settings(); state.lastBackup = new Date().toISOString(); save('lifeos_settings', state); renderAll(); toast(copy('BACKUP CREATED', '备份已创建'));
+ const state = settings(); state.lastBackup = new Date().toISOString(); save('lifeos_settings', state); renderAll(); toast('feedback.backup');
}
const CLOUD_KEYS = { profile: 'lifeos_profile', memories: 'lifeos_memories', museum: 'lifeos_museum', settings: 'lifeos_settings', system: 'lifeos_system' };
let cloudReady = false, applyingCloud = false, syncTimer = null, syncErrorShown = false;
@@ -151,15 +105,15 @@
if (!cloudReady || applyingCloud) return;
clearTimeout(syncTimer);
syncTimer = setTimeout(() => uploadCloudState().catch(() => {
- if (!syncErrorShown) { syncErrorShown = true; toast(copy('CLOUD SYNC PAUSED — CHANGES STAY ON THIS DEVICE', '云端同步暂停——更改仍保存在此设备')); }
+ if (!syncErrorShown) { syncErrorShown = true; toast('addon.syncPaused'); }
}), 450);
}
function migrationPrompt(state) {
const dialog = document.createElement('div');
dialog.id = 'lifeos-cloud-migration';
- dialog.innerHTML = `${copy('CLOUD SYNC READY', '云端同步已就绪')}
${copy('Move this browser’s Life OS to the cloud?', '将此浏览器的 Life OS 迁移到云端?')}
${copy('Your existing local records will become the first synced copy for this ChatGPT account.', '现有本地记录将成为这个 ChatGPT 账号的第一份云端同步数据。')}
`;
+ dialog.innerHTML = `${t('addon.syncReady')}
${t('addon.migrateQuestion')}
${t('addon.migrateBody')}
`;
document.body.append(dialog);
- dialog.querySelector('[data-cloud="move"]').addEventListener('click', async () => { dialog.remove(); cloudReady = true; await uploadCloudState(); toast(copy('LOCAL LIFE OS MIGRATED', '本地 Life OS 已迁移')); });
+ dialog.querySelector('[data-cloud="move"]').addEventListener('click', async () => { dialog.remove(); cloudReady = true; await uploadCloudState(); toast('addon.migrated'); });
dialog.querySelector('[data-cloud="empty"]').addEventListener('click', async () => { dialog.remove(); applyCloudState(cloudFallback); cloudReady = true; await uploadCloudState(); });
}
async function startCloudSync() {
@@ -170,14 +124,14 @@
const local = cloudSnapshot();
if (state) {
if (hasLocalLife(local)) localStorage.setItem('lifeos_precloud_backup_v1', JSON.stringify({ exportedAt: new Date().toISOString(), state: local }));
- applyCloudState(state); cloudReady = true; toast(copy('CLOUD LIFE OS RESTORED', '云端 Life OS 已恢复'));
+ applyCloudState(state); cloudReady = true; toast('addon.cloudRestored');
} else if (hasLocalLife(local)) {
migrationPrompt(local);
} else {
cloudReady = true; await uploadCloudState();
}
} catch {
- if (!syncErrorShown) { syncErrorShown = true; toast(copy('CLOUD STORAGE UNAVAILABLE — LOCAL MODE ACTIVE', '云端存储暂不可用——已使用本地模式')); }
+ if (!syncErrorShown) { syncErrorShown = true; toast('addon.cloudUnavailable'); }
}
}
const originalSetItem = Storage.prototype.setItem;
@@ -189,11 +143,10 @@
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(); polishLocaleChrome(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title'] });
+ new MutationObserver(() => { wireToday(); wireLanguageControl(); polishSystemChrome(); }).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/i18n-renderers.js b/public/i18n-renderers.js
new file mode 100644
index 0000000..a2feea8
--- /dev/null
+++ b/public/i18n-renderers.js
@@ -0,0 +1,66 @@
+/* Render-time localisation layer. User-created records are deliberately never translated. */
+(() => {
+ const t = (key, values) => LifeI18n.t(key, values);
+ const locale = () => lang() === 'zh' ? 'zh-CN' : 'en-US';
+ const weekday = value => new Intl.DateTimeFormat(locale(), { weekday: 'long' }).format(new Date(value + 'T12:00:00'));
+ const uiDate = value => new Intl.DateTimeFormat(locale(), { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }).format(new Date(value + 'T12:00:00'));
+ const short = value => new Intl.DateTimeFormat(locale(), { month: 'short', day: 'numeric' }).format(new Date(value + 'T12:00:00')).toUpperCase();
+ const weather = mood => t(`weather.${mood}`);
+ const rootModal = () => document.getElementById('modal');
+
+ window.appName = id => t(`app.${id}`);
+ window.moodName = weather;
+ window.fmtDate = uiDate;
+ window.shortDate = short;
+ window.applyLanguage = () => { document.documentElement.lang = lang() === 'zh' ? 'zh-CN' : 'en'; document.title = lang() === 'zh' ? 'Life OS / 人生操作系统' : 'Life OS'; };
+ window.setLanguage = value => {
+ const state = settings(); state.language = value; state.languageMode = value === 'auto' ? 'auto' : 'manual'; save('lifeos_settings', state);
+ renderIcons(); renderAll();
+ toast(value === 'auto' ? t(lang() === 'zh' ? 'feedback.followingZh' : 'feedback.followingEn') : t(value === 'zh' ? 'feedback.languageZh' : 'feedback.languageEn'));
+ };
+ window.today = container => {
+ const memory = mems().find(item => item.date === day());
+ if (todayDraft === null) { selectedMood = memory?.mood || 'cloudy'; selectedEnergy = memory?.energy || 3; }
+ const film = memory || filmFor(selectedMood, selectedEnergy), text = todayDraft === null ? (memory?.text || '') : todayDraft;
+ container.innerHTML = `${weekday(day()).toUpperCase()}
${uiDate(day())}
${t('today.prompt')}
`;
+ };
+ window.archiveToday = () => {
+ const text = document.querySelector('#entry')?.value.trim() || '', old = mems().find(item => item.date === day()), film = old || filmFor(selectedMood, selectedEnergy), now = new Date().toISOString();
+ const memory = { id: old?.id || `memory_${crypto.randomUUID()}`, date: day(), text, mood: selectedMood, energy: selectedEnergy, filmTitle: film.filmTitle, filmSubtitle: film.filmSubtitle, filmLine: film.filmLine, createdAt: old?.createdAt || now, updatedAt: now };
+ const status = document.querySelector('#saveStatus'); status.textContent = t('today.archiving');
+ setTimeout(() => { save('lifeos_memories', [...mems().filter(item => item.date !== day()), memory].sort((a, b) => b.date.localeCompare(a.date))); todayDraft = null; status.textContent = t('today.archived'); renderAll(); }, 360);
+ };
+ window.timeline = container => {
+ const all = mems();
+ container.innerHTML = `${t('timeline.eyebrow')}
${lang() === 'zh' ? `${all.length} 条记忆已归档` : `${all.length} ${all.length === 1 ? 'MEMORY' : 'MEMORIES'} ARCHIVED`}
${all.length ? `${all.map(memory => `
`).join('')}
` : `${t('timeline.emptyTitle')}
${t('timeline.emptyBody')}
`}`;
+ };
+ window.detail = id => {
+ const memory = mems().find(item => item.id === id); if (!memory) return;
+ rootModal().innerHTML = `${t('timeline.detail')}
${esc(memory.filmTitle)}
${uiDate(memory.date)} · ${weather(memory.mood)} · ${t('common.energy')} ${memory.energy}
${esc(memory.text || t('timeline.noNote')).replace(/\n/g,'
')}
${memory.filmSubtitle}
`;
+ };
+ window.editMemory = id => { const memory = mems().find(item => item.id === id); rootModal().innerHTML = ''; openApp('today'); selectedMood = memory.mood; selectedEnergy = memory.energy; const all = mems(), index = all.findIndex(item => item.id === id); all[index].date = day(); save('lifeos_memories', all); today(document.querySelector('#content-today')); document.querySelector('#entry').value = memory.text; toast(t('timeline.editing')); };
+ window.preserve = id => { const all = museum(); if (!all.some(item => item.memoryId === id)) { save('lifeos_museum', [...all, { id: `artifact_${crypto.randomUUID()}`, memoryId: id, preservedAt: new Date().toISOString() }]); toast(t('timeline.added')); } rootModal().innerHTML = ''; renderAll(); };
+ window.confirmDelete = id => { rootModal().innerHTML = `${t('timeline.confirmDelete')}
${t('timeline.remove')}
${t('timeline.irreversible')}
`; };
+ window.deleteMemory = id => { save('lifeos_memories', mems().filter(item => item.id !== id)); save('lifeos_museum', museum().filter(item => item.memoryId !== id)); rootModal().innerHTML = ''; renderAll(); toast(t('timeline.removed')); };
+ window.insights = container => {
+ const all = mems(), recent = all.filter(item => Date.now() - new Date(item.date + 'T12:00:00') < 2592e6), counts = { clear: 0, light: 0, cloudy: 0, rain: 0, storm: 0 }; recent.forEach(item => counts[item.mood]++);
+ const average = all.length ? (all.reduce((sum, item) => sum + item.energy, 0) / all.length).toFixed(1) : '—', dayCounts = {}; all.forEach(item => { const name = weekday(item.date); dayCounts[name] = (dayCounts[name] || 0) + 1; }); const active = Object.entries(dayCounts).sort((a,b) => b[1] - a[1])[0]?.[0] || '—';
+ container.innerHTML = `${t('insights.eyebrow')}
${t('insights.title')}
${t('insights.weather')}
${Object.entries(counts).map(([key, value]) => `${weather(key)} ${value} ${t('common.days')}
`).join('')}${t('insights.average')}
${average} / 5
${all.slice(0,10).reverse().map(item => ``).join('') || `${t('insights.noEntries')}`}
${t('insights.active')}
${active}
${t('insights.current')}
${streak(all)} ${t('common.days')}
${t('insights.longest',{count:longest(all)})}
`;
+ };
+ window.profileApp = container => {
+ const profileData = profile(), all = mems(), common = Object.entries(all.reduce((out, item) => (out[item.mood] = (out[item.mood] || 0) + 1, out), {})).sort((a,b) => b[1] - a[1])[0]?.[0] || '—', average = all.length ? (all.reduce((sum,item) => sum + item.energy,0) / all.length).toFixed(1) : '—', started = Math.max(1, Math.floor((Date.now() - new Date(profileData.startDate)) / 864e5));
+ container.innerHTML = `${t('profile.eyebrow')}
${profileData.name ? esc(profileData.name) + ' · ' : ''}${t('profile.online')}
${started}${t('profile.daysOnline')}
${all.length}${t('profile.memories')}
${museum().length}${t('profile.museumItems')}
${streak(all)}${t('profile.streak')}
${longest(all)}${t('profile.longest')}
${common === '—' ? common : weather(common)}${t('profile.commonWeather')}
${t('profile.personal')}
${t('profile.average',{value:average})}
`;
+ };
+ 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() })]).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.storage')}
${t('system.local')}
${lang() === 'zh' ? `${mems().length} 条记忆 · ${museum().length} 件博物馆藏品 · 约 ${Math.ceil(bytes/1024)} KB` : `${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB`}
${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')}
Life OS
Version 1.1.0 · Schema v1
`;
+ };
+ window.importPreview = () => { const data = importData; rootModal().innerHTML = `${t('dialog.backup')}
${t('dialog.ready')}
${t('dialog.exported',{date:short(data.exportedAt.slice(0,10))})}
${t('dialog.memories',{count:data.memories.length})}
${t('dialog.museumItems',{count:data.museum.length})}
`; };
+ window.replaceConfirm = () => { rootModal().innerHTML = `${t('dialog.final')}
${t('dialog.replaceQuestion')}
${t('dialog.replaceWarning')}
`; };
+ window.resetConfirm = () => { rootModal().innerHTML = `${t('system.reset')}
${t('dialog.resetQuestion')}
${t('dialog.resetWarning')}
`; };
+ window.toast = key => { const message = key.includes('.') ? t(key) : key; const node = document.createElement('div'); node.className = 'toast'; node.textContent = message; document.body.append(node); setTimeout(() => node.remove(), 2000); };
+ window.minWin = id => { const windowNode = windows[id]; if (!windowNode) return; windowNode.classList.add('min'); if (activeWindow === id) { activeWindow = Object.keys(windows).find(key => key !== id && !windows[key].classList.contains('min')) || null; if (activeWindow) focus(windows[activeWindow]); } renderTaskbar(); toast('feedback.minimized'); };
+ // Render every surface again after the overrides are installed; no DOM walking is involved.
+ renderIcons(); renderAll(); applyLanguage();
+})();
diff --git a/public/i18n.js b/public/i18n.js
new file mode 100644
index 0000000..68884de
--- /dev/null
+++ b/public/i18n.js
@@ -0,0 +1,40 @@
+/* Life OS internationalisation: one locale catalogue, no DOM text rewriting. */
+(() => {
+ const en = {
+ app: { today: 'TODAY', timeline: 'TIMELINE', museum: 'MUSEUM', insights: 'INSIGHTS', profile: 'PROFILE', system: 'SYSTEM' },
+ common: { save: 'SAVE', cancel: 'CANCEL', edit: 'EDIT', delete: 'DELETE', close: 'Close', open: 'Open', minimize: 'Minimize', maximize: 'Maximize', restore: 'Restore window', energy: 'ENERGY', days: 'DAYS', never: 'NEVER' },
+ today: { prompt: 'How was your day?', placeholder: 'Write something about today...', archive: 'ARCHIVE TODAY', update: 'UPDATE MEMORY', weather: "TODAY'S WEATHER", energy: 'ENERGY LEVEL', film: "TODAY'S FILM", archiving: 'ARCHIVING MEMORY...', archived: 'MEMORY ARCHIVED' },
+ weather: { clear: 'Clear', light: 'Light', cloudy: 'Cloudy', rain: 'Rain', storm: 'Storm' },
+ timeline: { eyebrow: 'MEMORY ARCHIVE', emptyTitle: 'No memories yet.', emptyBody: 'Open Today when you are ready to keep one.', detail: 'MEMORY DETAIL', noNote: 'No written note.', editing: 'EDITING AS TODAY — SAVE TO UPDATE', preserved: 'PRESERVED', addMuseum: 'ADD TO MUSEUM', confirmDelete: 'CONFIRM DELETE', remove: 'Remove this memory?', irreversible: 'This cannot be undone.', deleteMemory: 'DELETE MEMORY', removed: 'MEMORY REMOVED', added: 'MEMORY PRESERVED' },
+ museum: { eyebrow: 'MUSEUM', title: 'Preserved moments', subtitle: 'Worth keeping for the long view.', empty: 'A museum begins with one preserved moment.', preserved: 'STATUS: PRESERVED' },
+ insights: { eyebrow: 'LAST 30 DAYS', title: 'Insights, quietly.', weather: 'WEATHER', average: 'AVERAGE ENERGY', active: 'MOST ACTIVE DAY', current: 'CURRENT STREAK', longest: 'Longest streak: {count} days', noEntries: 'No entries yet' },
+ profile: { eyebrow: 'SYSTEM PROFILE', online: 'LIFE OS ONLINE', daysOnline: 'DAYS ONLINE', memories: 'MEMORIES', museumItems: 'MUSEUM ITEMS', streak: 'CURRENT STREAK', longest: 'LONGEST STREAK', commonWeather: 'COMMON WEATHER', personal: 'PERSONAL SYSTEM', name: 'Display Name', birthday: 'Birthday (optional)', start: 'Life OS Start Date', motto: 'Personal Motto', save: 'SAVE PROFILE', saved: 'PROFILE SAVED', average: 'AVERAGE ENERGY {value} / 5' },
+ system: { eyebrow: 'SYSTEM SETTINGS', title: 'System, local and yours.', appearance: 'APPEARANCE', language: 'LANGUAGE', browserLanguage: 'BROWSER LANGUAGE', detectedZh: 'Detected: Chinese', detectedEn: 'Detected: English', auto: 'AUTO', data: 'DATA', storage: 'STORAGE', dataStorage: 'DATA STORAGE', export: 'EXPORT DATA', import: 'IMPORT DATA', reset: 'RESET LIFE OS', local: 'Local Storage', stored: 'Your data is stored locally in this browser.', backups: 'Create regular backups to prevent data loss.', lastBackup: 'LAST BACKUP · {value}', recommended: 'BACKUP RECOMMENDED', version: 'VERSION', theme: { dark: 'DARK', light: 'LIGHT', auto: 'AUTO' } },
+ dialog: { backup: 'LIFE OS BACKUP', ready: 'Ready to restore', exported: 'Exported: {date}', memories: 'Memories: {count}', museumItems: 'Museum Items: {count}', merge: 'MERGE', replaceAll: 'REPLACE ALL DATA', final: 'FINAL CONFIRMATION', replaceQuestion: 'Replace all Life OS data?', replaceWarning: 'This will replace all current Life OS data.', replace: 'REPLACE DATA', resetQuestion: 'Reset all local data?', resetWarning: 'Export a backup first if you want to keep it.', resetData: 'RESET DATA' },
+ feedback: { backup: 'BACKUP CREATED', invalid: 'INVALID BACKUP FILE', restored: 'SYSTEM RESTORED', restoring: 'RESTORING SYSTEM...', minimized: 'MINIMIZED — TAP THE APP ICON TO RESTORE', languageZh: 'Language set to Chinese', languageEn: 'Language set to English', followingZh: 'Following browser language: Chinese', followingEn: 'Following browser language: English' },
+ addon: { draftRestored: 'DRAFT RESTORED', searchTitle: 'SEARCH MEMORY ARCHIVE', searchPlaceholder: 'Search titles, notes, moods…', searchHint: 'ESC to close · click a result to open it', untitled: 'Untitled memory', noResults: 'No memories found.', syncPaused: 'CLOUD SYNC PAUSED — CHANGES STAY ON THIS DEVICE', syncReady: 'CLOUD SYNC READY', migrateQuestion: 'Move this browser’s Life OS to the cloud?', migrateBody: 'Your existing local records will become the first synced copy for this ChatGPT account.', migrate: 'MIGRATE & SYNC', fresh: 'START FRESH', migrated: 'LOCAL LIFE OS MIGRATED', cloudRestored: 'CLOUD LIFE OS RESTORED', cloudUnavailable: 'CLOUD STORAGE UNAVAILABLE — LOCAL MODE ACTIVE' }
+ };
+ const zh = {
+ app: { today: '今日', timeline: '时间线', museum: '人生博物馆', insights: '洞察', profile: '个人资料', system: '系统' },
+ common: { save: '保存', cancel: '取消', edit: '编辑', delete: '删除', close: '关闭', open: '打开', minimize: '最小化', maximize: '最大化', restore: '恢复窗口', energy: '能量', days: '天', never: '从未' },
+ today: { prompt: '今天过得怎么样?', placeholder: '写下今天的片段…', archive: '归档今日', update: '更新记忆', weather: '今日天气', energy: '能量等级', film: '今日电影', archiving: '正在归档…', archived: '记忆已归档' },
+ weather: { clear: '晴朗', light: '微光', cloudy: '多云', rain: '雨天', storm: '风暴' },
+ timeline: { eyebrow: '记忆档案', emptyTitle: '还没有记忆。', emptyBody: '准备好记录时,打开“今日”。', detail: '记忆详情', noNote: '尚未写下记录。', editing: '正在以今日记录编辑 — 保存即可更新', preserved: '已珍藏', addMuseum: '加入博物馆', confirmDelete: '确认删除', remove: '删除这条记忆?', irreversible: '此操作无法撤销。', deleteMemory: '删除记忆', removed: '记忆已删除', added: '记忆已珍藏' },
+ museum: { eyebrow: '人生博物馆', title: '被珍藏的瞬间', subtitle: '值得留给漫长回望的片段。', empty: '从一份被珍藏的记忆开始。', preserved: '状态:已珍藏' },
+ insights: { eyebrow: '最近 30 天', title: '安静地看见时间。', weather: '天气', average: '平均能量', active: '最活跃日', current: '当前连续记录', longest: '最长连续记录:{count} 天', noEntries: '暂无记录' },
+ profile: { eyebrow: '系统档案', online: 'LIFE OS 在线', daysOnline: '系统运行天数', memories: '记忆', museumItems: '博物馆藏品', streak: '当前连续记录', longest: '最长连续记录', commonWeather: '常见天气', personal: '个人系统', name: '显示名称', birthday: '生日(选填)', start: 'Life OS 启动日期', motto: '个人格言', save: '保存资料', saved: '资料已保存', average: '平均能量 {value} / 5' },
+ system: { eyebrow: '系统设置', title: '你的个人系统。', appearance: '外观', language: '语言', browserLanguage: '浏览器语言', detectedZh: '当前识别:中文', detectedEn: '当前识别:英语', auto: '跟随浏览器', data: '数据', storage: '存储', dataStorage: '数据存储', export: '导出数据', import: '导入数据', reset: '重置 Life OS', local: '本地存储', stored: '你的数据仅存储在此浏览器中。', backups: '定期备份,让记忆一直在。', lastBackup: '上次备份 · {value}', recommended: '建议备份', version: '版本', theme: { dark: '深色', light: '浅色', auto: '跟随系统' } },
+ dialog: { backup: 'Life OS 备份', ready: '准备恢复', exported: '导出时间:{date}', memories: '记忆:{count}', museumItems: '博物馆藏品:{count}', merge: '合并', replaceAll: '替换全部数据', final: '最终确认', replaceQuestion: '替换全部 Life OS 数据?', replaceWarning: '这将覆盖当前所有 Life OS 数据。', replace: '替换数据', resetQuestion: '重置所有本地数据?', resetWarning: '如需保留数据,请先导出备份。', resetData: '重置数据' },
+ feedback: { backup: '备份已创建', invalid: '无效备份文件', restored: '系统已恢复', restoring: '正在恢复系统…', minimized: '已最小化 — 点击任务栏图标即可恢复', languageZh: '语言已切换为中文', languageEn: 'Language set to English', followingZh: '已跟随浏览器语言:中文', followingEn: 'Following browser language: English' },
+ addon: { draftRestored: '草稿已恢复', searchTitle: '搜索记忆档案', searchPlaceholder: '搜索标题、记录或天气…', searchHint: '按 Esc 关闭 · 点击结果即可打开', untitled: '未命名记忆', noResults: '没有找到记忆。', syncPaused: '云端同步暂停——更改仍保留在此设备', syncReady: '云端同步已就绪', migrateQuestion: '将此浏览器的 Life OS 迁移到云端?', migrateBody: '现有本地记录将成为这个 ChatGPT 账号的第一份云端同步数据。', migrate: '迁移并同步', fresh: '从空白开始', migrated: '本地 Life OS 已迁移', cloudRestored: '云端 Life OS 已恢复', cloudUnavailable: '云端存储暂不可用——已使用本地模式' }
+ };
+ const get = (object, key) => key.split('.').reduce((value, part) => value?.[part], object);
+ function currentLocale() { return typeof window.lang === 'function' ? window.lang() : 'en'; }
+ function t(key, values = {}) {
+ const dictionary = currentLocale() === 'zh' ? zh : en;
+ const fallback = get(en, key) || key;
+ return String(get(dictionary, key) ?? fallback).replace(/\{(\w+)\}/g, (_, name) => values[name] ?? `{${name}}`);
+ }
+ function date(value, options) { return new Intl.DateTimeFormat(currentLocale() === 'zh' ? 'zh-CN' : 'en-US', options).format(new Date(value)); }
+ window.LifeI18n = { t, date, dictionaries: { en, zh } };
+})();
diff --git a/public/index.html b/public/index.html
index dd8eb6f..50543a3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -40,4 +40,4 @@ function streak(a){let s=0,d=new Date();while(a.some(m=>m.date===d.toISOString()
function profileApp(c){let p=profile(),a=mems(),common=Object.entries(a.reduce((o,m)=>(o[m.mood]=(o[m.mood]||0)+1,o),{})).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—',avg=a.length?(a.reduce((x,m)=>x+m.energy,0)/a.length).toFixed(1):'—',started=Math.max(1,Math.floor((Date.now()-new Date(p.startDate))/864e5));c.innerHTML=`SYSTEM PROFILE
${p.name?esc(p.name)+' · ':''}LIFE OS ONLINE
${started}DAYS ONLINE
${a.length}MEMORIES
${museum().length}MUSEUM ITEMS
${streak(a)}CURRENT STREAK
${longest(a)}LONGEST STREAK
${cap(common)}COMMON WEATHER
PERSONAL SYSTEM
AVERAGE ENERGY ${avg} / 5
`}function saveProfile(){save('lifeos_profile',{name:pname.value,birthday:pbday.value,startDate:pstart.value||day(),motto:pmotto.value});renderAll();toast('PROFILE SAVED')}
function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft,cWeek=new Date().toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long'}).toUpperCase();c.innerHTML=`${cWeek}
${fmtDate(day()).replace(/^[^,]+, /,'')}
${i18n('How was your day?')}
`}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}
function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`SYSTEM SETTINGS
System, local and yours.
APPEARANCE
${['dark','light','auto'].map(x=>``).join('')}
STORAGE
Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB
DATA STORAGE
Your data is stored locally in this browser.
Create regular backups to prevent data loss.
${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'
BACKUP RECOMMENDED':''}
VERSION
Life OS
Version 0.1 · Schema v1
`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:'0.1',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||d.schemaVersion!==1||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=`LIFE OS BACKUP
Ready to restore
Exported: ${fmtDate(d.exportedAt.slice(0,10))}
Memories: ${d.memories.length}
Museum Items: ${d.museum.length}
`}function replaceConfirm(){modal.innerHTML=`FINAL CONFIRMATION
Replace all Life OS data?
This will replace all current Life OS data.
`}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=`RESET LIFE OS
Reset all local data?
Export a backup first if you want to keep it.
`}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)}init();
-