Typesafe-i18n для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Typesafe-i18n: Интернационализация с полной поддержкой типов в TypeScript

В современной веб-разработке интернационализация (i18n) — это не просто перевод текста, а целая система управления локализацией. Typesafe-i18n — это библиотека для TypeScript, которая поднимает интернационализацию на новый уровень, обеспечивая полную типовую безопасность на всех этапах: от написания переводов до их использования в коде.

Зачем нужна эта библиотека? Традиционные решения для i18n (например, i18next) часто работают с динамическими строками, что приводит к ошибкам: опечаткам в ключах, неверным аргументам или отсутствующим переводам. Typesafe-i18n решает эту проблему кардинально: если вы написали LL.WELCOME({name: "Алексей"}), TypeScript проверит, существует ли ключ WELCOME, принимает ли он параметр name и какого типа этот параметр. Ошибки локализации становятся ошибками компиляции, а не runtime-багами.

Библиотека идеально подходит для проектов, где важна строгая типизация: React/Vue/Angular приложения, Node.js серверы, библиотеки компонентов. Она поддерживает множественное число, интерполяцию, вложенные ключи, асинхронную загрузку и генерацию типов на основе ваших файлов переводов. В этой статье мы разберем все аспекты работы с Typesafe-i18n: от установки до продвинутых техник.

Установка

Установка производится через npm (или yarn/pnpm):

npm install typesafe-i18n

Для генерации типов и файлов переводов потребуется дополнительный пакет:

npm install --save-dev typesafe-i18n-cli

После установки инициализируйте проект:

npx typesafe-i18n --init

Эта команда создаст базовую структуру папок и файлов конфигурации.

Быстрый старт: минимальный рабочий пример

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

Шаг 1: Создайте файл перевода

Создайте файл src/i18n/en/index.ts:

import type { Translation } from '../i18n-types'

const en: Translation = {
   HI: 'Hello {name}!',
   GOODBYE: 'Goodbye, {name}!'
}

export default en

Шаг 2: Создайте файл перевода для русского языка

Создайте файл src/i18n/ru/index.ts:

import type { Translation } from '../i18n-types'

const ru: Translation = {
   HI: 'Привет, {name}!',
   GOODBYE: 'До свидания, {name}!'
}

export default ru

Шаг 3: Запустите генерацию типов

npx typesafe-i18n

Шаг 4: Используйте в коде

import { i18n } from './i18n/i18n-util'
import { loadLocale } from './i18n/i18n-util.sync'
import { L } from './i18n/i18n-util'

// Загружаем локали
loadLocale('en')
loadLocale('ru')

// Создаем экземпляр для английского
const LL_en = i18n('en')
console.log(LL_en.HI({ name: 'Alex' })) // Вывод: Hello Alex!

// Переключаемся на русский
const LL_ru = i18n('ru')
console.log(LL_ru.HI({ name: 'Алексей' })) // Вывод: Привет, Алексей!

Обратите внимание: если вы попытаетесь вызвать LL_en.HI() без аргумента name, TypeScript выдаст ошибку компиляции. Если вы опечатаетесь в ключе (LL_en.Hello) — тоже ошибка.

Основные методы и функции

Рассмотрим 8 ключевых функций и методов библиотеки Typesafe-i18n.

1. i18n(locale: Locales): TranslationFunctions

Сигнатура: i18n(locale: Locales): TranslationFunctions

Что делает: Создает экземпляр переводчика для указанной локали. Возвращает объект со всеми функциями перевода.

import { i18n } from './i18n/i18n-util'
import { loadLocale } from './i18n/i18n-util.sync'

loadLocale('en')
const LL = i18n('en')
console.log(LL.HI({ name: 'World' })) // "Hello World!"

2. loadLocale(locale: Locales): void

Сигнатура: loadLocale(locale: Locales): void

Что делает: Синхронно загружает файл перевода для указанной локали. Должен быть вызван до использования i18n().

import { loadLocale } from './i18n/i18n-util.sync'

// Загружаем несколько локалей
loadLocale('en')
loadLocale('ru')
loadLocale('de')

3. loadLocaleAsync(locale: Locales): Promise<void>

Сигнатура: loadLocaleAsync(locale: Locales): Promise<void>

Что делает: Асинхронно загружает файл перевода. Полезно для ленивой загрузки больших файлов локализации.

import { loadLocaleAsync } from './i18n/i18n-util.async'

async function initApp() {
   await loadLocaleAsync('en')
   const LL = i18n('en')
   console.log(LL.HI({ name: 'Async World' }))
}

4. detectLocale(): Locales

Сигнатура: detectLocale(): Locales

Что делает: Определяет предпочтительную локаль пользователя на основе браузера (navigator.language) или окружения.

import { detectLocale } from './i18n/i18n-util'
import { i18n } from './i18n/i18n-util'
import { loadLocale } from './i18n/i18n-util.sync'

const locale = detectLocale()
loadLocale(locale)
const LL = i18n(locale)
console.log(LL.HI({ name: 'User' }))

5. isLocale(locale: string): locale is Locales

Сигнатура: isLocale(locale: string): locale is Locales

Что делает: Проверяет, является ли переданная строка допустимой локалью (тип-гард).

import { isLocale } from './i18n/i18n-util'

const userLocale = navigator.language // например, "fr-CH"
if (isLocale(userLocale)) {
   // TypeScript знает, что userLocale — это "en" | "ru" | "de"
   loadLocale(userLocale)
   const LL = i18n(userLocale)
}

6. getLocale(): Locales

Сигнатура: getLocale(): Locales

Что делает: Возвращает текущую активную локаль (по умолчанию — первую загруженную).

import { getLocale, i18n, loadLocale } from './i18n/i18n-util'

loadLocale('en')
const LL = i18n('en')
console.log(getLocale()) // "en"

7. setLocale(locale: Locales): void

Сигнатура: setLocale(locale: Locales): void

Что делает: Устанавливает новую активную локаль. Все последующие вызовы i18n() будут использовать эту локаль по умолчанию.

import { setLocale, i18n, loadLocale } from './i18n/i18n-util'

loadLocale('en')
loadLocale('ru')

setLocale('ru')
const LL = i18n() // теперь по умолчанию русский
console.log(LL.HI({ name: 'Мир' })) // "Привет, Мир!"

8. L — объект с типами локалей

Сигнатура: export const L: Locales

Что делает: Предоставляет константы для всех доступных локалей. Полезно для автодополнения.

import { L, loadLocale, i18n } from './i18n/i18n-util'

// L.en, L.ru, L.de — это строковые литералы
loadLocale(L.en)
const LL = i18n(L.en)

Реальный практический пример

Создадим полноценное React-приложение с переключением языка, поддержкой множественного числа и интерполяции.

1. Структура файлов переводов

src/i18n/en/index.ts:

import type { Translation } from '../i18n-types'

const en: Translation = {
   APP_TITLE: 'Task Manager',
   TASK_COUNT: '{count} task(s) remaining',
   ADD_TASK: 'Add task',
   DELETE_TASK: 'Delete',
   COMPLETED: 'Completed',
   PENDING: 'Pending',
   GREETING: 'Hello, {name}! You have {count} pending tasks.',
   FOOTER: '© 2024 Task Manager. All rights reserved.'
}

export default en

src/i18n/ru/index.ts:

import type { Translation } from '../i18n-types'

const ru: Translation = {
   APP_TITLE: 'Менеджер задач',
   TASK_COUNT: 'Осталось {count} задач(а)',
   ADD_TASK: 'Добавить задачу',
   DELETE_TASK: 'Удалить',
   COMPLETED: 'Выполнено',
   PENDING: 'В ожидании',
   GREETING: 'Привет, {name}! У вас {count} задач(и) в ожидании.',
   FOOTER: '© 2024 Менеджер задач. Все права защищены.'
}

export default ru

2. React-компонент с локализацией

src/App.tsx:

import React, { useState, useEffect } from 'react'
import {
   i18n,
   loadLocale,
   detectLocale,
   isLocale,
   setLocale,
   getLocale
} from './i18n/i18n-util'
import { L } from './i18n/i18n-util'

// Типы для задач
interface Task {
   id: number
   text: string
   completed: boolean
}

const App: React.FC = () => {
   // Состояния
   const [currentLocale, setCurrentLocale] = useState('en')
   const [tasks, setTasks] = useState([
      { id: 1, text: 'Learn TypeScript', completed: false },
      { id: 2, text: 'Build a project', completed: true },
      { id: 3, text: 'Write documentation', completed: false }
   ])
   const [newTaskText, setNewTaskText] = useState('')
   const [LL, setLL] = useState(() => i18n('en'))

   // Инициализация: определяем локаль и загружаем переводы
   useEffect(() => {
      const detected = detectLocale()
      const locale = isLocale(detected) ? detected : 'en'
      
      // Загружаем обе локали заранее
      loadLocale('en')
      loadLocale('ru')
      
      setCurrentLocale(locale)
      setLL(() => i18n(locale))
   }, [])

   // Переключение языка
   const switchLanguage = (locale: typeof L.en) => {
      setLocale(locale)
      setCurrentLocale(locale)
      setLL(() => i18n(locale))
   }

   // Добавление задачи
   const addTask = () => {
      if (!newTaskText.trim()) return
      const newTask: Task = {
         id: Date.now(),
         text: newTaskText,
         completed: false
      }
      setTasks(prev => [...prev, newTask])
      setNewTaskText('')
   }

   // Переключение статуса задачи
   const toggleTask = (id: number) => {
      setTasks(prev =>
         prev.map(task =>
            task.id === id ? { ...task, completed: !task.completed } : task
         )
      )
   }

   // Удаление задачи
   const deleteTask = (id: number) => {
      setTasks(prev => prev.filter(task => task.id !== id))
   }

   // Подсчет задач
   const pendingTasks = tasks.filter(t => !t.completed).length
   const completedTasks = tasks.filter(t => t.completed).length

   return (
      
{/* Заголовок */}

{LL.APP_TITLE()}

{/* Переключатель языка */}
{/* Приветствие */}

{LL.GREETING({ name: 'User', count: pendingTasks })}

{/* Счетчик задач */}

{LL.TASK_COUNT({ count: pendingTasks })} | {LL.COMPLETED()}: {completedTasks} | {LL.PENDING()}: {pendingTasks}

{/* Форма добавления */}

Рекомендаций