Daily MaverickAnalysis: Mteto Nyati retained in Eskom board chair seat as tariff debate heats upThe Jerusalem PostIsraeli projects for disabled children place first, second in Helen Hamlyn Center Fixperts awardsCNN TürkKütüphane, kafe, deneme kulübü… MEB harekete geçti: KapatılacakPunchWhy Edo-Kogi boundary dispute remains unresolved — Commission bossInquirerTyphoon Queenie may exit PAR Sunday – PagasaBollywood HungamaRamayana makers drop new poster featuring Ranbir Kapoor, Sai Pallavi and Ravie Dubey after a special event with MrBeast한겨레‘MB 기운’ 얻었다는 나경원 “보수정권 성과 숟가락 올리기 여념 없다”UOLVeja a previsão do tempo para Ivaiporã neste domingo (27)SözcüYENİ Parti milletvekili Mustafa Erdem hakkında soruşturmaBBC عربياحتجاز أشخاص وإجلاء سكان بعد إعلان حالة طوارئ قرب قاعدة بريطانية - أمريكيةDaily MailBurnham faces Labour conference civil war: Rayner stands by 'drive-by shooting' on immigration plans, Left accuses PM of 'ghosting' - and Chancellor admits he might need to find billions for fuel bailoutStraits Times SportDouble whammy for McLaren’s Lando Norris after Spanish and Azerbaijan GP woes
The Daily Newsstand · Free, Always
Sunday, September 27, 2026

Переводчик в браузере, который учит слова: встроенный Translator API Chrome, свой ИИ-ключ и интервальные повторения

Translate

Было популярное китайское расширение Dadda Translate (达达划词翻译): выделяешь слово, получаешь перевод, сохраняешь в словарик, и слово потом всплывает уведомлением, пока не запомнится. Оно перестало работать: API Sogou и Shanbay, на которых держался перевод, изменились или исчезли. К тому же оно было только для Chrome и только для пары английский → китайский.

Я переписал его с нуля. Получился WordToast для Firefox, Chrome и Edge (GitHub). Он переводит на язык браузера, умеет работать полностью на устройстве и объяснять слово в контексте через ваш собственный ИИ-ключ. В статье расскажу о трёх технических частях:

  1. встроенный в Chrome Translator API: полезный, но с неочевидными ограничениями;

  2. вызов LLM прямо из расширения, без своего сервера;

  3. всплывающие карточки поверх любого сайта, которые не ломаются от чужого CSS и CSP.

Несколько движков и откат

Движков перевода пять:

  • Google (публичный эндпоинт translate.googleapis.com);

  • MyMemory;

  • DeepL с вашим ключом;

  • встроенный переводчик Chrome;

  • любая LLM.

Движки выстроены в цепочку: если первый вернул ошибку, лимит или пустой ответ, берётся следующий. Для одиночных английских слов карточку дополняет словарь dictionaryapi.dev: части речи, определения, примеры, произношение.

Пара практических наблюдений:

  • У бесплатных API бывают долгие ответы. dictionaryapi.dev иногда отвечает по 10 секунд или отдаёт 5xx. Поэтому у каждого запроса свой таймаут: карточка сначала показывает перевод, а словарная часть догружается следом.

  • У DeepL два адреса. Бесплатный ключ определяется по суффиксу :fx и ходит на api-free.deepl.com, платный — на api.deepl.com. Об этом легко забыть и получить загадочный 403.

  • Оригинал делал опасную вещь. Dadda скачивал скрипт с сайта переводчика и выполнял его через eval, чтобы получить токен. В MV3 такое запрещено, да и раньше это было плохой идеей. В WordToast никакого внешнего кода нет.

Перевод на устройстве: Translator API в Chrome

Начиная с Chrome 138 в браузере есть встроенные модели перевода и определения языка: Translator и LanguageDetector. Текст никуда не уходит, всё считается локально. Для расширения, которое видит всё, что вы выделяете на страницах, это сильный аргумент в пользу приватности.

const g = globalThis as any;

async function onDeviceTranslate(text: string, to: string) {
  const [best] = await (await g.LanguageDetector.create()).detect(text);
  if (!best || best.confidence < 0.3 || best.detectedLanguage === "und") throw new Error("language not detected");
  const from = best.detectedLanguage.split("-")[0];
  const availability = await g.Translator.availability({ sourceLanguage: from, targetLanguage: to });
  if (availability === "unavailable") throw new Error(`${from}→${to} unsupported`);
  const translator = await g.Translator.create({ sourceLanguage: from, targetLanguage: to });
  return translator.translate(text);
}

Подводные камни:

  • Не работает в service worker. API доступен только в оконных контекстах, а фон MV3-расширения — это как раз service worker. Поэтому перевод на устройстве вызывается из content script, а не из фона, где живут остальные движки.

  • Первая загрузка модели требует жеста пользователя. Модель языковой пары скачивается при первом использовании, и create() без клика пользователя может упасть. Поэтому объекты переводчиков кешируются по паре языков, а ошибку создания мы не кешируем, чтобы при следующем клике попробовать снова.

  • Короткому тексту детектор часто не уверен. На одиночном слове уверенность бывает низкой. Ниже порога 0.3 мы честно откатываемся на следующий движок, а не угадываем язык.

  • В Firefox такого API нет. Там этот движок просто скрыт в настройках.

LLM из расширения, без своего сервера

Кнопка «Объяснить с ИИ» даёт значение слова в этом контексте: мы передаём модели предложение, в котором слово встретилось. Плюс примеры и заметки об употреблении.

Можно подключить OpenAI, Claude, Gemini или любой OpenAI-совместимый эндпоинт — OpenRouter или локальную Ollama.

Своего сервера нет: запрос уходит из расширения напрямую провайдеру с ключом пользователя. SDK я сознательно не брал: три SDK раздули бы расширение, а нужен один короткий запрос. Хватает одной функции, которая строит запрос под провайдера.

Нюанс с Claude: Anthropic API из браузерного контекста отвечает ошибкой CORS, пока вы явно не подтвердите, что понимаете риск держать ключ на клиенте:

headers: {
  "content-type": "application/json",
  "x-api-key": cfg.apiKey,
  "anthropic-version": "2023-06-01",
  // Обязателен для вызовов из браузерного контекста
  "anthropic-dangerous-direct-browser-access": "true",
}

Здесь риск осознанный: это ключ самого пользователя, он хранится в storage.local его браузера и уходит только провайдеру.

Для локальной Ollama отдельно нужен доступ к http://localhost:11434. Именно поэтому провайдер «custom» принимает произвольный base URL.

Карточка поверх любого сайта

Карточка перевода и всплывающие слова для повторения рисуются прямо на странице. Значит, они живут в чужом DOM, под чужим CSS и чужой CSP. Решение такое:

  • Собственный элемент <wordtoast-root> и shadow root. Стили страницы не пролезают внутрь, а наши не вылезают наружу. На корневой элемент ставится all: initial и максимальный z-index.

  • Стили через adoptedStyleSheets. Страница с жёсткой CSP (style-src без unsafe-inline) заблокирует вставленный <style>. Сконструированные таблицы стилей под эту директиву не попадают:

const root = host.attachShadow({ mode: "open" });
try {
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(css);
  root.adoptedStyleSheets = [sheet]; // не блокируется style-src страницы
} catch {
  const style = document.createElement("style"); // запасной вариант
  style.textContent = css;
  root.appendChild(style);
}
  • Координаты от выделения. Позиция берётся из getBoundingClientRect() выделения, у полей ввода — из самого поля. Карточка переворачивается вверх, если снизу не помещается.

Интервальные повторения

Сохранённое слово запоминается вместе с предложением, где вы его нашли, и ссылкой на страницу. Потом оно всплывает небольшой карточкой в углу страницы с кнопками «Помню» и «Забыл». Интервалы растут:

export const STAGE_DELAYS = [5 * MIN, 30 * MIN, 12 * HOUR, DAY, 3 * DAY, 7 * DAY, 21 * DAY, 60 * DAY];

Первые два шага (5 и 30 минут) взяты у Dadda, дальше классическая схема с растущими интервалами. «Помню» переводит слово на следующую ступень, «Забыл» начинает заново. Если карточку проигнорировали, она вернётся через 20 минут.

Особенность MV3: у фонового service worker нет долгоживущих таймеров. Раз в минуту срабатывает chrome.alarms, фон ищет слово, которое пора повторить, и отправляет его в content script активной вкладки. Всё состояние хранится в storage, поэтому worker может спокойно засыпать между проверками.

Словарь экспортируется в Anki (TSV: лицевая сторона, оборот, теги), CSV и JSON.

Мелочь, которая съела вечер: горячие клавиши

Я хотел горячие клавиши Alt+Shift+W и Alt+Shift+T. Chrome молча не назначает сочетания, которые считает зарезервированными или занятыми: ни ошибки в консоли, ни предупреждения, команда просто остаётся без клавиши.

Проверить это можно только через chrome.commands.getAll(). Теперь это делает E2E-тест, а страница настроек показывает фактически назначенные сочетания со ссылкой на chrome://extensions/shortcuts.

Итог

  • Выделил → перевод на язык браузера. Любая пара языков, откат между движками.

  • Перевод на устройстве в Chrome, свой ключ для DeepL и LLM, «объяснить в контексте».

  • Словарь с контекстом, всплывающие повторения, экспорт в Anki.

  • Firefox, Chrome, Edge, одна кодовая база на MV3. Без аккаунтов и аналитики, MIT (как у оригинала).

Код и релизы: https://github.com/Perruer/wordtoast

Если пригодится, поддержать разработку можно на Boosty.

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.