[glm-grade=B+] feat(i18n): complete card/filter/action translation keys for 31 languages
Full language display names + RTL set (ar/fa/ur) on /i18n/languages; card.internet/auth/tailscale/dashca, status pills, filter bar and batch-operation strings added to every language dictionary. Frontend: English now loads the server dictionary too (keys are semantic ids, not fallback copy), failed loads keep existing DOM text instead of exposing raw keys, isLoaded() gate for pre-load renders. Rebuilt status/dist. Tests: i18n-cards 9/9, full suite 1837/1837.
This commit is contained in:
+19
-3
@@ -1,6 +1,12 @@
|
||||
// ========== GRID & STATUS HELPERS ==========
|
||||
(function () {
|
||||
|
||||
function statusLabel(up) {
|
||||
const i18n = window.DCI18n;
|
||||
if (!i18n || !i18n.isLoaded()) return up ? 'ON' : 'OFF';
|
||||
return i18n.t(up ? 'card.status.on' : 'card.status.off');
|
||||
}
|
||||
|
||||
/* Enhanced status helpers with response time tracking */
|
||||
function setQuick(id, up, responseTime = null) {
|
||||
const dot = document.getElementById(id + '-dot');
|
||||
@@ -14,7 +20,7 @@
|
||||
}
|
||||
|
||||
if (pill) {
|
||||
pill.textContent = up ? 'ON' : 'OFF';
|
||||
pill.textContent = statusLabel(up);
|
||||
pill.classList.toggle('on', up);
|
||||
pill.classList.toggle('off', !up);
|
||||
}
|
||||
@@ -172,7 +178,10 @@
|
||||
|
||||
row.appendChild(el('span', 'spacer'));
|
||||
|
||||
const pill = el('span', 'badge off', 'OFF'); pill.id = 'badge-' + s.id; row.appendChild(pill);
|
||||
const pill = el('span', 'badge off', 'OFF');
|
||||
pill.id = 'badge-' + s.id;
|
||||
pill.setAttribute('data-i18n-live-status', 'binary');
|
||||
row.appendChild(pill);
|
||||
|
||||
// Update available badge (hidden by default, shown when update detected)
|
||||
const updateBadge = el('span', 'update-available-badge', 'UPDATE');
|
||||
@@ -301,6 +310,7 @@
|
||||
}
|
||||
|
||||
const btn = el('button', null, 'Open');
|
||||
btn.setAttribute('data-i18n', 'action.open');
|
||||
btn.onclick = () => window.open(serviceUrl(s.id), '_blank', 'noopener');
|
||||
btnRow.appendChild(btn);
|
||||
card.appendChild(btnRow);
|
||||
@@ -309,6 +319,12 @@
|
||||
root.appendChild(card);
|
||||
}
|
||||
|
||||
// Cards can be created after the initial language pass. Translate their
|
||||
// initial live state and controls immediately instead of waiting for poll.
|
||||
if (window.DCI18n && window.DCI18n.isLoaded()) {
|
||||
window.DCI18n.applyTranslations();
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
root.querySelectorAll('.card').forEach(card => card.classList.add('loaded'));
|
||||
});
|
||||
@@ -332,7 +348,7 @@
|
||||
}
|
||||
|
||||
if (pill) {
|
||||
pill.textContent = up ? 'ON' : 'OFF';
|
||||
pill.textContent = statusLabel(up);
|
||||
pill.classList.toggle('on', up);
|
||||
pill.classList.toggle('off', !up);
|
||||
}
|
||||
|
||||
@@ -147,18 +147,21 @@ function renderDnsCards() {
|
||||
`<span id="${safeId}-dot" class="dot bad at-bl"></span>`
|
||||
+ `<div class="row"><div class="logo-wrap">${svgIcon}</div>`
|
||||
+ `<span class="name">${label}</span><span class="spacer"></span>`
|
||||
+ `<span id="${safeId}-pill" class="badge off">OFF</span></div>`
|
||||
+ `<span id="${safeId}-pill" class="badge off" data-i18n-live-status="binary">OFF</span></div>`
|
||||
+ `<div class="response-row"><span id="${safeId}-time" class="response-time">--</span></div>`
|
||||
+ `<div class="health-row" id="health-${safeId}"><span id="uptime-${safeId}" class="uptime-chip">--</span><div class="uptime-mini-bar"><div class="fill" id="uptime-bar-${safeId}" style="width: 0%"></div></div></div>`
|
||||
+ `<div class="btn-row">`
|
||||
+ `<button id="${safeId}-restart" class="restart-btn">Restart</button>`
|
||||
+ `<button id="${safeId}-restart" class="restart-btn" data-i18n="action.restart">Restart</button>`
|
||||
+ `<button id="${safeId}-update" class="update-btn" title="Update DNS server">⬆️</button>`
|
||||
+ `<button id="${safeId}-open">Open</button>`
|
||||
+ `<button id="${safeId}-logs" class="logs-btn">Logs</button>`
|
||||
+ `<button id="${safeId}-open" data-i18n="action.open">Open</button>`
|
||||
+ `<button id="${safeId}-logs" class="logs-btn" data-i18n="action.logs">Logs</button>`
|
||||
+ `<button id="${safeId}-settings" class="settings-btn">⚙️</button>`
|
||||
+ `</div>`;
|
||||
topRow.insertBefore(card, firstChild);
|
||||
});
|
||||
if (window.DCI18n && window.DCI18n.isLoaded()) {
|
||||
window.DCI18n.applyTranslations();
|
||||
}
|
||||
}
|
||||
window.renderDnsCards = renderDnsCards;
|
||||
|
||||
|
||||
+29
-18
@@ -50,11 +50,6 @@
|
||||
// reqId is the monotonic token incremented by the caller (setLanguage/init).
|
||||
// If not provided (direct API call), allocate one for backward compatibility.
|
||||
if (reqId === undefined) reqId = ++_langRequestId;
|
||||
if (lang === DEFAULT_LANG) {
|
||||
translations = {}; // English is the default — no translation needed
|
||||
loaded = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/v1/i18n/translations/${lang}`);
|
||||
// Guard against out-of-order resolution: if another loadTranslations
|
||||
@@ -80,11 +75,15 @@
|
||||
}
|
||||
|
||||
function t(key) {
|
||||
if (currentLang === DEFAULT_LANG) return key;
|
||||
// If translations didn't load, fall back to the English key
|
||||
// Translation keys are semantic identifiers, not English fallback copy.
|
||||
// Callers that render before loading should retain their existing DOM text.
|
||||
return translations[key] || key;
|
||||
}
|
||||
|
||||
function isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
function setLanguage(lang) {
|
||||
if (!SUPPORTED_LANGS.includes(lang)) return;
|
||||
currentLang = lang;
|
||||
@@ -97,8 +96,9 @@
|
||||
|
||||
const reqId = ++_langRequestId; // increment BEFORE calling loadTranslations
|
||||
loadTranslations(lang, reqId).then(() => {
|
||||
// Only apply if this is still the latest request.
|
||||
if (reqId === _langRequestId) applyTranslations();
|
||||
// Only apply a successfully loaded dictionary. On HTTP/network failure,
|
||||
// retain the existing readable DOM copy instead of exposing semantic keys.
|
||||
if (reqId === _langRequestId && loaded) applyTranslations();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,12 +113,25 @@
|
||||
// leaving the previous language's translated text visible.
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
el.textContent = t(key);
|
||||
const prefix = el.getAttribute('data-i18n-prefix') || '';
|
||||
el.textContent = prefix + t(key);
|
||||
});
|
||||
// Apply to placeholders
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-placeholder');
|
||||
el.placeholder = t(key);
|
||||
const prefix = el.getAttribute('data-i18n-prefix') || '';
|
||||
el.placeholder = prefix + t(key);
|
||||
});
|
||||
// Live status pills derive the translation key from current runtime state.
|
||||
// A language switch must never reset an online card to its initial OFF copy.
|
||||
document.querySelectorAll('[data-i18n-live-status]').forEach(el => {
|
||||
const card = el.closest('[data-status]');
|
||||
const isOn = card && card.getAttribute('data-status') === 'on';
|
||||
const mode = el.getAttribute('data-i18n-live-status');
|
||||
const key = mode === 'yesno'
|
||||
? (isOn ? 'card.status.yes' : 'card.status.no')
|
||||
: (isOn ? 'card.status.on' : 'card.status.off');
|
||||
el.textContent = t(key);
|
||||
});
|
||||
// Apply to titles
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
@@ -226,12 +239,10 @@
|
||||
|
||||
function start() {
|
||||
createLanguageSelector();
|
||||
if (currentLang !== DEFAULT_LANG) {
|
||||
const reqId = ++_langRequestId;
|
||||
loadTranslations(currentLang, reqId).then(() => {
|
||||
if (reqId === _langRequestId) applyTranslations();
|
||||
});
|
||||
}
|
||||
const reqId = ++_langRequestId;
|
||||
loadTranslations(currentLang, reqId).then(() => {
|
||||
if (reqId === _langRequestId && loaded) applyTranslations();
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -242,7 +253,7 @@
|
||||
}
|
||||
|
||||
// Expose globally
|
||||
window.DCI18n = { t, setLanguage, getLanguage, applyTranslations, loadTranslations };
|
||||
window.DCI18n = { t, setLanguage, getLanguage, isLoaded, applyTranslations, loadTranslations };
|
||||
|
||||
// Auto-init
|
||||
init();
|
||||
|
||||
@@ -169,6 +169,11 @@
|
||||
'4h': '4 hours', '8h': '8 hours', '12h': '12 hours', '24h': '24 hours', 'never': 'Disabled'
|
||||
};
|
||||
|
||||
function translated(key, fallback) {
|
||||
const i18n = window.DCI18n;
|
||||
return i18n && i18n.isLoaded() ? i18n.t(key) : fallback;
|
||||
}
|
||||
|
||||
function updateAuthCard(active, duration) {
|
||||
const card = document.getElementById('auth-card');
|
||||
const pill = document.getElementById('auth-pill');
|
||||
@@ -179,15 +184,15 @@
|
||||
if (active) {
|
||||
card.setAttribute('data-status', 'on');
|
||||
pill.className = 'badge on';
|
||||
pill.textContent = 'YES';
|
||||
pill.textContent = translated('card.status.yes', 'YES');
|
||||
dot.className = 'dot ok at-bl';
|
||||
statusText.textContent = 'Session: ' + (DURATION_LABELS[duration] || duration);
|
||||
} else {
|
||||
card.setAttribute('data-status', 'off');
|
||||
pill.className = 'badge off';
|
||||
pill.textContent = 'NO';
|
||||
pill.textContent = translated('card.status.no', 'NO');
|
||||
dot.className = 'dot bad at-bl';
|
||||
statusText.textContent = 'Not configured';
|
||||
statusText.textContent = translated('card.auth.not_configured', 'Not configured');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user