Стратегии i18n в веб-компонентах

Международная локализация (i18n) веб-приложений требует аккуратного подхода к управлению текстовым содержимым и форматированием данных, особенно когда приложение построено с использованием веб-компонентов и Lit. Lit обеспечивает декларативный подход к рендерингу, что создаёт уникальные возможности и ограничения при внедрении i18n.


Основные подходы к локализации

  1. Локализация через атрибуты компонентов Каждый компонент может принимать набор локализованных строк через свойства или атрибуты. Такой подход прост в реализации и хорошо подходит для компонентов, повторно используемых в разных частях приложения. Пример:
import { LitElement, html } from 'lit';
import { property } from 'lit/decorators.js';

class GreetingComponent extends LitElement {
  @property({ type: String }) greeting = 'Hello';

  render() {
    return html`<p>${this.greeting}, user!</p>`;
  }
}

customElements.define('greeting-component', GreetingComponent);

Смена языка выполняется изменением свойства greeting. Этот способ удобен для небольших компонентов с ограниченным набором текстов.


  1. Использование JSON-файлов с переводами Более масштабируемый подход — хранение всех строк в отдельных JSON-файлах, структурированных по языкам. Например:
// en.json
{
  "greeting": "Hello",
  "farewell": "Goodbye"
}

// ru.json
{
  "greeting": "Привет",
  "farewell": "До свидания"
}

Компонент может динамически загружать нужный файл:

import { LitElement, html } from 'lit';

class I18nComponent extends LitElement {
  static properties = {
    locale: {},
    messages: {}
  };

  constructor() {
    super();
    this.locale = 'en';
    this.messages = {};
    this.loadMessages(this.locale);
  }

  async loadMessages(locale) {
    const messages = await import(`./locales/${locale}.json`);
    this.messages = messages.default;
  }

  render() {
    return html`<p>${this.messages.greeting}</p>`;
  }
}

customElements.define('i18n-component', I18nComponent);

Преимущество этого метода — централизованное хранение и возможность асинхронной подгрузки локализации.


Реактивность и обновление языка

Lit строит рендеринг на реактивных свойствах. Это позволяет легко переключать язык без перезагрузки страницы. Для этого достаточно обновить свойство locale и повторно загрузить строки перевода:

this.locale = 'ru';
await this.loadMessages(this.locale);
this.requestUpdate();

Важный момент: requestUpdate() гарантирует, что изменения свойства messages будут учтены в следующем рендере.


Интернационализация чисел, дат и валют

Для корректного отображения числовых данных и дат лучше использовать стандартный API Intl. Пример форматирования даты:

const date = new Date();
const formattedDate = new Intl.DateTimeFormat('ru-RU', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
}).format(date);

Комбинируя это с реактивными свойствами Lit, можно строить динамические компоненты с локализованным выводом чисел, дат и валют:

html`<p>Сумма: ${new Intl.NumberFormat(this.locale, { style: 'currency', currency: 'RUB' }).format(12345.67)}</p>`

Динамические сообщения и плейсхолдеры

Часто необходимо использовать шаблонные строки с переменными, например: «Привет, {username}!». Для этого применяются функции-шаблоны:

function translate(messages, key, params = {}) {
  let message = messages[key] || key;
  Object.keys(params).forEach(k => {
    message = message.replace(`{${k}}`, params[k]);
  });
  return message;
}

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

html`<p>${translate(this.messages, 'greetingUser', { username: 'Иван' })}</p>`

Такой подход позволяет централизованно управлять плейсхолдерами и избегать дублирования логики в компонентах.


Интеграция с глобальным состоянием локали

В крупных приложениях полезно хранить текущую локаль в глобальном состоянии, например через ReactiveController или глобальный объект store. Компоненты Lit подписываются на изменения локали и автоматически обновляют отображение:

class LocaleController {
  listeners = [];
  locale = 'en';

  setLocale(newLocale) {
    this.locale = newLocale;
    this.listeners.forEach(fn => fn(this.locale));
  }

  subscribe(fn) {
    this.listeners.push(fn);
    fn(this.locale);
  }
}

Компонент:

this.localeController.subscribe(locale => {
  this.locale = locale;
  this.loadMessages(locale);
});

Это обеспечивает единообразную локализацию на всех уровнях приложения.


Оптимизация и lazy-loading

Для крупных проектов критически важно подгружать только необходимые переводы. Разделение JSON-файлов по компонентам и языкам, использование динамического import() и кэширования сообщений уменьшает нагрузку на клиент и ускоряет рендеринг.


Особенности Lit при i18n

  • Реактивные свойства обеспечивают автоматическое обновление UI при смене локали.
  • Декларативный html-шаблон позволяет легко вставлять локализованные строки без сложной логики.
  • Можно комбинировать локализацию контента и форматирование чисел/дат через API Intl.
  • Поддержка плейсхолдеров и динамических сообщений упрощает масштабирование.

Эти принципы позволяют создавать гибкие и масштабируемые веб-компоненты, полностью готовые к международной локализации, без потери преимуществ реактивного рендеринга Lit.