|
12 | 12 | if (!api?.runtime || !api?.storage?.local) return; |
13 | 13 |
|
14 | 14 | const STORAGE_KEY = 'selectionShortcutEnabled'; |
15 | | - const TRANSLATION_STORAGE_KEY = 'selectionTranslateLanguage'; |
| 15 | + const LOCALE_STORAGE_KEY = 'wbLocale'; |
16 | 16 | const SUBMIT_MESSAGE = 'WB_SELECTION_SHORTCUT_SUBMIT'; |
17 | 17 | const GAP = 8; |
18 | 18 | const BUTTON_SIZE = 44; |
19 | 19 | const POPUP_WIDTH = 316; |
20 | 20 | const TRANSLATION_LANGUAGES = Object.freeze([ |
21 | | - ['en', 'English'], ['es', 'Spanish'], ['fr', 'French'], ['tr', 'Turkish'], |
22 | | - ['zh', 'Chinese'], ['ru', 'Russian'], ['uk', 'Ukrainian'], ['ar', 'Arabic'], |
23 | | - ['ja', 'Japanese'], ['ko', 'Korean'], ['id', 'Indonesian'], ['th', 'Thai'], |
24 | | - ['ms', 'Malay'], ['tl', 'Filipino'], ['pl', 'Polish'], ['he', 'Hebrew'], |
| 21 | + 'en', 'es', 'fr', 'tr', 'zh', 'ru', 'uk', 'ar', |
| 22 | + 'ja', 'ko', 'id', 'th', 'ms', 'tl', 'pl', 'he', |
25 | 23 | ]); |
26 | 24 |
|
27 | 25 | let enabled = true; |
|
33 | 31 | let shadow = null; |
34 | 32 | let shortcut = null; |
35 | 33 | let popup = null; |
36 | | - let mainView = null; |
37 | | - let translateView = null; |
38 | 34 | let question = null; |
39 | 35 | let sendButton = null; |
40 | | - let translationSelect = null; |
41 | | - let translationSubmit = null; |
42 | | - let translationLanguage = ''; |
| 36 | + let interfaceLanguage = resolveInterfaceLanguage(''); |
43 | 37 | let toast = null; |
44 | 38 | let toastTimer = null; |
45 | 39 | let selectionTimer = null; |
|
53 | 47 | } |
54 | 48 |
|
55 | 49 | function isSupportedTranslationLanguage(value) { |
56 | | - return TRANSLATION_LANGUAGES.some(([code]) => code === value); |
| 50 | + return TRANSLATION_LANGUAGES.includes(value); |
| 51 | + } |
| 52 | + |
| 53 | + function resolveInterfaceLanguage(value) { |
| 54 | + const preferred = String(value || '').trim().toLowerCase(); |
| 55 | + if (isSupportedTranslationLanguage(preferred)) return preferred; |
| 56 | + const browserLanguage = String(navigator.language || 'en').slice(0, 2).toLowerCase(); |
| 57 | + return isSupportedTranslationLanguage(browserLanguage) ? browserLanguage : 'en'; |
57 | 58 | } |
58 | 59 |
|
59 | 60 | function readSelection() { |
|
91 | 92 | host.id = 'webbrain-selection-shortcut-host'; |
92 | 93 | host.style.cssText = 'position:fixed;inset:0;width:100vw;height:100vh;display:block;pointer-events:none;z-index:2147483647'; |
93 | 94 | shadow = host.attachShadow({ mode: 'closed' }); |
94 | | - const translationOptions = TRANSLATION_LANGUAGES |
95 | | - .map(([code, label]) => `<option value="${code}">${label}</option>`) |
96 | | - .join(''); |
97 | 95 | shadow.innerHTML = ` |
98 | 96 | <style> |
99 | 97 | :host { |
|
103 | 101 | color-scheme:light dark; |
104 | 102 | } |
105 | 103 | * { box-sizing:border-box; } |
106 | | - button,textarea,select { font:inherit; } |
| 104 | + button,textarea { font:inherit; } |
107 | 105 | [hidden] { display:none !important; } |
108 | 106 | .shortcut { |
109 | 107 | position:fixed; width:${BUTTON_SIZE}px; height:${BUTTON_SIZE}px; display:grid; |
|
148 | 146 | .send svg { width:15px; height:15px; } |
149 | 147 | .divider { height:1px; margin:10px 0 4px; background:var(--border); } |
150 | 148 | .hide { padding:9px 12px; color:var(--muted); font-size:13px; } |
151 | | - .translate-view { padding:2px; } |
152 | | - .translate-header { display:flex; align-items:center; gap:8px; margin-bottom:12px; } |
153 | | - .back { |
154 | | - width:32px; height:32px; display:grid; place-items:center; padding:0; border:0; |
155 | | - border-radius:9px; background:transparent; color:var(--muted); font-size:20px; cursor:pointer; |
156 | | - } |
157 | | - .back:hover { background:var(--hover); color:var(--text); } |
158 | | - .translate-title { font-size:15px; font-weight:650; } |
159 | | - .language-select { |
160 | | - width:100%; padding:10px 11px; border:1px solid var(--border); border-radius:11px; |
161 | | - background:var(--bg); color:var(--text); font-size:14px; |
162 | | - } |
163 | | - .translate-submit { |
164 | | - width:100%; margin-top:10px; padding:10px 12px; border:0; border-radius:11px; |
165 | | - background:var(--accent); color:#fff; font-size:14px; font-weight:650; cursor:pointer; |
166 | | - } |
167 | | - .translate-submit:hover:not(:disabled) { background:var(--accent-strong); } |
168 | | - .translate-submit:disabled { opacity:.42; cursor:default; } |
169 | | - .shortcut:focus-visible,.action:focus-visible,.hide:focus-visible,.send:focus-visible, |
170 | | - .back:focus-visible,.language-select:focus-visible,.translate-submit:focus-visible,textarea:focus-visible { |
| 149 | + .shortcut:focus-visible,.action:focus-visible,.hide:focus-visible,.send:focus-visible,textarea:focus-visible { |
171 | 150 | outline:3px solid rgba(108,99,255,.34); outline-offset:2px; |
172 | 151 | } |
173 | 152 | .toast { |
|
195 | 174 | <button class="action" type="button" data-action="explain">Explain</button> |
196 | 175 | <button class="action" type="button" data-action="quiz">Quiz me</button> |
197 | 176 | <button class="action" type="button" data-action="proofread">Proofread</button> |
198 | | - <button class="action" type="button" data-action="translate">Translate…</button> |
| 177 | + <button class="action" type="button" data-action="translate">Translate</button> |
199 | 178 | </div> |
200 | 179 | <div class="question-wrap"> |
201 | 180 | <textarea maxlength="2000" rows="3" aria-label="Ask WebBrain a question" placeholder="Ask WebBrain…"></textarea> |
|
206 | 185 | <div class="divider"></div> |
207 | 186 | <button class="hide" type="button">Hide selection shortcut</button> |
208 | 187 | </div> |
209 | | - <div class="translate-view" hidden> |
210 | | - <div class="translate-header"> |
211 | | - <button class="back" type="button" aria-label="Back to selection actions">←</button> |
212 | | - <div class="translate-title">Translate to</div> |
213 | | - </div> |
214 | | - <select class="language-select" aria-label="Translation language"> |
215 | | - <option value="">Choose language…</option> |
216 | | - ${translationOptions} |
217 | | - </select> |
218 | | - <button class="translate-submit" type="button" disabled>Translate</button> |
219 | | - </div> |
220 | 188 | </div> |
221 | 189 | <div class="toast" role="status" aria-live="polite" hidden></div> |
222 | 190 | `; |
223 | 191 |
|
224 | 192 | shortcut = shadow.querySelector('.shortcut'); |
225 | 193 | popup = shadow.querySelector('.popup'); |
226 | | - mainView = shadow.querySelector('.main-view'); |
227 | | - translateView = shadow.querySelector('.translate-view'); |
228 | 194 | question = shadow.querySelector('textarea'); |
229 | 195 | sendButton = shadow.querySelector('.send'); |
230 | | - translationSelect = shadow.querySelector('.language-select'); |
231 | | - translationSubmit = shadow.querySelector('.translate-submit'); |
232 | 196 | toast = shadow.querySelector('.toast'); |
233 | | - translationSelect.value = isSupportedTranslationLanguage(translationLanguage) ? translationLanguage : ''; |
234 | | - translationSubmit.disabled = !translationSelect.value; |
235 | 197 |
|
236 | 198 | shortcut.addEventListener('click', (event) => { |
237 | 199 | if (event.isTrusted && snapshot && !submitting) openPopup(); |
238 | 200 | }); |
239 | 201 | shadow.querySelectorAll('[data-action]').forEach((button) => { |
240 | 202 | button.addEventListener('click', (event) => { |
241 | 203 | if (!event.isTrusted) return; |
242 | | - if (button.dataset.action === 'translate') openTranslateView(); |
| 204 | + if (button.dataset.action === 'translate') submitSelection('translate', '', interfaceLanguage); |
243 | 205 | else submitSelection(button.dataset.action); |
244 | 206 | }); |
245 | 207 | }); |
|
255 | 217 | sendButton.addEventListener('click', (event) => { |
256 | 218 | if (event.isTrusted) submitSelection('custom', question.value); |
257 | 219 | }); |
258 | | - shadow.querySelector('.back').addEventListener('click', (event) => { |
259 | | - if (event.isTrusted) showMainView(true); |
260 | | - }); |
261 | | - translationSelect.addEventListener('change', () => { |
262 | | - translationSubmit.disabled = !translationSelect.value; |
263 | | - }); |
264 | | - translationSubmit.addEventListener('click', (event) => { |
265 | | - if (!event.isTrusted || !translationSelect.value) return; |
266 | | - submitSelection('translate', '', translationSelect.value); |
267 | | - }); |
268 | 220 | shadow.querySelector('.hide').addEventListener('click', (event) => { |
269 | 221 | if (event.isTrusted) disableShortcut(); |
270 | 222 | }); |
271 | 223 | shadow.addEventListener('keydown', (event) => { |
272 | 224 | if (event.key !== 'Escape') return; |
273 | 225 | event.preventDefault(); |
274 | | - if (!translateView.hidden) showMainView(true); |
275 | | - else if (!popup.hidden) closePopup(true); |
| 226 | + if (!popup.hidden) closePopup(true); |
276 | 227 | else dismissSurface(); |
277 | 228 | }); |
278 | 229 | host.addEventListener('pointerdown', () => { interacting = true; }, true); |
|
311 | 262 | ensureSurface(); |
312 | 263 | snapshot = nextSnapshot; |
313 | 264 | popup.hidden = true; |
314 | | - showMainView(false); |
315 | 265 | question.value = ''; |
316 | 266 | sendButton.disabled = true; |
317 | 267 | shortcut.hidden = false; |
|
321 | 271 | function openPopup() { |
322 | 272 | if (!snapshot || submitting) return; |
323 | 273 | ensureSurface(); |
324 | | - showMainView(false); |
325 | 274 | popup.hidden = false; |
326 | 275 | popup.style.visibility = 'hidden'; |
327 | 276 | positionPopup(); |
328 | 277 | popup.style.visibility = ''; |
329 | 278 | shadow.querySelector('[data-action="summarize"]')?.focus(); |
330 | 279 | } |
331 | 280 |
|
332 | | - function openTranslateView() { |
333 | | - if (!snapshot || !mainView || !translateView) return; |
334 | | - mainView.hidden = true; |
335 | | - translateView.hidden = false; |
336 | | - translationSelect.value = isSupportedTranslationLanguage(translationLanguage) ? translationLanguage : ''; |
337 | | - translationSubmit.disabled = !translationSelect.value; |
338 | | - positionPopup(); |
339 | | - translationSelect.focus(); |
340 | | - } |
341 | | - |
342 | | - function showMainView(restoreFocus) { |
343 | | - if (!mainView || !translateView) return; |
344 | | - mainView.hidden = false; |
345 | | - translateView.hidden = true; |
346 | | - if (popup && !popup.hidden) positionPopup(); |
347 | | - if (restoreFocus) shadow.querySelector('[data-action="translate"]')?.focus(); |
348 | | - } |
349 | | - |
350 | 281 | function closePopup(restoreFocus) { |
351 | 282 | if (!popup) return; |
352 | 283 | popup.hidden = true; |
353 | | - showMainView(false); |
354 | 284 | question.value = ''; |
355 | 285 | sendButton.disabled = true; |
356 | 286 | if (restoreFocus && shortcut && !shortcut.hidden) shortcut.focus(); |
|
360 | 290 | snapshot = null; |
361 | 291 | if (shortcut) shortcut.hidden = true; |
362 | 292 | if (popup) popup.hidden = true; |
363 | | - showMainView(false); |
364 | 293 | if (question) question.value = ''; |
365 | 294 | if (sendButton) sendButton.disabled = true; |
366 | 295 | } |
367 | 296 |
|
368 | 297 | function destroySurface() { |
369 | 298 | hideToast(); |
370 | 299 | host?.remove(); |
371 | | - host = shadow = shortcut = popup = mainView = translateView = question = sendButton = null; |
372 | | - translationSelect = translationSubmit = toast = null; |
| 300 | + host = shadow = shortcut = popup = question = sendButton = toast = null; |
373 | 301 | snapshot = null; |
374 | 302 | } |
375 | 303 |
|
|
402 | 330 | question: action === 'custom' ? String(customQuestion).trim() : undefined, |
403 | 331 | language: action === 'translate' ? language : undefined, |
404 | 332 | }; |
405 | | - if (action === 'translate') { |
406 | | - translationLanguage = language; |
407 | | - Promise.resolve(api.storage.local.set({ [TRANSLATION_STORAGE_KEY]: language })).catch(() => {}); |
408 | | - } |
409 | 333 | submitting = true; |
410 | 334 | dismissSurface(); |
411 | 335 | try { |
|
466 | 390 | enabled = changes[STORAGE_KEY].newValue !== false; |
467 | 391 | if (!enabled) destroySurface(); |
468 | 392 | } |
469 | | - if (changes[TRANSLATION_STORAGE_KEY]) { |
470 | | - const nextLanguage = String(changes[TRANSLATION_STORAGE_KEY].newValue || ''); |
471 | | - translationLanguage = isSupportedTranslationLanguage(nextLanguage) ? nextLanguage : ''; |
472 | | - if (translationSelect) translationSelect.value = translationLanguage; |
473 | | - } |
| 393 | + if (changes[LOCALE_STORAGE_KEY]) interfaceLanguage = resolveInterfaceLanguage(changes[LOCALE_STORAGE_KEY].newValue); |
474 | 394 | }); |
475 | | - Promise.resolve(api.storage.local.get({ [STORAGE_KEY]: true, [TRANSLATION_STORAGE_KEY]: '' })) |
| 395 | + Promise.resolve(api.storage.local.get({ [STORAGE_KEY]: true, [LOCALE_STORAGE_KEY]: '' })) |
476 | 396 | .then((stored) => { |
477 | 397 | enabled = stored?.[STORAGE_KEY] !== false; |
478 | | - const storedLanguage = String(stored?.[TRANSLATION_STORAGE_KEY] || ''); |
479 | | - translationLanguage = isSupportedTranslationLanguage(storedLanguage) ? storedLanguage : ''; |
| 398 | + interfaceLanguage = resolveInterfaceLanguage(stored?.[LOCALE_STORAGE_KEY]); |
480 | 399 | if (!enabled) destroySurface(); |
481 | 400 | }) |
482 | 401 | .catch(() => { enabled = true; }); |
|
485 | 404 | window.__webbrainSelectionShortcut = { |
486 | 405 | refreshFromSelection, |
487 | 406 | openPopup, |
488 | | - openTranslateView, |
489 | | - submitPreset: (action) => submitSelection(action), |
| 407 | + submitPreset: (action) => action === 'translate' |
| 408 | + ? submitSelection('translate', '', interfaceLanguage) |
| 409 | + : submitSelection(action), |
490 | 410 | submitCustom: (value) => submitSelection('custom', value), |
491 | | - submitTranslate: (language) => submitSelection('translate', '', language), |
492 | | - setTranslationLanguage: (language) => { |
493 | | - if (!translationSelect) return; |
494 | | - translationSelect.value = isSupportedTranslationLanguage(language) ? language : ''; |
495 | | - translationSubmit.disabled = !translationSelect.value; |
496 | | - }, |
497 | 411 | hideShortcut: disableShortcut, |
498 | 412 | getState: () => ({ |
499 | 413 | enabled, |
500 | 414 | suppressed, |
501 | 415 | submitting, |
502 | | - translationLanguage, |
| 416 | + interfaceLanguage, |
503 | 417 | hasSelection: !!snapshot?.text, |
504 | 418 | shortcutVisible: !!shortcut && !shortcut.hidden, |
505 | 419 | popupVisible: !!popup && !popup.hidden, |
506 | 420 | toastVisible: !!toast && !toast.hidden, |
507 | | - translateViewVisible: !!translateView && !translateView.hidden, |
508 | | - translationSelectValue: translationSelect?.value || '', |
509 | 421 | shortcutRect: shortcut && !shortcut.hidden ? shortcut.getBoundingClientRect().toJSON() : null, |
510 | 422 | summarizeRect: popup && !popup.hidden |
511 | 423 | ? shadow.querySelector('[data-action="summarize"]')?.getBoundingClientRect().toJSON() || null |
512 | 424 | : null, |
513 | | - translateRect: popup && !popup.hidden && !mainView.hidden |
| 425 | + translateRect: popup && !popup.hidden |
514 | 426 | ? shadow.querySelector('[data-action="translate"]')?.getBoundingClientRect().toJSON() || null |
515 | 427 | : null, |
516 | | - translateSubmitRect: popup && !popup.hidden && !translateView.hidden && !translationSubmit.disabled |
517 | | - ? translationSubmit.getBoundingClientRect().toJSON() |
518 | | - : null, |
519 | 428 | }), |
520 | 429 | }; |
521 | 430 | })(); |
0 commit comments