Модуль @nuxtjs/i18n, стратегии локализации URL, useI18n composable, pluralization и lazy-загрузка переводов
Интернационализация (i18n) — это архитектурный выбор, который лучше сделать в начале проекта. Добавить поддержку нескольких языков постфактум значительно сложнее, чем изначально строить с учётом локализации.
i18n решает три класса проблем:
Переводы — замена строк в интерфейсе в зависимости от языка. Это не просто подстановка слов — разные языки имеют разный порядок слов, грамматические формы, длину.
Форматирование — дата 2026-04-03 в русской локали выглядит как 3 апреля 2026 г., в американской как April 3, 2026. Числа, валюты, единицы измерения — всё это локалезависимо.
Маршрутизация — URL может включать код языка: /ru/about, /en/about. Это влияет на SEO и пользовательский опыт.
npm install @nuxtjs/i18nКонфигурация в nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
// Список поддерживаемых локалей
locales: [
{ code: 'ru', name: 'Русский', file: 'ru.json' },
{ code: 'en', name: 'English', file: 'en.json' },
{ code: 'de', name: 'Deutsch', file: 'de.json' }
],
defaultLocale: 'ru',
// Путь к файлам переводов
langDir: 'locales/',
// Стратегия маршрутизации
strategy: 'prefix_except_default',
// Lazy loading — загружаем переводы по запросу
lazy: true
}
})Стратегия определяет как язык отражается в URL:
no_prefix — нет префикса вообще. Язык определяется через cookie или Accept-Language. URL: /about для всех языков. Плохо для SEO — у страниц нет канонических URL по языку.
prefix_except_default — префикс для всех языков кроме дефолтного. URL: /about (русский), /en/about (английский). Компромисс: дефолтный язык не получает дополнительный сегмент URL.
prefix — префикс для всех языков включая дефолтный. URL: /ru/about, /en/about. Идеально для SEO и multilingual сайтов — все URL однозначны.
prefix_and_default — как prefix, но дефолтный язык доступен и с, и без префикса (редирект). Полезно при миграции с существующего сайта.
Создайте директорию locales/ в корне проекта:
// locales/ru.json
{
"common": {
"save": "Сохранить",
"cancel": "Отмена",
"loading": "Загрузка...",
"error": "Произошла ошибка"
},
"nav": {
"home": "Главная",
"about": "О нас",
"courses": "Курсы",
"profile": "Профиль"
},
"auth": {
"login": "Войти",
"logout": "Выйти",
"register": "Зарегистрироваться",
"email": "Email",
"password": "Пароль",
"forgot_password": "Забыли пароль?"
},
"course": {
"title": "Название курса",
"lessons_count": "{count} урок | {count} урока | {count} уроков"
}
}// locales/en.json
{
"common": {
"save": "Save",
"cancel": "Cancel",
"loading": "Loading...",
"error": "An error occurred"
},
"nav": {
"home": "Home",
"about": "About",
"courses": "Courses",
"profile": "Profile"
},
"auth": {
"login": "Sign In",
"logout": "Sign Out",
"register": "Sign Up",
"email": "Email",
"password": "Password",
"forgot_password": "Forgot password?"
},
"course": {
"title": "Course Title",
"lessons_count": "{count} lesson | {count} lessons"
}
}useI18n() предоставляет все инструменты для работы с переводами в компонентах:
<script setup lang="ts">
const {
t, // функция перевода
locale, // текущая локаль (ref)
locales, // все доступные локали
setLocale // программное переключение языка
} = useI18n()
// Переключение языка
async function switchToEnglish() {
await setLocale('en')
}
</script>
<template>
<nav>
<NuxtLink to="/">{{ t('nav.home') }}</NuxtLink>
<NuxtLink to="/about">{{ t('nav.about') }}</NuxtLink>
</nav>
<p>Текущий язык: {{ locale }}</p>
<button @click="switchToEnglish">Switch to English</button>
</template>Параметры в переводах позволяют вставлять динамические значения:
{
"greeting": "Привет, {name}!",
"items_count": "У вас {count} {item} в корзине",
"last_seen": "Последний визит: {date}"
}<script setup lang="ts">
const { t } = useI18n()
const userName = ref('Иван')
const cartItems = ref(3)
</script>
<template>
<!-- t('greeting', { name: 'Иван' }) → "Привет, Иван!" -->
<p>{{ t('greeting', { name: userName }) }}</p>
<!-- Несколько параметров -->
<p>{{ t('items_count', { count: cartItems, item: 'товара' }) }}</p>
</template>Русский язык имеет три формы множественного числа (не две как в английском). @nuxtjs/i18n поддерживает это через символ |:
{
"lessons": "0 уроков | {count} урок | {count} урока | {count} уроков",
"days": "{count} день | {count} дня | {count} дней",
"files": "{count} файл | {count} файла | {count} файлов"
}Правило для русских плюралей:
<script setup lang="ts">
const { t } = useI18n()
// t('lessons', 1) → "1 урок"
// t('lessons', 3) → "3 урока"
// t('lessons', 5) → "5 уроков"
// t('lessons', 21) → "21 урок"
</script>
<template>
<p>{{ t('lessons', lessonsCount) }}</p>
</template>Для навигации с учётом текущего языка используйте localePath:
<script setup lang="ts">
const localePath = useLocalePath()
</script>
<template>
<!-- Автоматически добавляет /en/ префикс при необходимости -->
<NuxtLink :to="localePath('/about')">О нас</NuxtLink>
<NuxtLink :to="localePath('/courses/vue')">Курс по Vue</NuxtLink>
<!-- Или через name маршрута -->
<NuxtLink :to="localePath({ name: 'courses-id', params: { id: '123' } })">
Курс
</NuxtLink>
</template>Никогда не хардкодьте /ru/about или /en/about — это сломается при смене стратегии маршрутизации.
<script setup lang="ts">
const { locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()
</script>
<template>
<div class="language-switcher">
<NuxtLink
v-for="locale in locales"
:key="locale.code"
:to="switchLocalePath(locale.code)"
>
{{ locale.name }}
</NuxtLink>
</div>
</template>switchLocalePath генерирует URL текущей страницы для другого языка. При переходе пользователь остаётся на той же странице, но в другой локали.
@nuxtjs/i18n автоматически генерирует hreflang теги, которые помогают Google показывать правильную языковую версию страницы:
<!-- pages/about.vue -->
<script setup lang="ts">
const { t } = useI18n()
// Генерирует SEO теги для всех языков
const head = useLocaleHead({
addDirAttribute: true, // Добавляет dir="ltr/rtl"
addSeoAttributes: true // Добавляет hreflang и canonical
})
useHead(head)
// Или вручную через useSeoMeta
useSeoMeta({
title: t('about.meta_title'),
description: t('about.meta_description')
})
</script>Результирующий HTML будет содержать:
<html lang="ru" dir="ltr">
<head>
<link rel="canonical" href="https://example.com/about" />
<link rel="alternate" hreflang="ru" href="https://example.com/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />
</head>При lazy: true файлы переводов загружаются только по необходимости. Для языков с большими файлами переводов это значительно уменьшает initial bundle:
// nuxt.config.ts
i18n: {
lazy: true,
langDir: 'locales/',
locales: [
{ code: 'ru', file: 'ru.json' },
{ code: 'en', file: 'en.json' }
]
}С lazy: true при инициализации загружается только дефолтная локаль. Переключение на другой язык инициирует загрузку соответствующего файла.
Для автодополнения ключей переводов в TypeScript создайте декларацию типов:
// types/i18n.d.ts
import ru from '../locales/ru.json'
// Типизируем структуру переводов
declare module '#i18n' {
export interface NuxtI18nOptions {
messages: {
ru: typeof ru
en: typeof ru // En использует ту же структуру что ru
}
}
}После этого t('nav.home') будет проверяться TypeScript — опечатки в ключах станут ошибками компиляции.
Для форматирования дат и чисел не нужны дополнительные библиотеки — JavaScript Intl API прекрасно справляется:
<script setup lang="ts">
const { locale } = useI18n()
// Форматирование числа под текущую локаль
function formatNumber(value: number): string {
return new Intl.NumberFormat(locale.value, {
minimumFractionDigits: 0,
maximumFractionDigits: 2
}).format(value)
}
// Форматирование цены
function formatPrice(value: number, currency: string = 'RUB'): string {
return new Intl.NumberFormat(locale.value, {
style: 'currency',
currency
}).format(value)
}
// Форматирование даты
function formatDate(date: Date | string): string {
return new Intl.DateTimeFormat(locale.value, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date(date))
}
</script>
<template>
<p>Цена: {{ formatPrice(1990) }}</p>
<!-- ru: "1 990 ₽", en: "₽1,990.00" -->
<p>Дата: {{ formatDate('2026-04-03') }}</p>
<!-- ru: "3 апреля 2026 г.", en: "April 3, 2026" -->
</template>Создайте composable useFormatters() для переиспользования этих функций во всём приложении — и не повторяйте одинаковую логику форматирования в каждом компоненте.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.