При проектировании API, поддерживающих несколько языков, ключевым аспектом становится единообразная локализация всех текстовых ответов: сообщений об ошибках, описаний сущностей, метаданных и системных уведомлений. В экосистеме JavaScript для этих задач широко используется i18next, предоставляющий механизм управления переводами, интерполяцией и контекстной адаптацией строк.
Мультиязычный API обычно строится вокруг идеи отделения бизнес-логики от слоя представления текста. Это означает, что API возвращает не «готовые фразы», а ключи перевода либо уже локализованные строки в зависимости от конфигурации.
Существует два основных подхода:
Гибридный подход также применяется, когда часть сообщений локализуется на сервере (например, ошибки), а часть — на клиенте (например, UI-лейблы).
Определение языка — фундаментальный этап формирования ответа. Наиболее распространённые источники языка:
Accept-Language?lang=ru)Пример обработки заголовка:
const language = req.headers["accept-language"]?.split(",")[0] || "en";
В реальных системах используется более строгий парсинг с нормализацией и fallback-цепочками.
i18next предоставляет механизм загрузки ресурсов переводов и выбора языка на лету. В серверных приложениях чаще всего используется вместе с Express или Fastify.
Базовая инициализация:
import i18next from "i18next";
import Backend from "i18next-fs-backend";
i18next
.use(Backend)
.init({
fallbackLng: "en",
preload: ["en", "ru", "de"],
backend: {
loadPath: "./locales/{{lng}}/{{ns}}.json"
}
});
Структура ресурсов:
// locales/ru/errors.json
{
"USER_NOT_FOUND": "Пользователь не найден",
"INVALID_TOKEN": "Недействительный токен"
}
При формировании ответа API перевод применяется на уровне формирования DTO.
app.get("/user/:id", (req, res) => {
const lng = req.language;
const user = getUser(req.params.id);
if (!user) {
return res.status(404).json({
error: {
code: "USER_NOT_FOUND",
message: i18next.t("errors:USER_NOT_FOUND", { lng })
}
});
}
res.json({
data: {
id: user.id,
name: user.name
}
});
});
В данном подходе ключевым элементом становится разделение:
code — стабильный идентификатор ошибкиmessage — локализованное представлениеОдна из сложностей мультиязычных API — контекст. Одинаковый ключ может иметь разные переводы в зависимости от состояния системы.
{
"STATUS": {
"ACTIVE": "Активен",
"ACTIVE_ADMIN": "Активен (администратор)",
"ACTIVE_BANNED": "Активен, но ограничен"
}
}
Использование контекста:
i18next.t("STATUS.ACTIVE", { context: "ADMIN" });
Контекст позволяет избежать дублирования ключей и повышает семантическую точность переводов.
API ответы часто содержат динамические данные. i18next поддерживает интерполяцию:
{
"WELCOME_USER": "Добро пожаловать, {{name}}"
}
i18next.t("WELCOME_USER", {
name: "Alex",
lng: "ru"
});
В API это применяется для формирования человекочитаемых сообщений без нарушения структуры данных.
Сложные API часто возвращают агрегированные данные, требующие правильного склонения.
{
"ITEM_COUNT": "Выбрано {{count}} элемент",
"ITEM_COUNT_plural": "Выбрано {{count}} элементов"
}
i18next.t("ITEM_COUNT", { count: 5, lng: "ru" });
Плюрализация критична для:
В API принято отделять технический код ошибки от пользовательского сообщения.
{
"error": {
"code": "AUTH_EXPIRED",
"message": "Сессия истекла"
}
}
Такой подход позволяет:
В крупных API переводные ресурсы разделяются на namespaces:
autherrorsvalidationnotificationsПример структуры:
locales/
ru/
auth.json
errors.json
validation.json
Использование:
i18next.t("auth:LOGIN_SUCCESS", { lng: "ru" });
Такой подход уменьшает конфликт ключей и улучшает масштабируемость.
API часто требует поддержки переключения языка без перезапуска сервиса.
app.use((req, res, next) => {
const lng = req.headers["accept-language"] || "en";
req.language = lng;
next();
});
Далее язык передаётся в каждый вызов t():
i18next.t("errors:INVALID_TOKEN", { lng: req.language });
При кэшировании API важно учитывать язык как часть ключа.
Ошибка проектирования:
GET /products -> cached response (ru)
GET /products -> returns ru for en user
Корректный подход:
cache key = /products:en
cache key = /products:ru
При использовании CDN или Redis язык включается в hash ключа.
В GraphQL API локализация часто реализуется через контекст запроса:
const resolvers = {
Query: {
user: (_, __, context) => {
return {
message: i18next.t("WELCOME_USER", {
lng: context.lng,
name: context.user.name
})
};
}
}
};
Преимущество GraphQL — возможность возвращать локализованные поля выборочно, не перегружая ответ.
API часто возвращает вложенные объекты, содержащие локализованные поля:
{
product: {
id: 1,
title: i18next.t("product:TITLE_PHONE"),
description: i18next.t("product:DESC_PHONE")
}
}
В более сложных системах применяется стратегия «локализации на уровне сериализации», когда каждый DTO проходит через слой трансформации.
Fallback играет критическую роль в стабильности API:
i18next.init({
fallbackLng: ["en", "ru"]
});
Поведение:
При изменении API важно учитывать совместимость переводов:
Стратегии:
errors_v1,
errors_v2)Локализация может стать источником утечек информации, если в переводах присутствуют технические детали.
Проблемные случаи:
Поэтому API слой должен фильтровать сообщения и использовать только безопасные ключи перевода.
Основные оптимизации:
При высокой нагрузке важна стратегия «один раз загрузить — много раз использовать», так как обращения к файловой системе становятся узким местом.
Типовая структура мультиязычного ответа:
{
meta: {
lang: "ru",
timestamp: 1710000000
},
data: {},
errors: [
{
code: "VALIDATION_ERROR",
message: i18next.t("validation:REQUIRED_FIELD", { lng: "ru" })
}
]
}
Такой формат позволяет отделить данные от представления и сохранить предсказуемость контракта.
Помимо пользовательских данных API часто локализует:
Это особенно важно в распределённых системах, где логи агрегируются из разных сервисов и должны быть читаемыми в едином формате.