Files
pyTorrent/pytorrent/static/js/i18n.js
Mateusz Gruszczyński 22e2983dc2 multilang_1
2026-05-29 13:18:53 +02:00

2 lines
5.9 KiB
JavaScript

export const i18nSource = "\n const I18N_DEFAULT_LANGUAGE = 'en_US';\n const I18N_ATTRIBUTE_NAMES = ['title', 'placeholder', 'aria-label', 'alt'];\n let currentLanguage = String(window.PYTORRENT?.language || I18N_DEFAULT_LANGUAGE);\n let currentTranslations = {};\n let i18nObserver = null;\n let i18nApplying = false;\n\n function i18nKey(text) {\n return String(text ?? '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/[%/\\\\.]+/g, ' ')\n .replace(/[^A-Za-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toUpperCase();\n }\n\n function isTechnicalI18nText(text) {\n const value = String(text ?? '').trim();\n if (!value) return true;\n if (/^[A-Z]{1,4}$/.test(value)) return true;\n if (/^[a-z][a-z0-9+.-]*:\\/\\/\\S+$/i.test(value)) return true;\n if (/^\\/[\\w./:-]+$/.test(value)) return true;\n if (/^\\d+(?:\\.\\d+)?\\s*(?:B|KiB|MiB|GiB|TiB|B\\/s|KiB\\/s|MiB\\/s|GiB\\/s|Mbit\\/s|Gbit\\/s)$/i.test(value)) return true;\n if (/^\\d+\\s*B\\/s\\s*\\/\\s*\\d+\\s*B\\/s$/i.test(value)) return true;\n if (/^\\d+(?:s|m|h|d)$/i.test(value)) return true;\n return false;\n }\n\n function interpolateI18n(text, params = {}) {\n return String(text).replace(/\\{([A-Za-z0-9_]+)\\}/g, (_, key) => params[key] ?? '');\n }\n\n function t(key, params = {}) {\n const rawKey = String(key || '');\n if (isTechnicalI18nText(rawKey)) return interpolateI18n(rawKey, params);\n const normalized = rawKey.includes('.') ? rawKey.split('.').pop() : rawKey;\n const upper = i18nKey(normalized) || rawKey;\n const value = currentTranslations[rawKey] ?? currentTranslations[upper] ?? currentTranslations[i18nKey(rawKey)] ?? rawKey;\n return interpolateI18n(value, params);\n }\n\n function translatePlainText(text) {\n const raw = String(text ?? '');\n const trimmed = raw.trim();\n if (!trimmed || !/[A-Za-z]/.test(trimmed) || isTechnicalI18nText(trimmed)) return raw;\n const translated = currentTranslations[i18nKey(trimmed)];\n if (!translated || translated === trimmed) return raw;\n return raw.replace(trimmed, translated);\n }\n\n function shouldSkipI18nNode(node) {\n const parent = node.parentElement;\n if (!parent) return true;\n return !!parent.closest('script, style, code, pre, textarea, input, select, option, .fi, [data-i18n-skip]');\n }\n\n function applyI18n(root = document.body) {\n if (!root || i18nApplying || currentLanguage === I18N_DEFAULT_LANGUAGE) return;\n i18nApplying = true;\n try {\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n if (shouldSkipI18nNode(node)) return NodeFilter.FILTER_REJECT;\n return /[A-Za-z]/.test(node.nodeValue || '') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;\n }\n });\n const textNodes = [];\n while (walker.nextNode()) textNodes.push(walker.currentNode);\n textNodes.forEach(node => {\n const next = translatePlainText(node.nodeValue);\n if (next !== node.nodeValue) node.nodeValue = next;\n });\n root.querySelectorAll?.('[title], [placeholder], [aria-label], [alt]').forEach(el => {\n if (el.closest('script, style, code, pre, [data-i18n-skip]')) return;\n I18N_ATTRIBUTE_NAMES.forEach(attr => {\n if (!el.hasAttribute(attr)) return;\n const value = el.getAttribute(attr);\n const next = translatePlainText(value);\n if (next !== value) el.setAttribute(attr, next);\n });\n });\n } finally {\n i18nApplying = false;\n }\n }\n\n async function loadLanguage(language) {\n const safeLanguage = window.PYTORRENT?.supportedLanguages?.[language] ? language : I18N_DEFAULT_LANGUAGE;\n const response = await fetch(`/static/i18n/${encodeURIComponent(safeLanguage)}.json`, { cache: 'no-store' });\n const data = await response.json();\n currentLanguage = safeLanguage;\n currentTranslations = data.translations || {};\n document.documentElement.lang = safeLanguage.replace('_', '-');\n window.PYTORRENT.language = safeLanguage;\n return safeLanguage;\n }\n\n function scheduleI18n(root = document.body) {\n clearTimeout(window.__pyTorrentI18nTimer);\n window.__pyTorrentI18nTimer = setTimeout(() => applyI18n(root), 0);\n }\n\n function startI18nObserver() {\n if (i18nObserver || currentLanguage === I18N_DEFAULT_LANGUAGE) return;\n i18nObserver = new MutationObserver(mutations => {\n if (i18nApplying) return;\n for (const mutation of mutations) {\n if (mutation.type === 'childList' || mutation.type === 'attributes') {\n scheduleI18n(document.body);\n return;\n }\n }\n });\n i18nObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: I18N_ATTRIBUTE_NAMES });\n }\n\n async function setLanguage(language) {\n await loadLanguage(language);\n if (i18nObserver) { i18nObserver.disconnect(); i18nObserver = null; }\n if (currentLanguage !== I18N_DEFAULT_LANGUAGE) {\n applyI18n(document.body);\n startI18nObserver();\n } else {\n window.location.reload();\n }\n }\n\n function updateLanguageFlagHint() {\n const select = $('languageSelect');\n const hint = $('languageFlagHint');\n if (!select || !hint) return;\n const info = window.PYTORRENT?.supportedLanguages?.[select.value] || window.PYTORRENT?.supportedLanguages?.[I18N_DEFAULT_LANGUAGE] || {flag:'us'};\n const flag = String(info.flag || 'us').toLowerCase();\n hint.innerHTML = `<span class=\"fi fi-${esc(flag)}\"></span> /static/libs/flag-icons/7.2.3/flags/4x3/${esc(flag)}.svg`;\n }\n\n loadLanguage(currentLanguage).then(() => {\n updateLanguageFlagHint();\n if (currentLanguage !== I18N_DEFAULT_LANGUAGE) {\n applyI18n(document.body);\n startI18nObserver();\n }\n }).catch(() => {\n currentLanguage = I18N_DEFAULT_LANGUAGE;\n currentTranslations = {};\n });\n";