Мультиязычные API ответы

При проектировании API, поддерживающих несколько языков, ключевым аспектом становится единообразная локализация всех текстовых ответов: сообщений об ошибках, описаний сущностей, метаданных и системных уведомлений. В экосистеме JavaScript для этих задач широко используется i18next, предоставляющий механизм управления переводами, интерполяцией и контекстной адаптацией строк.

Архитектура мультиязычного API

Мультиязычный API обычно строится вокруг идеи отделения бизнес-логики от слоя представления текста. Это означает, что API возвращает не «готовые фразы», а ключи перевода либо уже локализованные строки в зависимости от конфигурации.

Существует два основных подхода:

  • Серверная локализация — API возвращает строки на языке клиента
  • Клиентская локализация — API возвращает ключи и параметры, а перевод выполняется на стороне клиента

Гибридный подход также применяется, когда часть сообщений локализуется на сервере (например, ошибки), а часть — на клиенте (например, UI-лейблы).

Определение языка запроса

Определение языка — фундаментальный этап формирования ответа. Наиболее распространённые источники языка:

  • HTTP-заголовок Accept-Language
  • JWT или session claims
  • Query-параметр (?lang=ru)
  • Профиль пользователя в базе данных

Пример обработки заголовка:

const language = req.headers["accept-language"]?.split(",")[0] || "en";

В реальных системах используется более строгий парсинг с нормализацией и fallback-цепочками.

Интеграция i18next в API слой

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 ответов

При формировании ответа 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

Одна из сложностей мультиязычных 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

Сложные API часто возвращают агрегированные данные, требующие правильного склонения.

{
  "ITEM_COUNT": "Выбрано {{count}} элемент",
  "ITEM_COUNT_plural": "Выбрано {{count}} элементов"
}
i18next.t("ITEM_COUNT", { count: 5, lng: "ru" });

Плюрализация критична для:

  • корзин интернет-магазинов
  • уведомлений
  • логов операций
  • статистических ответов

Локализация ошибок и кодов

В API принято отделять технический код ошибки от пользовательского сообщения.

{
  "error": {
    "code": "AUTH_EXPIRED",
    "message": "Сессия истекла"
  }
}

Такой подход позволяет:

  • сохранять стабильность интеграций
  • изменять тексты без изменения контракта API
  • поддерживать множественные языки без дублирования логики

Namespaces и модульность переводов

В крупных API переводные ресурсы разделяются на namespaces:

  • auth
  • errors
  • validation
  • notifications

Пример структуры:

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 и мультиязычность

В 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-языков

Fallback играет критическую роль в стабильности API:

i18next.init({
  fallbackLng: ["en", "ru"]
});

Поведение:

  • отсутствует перевод на ru → используется en
  • отсутствует en → используется ключ
  • отсутствует ключ → возвращается fallback string

Версионирование переводов

При изменении API важно учитывать совместимость переводов:

  • v1 API использует старые ключи
  • v2 API вводит обновлённые сообщения

Стратегии:

  • параллельные namespaces (errors_v1, errors_v2)
  • feature flags для переводов
  • отдельные resource bundles по версии API

Безопасность мультиязычных ответов

Локализация может стать источником утечек информации, если в переводах присутствуют технические детали.

Проблемные случаи:

  • SQL ошибки в тексте перевода
  • stack trace в локализованных сообщениях
  • внутренние коды системы

Поэтому API слой должен фильтровать сообщения и использовать только безопасные ключи перевода.

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

Основные оптимизации:

  • preload языков вместо динамической загрузки
  • кэширование переводов в памяти
  • минимизация namespaces
  • использование ICU-плагинов только при необходимости

При высокой нагрузке важна стратегия «один раз загрузить — много раз использовать», так как обращения к файловой системе становятся узким местом.

Структурирование ответа API

Типовая структура мультиязычного ответа:

{
  meta: {
    lang: "ru",
    timestamp: 1710000000
  },
  data: {},
  errors: [
    {
      code: "VALIDATION_ERROR",
      message: i18next.t("validation:REQUIRED_FIELD", { lng: "ru" })
    }
  ]
}

Такой формат позволяет отделить данные от представления и сохранить предсказуемость контракта.

Интернационализация системных сообщений

Помимо пользовательских данных API часто локализует:

  • сообщения логирования
  • audit trail
  • системные уведомления
  • фоновые задачи (cron/queue messages)

Это особенно важно в распределённых системах, где логи агрегируются из разных сервисов и должны быть читаемыми в едином формате.