Skip to content

Интернационализация (i18n)

Фреймворк поддерживает интернационализацию на основе стандартной библиотеки gettext. Переводы организованы по доменам, что позволяет разделять локализацию между модулями приложения, а язык каждого запроса определяется автоматически из строки запроса или заголовка Accept-Language.

Поиск сообщений проходит по цепочке fallback: запрошенный язык → язык по умолчанию (en) → сам идентификатор сообщения. Благодаря этому отсутствие перевода никогда не приводит к ошибке запроса — возвращается исходная строка.

Конфигурация

Переводы настраиваются в опциональной секции translations файла config.json:

json
{
    "translations": [
        {
            "domain": "identity",
            "path": "backend/identity/locale"
        },
        {
            "domain": "shop",
            "path": "backend/shop/locale"
        }
    ]
}

Опциональная секция

Секция translations не обязательна. Если приложению не нужна локализация, просто не указывайте её — карта переводов не создаётся, а функции перевода возвращают исходные строки.

domain строка

Имя домена переводов. Используется для обращения к конкретному набору переводов в коде. Каждая запись должна содержать непустой domain.

path строка

Путь до директории с файлами локализации (передаётся в bindtextdomain библиотеки gettext). Каждая запись должна содержать непустой path.

Язык по умолчанию зашит в код

Язык по умолчанию (fallback) жёстко задан как en и не может быть переопределён для отдельного домена в config.json. Все домены используют en в качестве резервного языка вне зависимости от конфигурации.

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

Файлы переводов должны располагаться по следующему пути:

<path>/<lang>/LC_MESSAGES/<domain>.mo

Например, для домена identity и языка ru:

backend/identity/locale/ru/LC_MESSAGES/identity.mo
backend/identity/locale/en/LC_MESSAGES/identity.mo

Загружаются только скомпилированные .mo-файлы — gettext не читает .po-исходники во время выполнения.

Создание файлов переводов

1. Создание PO-файла

Создайте файл identity.po для каждого языка:

po
# Russian translations for identity module
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: ru\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"

msgid "Welcome"
msgstr "Добро пожаловать"

msgid "Invalid credentials"
msgstr "Неверные учетные данные"

# Plural forms
msgid "error"
msgid_plural "errors"
msgstr[0] "ошибка"
msgstr[1] "ошибки"
msgstr[2] "ошибок"

Кодировка должна быть UTF-8

Фреймворк привязывает каждый домен к кодировке UTF-8, поэтому в PO-файле параметр Content-Type должен содержать charset=UTF-8. После этого строки возвращаются в UTF-8 независимо от исходной кодировки.

2. Компиляция в MO-файл

bash
msgfmt -o identity.mo identity.po

Перезагрузка после перекомпиляции

.mo-файлы загружаются gettext при старте / первом обращении и могут кэшироваться библиотекой C. После перекомпиляции переводов перезапустите сервер, чтобы новые строки гарантированно применились.

Использование в коде

Подключите заголовочный файл:

c
#include "translation.h"

Простой перевод

Функция tr возвращает перевод по идентификатору сообщения:

c
const char* message = tr(ctx, "identity", "Welcome");
// Результат: "Добро пожаловать" (для ru) или "Welcome" (для en)

Если перевод для запрошенного языка не найден, функция выполняет fallback к языку по умолчанию (en); если и его нет — возвращает сам msgid.

Внимание

Не освобождайте память, возвращаемую функцией tr. Строкой владеет рантайм gettext, и она может переиспользоваться между вызовами.

Перевод с подстановкой

Функция trf заменяет плейсхолдеры {key} на переданные значения:

c
// PO-файл: msgid "Hello, {name}!" msgstr "Привет, {name}!"

char* message = trf(ctx, "identity", "Hello, {name}!", "name", username, NULL);
// Результат: "Привет, Иван!"

free(message); // Необходимо освободить память

Список аргументов

Аргументы передаются парами "ключ", "значение" и завершаются NULL. Поддерживается до 32 пар "ключ-значение" — лишние пары сверх этого лимита игнорируются.

Множественные формы

Функция trn выбирает правильную форму в зависимости от числа:

c
const char* message = trn(ctx, "identity", "error", "errors", error_count);
// 1 -> "ошибка"
// 2 -> "ошибки"
// 5 -> "ошибок"

Если перевод для запрошенного языка не найден, функция выполняет fallback к языку по умолчанию (en); если и его нет — возвращает singular при n == 1, иначе plural.

Множественные формы с подстановкой

Функция trnf комбинирует множественные формы и подстановку:

c
char count_str[16];
snprintf(count_str, sizeof(count_str), "%d", count);

char* message = trnf(ctx, "identity", "{n} error found", "{n} errors found",
                     count, "n", count_str, NULL);
// 1 -> "1 ошибка найдена"
// 3 -> "3 ошибки найдено"

free(message);

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

Язык определяется автоматически для каждого запроса в следующем порядке приоритета:

  1. Query-параметр lang?lang=ru
  2. Заголовок Accept-Language — извлекается только основной код языка (например, ru-RU,ru;q=0.9,en-US;q=0.8ru)
  3. Язык по умолчаниюen

Поддерживаемые локали

Определённый код языка отображается на системную локаль перед поиском в gettext. Из коробки поддерживаются следующие коды:

КодЛокаль
enen_US.utf8
ruru_RU.utf8
dede_DE.utf8
frfr_FR.utf8
eses_ES.utf8
zhzh_CN.utf8
jaja_JP.utf8

Если запрошенный язык отсутствует в таблице, фреймворк использует локаль C.UTF-8. При этом сам файл <lang>/LC_MESSAGES/<domain>.mo всё равно должен существовать, чтобы перевод был отдан.

Пример

GET /api/users?lang=ru
Accept-Language: en-US,en;q=0.9

В этом случае будет использован русский язык (ru), так как query-параметр имеет наивысший приоритет.

API-справочник

tr

c
const char* tr(httpctx_t* ctx, const char* domain, const char* msgid);
ПараметрОписание
ctxHTTP-контекст для определения языка
domainДомен переводов
msgidИдентификатор сообщения
ВозвратПереведённая строка (не освобождать); fallback на язык по умолчанию, затем на msgid

trf

c
char* trf(httpctx_t* ctx, const char* domain, const char* msgid, ...);
ПараметрОписание
ctxHTTP-контекст для определения языка
domainДомен переводов
msgidИдентификатор сообщения с плейсхолдерами
...Пары "ключ", "значение", завершённые NULL (максимум 32 пары)
ВозвратПереведённая строка (вызывающий должен освободить), либо NULL при ошибке выделения памяти

trn

c
const char* trn(httpctx_t* ctx, const char* domain, const char* singular,
                const char* plural, unsigned long n);
ПараметрОписание
ctxHTTP-контекст для определения языка
domainДомен переводов
singularФорма единственного числа
pluralФорма множественного числа
nЧисло для выбора формы
ВозвратПереведённая строка (не освобождать); fallback на язык по умолчанию, затем на singular/plural

trnf

c
char* trnf(httpctx_t* ctx, const char* domain, const char* singular,
           const char* plural, unsigned long n, ...);
ПараметрОписание
ctxHTTP-контекст для определения языка
domainДомен переводов
singularФорма единственного числа с плейсхолдерами
pluralФорма множественного числа с плейсхолдерами
nЧисло для выбора формы
...Пары "ключ", "значение", завершённые NULL (максимум 32 пары)
ВозвратПереведённая строка (вызывающий должен освободить), либо NULL при ошибке выделения памяти

Пример использования

c
#include "http.h"
#include "translation.h"

void get_profile(httpctx_t* ctx) {
    // Простой перевод
    const char* title = tr(ctx, "identity", "Profile");

    // Перевод с подстановкой
    char* greeting = trf(ctx, "identity", "Welcome back, {name}!",
                         "name", user->name, NULL);

    // Множественное число
    char count_str[16];
    snprintf(count_str, sizeof(count_str), "%d", notification_count);
    char* notifications = trnf(ctx, "identity",
        "You have {n} new notification",
        "You have {n} new notifications",
        notification_count, "n", count_str, NULL);

    // ... формирование ответа ...

    free(greeting);
    free(notifications);
}

Выпущено под лицензией MIT.