UNPKG

@react-lib-tech/multi-lang-renderer

Version:

A tiny React component library (JS)

759 lines (758 loc) 25.9 kB
// 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