[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:
Krystie
2026-08-15 00:01:17 -07:00
parent bd40fb1c17
commit ef685e515e
11 changed files with 495 additions and 167 deletions
+29 -18
View File
@@ -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();