mirror of
git://f0xx.org/ac/ac-platform-web
synced 2026-07-29 06:57:38 +03:00
208 lines
6.2 KiB
JavaScript
208 lines
6.2 KiB
JavaScript
/**
|
|
* Crash console i18n — JSON catalogs (en default), same key style as Android strings.xml.
|
|
*/
|
|
(function (global) {
|
|
const STORAGE_KEY = 'crash_console_lang';
|
|
const SUPPORTED = ['en', 'ru'];
|
|
const DEFAULT_LANG = 'en';
|
|
|
|
/** @type {Record<string, { flag: string, code: string }>} */
|
|
const LANG_DISPLAY = {
|
|
en: { flag: '\uD83C\uDDEC\uD83C\uDDE7', code: 'EN' },
|
|
ru: { flag: '\uD83C\uDDF7\uD83C\uDDFA', code: 'RU' },
|
|
};
|
|
|
|
let lang = DEFAULT_LANG;
|
|
let catalog = {};
|
|
let fallback = {};
|
|
let readyResolve;
|
|
const ready = new Promise((resolve) => {
|
|
readyResolve = resolve;
|
|
});
|
|
|
|
function basePath() {
|
|
const body = document.body;
|
|
return body ? body.getAttribute('data-base-path') || '' : '';
|
|
}
|
|
|
|
/** Close stray modal layers so fixed backdrops never block login/console UI. */
|
|
function dismissPlatformOverlays() {
|
|
document.querySelectorAll('#graph-detail-overlay, #tag-modal, #hub-docs-modal').forEach((el) => {
|
|
el.hidden = true;
|
|
el.setAttribute('aria-hidden', 'true');
|
|
});
|
|
const dialog = document.getElementById('ticket-create-dialog');
|
|
if (dialog && dialog.open && typeof dialog.close === 'function') {
|
|
dialog.close();
|
|
}
|
|
}
|
|
|
|
function storedLang() {
|
|
try {
|
|
const v = localStorage.getItem(STORAGE_KEY);
|
|
return SUPPORTED.includes(v) ? v : DEFAULT_LANG;
|
|
} catch {
|
|
return DEFAULT_LANG;
|
|
}
|
|
}
|
|
|
|
function persistLang(code) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, code);
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
function fetchCatalog(code) {
|
|
const url = basePath() + '/assets/i18n/' + encodeURIComponent(code) + '.json';
|
|
return fetch(url, { credentials: 'same-origin' }).then((res) => {
|
|
if (!res.ok) throw new Error('i18n load failed: ' + code);
|
|
return res.json();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} key
|
|
* @param {Record<string, string|number>|undefined} params
|
|
*/
|
|
function t(key, params) {
|
|
let s = catalog[key] ?? fallback[key] ?? key;
|
|
if (params) {
|
|
Object.keys(params).forEach((k) => {
|
|
s = s.split('{' + k + '}').join(String(params[k]));
|
|
});
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function apply(root) {
|
|
const scope = root || document;
|
|
scope.querySelectorAll('[data-i18n]').forEach((el) => {
|
|
const key = el.getAttribute('data-i18n');
|
|
if (!key) return;
|
|
if (el.hasAttribute('data-i18n-html')) {
|
|
el.innerHTML = t(key);
|
|
} else {
|
|
el.textContent = t(key);
|
|
}
|
|
});
|
|
scope.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
|
|
const key = el.getAttribute('data-i18n-placeholder');
|
|
if (key) el.setAttribute('placeholder', t(key));
|
|
});
|
|
scope.querySelectorAll('[data-i18n-title]').forEach((el) => {
|
|
const key = el.getAttribute('data-i18n-title');
|
|
if (key) el.setAttribute('title', t(key));
|
|
});
|
|
scope.querySelectorAll('[data-i18n-aria]').forEach((el) => {
|
|
const key = el.getAttribute('data-i18n-aria');
|
|
if (key) el.setAttribute('aria-label', t(key));
|
|
});
|
|
scope.querySelectorAll('select[data-i18n-options]').forEach((sel) => {
|
|
const prefix = sel.getAttribute('data-i18n-options');
|
|
if (!prefix) return;
|
|
sel.querySelectorAll('option[data-i18n]').forEach((opt) => {
|
|
const k = opt.getAttribute('data-i18n');
|
|
if (k) opt.textContent = t(k);
|
|
});
|
|
});
|
|
scope.querySelectorAll('.platform-footer__left[data-i18n], footer[data-i18n="footer.copyright"]').forEach((footer) => {
|
|
const year = footer.getAttribute('data-year') || String(new Date().getFullYear());
|
|
footer.textContent = t(footer.getAttribute('data-i18n') || 'footer.copyright', { year });
|
|
});
|
|
scope.querySelectorAll('[data-i18n="detail.report_meta"]').forEach((el) => {
|
|
el.textContent = t('detail.report_meta', {
|
|
id: el.getAttribute('data-i18n-id') || '',
|
|
type: el.getAttribute('data-i18n-type') || '',
|
|
});
|
|
});
|
|
}
|
|
|
|
function langOptionLabel(code) {
|
|
const meta = LANG_DISPLAY[code] || LANG_DISPLAY[DEFAULT_LANG];
|
|
return meta.flag + ' ' + meta.code;
|
|
}
|
|
|
|
function syncLangSelect() {
|
|
document.querySelectorAll('.lang-select').forEach((sel) => {
|
|
SUPPORTED.forEach((code) => {
|
|
const opt = sel.querySelector('option[value="' + code + '"]');
|
|
if (opt) opt.textContent = langOptionLabel(code);
|
|
});
|
|
if (sel.value !== lang) sel.value = lang;
|
|
});
|
|
syncLocaleDisplay(lang);
|
|
}
|
|
|
|
function syncLocaleDisplay(code) {
|
|
const meta = LANG_DISPLAY[code] || LANG_DISPLAY[DEFAULT_LANG];
|
|
document.querySelectorAll('.locale-flag').forEach((el) => {
|
|
el.textContent = meta.flag;
|
|
});
|
|
document.querySelectorAll('.locale-code').forEach((el) => {
|
|
el.textContent = meta.code;
|
|
});
|
|
}
|
|
|
|
async function setLang(code, options) {
|
|
const opts = options || {};
|
|
if (!SUPPORTED.includes(code)) code = DEFAULT_LANG;
|
|
fallback = await fetchCatalog(DEFAULT_LANG);
|
|
catalog = code === DEFAULT_LANG ? { ...fallback } : await fetchCatalog(code);
|
|
lang = code;
|
|
document.documentElement.setAttribute('lang', code);
|
|
persistLang(code);
|
|
syncLangSelect();
|
|
if (!opts.silent) {
|
|
apply(document);
|
|
document.dispatchEvent(new CustomEvent('crash-i18n-change', { detail: { lang: code } }));
|
|
}
|
|
}
|
|
|
|
function getLang() {
|
|
return lang;
|
|
}
|
|
|
|
function initLangSelect() {
|
|
document.querySelectorAll('.lang-select').forEach((sel) => {
|
|
sel.value = lang;
|
|
if (sel.dataset.i18nBound === '1') return;
|
|
sel.dataset.i18nBound = '1';
|
|
sel.addEventListener('change', () => {
|
|
setLang(sel.value).catch(() => setLang(DEFAULT_LANG));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function init() {
|
|
dismissPlatformOverlays();
|
|
lang = storedLang();
|
|
document.documentElement.setAttribute('lang', lang);
|
|
await setLang(lang, { silent: true });
|
|
apply(document);
|
|
initLangSelect();
|
|
syncLocaleDisplay(lang);
|
|
readyResolve();
|
|
}
|
|
|
|
global.CrashI18n = {
|
|
t,
|
|
apply,
|
|
setLang,
|
|
getLang,
|
|
init,
|
|
ready,
|
|
SUPPORTED,
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
init().catch(() => {
|
|
lang = DEFAULT_LANG;
|
|
readyResolve();
|
|
});
|
|
});
|
|
} else {
|
|
init().catch(() => readyResolve());
|
|
}
|
|
})(typeof window !== 'undefined' ? window : globalThis);
|