Files
Life-OS/public/i18n-renderers.js
T
2026-09-27 10:47:05 +08:00

68 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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.closeModal = () => { rootModal().innerHTML = ''; };
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" 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(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(), alreadyPreserved = all.some(item => item.memoryId === id); if (alreadyPreserved) { save('lifeos_museum', all.filter(item => item.memoryId !== id)); toast(t('timeline.unpreserved')); } else { save('lifeos_museum', [...all, { id: `artifact_${crypto.randomUUID()}`, memoryId: id, preservedAt: new Date().toISOString() }]); toast(t('timeline.added')); } closeModal(); 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();
})();