Интернационализация (i18n)
Фреймворк поддерживает интернационализацию на основе стандартной библиотеки gettext. Переводы организованы по доменам, что позволяет разделять локализацию между модулями приложения, а язык каждого запроса определяется автоматически из строки запроса или заголовка Accept-Language.
Поиск сообщений проходит по цепочке fallback: запрошенный язык → язык по умолчанию (en) → сам идентификатор сообщения. Благодаря этому отсутствие перевода никогда не приводит к ошибке запроса — возвращается исходная строка.
Конфигурация
Переводы настраиваются в опциональной секции translations файла config.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 для каждого языка:
# 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-файл
msgfmt -o identity.mo identity.poПерезагрузка после перекомпиляции
.mo-файлы загружаются gettext при старте / первом обращении и могут кэшироваться библиотекой C. После перекомпиляции переводов перезапустите сервер, чтобы новые строки гарантированно применились.
Использование в коде
Подключите заголовочный файл:
#include "translation.h"Простой перевод
Функция tr возвращает перевод по идентификатору сообщения:
const char* message = tr(ctx, "identity", "Welcome");
// Результат: "Добро пожаловать" (для ru) или "Welcome" (для en)Если перевод для запрошенного языка не найден, функция выполняет fallback к языку по умолчанию (en); если и его нет — возвращает сам msgid.
Внимание
Не освобождайте память, возвращаемую функцией tr. Строкой владеет рантайм gettext, и она может переиспользоваться между вызовами.
Перевод с подстановкой
Функция trf заменяет плейсхолдеры {key} на переданные значения:
// PO-файл: msgid "Hello, {name}!" msgstr "Привет, {name}!"
char* message = trf(ctx, "identity", "Hello, {name}!", "name", username, NULL);
// Результат: "Привет, Иван!"
free(message); // Необходимо освободить памятьСписок аргументов
Аргументы передаются парами "ключ", "значение" и завершаются NULL. Поддерживается до 32 пар "ключ-значение" — лишние пары сверх этого лимита игнорируются.
Множественные формы
Функция trn выбирает правильную форму в зависимости от числа:
const char* message = trn(ctx, "identity", "error", "errors", error_count);
// 1 -> "ошибка"
// 2 -> "ошибки"
// 5 -> "ошибок"Если перевод для запрошенного языка не найден, функция выполняет fallback к языку по умолчанию (en); если и его нет — возвращает singular при n == 1, иначе plural.
Множественные формы с подстановкой
Функция trnf комбинирует множественные формы и подстановку:
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);Определение языка
Язык определяется автоматически для каждого запроса в следующем порядке приоритета:
- Query-параметр
lang—?lang=ru - Заголовок
Accept-Language— извлекается только основной код языка (например,ru-RU,ru;q=0.9,en-US;q=0.8→ru) - Язык по умолчанию —
en
Поддерживаемые локали
Определённый код языка отображается на системную локаль перед поиском в gettext. Из коробки поддерживаются следующие коды:
| Код | Локаль |
|---|---|
en | en_US.utf8 |
ru | ru_RU.utf8 |
de | de_DE.utf8 |
fr | fr_FR.utf8 |
es | es_ES.utf8 |
zh | zh_CN.utf8 |
ja | ja_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
const char* tr(httpctx_t* ctx, const char* domain, const char* msgid);| Параметр | Описание |
|---|---|
| ctx | HTTP-контекст для определения языка |
| domain | Домен переводов |
| msgid | Идентификатор сообщения |
| Возврат | Переведённая строка (не освобождать); fallback на язык по умолчанию, затем на msgid |
trf
char* trf(httpctx_t* ctx, const char* domain, const char* msgid, ...);| Параметр | Описание |
|---|---|
| ctx | HTTP-контекст для определения языка |
| domain | Домен переводов |
| msgid | Идентификатор сообщения с плейсхолдерами |
| ... | Пары "ключ", "значение", завершённые NULL (максимум 32 пары) |
| Возврат | Переведённая строка (вызывающий должен освободить), либо NULL при ошибке выделения памяти |
trn
const char* trn(httpctx_t* ctx, const char* domain, const char* singular,
const char* plural, unsigned long n);| Параметр | Описание |
|---|---|
| ctx | HTTP-контекст для определения языка |
| domain | Домен переводов |
| singular | Форма единственного числа |
| plural | Форма множественного числа |
| n | Число для выбора формы |
| Возврат | Переведённая строка (не освобождать); fallback на язык по умолчанию, затем на singular/plural |
trnf
char* trnf(httpctx_t* ctx, const char* domain, const char* singular,
const char* plural, unsigned long n, ...);| Параметр | Описание |
|---|---|
| ctx | HTTP-контекст для определения языка |
| domain | Домен переводов |
| singular | Форма единственного числа с плейсхолдерами |
| plural | Форма множественного числа с плейсхолдерами |
| n | Число для выбора формы |
| ... | Пары "ключ", "значение", завершённые NULL (максимум 32 пары) |
| Возврат | Переведённая строка (вызывающий должен освободить), либо NULL при ошибке выделения памяти |
Пример использования
#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);
}