// Client-side vehicle name translator. // // Loads the localized vehicle name map produced by the bot's // init_vehicle_translation_cache() (BOT/utils.py) and exposes: // // 1. window.vehicleI18n.translate(internal, fallback, lang?) // Synchronous lookup. Returns the localized name when available, // otherwise English, otherwise the supplied fallback. // // 2. window.vehicleI18n.apply(root?) // Walks `root` (defaults to ) and rewrites the textContent of // every element carrying `data-vehicle-internal=""` to the // localized name. The element's *original* text is captured into // `data-vehicle-fallback` on first apply so re-applies (e.g. after // lang switch) don't lose the fallback. // // On startup the module: // - Eagerly loads the multilang map (via window.apiClient — /api/* is // gated by the website's apiSecurityCheck middleware). // - On first successful load + DOMContentLoaded, runs apply(document.body) // and installs a MutationObserver so any future inserts auto-translate. // // Server-side EJS or client-side template strings only need to add the // data attribute — no per-page wiring required. (function () { const STORAGE_KEY = 'vehicleI18nCache_v2'; const TTL_MS = 24 * 60 * 60 * 1000; let _map = null; // { internal: { en, ru, ... } } let _source = null; // 'multilang' | 'english_only' | 'none' let _loadingPromise = null; let _observerInstalled = false; function readCache() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || !parsed.fetchedAt || (Date.now() - parsed.fetchedAt) > TTL_MS) return null; if (parsed.source !== 'multilang') return null; return parsed.vehicles || null; } catch (e) { return null; } } function writeCache(vehicles, source) { if (source !== 'multilang') return; try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ fetchedAt: Date.now(), source, vehicles })); } catch (e) { // quota exceeded or storage disabled } } function fireReady(detail) { // Defer to next tick so any listeners installed after this module // executes still see the event. setTimeout(() => document.dispatchEvent(new CustomEvent('vehicle-i18n-ready', { detail })), 0); } async function ensureLoaded() { if (_map) return _map; const cached = readCache(); if (cached) { _map = cached; _source = 'multilang'; fireReady({ source: 'multilang', cached: true }); return _map; } if (_loadingPromise) return _loadingPromise; _loadingPromise = (async () => { try { if (!window.apiClient) { await new Promise((resolve) => { const tick = () => window.apiClient ? resolve() : setTimeout(tick, 50); tick(); }); } const body = await window.apiClient.request('/api/i18n/vehicles'); _map = body && body.vehicles ? body.vehicles : {}; _source = body && body.source ? body.source : 'none'; writeCache(_map, _source); fireReady({ source: _source }); return _map; } catch (e) { console.error('vehicle i18n load failed', e); _map = {}; _source = 'none'; return _map; } finally { _loadingPromise = null; } })(); return _loadingPromise; } function currentLang() { const m = (document.cookie || '').match(/(?:^|;\s*)lang=([\w-]+)/); return (m && m[1]) || (document.documentElement.lang || 'en'); } // Case-insensitive map lookup. The DB historically stored mixed casings of // vehicle_internal (e.g. germ_leopard_I vs germ_leopard_i), and the API // now lowercases them; the bot's translation cache may or may not match // that casing depending on its source. Try the literal key first, then // lowercase, then a one-time-built lowercased index for everything else. let _lowerIndex = null; function lookup(internal) { if (!_map || !internal) return null; if (_map[internal]) return _map[internal]; const lower = String(internal).toLowerCase(); if (_map[lower]) return _map[lower]; if (!_lowerIndex) { _lowerIndex = {}; for (const k of Object.keys(_map)) _lowerIndex[k.toLowerCase()] = _map[k]; } return _lowerIndex[lower] || null; } function translate(internal, fallback, lang) { const lng = lang || currentLang(); if (!_map || !internal) return fallback || internal || ''; const entry = lookup(internal); if (!entry) return fallback || internal; const localized = entry[lng] || entry.en || fallback || internal; // The player-rendered display the DB stores can have a leading // country-leak / event glyph (▄ ◘ ◢ ␗ etc.) that WT's client prepends // at draw time. The translation map only knows the canonical name // without that prefix, so naive replacement would lose it — making // e.g. "▄F-16A ADF" (Italy) and "F-16A ADF" (no leak) look identical. // Preserve any leading run of non-letter / non-digit / non-space chars // from the original fallback when the translation doesn't already // include it. if (fallback) { const m = String(fallback).match(/^[^\p{L}\p{N}\s]+/u); if (m && !localized.startsWith(m[0])) return m[0] + localized; } return localized; } function applyToElement(el) { if (!el || !el.dataset || !el.dataset.vehicleInternal) return; const internal = el.dataset.vehicleInternal; if (!internal) return; // Capture the rendered fallback once so language re-switches still have // something to fall back on if the map ever loses an entry. if (el.dataset.vehicleFallback === undefined) { el.dataset.vehicleFallback = el.textContent || ''; } const lng = currentLang(); // Cache the lang we last applied so we don't fight the DOM on every // mutation tick. if (el.dataset.vehicleAppliedLang === lng) return; el.textContent = translate(internal, el.dataset.vehicleFallback, lng); el.dataset.vehicleAppliedLang = lng; } function apply(root) { if (!_map) return; const node = root || document.body; if (!node) return; if (node.nodeType === 1 && node.dataset && node.dataset.vehicleInternal) { applyToElement(node); } if (node.querySelectorAll) { node.querySelectorAll('[data-vehicle-internal]').forEach(applyToElement); } } function installMutationObserver() { if (_observerInstalled || typeof MutationObserver === 'undefined' || !document.body) return; _observerInstalled = true; const obs = new MutationObserver((mutations) => { for (const m of mutations) { for (const node of m.addedNodes) { if (node.nodeType === 1) apply(node); } if (m.type === 'attributes' && m.target && m.target.dataset && m.target.dataset.vehicleInternal) { delete m.target.dataset.vehicleAppliedLang; applyToElement(m.target); } } }); obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-vehicle-internal'], }); } function autoApply() { if (!document.body) return; apply(document.body); installMutationObserver(); } document.addEventListener('vehicle-i18n-ready', autoApply); window.vehicleI18n = { ensureLoaded, translate, apply, get ready() { return _map !== null; }, get source() { return _source; }, get currentLang() { return currentLang(); }, invalidate() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) { /* ignore */ } _map = null; _source = null; _lowerIndex = null; // Also clear apply-state markers so a refresh re-translates. document.querySelectorAll('[data-vehicle-applied-lang]').forEach(el => { delete el.dataset.vehicleAppliedLang; }); }, }; // Kick off load. apply() runs from the ready event handler above. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', ensureLoaded); } else { ensureLoaded(); } })();