@@ -9,8 +9,120 @@ const loadScript = require('./load-script');
99 * @type {string }
1010 */
1111const CROWDIN_DIST_MIRROR = 'https://cdn.jsdelivr.net/gh/LizardByte/i18n@dist' ;
12+ const CROWDIN_TRANSLATION_SETTLE_DELAY_MS = 50 ;
1213const CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS = 100 ;
1314const CROWDIN_PLATFORM_STYLING_RETRY_DELAY_MS = 50 ;
15+ const CROWDIN_INLINE_ELEMENT_SELECTOR = [
16+ 'a' ,
17+ 'abbr' ,
18+ 'b' ,
19+ 'cite' ,
20+ 'code' ,
21+ 'del' ,
22+ 'em' ,
23+ 'i' ,
24+ 'ins' ,
25+ 'kbd' ,
26+ 'mark' ,
27+ 'q' ,
28+ 's' ,
29+ 'samp' ,
30+ 'small' ,
31+ 'span' ,
32+ 'strong' ,
33+ 'sub' ,
34+ 'sup' ,
35+ 'time' ,
36+ 'u' ,
37+ 'var' ,
38+ ] . join ( ',' ) ;
39+
40+ /**
41+ * Records whitespace that separates text from inline elements before Crowdin translates the page.
42+ * @returns {Array<{node: Text, leading: boolean, trailing: boolean}> } Recorded text-node boundaries.
43+ */
44+ function _captureCrowdinWhitespaceBoundaries ( ) {
45+ const boundaries = [ ] ;
46+ const walker = document . createTreeWalker ( document . body , globalThis . NodeFilter . SHOW_TEXT ) ;
47+ let node = walker . nextNode ( ) ;
48+
49+ while ( node !== null ) {
50+ const previousIsInline = node . previousSibling instanceof globalThis . Element &&
51+ node . previousSibling . matches ( CROWDIN_INLINE_ELEMENT_SELECTOR ) ;
52+ const nextIsInline = node . nextSibling instanceof globalThis . Element &&
53+ node . nextSibling . matches ( CROWDIN_INLINE_ELEMENT_SELECTOR ) ;
54+ const leading = previousIsInline && / ^ \s / . test ( node . data ) ;
55+ const trailing = nextIsInline && / \s $ / . test ( node . data ) ;
56+
57+ if ( leading || trailing ) {
58+ boundaries . push ( { node, leading, trailing } ) ;
59+ }
60+
61+ node = walker . nextNode ( ) ;
62+ }
63+
64+ return boundaries ;
65+ }
66+
67+ /**
68+ * Restores whitespace that Crowdin removed from translated text-node boundaries.
69+ * @param {Array<{node: Text, leading: boolean, trailing: boolean}> } boundaries Recorded text-node boundaries.
70+ */
71+ function _restoreCrowdinWhitespaceBoundaries ( boundaries ) {
72+ boundaries . forEach ( ( boundary ) => {
73+ if ( ! boundary . node . isConnected ) return ;
74+
75+ if ( boundary . leading && ! / ^ \s / . test ( boundary . node . data ) ) {
76+ boundary . node . data = ' ' + boundary . node . data ;
77+ }
78+ if ( boundary . trailing && ! / \s $ / . test ( boundary . node . data ) ) {
79+ boundary . node . data += ' ' ;
80+ }
81+ } ) ;
82+ }
83+
84+ /**
85+ * Creates the Website Translator callback that repairs whitespace after translation mutations settle.
86+ * @param {Array<{node: Text, leading: boolean, trailing: boolean}> } boundaries Recorded text-node boundaries.
87+ * @returns {Function } Website Translator callback.
88+ */
89+ function _createCrowdinTranslationCallback ( boundaries ) {
90+ let listeningForLanguageChanges = false ;
91+ let translationObserver ;
92+ let restoreTimer ;
93+
94+ function restoreAfterTranslationSettles ( ) {
95+ globalThis . clearTimeout ( restoreTimer ) ;
96+ restoreTimer = globalThis . setTimeout ( function ( ) {
97+ translationObserver . disconnect ( ) ;
98+ translationObserver = undefined ;
99+ restoreTimer = undefined ;
100+ _restoreCrowdinWhitespaceBoundaries ( boundaries ) ;
101+ } , CROWDIN_TRANSLATION_SETTLE_DELAY_MS ) ;
102+ }
103+
104+ function observeTranslationMutations ( ) {
105+ translationObserver ?. disconnect ( ) ;
106+ translationObserver = new globalThis . MutationObserver ( restoreAfterTranslationSettles ) ;
107+ translationObserver . observe ( document . body , {
108+ characterData : true ,
109+ childList : true ,
110+ subtree : true ,
111+ } ) ;
112+ restoreAfterTranslationSettles ( ) ;
113+ }
114+
115+ return function waitForCrowdinTranslation ( ) {
116+ const i18next = globalThis . i18nextify ?. i18next ;
117+
118+ if ( ! listeningForLanguageChanges && typeof i18next ?. on === 'function' ) {
119+ i18next . on ( 'languageChanged' , observeTranslationMutations ) ;
120+ listeningForLanguageChanges = true ;
121+ }
122+
123+ observeTranslationMutations ( ) ;
124+ } ;
125+ }
14126
15127/**
16128 * Monkey-patches globalThis.fetch to redirect Crowdin distribution requests to
@@ -171,8 +283,11 @@ function initCrowdIn(project = 'LizardByte', platform = null) {
171283 let currentBaseUrl = globalThis . location . origin ;
172284
173285 // Initialize Crowdin translator
286+ const whitespaceBoundaries = _captureCrowdinWhitespaceBoundaries ( ) ;
287+
174288 globalThis . proxyTranslator . init ( {
175289 baseUrl : currentBaseUrl ,
290+ callback : _createCrowdinTranslationCallback ( whitespaceBoundaries ) ,
176291 distribution : projectSettings [ project ] . distribution ,
177292 defaultLanguage : "en" ,
178293 languageTitles : languageTitles ,
0 commit comments