Files
Life-OS/public/i18n-renderers.js
T
2026-09-25 21:59:01 -05:00

67 lines
15 KiB
JavaScript

/* 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 = `<div class="eyebrow">${weekday(day()).toUpperCase()}</div><h1>${uiDate(day())}</h1><p class="sub">${t('today.prompt')}</p><div class="today-grid"><div><textarea id="entry" class="entry" placeholder="${t('today.placeholder')}" oninput="todayDraft=this.value">${esc(text)}</textarea><div class="actions"><button class="primary" onclick="archiveToday()">${t(memory ? 'today.update' : 'today.archive')}</button><span id="saveStatus" class="status"></span></div></div><aside><div class="eyebrow">${t('today.weather')}</div><div class="choices">${['clear','light','cloudy','rain','storm'].map(item => `<button class="chip ${item === selectedMood ? 'selected' : ''}" onclick="setMood('${item}')">${weather(item)}</button>`).join('')}</div><div class="eyebrow" style="margin-top:22px">${t('today.energy')}</div><div class="energy">${[1,2,3,4,5].map(item => `<button class="${item === selectedEnergy ? 'selected' : ''}" onclick="setEnergy(${item})">${item}</button>`).join('')}</div><div class="film"><div class="eyebrow">${t('today.film')}</div><div class="film-title">${film.filmTitle}</div><div class="sub">${film.filmSubtitle}</div><p class="film-copy">“${film.filmLine}”</p></div></aside></div>`;
};
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 = `<div class="eyebrow">${t('timeline.eyebrow')}</div><h1>${lang() === 'zh' ? `${all.length} 条记忆已归档` : `${all.length} ${all.length === 1 ? 'MEMORY' : 'MEMORIES'} ARCHIVED`}</h1>${all.length ? `<div class="timeline">${all.map(memory => `<article class="memory"><button onclick="detail('${memory.id}')"><div class="date">${short(memory.date)} · ${weather(memory.mood).toUpperCase()} · ${t('common.energy')} ${memory.energy}</div><div class="mtitle">${esc(memory.filmTitle)}</div><div class="snippet">${esc(memory.text || t('timeline.noNote'))}</div></button></article>`).join('')}</div>` : `<div class="empty">${t('timeline.emptyTitle')}<br>${t('timeline.emptyBody')}</div>`}`;
};
window.detail = id => {
const memory = mems().find(item => item.id === id); if (!memory) return;
rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('timeline.detail')}</div><h1>${esc(memory.filmTitle)}</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><p class="film-copy">${memory.filmSubtitle}</p><div class="actions"><button class="primary" onclick="editMemory('${memory.id}')">${t('common.edit')}</button><button class="secondary" onclick="preserve('${memory.id}')">${museum().some(item => item.memoryId === memory.id) ? t('timeline.preserved') : t('timeline.addMuseum')}</button><button class="danger" onclick="confirmDelete('${memory.id}')">${t('common.delete')}</button></div></div></div>`;
};
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 = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('timeline.confirmDelete')}</div><h2>${t('timeline.remove')}</h2><p class="sub">${t('timeline.irreversible')}</p><div class="actions"><button class="danger" onclick="deleteMemory('${id}')">${t('timeline.deleteMemory')}</button><button class="secondary" onclick="modal.innerHTML=''">${t('common.cancel')}</button></div></div></div>`; };
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 = `<div class="eyebrow">${t('insights.eyebrow')}</div><h1>${t('insights.title')}</h1><div class="insights"><section class="metric"><div class="eyebrow">${t('insights.weather')}</div>${Object.entries(counts).map(([key, value]) => `<div class="sub">${weather(key)} <b style="float:right;color:var(--ink)">${value} ${t('common.days')}</b></div>`).join('')}</section><section class="metric"><div class="eyebrow">${t('insights.average')}</div><div class="big">${average}<span style="font-size:16px"> / 5</span></div><div class="bars">${all.slice(0,10).reverse().map(item => `<i class="bar" style="height:${item.energy * 18}%"></i>`).join('') || `<span class="sub">${t('insights.noEntries')}</span>`}</div></section><section class="metric"><div class="eyebrow">${t('insights.active')}</div><div class="big" style="font-size:26px">${active}</div></section><section class="metric"><div class="eyebrow">${t('insights.current')}</div><div class="big">${streak(all)}<span style="font-size:16px"> ${t('common.days')}</span></div><div class="sub">${t('insights.longest',{count:longest(all)})}</div></section></div>`;
};
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 = `<div class="eyebrow">${t('profile.eyebrow')}</div><h1>${profileData.name ? esc(profileData.name) + ' · ' : ''}${t('profile.online')}</h1><div class="profile-grid"><div class="stat"><b>${started}</b><span>${t('profile.daysOnline')}</span></div><div class="stat"><b>${all.length}</b><span>${t('profile.memories')}</span></div><div class="stat"><b>${museum().length}</b><span>${t('profile.museumItems')}</span></div><div class="stat"><b>${streak(all)}</b><span>${t('profile.streak')}</span></div><div class="stat"><b>${longest(all)}</b><span>${t('profile.longest')}</span></div><div class="stat"><b>${common === '—' ? common : weather(common)}</b><span>${t('profile.commonWeather')}</span></div></div><div class="eyebrow">${t('profile.personal')}</div><div class="formrow"><label>${t('profile.name')}</label><input id="pname" value="${esc(profileData.name)}"></div><div class="formrow"><label>${t('profile.birthday')}</label><input id="pbday" type="date" value="${esc(profileData.birthday)}"></div><div class="formrow"><label>${t('profile.start')}</label><input id="pstart" type="date" value="${esc(profileData.startDate)}"></div><div class="formrow"><label>${t('profile.motto')}</label><input id="pmotto" value="${esc(profileData.motto)}"></div><div class="actions"><button class="primary" onclick="saveProfile()">${t('profile.save')}</button><span class="status">${t('profile.average',{value:average})}</span></div>`;
};
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 = `<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} 件博物馆藏品 · 约 ${Math.ceil(bytes/1024)} KB` : `${mems().length} memories · ${museum().length} museum items · 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>Life OS<br>Version 1.1.0 · Schema v1</p></section></div>`;
};
window.importPreview = () => { const data = importData; 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})}</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>`; };
window.resetConfirm = () => { rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('system.reset')}</div><h2>${t('dialog.resetQuestion')}</h2><p class="sub">${t('dialog.resetWarning')}</p><div class="actions"><button class="danger" onclick="['lifeos_memories','lifeos_museum','lifeos_profile','lifeos_settings','lifeos_system'].forEach(k=>localStorage.removeItem(k));location.reload()">${t('dialog.resetData')}</button><button class="secondary" onclick="modal.innerHTML=''">${t('common.cancel')}</button></div></div></div>`; };
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();
})();