- BrainTools - https://www.braintools.ru -

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

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

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

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

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

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

WordToast

WordToast

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

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

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

  • MyMemory;

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

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

  • любая LLM.

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

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

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

  • У DeepL два адреса. Бесплатный ключ определяется по суффиксу :fx и ходит на api-free.deepl.com [5], платный — на api.deepl.com [6]. Об этом легко забыть и получить загадочный 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 [7]. Именно поэтому провайдер «custom» принимает произвольный base URL.

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

Карточка перевода и всплывающие слова для повторения [8] рисуются прямо на странице. Значит, они живут в чужом 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 [1]

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

Автор: Perruer

Источник [10]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/36097

URLs in this post:

[1] GitHub: https://github.com/Perruer/wordtoast

[2] translate.googleapis.com: http://translate.googleapis.com

[3] ошибку: http://www.braintools.ru/article/4192

[4] dictionaryapi.dev: http://dictionaryapi.dev

[5] api-free.deepl.com: http://api-free.deepl.com

[6] api.deepl.com: http://api.deepl.com

[7] http://localhost:11434: http://localhost:11434

[8] повторения: http://www.braintools.ru/article/4012

[9] Boosty: https://boosty.to/mikio_kuroki/donate

[10] Источник: https://habr.com/ru/articles/1086294/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1086294

www.BrainTools.ru

Rambler's Top100