i18next browser extension

Особенности интернационализации в браузерных расширениях

Браузерные расширения работают в особом окружении: изолированные контексты (background/service worker, content scripts, popup, options page), строгие политики безопасности (CSP), ограниченный доступ к DOM и асинхронная модель сообщений между частями расширения. Это напрямую влияет на архитектуру интернационализации.

Ключевая сложность — необходимость синхронного отображения переведённого интерфейса в нескольких изолированных контекстах при едином источнике состояния языка.

i18next в этом сценарии выступает как централизованный слой управления переводами, но требует адаптации под распределённую архитектуру расширений.


Архитектурная модель i18next в расширении

Типовая структура расширения:

  • background / service worker — хранение глобального состояния языка
  • popup — UI управления
  • options page — настройки
  • content scripts — перевод страниц
  • shared module — общая инициализация i18n

Основная идея — единый экземпляр i18next или синхронизированные экземпляры.

Централизованный вариант

Язык хранится в chrome.storage:

chrome.storage.local.set({ language: 'ru' });

Каждый контекст при инициализации читает значение:

import i18next from 'i18next';

chrome.storage.local.get(['language'], (res) => {
  i18next.init({
    lng: res.language || 'en',
    fallbackLng: 'en',
    resources: {
      en: { translation: { hello: "Hello" } },
      ru: { translation: { hello: "Привет" } }
    }
  });
});

Инициализация i18next в контексте расширения

Особенность расширений — необходимость отложенной инициализации до получения конфигурации.

Ключевые параметры:

  • lng — текущий язык
  • fallbackLng — резервный язык
  • resources или backend-загрузка
  • ns — пространства имён
  • defaultNS — основной namespace

Пример конфигурации:

i18next.init({
  lng: 'en',
  fallbackLng: 'en',
  ns: ['common', 'popup'],
  defaultNS: 'common',
  interpolation: {
    escapeValue: false
  }
});

Загрузка переводов через backend

В расширениях предпочтительно не хранить все переводы в bundle. Используется динамическая загрузка:

  • локальные JSON-файлы
  • fetch через chrome.runtime.getURL
  • кастомный backend i18next

Пример загрузки:

fetch(chrome.runtime.getURL('locales/ru/translation.json'))
  .then(res => res.json())
  .then(data => {
    i18next.addResourceBundle('ru', 'translation', data);
  });

Синхронизация языка между контекстами

Основная проблема расширений — несогласованность состояний.

Решение — message passing:

background (источник истины)

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'LANG_CHANGE') {
    chrome.storage.local.set({ language: msg.language });
    sendResponse({ ok: true });
  }
});

content script / popup

chrome.runtime.sendMessage({
  type: 'LANG_CHANGE',
  language: 'ru'
});

Обновление i18next при смене языка

Каждый контекст должен реагировать на изменение языка:

chrome.storage.onChanged.addListener((changes) => {
  if (changes.language) {
    i18next.changeLanguage(changes.language.newValue);
  }
});

Это обеспечивает мгновенное обновление UI без перезагрузки расширения.


Content scripts и перевод DOM

Content scripts часто используют i18next для перевода страницы.

Базовый подход

HTML-элементы помечаются атрибутами:

<span data-i18n="hello"></span>

Рендер:

document.querySelectorAll('[data-i18n]').forEach(el => {
  el.textContent = i18next.t(el.getAttribute('data-i18n'));
});

Динамический DOM

Для SPA-страниц применяется MutationObserver:

const observer = new MutationObserver(() => {
  document.querySelectorAll('[data-i18n]').forEach(el => {
    el.textContent = i18next.t(el.dataset.i18n);
  });
});

observer.observe(document.body, { childList: true, subtree: true });

Popup — краткоживущий контекст, поэтому инициализация должна быть лёгкой.

document.getElementById('title').textContent = i18next.t('popup.title');

Options page часто содержит переключатель языка:

document.getElementById('lang').addEventListener('change', (e) => {
  const lng = e.target.value;

  chrome.runtime.sendMessage({
    type: 'LANG_CHANGE',
    language: lng
  });

  i18next.changeLanguage(lng);
});

Хранение состояния языка

Используются:

  • chrome.storage.local — основной вариант
  • chrome.storage.sync — синхронизация между устройствами

Рекомендуемая структура:

{
  language: "ru",
  fallback: "en"
}

Работа с namespaces

Namespaces позволяют разделять переводы по функциональным блокам:

  • popup
  • settings
  • content
  • common

Инициализация:

i18next.init({
  ns: ['popup', 'settings'],
  defaultNS: 'popup'
});

Использование:

i18next.t('title', { ns: 'settings' });

Производительность в расширениях

Важные оптимизации:

  • минимизация bundle переводов
  • lazy-loading namespaces
  • избегание повторной инициализации i18next
  • кэширование ресурсов в памяти

Пример ленивой загрузки:

async function loadNamespace(lng, ns) {
  const res = await fetch(
    chrome.runtime.getURL(`locales/${lng}/${ns}.json`)
  );
  const data = await res.json();
  i18next.addResourceBundle(lng, ns, data);
}

Ограничения CSP

Manifest V3 накладывает строгие ограничения:

  • запрещён inline JS
  • ограничен eval
  • fetch только по разрешённым источникам

Следствие: все переводы должны быть статическими файлами или загружаться через chrome.runtime.getURL.


Отладка интернационализации

Типичные проблемы:

  • язык не обновляется между контекстами
  • i18next инициализируется дважды
  • отсутствуют namespaces
  • content script не видит ресурсы

Практика диагностики:

console.log(i18next.language);
console.log(i18next.options);
console.log(i18next.store.data);

Интеграция с DevTools и логированием

i18next поддерживает debug-режим:

i18next.init({
  debug: true
});

В расширениях это полезно для:

  • проверки ключей переводов
  • выявления missing keys
  • анализа fallback-цепочек

Обработка pluralization и интерполяции

Plural rules критичны в UI расширений:

i18next.t('items', { count: 5 });

Ресурс:

{
  "items": "{{count}} item",
  "items_plural": "{{count}} items"
}

Интерполяция:

i18next.t('welcome', { name: 'Alex' });
{
  "welcome": "Hello, {{name}}"
}

Частые архитектурные ошибки

  • создание нескольких независимых i18next-инстансов
  • отсутствие синхронизации storage
  • хранение переводов в popup-only контексте
  • отсутствие fallbackLng
  • жесткая привязка UI к языку без реактивности

Расширенная схема взаимодействия компонентов

  • background хранит язык
  • popup меняет язык
  • storage синхронизирует состояние
  • content scripts реагируют на изменения
  • i18next выполняет рендер переводов в каждом контексте отдельно

Такая модель обеспечивает устойчивую работу интернационализации в распределённой среде расширения браузера.