feat(i18n): expand Life OS to ten locales
This commit is contained in:
1 parent
a88a7f550f
commit
87276a051e
25 files changed
+6080
-87
No files matched your search
+1
-1
@@ -3,7 +3,7 @@
|
||||
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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
||||
const APP_VERSION = '1.16.0';
|
||||
const APP_VERSION = '1.17.0';
|
||||
window.LifeOSVersion = APP_VERSION;
|
||||
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser', 'terminal', 'database'];
|
||||
const t = (key, values) => LifeI18n.t(key, values);
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
let selected = localKey(new Date()), month = new Date(`${selected}T12:00:00`), view = 'month';
|
||||
const fromKey = key => new Date(`${key}T12:00:00`);
|
||||
const esc = value => String(value || '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
||||
const formatDate = (key, options = { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }) => new Intl.DateTimeFormat(lang() === 'zh' ? 'zh-CN' : 'en-US', options).format(fromKey(key));
|
||||
const formatDate = (key, options = { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }) => new Intl.DateTimeFormat(LifeI18n.locale(), options).format(fromKey(key));
|
||||
const localDatePart = value => value ? localKey(new Date(value)) : '';
|
||||
const system = () => { try { return JSON.parse(localStorage.getItem('lifeos_system') || '{}'); } catch { return {}; } };
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
}
|
||||
|
||||
function render(container) {
|
||||
const title = new Intl.DateTimeFormat(lang() === 'zh' ? 'zh-CN' : 'en-US', { month: 'long', year: 'numeric' }).format(month);
|
||||
const title = new Intl.DateTimeFormat(LifeI18n.locale(), { month: 'long', year: 'numeric' }).format(month);
|
||||
const heading = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'].map((key, index) => `<span class="cal-weekday ${index > 4 ? 'weekend' : ''}">${t(key)}</span>`).join('');
|
||||
container.innerHTML = `<div class="eyebrow">${t('eyebrow')}</div><header class="cal-toolbar"><div class="cal-title"><button class="cal-nav" aria-label="${t('previous')}" onclick="moveCalendarMonth(-1)">‹</button><h1>${esc(title)}</h1><button class="cal-nav" aria-label="${t('next')}" onclick="moveCalendarMonth(1)">›</button></div><div class="cal-toolbar-actions"><button class="secondary cal-today" onclick="jumpCalendarToday()">${t('today')}</button><div class="cal-view-switch"><button class="${view === 'month' ? 'active' : ''}" aria-pressed="${view === 'month'}" onclick="setCalendarView('month')">${t('month')}</button><button class="${view === 'day' ? 'active' : ''}" aria-pressed="${view === 'day'}" onclick="setCalendarView('day')">${t('day')}</button></div></div></header>${view === 'month' ? `<div class="cal-month-layout"><section class="cal-month"><div class="cal-grid cal-weekdays">${heading}</div><div class="cal-grid cal-days">${monthCells()}</div><div class="cal-legend"><span><i class="memory"></i>${t('memory')}</span><span><i class="task"></i>${t('task')}</span><span><i class="goal"></i>${t('goal')}</span></div></section>${selectedDayMarkup()}</div>` : `<section class="cal-single-day">${selectedDayMarkup()}<div class="cal-day-navigate"><button class="secondary" onclick="moveCalendarDay(-1)">← ${t('previousDay')}</button><button class="secondary" onclick="moveCalendarDay(1)">${t('nextDay')} →</button></div></section>`}`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* 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 locale = () => LifeI18n.locale();
|
||||
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();
|
||||
@@ -10,7 +10,7 @@
|
||||
const linkedTasks = () => sys().tasksBoard?.tasks || [];
|
||||
const escAttr = value => esc(value).replace(/\"/g, '"');
|
||||
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 localizedFilmField = (memory, field) => { const index = LifeI18n.supported.reduce((found, code) => found >= 0 ? found : LifeI18n.dictionaries[code]?.prompt.films.findIndex(film => film.title === memory.filmTitle), -1); return index < 0 ? (memory[field] || '') : (LifeI18n.dictionaries[lang()]?.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>`;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
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 = 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.applyLanguage = () => { document.documentElement.lang = LifeI18n.locale(); document.documentElement.dir = lang() === 'ar' ? 'rtl' : 'ltr'; 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.today = container => {
|
||||
const entryDate = todayTargetDate || day(), memory = mems().find(item => item.date === entryDate);
|
||||
@@ -109,8 +109,8 @@
|
||||
};
|
||||
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(), preference = LifeI18n.preference(), 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 data-settings-group="appearance"><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 data-settings-group="language"><div class="eyebrow">${t('system.language')}</div><p class="sub">${detected}</p><div class="choices"><button class="chip ${preference === 'auto' ? 'selected' : ''}" onclick="window.setLanguage('auto')">${t('system.auto')}</button><button class="chip ${preference === 'en' ? 'selected' : ''}" onclick="window.setLanguage('en')">${t('system.languageEnglish')}</button><button class="chip ${preference === 'zh' ? 'selected' : ''}" onclick="window.setLanguage('zh')">${t('system.languageChinese')}</button></div></section><section class="notification-settings" data-settings-group="notifications">${window.LifeOSNotifications?.markup() || ''}</section><section data-settings-group="data"><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 data-settings-group="data"><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 data-settings-group="data"><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 data-settings-group="about"><div class="eyebrow">${t('system.version')}</div><p>${t('system.release',{version:window.LifeOSVersion||'1.16.0'})}</p></section></div>`;
|
||||
const state = settings(), preference = LifeI18n.preference(), 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 = t('system.browserDetected', { name: LifeI18n.names[LifeI18n.browserLanguage()] });
|
||||
container.innerHTML = `<div class="eyebrow">${t('system.eyebrow')}</div><h1>${t('system.title')}</h1><div class="settings"><section data-settings-group="appearance"><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 data-settings-group="language"><div class="eyebrow">${t('system.language')}</div><p class="sub">${detected}</p><div class="choices language-choices"><button class="chip ${preference === 'auto' ? 'selected' : ''}" onclick="window.setLanguage('auto')">${t('system.auto')}</button>${LifeI18n.supported.map(code => `<button class="chip ${preference === code ? 'selected' : ''}" lang="${code}" dir="${code === 'ar' ? 'rtl' : 'ltr'}" onclick="window.setLanguage('${code}')">${LifeI18n.names[code]}</button>`).join('')}</div></section><section class="notification-settings" data-settings-group="notifications">${window.LifeOSNotifications?.markup() || ''}</section><section data-settings-group="data"><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 data-settings-group="data"><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 data-settings-group="data"><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 data-settings-group="about"><div class="eyebrow">${t('system.version')}</div><p>${t('system.release',{version:window.LifeOSVersion||'1.17.0'})}</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>`; };
|
||||
|
||||
Loaded 3 of 25 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user