@@ -5,23 +5,26 @@ import set from 'lodash/set';
5
5
const css = `
6
6
.live-translator-enable-button {
7
7
position: fixed !important;
8
- top: 0 ;
9
- left: 0 ;
8
+ top: 2px ;
9
+ left: 2px ;
10
10
z-index: 10000;
11
11
padding: 2px;
12
12
color: black;
13
13
background: rgba(255, 255, 255, 0.6);
14
14
font-family: sans-serif;
15
15
font-size: 8px;
16
+ border-radius: 10px;
17
+ display: flex;
18
+ gap: 2px;
19
+ align-items: center;
16
20
}
17
21
.live-translator-enable-button:hover {
18
22
background: white;
19
23
}
20
24
.live-translator-enable-button-indicator {
21
25
display: inline-block;
22
- height: 6px;
23
- width: 6px;
24
- margin-left: 2px;
26
+ height: 10px;
27
+ width: 10px;
25
28
border-radius: 100%;
26
29
background-color: red;
27
30
}
@@ -82,22 +85,20 @@ function deepForIn(object, fn) {
82
85
} ;
83
86
forIn ( object , iteratee ) ;
84
87
}
85
- export function encodeMessages ( messagesObject ) {
88
+ function encodeMessages ( messagesObject , locale ) {
86
89
const messages = cloneDeep ( messagesObject ) ;
87
- forIn ( messages , ( localeMessages , locale ) => {
88
- deepForIn ( localeMessages , ( message , path ) => {
89
- const parts = message . split ( '|' ) . map ( part => part . trim ( ) ) ;
90
- for ( let i = 0 ; i < parts . length ; i ++ ) {
91
- const meta = ZeroWidthEncoder . encode ( JSON . stringify ( {
92
- locale,
93
- message,
94
- path,
95
- choice : i || undefined ,
96
- } ) ) ;
97
- parts [ i ] = meta + parts [ i ] ;
98
- }
99
- set ( localeMessages , path , parts . join ( ' | ' ) ) ;
100
- } ) ;
90
+ deepForIn ( messages , ( message , path ) => {
91
+ const parts = message . split ( '|' ) . map ( part => part . trim ( ) ) ;
92
+ for ( let i = 0 ; i < parts . length ; i ++ ) {
93
+ const meta = ZeroWidthEncoder . encode ( JSON . stringify ( {
94
+ locale,
95
+ message,
96
+ path,
97
+ choice : i || undefined ,
98
+ } ) ) ;
99
+ parts [ i ] = meta + parts [ i ] ;
100
+ }
101
+ set ( messages , path , parts . join ( ' | ' ) ) ;
101
102
} ) ;
102
103
return messages ;
103
104
}
@@ -149,13 +150,40 @@ class ZeroWidthEncoder {
149
150
return text ;
150
151
}
151
152
}
153
+ class Cache {
154
+ _cache = { } ;
155
+ has ( key ) {
156
+ return key in this . _cache ;
157
+ }
158
+ store ( key , value ) {
159
+ this . _cache [ key ] = { value, locked : true } ;
160
+ }
161
+ lock ( key ) {
162
+ this . _cache [ key ] . locked = true ;
163
+ }
164
+ clear ( force = false ) {
165
+ for ( const key in this . _cache ) {
166
+ if ( ! force && this . _cache [ key ] . locked ) {
167
+ this . _cache [ key ] . locked = false ;
168
+ }
169
+ else {
170
+ this . _cache [ key ] . value . remove ( ) ;
171
+ delete this . _cache [ key ] ;
172
+ }
173
+ }
174
+ }
175
+ get length ( ) {
176
+ return Object . keys ( this . _cache ) . length ;
177
+ }
178
+ }
152
179
class LiveTranslatorManager {
153
180
_enabled ;
154
181
_options ;
155
182
_enableButton ;
156
183
_indicator ;
157
184
_box ;
158
185
_wrapper ;
186
+ _cache = new Cache ( ) ;
159
187
constructor ( options ) {
160
188
this . _enabled = false ;
161
189
this . _options = options ;
@@ -190,7 +218,11 @@ class LiveTranslatorManager {
190
218
this . _box . classList . add ( 'live-translator-box' ) ;
191
219
this . _wrapper . appendChild ( this . _box ) ;
192
220
// initialize encode
193
- // encode is moved to i18n.ts file
221
+ for ( const locale of this . i18n . availableLocales ) {
222
+ let messages = this . i18n . getLocaleMessage ( locale ) ;
223
+ messages = encodeMessages ( messages , locale ) ;
224
+ this . i18n . setLocaleMessage ( locale , messages ) ;
225
+ }
194
226
// initialize decode & render
195
227
const throttler = throttle ( ( ) => this . render ( ) , this . _options . refreshRate || 50 ) ;
196
228
const observer = new MutationObserver ( throttler ) ;
@@ -208,6 +240,9 @@ class LiveTranslatorManager {
208
240
get root ( ) {
209
241
return this . _options . root || document . documentElement ;
210
242
}
243
+ get i18n ( ) {
244
+ return this . _options . i18n . global || this . _options . i18n ;
245
+ }
211
246
toggle ( enable ) {
212
247
if ( enable !== undefined ) {
213
248
this . _enabled = enable ;
@@ -219,14 +254,11 @@ class LiveTranslatorManager {
219
254
localStorage . setItem ( 'live-translator-enabled' , JSON . stringify ( this . _enabled ) ) ;
220
255
}
221
256
console . log ( `%c Live Translator ${ this . _enabled ? 'ON' : 'OFF' } ` , 'background: #222; color: #bada55' ) ;
257
+ if ( ! this . _enabled ) {
258
+ this . _cache . clear ( true ) ;
259
+ }
222
260
}
223
261
render ( ) {
224
- this . _box . style . display = 'none' ;
225
- document .
226
- querySelectorAll ( '.live-translator-badge-container' ) .
227
- forEach ( ( elem ) => {
228
- elem . remove ( ) ;
229
- } ) ;
230
262
this . _indicator . style . background = this . _enabled ? 'lightgreen' : 'red' ;
231
263
if ( ! this . _enabled ) {
232
264
return ;
@@ -236,13 +268,16 @@ class LiveTranslatorManager {
236
268
while ( queue . length > 0 ) {
237
269
const node = queue . pop ( ) ;
238
270
const badges = [ ] ;
271
+ let cacheKeyParts = [ ] ;
239
272
if ( node instanceof Text ) {
240
273
const matches = node . textContent . match ( re ) ;
241
274
for ( const match of matches ?? [ ] ) {
242
275
const meta = JSON . parse ( ZeroWidthEncoder . decode ( match ) ) ;
243
276
const badge = createBadge ( meta , this . _options , node ) ;
244
277
badge . addEventListener ( 'mouseenter' , ( ) => this . showBox ( node ) ) ;
278
+ badge . addEventListener ( 'mouseleave' , ( ) => this . hideBox ( ) ) ;
245
279
badges . push ( badge ) ;
280
+ cacheKeyParts . push ( meta . path ) ;
246
281
}
247
282
}
248
283
const attributes = ( node . attributes ? [ ...node . attributes ] : [ ] )
@@ -253,7 +288,9 @@ class LiveTranslatorManager {
253
288
const meta = JSON . parse ( ZeroWidthEncoder . decode ( m ) ) ;
254
289
const badge = createBadge ( meta , this . _options , node , attribute . name ) ;
255
290
badge . addEventListener ( 'mouseenter' , ( ) => this . showBox ( node , true ) ) ;
291
+ badge . addEventListener ( 'mouseleave' , ( ) => this . hideBox ( ) ) ;
256
292
badges . push ( badge ) ;
293
+ cacheKeyParts . push ( meta . path ) ;
257
294
}
258
295
}
259
296
if ( badges . length ) {
@@ -264,13 +301,19 @@ class LiveTranslatorManager {
264
301
const clientRect = getBoundingClientRect ( node ) ;
265
302
position . top = clientRect . top + window . scrollY ;
266
303
position . left = clientRect . left + window . screenX ;
267
- isVisible = isVisible || node . parentElement . contains ( document . elementFromPoint ( clientRect . left + clientRect . width / 2 , clientRect . top + clientRect . height / 2 ) ) ;
304
+ const elemOnTop = document . elementFromPoint ( clientRect . left + clientRect . width / 2 , clientRect . top + clientRect . height / 2 ) ;
305
+ isVisible = isVisible ||
306
+ node . parentElement . contains ( elemOnTop ) ||
307
+ this . _wrapper . contains ( elemOnTop ) ;
268
308
}
269
309
else {
270
310
const clientRect = node . getClientRects ( ) [ 0 ] ;
271
311
position . top = clientRect . top + clientRect . height - 10 + window . scrollY ;
272
312
position . left = clientRect . left + window . screenX ;
273
- isVisible = isVisible || node . contains ( document . elementFromPoint ( clientRect . left + clientRect . width / 2 , clientRect . top + clientRect . height / 2 ) ) ;
313
+ const elemOnTop = document . elementFromPoint ( clientRect . left + clientRect . width / 2 , clientRect . top + clientRect . height / 2 ) ;
314
+ isVisible = isVisible ||
315
+ node . contains ( elemOnTop ) ||
316
+ this . _wrapper . contains ( elemOnTop ) ;
274
317
}
275
318
if ( ! isVisible ) {
276
319
continue ;
@@ -280,19 +323,28 @@ class LiveTranslatorManager {
280
323
// console.warn('Could not get bounding box for', node);
281
324
continue ;
282
325
}
283
- const container = document . createElement ( 'span' ) ;
284
- container . classList . add ( 'live-translator-badge-container' ) ;
285
- container . style . top = position . top + 'px' ;
286
- container . style . left = position . left + 'px' ;
287
- this . _wrapper . appendChild ( container ) ;
288
- for ( const badge of badges ) {
289
- container . appendChild ( badge ) ;
326
+ cacheKeyParts . unshift ( position . left , position . top ) ;
327
+ const cacheKey = cacheKeyParts . join ( ';' ) ;
328
+ if ( ! this . _cache . has ( cacheKey ) ) {
329
+ const container = document . createElement ( 'span' ) ;
330
+ container . classList . add ( 'live-translator-badge-container' ) ;
331
+ container . style . top = position . top + 'px' ;
332
+ container . style . left = position . left + 'px' ;
333
+ this . _wrapper . appendChild ( container ) ;
334
+ for ( const badge of badges ) {
335
+ container . appendChild ( badge ) ;
336
+ }
337
+ this . _cache . store ( cacheKey , container ) ;
338
+ }
339
+ else {
340
+ this . _cache . lock ( cacheKey ) ;
290
341
}
291
342
}
292
343
for ( const child of node . childNodes ) {
293
344
queue . push ( child ) ;
294
345
}
295
346
}
347
+ this . _cache . clear ( ) ;
296
348
}
297
349
showBox ( node , attribute = false ) {
298
350
const rect = ! attribute ? getBoundingClientRect ( node ) : node . getClientRects ( ) [ 0 ] ;
@@ -312,6 +364,9 @@ class LiveTranslatorManager {
312
364
this . _box . style . height = rect . height + 2 * padding + 'px' ;
313
365
this . _box . style . display = 'block' ;
314
366
}
367
+ hideBox ( ) {
368
+ this . _box . style . display = 'none' ;
369
+ }
315
370
}
316
371
const createBadge = ( meta , options , node , attribute ) => {
317
372
const badge = document . createElement ( 'a' ) ;
0 commit comments