Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Локализация веб-приложения: gettext, i18n-практика

Локализация и i18n в веб-приложении: gettext, форматы и подводные камни

Как подготовить приложение к переводам: gettext и каталоги, locale-переговоры, плюрализация и форматирование дат, организация строк и типичные ошибки i18n.

Прямой ответ

Локализация (l10n — переводы/форматы для конкретной локали) опирается на интернационализацию (i18n — готовность кода к ним), и 90% работы — правильная i18n до появления переводчиков: (1) все пользовательские строки через функцию перевода — _("Заказ создан") (gettext-семантика:msgid — исходная строка, каталоги .po/.mo — переводы; «строки в коде + перевод постфактум регулярками» — боль на порядок больше), (2) никакой склейки предложений из кусков — f"Найдено {n} файл(ов)" собирается по-разному в разных языках (порядок слов/род/плюрал — «Found {n} files» vs «{n} файла найдено» — склейка непереводима в принципе; целые фразы с плейсхолдерами — единственная форма), (3) плюрализация через ngettext (в русском три формы — 1 файл/2 файла/5 файлов — gettext plural forms это выражают; «if n==1 else ...» в коде — хардкод грамматики одного языка), (4) форматирование дат/чисел — библиотекой (Babel: format_date(dt, locale='de_DE') — 21.10.2026 против 10/21/2026 — даты и tz: хранение UTC, отображение в локали пользователя), (5) выбор локали — переговоры: профиль пользователя (явный выбор) > Accept-Language заголовок > дефолт (с кешированием выбора; для API — заголовок/параметр и локализованные сообщения об ошибках, не локализованные коды/поля — валидация). Подводные камни, которые дешевле знать заранее: длина строк (немецкий +30% к английскому — вёрстка/кнопки/BД-колонки с запасом), сортировка и поиск по локали (collation — «ä» сортируется по-разному в de и sv), unicode в БД (UTF-8 везде — колонки/соединения/клиент), и RTL как отдельный пласт (не «потом доделаем» — если рынок планируется — закладывать в вёрстку сразу).

Механика gettext: от строк до каталогов

Поток: код помечается _() / ngettext() → xgettext (или pybabel extract) извлекает msgid в шаблон .pot → переводчики заполняют .po на язык (msgfmt-компиляция в бинарный .mo) → рантайм gettext подставляет по активной локали (домены — отдельные каталоги на модуль/сервис; locale/ru/LC_MESSAGES/app.mo — стандартный layout). Практика: msgid — осмысленный английский или исходный язык продукта (пустые msgid-ключи «BTN_SAVE» — читаемость кода хуже, контекст потерян; gettext-подход «строка = ключ» проще для малых проектов, ключи — при частой смене исходных формулировок — выбор фиксировать в ADR); контексты (pgettext("menu", "File") vs pgettext("noun", "File")) — одно слово, разные переводы по смыслу — механизм есть, использовать; комментарии для переводчиков (# TRANSLATORS: это кнопка, не документ) — половина качества перевода — контекст, которого переводчик не видит; обновление каталогов — в CI/скрипте (msgmerge — новые строки подтягиваются, «нечёткие» (fuzzy — изменившийся msgid) помечаются на перепроверку); переводы в репозитории (git — версионирование/ревью переводов — структура проекта: locale/ рядом с кодом), Crowdin/Weblate — когда переводчиков много и они не в git.

Данные, форматы и UI-слой

Локализация данных (не только строк): даты/числа/валюты — Babel/formatting по локали пользователя (хранение — UTC/числа без формата — даты); пользовательский контент — не переводится машинно по умолчанию (переводы описаний товаров — отдельная фича с кешем и пометкой «машинный»); поиск — морфология и collation по языку (full-text — словари по языку; тривиальный LIKE по «ё»/«е» — отдельная русская классика — нормализация ввода); URL/SEO — локализованные пути (/de/preise против /prices) с hreflang — отдельная SEO-задача (для контент-сайтов критична — наш блог — живой пример: slug-и транслитом + punycode-домен). UI-практика: строки с запасом длины (CSS не фиксированной ширины; БД-колонки под переводы — varchar с запасом), иконки/цвета/жесты — культурная проверка (галерея примеров ошибок i18n — классическая литература), тесты локализации: pseudo-localization (автозамена строк на «[Ţéšţ]»-удлинённые — ловит непомеченные строки и обрезания до реальных переводов), скриншот-тесты ключевых экранов на 2–3 локалях (CI — pytest-инфраструктура), и тест «весь UI-текст проходит через _()» (линтер/AST-скан на русские/английские литералы вне gettext — регресс «новой непереведённой строки»). Процесс: переводы — не «строки в таблице Excel» (контекст/плюралы/длина теряются — .po/платформы переводов с скриншотами), релизный цикл — переводы догоняют код на итерацию (новые строки в релизе N переводятся к N+1 — «непереведённый fallback на исходный язык» — приемлемое поведение, заложенное в gettext природой).

Практический сценарий: второй язык продукта за две итерации

Исходник: SaaS на русском, запрос — английский (выход на EU-рынок). Итерация 1 (i18n): инвентаризация строк (AST-скан: 340 литералов в UI-слое, 90 — в сообщениях об ошибках — все в _(); найдено 14 мест склейки фраз («Статус: » + status — переписаны на целые фразы с плейсхолдерами) и 6 «if n == 1» плюралов (→ ngettext); Babel подключён на даты/числа (формат «01.02.2026» в UI — format_date по локали, в API — ISO всегда); переговоры локали: профиль > Accept-Language > ru-дефолт; pseudo-localization-прогон — 23 непомеченные строки (заголовки колонок, тосты) — добиты; вёрстка: 9 кнопок с обрезанием при +30% длины — CSS-фиксы. Итерация 2 (l10n): .pot → en-каталог (переводчик с контекст-комментариями — «Save» как глагол/существительное различены pgettext-ом); ревью переводов в git (PR с .po — разработчик + носитель); скриншот-тесты 12 ключевых экранов en/ru в CI; релиз: англоязычный пользователь видит полный en-UI, ошибка валидации — «Invalid email» (сообщения локализованы, коды — нет); SEO-страницы — hreflang + локализованные slug-и (отдельная задача маркетинга — учтена в плане). Итог: 430 строк в двух каталогах, ноль склеек фраз, плюралы корректны, добавление третьего языка — «только каталог» (проверено на de — 3 дня работы переводчика, ноль строк кода).

FAQ

gettext против i18n-библиотек с JSON-ключами (i18next-стиль)? Для серверного Python gettext — стандарт (инструменты/плюралы/контексты/экосистема переводческих платформ); JSON-ключи популярны во фронтенд-экосистемах (react-i18next) — там свой выбор; в фуллстеке — согласовать: один источник строк (backend-каталоги с экспортом или отдельные каталоги на слой с синхронизацией ключей); «две системы переводов живут своей жизнью» — рассинхрон терминов гарантирован.

Как переводить сообщения об ошибках в API? Код ошибки стабилен и не локализуется (error: "validation_failed" — клиентская логика по коду), человекочитаемое message — локализуется по Accept-Language/параметру; документация кодов — вне перевода (контракт — OpenAPI); логи — всегда на одном языке (английском — инженеры читают; локализованные логи — ад диагностики).

Что делать с уже существующим «хардкодным» контентом в БД? Пользовательский контент — не i18n-задача (переводы контента — продуктовая фича: поля переводов/отдельные таблицы локалей/внешние переводческие сервисы с кешем); системный контент (шаблоны писем, тексты статусов в БД) — вынести в gettext/каталоги (тексты в БД «чтобы менять без деплоя» — соблазн, который ломает плюралы/контексты/версионирование — лучше деплой с каталогом).

Стоит ли делать машинный перевод строк как fallback? Как явный, помеченный fallback для длинного контента — да (перевод описания «машинный, может быть неточен» лучше пустоты); для UI-строк — нет (кривой машинный «Сохранить→Save→Sparen» в интерфейсе бьёт по доверию сильнее отсутствующего перевода; UI-каталог мал и переводится быстро — fallback на исходный язык честнее).

Источники: документация Python gettext, GNU gettext manual, Babel — ссылки в шапке.