/** * Runtime label translator for asm_ui_app. * Replaces hardcoded English labels/sections using Frappe Translation doctype. */ (function () { "use strict"; var API_METHOD = "/api/method/translation_tool.api.translations.get_translations"; var STORAGE_KEY = "i18nextLng"; var VERSION = "1.0.2"; var translations = {}; var isArabic = false; var isApplying = false; var debounceTimer = null; var observer = null; var reapplyInterval = null; /** Text nodes cannot use .dataset — store originals in WeakMap */ var textOriginals = new WeakMap(); var trackedTextNodes = []; var ELEMENT_SELECTORS = "label, h1, h2, h3, h4, h5, h6, th, legend, nav span, aside span, [role='tab']"; var SKIP_ANCESTOR_SELECTORS = "input, textarea, select, option, script, style, noscript, code, pre"; var SKIP_VALUE_SELECTORS = "td, [data-value], .recharts-wrapper, .toast, [class*='toast']"; function log() { if (window.__TRANSLATION_TOOL_DEBUG__) { console.log.apply( console, ["[translation_tool]"].concat([].slice.call(arguments)) ); } } function getActiveLanguage() { try { var stored = localStorage.getItem(STORAGE_KEY); if (stored) return stored; } catch (e) { /* ignore */ } return document.documentElement.getAttribute("lang") || "en"; } function shouldTranslate() { var lang = getActiveLanguage(); var dir = document.documentElement.getAttribute("dir"); return lang === "ar" || dir === "rtl"; } function parentElement(node) { return node && node.nodeType === Node.TEXT_NODE ? node.parentElement : node; } function shouldSkipNode(node) { var el = parentElement(node); if (!el) return true; if (el.closest(SKIP_ANCESTOR_SELECTORS)) return true; if (el.closest(SKIP_VALUE_SELECTORS)) return true; if (el.closest("[data-tt-skip]")) return true; if (el.isContentEditable) return true; return false; } function translateExactText(text) { var trimmed = (text || "").trim(); if (!trimmed) return null; return translations[trimmed] || null; } function restoreOriginals(root) { trackedTextNodes.forEach(function (textNode) { if (textOriginals.has(textNode) && textNode.parentNode) { textNode.textContent = textOriginals.get(textNode); } }); trackedTextNodes = []; textOriginals = new WeakMap(); root.querySelectorAll("[data-tt-el-original]").forEach(function (el) { el.textContent = el.dataset.ttElOriginal; delete el.dataset.ttElOriginal; }); root.querySelectorAll("[data-tt-attr-original]").forEach(function (el) { var attr = el.dataset.ttAttrOriginalName; if (attr) { el.setAttribute(attr, el.dataset.ttAttrOriginal); } delete el.dataset.ttAttrOriginal; delete el.dataset.ttAttrOriginalName; }); } function rememberTextNode(textNode, original) { if (!textOriginals.has(textNode)) { textOriginals.set(textNode, original); trackedTextNodes.push(textNode); } } function translateTextNode(textNode) { if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return; if (shouldSkipNode(textNode)) return; var original = textNode.textContent; var trimmed = original.trim(); if (!trimmed) return; var translated = translateExactText(trimmed); if (!translated) return; rememberTextNode(textNode, original); var leading = (original.match(/^\s*/) || [""])[0]; var trailing = (original.match(/\s*$/) || [""])[0]; textNode.textContent = leading + translated + trailing; } function translateLeafElement(el) { if (!el || el.nodeType !== 1) return; if (shouldSkipNode(el)) return; if (el.childElementCount > 0) return; var original = el.textContent; var trimmed = original.trim(); if (!trimmed) return; var translated = translateExactText(trimmed); if (!translated) return; if (!el.dataset.ttElOriginal) { el.dataset.ttElOriginal = original; } el.textContent = translated; } function translateAttributes(root) { root.querySelectorAll("[placeholder], [title], [aria-label]").forEach(function (el) { if (shouldSkipNode(el)) return; ["placeholder", "title", "aria-label"].forEach(function (attr) { var value = el.getAttribute(attr); if (!value) return; var trimmed = value.trim(); var translated = translateExactText(trimmed); if (!translated) return; if (!el.dataset.ttAttrOriginal) { el.dataset.ttAttrOriginal = value; el.dataset.ttAttrOriginalName = attr; } el.setAttribute(attr, translated); }); }); } function walkAndTranslateText(root) { var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); var batch = []; var node; while ((node = walker.nextNode())) { batch.push(node); } batch.forEach(translateTextNode); } function applyTranslations(root) { if (!isArabic || !Object.keys(translations).length) return; isApplying = true; try { translateAttributes(root); root.querySelectorAll(ELEMENT_SELECTORS).forEach(translateLeafElement); walkAndTranslateText(root); } catch (err) { console.error("[translation_tool] applyTranslations error:", err); } finally { isApplying = false; } } function scheduleApply() { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(function () { var root = document.getElementById("root"); if (!root || !isArabic) return; applyTranslations(root); }, 80); } function setupObserver() { var root = document.getElementById("root"); if (!root || observer) return; observer = new MutationObserver(function () { if (isApplying || !isArabic) return; scheduleApply(); }); observer.observe(root, { childList: true, subtree: true, characterData: true, }); } function setupReapplyInterval() { if (reapplyInterval) return; reapplyInterval = setInterval(function () { if (!isArabic) return; var root = document.getElementById("root"); if (root) applyTranslations(root); }, 2000); } function clearReapplyInterval() { if (reapplyInterval) { clearInterval(reapplyInterval); reapplyInterval = null; } } function fetchTranslations(language) { var url = API_METHOD + "?language=" + encodeURIComponent(language); return fetch(url, { method: "GET", credentials: "include", headers: { Accept: "application/json" }, }) .then(function (res) { return res.json(); }) .then(function (payload) { var message = payload.message || payload; if (message && message.success && message.translations) { return message.translations; } return {}; }) .catch(function (err) { console.warn("[translation_tool] Failed to load translations:", err); return {}; }); } function activateArabic() { isArabic = true; return fetchTranslations("ar").then(function (map) { translations = map || {}; log("Loaded", Object.keys(translations).length, "translations"); var root = document.getElementById("root"); if (root) applyTranslations(root); setupObserver(); setupReapplyInterval(); }); } function deactivateTranslation() { isArabic = false; translations = {}; clearReapplyInterval(); var root = document.getElementById("root"); if (root) restoreOriginals(root); } function syncLanguage() { if (shouldTranslate()) { activateArabic(); } else { deactivateTranslation(); } } function watchLanguageChanges() { var htmlObserver = new MutationObserver(function () { syncLanguage(); }); htmlObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["lang", "dir"], }); window.addEventListener("storage", function (event) { if (event.key === STORAGE_KEY) syncLanguage(); }); setInterval(function () { var active = shouldTranslate(); if (active !== isArabic) syncLanguage(); }, 1000); } function init() { if (!document.getElementById("root")) { document.addEventListener("DOMContentLoaded", init); return; } watchLanguageChanges(); syncLanguage(); log("label_translator initialized v" + VERSION); } init(); })();