fix(i18n): localize all user-facing Life OS text

This commit is contained in:
Codex committed 2026-09-27 14:24:42 +00:00
1 parent 4297f1ee42
commit a95d3652ca
12 files changed
+1057 -110

No files matched your search

+7 -17
View File
@@ -2,8 +2,9 @@
(() => {
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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[char]));
const APP_VERSION = '1.5.1';
const escapeHtml = value => String(value || '').replace(/[&<>]/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
const APP_VERSION = '1.5.2';
window.LifeOSVersion = APP_VERSION;
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system'];
const t = (key, values) => LifeI18n.t(key, values);
@@ -44,7 +45,7 @@
closeSearch();
const box = document.createElement('div');
box.id = 'lifeos-search';
box.innerHTML = `<div class="lifeos-search-backdrop"></div><section class="lifeos-search-panel"><div class="lifeos-search-head"><span>${t('addon.searchTitle')}</span><button aria-label="${t('common.close')}">×</button></div><input autofocus placeholder="${t('addon.searchPlaceholder')}"><div class="lifeos-search-results"></div><p class="lifeos-search-hint">${t('addon.searchHint')}</p></section>`;
box.innerHTML = `<div class="lifeos-search-backdrop"></div><section class="lifeos-search-panel"><div class="lifeos-search-head"><span>${t('addon.searchTitle')}</span><button aria-label="${t('common.close')}">×</button></div><input autofocus aria-label="${t('addon.searchPlaceholder')}" placeholder="${t('addon.searchPlaceholder')}"><div class="lifeos-search-results"></div><p class="lifeos-search-hint">${t('addon.searchHint')}</p></section>`;
document.body.append(box);
const input = box.querySelector('input');
const results = box.querySelector('.lifeos-search-results');
@@ -53,7 +54,7 @@
try { memories = JSON.parse(localStorage.getItem('lifeos_memories') || '[]'); } catch {}
const query = input.value.trim().toLowerCase();
const found = memories.filter(m => !query || [m.title, m.filmTitle, m.text, m.mood, m.date].join(' ').toLowerCase().includes(query)).slice(0, 8);
results.innerHTML = found.length ? found.map(m => `<button data-id="${m.id}"><small>${m.date} · ${t(`weather.${m.mood || 'cloudy'}`).toUpperCase()}</small><strong>${escapeHtml(m.title || m.text?.trim().split('\n')[0].slice(0, 64) || t('timeline.untitled'))}</strong><span>${(m.text || t('timeline.noNote')).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]))}</span></button>`).join('') : `<div class="lifeos-search-empty">${t('addon.noResults')}</div>`;
results.innerHTML = found.length ? found.map(m => `<button data-id="${m.id}" aria-label="${t('addon.openMemory',{title:escapeHtml(m.title || m.text?.trim().split('\n')[0].slice(0, 64) || t('timeline.untitled'))})}"><small>${LifeI18n.date(m.date,{dateStyle:'medium'})} · ${t(`weather.${m.mood || 'cloudy'}`).toUpperCase()}</small><strong>${escapeHtml(m.title || m.text?.trim().split('\n')[0].slice(0, 64) || t('timeline.untitled'))}</strong><span>${(m.text || t('timeline.noNote')).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]))}</span></button>`).join('') : `<div class="lifeos-search-empty">${t('addon.noResults')}</div>`;
results.querySelectorAll('button').forEach(button => button.addEventListener('click', () => { closeSearch(); openApp('timeline'); setTimeout(() => detail(button.dataset.id), 20); }));
};
input.addEventListener('input', render);
@@ -65,17 +66,7 @@
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}.launchitem svg{width:28px;height:28px;display:block;margin:auto;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.launchitem:hover svg{color:var(--accent)}`;
document.head.append(style);
function polishSystemChrome() {
document.querySelector('#desktop')?.removeAttribute('title');
document.querySelectorAll('.systemline').forEach(node => {
const next = node.textContent.replace(/LIFE OS v0\.1/g, `LIFE OS v${APP_VERSION}`);
if (next !== node.textContent) node.textContent = next;
});
document.querySelectorAll('#content-system .settings section:last-child p').forEach(node => {
const next = node.innerHTML.replace(/Version 0\.1/g, `Version ${APP_VERSION}`).replace(/版本 0\.1/g, `版本 ${APP_VERSION}`);
if (next !== node.innerHTML) node.innerHTML = next;
});
}
function exportData() {
const data = { app: 'Life OS', version: APP_VERSION, schemaVersion: 2, exportedAt: new Date().toISOString(), profile: profile(), memories: mems(), museum: museum(), settings: settings(), system: sys() };
const link = document.createElement('a');
@@ -165,10 +156,9 @@
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(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title'] });
wireToday();
wireLanguageControl();
polishSystemChrome();
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');
+10 -4
View File
@@ -10,6 +10,7 @@
const linkedTasks = () => sys().tasksBoard?.tasks || [];
const escAttr = value => esc(value).replace(/\"/g, '&quot;');
const titleOf = memory => memory.title?.trim() || memory.text?.trim().split('\n')[0].slice(0, 64) || t('timeline.untitled');
const localizedFilmField = (memory, field) => { const index = LifeI18n.dictionaries.en.prompt.films.findIndex(film => film.title === memory.filmTitle); return index < 0 ? (memory[field] || '') : (LifeI18n.dictionaries[lang() === 'zh' ? 'zh' : 'en'].prompt.films[index]?.[field === 'filmTitle' ? 'title' : 'subtitle'] || memory[field] || ''); };
const tagList = value => [...new Set(String(value || '').split(/[,,]/).map(tag => tag.trim()).filter(Boolean))].slice(0, 12);
const memoryMetaFields = memory => `<div class="memory-meta-form"><label>${t('memory.tags')}<input id="memory-tags" maxlength="180" value="${esc((memory?.tags || []).join(', '))}" placeholder="${t('memory.tagsPlaceholder')}"></label><label>${t('memory.place')}<input id="memory-place" maxlength="100" value="${esc(memory?.location || '')}" placeholder="${t('memory.placePlaceholder')}"></label><label>${t('memory.linkTask')}<select id="memory-task"><option value="">${t('memory.noTask')}</option>${linkedTasks().map(task => `<option value="${esc(task.id)}" ${task.id === memory?.taskId ? 'selected' : ''}>${esc(task.title)}</option>`).join('')}</select></label></div>`;
@@ -19,10 +20,11 @@
window.shortDate = short;
window.addMemoryForDate = date => { todayTargetDate = date; todayDraft = null; todayTitleDraft = null; openApp('today'); };
window.closeModal = () => { rootModal().innerHTML = ''; };
window.applyLanguage = () => { document.documentElement.lang = lang() === 'zh' ? 'zh-CN' : 'en'; document.title = lang() === 'zh' ? 'Life OS / 人生操作系统' : 'Life OS'; };
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(); state.language = value; state.languageMode = value === 'auto' ? 'auto' : 'manual'; save('lifeos_settings', state);
renderIcons(); renderAll();
renderIcons(); renderAll(); applyLanguage();
toast(value === 'auto' ? t(lang() === 'zh' ? 'feedback.followingZh' : 'feedback.followingEn') : t(value === 'zh' ? 'feedback.languageZh' : 'feedback.languageEn'));
};
window.today = container => {
@@ -48,12 +50,16 @@
const isSaved = memory => museum().some(item => item.memoryId === memory.id);
container.innerHTML = `<div class="eyebrow">${t('timeline.eyebrow')}</div><h1>${t('timeline.heading',{count:filtered.length,total:all.length})}</h1><div class="timeline-tools"><label class="timeline-search"><span class="sr-only">${t('timeline.search')}</span><input type="search" value="${esc(query)}" placeholder="${t('timeline.searchPlaceholder')}" oninput="setTimelineQuery(this.value)"></label><label class="timeline-tag-filter"><span class="sr-only">${t('timeline.filterTag')}</span><select onchange="setTimelineTag(this.value)"><option value="">${t('timeline.allTags')}</option>${tags.map(tag => `<option value="${esc(tag)}" ${tag === selectedTag ? 'selected' : ''}>${esc(tag)}</option>`).join('')}</select></label></div>${filtered.length ? `<div class="timeline">${filtered.map(memory => { const task = linkedTasks().find(item => item.id === memory.taskId); return `<article class="memory"><button class="memory-open" onclick="detail('${esc(memory.id)}')"><div class="date">${short(memory.date)} · ${weather(memory.mood).toUpperCase()} · ${t('common.energy')} ${memory.energy}${isSaved(memory) ? ` · ★ ${t('timeline.favorite')}` : ''}</div><div class="mtitle">${esc(titleOf(memory))}</div><div class="snippet">${esc(memory.text || t('timeline.noNote'))}</div><div class="memory-badges">${memory.location ? `<span>⌖ ${esc(memory.location)}</span>` : ''}${task ? `<span>↗ ${esc(task.title)}</span>` : ''}${(memory.tags || []).map(tag => `<span># ${esc(tag)}</span>`).join('')}</div></button></article>`; }).join('')}</div>` : `<div class="empty">${all.length ? t('timeline.noMatches') : `${t('timeline.emptyTitle')}<br>${t('timeline.emptyBody')}`}</div>`}`;
};
window.museumApp = container => {
const saved = museum(), memories = mems();
container.innerHTML = `<div class="eyebrow">${t('museum.eyebrow')}</div><h1>${t('museum.title')}</h1><p class="sub">${t('museum.subtitle')}</p>${saved.length ? `<div class="museum">${saved.map((artifact,index) => { const memory = memories.find(item => item.id === artifact.memoryId); if (!memory) return ''; return `<button class="artifact" onclick="detail('${esc(memory.id)}')"><span>${t('museum.collection')} #${String(index + 1).padStart(2,'0')}</span><b>${esc(titleOf(memory))}</b><small>${LifeI18n.date(memory.date,{dateStyle:'medium'})} · ${t('museum.preserved')}</small></button>`; }).join('')}</div>` : `<div class="empty">${t('museum.empty')}</div>`}`;
};
window.setTimelineQuery = value => { window.timelineQuery = value; const field = document.querySelector('#content-timeline input[type=search]'); const start = field?.selectionStart; timeline(document.querySelector('#content-timeline')); const next = document.querySelector('#content-timeline input[type=search]'); next?.focus(); if (start !== undefined) next?.setSelectionRange(start,start); };
window.setTimelineTag = value => { window.timelineTag = value; timeline(document.querySelector('#content-timeline')); };
window.detail = id => {
const memory = mems().find(item => item.id === id); if (!memory) return;
const task = linkedTasks().find(item => item.id === memory.taskId), favorite = museum().some(item => item.memoryId === memory.id);
rootModal().innerHTML = `<div class="modal" onclick="if(event.target === this) closeModal()"><div class="modalbox" role="dialog" aria-modal="true" aria-labelledby="memory-detail-title"><button class="modal-close" type="button" onclick="closeModal()" aria-label="${t('common.close')}">×</button><div class="eyebrow">${t('timeline.detail')}</div><h1 id="memory-detail-title">${esc(titleOf(memory))}</h1><p class="sub">${uiDate(memory.date)} · ${weather(memory.mood)} · ${t('common.energy')} ${memory.energy}</p><p style="line-height:1.65">${esc(memory.text || t('timeline.noNote')).replace(/\n/g,'<br>')}</p>${memory.location ? `<p class="memory-detail-meta">⌖ ${esc(memory.location)}</p>` : ''}${task ? `<p class="memory-detail-meta">↗ ${esc(t('memory.relatedTask'))}: <button class="text-link" onclick="closeModal();openApp('tasks')">${esc(task.title)}</button></p>` : ''}${(memory.tags || []).length ? `<div class="memory-badges detail-badges">${memory.tags.map(tag => `<span># ${esc(tag)}</span>`).join('')}</div>` : ''}<p class="film-copy"><b>${t('today.film')}: ${esc(memory.filmTitle || '')}</b><br>${esc(memory.filmSubtitle || '')}</p><div class="actions"><button class="primary" onclick="editMemory('${esc(memory.id)}')">${t('common.edit')}</button><button class="secondary" onclick="preserve('${esc(memory.id)}')">${favorite ? t('timeline.preserved') : t('memory.favorite')}</button><button class="danger" onclick="confirmDelete('${esc(memory.id)}')">${t('common.delete')}</button></div></div></div>`;
rootModal().innerHTML = `<div class="modal" onclick="if(event.target === this) closeModal()"><div class="modalbox" role="dialog" aria-modal="true" aria-labelledby="memory-detail-title"><button class="modal-close" type="button" onclick="closeModal()" aria-label="${t('common.close')}">×</button><div class="eyebrow">${t('timeline.detail')}</div><h1 id="memory-detail-title">${esc(titleOf(memory))}</h1><p class="sub">${uiDate(memory.date)} · ${weather(memory.mood)} · ${t('common.energy')} ${memory.energy}</p><p style="line-height:1.65">${esc(memory.text || t('timeline.noNote')).replace(/\n/g,'<br>')}</p>${memory.location ? `<p class="memory-detail-meta">⌖ ${esc(memory.location)}</p>` : ''}${task ? `<p class="memory-detail-meta">↗ ${esc(t('memory.relatedTask'))}: <button class="text-link" onclick="closeModal();openApp('tasks')">${esc(task.title)}</button></p>` : ''}${(memory.tags || []).length ? `<div class="memory-badges detail-badges">${memory.tags.map(tag => `<span># ${esc(tag)}</span>`).join('')}</div>` : ''}<p class="film-copy"><b>${t('today.film')}: ${esc(localizedFilmField(memory,'filmTitle'))}</b><br>${esc(localizedFilmField(memory,'filmSubtitle'))}</p><div class="actions"><button class="primary" onclick="editMemory('${esc(memory.id)}')">${t('common.edit')}</button><button class="secondary" onclick="preserve('${esc(memory.id)}')">${favorite ? t('timeline.preserved') : t('memory.favorite')}</button><button class="danger" onclick="confirmDelete('${esc(memory.id)}')">${t('common.delete')}</button></div></div></div>`;
};
window.editMemory = id => { const memory = mems().find(item => item.id === id); if (!memory) return; rootModal().innerHTML = `<div class="modal" onclick="if(event.target===this) closeModal()"><form class="modalbox memory-edit-form" onsubmit="saveMemoryEdit(event,'${esc(memory.id)}')" role="dialog" aria-modal="true" aria-labelledby="memory-edit-title"><button type="button" class="modal-close" onclick="closeModal()" aria-label="${t('common.close')}">×</button><div class="eyebrow">${t('timeline.detail')}</div><h2 id="memory-edit-title">${t('memory.edit')}</h2><p class="sub">${uiDate(memory.date)}</p><label>${t('memory.title')}<input id="memory-edit-title" maxlength="120" value="${escAttr(memory.title || '')}" placeholder="${t('memory.titlePlaceholder')}"></label><label>${t('memory.note')}<textarea id="memory-edit-note" rows="5">${esc(memory.text || '')}</textarea></label>${memoryMetaFields(memory)}<div class="actions"><button class="primary" type="submit">${t('common.save')}</button><button class="secondary" type="button" onclick="closeModal()">${t('common.cancel')}</button></div></form></div>`; };
window.saveMemoryEdit = (event,id) => { event.preventDefault(); const all=mems(), memory=all.find(item=>item.id===id); if(!memory)return; memory.title=document.querySelector('#memory-edit-title').value.trim(); memory.text=document.querySelector('#memory-edit-note').value.trim(); memory.tags=tagList(document.querySelector('#memory-tags').value); memory.location=document.querySelector('#memory-place').value.trim(); memory.taskId=document.querySelector('#memory-task').value||''; memory.updatedAt=new Date().toISOString(); save('lifeos_memories',all); closeModal(); renderAll(); detail(id); };
@@ -72,7 +78,7 @@
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 = `<div class="eyebrow">${t('system.eyebrow')}</div><h1>${t('system.title')}</h1><div class="settings"><section><div class="eyebrow">${t('system.appearance')}</div><div class="choices">${['dark','light','auto'].map(item => `<button class="chip ${state.theme === item ? 'selected' : ''}" onclick="theme('${item}')">${t(`system.theme.${item}`)}</button>`).join('')}</div></section><section><div class="eyebrow">${t('system.language')}</div><p class="sub">${detected}</p><div class="choices"><button class="chip ${state.languageMode !== 'manual' ? 'selected' : ''}" onclick="setLanguage('auto')">${t('system.auto')}</button><button class="chip ${state.languageMode === 'manual' && state.language === 'en' ? 'selected' : ''}" onclick="setLanguage('en')">ENGLISH</button><button class="chip ${state.languageMode === 'manual' && state.language === 'zh' ? 'selected' : ''}" onclick="setLanguage('zh')">中文</button></div></section><section><div class="eyebrow">${t('system.data')}</div><div class="actions"><button class="primary" onclick="exportData()">${t('system.export')}</button><button class="secondary" onclick="document.querySelector('#importer').click()">${t('system.import')}</button><button class="danger" onclick="resetConfirm()">${t('system.reset')}</button><input id="importer" class="hidden" type="file" accept=".json" onchange="readImport(this)"></div></section><section><div class="eyebrow">${t('system.storage')}</div><p>${t('system.local')}<br>${lang() === 'zh' ? `${mems().length} 条记忆 · ${museum().length} 件博物馆藏品 · ${sys().tasksBoard?.tasks?.length || 0} 项待办 · 约 ${Math.ceil(bytes/1024)} KB` : `${mems().length} memories · ${museum().length} museum items · ${sys().tasksBoard?.tasks?.length || 0} tasks · Approx. ${Math.ceil(bytes/1024)} KB`}</p></section><section><div class="eyebrow">${t('system.dataStorage')}</div><p>${t('system.stored')}<br>${t('system.backups')}</p><p>${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `<br><b style="color:var(--accent)">${t('system.recommended')}</b>` : ''}</p></section><section><div class="eyebrow">${t('system.version')}</div><p>${lang() === 'zh' ? 'Life OS<br>版本 1.5.0 · 备份格式 v2 · 数据库结构 v1' : 'Life OS<br>Version 1.5.0 · Backup schema v2 · D1 schema v1'}</p></section></div>`;
container.innerHTML = `<div class="eyebrow">${t('system.eyebrow')}</div><h1>${t('system.title')}</h1><div class="settings"><section><div class="eyebrow">${t('system.appearance')}</div><div class="choices">${['dark','light','auto'].map(item => `<button class="chip ${state.theme === item ? 'selected' : ''}" onclick="theme('${item}')">${t(`system.theme.${item}`)}</button>`).join('')}</div></section><section><div class="eyebrow">${t('system.language')}</div><p class="sub">${detected}</p><div class="choices"><button class="chip ${state.languageMode !== 'manual' ? 'selected' : ''}" onclick="setLanguage('auto')">${t('system.auto')}</button><button class="chip ${state.languageMode === 'manual' && state.language === 'en' ? 'selected' : ''}" onclick="setLanguage('en')">${t('system.languageEnglish')}</button><button class="chip ${state.languageMode === 'manual' && state.language === 'zh' ? 'selected' : ''}" onclick="setLanguage('zh')">${t('system.languageChinese')}</button></div></section><section><div class="eyebrow">${t('system.data')}</div><div class="actions"><button class="primary" onclick="exportData()">${t('system.export')}</button><button class="secondary" onclick="document.querySelector('#importer').click()">${t('system.import')}</button><button class="danger" onclick="resetConfirm()">${t('system.reset')}</button><input id="importer" class="hidden" type="file" accept=".json" onchange="readImport(this)"></div></section><section><div class="eyebrow">${t('system.storage')}</div><p>${t('system.local')}<br>${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))})}</p></section><section><div class="eyebrow">${t('system.dataStorage')}</div><p>${t('system.stored')}<br>${t('system.backups')}</p><p>${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `<br><b style="color:var(--accent)">${t('system.recommended')}</b>` : ''}</p></section><section><div class="eyebrow">${t('system.version')}</div><p>${t('system.release',{version:LifeOSVersion||'1.5.2'})}</p></section></div>`;
};
window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('dialog.backup')}</div><h2>${t('dialog.ready')}</h2><p class="sub">${t('dialog.exported',{date:short(data.exportedAt.slice(0,10))})}<br>${t('dialog.memories',{count:data.memories.length})}<br>${t('dialog.museumItems',{count:data.museum.length})}<br>${t('dialog.goals',{count:board.goals?.length || 0})}<br>${t('dialog.tasks',{count:board.tasks?.length || 0})}</p><div class="actions"><button class="primary" onclick="applyImport('merge')">${t('dialog.merge')}</button><button class="danger" onclick="replaceConfirm()">${t('dialog.replaceAll')}</button><button class="secondary" onclick="modal.innerHTML=''">${t('common.cancel')}</button></div></div></div>`; };
window.replaceConfirm = () => { rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('dialog.final')}</div><h2>${t('dialog.replaceQuestion')}</h2><p class="sub">${t('dialog.replaceWarning')}</p><div class="actions"><button class="danger" onclick="applyImport('replace')">${t('dialog.replace')}</button><button class="secondary" onclick="importPreview()">${t('common.cancel')}</button></div></div></div>`; };
+18 -42
View File
@@ -1,46 +1,22 @@
/* Life OS internationalisation: one locale catalogue, no DOM text rewriting. */
/* Translation lookup and locale-aware formatting. Copy lives in /locales/*.json. */
(() => {
const en = {
app: { today: 'TODAY', timeline: 'TIMELINE', museum: 'MUSEUM', insights: 'INSIGHTS', profile: 'PROFILE', tasks: 'TASKS', calendar: 'CALENDAR', 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', archiveDate: 'SAVE MEMORY', updateDate: 'UPDATE MEMORY', backToToday: 'Back to today', weather: "TODAY'S WEATHER", energy: 'ENERGY LEVEL', film: 'DAILY PROMPT', archiving: 'ARCHIVING MEMORY...', archived: 'MEMORY ARCHIVED' },
weather: { clear: 'Clear', light: 'Light', cloudy: 'Cloudy', rain: 'Rain', storm: 'Storm' },
timeline: { eyebrow: 'MEMORY ARCHIVE', heading: '{count} OF {total} MEMORIES', search: 'Search memories', searchPlaceholder: 'Search notes, tags, places, tasks…', filterTag: 'Filter by tag', allTags: 'ALL TAGS', noMatches: 'No memories match these filters.', favorite: 'FAVORITE', emptyTitle: 'No memories yet.', emptyBody: 'Open Today when you are ready to keep one.', detail: 'MEMORY DETAIL', untitled: 'Untitled memory', noNote: 'No written note.', editing: 'EDITING AS TODAY — SAVE TO UPDATE', preserved: 'FAVORITED', addMuseum: 'ADD TO MUSEUM', confirmDelete: 'CONFIRM DELETE', remove: 'Remove this memory?', irreversible: 'This cannot be undone.', deleteMemory: 'DELETE MEMORY', removed: 'MEMORY REMOVED', added: 'MEMORY FAVORITED', unpreserved: 'REMOVED FROM FAVORITES' },
memory: { tags: 'TAGS', tagsPlaceholder: 'Separate tags with commas', place: 'PLACE / SCENE (OPTIONAL)', placePlaceholder: 'Where or what was the setting?', linkTask: 'RELATED TASK (OPTIONAL)', noTask: 'NO RELATED TASK', relatedTask: 'Related task', favorite: 'ADD TO FAVORITES', edit: 'EDIT MEMORY DETAILS', note: 'NOTE', title: 'TITLE (OPTIONAL)', titlePlaceholder: 'Give this memory a title' },
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}', goals: 'Goals: {count}', tasks: 'Tasks: {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' },
calendar: { eyebrow: 'MEMORIES & MILESTONES', month: 'MONTH', day: 'DAY', today: 'TODAY', mon: 'MON', tue: 'TUE', wed: 'WED', thu: 'THU', fri: 'FRI', sat: 'SAT', sun: 'SUN', previous: 'Previous month', next: 'Next month', previousDay: 'Previous day', nextDay: 'Next day', dayAtGlance: 'THAT DAY', memory: 'MEMORY', task: 'TASK', goal: 'GOAL', items: 'items', taskDue: 'TASK DUE', taskDone: 'TASK COMPLETED', goalCreated: 'GOAL CREATED', goalTarget: 'GOAL TARGET DATE', goalDone: 'GOAL COMPLETED', emptyDay: 'No memories or milestones recorded for this day.', openToday: 'Write a memory', writeMemory: 'Add memory', openTasks: 'Open tasks' },
tasks: { eyebrow: 'GOALS & TASKS', title: 'Make room for what matters.', subtitle: 'A small, steady plan for today and the days ahead.', addGoal: 'NEW GOAL', addTask: 'ADD TASK', taskPlaceholder: 'What needs doing?', goalPlaceholder: 'What do you want to move toward?', goalDescription: 'A note about this goal (optional)', finishedGoals: 'COMPLETED GOALS', goalComplete: 'COMPLETE GOAL', goalReopen: 'REOPEN GOAL', all: 'ALL', today: 'TODAY', upcoming: 'UPCOMING', done: 'DONE', activeGoals: 'ACTIVE GOALS', noGoals: 'Start with one goal worth moving toward.', noTasks: 'Nothing on this list yet.', due: 'DUE {date}', noDate: 'NO DUE DATE', priority: { low: 'LOW', normal: 'NORMAL', high: 'HIGH' }, goal: 'GOAL', anyGoal: 'NO GOAL', save: 'SAVE', cancel: 'CANCEL', complete: 'MARK COMPLETE', reopen: 'REOPEN', delete: 'DELETE', goalTitle: 'New goal', taskTitle: 'New task', goalCreated: 'GOAL CREATED', taskAdded: 'TASK ADDED', taskRemoved: 'TASK REMOVED', goalRemoved: 'GOAL REMOVED', confirmGoalDelete: 'Delete this goal?', confirmGoalDeleteBody: 'Tasks linked to it will be kept without a goal.', target: 'TARGET DATE', completed: 'COMPLETED', progress: '{done} OF {total} DONE', desktopShow: 'SHOW COUNTDOWN', desktopHide: 'HIDE COUNTDOWN', countdown: 'GOAL COUNTDOWN', daysLeft: '{count} days left', todayTarget: 'Target is today', daysOverdue: '{count} days overdue', pinOnCreate: 'Show countdown on desktop', todayHeading: 'Focus for today', openCount: '{count} OPEN TASKS', overdue: 'OVERDUE' }
};
const zh = {
app: { today: '今日', timeline: '时间线', museum: '人生博物馆', insights: '洞察', profile: '个人资料', tasks: '目标与待办', calendar: '日历', system: '系统' },
common: { save: '保存', cancel: '取消', edit: '编辑', delete: '删除', close: '关闭', open: '打开', minimize: '最小化', maximize: '最大化', restore: '恢复窗口', energy: '能量', days: '天', never: '从未' },
today: { prompt: '今天过得怎么样?', placeholder: '写下今天的片段…', archive: '归档今日', update: '更新记忆', archiveDate: '保存记忆', updateDate: '更新记忆', backToToday: '返回今天', weather: '今日天气', energy: '能量等级', film: '今日灵感', archiving: '正在归档…', archived: '记忆已归档' },
weather: { clear: '晴朗', light: '微光', cloudy: '多云', rain: '雨天', storm: '风暴' },
timeline: { eyebrow: '记忆档案', heading: '{count} / {total} 条记忆', search: '搜索记忆', searchPlaceholder: '搜索记录、标签、地点或待办…', filterTag: '按标签筛选', allTags: '全部标签', noMatches: '没有符合筛选条件的记忆。', favorite: '已收藏', emptyTitle: '还没有记忆。', emptyBody: '准备好记录时,打开“今日”。', detail: '记忆详情', untitled: '当天记录', noNote: '尚未写下记录。', editing: '正在以今日记录编辑 — 保存即可更新', preserved: '已收藏', addMuseum: '加入收藏', confirmDelete: '确认删除', remove: '删除这条记忆?', irreversible: '此操作无法撤销。', deleteMemory: '删除记忆', removed: '记忆已删除', added: '记忆已收藏', unpreserved: '已取消收藏' },
memory: { tags: '标签', tagsPlaceholder: '多个标签请用逗号分隔', place: '地点 / 场景(选填)', placePlaceholder: '在哪里,或当时是什么场景?', linkTask: '关联待办(选填)', noTask: '不关联待办', relatedTask: '关联待办', favorite: '加入收藏', edit: '编辑记忆详情', note: '记录内容', title: '标题(选填)', titlePlaceholder: '为这条记忆取个标题' },
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}', goals: '目标:{count}', tasks: '待办:{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: '云端存储暂不可用——已使用本地模式' },
calendar: { eyebrow: '记忆与生活节点', month: '月', day: '日', today: '今天', mon: '一', tue: '二', wed: '三', thu: '四', fri: '五', sat: '六', sun: '日', previous: '上个月', next: '下个月', previousDay: '前一天', nextDay: '后一天', dayAtGlance: '这一天', memory: '记忆', task: '待办', goal: '目标', items: '项', taskDue: '待办截止', taskDone: '完成待办', goalCreated: '创建目标', goalTarget: '目标日期', goalDone: '完成目标', emptyDay: '这一天还没有记忆或目标节点。', openToday: '写下一段记忆', writeMemory: '新增记忆', openTasks: '打开待办' },
tasks: { eyebrow: '目标与待办', title: '把心力留给重要的事。', subtitle: '为今天和接下来的日子,排一个轻量、踏实的计划。', addGoal: '新建目标', addTask: '添加待办', taskPlaceholder: '接下来要做什么?', goalPlaceholder: '你想朝什么方向前进?', goalDescription: '关于这个目标的备注(选填)', finishedGoals: '已完成的目标', goalComplete: '完成目标', goalReopen: '重新开启目标', all: '全部', today: '今天', upcoming: '即将开始', done: '已完成', activeGoals: '进行中的目标', noGoals: '从一个值得继续靠近的目标开始。', noTasks: '这里暂时没有待办。', due: '截止 {date}', noDate: '无截止日期', priority: { low: '低', normal: '普通', high: '高' }, goal: '目标', anyGoal: '不关联目标', save: '保存', cancel: '取消', goalTitle: '新建目标', taskTitle: '新建待办', goalCreated: '目标已创建', taskAdded: '待办已添加', taskRemoved: '待办已删除', goalRemoved: '目标已删除', confirmGoalDelete: '删除这个目标?', confirmGoalDeleteBody: '关联的待办会保留,但不再关联此目标。', target: '目标日期', completed: '已完成', complete: '标记完成', reopen: '重新打开', delete: '删除', progress: '已完成 {done} / {total}', desktopShow: '显示倒计时', desktopHide: '隐藏倒计时', countdown: '目标倒计时', daysLeft: '还有 {count} 天', todayTarget: '目标就在今天', daysOverdue: '已过期 {count} 天', pinOnCreate: '在桌面显示倒计时', todayHeading: '今天要推进的事', openCount: '还有 {count} 项待办', overdue: '已逾期' }
};
const get = (object, key) => key.split('.').reduce((value, part) => value?.[part], object);
function currentLocale() { return typeof window.lang === 'function' ? window.lang() : 'en'; }
const dictionaries = window.LifeOSLocales || {};
const en = dictionaries.en || {};
const zh = dictionaries['zh-CN'] || {};
const lookup = (object, path) => path.split('.').reduce((value, part) => value?.[part], object);
const current = () => typeof window.lang === 'function' && window.lang() === 'zh' ? 'zh' : '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}}`);
const selected = current() === 'zh' ? zh : en;
const template = lookup(selected, key) ?? lookup(en, key) ?? key;
return String(template).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 } };
function locale() { return current() === '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 } };
})();
Loaded 3 of 12 files, more files were not shown because too many files have changed in this diff. Show more