feat: full i18n support - all pages use Lang.t() for zh/en

This commit is contained in:
2026-07-27 20:31:41 +08:00
parent 860bc015e8
commit de58a1c42f
10 changed files with 578 additions and 282 deletions
+43 -43
View File
@@ -27,7 +27,7 @@ const PageDashboard = {
const allItem = document.createElement('div');
allItem.className = 'category-item active';
allItem.textContent = '全部';
allItem.textContent = Lang.t('allCategory');
allItem.dataset.cat = 'all';
listEl.appendChild(allItem);
@@ -46,7 +46,7 @@ const PageDashboard = {
async _loadThemes() {
const gridEl = document.getElementById('dash-theme-grid');
if (!gridEl) return;
gridEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-tertiary);">加载中...</div>';
gridEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-tertiary);">' + Lang.t('loading') + '</div>';
if (this._marketMode) {
await this._loadMarket(gridEl);
@@ -61,9 +61,9 @@ const PageDashboard = {
if (searchQuery) { themes = themes.filter(t => (t.name||'').toLowerCase().includes(searchQuery) || (t.description||'').toLowerCase().includes(searchQuery) || (t.author||'').toLowerCase().includes(searchQuery)); }
const sortBy = document.getElementById('dash-sort')?.value || 'name';
themes.sort((a, b) => (a[sortBy]||'').localeCompare(b[sortBy]||''));
if (themes.length === 0) { gridEl.innerHTML = this._emptyHtml('当前游戏没有兼容的仪表盘'); return; }
if (themes.length === 0) { gridEl.innerHTML = this._emptyHtml(Lang.t('noCompatibleDash')); return; }
gridEl.innerHTML = themes.map(t => this._themeCardHtml(t)).join('');
} catch (e) { gridEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">加载失败</div>'; }
} catch (e) { gridEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">' + Lang.t('loadFailed') + '</div>'; }
},
async _loadMarket(gridEl) {
@@ -71,7 +71,7 @@ const PageDashboard = {
const res = await fetch('/api/market/dashboards');
const data = await res.json();
const items = data.items || [];
if (!items.length) { gridEl.innerHTML = this._emptyHtml('市场暂无仪表盘'); return; }
if (!items.length) { gridEl.innerHTML = this._emptyHtml(Lang.t('marketEmpty')); return; }
gridEl.innerHTML = items.map(d => {
const icon = d.icon || '📦';
return `<div class="glass-card theme-card" data-market-id="${d.id}">
@@ -82,12 +82,12 @@ const PageDashboard = {
<span>${d.category||''}</span><span>v${d.version||'1.0'}</span>
</div>
<div class="theme-card-meta" style="margin-top:4px">
<span>${d.author||''}</span><span style="color:var(--text-tertiary)">${d.downloads||0} 次下载</span>
<span>${d.author||''}</span><span style="color:var(--text-tertiary)">${Lang.t('downloadCountShort').replace('%d', d.downloads||0)}</span>
</div>
<button class="btn btn-sm btn-primary btn-market-install" data-id="${d.id}" data-file="${d.file||''}" style="margin-top:8px;width:100%">安装</button>
<button class="btn btn-sm btn-primary btn-market-install" data-id="${d.id}" data-file="${d.file||''}" style="margin-top:8px;width:100%">${Lang.t('install')}</button>
</div></div>`;
}).join('');
} catch (e) { gridEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">市场加载失败</div>'; }
} catch (e) { gridEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">' + Lang.t('marketLoadFailed') + '</div>'; }
},
_isGameSupported(theme) {
@@ -101,8 +101,8 @@ const PageDashboard = {
return `
<div class="empty-state" style="grid-column:1/-1;">
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z" fill="currentColor"/></svg>
<h4>${msg || '暂无仪表盘主题'}</h4>
<p>创建或导入你的仪表盘主题</p>
<h4>${msg || Lang.t('noDashboards')}</h4>
<p>${Lang.t('createFirstDash')}</p>
</div>`;
},
@@ -149,8 +149,8 @@ const PageDashboard = {
const fd = new FormData(); fd.append('file', file);
const res = await fetch('/api/dashboards/import', { method: 'POST', body: fd });
const data = await res.json();
if (data.ok) { Toast.show('导入成功', 'success'); this._loadThemes(); }
else Toast.show('导入失败', 'error');
if (data.ok) { Toast.show(Lang.t('importSuccess'), 'success'); this._loadThemes(); }
else Toast.show(Lang.t('importFailed'), 'error');
}; input.click();
});
@@ -171,14 +171,14 @@ const PageDashboard = {
try {
const res = await fetch(`/api/market/install?id=${id}&filename=${filename}`);
const data = await res.json();
if (data.ok) { Toast.show('安装成功', 'success'); this._loadThemes(); }
else Toast.show('安装失败', 'error');
} catch (e) { Toast.show('安装失败', 'error'); }
if (data.ok) { Toast.show(Lang.t('installSuccess'), 'success'); this._loadThemes(); }
else Toast.show(Lang.t('installFailed'), 'error');
} catch (e) { Toast.show(Lang.t('installFailed'), 'error'); }
},
_showMarketUpload() {
if (!this._marketToken) {
Toast.show('请先登录市场账户', 'info');
Toast.show(Lang.t('marketLoginPrompt'), 'info');
this._showMarketLogin();
return;
}
@@ -193,8 +193,8 @@ const PageDashboard = {
try {
const res = await fetch('/api/market/upload', { method: 'POST', body: fd });
const data = await res.json();
Toast.show(data.error ? '上传失败: ' + data.error : '上传成功', data.error ? 'error' : 'success');
} catch (e) { Toast.show('上传失败', 'error'); }
Toast.show(data.error ? Lang.t('uploadFailedMsg') + data.error : Lang.t('uploadSuccess'), data.error ? 'error' : 'success');
} catch (e) { Toast.show(Lang.t('uploadFailed'), 'error'); }
}; input.click();
},
@@ -202,13 +202,13 @@ const PageDashboard = {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `<div class="modal" style="min-width:360px;max-width:400px">
<h3>市场</h3>
<div class="form-group"><label>邮箱</label><input id="ml-email" type="email"></div>
<div class="form-group"><label>密码</label><input id="ml-pass" type="password"></div>
<h3>${Lang.t('marketLogin')}</h3>
<div class="form-group"><label>${Lang.t('email')}</label><input id="ml-email" type="email"></div>
<div class="form-group"><label>${Lang.t('password')}</label><input id="ml-pass" type="password"></div>
<div class="modal-actions">
<button class="btn btn-secondary btn-sm" id="ml-logout" style="display:none;margin-right:auto">退出</button>
<button class="btn btn-secondary btn-sm" id="ml-register">注册</button>
<button class="btn btn-primary btn-sm" id="ml-login">登录</button>
<button class="btn btn-secondary btn-sm" id="ml-logout" style="display:none;margin-right:auto">${Lang.t('logout')}</button>
<button class="btn btn-secondary btn-sm" id="ml-register">${Lang.t('register')}</button>
<button class="btn btn-primary btn-sm" id="ml-login">${Lang.t('login')}</button>
</div></div>`;
document.body.appendChild(overlay);
@@ -221,7 +221,7 @@ const PageDashboard = {
localStorage.removeItem('turbosu-market-token');
overlay.remove();
this._updateMarketUI();
Toast.show('已退出', 'info');
Toast.show(Lang.t('loggedOut'), 'info');
});
}
@@ -234,17 +234,17 @@ const PageDashboard = {
if (data.token) {
this._marketToken = data.token;
localStorage.setItem('turbosu-market-token', data.token);
Toast.show('登录成功', 'success');
Toast.show(Lang.t('loginSuccess'), 'success');
overlay.remove();
this._updateMarketUI();
} else { Toast.show(data.message || '登录失败', 'error'); }
} else { Toast.show(data.message || Lang.t('loginFailed'), 'error'); }
});
overlay.querySelector('#ml-register').addEventListener('click', async () => {
const email = document.getElementById('ml-email').value;
const pass = document.getElementById('ml-pass').value;
const res = await fetch('/api/market/register', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({email, password: pass, passwordConfirm: pass}) });
const data = await res.json();
Toast.show(data.error ? '注册失败: ' + data.error : '注册成功,请查收验证邮件', data.error ? 'error' : 'success');
Toast.show(data.error ? Lang.t('registerFailed') + ': ' + data.error : Lang.t('registerSuccess'), data.error ? 'error' : 'success');
});
},
@@ -253,11 +253,11 @@ const PageDashboard = {
const uploadBtn = document.getElementById('btn-market-upload');
if (!loginBtn || !uploadBtn) return;
if (this._marketToken) {
loginBtn.textContent = '已登录';
loginBtn.textContent = Lang.t('loggedIn');
loginBtn.className = 'btn btn-sm btn-secondary';
uploadBtn.style.display = '';
} else {
loginBtn.textContent = '登录';
loginBtn.textContent = Lang.t('login');
loginBtn.className = 'btn btn-sm btn-primary';
uploadBtn.style.display = 'none';
}
@@ -268,7 +268,7 @@ const PageDashboard = {
const url = `${location.origin}/dashboard/${themeId}${gameParam}`;
window.open(url, '_blank');
navigator.clipboard.writeText(url).then(() => {
Toast.show('链接已复制,可在局域网设备访问', 'success');
Toast.show(Lang.t('linkCopied'), 'success');
}).catch(() => {
Toast.show(`打开地址: ${url}`, 'info');
});
@@ -279,7 +279,7 @@ const PageDashboard = {
a.href = `/api/dashboards/${themeId}/export`;
a.download = `${themeId}.tsd`;
a.click();
Toast.show('正在下载 .tsd 文件...', 'info');
Toast.show(Lang.t('exportTsdHint'), 'info');
},
_themeCardHtml(theme) {
@@ -287,7 +287,7 @@ const PageDashboard = {
const aspect = theme.aspect_ratio || 'auto';
const supported = theme.supported_games || 'all';
const isAll = supported === 'all';
const compatLabel = isAll ? '全' : '限';
const compatLabel = isAll ? Lang.t('fullCompatible') : Lang.t('limitedCompatible');
const compatClass = isAll ? 'badge-info' : 'badge-warning';
const preview = theme.preview_ext ? `/dashboard/${theme.id}/preview.${theme.preview_ext}` : '';
const bgStyle = preview ? `background-image:url(${preview});background-size:cover;background-position:center` : '';
@@ -309,7 +309,7 @@ const PageDashboard = {
},
async _createNew() {
const name = prompt('仪表盘名称:', 'My Dashboard');
const name = prompt(Lang.t('dashboardNamePrompt'), 'My Dashboard');
if (!name) return;
try {
const html = '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>TurboSu - ' + name + '</title><style>*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;font-family:sans-serif}#root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}#box{position:relative;overflow:hidden}#box.contain{max-width:100vw;max-height:100vh}#box.cover{min-width:100vw;min-height:100vh}#box.fill{width:100vw;height:100vh}</style></head><body><div id="root"><div id="box" class="contain"><div data-bind="speed_kmh" style="font-size:80px;font-weight:200">0</div></div></div><script>const M={aspect_ratio:"auto",render_mode:"contain"};(function(){const b=document.getElementById("box"),p=location.protocol==="https:"?"wss:":"ws:";let w,r;function a(){const rt=M.aspect_ratio==="auto"?null:(c=>{const p=c.split(":");return p.length===2?+p[0]/+p[1]:null})(M.aspect_ratio),md=M.render_mode||"contain";if(!rt){b.style.width="100vw";b.style.height="100vh";b.className="fill";return}const vw=window.innerWidth,vh=window.innerHeight,vr=vw/vh;let cw,ch;if(md==="cover"){cw=vw;ch=vw/rt;b.className="cover"}else{b.className="contain";cw=vr>rt?vh*rt:vw;ch=vr>rt?vh:vw/rt}b.style.width=cw+"px";b.style.height=ch+"px"}function c(){w=new WebSocket(p+"//"+location.host+"/ws");w.onopen=()=>{if(r){clearTimeout(r);r=null}};w.onmessage=(e)=>{try{const m=JSON.parse(e.data);if(m.type==="telemetry")document.querySelectorAll("[data-bind]").forEach(el=>{const v=m.data[el.getAttribute("data-bind")];if(v!==undefined)el.textContent=typeof v==="number"?v.toFixed(1):v})}catch(e){}};w.onclose=()=>{r=setTimeout(c,2000)}}a();window.addEventListener("resize",a);c()})();<\/script></body></html>';
@@ -318,9 +318,9 @@ const PageDashboard = {
body: JSON.stringify({name, category: 'custom', author: 'Local', config: {icon: '📊', aspect_ratio: 'auto', render_mode: 'contain'}, index_html: html})
});
const data = await res.json();
if (data.id) { Toast.show('创建成功', 'success'); this._loadThemes(); }
else Toast.show('创建失败', 'error');
} catch (e) { Toast.show('创建失败', 'error'); }
if (data.id) { Toast.show(Lang.t('createSuccess'), 'success'); this._loadThemes(); }
else Toast.show(Lang.t('createFailed'), 'error');
} catch (e) { Toast.show(Lang.t('createFailed'), 'error'); }
},
_template() {
@@ -329,17 +329,17 @@ const PageDashboard = {
<div class="dashboard-sub-sidebar">
<div id="dash-category-list"></div>
<div style="padding:8px 12px;margin-top:auto;display:flex;flex-direction:column;gap:4px">
<button id="btn-new-dashboard" class="btn btn-sm btn-primary" style="width:100%;">+ 新建</button>
<button id="btn-import-dashboard" class="btn btn-sm btn-secondary" style="width:100%;">导入 .tsd</button>
<button id="btn-new-dashboard" class="btn btn-sm btn-primary" style="width:100%;">${Lang.t('newDashboard')}</button>
<button id="btn-import-dashboard" class="btn btn-sm btn-secondary" style="width:100%;">${Lang.t('importTsd')}</button>
</div>
</div>
<div class="dashboard-main">
<div id="dash-search-box" style="display:flex;gap:8px;margin-bottom:12px;align-items:center">
<input id="dash-search" type="text" placeholder="搜索仪表盘..." style="flex:1;max-width:300px">
<input id="dash-search" type="text" placeholder="${Lang.t('searchDashboards')}" style="flex:1;max-width:300px">
<select id="dash-sort" style="width:120px">
<option value="name">名称</option>
<option value="category">分类</option>
<option value="author">作者</option>
<option value="name">${Lang.t('sortByName')}</option>
<option value="category">${Lang.t('sortByCategory')}</option>
<option value="author">${Lang.t('sortByAuthor')}</option>
</select>
</div>
<div id="dash-theme-grid" class="theme-grid"></div>