Vue и Vue Intl

Библиотека FormatJS предоставляет набор инструментов для локализации интерфейсов JavaScript-приложений. В экосистеме Vue она чаще всего используется совместно с библиотеками:

  • vue-intl
  • vue-i18n + FormatJS API
  • react-intl-подобными обёртками для Vue
  • низкоуровневыми пакетами @formatjs/intl-*

Основная задача FormatJS — унификация работы с:

  • переводами;
  • форматированием дат;
  • чисел и валют;
  • pluralization;
  • ICU MessageFormat;
  • локалями браузера;
  • динамической загрузкой переводов.

FormatJS опирается на стандарт Intl, встроенный в JavaScript.


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

Для Vue-проекта обычно используются следующие пакеты:

npm install vue-intl @formatjs/intl

Дополнительно могут понадобиться:

npm install @formatjs/intl-pluralrules
npm install @formatjs/intl-relativetimeformat
npm install @formatjs/intl-numberformat

Для Vue 3 чаще применяется собственная интеграция через Composition API.


Структура локализации проекта

Типичная структура каталогов:

src/
├── i18n/
│   ├── messages/
│   │   ├── en.json
│   │   ├── ru.json
│   │   └── kz.json
│   ├── index.js
│   └── formatter.js
├── components/
└── App.vue

Файлы переводов:

{
  "app.title": "Dashboard",
  "user.greeting": "Hello, {name}",
  "cart.items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}

Создание IntlProvider

Во Vue часто создаётся глобальный объект форматирования.

formatter.js

import { createIntl, createIntlCache } from '@formatjs/intl'

const cache = createIntlCache()

export function createFormatter(locale, messages) {
    return createIntl(
        {
            locale,
            messages
        },
        cache
    )
}

Инициализация локализации

i18n/index.js

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

import { createFormatter } from './formatter'

const messages = {
    en,
    ru
}

const locale = navigator.language.startsWith('ru')
    ? 'ru'
    : 'en'

export const intl = createFormatter(
    locale,
    messages[locale]
)

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

main.js

import { createApp } from 'vue'
import App from './App.vue'

import { intl } from './i18n'

const app = createApp(App)

app.provide('intl', intl)

app.mount('#app')

Получение intl внутри компонента

Composition API

<script setup>
import { inject } from 'vue'

const intl = inject('intl')

const title = intl.formatMessage({
    id: 'app.title'
})
</script>

<template>
    <h1>{{ title }}</h1>
</template>

Форматирование сообщений

formatMessage

Главный API библиотеки — formatMessage.

intl.formatMessage({
    id: 'app.title'
})

С параметрами:

intl.formatMessage(
    {
        id: 'user.greeting'
    },
    {
        name: 'Alex'
    }
)

Результат:

Hello, Alex

ICU MessageFormat

FormatJS использует ICU-синтаксис.

Переменные

{
  "welcome": "Welcome, {name}"
}
intl.formatMessage(
    { id: 'welcome' },
    { name: 'John' }
)

Pluralization

{
  "notifications": "{count, plural, =0 {Нет уведомлений} one {# уведомление} few {# уведомления} many {# уведомлений} other {# уведомления}}"
}
intl.formatMessage(
    { id: 'notifications' },
    { count: 5 }
)

Select

{
  "gender.message": "{gender, select, male {Он} female {Она} other {Они}} вошёл в систему"
}
intl.formatMessage(
    { id: 'gender.message' },
    { gender: 'female' }
)

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

{
  "complex": "{count, plural, one {{gender, select, male {Он} female {Она} other {Они}} добавил файл} other {{gender, select, male {Он} female {Она} other {Они}} добавили # файлов}}"
}

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

formatDate

intl.formatDate(new Date())

Настройка формата

intl.formatDate(new Date(), {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
})

Для русской локали:

12 марта 2026 г.

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

intl.formatTime(new Date(), {
    hour: '2-digit',
    minute: '2-digit'
})

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

intl.formatDateTimeRange(
    new Date('2026-01-01'),
    new Date('2026-01-15')
)

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

formatNumber

intl.formatNumber(1000000)

Валюты

intl.formatNumber(2500, {
    style: 'currency',
    currency: 'USD'
})

Проценты

intl.formatNumber(0.75, {
    style: 'percent'
})

Compact notation

intl.formatNumber(1500000, {
    notation: 'compact'
})

Результат:

1.5M

Relative Time Format

formatRelativeTime

intl.formatRelativeTime(-1, 'day')

Результат:

yesterday

Примеры

intl.formatRelativeTime(-5, 'minute')
intl.formatRelativeTime(2, 'week')

List Format

formatList

intl.formatList([
    'Vue',
    'React',
    'Angular'
])

Результат:

Vue, React, and Angular

Display Names

formatDisplayName

intl.formatDisplayName('US', {
    type: 'region'
})

Интеграция с Composition API

Создание composable

useIntl.js

import { inject } fr om 'vue'

export function useIntl() {
    return inject('intl')
}

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

<script setup>
import { useIntl } from '@/i18n/useIntl'

const intl = useIntl()

const price = intl.formatNumber(1999, {
    style: 'currency',
    currency: 'EUR'
})
</script>

Реактивная смена локали

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

i18n/store.js

import { ref, computed } from 'vue'

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

import { createFormatter } from './formatter'

const locale = ref('ru')

const dictionaries = {
    en,
    ru
}

export function useI18n() {

    const intl = computed(() => {
        return createFormatter(
            locale.value,
            dictionaries[locale.value]
        )
    })

    function setLocale(newLocale) {
        locale.value = newLocale
    }

    return {
        locale,
        intl,
        setLocale
    }
}

Переключение языка

<script setup>
import { useI18n } from '@/i18n/store'

const { setLocale } = useI18n()
</script>

<template>
    <button @click="setLocale('ru')">
        RU
    </button>

    <button @click="setLocale('en')">
        EN
    </button>
</template>

Lazy Loading переводов

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

async function loadMessages(locale) {

    const messages = await import(
        `./messages/${locale}.json`
    )

    return messages.default
}

Асинхронная смена локали

async function setLocale(locale) {

    const messages = await loadMessages(locale)

    intl.value = createFormatter(
        locale,
        messages
    )
}

Организация переводов

Namespaces

{
  "auth.login": "Login",
  "auth.logout": "Logout",
  "dashboard.title": "Dashboard"
}

Модульная структура

messages/
├── auth/
│   ├── en.json
│   └── ru.json
├── dashboard/
│   ├── en.json
│   └── ru.json

Объединение переводов

import authEn from './auth/en.json'
import dashboardEn from './dashboard/en.json'

export default {
    ...authEn,
    ...dashboardEn
}

Rich Text Formatting

FormatJS поддерживает JSX-подобные конструкции.

Сообщение

{
  "article": "Read <link>documentation</link>"
}

Обработка тегов

intl.formatMessage(
    {
        id: 'article'
    },
    {
        link: chunks => `<a href="/docs">${chunks}</a>`
    }
)

Валидация переводов

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

function checkMissingKeys(messages, requiredKeys) {

    return requiredKeys.filter(
        key => !messages[key]
    )
}

Извлечение сообщений

FormatJS предоставляет CLI-инструменты.

Установка CLI

npm install --save-dev @formatjs/cli

Извлечение переводов

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

Компиляция сообщений

formatjs compile-folder lang compiled-lang

Babel Plugin

Установка

npm install --save-dev babel-plugin-formatjs

Настройка Babel

module.exports = {
    plugins: [
        [
            'formatjs',
            {
                idInterpolationPattern:
                    '[sha512:contenthash:base64:6]'
            }
        ]
    ]
}

TypeScript и FormatJS

Типизация сообщений

type Messages = {
    'app.title': string
    'auth.login': string
}

Типизация formatMessage

function t(
    id: keyof Messages
) {
    return intl.formatMessage({ id })
}

SSR и Nuxt

Проблемы серверного рендера

При SSR необходимо:

  • синхронизировать локаль;
  • передавать переводы с сервера;
  • избегать hydration mismatch;
  • учитывать timezone.

Nuxt Plugin

export default defineNuxtPlugin(() => {

    const locale = 'ru'

    const intl = createFormatter(
        locale,
        messages[locale]
    )

    return {
        provide: {
            intl
        }
    }
})

Polyfills

Некоторые браузеры не поддерживают весь API Intl.

Подключение polyfill

import '@formatjs/intl-pluralrules/polyfill'
import '@formatjs/intl-relativetimeformat/polyfill'

Производительность

Кэширование

createIntlCache() уменьшает количество повторных операций форматирования.

const cache = createIntlCache()

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

Компиляция ICU-сообщений во время сборки значительно ускоряет runtime.


Lazy loading локалей

Загрузка только активного языка уменьшает размер bundle.


Обработка ошибок

onError

const intl = createIntl(
    {
        locale: 'ru',
        messages,
        onError(error) {
            console.error(error)
        }
    },
    cache
)

Fallback locale

Запасной язык

function translate(id) {

    return (
        messages.ru[id]
        || messages.en[id]
        || id
    )
}

Тестирование локализации

Unit-тесты

import { createIntl } from '@formatjs/intl'

test('formats message', () => {

    const intl = createIntl({
        locale: 'en',
        messages: {
            hello: 'Hello'
        }
    })

    expect(
        intl.formatMessage({
            id: 'hello'
        })
    ).toBe('Hello')
})

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

Пример URL

/ru/dashboard
/en/dashboard

Vue Router

{
    path: '/:locale/dashboard',
    component: Dashboard
}

Определение языка браузера

const locale = navigator.language

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

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

const locale = supported.includes(
    navigator.language
)
    ? navigator.language
    : 'en'

Локализация Pinia Store

Использование intl внутри store

import { defineStore } from 'pinia'

export const useCartStore = defineStore(
    'cart',
    {
        actions: {

            getMessage(intl) {

                return intl.formatMessage(
                    {
                        id: 'cart.title'
                    }
                )
            }
        }
    }
)

Локализация форм

Сообщения валидации

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

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

errors.push(
    intl.formatMessage({
        id: 'validation.required'
    })
)

Интеграция с Vite

Импорт JSON

import messages from './messages/ru.json'

Оптимизация сборки

export default defineConfig({
    build: {
        sourcemap: false
    }
})

Отличия FormatJS от vue-i18n

Возможность FormatJS vue-i18n
ICU MessageFormat Да Частично
Нативный Intl API Да Да
Экосистема React Сильная Нет
Гибкость Высокая Средняя
Простота интеграции Ниже Выше
Низкоуровневый контроль Да Ограничен

Когда использовать FormatJS

FormatJS особенно эффективен в проектах, где необходимы:

  • сложные ICU-конструкции;
  • единая система локализации для React и Vue;
  • точное форматирование чисел и дат;
  • SSR;
  • поддержка большого количества языков;
  • высокая производительность;
  • строгая типизация;
  • централизованная архитектура переводов.