UNPKG

pseudo-localization

Version:

pseudo-localization for internationalization testing

106 lines 4.55 kB
import { pseudoLocalizeString, } from "./localize.js"; function isNonEmptyString(str) { return !!str && typeof str === 'string'; } /** * A container for pseudo-localization of the DOM. * * @example * new PseudoLocalizeDom().start(); */ export class PseudoLocalizeDom { /** * Indicates which elements the pseudo-localization is currently running on. */ #enabledOn = new WeakSet(); /** * Start pseudo-localizing the DOM. * * Returns a stop function to disable pseudo-localization. */ start({ strategy = 'accented', blacklistedNodeNames = ['STYLE'], root, } = {}) { const rootEl = root ?? document.body; if (this.#enabledOn.has(rootEl)) { console.warn(`Start aborted. pseudo-localization is already enabled on`, rootEl); return () => { }; } const observerConfig = { characterData: true, childList: true, subtree: true, }; const textNodesUnder = (node) => { const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, (node) => { const isAllWhitespace = node.nodeValue && !/[^\s]/.test(node.nodeValue); if (isAllWhitespace) { return NodeFilter.FILTER_REJECT; } const isBlacklistedNode = node.parentElement && blacklistedNodeNames.includes(node.parentElement.nodeName); if (isBlacklistedNode) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; }); let currNode; const textNodes = []; while ((currNode = walker.nextNode())) textNodes.push(currNode); return textNodes; }; const pseudoLocalize = (node) => { const textNodesUnderElement = textNodesUnder(node); for (const textNode of textNodesUnderElement) { const nodeValue = textNode.nodeValue; if (isNonEmptyString(nodeValue)) { textNode.nodeValue = pseudoLocalizeString(nodeValue, { strategy }); } } }; // Pseudo localize the DOM pseudoLocalize(document.body); // Start observing the DOM for changes and run // pseudo localization on any added text nodes const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { // Turn the observer off while performing dom manipulation to prevent // infinite dom mutation callback loops observer.disconnect(); // For every node added, recurse down it's subtree and convert // all children as well mutation.addedNodes.forEach(pseudoLocalize.bind(this)); observer.observe(document.body, observerConfig); } else if (mutation.type === 'characterData') { const nodeValue = mutation.target.nodeValue; const isBlacklistedNode = !!mutation.target.parentElement && blacklistedNodeNames.includes(mutation.target.parentElement.nodeName); if (isNonEmptyString(nodeValue) && !isBlacklistedNode) { // Turn the observer off while performing dom manipulation to prevent // infinite dom mutation callback loops observer.disconnect(); // The target will always be a text node so it can be converted // directly mutation.target.nodeValue = pseudoLocalizeString(nodeValue, { strategy, }); observer.observe(document.body, observerConfig); } } } }); observer.observe(document.body, observerConfig); this.#enabledOn.add(rootEl); const stop = () => { if (!this.#enabledOn.has(rootEl)) { console.warn('Stop aborted. pseudo-localization is not running on', rootEl); return; } observer.disconnect(); this.#enabledOn.delete(rootEl); }; return stop; } } //# sourceMappingURL=dom.js.map