Интернационализация в приложениях на Svelte строится вокруг трёх основных задач:
Библиотека FormatJS предоставляет набор стандартов и инструментов,
основанных на спецификации ICU MessageFormat и API Intl,
встроенных в JavaScript.
В экосистеме Svelte чаще всего используется пакет:
intl-messageformatДополнительно могут применяться:
@formatjs/intl@formatjs/cli@formatjs/icu-messageformat-parserFormatJS не навязывает архитектуру, поэтому интеграция в 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 {# уведомления}}"
}
В Svelte основным механизмом реактивности являются stores.
import { writable } from 'svelte/store'
export const locale = writable('ru')
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)
}
<script>
import { t } from './i18n'
</script>
<h1>{t('app.title')}</h1>
<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 все компоненты автоматически перерисовываются.
FormatJS использует ICU-синтаксис — промышленный стандарт интернационализации.
Поддерживаются:
{
"cart.items": "{count, plural,
=0 {Корзина пуста}
one {# товар}
few {# товара}
many {# товаров}
other {# товара}
}"
}
<script>
import { t } from './i18n'
let count = 5
</script>
<p>{t('cart.items', { count })}</p>
Позволяют переключать текст по значению.
{
"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 ₽
{
"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'
}
}
)
Для более удобной интеграции в Svelte можно создать derived store.
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)
}
}
)
<script>
import { translator } from './i18n/translator'
$: t = $translator
</script>
<h1>{t('app.title')}</h1>
Крупные приложения не загружают все языки одновременно.
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)
}
В SvelteKit интернационализация особенно важна для SSR.
Основные задачи:
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)
}
export function load({ locals }) {
return {
locale: locals.locale
}
}
<script>
import { locale } from '$lib/i18n/store'
export let data
$: locale.set(data.locale)
</script>
<slot />
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]
Система переводов должна поддерживать резервный язык.
const fallbackLocale = 'en'
function getMessage(locale, id) {
return (
messages[locale]?.[id] ??
messages[fallbackLocale]?.[id] ??
id
)
}
Для разработки полезно логирование.
if (!message) {
console.warn(
`Missing translation: ${id}`
)
}
В TypeScript можно автоматически типизировать ключи.
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
formatjs compile lang/en.json
FormatJS позволяет компилировать ICU заранее.
Преимущества:
formatjs compile-folder \
lang/ compiled-lang/
ICU не предназначен для хранения HTML.
Плохой пример:
{
"welcome": "<b>Добро пожаловать</b>"
}
Лучше разделять структуру и текст.
<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}
Backend может возвращать ключи сообщений.
Пример ответа API:
{
"error": "errors.accessDenied"
}
Frontend:
t(response.error)
Крупные приложения разбивают переводы.
locales/
├── common/
├── dashboard/
├── auth/
└── profile/
const messages = {
...common,
...dashboard,
...auth
}
{
"auth.login": "Вход",
"auth.logout": "Выход"
}
Сложные конструкции ухудшают читаемость.
Плохой вариант:
{
"huge.message": "{count, plural, ...}"
}
Лучше разделять сообщения логически.
В SvelteKit:
<svelte:head>
<title>
{t('page.title')}
</title>
</svelte:head>
<input
placeholder={t('search.placeholder')}
/>
<button
aria-label={t('buttons.close')}
>
×
</button>
Для арабского и иврита:
<html dir="rtl">
const rtlLocales = ['ar', 'he']
const dir =
rtlLocales.includes(locale)
? 'rtl'
: 'ltr'
<svelte:head>
<html lang={$locale} dir={dir} />
</svelte:head>
Проверяются:
const ruKeys = Object.keys(ru)
const enKeys = Object.keys(en)
const missing =
ruKeys.filter(
key => !enKeys.includes(key)
)
expect(
t('app.title')
).toMatchSnapshot()
Неправильно:
$: text = new IntlMessageFormat(...)
Неправильно:
{
"dangerous": "<script>"
}
Ошибка приводит к пустым строкам интерфейса.
Плохо:
{
"admin.message":
"Администратор может..."
}
при сложной ролевой логике.
Обычно включает:
src/
├── lib/
│ ├── i18n/
│ │ ├── client.js
│ │ ├── server.js
│ │ ├── formatter.js
│ │ ├── cache.js
│ │ ├── locale.js
│ │ └── loaders/
│ └── stores/
├── routes/
├── messages/
│ ├── en/
│ └── ru/
└── tests/