add SREBOT, SHARED, TSSBOT contents (fixup for #1223)

PR #1223 only staged the deletions of the old paths because the new
top-level directories were still untracked when the commit was authored.
This commit adds the actual restructured tree: SREBOT/ (existing bot),
SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES,
update_game_files), and TSSBOT/ (skeleton).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
FURRO404
2026-05-13 23:17:02 -07:00
commit 2b399fdb81
186 changed files with 96596 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
// 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 <body>) and rewrites the textContent of
// every element carrying `data-vehicle-internal="<cdk>"` 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();
}
})();