Initial commit of translation tool
This commit is contained in:
commit
2452b47fb6
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Translation Tool
|
||||||
|
|
||||||
|
Runtime label translation for ASM UI (`asm_ui_app`) using Frappe **Translation** doctype records.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Bulk API: flat `{ source_text: translated_text }` map for a language
|
||||||
|
- Client-side `label_translator.js` replaces hardcoded English labels/sections when Arabic is active
|
||||||
|
- Cache invalidation when Translation records change
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/frappe-bench
|
||||||
|
bench get-app translation_tool # if not already in apps/
|
||||||
|
bench --site <site> install-app translation_tool
|
||||||
|
bench build --app translation_tool
|
||||||
|
bench --site <site> clear-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure `translation_tool` is listed in `sites/<site>/apps.txt` after install.
|
||||||
|
|
||||||
|
## Translation records
|
||||||
|
|
||||||
|
Create rows in **Translation** doctype:
|
||||||
|
|
||||||
|
| language | source_text | translated_text |
|
||||||
|
|----------|-------------|-----------------|
|
||||||
|
| ar | Asset Information | معلومات الأصل |
|
||||||
|
| ar | Workflow Actions | إجراءات سير العمل |
|
||||||
|
|
||||||
|
`source_text` must match the visible English UI string exactly.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- `translation_tool.api.translations.get_translations?language=ar`
|
||||||
|
- `translation_tool.api.translations.get_translation?source_text=...&language=ar`
|
||||||
|
- `translation_tool.api.translations.get_available_languages`
|
||||||
14
pyproject.toml
Normal file
14
pyproject.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[project]
|
||||||
|
name = "translation_tool"
|
||||||
|
authors = [
|
||||||
|
{ name = "support@seeraarabia.com", email = "support@seeraarabia.com" }
|
||||||
|
]
|
||||||
|
description = "Runtime label translation for ASM UI using Frappe Translation doctype"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
readme = "README.md"
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["flit_core >=3.4,<4"]
|
||||||
|
build-backend = "flit_core.buildapi"
|
||||||
1
translation_tool/__init__.py
Normal file
1
translation_tool/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.0.1"
|
||||||
BIN
translation_tool/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
translation_tool/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
translation_tool/__pycache__/hooks.cpython-310.pyc
Normal file
BIN
translation_tool/__pycache__/hooks.cpython-310.pyc
Normal file
Binary file not shown.
0
translation_tool/api/__init__.py
Normal file
0
translation_tool/api/__init__.py
Normal file
BIN
translation_tool/api/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
translation_tool/api/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
translation_tool/api/__pycache__/translations.cpython-310.pyc
Normal file
BIN
translation_tool/api/__pycache__/translations.cpython-310.pyc
Normal file
Binary file not shown.
111
translation_tool/api/translations.py
Normal file
111
translation_tool/api/translations.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
CACHE_KEY = "translation_tool:flat_map:{language}"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_flat_map(language: str) -> dict[str, str]:
|
||||||
|
rows = frappe.get_all(
|
||||||
|
"Translation",
|
||||||
|
filters={"language": language},
|
||||||
|
fields=["source_text", "translated_text"],
|
||||||
|
limit_page_length=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
translation_map: dict[str, str] = {}
|
||||||
|
for row in rows:
|
||||||
|
source = (row.get("source_text") or "").strip()
|
||||||
|
translated = (row.get("translated_text") or "").strip()
|
||||||
|
if source and translated:
|
||||||
|
translation_map[source] = translated
|
||||||
|
|
||||||
|
return translation_map
|
||||||
|
|
||||||
|
|
||||||
|
def get_cached_flat_map(language: str) -> dict[str, str]:
|
||||||
|
if not language:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
cache_key = CACHE_KEY.format(language=language)
|
||||||
|
cached = frappe.cache().get_value(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
translation_map = _build_flat_map(language)
|
||||||
|
frappe.cache().set_value(cache_key, translation_map)
|
||||||
|
return translation_map
|
||||||
|
|
||||||
|
|
||||||
|
def clear_translation_cache(language: str | None = None) -> None:
|
||||||
|
if language:
|
||||||
|
frappe.cache().delete_value(CACHE_KEY.format(language=language))
|
||||||
|
return
|
||||||
|
|
||||||
|
for lang_row in frappe.get_all("Translation", distinct=True, pluck="language"):
|
||||||
|
if lang_row:
|
||||||
|
frappe.cache().delete_value(CACHE_KEY.format(language=lang_row))
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_translations(language: str = "ar") -> dict:
|
||||||
|
"""Return flat source_text -> translated_text map for a language."""
|
||||||
|
try:
|
||||||
|
language = (language or "ar").strip()
|
||||||
|
translation_map = get_cached_flat_map(language)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"language": language,
|
||||||
|
"count": len(translation_map),
|
||||||
|
"translations": translation_map,
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
frappe.log_error(title="Translation Tool API Error", message=frappe.get_traceback())
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(exc),
|
||||||
|
"language": language,
|
||||||
|
"count": 0,
|
||||||
|
"translations": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=False)
|
||||||
|
def get_translation(source_text: str, language: str = "ar") -> dict:
|
||||||
|
"""Return a single translation by exact source_text match."""
|
||||||
|
source_text = (source_text or "").strip()
|
||||||
|
language = (language or "ar").strip()
|
||||||
|
|
||||||
|
if not source_text:
|
||||||
|
return {"success": False, "error": "source_text is required"}
|
||||||
|
|
||||||
|
translation_map = get_cached_flat_map(language)
|
||||||
|
translated = translation_map.get(source_text)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"source_text": source_text,
|
||||||
|
"translated_text": translated or source_text,
|
||||||
|
"found": bool(translated),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=False)
|
||||||
|
def get_available_languages() -> dict:
|
||||||
|
"""List languages that have Translation records."""
|
||||||
|
try:
|
||||||
|
languages = frappe.db.sql(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT language
|
||||||
|
FROM `tabTranslation`
|
||||||
|
WHERE language IS NOT NULL AND language != ''
|
||||||
|
ORDER BY language
|
||||||
|
""",
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
language_list = [row.language for row in languages if row.get("language")]
|
||||||
|
if "en" not in language_list:
|
||||||
|
language_list.insert(0, "en")
|
||||||
|
|
||||||
|
return {"success": True, "languages": language_list}
|
||||||
|
except Exception as exc:
|
||||||
|
frappe.log_error(title="Translation Tool API Error", message=frappe.get_traceback())
|
||||||
|
return {"success": False, "error": str(exc), "languages": ["en"]}
|
||||||
0
translation_tool/events/__init__.py
Normal file
0
translation_tool/events/__init__.py
Normal file
BIN
translation_tool/events/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
translation_tool/events/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
translation_tool/events/__pycache__/translation.cpython-310.pyc
Normal file
BIN
translation_tool/events/__pycache__/translation.cpython-310.pyc
Normal file
Binary file not shown.
12
translation_tool/events/translation.py
Normal file
12
translation_tool/events/translation.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from translation_tool.api.translations import clear_translation_cache
|
||||||
|
|
||||||
|
|
||||||
|
def clear_cache_for_doc(doc, method=None):
|
||||||
|
clear_translation_cache(getattr(doc, "language", None))
|
||||||
|
|
||||||
|
if method == "on_update" and doc.has_value_changed("language"):
|
||||||
|
before = doc.get_doc_before_save()
|
||||||
|
if before and before.language:
|
||||||
|
clear_translation_cache(before.language)
|
||||||
13
translation_tool/hooks.py
Normal file
13
translation_tool/hooks.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
app_name = "translation_tool"
|
||||||
|
app_title = "Translation Tool"
|
||||||
|
app_publisher = "support@seeraarabia.com"
|
||||||
|
app_description = "Runtime label translation for ASM UI using Frappe Translation doctype"
|
||||||
|
app_email = "support@seeraarabia.com"
|
||||||
|
app_license = "mit"
|
||||||
|
|
||||||
|
doc_events = {
|
||||||
|
"Translation": {
|
||||||
|
"on_update": "translation_tool.events.translation.clear_cache_for_doc",
|
||||||
|
"on_trash": "translation_tool.events.translation.clear_cache_for_doc",
|
||||||
|
},
|
||||||
|
}
|
||||||
1
translation_tool/modules.txt
Normal file
1
translation_tool/modules.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Translation Tool
|
||||||
6
translation_tool/patches.txt
Normal file
6
translation_tool/patches.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[pre_model_sync]
|
||||||
|
# Patches added in this section will be executed before doctypes are migrated
|
||||||
|
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
|
||||||
|
|
||||||
|
[post_model_sync]
|
||||||
|
# Patches added in this section will be executed after doctypes are migrated
|
||||||
316
translation_tool/public/js/label_translator.js
Normal file
316
translation_tool/public/js/label_translator.js
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
})();
|
||||||
0
translation_tool/translation_tool/__init__.py
Normal file
0
translation_tool/translation_tool/__init__.py
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user