@@ -11,6 +11,98 @@ const loadScript = require('./load-script');
1111const CROWDIN_DIST_MIRROR = 'https://cdn.jsdelivr.net/gh/LizardByte/i18n@dist' ;
1212const CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS = 100 ;
1313const 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+ * Records whitespace that separates text from inline elements before Crowdin translates the page.
41+ * @returns {Array<{node: Text, leading: boolean, trailing: boolean}> } Recorded text-node boundaries.
42+ */
43+ function _captureCrowdinWhitespaceBoundaries ( ) {
44+ const boundaries = [ ] ;
45+ const walker = document . createTreeWalker ( document . body , globalThis . NodeFilter . SHOW_TEXT ) ;
46+ let node = walker . nextNode ( ) ;
47+
48+ while ( node !== null ) {
49+ const previousIsInline = node . previousSibling instanceof globalThis . Element &&
50+ node . previousSibling . matches ( CROWDIN_INLINE_ELEMENT_SELECTOR ) ;
51+ const nextIsInline = node . nextSibling instanceof globalThis . Element &&
52+ node . nextSibling . matches ( CROWDIN_INLINE_ELEMENT_SELECTOR ) ;
53+ const leading = previousIsInline && / ^ \s / . test ( node . data ) ;
54+ const trailing = nextIsInline && / \s $ / . test ( node . data ) ;
55+
56+ if ( leading || trailing ) {
57+ boundaries . push ( { node, leading, trailing } ) ;
58+ }
59+
60+ node = walker . nextNode ( ) ;
61+ }
62+
63+ return boundaries ;
64+ }
65+
66+ /**
67+ * Restores whitespace that Crowdin removed from translated text-node boundaries.
68+ * @param {Array<{node: Text, leading: boolean, trailing: boolean}> } boundaries Recorded text-node boundaries.
69+ */
70+ function _restoreCrowdinWhitespaceBoundaries ( boundaries ) {
71+ boundaries . forEach ( ( boundary ) => {
72+ if ( ! boundary . node . isConnected ) return ;
73+
74+ if ( boundary . leading && ! / ^ \s / . test ( boundary . node . data ) ) {
75+ boundary . node . data = ' ' + boundary . node . data ;
76+ }
77+ if ( boundary . trailing && ! / \s $ / . test ( boundary . node . data ) ) {
78+ boundary . node . data += ' ' ;
79+ }
80+ } ) ;
81+ }
82+
83+ /**
84+ * Creates the Website Translator callback that repairs whitespace after translations are applied.
85+ * @param {Array<{node: Text, leading: boolean, trailing: boolean}> } boundaries Recorded text-node boundaries.
86+ * @returns {Function } Website Translator callback.
87+ */
88+ function _createCrowdinTranslationCallback ( boundaries ) {
89+ let listeningForLanguageChanges = false ;
90+
91+ return function restoreCrowdinWhitespace ( ) {
92+ const i18next = globalThis . i18nextify ?. i18next ;
93+
94+ if ( ! listeningForLanguageChanges && typeof i18next ?. on === 'function' ) {
95+ i18next . on ( 'languageChanged' , function ( ) {
96+ globalThis . setTimeout ( function ( ) {
97+ _restoreCrowdinWhitespaceBoundaries ( boundaries ) ;
98+ } , 0 ) ;
99+ } ) ;
100+ listeningForLanguageChanges = true ;
101+ }
102+
103+ _restoreCrowdinWhitespaceBoundaries ( boundaries ) ;
104+ } ;
105+ }
14106
15107/**
16108 * Monkey-patches globalThis.fetch to redirect Crowdin distribution requests to
@@ -171,8 +263,11 @@ function initCrowdIn(project = 'LizardByte', platform = null) {
171263 let currentBaseUrl = globalThis . location . origin ;
172264
173265 // Initialize Crowdin translator
266+ const whitespaceBoundaries = _captureCrowdinWhitespaceBoundaries ( ) ;
267+
174268 globalThis . proxyTranslator . init ( {
175269 baseUrl : currentBaseUrl ,
270+ callback : _createCrowdinTranslationCallback ( whitespaceBoundaries ) ,
176271 distribution : projectSettings [ project ] . distribution ,
177272 defaultLanguage : "en" ,
178273 languageTitles : languageTitles ,
0 commit comments