Svelte

Интернационализация в приложениях на Svelte строится вокруг трёх основных задач:

  • хранение переводов;
  • определение текущей локали;
  • форматирование текста, чисел, дат и сообщений.

Библиотека FormatJS предоставляет набор стандартов и инструментов, основанных на спецификации ICU MessageFormat и API Intl, встроенных в JavaScript.

В экосистеме Svelte чаще всего используется пакет:

  • intl-messageformat

Дополнительно могут применяться:

  • @formatjs/intl
  • @formatjs/cli
  • @formatjs/icu-messageformat-parser

FormatJS не навязывает архитектуру, поэтому интеграция в Svelte выполняется вручную через stores, контекст или собственные сервисы локализации.


Установка зависимостей

npm install intl-messageformat

Для извлечения переводимых строк:

npm install --save-dev @formatjs/cli

Структура проекта

Типичная структура:

src/
├── i18n/
│   ├── index.js
│   ├── locales/
│   │   ├── en.json
│   │   └── ru.json
│   └── store.js
├── routes/
└── components/

Файлы локализации

Английская локаль

{
  "app.title": "Dashboard",
  "menu.profile": "Profile",
  "notifications.count": "{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
}

Русская локаль

{
  "app.title": "Панель управления",
  "menu.profile": "Профиль",
  "notifications.count": "{count, plural, =0 {Нет уведомлений} one {# уведомление} few {# уведомления} many {# уведомлений} other {# уведомления}}"
}

Создание store локализации

В Svelte основным механизмом реактивности являются stores.

store.js

import { writable } from 'svelte/store'

export const locale = writable('ru')

Создание сервиса переводов

index.js

import { get } from 'svelte/store'
import { locale } from './store'
import { IntlMessageFormat } from 'intl-messageformat'

import ru from './locales/ru.json'
import en from './locales/en.json'

const messages = {
  ru,
  en
}

export function t(id, values = {}) {
  const currentLocale = get(locale)

  const message = messages[currentLocale][id]

  if (!message) {
    return id
  }

  const formatter = new IntlMessageFormat(
    message,
    currentLocale
  )

  return formatter.format(values)
}

Использование переводов в компонентах

App.svelte

<script>
  import { t } from './i18n'
</script>

<h1>{t('app.title')}</h1>

Реактивное изменение языка

LanguageSwitcher.svelte

<script>
  import { locale } from './i18n/store'

  function setLocale(lang) {
    locale.set(lang)
  }
</script>

<button on:click={() => setLocale('ru')}>
  RU
</button>

<button on:click={() => setLocale('en')}>
  EN
</button>

При изменении store все компоненты автоматически перерисовываются.


ICU MessageFormat

FormatJS использует ICU-синтаксис — промышленный стандарт интернационализации.

Поддерживаются:

  • plural;
  • select;
  • number;
  • date;
  • time;
  • nested messages.

Pluralization

Сообщение

{
  "cart.items": "{count, plural,
    =0 {Корзина пуста}
    one {# товар}
    few {# товара}
    many {# товаров}
    other {# товара}
  }"
}

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

<script>
  import { t } from './i18n'

  let count = 5
</script>

<p>{t('cart.items', { count })}</p>

Sel ect Expressions

Позволяют переключать текст по значению.

{
  "user.gender": "{gender, select,
    male {Он автор}
    female {Она автор}
    other {Автор}
  }"
}

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

t('user.gender', {
  gender: 'female'
})

Вложенные конструкции

ICU поддерживает сложную композицию.

{
  "complex.message": "{count, plural,
    one {{gender, select,
      male {Он добавил}
      female {Она добавила}
      other {Пользователь добавил}
    } # комментарий}
    other {{gender, select,
      male {Он добавил}
      female {Она добавила}
      other {Пользователь добавил}
    } # комментариев}
  }"
}

Форматирование чисел

FormatJS опирается на Intl.NumberFormat.

const number = new Intl.NumberFormat(
  'ru-RU',
  {
    style: 'currency',
    currency: 'RUB'
  }
).format(12500)

Результат:

12 500,00 ₽

Форматирование чисел через ICU

{
  "price.label": "Цена: {price, number, ::currency/RUB}"
}

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

t('price.label', {
  price: 1500
})

Форматирование дат

{
  "post.date": "Дата публикации: {createdAt, date, long}"
}

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

t('post.date', {
  createdAt: new Date()
})

Форматирование времени

{
  "event.time": "Начало: {startTime, time, short}"
}

Пользовательские форматы

FormatJS поддерживает кастомные форматы.

const formatter = new IntlMessageFormat(
  '{value, number, customCurrency}',
  'ru',
  {
    customCurrency: {
      style: 'currency',
      currency: 'KZT'
    }
  }
)

Реактивный store переводчика

Для более удобной интеграции в Svelte можно создать derived store.

translator.js

import { derived } fr om 'svelte/store'
import { locale } fr om './store'

import ru from './locales/ru.json'
import en from './locales/en.json'

import { IntlMessageFormat } from 'intl-messageformat'

const messages = {
  ru,
  en
}

export const translator = derived(
  locale,
  ($locale) => {
    return (id, values = {}) => {
      const message =
        messages[$locale][id]

      if (!message) {
        return id
      }

      const formatter =
        new IntlMessageFormat(
          message,
          $locale
        )

      return formatter.format(values)
    }
  }
)

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

<script>
  import { translator } from './i18n/translator'

  $: t = $translator
</script>

<h1>{t('app.title')}</h1>

Lazy Loading локалей

Крупные приложения не загружают все языки одновременно.

Динамический импорт

async function loadLocale(lang) {
  const messages = await import(
    `./locales/${lang}.json`
  )

  return messages.default
}

Асинхронная загрузка переводов

import { writable } from 'svelte/store'

export const messages = writable({})

export async function setLocale(lang) {
  const module = await import(
    `./locales/${lang}.json`
  )

  messages.set(module.default)
}

Кэширование форматтеров

Создание IntlMessageFormat — дорогостоящая операция.

Плохой вариант:

new IntlMessageFormat(...)

при каждом рендере.

Правильный подход — кэш.

const cache = new Map()

function getFormatter(message, locale) {
  const key = `${locale}:${message}`

  if (!cache.has(key)) {
    cache.set(
      key,
      new IntlMessageFormat(
        message,
        locale
      )
    )
  }

  return cache.get(key)
}

Оптимизированный переводчик

export function t(id, values = {}) {
  const currentLocale = get(locale)

  const message =
    messages[currentLocale][id]

  if (!message) {
    return id
  }

  const formatter =
    getFormatter(
      message,
      currentLocale
    )

  return formatter.format(values)
}

SSR и SvelteKit

В SvelteKit интернационализация особенно важна для SSR.

Основные задачи:

  • определение локали на сервере;
  • генерация HTML на нужном языке;
  • предотвращение hydration mismatch.

Определение локали в hooks.server.js

export async function handle({
  event,
  resolve
}) {
  const language =
    event.request.headers.get(
      'accept-language'
    )

  event.locals.locale =
    language?.startsWith('ru')
      ? 'ru'
      : 'en'

  return resolve(event)
}

Передача локали в layout

+layout.server.js

export function load({ locals }) {
  return {
    locale: locals.locale
  }
}

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

+layout.svelte

<script>
  import { locale } from '$lib/i18n/store'

  export let data

  $: locale.set(data.locale)
</script>

<slot />

Локализованные URL

SvelteKit удобно комбинируется с маршрутизацией по языкам.

Примеры:

/ru/dashboard
/en/dashboard

Создание параметризованных маршрутов

src/routes/[lang]/+layout.svelte

Получение параметра языка

export function load({ params }) {
  return {
    locale: params.lang
  }
}

Проверка поддерживаемых языков

const supported = ['ru', 'en']

if (!supported.includes(params.lang)) {
  throw error(404)
}

Автоматическое определение языка браузера

const language =
  navigator.language

или:

const language =
  navigator.languages[0]

Fallback-локаль

Система переводов должна поддерживать резервный язык.

const fallbackLocale = 'en'

Реализация fallback

function getMessage(locale, id) {
  return (
    messages[locale]?.[id] ??
    messages[fallbackLocale]?.[id] ??
    id
  )
}

Отсутствующие переводы

Для разработки полезно логирование.

if (!message) {
  console.warn(
    `Missing translation: ${id}`
  )
}

Типизация переводов

В TypeScript можно автоматически типизировать ключи.

messages.ts

import ru from './locales/ru.json'

export type MessageKey =
  keyof typeof ru

Типизированный переводчик

export function t(
  id: MessageKey,
  values?: Record<string, unknown>
) {
  // ...
}

Извлечение переводимых сообщений

FormatJS CLI умеет анализировать исходный код.

Пример:

formatjs extract "src/**/*.{js,svelte}"

Генерация файлов переводов

formatjs extract \
  "src/**/*.{js,svelte}" \
  --out-file lang/en.json

Проверка ICU-сообщений

formatjs compile lang/en.json

Предкомпиляция сообщений

FormatJS позволяет компилировать ICU заранее.

Преимущества:

  • меньше runtime-нагрузка;
  • быстрее рендеринг;
  • меньше парсинга в браузере.

Компиляция переводов

formatjs compile-folder \
  lang/ compiled-lang/

Работа с HTML

ICU не предназначен для хранения HTML.

Плохой пример:

{
  "welcome": "<b>Добро пожаловать</b>"
}

Безопасная вставка HTML

Лучше разделять структуру и текст.

<strong>{t('welcome')}</strong>

Форматирование списков

API Intl.ListFormat:

const formatter =
  new Intl.ListFormat('ru', {
    style: 'long',
    type: 'conjunction'
  })

formatter.format([
  'JavaScript',
  'Svelte',
  'FormatJS'
])

Результат:

JavaScript, Svelte и FormatJS

Форматирование относительного времени

const formatter =
  new Intl.RelativeTimeFormat(
    'ru',
    {
      numeric: 'auto'
    }
  )

formatter.format(-1, 'day')

Результат:

вчера

Форматирование диапазонов

const formatter =
  new Intl.DateTimeFormat(
    'ru'
  )

formatter.formatRange(
  new Date('2025-01-01'),
  new Date('2025-01-05')
)

Локализация ошибок

{
  "errors.required": "Поле обязательно",
  "errors.email": "Некорректный email"
}

Интеграция с формами

{#if errors.email}
  <span>
    {t('errors.email')}
  </span>
{/if}

Интернационализация API-ошибок

Backend может возвращать ключи сообщений.

Пример ответа API:

{
  "error": "errors.accessDenied"
}

Frontend:

t(response.error)

Разделение переводов по модулям

Крупные приложения разбивают переводы.

locales/
├── common/
├── dashboard/
├── auth/
└── profile/

Слияние модулей

const messages = {
  ...common,
  ...dashboard,
  ...auth
}

Namespace-подход

{
  "auth.login": "Вход",
  "auth.logout": "Выход"
}

Проблема длинных ICU-сообщений

Сложные конструкции ухудшают читаемость.

Плохой вариант:

{
  "huge.message": "{count, plural, ...}"
}

Лучше разделять сообщения логически.


Локализация meta-тегов

В SvelteKit:

<svelte:head>
  <title>
    {t('page.title')}
  </title>
</svelte:head>

Локализация атрибутов

<input
  placeholder={t('search.placeholder')}
/>

Интернационализация aria-label

<button
  aria-label={t('buttons.close')}
>
  ×
</button>

Поддержка RTL

Для арабского и иврита:

<html dir="rtl">

Автоматическое определение направления

const rtlLocales = ['ar', 'he']

const dir =
  rtlLocales.includes(locale)
    ? 'rtl'
    : 'ltr'

Изменение direction в Svelte

<svelte:head>
  <html lang={$locale} dir={dir} />
</svelte:head>

Тестирование переводов

Проверяются:

  • наличие всех ключей;
  • корректность ICU;
  • fallback;
  • plural rules;
  • SSR.

Проверка отсутствующих ключей

const ruKeys = Object.keys(ru)
const enKeys = Object.keys(en)

const missing =
  ruKeys.filter(
    key => !enKeys.includes(key)
  )

Snapshot-тестирование

expect(
  t('app.title')
).toMatchSnapshot()

Распространённые ошибки

Создание formatter при каждом рендере

Неправильно:

$: text = new IntlMessageFormat(...)

Хранение HTML в переводах

Неправильно:

{
  "dangerous": "<script>"
}

Отсутствие fallback

Ошибка приводит к пустым строкам интерфейса.


Смешивание бизнес-логики и переводов

Плохо:

{
  "admin.message":
    "Администратор может..."
}

при сложной ролевой логике.


Архитура production-уровня

Обычно включает:

  • lazy loading локалей;
  • предкомпиляцию ICU;
  • кэширование formatter;
  • SSR;
  • typed translations;
  • namespace modules;
  • fallback locale;
  • extraction pipeline;
  • CI-проверки переводов;
  • CDN-кэширование JSON-файлов.

Пример production-структуры

src/
├── lib/
│   ├── i18n/
│   │   ├── client.js
│   │   ├── server.js
│   │   ├── formatter.js
│   │   ├── cache.js
│   │   ├── locale.js
│   │   └── loaders/
│   └── stores/
├── routes/
├── messages/
│   ├── en/
│   └── ru/
└── tests/