1
1
import throttle from 'lodash/throttle' ;
2
+ import forIn from 'lodash/forIn' ;
3
+ import cloneDeep from 'lodash/cloneDeep' ;
4
+ import set from 'lodash/set' ;
2
5
const css = `
3
6
.live-translator-enable-button {
4
7
position: fixed !important;
@@ -56,7 +59,50 @@ const css = `
56
59
background: #00c0ff !important;
57
60
box-shadow: 0px 0px 5px #00c0ff !important;
58
61
}
62
+ .live-translator-box {
63
+ outline: solid 2px green;
64
+ background: green;
65
+ opacity: 0.1;
66
+ position: absolute;
67
+ border-radius: 4px;
68
+ z-index: 9999;
69
+ display: none;
70
+ }
71
+ .live-translator-box.attribute {
72
+ outline: solid 2px blue;
73
+ background: blue;
74
+ }
59
75
` ;
76
+ function deepForIn ( object , fn ) {
77
+ const iteratee = ( v , k ) => {
78
+ if ( typeof v === 'object' ) {
79
+ forIn ( v , ( childV , childK ) => iteratee ( childV , `${ k } .${ childK } ` ) ) ;
80
+ }
81
+ else {
82
+ fn ( v , k ) ;
83
+ }
84
+ } ;
85
+ forIn ( object , iteratee ) ;
86
+ }
87
+ export function encodeMessages ( messagesObject ) {
88
+ const messages = cloneDeep ( messagesObject ) ;
89
+ forIn ( messages , ( localeMessages , locale ) => {
90
+ deepForIn ( localeMessages , ( 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 ( localeMessages , path , parts . join ( ' | ' ) ) ;
102
+ } ) ;
103
+ } ) ;
104
+ return messages ;
105
+ }
60
106
class ZeroWidthEncoder {
61
107
static START = '\u200B' ;
62
108
static ZERO = '\u200C' ;
@@ -110,6 +156,7 @@ class LiveTranslatorManager {
110
156
_options ;
111
157
_enableButton ;
112
158
_indicator ;
159
+ _box ;
113
160
constructor ( options ) {
114
161
this . _enabled = false ;
115
162
this . _options = options ;
@@ -134,35 +181,14 @@ class LiveTranslatorManager {
134
181
this . _enableButton . appendChild ( this . _indicator ) ;
135
182
this . _enableButton . addEventListener ( 'click' , ( ) => {
136
183
this . toggle ( ) ;
137
- this . refreshI18n ( ) ;
138
184
this . render ( ) ;
139
185
} ) ;
140
186
document . body . appendChild ( this . _enableButton ) ;
187
+ this . _box = document . createElement ( 'div' ) ;
188
+ this . _box . classList . add ( 'live-translator-box' ) ;
189
+ document . body . appendChild ( this . _box ) ;
141
190
// initialize encode
142
- const originalFormatter = this . _options . i18n . formatter ;
143
- const self = this ;
144
- this . _options . i18n . formatter = {
145
- interpolate ( message , values , path ) {
146
- const original = originalFormatter . interpolate ( message , values , path ) ;
147
- let meta = '' ;
148
- try {
149
- // filter nested objects, replace inner objects with string 'object'
150
- // this is needed when values from <i18n> tags are circular dependent objects
151
- const filteredValues = Object . fromEntries ( Object . entries ( values || { } )
152
- . map ( ( [ key , value ] ) => [ key , typeof value !== 'object' ? value : 'object' ] ) ) ;
153
- meta = ZeroWidthEncoder . encode ( JSON . stringify ( {
154
- message,
155
- values : filteredValues ,
156
- path,
157
- locale : self . _options . i18n . locale ,
158
- } ) ) ;
159
- }
160
- catch ( exception ) {
161
- console . warn ( message , values , path , self . _options . i18n . locale , exception ) ;
162
- }
163
- return ( original && meta && self . _enabled ) ? [ meta , ...original ] : original ;
164
- } ,
165
- } ;
191
+ // encode is moved to i18n.ts file
166
192
// initialize decode & render
167
193
const throttler = throttle ( ( ) => this . render ( ) , 800 ) ;
168
194
const observer = new MutationObserver ( throttler ) ;
@@ -173,15 +199,10 @@ class LiveTranslatorManager {
173
199
childList : false ,
174
200
} ) ;
175
201
document . documentElement . addEventListener ( 'mousemove' , throttler ) ;
202
+ window . setInterval ( throttler , 1000 ) ;
176
203
// render for the first time
177
- this . refreshI18n ( ) ;
178
204
this . render ( ) ;
179
205
}
180
- refreshI18n ( ) {
181
- const originalLocale = this . _options . i18n . locale ;
182
- this . _options . i18n . locale = '' ;
183
- this . _options . i18n . locale = originalLocale ;
184
- }
185
206
toggle ( enable ) {
186
207
if ( enable !== undefined ) {
187
208
this . _enabled = enable ;
@@ -196,6 +217,7 @@ class LiveTranslatorManager {
196
217
}
197
218
render ( ) {
198
219
const badgeWrappers = document . querySelectorAll ( '.live-translator-badge-wrapper' ) ;
220
+ this . _box . style . display = 'none' ;
199
221
badgeWrappers . forEach ( ( wrapper ) => {
200
222
wrapper . remove ( ) ;
201
223
} ) ;
@@ -209,11 +231,14 @@ class LiveTranslatorManager {
209
231
const node = queue . pop ( ) ;
210
232
const badges = [ ] ;
211
233
const parent = node . parentElement ;
234
+ const rect = getBoundingClientRect ( node ) ;
212
235
if ( node instanceof Text ) {
213
236
const matches = node . textContent . match ( re ) ;
214
237
for ( const match of matches ?? [ ] ) {
215
238
const meta = JSON . parse ( ZeroWidthEncoder . decode ( match ) ) ;
216
- badges . push ( createBadge ( meta , this . _options ) ) ;
239
+ const badge = createBadge ( meta , this . _options , node ) ;
240
+ badge . addEventListener ( 'mouseenter' , ( ) => this . showBox ( node ) ) ;
241
+ badges . push ( badge ) ;
217
242
}
218
243
}
219
244
const attributes = ( node . attributes ? [ ...node . attributes ] : [ ] )
@@ -222,7 +247,9 @@ class LiveTranslatorManager {
222
247
for ( const { attribute, match } of attributes ) {
223
248
for ( const m of match ) {
224
249
const meta = JSON . parse ( ZeroWidthEncoder . decode ( m ) ) ;
225
- badges . push ( createBadge ( meta , this . _options , attribute . name ) ) ;
250
+ const badge = createBadge ( meta , this . _options , node , attribute . name ) ;
251
+ badge . addEventListener ( 'mouseenter' , ( ) => this . showBox ( node , true ) ) ;
252
+ badges . push ( badge ) ;
226
253
}
227
254
}
228
255
if ( badges . length ) {
@@ -231,8 +258,11 @@ class LiveTranslatorManager {
231
258
container = node . previousElementSibling ;
232
259
}
233
260
else {
261
+ const parentRect = getBoundingClientRect ( node instanceof Text ? parent : node ) ;
234
262
container = document . createElement ( 'span' ) ;
235
263
container . classList . add ( 'live-translator-badge-container' ) ;
264
+ container . style . top = rect . top - parentRect . top + 'px' ;
265
+ container . style . left = rect . left - parentRect . left + 'px' ;
236
266
const relativeWrapper = document . createElement ( 'span' ) ;
237
267
relativeWrapper . classList . add ( 'live-translator-badge-wrapper' ) ;
238
268
relativeWrapper . appendChild ( container ) ;
@@ -247,8 +277,26 @@ class LiveTranslatorManager {
247
277
}
248
278
}
249
279
}
280
+ showBox ( node , attribute = false ) {
281
+ const rect = ! attribute ? getBoundingClientRect ( node ) : node . getClientRects ( ) [ 0 ] ;
282
+ if ( ! rect ) {
283
+ return ;
284
+ }
285
+ if ( attribute ) {
286
+ this . _box . classList . add ( 'attribute' ) ;
287
+ }
288
+ else {
289
+ this . _box . classList . remove ( 'attribute' ) ;
290
+ }
291
+ const padding = 2 ;
292
+ this . _box . style . top = rect . top - padding + window . scrollY + 'px' ;
293
+ this . _box . style . left = rect . left - padding + window . scrollX + 'px' ;
294
+ this . _box . style . width = rect . width + 2 * padding + 'px' ;
295
+ this . _box . style . height = rect . height + 2 * padding + 'px' ;
296
+ this . _box . style . display = 'block' ;
297
+ }
250
298
}
251
- const createBadge = ( meta , options , attribute ) => {
299
+ const createBadge = ( meta , options , node , attribute ) => {
252
300
const badge = document . createElement ( 'a' ) ;
253
301
badge . classList . add ( 'live-translator-badge' ) ;
254
302
let title = meta . path + ': ' + meta . message ;
@@ -269,6 +317,11 @@ const createBadge = (meta, options, attribute) => {
269
317
} ) ;
270
318
return badge ;
271
319
} ;
320
+ function getBoundingClientRect ( node , textOffset ) {
321
+ const range = document . createRange ( ) ;
322
+ range . selectNodeContents ( node ) ;
323
+ return range . getBoundingClientRect ( ) ;
324
+ }
272
325
export const LiveTranslatorPlugin = {
273
326
install ( app , options ) {
274
327
console . log ( 'LiveTranslator is installed' ) ;
0 commit comments