@lizardbyte/shared-web 2026.726.204939 → 2026.812.190302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/crowdin.js CHANGED
@@ -1,2 +1,2 @@
1
- (()=>{var t={714(t,e,i){const o=i(836);function n(t,e){e>=100||globalThis.setTimeout(function(){s(t,e+1)},50)}function s(t,e=0){const i=document.getElementById("crowdin-language-picker");if("sphinx"===t){const o=document.getElementsByClassName("cr-picker-button")[0],s=document.getElementsByClassName("sidebar-sticky")[0];return null===i||void 0===o||void 0===s?void n(t,e):(i.classList.remove("cr-position-bottom-left"),i.style.width=o.offsetWidth+10+"px",i.style.position="relative",i.style.left="10px",i.style.bottom="10px",void s.appendChild(i))}const o=document.querySelector(".sidebar .sidebar-elems")||document.querySelector(".sidebar");null!==i&&null!==o?(i.classList.remove("cr-position-bottom-left"),i.classList.add("rustdoc-crowdin-picker"),i.style.position="static",i.style.left="auto",i.style.bottom="auto",o.appendChild(i)):n(t,e)}function r(t="LizardByte",e=null){["LizardByte","LizardByte-docs"].includes(t)?["sphinx","rustdoc",null].includes(e)?(function(){if("function"!=typeof globalThis.fetch)return;if(globalThis._crowdinMirrorInstalled)return;globalThis._crowdinMirrorInstalled=!0;const t=globalThis.fetch.bind(globalThis);globalThis.fetch=function(e,i){if("string"==typeof e){let o;try{o=new URL(e)}catch{}if("https:"===o?.protocol&&"distributions.crowdin.net"===o.hostname){const e="https://cdn.jsdelivr.net/gh/LizardByte/i18n@dist"+o.pathname+o.search+o.hash;return t(e,i)}}return t(e,i)}}(),o("https://website-translator.app.crowdin.net/assets/proxy-translator.js",function(){let i={bg:"Български (Bulgarian)",cs:"Čeština (Czech)",de:"Deutsch (German)",en:"English","en-GB":"English, United Kingdom","en-US":"English, United States","es-ES":"Español (Spanish)",fr:"Français (French)",hu:"Magyar (Hungarian)",it:"Italiano (Italian)",ja:"日本語 (Japanese)",ko:"한국어 (Korean)",pl:"Polski (Polish)","pt-BR":"Português, Brasileiro (Portuguese, Brazilian)","pt-PT":"Português (Portuguese)",ru:"Русский (Russian)","sv-SE":"svenska (Swedish)",tr:"Türkçe (Turkish)",uk:"Українська (Ukranian)",vi:"Tiếng Việt (Vietnamese)","zh-CN":"简体中文 (Chinese Simplified)","zh-TW":"繁體中文 (Chinese Traditional)"};i=Object.fromEntries(Object.entries(i).sort((t,e)=>t[1].localeCompare(e[1])));let o=globalThis.location.origin;globalThis.proxyTranslator.init({baseUrl:o,distribution:{LizardByte:{baseUrl:"https://app.lizardbyte.dev",distribution:"458f881791aebba1d4dde491bw4"},"LizardByte-docs":{baseUrl:"https://docs.lizardbyte.dev",distribution:"d6c830ba4b41106fefe5d391bw4"}}[t].distribution,defaultLanguage:"en",languageTitles:i,showDefaultLanguageInUrl:!1,languageRoutingMethod:"query",position:"bottom-left",submenuPosition:"top-left",poweredBy:!1}),null!==e&&s(e)})):console.error('Invalid UI. Must be "sphinx", "rustdoc", or null'):console.error('Invalid project. Must be "LizardByte" or "LizardByte-docs"')}void 0!==globalThis.window&&(globalThis.initCrowdIn=r),t.exports=r},836(t){function e(t,e){const i=document.createElement("script");i.src=t,i.async=!0,i.onload=()=>{e&&e(null,i)},i.onerror=()=>{e&&e(new Error(`Failed to load script: ${t}`))},document.head.appendChild(i)}void 0!==globalThis.window&&(globalThis.loadScript=e),t.exports=e}};const e={};!function i(o){const n=e[o];if(void 0!==n)return n.exports;const s=e[o]={exports:{}};return t[o](s,s.exports,i),s.exports}(714)})();
1
+ (()=>{var e={714(e,t,n){const i=n(836),o=["a","abbr","b","cite","code","del","em","i","ins","kbd","mark","q","s","samp","small","span","strong","sub","sup","time","u","var"].join(",");function s(e){return e instanceof globalThis.Element&&e.matches(o)?e:null}function r(e){const t=e.trimStart();return e.slice(0,e.length-t.length)}function l(e){const t=e.trimEnd();return e.slice(t.length)}function a(e){const t=new globalThis.MutationObserver(n);function n(){t.disconnect(),function(e){e.forEach(e=>{const t=function(e){if(e.node.isConnected)return e.node;const t=e.previousInline?.nextSibling;if(t instanceof globalThis.Text&&t.isConnected)return e.node=t,e.node;const n=e.nextInline?.previousSibling;if(n instanceof globalThis.Text&&n.isConnected)return e.node=n,e.node;if(!e.whitespaceOnly)return null;const i=document.createTextNode("");if(e.previousInline?.isConnected)e.previousInline.after(i);else{if(!e.nextInline?.isConnected)return null;e.nextInline.before(i)}return e.node=i,e.node}(e);if(null===t)return;const n=r(t.data);e.leading&&n!==e.leadingWhitespace&&(t.data=e.leadingWhitespace+t.data.slice(n.length));const i=l(t.data);if(e.trailing&&i!==e.trailingWhitespace){const n=t.data.length-i.length;t.data=t.data.slice(0,n)+e.trailingWhitespace}})}(e),t.observe(document.body,{characterData:!0,childList:!0,subtree:!0})}return n}function c(e,t){t>=100||globalThis.setTimeout(function(){d(e,t+1)},50)}function d(e,t=0){const n=document.getElementById("crowdin-language-picker");if("sphinx"===e){const i=document.getElementsByClassName("cr-picker-button")[0],o=document.getElementsByClassName("sidebar-sticky")[0];return null===n||void 0===i||void 0===o?void c(e,t):(n.classList.remove("cr-position-bottom-left"),n.style.width=i.offsetWidth+10+"px",n.style.position="relative",n.style.left="10px",n.style.bottom="10px",void o.appendChild(n))}const i=document.querySelector(".sidebar .sidebar-elems")||document.querySelector(".sidebar");null!==n&&null!==i?(n.classList.remove("cr-position-bottom-left"),n.classList.add("rustdoc-crowdin-picker"),n.style.position="static",n.style.left="auto",n.style.bottom="auto",i.appendChild(n)):c(e,t)}function u(e="LizardByte",t=null){["LizardByte","LizardByte-docs"].includes(e)?["sphinx","rustdoc",null].includes(t)?(function(){if("function"!=typeof globalThis.fetch)return;if(globalThis._crowdinMirrorInstalled)return;globalThis._crowdinMirrorInstalled=!0;const e=globalThis.fetch.bind(globalThis);globalThis.fetch=function(t,n){if("string"==typeof t){let i;try{i=new URL(t)}catch{}if("https:"===i?.protocol&&"distributions.crowdin.net"===i.hostname){const t="https://cdn.jsdelivr.net/gh/LizardByte/i18n@dist"+i.pathname+i.search+i.hash;return e(t,n)}}return e(t,n)}}(),i("https://website-translator.app.crowdin.net/assets/proxy-translator.js",function(){let n={bg:"Български (Bulgarian)",cs:"Čeština (Czech)",de:"Deutsch (German)",en:"English","en-GB":"English, United Kingdom","en-US":"English, United States","es-ES":"Español (Spanish)",fr:"Français (French)",hu:"Magyar (Hungarian)",it:"Italiano (Italian)",ja:"日本語 (Japanese)",ko:"한국어 (Korean)",pl:"Polski (Polish)","pt-BR":"Português, Brasileiro (Portuguese, Brazilian)","pt-PT":"Português (Portuguese)",ru:"Русский (Russian)","sv-SE":"svenska (Swedish)",tr:"Türkçe (Turkish)",uk:"Українська (Ukranian)",vi:"Tiếng Việt (Vietnamese)","zh-CN":"简体中文 (Chinese Simplified)","zh-TW":"繁體中文 (Chinese Traditional)"};n=Object.fromEntries(Object.entries(n).sort((e,t)=>e[1].localeCompare(t[1])));let i=globalThis.location.origin;const o=function(){const e=[],t=document.createTreeWalker(document.body,globalThis.NodeFilter.SHOW_TEXT);let n=t.nextNode();for(;null!==n;){const i=s(n.previousSibling),o=s(n.nextSibling),a=null===i?"":r(n.data),c=null===o?"":l(n.data),d=""!==a,u=""!==c;(d||u)&&e.push({node:n,leading:d,leadingWhitespace:a,trailing:u,trailingWhitespace:c,previousInline:i,nextInline:o,whitespaceOnly:""===n.data.trim()}),n=t.nextNode()}return e}();globalThis.proxyTranslator.init({baseUrl:i,callback:a(o),distribution:{LizardByte:{baseUrl:"https://app.lizardbyte.dev",distribution:"458f881791aebba1d4dde491bw4"},"LizardByte-docs":{baseUrl:"https://docs.lizardbyte.dev",distribution:"d6c830ba4b41106fefe5d391bw4"}}[e].distribution,defaultLanguage:"en",languageTitles:n,showDefaultLanguageInUrl:!1,languageRoutingMethod:"query",position:"bottom-left",submenuPosition:"top-left",poweredBy:!1}),null!==t&&d(t)})):console.error('Invalid UI. Must be "sphinx", "rustdoc", or null'):console.error('Invalid project. Must be "LizardByte" or "LizardByte-docs"')}void 0!==globalThis.window&&(globalThis.initCrowdIn=u),e.exports=u},836(e){function t(e,t){const n=document.createElement("script");n.src=e,n.async=!0,n.onload=()=>{t&&t(null,n)},n.onerror=()=>{t&&t(new Error(`Failed to load script: ${e}`))},document.head.appendChild(n)}void 0!==globalThis.window&&(globalThis.loadScript=t),e.exports=t}};const t={};!function n(i){const o=t[i];if(void 0!==o)return o.exports;const s=t[i]={exports:{}};return e[i](s,s.exports,n),s.exports}(714)})();
2
2
  //# sourceMappingURL=crowdin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"crowdin.js","mappings":"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","sources":["webpack://@lizardbyte/shared-web/./src/js/crowdin.js","webpack://@lizardbyte/shared-web/./src/js/load-script.js","webpack://@lizardbyte/shared-web/webpack/bootstrap","webpack://@lizardbyte/shared-web/webpack/startup"],"sourcesContent":["const loadScript = require('./load-script');\n\n/**\n * jsDelivr CDN URL serving Crowdin distribution files from the crowdin-dist\n * git branch. jsDelivr unconditionally sets Access-Control-Allow-Origin: *,\n * so cross-origin fetch() calls succeed without any browser plugin.\n * The branch is refreshed daily by the \"Sync Crowdin Distribution\" workflow.\n * Structure mirrors https://distributions.crowdin.net/<hash>/… exactly.\n * @type {string}\n */\nconst CROWDIN_DIST_MIRROR = 'https://cdn.jsdelivr.net/gh/LizardByte/i18n@dist';\nconst CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS = 100;\nconst CROWDIN_PLATFORM_STYLING_RETRY_DELAY_MS = 50;\n\n/**\n * Monkey-patches globalThis.fetch to redirect Crowdin distribution requests to\n * the self-hosted GitHub Pages mirror.\n *\n * Must be called BEFORE proxy-translator.js is loaded so that every fetch()\n * the script makes is already intercepted.\n *\n * Idempotent – installs the interceptor at most once per page.\n */\nfunction _installCrowdinFetchInterceptor() {\n if (typeof globalThis.fetch !== 'function') return;\n if (globalThis._crowdinMirrorInstalled) return;\n globalThis._crowdinMirrorInstalled = true;\n\n const _origFetch = globalThis.fetch.bind(globalThis);\n\n globalThis.fetch = function crowdinMirrorFetch(url, options) {\n if (typeof url === 'string') {\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n // Not a valid absolute URL – pass through unchanged.\n }\n // Use exact hostname comparison to avoid prefix-match bypasses\n // (e.g. distributions.crowdin.net.evil.com) that would be flagged\n // by incomplete URL sanitisation checks.\n if (parsed?.protocol === 'https:' && parsed.hostname === 'distributions.crowdin.net') {\n const mirroredUrl = CROWDIN_DIST_MIRROR + parsed.pathname + parsed.search + parsed.hash;\n return _origFetch(mirroredUrl, options);\n }\n }\n return _origFetch(url, options);\n };\n}\n\n/**\n * Re-attempts platform styling while Crowdin inserts the language picker.\n * @param {string} platform - UI platform ('sphinx' or 'rustdoc').\n * @param {number} attempt - Current retry count.\n */\nfunction _retryCrowdinPlatformStyling(platform, attempt) {\n if (attempt >= CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS) {\n return;\n }\n\n globalThis.setTimeout(function() {\n _applyCrowdinPlatformStyling(platform, attempt + 1);\n }, CROWDIN_PLATFORM_STYLING_RETRY_DELAY_MS);\n}\n\n/**\n * Applies platform-specific placement after the Crowdin picker exists.\n * @param {string} platform - UI platform ('sphinx' or 'rustdoc').\n * @param {number} attempt - Current retry count.\n */\nfunction _applyCrowdinPlatformStyling(platform, attempt = 0) {\n const container = document.getElementById('crowdin-language-picker');\n\n if (platform === 'sphinx') {\n const button = document.getElementsByClassName('cr-picker-button')[0];\n const sidebar = document.getElementsByClassName('sidebar-sticky')[0];\n\n if (container === null || button === undefined || sidebar === undefined) {\n _retryCrowdinPlatformStyling(platform, attempt);\n return;\n }\n\n container.classList.remove('cr-position-bottom-left');\n container.style.width = button.offsetWidth + 10 + 'px';\n container.style.position = 'relative';\n container.style.left = '10px';\n container.style.bottom = '10px';\n\n // move button to related pages\n sidebar.appendChild(container);\n return;\n }\n\n const sidebar = document.querySelector('.sidebar .sidebar-elems') || document.querySelector('.sidebar');\n\n if (container === null || sidebar === null) {\n _retryCrowdinPlatformStyling(platform, attempt);\n return;\n }\n\n container.classList.remove('cr-position-bottom-left');\n container.classList.add('rustdoc-crowdin-picker');\n container.style.position = 'static';\n container.style.left = 'auto';\n container.style.bottom = 'auto';\n\n sidebar.appendChild(container);\n}\n\n/**\n * Initializes Crowdin translation widget based on project and UI platform.\n * @param {string} project - Project name ('LizardByte' or 'LizardByte-docs').\n * @param {string|null} platform - UI platform ('sphinx', 'rustdoc', or null).\n */\nfunction initCrowdIn(project = 'LizardByte', platform = null) {\n // Input validation\n if (!['LizardByte', 'LizardByte-docs'].includes(project)) {\n console.error('Invalid project. Must be \"LizardByte\" or \"LizardByte-docs\"');\n return;\n }\n if (!['sphinx', 'rustdoc', null].includes(platform)) {\n console.error('Invalid UI. Must be \"sphinx\", \"rustdoc\", or null');\n return;\n }\n\n // Redirect distribution CDN requests to our self-hosted GitHub Pages mirror\n // before the script is even loaded so every fetch() it makes is intercepted.\n _installCrowdinFetchInterceptor();\n\n loadScript('https://website-translator.app.crowdin.net/assets/proxy-translator.js', function() {\n // Configure base settings based on project\n const projectSettings = {\n 'LizardByte': {\n baseUrl: \"https://app.lizardbyte.dev\",\n distribution: \"458f881791aebba1d4dde491bw4\",\n },\n 'LizardByte-docs': {\n baseUrl: \"https://docs.lizardbyte.dev\",\n distribution: \"d6c830ba4b41106fefe5d391bw4\",\n }\n };\n\n let languageTitles = {\n \"bg\": \"Български (Bulgarian)\",\n \"cs\": \"Čeština (Czech)\",\n \"de\": \"Deutsch (German)\",\n \"en\": \"English\",\n \"en-GB\": \"English, United Kingdom\",\n \"en-US\": \"English, United States\",\n \"es-ES\": \"Español (Spanish)\",\n \"fr\": \"Français (French)\",\n \"hu\": \"Magyar (Hungarian)\",\n \"it\": \"Italiano (Italian)\",\n \"ja\": \"日本語 (Japanese)\",\n \"ko\": \"한국어 (Korean)\",\n \"pl\": \"Polski (Polish)\",\n \"pt-BR\": \"Português, Brasileiro (Portuguese, Brazilian)\",\n \"pt-PT\": \"Português (Portuguese)\",\n \"ru\": \"Русский (Russian)\",\n \"sv-SE\": \"svenska (Swedish)\",\n \"tr\": \"Türkçe (Turkish)\",\n \"uk\": \"Українська (Ukranian)\",\n \"vi\": \"Tiếng Việt (Vietnamese)\",\n \"zh-CN\": \"简体中文 (Chinese Simplified)\",\n \"zh-TW\": \"繁體中文 (Chinese Traditional)\",\n };\n // sort languages by name\n languageTitles = Object.fromEntries(Object.entries(languageTitles).sort((a, b) => a[1].localeCompare(b[1])));\n\n // use this to allow translations to work on PR preview builds\n let currentBaseUrl = globalThis.location.origin;\n\n // Initialize Crowdin translator\n globalThis.proxyTranslator.init({\n baseUrl: currentBaseUrl,\n distribution: projectSettings[project].distribution,\n defaultLanguage: \"en\",\n languageTitles: languageTitles,\n showDefaultLanguageInUrl: false,\n languageRoutingMethod: \"query\",\n position: \"bottom-left\",\n submenuPosition: \"top-left\",\n poweredBy: false,\n });\n\n // Apply styling based on UI framework\n if (platform === null) {\n return;\n }\n\n _applyCrowdinPlatformStyling(platform);\n });\n}\n\n// Expose to the global scope\nif (globalThis.window !== undefined) {\n globalThis.initCrowdIn = initCrowdIn;\n}\n\nmodule.exports = initCrowdIn;\n","/**\n * Load a script asynchronously, and add it to the DOM. Optionally, call a callback when the script has loaded.\n * @param {string} url The URL of the script to load.\n * @param {Function} callback An optional callback to call when the script has loaded.\n */\nfunction loadScript(url, callback) {\n const script = document.createElement('script');\n script.src = url;\n script.async = true;\n\n script.onload = () => {\n if (callback) callback(null, script);\n };\n\n script.onerror = () => {\n if (callback) callback(new Error(`Failed to load script: ${url}`));\n };\n\n document.head.appendChild(script);\n}\n\n// Expose to the global scope\nif (globalThis.window !== undefined) {\n globalThis.loadScript = loadScript;\n}\n\nmodule.exports = loadScript;\n","// The module cache\nconst __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tconst cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tconst module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nlet __webpack_exports__ = __webpack_require__(714);\n"],"names":["loadScript","__webpack_require__","_retryCrowdinPlatformStyling","platform","attempt","globalThis","setTimeout","_applyCrowdinPlatformStyling","container","document","getElementById","button","getElementsByClassName","sidebar","undefined","classList","remove","style","width","offsetWidth","position","left","bottom","appendChild","querySelector","add","initCrowdIn","project","includes","fetch","_crowdinMirrorInstalled","_origFetch","bind","url","options","parsed","URL","protocol","hostname","mirroredUrl","pathname","search","hash","_installCrowdinFetchInterceptor","languageTitles","bg","cs","de","en","fr","hu","it","ja","ko","pl","ru","tr","uk","vi","Object","fromEntries","entries","sort","a","b","localeCompare","currentBaseUrl","location","origin","proxyTranslator","init","baseUrl","distribution","LizardByte","defaultLanguage","showDefaultLanguageInUrl","languageRoutingMethod","submenuPosition","poweredBy","console","error","window","module","exports","callback","script","createElement","src","async","onload","onerror","Error","head","__webpack_module_cache__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""}
1
+ {"version":3,"file":"crowdin.js","mappings":"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","sources":["webpack://@lizardbyte/shared-web/./src/js/crowdin.js","webpack://@lizardbyte/shared-web/./src/js/load-script.js","webpack://@lizardbyte/shared-web/webpack/bootstrap","webpack://@lizardbyte/shared-web/webpack/startup"],"sourcesContent":["const loadScript = require('./load-script');\n\n/**\n * jsDelivr CDN URL serving Crowdin distribution files from the crowdin-dist\n * git branch. jsDelivr unconditionally sets Access-Control-Allow-Origin: *,\n * so cross-origin fetch() calls succeed without any browser plugin.\n * The branch is refreshed daily by the \"Sync Crowdin Distribution\" workflow.\n * Structure mirrors https://distributions.crowdin.net/<hash>/… exactly.\n * @type {string}\n */\nconst CROWDIN_DIST_MIRROR = 'https://cdn.jsdelivr.net/gh/LizardByte/i18n@dist';\nconst CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS = 100;\nconst CROWDIN_PLATFORM_STYLING_RETRY_DELAY_MS = 50;\nconst CROWDIN_INLINE_ELEMENT_SELECTOR = [\n 'a',\n 'abbr',\n 'b',\n 'cite',\n 'code',\n 'del',\n 'em',\n 'i',\n 'ins',\n 'kbd',\n 'mark',\n 'q',\n 's',\n 'samp',\n 'small',\n 'span',\n 'strong',\n 'sub',\n 'sup',\n 'time',\n 'u',\n 'var',\n].join(',');\n\n/**\n * Returns a sibling when it is an inline element whose boundary Crowdin may rewrite.\n * @param {Node|null} sibling Candidate sibling.\n * @returns {Element|null} Matching inline element.\n */\nfunction _getCrowdinInlineSibling(sibling) {\n if (!(sibling instanceof globalThis.Element)) return null;\n return sibling.matches(CROWDIN_INLINE_ELEMENT_SELECTOR) ? sibling : null;\n}\n\n/**\n * Returns the exact whitespace at the start of a string.\n * @param {string} text Text to inspect.\n * @returns {string} Leading whitespace.\n */\nfunction _getLeadingWhitespace(text) {\n const trimmedText = text.trimStart();\n return text.slice(0, text.length - trimmedText.length);\n}\n\n/**\n * Returns the exact whitespace at the end of a string.\n * @param {string} text Text to inspect.\n * @returns {string} Trailing whitespace.\n */\nfunction _getTrailingWhitespace(text) {\n const trimmedText = text.trimEnd();\n return text.slice(trimmedText.length);\n}\n\n/**\n * Records whitespace that separates text from inline elements before Crowdin translates the page.\n * @returns {Array<{\n * node: Text,\n * leading: boolean,\n * leadingWhitespace: string,\n * trailing: boolean,\n * trailingWhitespace: string,\n * previousInline: Element|null,\n * nextInline: Element|null,\n * whitespaceOnly: boolean\n * }>} Recorded text-node boundaries.\n */\nfunction _captureCrowdinWhitespaceBoundaries() {\n const boundaries = [];\n const walker = document.createTreeWalker(document.body, globalThis.NodeFilter.SHOW_TEXT);\n let node = walker.nextNode();\n\n while (node !== null) {\n const previousInline = _getCrowdinInlineSibling(node.previousSibling);\n const nextInline = _getCrowdinInlineSibling(node.nextSibling);\n const leadingWhitespace = previousInline === null ? '' : _getLeadingWhitespace(node.data);\n const trailingWhitespace = nextInline === null ? '' : _getTrailingWhitespace(node.data);\n const leading = leadingWhitespace !== '';\n const trailing = trailingWhitespace !== '';\n\n if (leading || trailing) {\n boundaries.push({\n node,\n leading,\n leadingWhitespace,\n trailing,\n trailingWhitespace,\n previousInline,\n nextInline,\n whitespaceOnly: node.data.trim() === '',\n });\n }\n\n node = walker.nextNode();\n }\n\n return boundaries;\n}\n\n/**\n * Finds the current text node for a boundary after Crowdin changes the DOM.\n * @param {Object} boundary Recorded whitespace boundary.\n * @returns {Text|null} Current or recreated text node.\n */\nfunction _resolveCrowdinWhitespaceNode(boundary) {\n if (boundary.node.isConnected) return boundary.node;\n\n const previousReplacement = boundary.previousInline?.nextSibling;\n if (previousReplacement instanceof globalThis.Text && previousReplacement.isConnected) {\n boundary.node = previousReplacement;\n return boundary.node;\n }\n const nextReplacement = boundary.nextInline?.previousSibling;\n if (nextReplacement instanceof globalThis.Text && nextReplacement.isConnected) {\n boundary.node = nextReplacement;\n return boundary.node;\n }\n if (!boundary.whitespaceOnly) return null;\n\n const replacement = document.createTextNode('');\n if (boundary.previousInline?.isConnected) {\n boundary.previousInline.after(replacement);\n } else if (boundary.nextInline?.isConnected) {\n boundary.nextInline.before(replacement);\n } else {\n return null;\n }\n\n boundary.node = replacement;\n return boundary.node;\n}\n\n/**\n * Restores whitespace that Crowdin removed from translated text-node boundaries.\n * @param {Array<Object>} boundaries Recorded text-node boundaries.\n */\nfunction _restoreCrowdinWhitespaceBoundaries(boundaries) {\n boundaries.forEach((boundary) => {\n const node = _resolveCrowdinWhitespaceNode(boundary);\n if (node === null) return;\n\n const currentLeadingWhitespace = _getLeadingWhitespace(node.data);\n if (boundary.leading && currentLeadingWhitespace !== boundary.leadingWhitespace) {\n node.data = boundary.leadingWhitespace + node.data.slice(currentLeadingWhitespace.length);\n }\n const currentTrailingWhitespace = _getTrailingWhitespace(node.data);\n if (boundary.trailing && currentTrailingWhitespace !== boundary.trailingWhitespace) {\n const translatedTextEnd = node.data.length - currentTrailingWhitespace.length;\n node.data = node.data.slice(0, translatedTextEnd) + boundary.trailingWhitespace;\n }\n });\n}\n\n/**\n * Creates the Website Translator callback and observes later translation mutations.\n * @param {Array<Object>} boundaries Recorded text-node boundaries.\n * @returns {Function} Website Translator callback.\n */\nfunction _createCrowdinTranslationCallback(boundaries) {\n const translationObserver = new globalThis.MutationObserver(restoreAndObserve);\n\n function restoreAndObserve() {\n translationObserver.disconnect();\n _restoreCrowdinWhitespaceBoundaries(boundaries);\n translationObserver.observe(document.body, {\n characterData: true,\n childList: true,\n subtree: true,\n });\n }\n\n return restoreAndObserve;\n}\n\n/**\n * Monkey-patches globalThis.fetch to redirect Crowdin distribution requests to\n * the self-hosted GitHub Pages mirror.\n *\n * Must be called BEFORE proxy-translator.js is loaded so that every fetch()\n * the script makes is already intercepted.\n *\n * Idempotent – installs the interceptor at most once per page.\n */\nfunction _installCrowdinFetchInterceptor() {\n if (typeof globalThis.fetch !== 'function') return;\n if (globalThis._crowdinMirrorInstalled) return;\n globalThis._crowdinMirrorInstalled = true;\n\n const _origFetch = globalThis.fetch.bind(globalThis);\n\n globalThis.fetch = function crowdinMirrorFetch(url, options) {\n if (typeof url === 'string') {\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n // Not a valid absolute URL – pass through unchanged.\n }\n // Use exact hostname comparison to avoid prefix-match bypasses\n // (e.g. distributions.crowdin.net.evil.com) that would be flagged\n // by incomplete URL sanitisation checks.\n if (parsed?.protocol === 'https:' && parsed.hostname === 'distributions.crowdin.net') {\n const mirroredUrl = CROWDIN_DIST_MIRROR + parsed.pathname + parsed.search + parsed.hash;\n return _origFetch(mirroredUrl, options);\n }\n }\n return _origFetch(url, options);\n };\n}\n\n/**\n * Re-attempts platform styling while Crowdin inserts the language picker.\n * @param {string} platform - UI platform ('sphinx' or 'rustdoc').\n * @param {number} attempt - Current retry count.\n */\nfunction _retryCrowdinPlatformStyling(platform, attempt) {\n if (attempt >= CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS) {\n return;\n }\n\n globalThis.setTimeout(function() {\n _applyCrowdinPlatformStyling(platform, attempt + 1);\n }, CROWDIN_PLATFORM_STYLING_RETRY_DELAY_MS);\n}\n\n/**\n * Applies platform-specific placement after the Crowdin picker exists.\n * @param {string} platform - UI platform ('sphinx' or 'rustdoc').\n * @param {number} attempt - Current retry count.\n */\nfunction _applyCrowdinPlatformStyling(platform, attempt = 0) {\n const container = document.getElementById('crowdin-language-picker');\n\n if (platform === 'sphinx') {\n const button = document.getElementsByClassName('cr-picker-button')[0];\n const sidebar = document.getElementsByClassName('sidebar-sticky')[0];\n\n if (container === null || button === undefined || sidebar === undefined) {\n _retryCrowdinPlatformStyling(platform, attempt);\n return;\n }\n\n container.classList.remove('cr-position-bottom-left');\n container.style.width = button.offsetWidth + 10 + 'px';\n container.style.position = 'relative';\n container.style.left = '10px';\n container.style.bottom = '10px';\n\n // move button to related pages\n sidebar.appendChild(container);\n return;\n }\n\n const sidebar = document.querySelector('.sidebar .sidebar-elems') || document.querySelector('.sidebar');\n\n if (container === null || sidebar === null) {\n _retryCrowdinPlatformStyling(platform, attempt);\n return;\n }\n\n container.classList.remove('cr-position-bottom-left');\n container.classList.add('rustdoc-crowdin-picker');\n container.style.position = 'static';\n container.style.left = 'auto';\n container.style.bottom = 'auto';\n\n sidebar.appendChild(container);\n}\n\n/**\n * Initializes Crowdin translation widget based on project and UI platform.\n * @param {string} project - Project name ('LizardByte' or 'LizardByte-docs').\n * @param {string|null} platform - UI platform ('sphinx', 'rustdoc', or null).\n */\nfunction initCrowdIn(project = 'LizardByte', platform = null) {\n // Input validation\n if (!['LizardByte', 'LizardByte-docs'].includes(project)) {\n console.error('Invalid project. Must be \"LizardByte\" or \"LizardByte-docs\"');\n return;\n }\n if (!['sphinx', 'rustdoc', null].includes(platform)) {\n console.error('Invalid UI. Must be \"sphinx\", \"rustdoc\", or null');\n return;\n }\n\n // Redirect distribution CDN requests to our self-hosted GitHub Pages mirror\n // before the script is even loaded so every fetch() it makes is intercepted.\n _installCrowdinFetchInterceptor();\n\n loadScript('https://website-translator.app.crowdin.net/assets/proxy-translator.js', function() {\n // Configure base settings based on project\n const projectSettings = {\n 'LizardByte': {\n baseUrl: \"https://app.lizardbyte.dev\",\n distribution: \"458f881791aebba1d4dde491bw4\",\n },\n 'LizardByte-docs': {\n baseUrl: \"https://docs.lizardbyte.dev\",\n distribution: \"d6c830ba4b41106fefe5d391bw4\",\n }\n };\n\n let languageTitles = {\n \"bg\": \"Български (Bulgarian)\",\n \"cs\": \"Čeština (Czech)\",\n \"de\": \"Deutsch (German)\",\n \"en\": \"English\",\n \"en-GB\": \"English, United Kingdom\",\n \"en-US\": \"English, United States\",\n \"es-ES\": \"Español (Spanish)\",\n \"fr\": \"Français (French)\",\n \"hu\": \"Magyar (Hungarian)\",\n \"it\": \"Italiano (Italian)\",\n \"ja\": \"日本語 (Japanese)\",\n \"ko\": \"한국어 (Korean)\",\n \"pl\": \"Polski (Polish)\",\n \"pt-BR\": \"Português, Brasileiro (Portuguese, Brazilian)\",\n \"pt-PT\": \"Português (Portuguese)\",\n \"ru\": \"Русский (Russian)\",\n \"sv-SE\": \"svenska (Swedish)\",\n \"tr\": \"Türkçe (Turkish)\",\n \"uk\": \"Українська (Ukranian)\",\n \"vi\": \"Tiếng Việt (Vietnamese)\",\n \"zh-CN\": \"简体中文 (Chinese Simplified)\",\n \"zh-TW\": \"繁體中文 (Chinese Traditional)\",\n };\n // sort languages by name\n languageTitles = Object.fromEntries(Object.entries(languageTitles).sort((a, b) => a[1].localeCompare(b[1])));\n\n // use this to allow translations to work on PR preview builds\n let currentBaseUrl = globalThis.location.origin;\n\n // Initialize Crowdin translator\n const whitespaceBoundaries = _captureCrowdinWhitespaceBoundaries();\n\n globalThis.proxyTranslator.init({\n baseUrl: currentBaseUrl,\n callback: _createCrowdinTranslationCallback(whitespaceBoundaries),\n distribution: projectSettings[project].distribution,\n defaultLanguage: \"en\",\n languageTitles: languageTitles,\n showDefaultLanguageInUrl: false,\n languageRoutingMethod: \"query\",\n position: \"bottom-left\",\n submenuPosition: \"top-left\",\n poweredBy: false,\n });\n\n // Apply styling based on UI framework\n if (platform === null) {\n return;\n }\n\n _applyCrowdinPlatformStyling(platform);\n });\n}\n\n// Expose to the global scope\nif (globalThis.window !== undefined) {\n globalThis.initCrowdIn = initCrowdIn;\n}\n\nmodule.exports = initCrowdIn;\n","/**\n * Load a script asynchronously, and add it to the DOM. Optionally, call a callback when the script has loaded.\n * @param {string} url The URL of the script to load.\n * @param {Function} callback An optional callback to call when the script has loaded.\n */\nfunction loadScript(url, callback) {\n const script = document.createElement('script');\n script.src = url;\n script.async = true;\n\n script.onload = () => {\n if (callback) callback(null, script);\n };\n\n script.onerror = () => {\n if (callback) callback(new Error(`Failed to load script: ${url}`));\n };\n\n document.head.appendChild(script);\n}\n\n// Expose to the global scope\nif (globalThis.window !== undefined) {\n globalThis.loadScript = loadScript;\n}\n\nmodule.exports = loadScript;\n","// The module cache\nconst __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tconst cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tconst module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nlet __webpack_exports__ = __webpack_require__(714);\n"],"names":["loadScript","__webpack_require__","CROWDIN_INLINE_ELEMENT_SELECTOR","join","_getCrowdinInlineSibling","sibling","globalThis","Element","matches","_getLeadingWhitespace","text","trimmedText","trimStart","slice","length","_getTrailingWhitespace","trimEnd","_createCrowdinTranslationCallback","boundaries","translationObserver","MutationObserver","restoreAndObserve","disconnect","forEach","boundary","node","isConnected","previousReplacement","previousInline","nextSibling","Text","nextReplacement","nextInline","previousSibling","whitespaceOnly","replacement","document","createTextNode","after","before","_resolveCrowdinWhitespaceNode","currentLeadingWhitespace","data","leading","leadingWhitespace","currentTrailingWhitespace","trailing","trailingWhitespace","translatedTextEnd","_restoreCrowdinWhitespaceBoundaries","observe","body","characterData","childList","subtree","_retryCrowdinPlatformStyling","platform","attempt","setTimeout","_applyCrowdinPlatformStyling","container","getElementById","button","getElementsByClassName","sidebar","undefined","classList","remove","style","width","offsetWidth","position","left","bottom","appendChild","querySelector","add","initCrowdIn","project","includes","fetch","_crowdinMirrorInstalled","_origFetch","bind","url","options","parsed","URL","protocol","hostname","mirroredUrl","pathname","search","hash","_installCrowdinFetchInterceptor","languageTitles","bg","cs","de","en","fr","hu","it","ja","ko","pl","ru","tr","uk","vi","Object","fromEntries","entries","sort","a","b","localeCompare","currentBaseUrl","location","origin","whitespaceBoundaries","walker","createTreeWalker","NodeFilter","SHOW_TEXT","nextNode","push","trim","_captureCrowdinWhitespaceBoundaries","proxyTranslator","init","baseUrl","callback","distribution","LizardByte","defaultLanguage","showDefaultLanguageInUrl","languageRoutingMethod","submenuPosition","poweredBy","console","error","window","module","exports","script","createElement","src","async","onload","onerror","Error","head","__webpack_module_cache__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "git+https://github.com/LizardByte/shared-web.git"
6
6
  },
7
- "version": "2026.726.204939",
7
+ "version": "2026.812.190302",
8
8
  "description": "Shared web assets for LizardByte projects",
9
9
  "license": "AGPL-3.0-only",
10
10
  "funding": "https://app.lizardbyte.dev",
@@ -29,26 +29,26 @@
29
29
  "@eslint/js": "10.0.1",
30
30
  "@jest/globals": "30.4.1",
31
31
  "babel-loader": "10.1.1",
32
- "clean-jsdoc-theme": "5.1.0",
32
+ "clean-jsdoc-theme": "5.1.1",
33
33
  "cross-env": "10.1.0",
34
34
  "css-loader": "7.1.4",
35
- "eslint": "10.8.0",
36
- "eslint-plugin-jest": "29.16.0",
37
- "globals": "17.8.0",
35
+ "eslint": "10.8.1",
36
+ "eslint-plugin-jest": "29.16.1",
37
+ "globals": "17.11.0",
38
38
  "jest": "30.4.2",
39
39
  "jest-environment-jsdom": "30.4.1",
40
40
  "jest-junit": "17.0.0",
41
41
  "jsdoc": "4.0.5",
42
42
  "mini-css-extract-plugin": "2.10.2",
43
43
  "node-fetch": "3.3.2",
44
- "npm-run-all2": "9.0.2",
45
- "postcss": "8.5.23",
44
+ "npm-run-all2": "9.0.3",
45
+ "postcss": "8.5.26",
46
46
  "postcss-loader": "8.2.1",
47
47
  "postcss-preset-env": "11.3.2",
48
48
  "sass": "1.102.0",
49
49
  "sass-loader": "17.0.0",
50
- "webpack": "5.109.0",
51
- "webpack-cli": "7.2.1",
50
+ "webpack": "5.109.2",
51
+ "webpack-cli": "7.2.2",
52
52
  "webpack-dev-server": "6.0.0"
53
53
  },
54
54
  "jest": {
package/src/js/crowdin.js CHANGED
@@ -11,6 +11,180 @@ const loadScript = require('./load-script');
11
11
  const CROWDIN_DIST_MIRROR = 'https://cdn.jsdelivr.net/gh/LizardByte/i18n@dist';
12
12
  const CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS = 100;
13
13
  const CROWDIN_PLATFORM_STYLING_RETRY_DELAY_MS = 50;
14
+ const CROWDIN_INLINE_ELEMENT_SELECTOR = [
15
+ 'a',
16
+ 'abbr',
17
+ 'b',
18
+ 'cite',
19
+ 'code',
20
+ 'del',
21
+ 'em',
22
+ 'i',
23
+ 'ins',
24
+ 'kbd',
25
+ 'mark',
26
+ 'q',
27
+ 's',
28
+ 'samp',
29
+ 'small',
30
+ 'span',
31
+ 'strong',
32
+ 'sub',
33
+ 'sup',
34
+ 'time',
35
+ 'u',
36
+ 'var',
37
+ ].join(',');
38
+
39
+ /**
40
+ * Returns a sibling when it is an inline element whose boundary Crowdin may rewrite.
41
+ * @param {Node|null} sibling Candidate sibling.
42
+ * @returns {Element|null} Matching inline element.
43
+ */
44
+ function _getCrowdinInlineSibling(sibling) {
45
+ if (!(sibling instanceof globalThis.Element)) return null;
46
+ return sibling.matches(CROWDIN_INLINE_ELEMENT_SELECTOR) ? sibling : null;
47
+ }
48
+
49
+ /**
50
+ * Returns the exact whitespace at the start of a string.
51
+ * @param {string} text Text to inspect.
52
+ * @returns {string} Leading whitespace.
53
+ */
54
+ function _getLeadingWhitespace(text) {
55
+ const trimmedText = text.trimStart();
56
+ return text.slice(0, text.length - trimmedText.length);
57
+ }
58
+
59
+ /**
60
+ * Returns the exact whitespace at the end of a string.
61
+ * @param {string} text Text to inspect.
62
+ * @returns {string} Trailing whitespace.
63
+ */
64
+ function _getTrailingWhitespace(text) {
65
+ const trimmedText = text.trimEnd();
66
+ return text.slice(trimmedText.length);
67
+ }
68
+
69
+ /**
70
+ * Records whitespace that separates text from inline elements before Crowdin translates the page.
71
+ * @returns {Array<{
72
+ * node: Text,
73
+ * leading: boolean,
74
+ * leadingWhitespace: string,
75
+ * trailing: boolean,
76
+ * trailingWhitespace: string,
77
+ * previousInline: Element|null,
78
+ * nextInline: Element|null,
79
+ * whitespaceOnly: boolean
80
+ * }>} Recorded text-node boundaries.
81
+ */
82
+ function _captureCrowdinWhitespaceBoundaries() {
83
+ const boundaries = [];
84
+ const walker = document.createTreeWalker(document.body, globalThis.NodeFilter.SHOW_TEXT);
85
+ let node = walker.nextNode();
86
+
87
+ while (node !== null) {
88
+ const previousInline = _getCrowdinInlineSibling(node.previousSibling);
89
+ const nextInline = _getCrowdinInlineSibling(node.nextSibling);
90
+ const leadingWhitespace = previousInline === null ? '' : _getLeadingWhitespace(node.data);
91
+ const trailingWhitespace = nextInline === null ? '' : _getTrailingWhitespace(node.data);
92
+ const leading = leadingWhitespace !== '';
93
+ const trailing = trailingWhitespace !== '';
94
+
95
+ if (leading || trailing) {
96
+ boundaries.push({
97
+ node,
98
+ leading,
99
+ leadingWhitespace,
100
+ trailing,
101
+ trailingWhitespace,
102
+ previousInline,
103
+ nextInline,
104
+ whitespaceOnly: node.data.trim() === '',
105
+ });
106
+ }
107
+
108
+ node = walker.nextNode();
109
+ }
110
+
111
+ return boundaries;
112
+ }
113
+
114
+ /**
115
+ * Finds the current text node for a boundary after Crowdin changes the DOM.
116
+ * @param {Object} boundary Recorded whitespace boundary.
117
+ * @returns {Text|null} Current or recreated text node.
118
+ */
119
+ function _resolveCrowdinWhitespaceNode(boundary) {
120
+ if (boundary.node.isConnected) return boundary.node;
121
+
122
+ const previousReplacement = boundary.previousInline?.nextSibling;
123
+ if (previousReplacement instanceof globalThis.Text && previousReplacement.isConnected) {
124
+ boundary.node = previousReplacement;
125
+ return boundary.node;
126
+ }
127
+ const nextReplacement = boundary.nextInline?.previousSibling;
128
+ if (nextReplacement instanceof globalThis.Text && nextReplacement.isConnected) {
129
+ boundary.node = nextReplacement;
130
+ return boundary.node;
131
+ }
132
+ if (!boundary.whitespaceOnly) return null;
133
+
134
+ const replacement = document.createTextNode('');
135
+ if (boundary.previousInline?.isConnected) {
136
+ boundary.previousInline.after(replacement);
137
+ } else if (boundary.nextInline?.isConnected) {
138
+ boundary.nextInline.before(replacement);
139
+ } else {
140
+ return null;
141
+ }
142
+
143
+ boundary.node = replacement;
144
+ return boundary.node;
145
+ }
146
+
147
+ /**
148
+ * Restores whitespace that Crowdin removed from translated text-node boundaries.
149
+ * @param {Array<Object>} boundaries Recorded text-node boundaries.
150
+ */
151
+ function _restoreCrowdinWhitespaceBoundaries(boundaries) {
152
+ boundaries.forEach((boundary) => {
153
+ const node = _resolveCrowdinWhitespaceNode(boundary);
154
+ if (node === null) return;
155
+
156
+ const currentLeadingWhitespace = _getLeadingWhitespace(node.data);
157
+ if (boundary.leading && currentLeadingWhitespace !== boundary.leadingWhitespace) {
158
+ node.data = boundary.leadingWhitespace + node.data.slice(currentLeadingWhitespace.length);
159
+ }
160
+ const currentTrailingWhitespace = _getTrailingWhitespace(node.data);
161
+ if (boundary.trailing && currentTrailingWhitespace !== boundary.trailingWhitespace) {
162
+ const translatedTextEnd = node.data.length - currentTrailingWhitespace.length;
163
+ node.data = node.data.slice(0, translatedTextEnd) + boundary.trailingWhitespace;
164
+ }
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Creates the Website Translator callback and observes later translation mutations.
170
+ * @param {Array<Object>} boundaries Recorded text-node boundaries.
171
+ * @returns {Function} Website Translator callback.
172
+ */
173
+ function _createCrowdinTranslationCallback(boundaries) {
174
+ const translationObserver = new globalThis.MutationObserver(restoreAndObserve);
175
+
176
+ function restoreAndObserve() {
177
+ translationObserver.disconnect();
178
+ _restoreCrowdinWhitespaceBoundaries(boundaries);
179
+ translationObserver.observe(document.body, {
180
+ characterData: true,
181
+ childList: true,
182
+ subtree: true,
183
+ });
184
+ }
185
+
186
+ return restoreAndObserve;
187
+ }
14
188
 
15
189
  /**
16
190
  * Monkey-patches globalThis.fetch to redirect Crowdin distribution requests to
@@ -171,8 +345,11 @@ function initCrowdIn(project = 'LizardByte', platform = null) {
171
345
  let currentBaseUrl = globalThis.location.origin;
172
346
 
173
347
  // Initialize Crowdin translator
348
+ const whitespaceBoundaries = _captureCrowdinWhitespaceBoundaries();
349
+
174
350
  globalThis.proxyTranslator.init({
175
351
  baseUrl: currentBaseUrl,
352
+ callback: _createCrowdinTranslationCallback(whitespaceBoundaries),
176
353
  distribution: projectSettings[project].distribution,
177
354
  defaultLanguage: "en",
178
355
  languageTitles: languageTitles,
@@ -86,6 +86,7 @@ describe('initCrowdIn', () => {
86
86
  jest.clearAllMocks();
87
87
  jest.useRealTimers();
88
88
  delete globalThis.window.proxyTranslator;
89
+ delete globalThis.window.i18nextify;
89
90
  delete globalThis._crowdinMirrorInstalled;
90
91
  });
91
92
 
@@ -114,6 +115,110 @@ describe('initCrowdIn', () => {
114
115
  );
115
116
  });
116
117
 
118
+ it('should only pass supported Website Translator options', () => {
119
+ initCrowdIn();
120
+
121
+ // Simulate script loading
122
+ jest.runAllTimers();
123
+
124
+ const options = globalThis.proxyTranslator.init.mock.calls[0][0];
125
+ expect(Object.keys(options).sort()).toEqual([
126
+ 'baseUrl',
127
+ 'callback',
128
+ 'defaultLanguage',
129
+ 'distribution',
130
+ 'languageRoutingMethod',
131
+ 'languageTitles',
132
+ 'position',
133
+ 'poweredBy',
134
+ 'showDefaultLanguageInUrl',
135
+ 'submenuPosition',
136
+ ]);
137
+ });
138
+
139
+ it('should restore whitespace around translated inline elements', () => {
140
+ globalThis.document.body.innerHTML = `
141
+ <p id="translated">
142
+ GitHub <em>Discussions</em> are available. <strong>Yearly:</strong> <strong>$14.99</strong>, billed.
143
+ </p>
144
+ <p id="detached">Before <i>removed</i></p>
145
+ <p id="next-anchor"><i>gone</i> <strong>kept</strong></p>
146
+ <p id="no-anchor"><i>gone</i> <strong>also gone</strong></p>
147
+ `;
148
+
149
+ initCrowdIn();
150
+ jest.runAllTimers();
151
+
152
+ const options = globalThis.proxyTranslator.init.mock.calls[0][0];
153
+ const translated = globalThis.document.getElementById('translated');
154
+ const emphasis = translated.querySelector('em');
155
+ const strongElements = translated.querySelectorAll('strong');
156
+ const detachedBoundary = globalThis.document.querySelector('#detached i').previousSibling;
157
+ const nextAnchor = globalThis.document.getElementById('next-anchor');
158
+ const nextAnchorWhitespace = nextAnchor.querySelector('i').nextSibling;
159
+ const noAnchor = globalThis.document.getElementById('no-anchor');
160
+
161
+ emphasis.previousSibling.replaceWith(document.createTextNode('GitHub'));
162
+ emphasis.nextSibling.replaceWith(document.createTextNode('are available. '));
163
+ strongElements[0].nextSibling.remove();
164
+ detachedBoundary.remove();
165
+ nextAnchor.querySelector('i').remove();
166
+ nextAnchorWhitespace.remove();
167
+ noAnchor.remove();
168
+
169
+ options.callback();
170
+ options.callback();
171
+
172
+ expect(translated.textContent.trim()).toBe(
173
+ 'GitHub Discussions are available. Yearly: $14.99, billed.'
174
+ );
175
+ expect(nextAnchor.textContent).toBe(' kept');
176
+ });
177
+
178
+ it('should restore line breaks between translated syntax-highlighted lines', () => {
179
+ globalThis.document.body.innerHTML = [
180
+ '<pre id="code-block"><span></span><span class="k">- [x]</span> This is a complete item\n',
181
+ '<span class="k">- [ ]</span> This is an incomplete item\n</pre>',
182
+ ].join('');
183
+
184
+ initCrowdIn();
185
+ jest.runAllTimers();
186
+
187
+ const options = globalThis.proxyTranslator.init.mock.calls[0][0];
188
+ const codeBlock = globalThis.document.getElementById('code-block');
189
+ const firstLineText = codeBlock.querySelector('.k').nextSibling;
190
+
191
+ firstLineText.data = firstLineText.data.trim();
192
+ options.callback();
193
+
194
+ expect(codeBlock.textContent).toBe(
195
+ '- [x] This is a complete item\n- [ ] This is an incomplete item\n'
196
+ );
197
+ });
198
+
199
+ it('should restore whitespace after later asynchronous DOM changes', async () => {
200
+ globalThis.document.body.innerHTML = '<p id="translated">Use <a href="#">this link</a> here.</p>';
201
+
202
+ initCrowdIn();
203
+ jest.runAllTimers();
204
+
205
+ const options = globalThis.proxyTranslator.init.mock.calls[0][0];
206
+ const translated = globalThis.document.getElementById('translated');
207
+ const link = translated.querySelector('a');
208
+
209
+ options.callback();
210
+ link.previousSibling.data = link.previousSibling.data.trimEnd();
211
+ await Promise.resolve();
212
+
213
+ expect(translated.textContent).toBe('Use this link here.');
214
+
215
+ jest.advanceTimersByTime(1000);
216
+ link.nextSibling.data = link.nextSibling.data.trimStart();
217
+ await Promise.resolve();
218
+
219
+ expect(translated.textContent).toBe('Use this link here.');
220
+ });
221
+
117
222
  it('should initialize proxyTranslator with LizardByte-docs settings', () => {
118
223
  initCrowdIn('LizardByte-docs');
119
224