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}
{/* Форма добавления */}
Также в библиотеке
Рекомендаций