@react-lib-tech/multi-lang-renderer
Version:
A tiny React component library (JS)
759 lines (758 loc) • 25.9 kB
JavaScript
// src/utils/TranslationContext.jsx
import React, {
createContext,
useState,
useContext,
useEffect,
useMemo,
useRef,
useCallback
} from "react";
import { jsx } from "react/jsx-runtime";
var TranslationContext = createContext(null);
var numberLikeRe = /^[\s\d.,:+/-]+$/;
var isNumberLike = (str) => str !== "" && numberLikeRe.test(str);
var defaultSpecials = ["\u2013", "-", "!", "?", "@", "#"];
var toObjectMap = (arr = []) => {
const map = {};
arr.forEach((item) => {
const [key, value] = Object.entries(item)[0] || [];
if (key) map[key] = value;
});
return map;
};
var raf = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 16);
function compressElements(elements) {
const arr = Array.from(elements);
const out = [];
for (let i = 0; i < arr.length; i++) {
let keep = true;
let node = arr[i].parentElement;
while (node) {
if (elements.has(node)) {
keep = false;
break;
}
node = node.parentElement;
}
if (keep) out.push(arr[i]);
}
return out;
}
var CONTAINER_TAGS = [
"button",
"div",
"span",
"p",
"b",
"a",
"strong",
"em",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"ul",
"ol",
"li",
"label",
"input",
"textarea",
"select",
"option",
"table",
"th",
"td",
"form",
"header",
"footer",
"section",
"article",
"aside",
"nav"
];
var CONTAINER_SELECTOR = CONTAINER_TAGS.join(",");
function normalizeText(str = "") {
return String(str).replace(/\s+/g, " ").trim();
}
function containsHTML(s = "") {
return /<[^>]+>/.test(s);
}
function preserveWhitespace(origText = "", core = "") {
const m = origText.match(/^(\s*)([\s\S]*?)(\s*)$/);
const leading = m && m[1] || "";
const trailing = m && m[3] || "";
return leading + (core || "") + trailing;
}
var TranslationProvider = ({
children,
JSONData = [],
lang = "en",
extraTags = [],
extraSpecialChars = [],
attributes = ["title", "placeholder"],
debug = false,
onScan,
// { allTexts, newTexts }
onKeysChange,
// { added, removed, changed }
onMutation = null
// optional callback receives detailed mutation infos
}) => {
const [language, setLanguage] = useState(lang);
const [translations, setTranslations] = useState({});
const [keySet, setKeySet] = useState(/* @__PURE__ */ new Set());
const translationsRef = useRef(translations);
const prevTranslationsRef = useRef(translations);
const keySetRef = useRef(keySet);
const specialsRef = useRef([...defaultSpecials, ...extraSpecialChars]);
const isTranslatingRef = useRef(false);
const processedThisTick = useRef(/* @__PURE__ */ new WeakSet());
const seenTextsRef = useRef(/* @__PURE__ */ new Set());
useEffect(() => {
translationsRef.current = translations;
}, [translations]);
useEffect(() => {
keySetRef.current = keySet;
}, [keySet]);
useEffect(() => {
specialsRef.current = [...defaultSpecials, ...extraSpecialChars];
}, [extraSpecialChars]);
useEffect(() => {
setLanguage(lang);
if (typeof document !== "undefined") {
document.body.setAttribute("lang", lang);
}
}, [lang]);
useEffect(() => {
const obj = toObjectMap(JSONData);
setTranslations(obj);
}, [JSONData]);
useEffect(() => {
const s = new Set(Object.keys(translations || {}));
setKeySet(s);
}, [translations]);
const translate = useCallback((key) => {
if (!key) return "";
return translationsRef.current[key] ?? key;
}, []);
const isSpecial = useCallback((str) => {
return specialsRef.current.some((char) => str.includes(char));
}, []);
const translateElementAttributes = useCallback(
(el) => {
attributes.forEach((attr) => {
try {
if (el.hasAttribute && el.hasAttribute(attr)) {
let key = el.getAttribute("data-lang") || el.getAttribute(attr);
if (!el.hasAttribute("data-lang") && key) {
el.setAttribute("data-lang", key);
}
if (!key && keySetRef.current.has(key)) {
el.setAttribute("data-lang", key);
}
const translated = translate(key || "") || key;
if (el.getAttribute(attr) !== translated) {
el.setAttribute(attr, translated || "");
}
}
} catch (e) {
if (debug) console.warn("translateElementAttributes error", e);
}
});
},
[translate, attributes, debug]
);
const translateTextNodes = useCallback(
(rootEl) => {
if (!rootEl || processedThisTick.current.has(rootEl)) return;
processedThisTick.current.add(rootEl);
translateElementAttributes(rootEl);
const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT, {
acceptNode(node2) {
const t = (node2.textContent || "").trim();
return t ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
});
let node;
const processedContainers = /* @__PURE__ */ new Map();
while (node = walker.nextNode()) {
const textNode = node;
const parent = textNode.parentElement;
if (!parent) continue;
if (!textNode.isConnected || !parent.isConnected) continue;
const container = parent.closest && parent.closest(CONTAINER_SELECTOR) || parent;
const fullText = normalizeText(container.textContent || "");
if (processedContainers.get(container) === fullText) continue;
processedContainers.set(container, fullText);
if (!fullText) continue;
if (isNumberLike(fullText)) continue;
let keyToUse = container.getAttribute("data-lang");
if (keySet.has(fullText)) {
container.setAttribute("data-lang", fullText);
keyToUse = fullText;
}
if (!seenTextsRef.current.has(fullText)) {
seenTextsRef.current.add(fullText);
onScan == null ? void 0 : onScan({
allTexts: Array.from(seenTextsRef.current),
newTexts: [fullText]
});
}
const translatedFull = translate(keyToUse || fullText) || fullText;
if (container.dataset.translated === translatedFull) continue;
if (!translatedFull) continue;
if (normalizeText(translatedFull) === fullText) {
if (container.dataset.translated) delete container.dataset.translated;
continue;
}
try {
if (containsHTML(translatedFull)) {
container.innerHTML = translatedFull;
container.dataset.translated = translatedFull;
const data = document.querySelectorAll(`data-lang[${keyToUse}]`);
console.log(data, "asjdhaskdjashdjkasdsh");
continue;
}
let leaves = [];
const w2 = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT,
{
acceptNode(n) {
const t = (n.textContent || "").trim();
return t ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
}
);
let ln;
while (ln = w2.nextNode()) leaves.push(ln);
if (leaves.length === 0) {
container.textContent = translatedFull;
container.dataset.translated = translatedFull;
continue;
}
if (leaves.length === 1) {
const orig = leaves[0].textContent || "";
const core = normalizeText(translatedFull);
leaves[0].nodeValue = preserveWhitespace(orig, core);
container.dataset.translated = translatedFull;
continue;
}
container.textContent = translatedFull;
container.dataset.translated = translatedFull;
} catch (err) {
if (debug) console.error("translateTextNodes error", err);
try {
const first = container.firstChild;
if (first && first.nodeType === Node.TEXT_NODE) {
first.nodeValue = normalizeText(translatedFull);
container.dataset.translated = translatedFull;
}
} catch (e) {
}
}
}
},
[
translateElementAttributes,
translate,
isSpecial,
isNumberLike,
debug,
onScan
]
);
const initialScan = useCallback(() => {
if (typeof document === "undefined") return;
const targeted = document.querySelectorAll(
"[data-lang], [title], [placeholder]"
);
const tags = Array.from(/* @__PURE__ */ new Set([...CONTAINER_TAGS, ...extraTags]));
const broad = document.querySelectorAll(tags.join(","));
raf(() => {
isTranslatingRef.current = true;
processedThisTick.current = /* @__PURE__ */ new WeakSet();
targeted.forEach((el) => translateTextNodes(el));
broad.forEach((el) => translateTextNodes(el));
const current = translationsRef.current;
const prev = prevTranslationsRef.current || {};
const currentKeys = Object.keys(current);
const prevKeys = Object.keys(prev);
const added = currentKeys.filter((k) => !prevKeys.includes(k));
const removed = prevKeys.filter((k) => !currentKeys.includes(k));
const changed = currentKeys.filter((k) => prevKeys.includes(k) && prev[k] !== current[k]).map((k) => ({ key: k, old: prev[k], new: current[k] }));
onKeysChange == null ? void 0 : onKeysChange({ added, removed, changed });
prevTranslationsRef.current = { ...current };
isTranslatingRef.current = false;
});
}, [extraTags, translateTextNodes, onKeysChange]);
useEffect(() => {
let frameId;
let timeoutId;
frameId = requestAnimationFrame(() => {
timeoutId = setTimeout(() => {
initialScan();
}, 0);
});
return () => {
cancelAnimationFrame(frameId);
clearTimeout(timeoutId);
};
}, [language, JSONData, translations, initialScan]);
useEffect(() => {
if (typeof document === "undefined") return;
const pending = /* @__PURE__ */ new Set();
let scheduled = false;
const flush = () => {
scheduled = false;
const targets = compressElements(pending);
pending.clear();
raf(() => {
isTranslatingRef.current = true;
processedThisTick.current = /* @__PURE__ */ new WeakSet();
targets.forEach((el) => translateTextNodes(el));
isTranslatingRef.current = false;
});
};
const queue = (el) => {
if (!(el instanceof Element) || isTranslatingRef.current) return;
pending.add(el);
if (!scheduled) {
scheduled = true;
raf(flush);
}
};
const observer = new MutationObserver((mutations) => {
var _a;
if (isTranslatingRef.current) return;
for (const m of mutations) {
if (m.target instanceof Element && (m.target.hasAttribute("data-translation-wrapper") || m.target.hasAttribute("data-translation-new"))) {
continue;
}
if (m.type === "childList") {
m.addedNodes.forEach((n) => {
if (n.nodeType === Node.ELEMENT_NODE) queue(n);
});
} else if (m.type === "characterData") {
const el = (_a = m.target) == null ? void 0 : _a.parentElement;
if (el instanceof Element) {
processedThisTick.current.delete(el);
queue(el);
}
} else if (m.type === "attributes") {
if (m.target instanceof Element) {
queue(m.target);
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: [...attributes, "data-lang"]
});
return () => observer.disconnect();
}, [translateTextNodes, attributes, onMutation]);
const value = useMemo(
() => ({
translate,
language,
setLanguage,
reload: initialScan,
keys: keySetRef.current
}),
[translate, language, initialScan]
);
return /* @__PURE__ */ jsx(TranslationContext.Provider, { value, children });
};
var useTranslation = () => {
const ctx = useContext(TranslationContext);
if (!ctx)
throw new Error("useTranslation must be used inside TranslationProvider");
return ctx;
};
// src/utils/GoogleTranslate.jsx
import {
useCallback as useCallback2,
useEffect as useEffect2,
useLayoutEffect,
useRef as useRef2,
useState as useState2
} from "react";
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
var SupportedLang = [
{ code: "af", label: "Afrikaans" },
{ code: "sq", label: "Albanian" },
{ code: "am", label: "Amharic" },
{ code: "ar", label: "Arabic" },
{ code: "hy", label: "Armenian" },
{ code: "az", label: "Azerbaijani" },
{ code: "eu", label: "Basque" },
{ code: "be", label: "Belarusian" },
{ code: "bn", label: "Bengali" },
{ code: "bs", label: "Bosnian" },
{ code: "bg", label: "Bulgarian" },
{ code: "ca", label: "Catalan" },
{ code: "ceb", label: "Cebuano" },
{ code: "ny", label: "Chichewa" },
{ code: "zh-CN", label: "Chinese (Simplified)" },
{ code: "zh-TW", label: "Chinese (Traditional)" },
{ code: "co", label: "Corsican" },
{ code: "hr", label: "Croatian" },
{ code: "cs", label: "Czech" },
{ code: "da", label: "Danish" },
{ code: "nl", label: "Dutch" },
{ code: "en", label: "English" },
{ code: "eo", label: "Esperanto" },
{ code: "et", label: "Estonian" },
{ code: "tl", label: "Filipino" },
{ code: "fi", label: "Finnish" },
{ code: "fr", label: "French" },
{ code: "fy", label: "Frisian" },
{ code: "gl", label: "Galician" },
{ code: "ka", label: "Georgian" },
{ code: "de", label: "German" },
{ code: "el", label: "Greek" },
{ code: "gu", label: "Gujarati" },
{ code: "ht", label: "Haitian Creole" },
{ code: "ha", label: "Hausa" },
{ code: "haw", label: "Hawaiian" },
{ code: "he", label: "Hebrew" },
{ code: "hi", label: "Hindi" },
{ code: "hmn", label: "Hmong" },
{ code: "hu", label: "Hungarian" },
{ code: "is", label: "Icelandic" },
{ code: "ig", label: "Igbo" },
{ code: "id", label: "Indonesian" },
{ code: "ga", label: "Irish" },
{ code: "it", label: "Italian" },
{ code: "ja", label: "Japanese" },
{ code: "jw", label: "Javanese" },
{ code: "kn", label: "Kannada" },
{ code: "kk", label: "Kazakh" },
{ code: "km", label: "Khmer" },
{ code: "ko", label: "Korean" },
{ code: "ku", label: "Kurdish (Kurmanji)" },
{ code: "ky", label: "Kyrgyz" },
{ code: "lo", label: "Lao" },
{ code: "la", label: "Latin" },
{ code: "lv", label: "Latvian" },
{ code: "lt", label: "Lithuanian" },
{ code: "lb", label: "Luxembourgish" },
{ code: "mk", label: "Macedonian" },
{ code: "mg", label: "Malagasy" },
{ code: "ms", label: "Malay" },
{ code: "ml", label: "Malayalam" },
{ code: "mt", label: "Maltese" },
{ code: "mi", label: "Maori" },
{ code: "mr", label: "Marathi" },
{ code: "mn", label: "Mongolian" },
{ code: "my", label: "Myanmar (Burmese)" },
{ code: "ne", label: "Nepali" },
{ code: "no", label: "Norwegian" },
{ code: "ps", label: "Pashto" },
{ code: "fa", label: "Persian" },
{ code: "pl", label: "Polish" },
{ code: "pt", label: "Portuguese" },
{ code: "pa", label: "Punjabi" },
{ code: "ro", label: "Romanian" },
{ code: "ru", label: "Russian" },
{ code: "sm", label: "Samoan" },
{ code: "gd", label: "Scots Gaelic" },
{ code: "sr", label: "Serbian" },
{ code: "st", label: "Sesotho" },
{ code: "sn", label: "Shona" },
{ code: "sd", label: "Sindhi" },
{ code: "si", label: "Sinhala" },
{ code: "sk", label: "Slovak" },
{ code: "sl", label: "Slovenian" },
{ code: "so", label: "Somali" },
{ code: "es", label: "Spanish" },
{ code: "su", label: "Sundanese" },
{ code: "sw", label: "Swahili" },
{ code: "sv", label: "Swedish" },
{ code: "tg", label: "Tajik" },
{ code: "ta", label: "Tamil" },
{ code: "te", label: "Telugu" },
{ code: "th", label: "Thai" },
{ code: "tr", label: "Turkish" },
{ code: "uk", label: "Ukrainian" },
{ code: "ur", label: "Urdu" },
{ code: "uz", label: "Uzbek" },
{ code: "vi", label: "Vietnamese" },
{ code: "cy", label: "Welsh" },
{ code: "xh", label: "Xhosa" },
{ code: "yi", label: "Yiddish" },
{ code: "yo", label: "Yoruba" },
{ code: "zu", label: "Zulu" }
];
var GoogleTranslate = ({
children,
defaultLang = "en",
getSupportedLangs = void 0,
className = "",
SkipTranslationTags = [
"button",
"select",
"input",
"textarea",
"script",
"style",
"code",
"pre",
"noscript",
"iframe",
"object"
],
SkipTranslationClassName = [],
SkipTranslationAttributes = [
"title",
"placeholder",
"aria-label",
"value",
"data-*"
],
SkipTranslationIds = []
}) => {
const [GoogleSelect, setGoogleSelect] = useState2(null);
const containerRef = useRef2(
document.getElementById("google_translate_element")
);
const childrenRef = useRef2(null);
useLayoutEffect(() => {
initGoogleTranslate().then((googleSelect) => {
getSupportedLangs == null ? void 0 : getSupportedLangs(googleSelect);
});
if (!document.getElementById("google-translate-script")) {
const StyelTranslate = `
.skiptranslate iframe { display: none !important; }
#goog-gt-tt { display: none !important; }
.goog-te-gadget-simple {
background-color: #FFF;
border: 1px solid #D5D5D5;
font-size: 10pt;
display: inline-block;
padding: 10px;
border-radius: 10px;
cursor: pointer;
}
.goog-te-combo {
appearance: none !important;
font-family: Roboto, Arial, sans-serif !important;
font-size: 14px !important;
font-weight: 500 !important;
border: 1px solid #c4c4c4 !important;
border-radius: 8px !important;
padding: 10px 12px !important;
background-color: #fff !important;
color: #333 !important;
transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
cursor: pointer !important;
}
.goog-te-combo:focus {
border-color: #1976d2 !important;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.25) !important;
outline: none !important;
}
.goog-te-combo:hover {
border-color: #1976d2 !important;
}
.VIpgJd-ZVi9od-aZ2wEe-wOHMyf{
display: none !important;
}
#google_translate_element {
display: none !important;
}
`;
const script = document.createElement("script");
script.id = "google-translate-script";
script.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
script.type = "text/javascript";
document.head.appendChild(script);
const StyelTranslateStyle = document.createElement("style");
StyelTranslateStyle.innerHTML = StyelTranslate;
document.head.appendChild(StyelTranslateStyle);
}
}, [containerRef]);
useEffect2(() => {
EveryCheck();
}, [defaultLang]);
const EveryCheck = useCallback2(() => {
var _a;
if (GoogleSelect && ((_a = GoogleSelect == null ? void 0 : GoogleSelect.options) == null ? void 0 : _a.length) > 0) {
document.documentElement.setAttribute("lang", defaultLang);
Array.from(GoogleSelect.options).forEach((item, index) => {
var _a2;
if (((_a2 = item == null ? void 0 : item.value) == null ? void 0 : _a2.toString()) === (defaultLang == null ? void 0 : defaultLang.toString())) {
GoogleSelect.value = defaultLang;
GoogleSelect.selectedIndex = index;
GoogleSelect.dispatchEvent(new Event("change", { bubbles: true }));
}
});
}
}, [GoogleSelect, defaultLang]);
const initGoogleTranslate = () => {
return new Promise((reslove, reject) => {
window.googleTranslateElementInit = () => {
const google_translate_element = document.createElement("div");
google_translate_element.id = "google_translate_element";
google_translate_element.setAttribute(
"suppressHydrationWarning",
"true"
);
document.body.appendChild(google_translate_element);
new window.google.translate.TranslateElement(
{
pageLanguage: defaultLang,
includedLanguages: SupportedLang.map((item) => item.code).join(","),
layout: window.google.translate.TranslateElement.InlineLayout.HORIZONTAL
},
"google_translate_element"
);
const checkCombo = () => {
var _a, _b;
const select = document.querySelector(".goog-te-combo");
if (!select) {
const CreateSelect = document.createElement("select");
CreateSelect.className = ".goog-te-combo";
containerRef.current.appendChild(CreateSelect);
setGoogleSelect(document.querySelector(".goog-te-combo"));
}
if (select.childNodes.length === 0) {
InsertOption(select);
const getOptions = Array.from(
(_a = document.querySelector(".goog-te-combo")) == null ? void 0 : _a.options
);
reslove(
getOptions.map((items) => {
return { code: items.value, label: items.innerHTML };
})
);
setGoogleSelect(document.querySelector(".goog-te-combo"));
} else {
const getOptions = Array.from(
(_b = document.querySelector(".goog-te-combo")) == null ? void 0 : _b.options
);
reslove(
getOptions.map((items) => {
return { code: items.value, label: items.innerHTML };
})
);
setGoogleSelect(document.querySelector(".goog-te-combo"));
}
};
checkCombo();
};
});
};
const InsertOption = useCallback2((select) => {
const existing = new Set(Array.from(select == null ? void 0 : select.options).map((o) => o == null ? void 0 : o.value));
SupportedLang.forEach((element) => {
if (!existing.has(element.code)) {
const option = document.createElement("option");
option.value = element.code;
option.textContent = element.label;
select.append(option);
}
});
}, []);
useEffect2(() => {
const protectInteractiveElements = () => {
const safeNodes = document.querySelectorAll(
"body *:not(.goog-te-menu-value):not(.goog-te-combo):not(.goog-te-banner-frame):not(.goog-te-balloon-frame):not(.VIpgJd-ZVi9od-ORHb-OEVmcd)"
);
safeNodes.forEach((el) => {
SkipTranslationTags.forEach((tag) => {
if (el.matches(tag)) {
markAsNoTranslate(el, true);
}
});
SkipTranslationClassName.forEach((cls) => {
if (el.classList.contains(cls)) {
markAsNoTranslate(el, true);
}
});
SkipTranslationIds.forEach((id) => {
if (el.id === id) {
markAsNoTranslate(el, true);
}
});
SkipTranslationAttributes.forEach((attr) => {
if (attr === "data-*") {
for (const at of el.attributes) {
if (at.name.startsWith("data-")) {
markAsNoTranslate(el, false);
}
}
} else if (el.hasAttribute(attr)) {
markAsNoTranslate(el, false);
}
});
if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {
const text = el.textContent.trim();
if (isNonTranslatableText(text)) {
markAsNoTranslate(el, true);
}
}
});
};
protectInteractiveElements();
const observer = new MutationObserver(protectInteractiveElements);
observer.observe(document.body, { childList: true, subtree: true });
return () => observer.disconnect();
}, [
SkipTranslationTags,
SkipTranslationClassName,
SkipTranslationIds,
SkipTranslationAttributes
]);
const markAsNoTranslate = (el, fullSkip = true) => {
if (fullSkip) {
if (!el.hasAttribute("translate")) el.setAttribute("translate", "no");
el.classList.add("notranslate");
el.setAttribute("data-lang", "notranslate");
} else {
el.setAttribute("translate", "no-attr");
el.classList.add("notranslate-attr");
}
for (const attr of Array.from(el.attributes)) {
if (attr.value === "" || attr.value == null) {
el.removeAttribute(attr.name);
}
}
};
const isNonTranslatableText = (text) => {
return /^\s*[{[].*[}\]]\s*$/.test(text) || // JSON-like
/^[A-Za-z0-9+/=]{20,}$/.test(text) || // base64/hash
/function\s*\(|=>/.test(text);
};
useEffect2(() => {
const removeBodyStyle = () => {
document.body.removeAttribute("style");
};
removeBodyStyle();
const observer = new MutationObserver(() => {
removeBodyStyle();
if (document.documentElement.getAttribute("lang") === "auto") {
document.documentElement.setAttribute("lang", defaultLang);
}
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ["style", "lang"]
});
return () => observer.disconnect();
}, []);
return /* @__PURE__ */ jsx2(Fragment, { children: /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsx2("div", { ref: childrenRef, children }) }) });
};
export {
GoogleTranslate,
TranslationProvider,
useTranslation
};
//# sourceMappingURL=index.mjs.map