Skip to content

Commit c61cb56

Browse files
committed
Translate selections in the WebBrain locale
1 parent 295c427 commit c61cb56

4 files changed

Lines changed: 81 additions & 260 deletions

File tree

src/chrome/src/content/selection-shortcut.js

Lines changed: 26 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@
1212
if (!api?.runtime || !api?.storage?.local) return;
1313

1414
const STORAGE_KEY = 'selectionShortcutEnabled';
15-
const TRANSLATION_STORAGE_KEY = 'selectionTranslateLanguage';
15+
const LOCALE_STORAGE_KEY = 'wbLocale';
1616
const SUBMIT_MESSAGE = 'WB_SELECTION_SHORTCUT_SUBMIT';
1717
const GAP = 8;
1818
const BUTTON_SIZE = 44;
1919
const POPUP_WIDTH = 316;
2020
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',
2523
]);
2624

2725
let enabled = true;
@@ -33,13 +31,9 @@
3331
let shadow = null;
3432
let shortcut = null;
3533
let popup = null;
36-
let mainView = null;
37-
let translateView = null;
3834
let question = null;
3935
let sendButton = null;
40-
let translationSelect = null;
41-
let translationSubmit = null;
42-
let translationLanguage = '';
36+
let interfaceLanguage = resolveInterfaceLanguage('');
4337
let toast = null;
4438
let toastTimer = null;
4539
let selectionTimer = null;
@@ -53,7 +47,14 @@
5347
}
5448

5549
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';
5758
}
5859

5960
function readSelection() {
@@ -91,9 +92,6 @@
9192
host.id = 'webbrain-selection-shortcut-host';
9293
host.style.cssText = 'position:fixed;inset:0;width:100vw;height:100vh;display:block;pointer-events:none;z-index:2147483647';
9394
shadow = host.attachShadow({ mode: 'closed' });
94-
const translationOptions = TRANSLATION_LANGUAGES
95-
.map(([code, label]) => `<option value="${code}">${label}</option>`)
96-
.join('');
9795
shadow.innerHTML = `
9896
<style>
9997
:host {
@@ -103,7 +101,7 @@
103101
color-scheme:light dark;
104102
}
105103
* { box-sizing:border-box; }
106-
button,textarea,select { font:inherit; }
104+
button,textarea { font:inherit; }
107105
[hidden] { display:none !important; }
108106
.shortcut {
109107
position:fixed; width:${BUTTON_SIZE}px; height:${BUTTON_SIZE}px; display:grid;
@@ -148,26 +146,7 @@
148146
.send svg { width:15px; height:15px; }
149147
.divider { height:1px; margin:10px 0 4px; background:var(--border); }
150148
.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 {
171150
outline:3px solid rgba(108,99,255,.34); outline-offset:2px;
172151
}
173152
.toast {
@@ -195,7 +174,7 @@
195174
<button class="action" type="button" data-action="explain">Explain</button>
196175
<button class="action" type="button" data-action="quiz">Quiz me</button>
197176
<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>
199178
</div>
200179
<div class="question-wrap">
201180
<textarea maxlength="2000" rows="3" aria-label="Ask WebBrain a question" placeholder="Ask WebBrain…"></textarea>
@@ -206,40 +185,23 @@
206185
<div class="divider"></div>
207186
<button class="hide" type="button">Hide selection shortcut</button>
208187
</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>
220188
</div>
221189
<div class="toast" role="status" aria-live="polite" hidden></div>
222190
`;
223191

224192
shortcut = shadow.querySelector('.shortcut');
225193
popup = shadow.querySelector('.popup');
226-
mainView = shadow.querySelector('.main-view');
227-
translateView = shadow.querySelector('.translate-view');
228194
question = shadow.querySelector('textarea');
229195
sendButton = shadow.querySelector('.send');
230-
translationSelect = shadow.querySelector('.language-select');
231-
translationSubmit = shadow.querySelector('.translate-submit');
232196
toast = shadow.querySelector('.toast');
233-
translationSelect.value = isSupportedTranslationLanguage(translationLanguage) ? translationLanguage : '';
234-
translationSubmit.disabled = !translationSelect.value;
235197

236198
shortcut.addEventListener('click', (event) => {
237199
if (event.isTrusted && snapshot && !submitting) openPopup();
238200
});
239201
shadow.querySelectorAll('[data-action]').forEach((button) => {
240202
button.addEventListener('click', (event) => {
241203
if (!event.isTrusted) return;
242-
if (button.dataset.action === 'translate') openTranslateView();
204+
if (button.dataset.action === 'translate') submitSelection('translate', '', interfaceLanguage);
243205
else submitSelection(button.dataset.action);
244206
});
245207
});
@@ -255,24 +217,13 @@
255217
sendButton.addEventListener('click', (event) => {
256218
if (event.isTrusted) submitSelection('custom', question.value);
257219
});
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-
});
268220
shadow.querySelector('.hide').addEventListener('click', (event) => {
269221
if (event.isTrusted) disableShortcut();
270222
});
271223
shadow.addEventListener('keydown', (event) => {
272224
if (event.key !== 'Escape') return;
273225
event.preventDefault();
274-
if (!translateView.hidden) showMainView(true);
275-
else if (!popup.hidden) closePopup(true);
226+
if (!popup.hidden) closePopup(true);
276227
else dismissSurface();
277228
});
278229
host.addEventListener('pointerdown', () => { interacting = true; }, true);
@@ -311,7 +262,6 @@
311262
ensureSurface();
312263
snapshot = nextSnapshot;
313264
popup.hidden = true;
314-
showMainView(false);
315265
question.value = '';
316266
sendButton.disabled = true;
317267
shortcut.hidden = false;
@@ -321,36 +271,16 @@
321271
function openPopup() {
322272
if (!snapshot || submitting) return;
323273
ensureSurface();
324-
showMainView(false);
325274
popup.hidden = false;
326275
popup.style.visibility = 'hidden';
327276
positionPopup();
328277
popup.style.visibility = '';
329278
shadow.querySelector('[data-action="summarize"]')?.focus();
330279
}
331280

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-
350281
function closePopup(restoreFocus) {
351282
if (!popup) return;
352283
popup.hidden = true;
353-
showMainView(false);
354284
question.value = '';
355285
sendButton.disabled = true;
356286
if (restoreFocus && shortcut && !shortcut.hidden) shortcut.focus();
@@ -360,16 +290,14 @@
360290
snapshot = null;
361291
if (shortcut) shortcut.hidden = true;
362292
if (popup) popup.hidden = true;
363-
showMainView(false);
364293
if (question) question.value = '';
365294
if (sendButton) sendButton.disabled = true;
366295
}
367296

368297
function destroySurface() {
369298
hideToast();
370299
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;
373301
snapshot = null;
374302
}
375303

@@ -402,10 +330,6 @@
402330
question: action === 'custom' ? String(customQuestion).trim() : undefined,
403331
language: action === 'translate' ? language : undefined,
404332
};
405-
if (action === 'translate') {
406-
translationLanguage = language;
407-
Promise.resolve(api.storage.local.set({ [TRANSLATION_STORAGE_KEY]: language })).catch(() => {});
408-
}
409333
submitting = true;
410334
dismissSurface();
411335
try {
@@ -466,17 +390,12 @@
466390
enabled = changes[STORAGE_KEY].newValue !== false;
467391
if (!enabled) destroySurface();
468392
}
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);
474394
});
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]: '' }))
476396
.then((stored) => {
477397
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]);
480399
if (!enabled) destroySurface();
481400
})
482401
.catch(() => { enabled = true; });
@@ -485,37 +404,27 @@
485404
window.__webbrainSelectionShortcut = {
486405
refreshFromSelection,
487406
openPopup,
488-
openTranslateView,
489-
submitPreset: (action) => submitSelection(action),
407+
submitPreset: (action) => action === 'translate'
408+
? submitSelection('translate', '', interfaceLanguage)
409+
: submitSelection(action),
490410
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-
},
497411
hideShortcut: disableShortcut,
498412
getState: () => ({
499413
enabled,
500414
suppressed,
501415
submitting,
502-
translationLanguage,
416+
interfaceLanguage,
503417
hasSelection: !!snapshot?.text,
504418
shortcutVisible: !!shortcut && !shortcut.hidden,
505419
popupVisible: !!popup && !popup.hidden,
506420
toastVisible: !!toast && !toast.hidden,
507-
translateViewVisible: !!translateView && !translateView.hidden,
508-
translationSelectValue: translationSelect?.value || '',
509421
shortcutRect: shortcut && !shortcut.hidden ? shortcut.getBoundingClientRect().toJSON() : null,
510422
summarizeRect: popup && !popup.hidden
511423
? shadow.querySelector('[data-action="summarize"]')?.getBoundingClientRect().toJSON() || null
512424
: null,
513-
translateRect: popup && !popup.hidden && !mainView.hidden
425+
translateRect: popup && !popup.hidden
514426
? shadow.querySelector('[data-action="translate"]')?.getBoundingClientRect().toJSON() || null
515427
: null,
516-
translateSubmitRect: popup && !popup.hidden && !translateView.hidden && !translationSubmit.disabled
517-
? translationSubmit.getBoundingClientRect().toJSON()
518-
: null,
519428
}),
520429
};
521430
})();

0 commit comments

Comments
 (0)