Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 132 · От первой надписи к локализации приложения

Gettext: переводы сайта, каталоги и множественные формы

Все функции Gettext: от первой переведённой кнопки до каталогов PO/MO, доменов модулей, русских форм количества и диагностики окружения.

Зачем локализация магазину: один код, разные надписи

На русском сайте кнопка называется «Оформление заказа», на английском — Checkout. Копировать весь шаблон ради другого языка неудобно: исправление корзины пришлось бы повторять в каждой копии. Gettext хранит надписи отдельно от логики. Код называет исходное сообщение, а каталог переводов возвращает текст нужного языка. Это поиск заранее подготовленного перевода, а не автоматический переводчик и не внешний AI-сервис.

Одна надпись проходит четыре шага
  1. PHP вызывает gettext("Cart")
  2. Выбраны язык ru и набор learning
  3. MO содержит Cart → Корзина
  4. В переменную вернулась строка Корзина

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Примеры с require "gettext-lab.php" используют приложенный учебный каталог: Cart → «Корзина», Checkout → «Оформление заказа», три русские формы слова «товар». Он входит в скачиваемый пример. Нужны расширение gettext и доступная русская locale. Здесь каталог разворачивается из включённой MO-заготовки; в проекте его собирают из PO командой msgfmt, как показано в начале главы. Запуск отдельным CLI-процессом изолирует изменения locale и домена.

Самый маленький работающий вызовСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$caption = gettext('Cart');
echo $caption, "\n";
Ожидаемый результат
Корзина

Техническую подготовку каталога выполняет приложенный gettext-lab.php. Ниже разбираем её отдельно. Сама новая операция — вызов gettext с одним ключом и сохранение возвращённой строки.

Не переводите технические имена полей, ключи JSON, SQL-колонки и идентификаторы маршрутов только ради языка интерфейса: получатель данных ждёт согласованный формат. Gettext удобен для кнопок, подсказок, писем и сообщений приложения. Названия товаров, которые сотрудники редактируют в базе, часто требуют отдельной модели переводов в БД. Форматирование дат, чисел и валют — отдельная задача; пример отдельного направления — работа с датами. Каталог отвечает именно за текст.

Установка расширения, системная locale и проверка окружения

Нужны две разные части: PHP-расширение gettext для поиска переводов во время работы и инструменты GNU gettext для подготовки каталогов. При сборке PHP используется --with-gettext[=DIR], стандартный каталог поиска библиотеки — /usr/local. В готовом дистрибутиве используйте совместимый пакет расширения; Windows DLL должна совпадать с PHP по версии, архитектуре и TS/NTS. Случайный DLL от другой сборки не является установкой.

Проверить наличие API в том PHP, которым запускается приложениеСамостоятельный пример
<?php
declare(strict_types=1);

var_dump(extension_loaded('gettext'));
var_dump(function_exists('gettext'));
var_dump(function_exists('ngettext'));
Ожидаемый результат
bool(true)
bool(true)
bool(true)

Это ожидается при включённом расширении. CLI и PHP веб-сервера могут читать разные ini. Проверка только в консоли не подтверждает конфигурацию PHP-FPM/Apache.

Locale — системный набор языковых и региональных настроек. setlocale возвращает false, когда указанное имя недоступно. На Linux ru_RU.UTF-8 должна быть установлена/сгенерирована средствами ОС; на Windows название CRT locale может выглядеть Russian_Russia.65001. Наличие файла ru.mo само по себе не устанавливает системную locale. Не выводите посетителям полный phpinfo: достаточно внутренней диагностики расширения, выбранной locale и ожидаемого тестового перевода.

НастройкаЗа что отвечает
setlocale(LC_MESSAGES, ...)Язык сообщений в поддерживающей эту категорию системе. На Windows LC_MESSAGES может отсутствовать.
LANGUAGE=ru:enGNU-список приоритетов переводов, если библиотека его поддерживает. В locale C/POSIX может игнорироваться.
LANG / LC_MESSAGES / LC_ALLПеременные окружения выбора locale; LC_ALL имеет приоритет над отдельными категориями, LANG служит общим fallback.
bindtextdomainКорень директорий каталогов.
textdomainНабор переводов по умолчанию.
bind_textdomain_codesetКодировка возвращаемой строки.

В учебном Windows-процессе отдельно задаются CRT locale и GNU окружение LC_ALL=ru_RU.UTF-8, LANGUAGE=ru: одна только успешная setlocale не обеспечила загрузку каталога. Это проверенная особенность данного окружения, а не рецепт бездумно менять LC_ALL во всём веб-сервисе. LC_ALL затрагивает все категории, включая числа и даты. Для production явно задайте языковую конфигурацию процесса, учитывайте SAPI и библиотеку; для многопоточного сервера глобальные переключения на каждого посетителя могут быть непригодны.

От PO к MO: что лежит на диске и кто готовит перевод

PO — редактируемый текст: msgid содержит исходный ключ, msgstr — перевод. POT — шаблон без готовых переводов. MO — скомпилированный каталог, который читает libintl. Если сохранить только PO, PHP не обязан его подхватить: сначала выполняется сборка MO. Имя домена learning соответствует learning.mo.

Минимальный PO с обычной и множественной формойФрагмент или команда
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<12 || n%100>14) ? 1 : 2);\n"

msgid "Cart"
msgstr "Корзина"

#, php-format
msgid "%d item"
msgid_plural "%d items"
msgstr[0] "%d товар"
msgstr[1] "%d товара"
msgstr[2] "%d товаров"

Это содержимое locale/ru/LC_MESSAGES/learning.po, не PHP-код. Формула относится именно к русским целым количествам. Переводчик заполняет все формы; флаг php-format помогает инструменту проверить placeholders.

Собрать каталог из PO инструментом GNUФрагмент или команда
msgfmt --check --check-format --output-file=locale/ru/LC_MESSAGES/learning.mo locale/ru/LC_MESSAGES/learning.po

Команду выполняют из корня проекта после создания директорий, при установленном GNU gettext. Успех создаёт learning.mo; ошибка проверки требует исправить PO. Это команда сборки, здесь её наличие в системе не предполагается.

Путь от корня проекта до переводаФрагмент или команда
project/
  index.php
  locale/
    ru/
      LC_MESSAGES/
        learning.po
        learning.mo
        checkout.mo
    en/
      LC_MESSAGES/
        learning.mo

В bindtextdomain передаётся project/locale. ru — каталог языка, LC_MESSAGES — категория, learning — домен. Другой каталог языка содержит другой перевод того же набора.

Список исходных сообщений получают xgettext из PHP-файлов с литералами. Затем msgmerge обновляет PO по новому POT, переводчик проверяет новые и fuzzy-записи, msgfmt собирает MO. Fuzzy означает, что соответствие требует внимания; такая запись обычно не попадает в итоговый перевод без явного специального режима. Ключи, собранные из переменных, извлечь надёжно нельзя. Не копируйте текст из HTML вместе с динамическими данными: постоянный шаблон и подстановка дают устойчивые ключи.

Извлечь сообщения и формы в шаблонФрагмент или команда
xgettext --language=PHP --from-code=UTF-8 --keyword=gettext:1 --keyword=_:1 --keyword=ngettext:1,2 --keyword=dgettext:2 --keyword=dngettext:2,3 --keyword=dcgettext:2 --keyword=dcngettext:2,3 --output=messages.pot src/cart.php src/checkout.php

Это пример команды для двух существующих PHP-файлов. Номера после имён указывают позиции строковых аргументов; они различаются у gettext и dgettext. Затем POT передают инструменту перевода, а не читают напрямую из PHP.

Количество, шаблон и результат — три разных значения

Русский каталог хранит три формы, хотя в вызове ngettext передаются две исходные английские. По count выбирается индекс перевода согласно Plural-Forms. Число 22 не становится текстом автоматически: выбранная строка %d товара передаётся в sprintf вместе с 22.

Подпись количества в корзине
  1. count = 22
  2. ngettext выбирает форму %d товара
  3. sprintf подставляет 22
  4. Покупатель видит 22 товара
Сформировать итоговую подпись из массива товаровСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$products = ['Фонарь', 'Кабель'];
$count = count($products);
$caption = sprintf(ngettext('%d item', '%d items', $count), $count);
echo $caption, "\n";
Ожидаемый результат
2 товара

count считает элементы массива, ngettext выбирает форму, sprintf собирает строку, echo выводит её. Каждый этап имеет собственный результат и может проверяться отдельно.

Если покупатель добавил пять штук одного товара, count($products) может считать позиции, а не суммарные единицы. Сначала определите бизнес-смысл количества, затем суммируйте нужные поля и только потом переводите подпись. Локализация не исправляет неверное исходное число. Для дробей, рода и сложных правил пригодятся специальные средства форматирования сообщений; не пытайтесь решить всё одной формулой целого count.

Все функции Gettext по задачам

Что нужноФункции
Перевести строку из текущего набораgettext · _
Перевести строку из заданного набораdgettext
Выбрать форму по количествуngettext · dngettext
Явно управлять категорией localedcgettext · dcngettext
Подключить и выбрать каталогbindtextdomain · textdomain
Выбрать кодировку результатаbind_textdomain_codeset

Ниже каждая функция имеет отдельную карточку: синтаксис, параметры, возврат, версии, ограничения, простой вызов и прикладное сочетание. У Gettext нет собственных пользовательских классов или PHP-констант расширения; LC_* относятся к locale и платформе, а не к отдельной enum Gettext.

Перевод не появился: проверка всей цепочки

НаблюдениеЧто проверять
Вернулся исходный английский текстТочный msgid, выбранные locale и домен, путь и имя MO; fallback часто не создаёт ошибки.
PO есть, перевод не меняетсяСобран ли MO, нет ли fuzzy, попали ли все формы; обслуживающий процесс может держать старый каталог в памяти.
В CLI работает, на сайте нетИной SAPI, ini, пользователь процесса, окружение, рабочая директория, права чтения и locale.
Часть сообщений переведенаТочные пробелы/регистр/пунктуация, разные домены, неполные или fuzzy-записи.
Вместо букв странные символыContent-Type в PO, реальные байты файла, codeset результата, charset HTML/JSON.
Неверно «21 товаров»Plural-Forms и порядок msgstr; передаётся ли настоящий count вместо количества строк запроса.
Два посетителя получают чужие языкиГлобальные locale/domain при общем процессе или потоках; нужна подходящая архитектура локализации.

При обновлении каталогов учитывайте кеш libintl: для долгоживущих процессов может требоваться управляемый reload работников после развёртывания. Не советуйте посетителю перезагружать страницу бесконечно и не меняйте домены случайными значениями при каждом запросе. Проверять развёрнутый перевод лучше заранее выбранной контрольной строкой, а не выводом путей и окружения на публичную страницу.

В обсуждениях встречаются попытки исправить всё очередной строкой setlocale. Причина может находиться в другом звене: не собран MO, не существует ожидаемой директории языка либо PHP веб-сервера использует другой набор настроек. Поэтому диагностический маршрут проходит от окружения к файлу и только потом к ключу сообщения.

Как встроить в сайт, письмо и API без смешения обязанностей

В начале приложения выбирают язык из фиксированного разрешённого списка, инициализируют каталог и кодировку. Компоненты используют явные домены; пользовательские данные подставляют в шаблоны после выбора перевода. Для HTML выполняют htmlspecialchars, для JSON — json_encode. В логах и API удобно сохранять стабильный код ошибки отдельно от локализованного текста, чтобы интеграция не зависела от языка человека.

Стабильный код и переводимая подпись в одном ответеСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$response = ['code' => 'checkout_ready', 'label' => dgettext('checkout', 'Checkout')];
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
{"code":"checkout_ready","label":"Оформление заказа"}

Интеграция читает code; интерфейс может показать label. На реальном HTTP-ответе также выставляют application/json и нужный статус. Код примера — локальное формирование тела, не отправка HTTP.

Домен не является защитной границей. Перевод с HTML нельзя безоговорочно доверять только потому, что он в MO; политика разметки и доверия к переводчикам определяется проектом. Функции не шифруют строку, не параметризуют SQL и не нормализуют произвольные внешние данные. Подготовка языка не должна выполняться перед каждой надписью: системные переключения и чтение больших каталогов имеют стоимость. Измеряйте реальные запросы, используйте устойчивые ключи и одинаковую конфигурацию работников.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

gettextНайти перевод одного сообщения в текущем домене.

PHP 4+ · gettext

gettext(string $message): string

Найти перевод одного сообщения в текущем домене.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
messageСтрока-ключ, обычно литерал в исходном языке.
РезультатПереведённая строка или исходное message при отсутствии перевода.
ДоменТекущий набор задаётся textdomain; путь к нему — bindtextdomain.

Сначала отдельный вызов: берём английский ключ Cart и получаем его перевод из активного каталога. Функция сама не выводит текст: echo ниже выполняет отдельное действие.

Примеры с require "gettext-lab.php" используют приложенный учебный каталог: Cart → «Корзина», Checkout → «Оформление заказа», три русские формы слова «товар». Он входит в скачиваемый пример. Нужны расширение gettext и доступная русская locale. Здесь каталог разворачивается из включённой MO-заготовки; в проекте его собирают из PO командой msgfmt, как показано в начале главы. Запуск отдельным CLI-процессом изолирует изменения locale и домена.

Получить строку и вывести еёСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$title = gettext('Cart');
echo $title, "\n";
echo gettext('There is no translation yet'), "\n";
Ожидаемый результат
Корзина
There is no translation yet

Первый ключ найден, второй отсутствует и вернулся без изменения. Любая лишняя точка, пробел или другой регистр образует другой msgid.

Когда в сообщении меняется имя покупателя, в каталоге оставляют постоянный шаблон. Gettext находит его, sprintf вставляет данные, htmlspecialchars экранирует готовую строку для HTML. При каждом этапе остаётся своя ответственность.

Приветствие покупателю без динамических ключейСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$customerName = 'Ира <VIP>';
$message = sprintf(gettext('Hello, %s!'), $customerName);
echo htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), "\n";
Ожидаемый результат
Здравствуйте, Ира &lt;VIP&gt;!

%s — место для строки, которое обрабатывает sprintf. Конкатенация gettext("Hello, " . $name) создала бы отдельный ключ на каждого человека. Перевод не заменяет HTML-экранирование.

Текст переводят перед выводом; числа, даты и валюты форматируют отдельными инструментами. Для перестановки нескольких аргументов в переводе полезны позиционные шаблоны sprintf, например %2$s: %1$d. Инструмент xgettext извлекает строковые литералы из исходников, но не знает все значения произвольных выражений. Пустой msgid зарезервирован каталогом под метаданные: не используйте его как перевод пустой кнопки. В PHP 8.5.11 длина message ограничена 4096 байтами; превышение вызывает ValueError. Большой текст страницы лучше разбить на осмысленные сообщения.

Рядом по задаче: _; dgettext; ngettext; textdomain.

_Короткое имя gettext для перевода надписей.

PHP 4+ · gettext

_(string $message): string

Короткое имя gettext для перевода надписей.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
messageТот же строковый ключ, что у gettext.
РезультатТот же перевод или исходный текст.
ДоступностьРасширение gettext; алиас не предоставляется самим синтаксисом PHP.

Сокращение _ является алиасом gettext с теми же аргументом, результатом и ограничениями. Это реальная функция расширения, а не специальный оператор PHP. Её короткое имя удобно при большом количестве небольших надписей в шаблоне.

Примеры с require "gettext-lab.php" используют приложенный учебный каталог: Cart → «Корзина», Checkout → «Оформление заказа», три русские формы слова «товар». Он входит в скачиваемый пример. Нужны расширение gettext и доступная русская locale. Здесь каталог разворачивается из включённой MO-заготовки; в проекте его собирают из PO командой msgfmt, как показано в начале главы. Запуск отдельным CLI-процессом изолирует изменения locale и домена.

Сопоставить полное и короткое именаСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
echo _('Checkout'), "\n";
var_dump(_('Cart') === gettext('Cart'));
Ожидаемый результат
Оформление заказа
bool(true)

Оба вызова обращаются к одному каталогу. Замена написания функции сама по себе не меняет язык.

Собрать переведённые подписи формыСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$labels = ['cart' => _('Cart'), 'submit' => _('Checkout')];
echo $labels['cart'], ' / ', $labels['submit'], "\n";
Ожидаемый результат
Корзина / Оформление заказа

Результаты сохраняются в обычный массив и передаются шаблону. Литералы видны извлечению сообщений. Имена полей формы остаются стабильными техническими идентификаторами, меняются только подписи.

В чужом проекте сначала выясните, какая система локализации там принята. Функции CMS вроде WordPress __() имеют собственный контракт и домены; это не другой вариант встроенного _ без дополнительных условий. Нельзя заново объявить функцию _, если расширение уже объявило её. Проверка function_exists полезна при диагностике, но молчаливый fallback не должен скрывать отсутствующий на production перевод.

Рядом по задаче: gettext; ngettext; dgettext.

dgettextПеревести сообщение из явно указанного домена.

PHP 4+ · gettext

dgettext(string $domain, string $message): string

Перевести сообщение из явно указанного домена.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
domainНепустое имя набора, заранее связанного с директорией.
messageКлюч для одного сообщения.
РезультатПеревод либо исходный текст. Текущий textdomain остаётся прежним.

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

Примеры с require "gettext-lab.php" используют приложенный учебный каталог: Cart → «Корзина», Checkout → «Оформление заказа», три русские формы слова «товар». Он входит в скачиваемый пример. Нужны расширение gettext и доступная русская locale. Здесь каталог разворачивается из включённой MO-заготовки; в проекте его собирают из PO командой msgfmt, как показано в начале главы. Запуск отдельным CLI-процессом изолирует изменения locale и домена.

Прочитать другой перевод того же ключаСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
echo gettext('Cart'), "\n";
echo dgettext('checkout', 'Cart'), "\n";
echo gettext('Cart'), "\n";
Ожидаемый результат
Корзина
Корзина оплаты
Корзина

Каталог checkout имеет собственный перевод Cart. Последняя строка снова использует learning: переключения текущего домена не было.

Собрать подписи двух частей сайтаСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$mainTitle = dgettext('learning', 'Cart');
$paymentTitle = dgettext('checkout', 'Checkout');
echo $mainTitle, ' → ', $paymentTitle, "\n";
Ожидаемый результат
Корзина → Оформление заказа

В коде видна принадлежность сообщения конкретному набору. Реальный путь каждого домена привязывают один раз при настройке приложения, а не при каждой строке.

Пустое имя домена начиная с PHP 8.4 вызывает ValueError. В реализации PHP 8.5.11 domain ограничен 1024 байтами, message — 4096. Не берите имя каталога напрямую из параметра запроса: приложение знает допустимые модули. Отсутствующий каталог может просто дать исходный текст; отдельно проверяйте развёртывание файлов и язык.

Рядом по задаче: gettext; dngettext; bindtextdomain; dcgettext.

ngettextВыбрать языковую форму сообщения по количеству.

PHP 4+ · gettext

ngettext(string $singular, string $plural, int $count): string

Выбрать языковую форму сообщения по количеству.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
singular / pluralДва исходных сообщения, например %d item и %d items.
countКоличество для выбора формы, не автоматическая подстановка.
РезультатВыбранный перевод-шаблон или исходная форма при отсутствии перевода.

Для подписи «1 товар / 2 товара / 5 товаров» недостаточно условия count===1. Язык перевода определяет число форм и правила их выбора. ngettext получает исходные единственное и множественное сообщения и количество; из MO выбирается нужная форма. Само число в строку функция не подставляет.

Примеры с require "gettext-lab.php" используют приложенный учебный каталог: Cart → «Корзина», Checkout → «Оформление заказа», три русские формы слова «товар». Он входит в скачиваемый пример. Нужны расширение gettext и доступная русская locale. Здесь каталог разворачивается из включённой MO-заготовки; в проекте его собирают из PO командой msgfmt, как показано в начале главы. Запуск отдельным CLI-процессом изолирует изменения locale и домена.

Выбрать форму, а затем подставить числоСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$count = 2;
$pattern = ngettext('%d item', '%d items', $count);
echo $pattern, "\n";
echo sprintf($pattern, $count), "\n";
Ожидаемый результат
%d товара
2 товара

Первый результат ещё содержит %d. Отдельный sprintf завершает фразу. Количество участвует и в выборе формы, и в подстановке, но эти действия не объединены.

Проверить подписи корзины для нескольких количествСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
foreach ([0, 1, 2, 5, 11, 21, 22] as $count) {
    echo sprintf(ngettext('%d item', '%d items', $count), $count), "\n";
}
Ожидаемый результат
0 товаров
1 товар
2 товара
5 товаров
11 товаров
21 товар
22 товара

Три формы и формула Plural-Forms хранятся в каталоге. PHP-код не содержит отдельного switch для русского языка и не меняется при добавлении другого корректного перевода.

Понять результат без каталога переводаСамостоятельный пример
<?php
declare(strict_types=1);

setlocale(LC_ALL, 'C');
putenv('LC_ALL=C');
putenv('LANGUAGE=');
textdomain('lesson-no-catalog');
foreach ([0, 1, 2] as $count) {
    echo sprintf(ngettext('%d item', '%d items', $count), $count), "\n";
}
Ожидаемый результат
0 items
1 item
2 items

Это контролируемый исходный язык без каталога, а не проверка русского перевода. Fallback выбирает singular для 1, plural для остальных показанных количеств.

count — int; для количества товаров используйте проверенное неотрицательное целое. Дробные величины, например «1,5 метра», требуют отдельной языковой модели, а не приведения к int. Каждая из двух исходных строк ограничена 4096 байтами в PHP 8.5.11. Не переводите только слово отдельно от предложения, если грамматика требует менять другие части фразы. Каталог должен содержать все формы и корректный Plural-Forms; переводчик отвечает за грамматику, программа — за корректные данные.

Рядом по задаче: gettext; dngettext; dcngettext.

dngettextВыбрать множественную форму в заданном домене.

PHP 4+ · gettext

dngettext(string $domain, string $singular, string $plural, int $count): string

Выбрать множественную форму в заданном домене.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
domainНепустое имя набора переводов.
singular / pluralИсходные сообщения; правила выбора перевода находятся в каталоге.
countЦелое количество для грамматики.
РезультатСтрока-шаблон; sprintf подставляет значения отдельно.

dngettext совмещает две потребности: выбрать каталог модуля и грамматическую форму по количеству. Текущий textdomain менять не нужно. В интерфейсе магазина и платёжном модуле могут быть свои переводы, но логика вызова остаётся одинаковой.

Примеры с require "gettext-lab.php" используют приложенный учебный каталог: Cart → «Корзина», Checkout → «Оформление заказа», три русские формы слова «товар». Он входит в скачиваемый пример. Нужны расширение gettext и доступная русская locale. Здесь каталог разворачивается из включённой MO-заготовки; в проекте его собирают из PO командой msgfmt, как показано в начале главы. Запуск отдельным CLI-процессом изолирует изменения locale и домена.

Явно указать набор и количествоСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$count = 5;
echo sprintf(dngettext('checkout', '%d item', '%d items', $count), $count), "\n";
echo textdomain(), "\n";
Ожидаемый результат
5 товаров
learning

Строка взята из checkout; текущий набор по-прежнему learning. Оба учебных каталога имеют одинаковую грамматику числительных.

Подготовить подписи двух корзинСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$cartCounts = ['current' => 1, 'saved' => 22];
foreach ($cartCounts as $cart => $count) {
    $label = sprintf(dngettext('learning', '%d item', '%d items', $count), $count);
    echo $cart, ': ', $label, "\n";
}
Ожидаемый результат
current: 1 товар
saved: 22 товара

Технические ключи current/saved не переводятся. Пользовательская подпись выбирается отдельно; затем её можно безопасно передать в HTML-шаблон.

Ограничения домена такие же, как у dgettext, а формы и количество — как у ngettext. С PHP 8.4 пустой domain вызывает ValueError. Если библиотека переключает глобальный домен и забывает вернуть его после ошибки, ломаются чужие надписи; явный параметр domain устраняет именно эту зависимость, но не глобальное состояние locale.

Рядом по задаче: ngettext; dgettext; dcngettext.

textdomainЗадать или узнать текущий набор переводов.

PHP 4+ · gettext

textdomain(?string $domain = null): string

Задать или узнать текущий набор переводов.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
domain = nullЧитать текущее имя. null разрешён с PHP 8.0.3, опускать аргумент можно с 8.4.
domain = имяВыбрать непустой набор. Путь к файлам задаётся отдельно.
РезультатСтрока — текущее имя домена после операции.

textdomain задаёт набор, в котором gettext и ngettext будут искать сообщения без явного domain. Имя learning означает файл learning.mo внутри директории выбранного языка. Настройка возвращает текущее имя, а не содержимое каталога и не признак наличия перевода.

Примеры с require "gettext-lab.php" используют приложенный учебный каталог: Cart → «Корзина», Checkout → «Оформление заказа», три русские формы слова «товар». Он входит в скачиваемый пример. Нужны расширение gettext и доступная русская locale. Здесь каталог разворачивается из включённой MO-заготовки; в проекте его собирают из PO командой msgfmt, как показано в начале главы. Запуск отдельным CLI-процессом изолирует изменения locale и домена.

Переключить текущий набор и прочитать его имяСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
echo textdomain(), "\n";
echo textdomain('checkout'), "\n";
echo gettext('Cart'), "\n";
Ожидаемый результат
learning
checkout
Корзина оплаты

Вызов без аргумента читает имя начиная с PHP 8.4. В PHP 8.0.3–8.3 для чтения передают null явно.

Вернуть прежний набор даже после ошибкиСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$previous = textdomain();
try {
    textdomain('checkout');
    echo gettext('Cart'), "\n";
    throw new RuntimeException('Учебная ошибка');
} catch (RuntimeException $error) {
    echo "Обработка ошибки\n";
} finally {
    textdomain($previous);
}
echo gettext('Cart'), "\n";
Ожидаемый результат
Корзина оплаты
Обработка ошибки
Корзина

finally выполнится и после исключения. Для отдельных надписей компонента обычно проще dgettext с явным domain: тогда временное переключение не требуется.

Не принимать пустой доменСамостоятельный пример
<?php
declare(strict_types=1);

try { textdomain(''); }
catch (ValueError $error) { echo "Нужно непустое имя домена\n"; }
Ожидаемый результат
Нужно непустое имя домена

В PHP 8.4+ пустой domain запрещён. Этот пример рассчитан на современную версию; в старом коде можно встретить другое поведение.

PHP 8.5.11 также отвергает имя "0" и имена длиннее 1024 байт. Настройка относится к процессу, а не к отдельному потоку или объекту. В многопоточном SAPI, coroutine-сервисе либо долгоживущем worker нельзя считать её личным полем текущего запроса. Инициализация один раз на обычный независимый процесс и явные домены уменьшают скрытые зависимости. Название установленного домена само по себе не доказывает, что MO загружен.

Рядом по задаче: gettext; dgettext; bindtextdomain.

bindtextdomainСвязать домен с директорией переводов или узнать путь.

PHP 4+ · gettext

bindtextdomain(string $domain, ?string $directory = null): string|false

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

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
domainИмя файла без .mo, например learning.
directoryКорень каталогов языков; null читает настройку; пустая строка означает cwd.
РезультатПолный путь установленной привязки либо false.
Версииnull с 8.0.3; необязательный второй аргумент с 8.4.

bindtextdomain отвечает на вопрос «где лежат наборы переводов?». Ей передают корень locale, а не путь прямо к MO и не каталог LC_MESSAGES. Затем libintl сама выбирает язык и ищет файл. Директория должна существовать и быть доступна серверному процессу.

Из привязки к файлу
  1. domain = learning
  2. directory = /srv/shop/locale
  3. выбран язык ru
  4. locale/ru/LC_MESSAGES/learning.mo
Привязать существующую директорию и прочитать настройкуСамостоятельный пример
<?php
declare(strict_types=1);

$directory = __DIR__ . '/locale';
if (!mkdir($directory, 0700)) throw new RuntimeException('Не создана директория');
$bound = bindtextdomain('learning', $directory);
var_dump($bound !== false);
var_dump(bindtextdomain('learning') === $bound);
Ожидаемый результат
bool(true)
bool(true)

Оба результата true подтверждают настройку пути. В директории ещё нет MO: это намеренно отдельный шаг, который не равен проверке перевода.

Проверить путь вместе с настоящим ключомСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
$root = bindtextdomain('learning');
var_dump($root !== false && is_dir($root));
echo dgettext('learning', 'Cart'), "\n";
Ожидаемый результат
bool(true)
Корзина

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

Распознать отсутствующий корень каталогаСамостоятельный пример
<?php
declare(strict_types=1);

$path = __DIR__ . '/not-created-locale';
var_dump(bindtextdomain('learning', $path));
Ожидаемый результат
bool(false)

Функция не создаёт директорию. При false приложение должно исправить развёртывание или явно выбрать fallback, а не продолжать с предположением, что перевод установлен.

Второй аргумент null читает существующую привязку с PHP 8.0.3, а с 8.4 его можно опустить. Пустая строка задаёт текущую рабочую директорию; для веб-приложения она может отличаться от директории скрипта, поэтому надёжнее абсолютный путь от __DIR__. В PHP 8.5.11 строка "0" тоже попадает в ветку текущей директории — не используйте её как путь. Возвращается полный путь или false при неудаче. Непустое имя домена длиной до 1024 байт обязательно в современной версии. Привязка хранится для процесса, не отдельно для потоков; несколько компонентов должны иметь разные имена наборов.

Рядом по задаче: textdomain; bind_textdomain_codeset; dgettext.

bind_textdomain_codesetЗадать кодировку возвращаемых переводов для домена.

PHP 4+ · gettext

bind_textdomain_codeset(string $domain, ?string $codeset = null): string|false

Задать кодировку возвращаемых переводов для домена.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
domainНепустой набор переводов.
codesetНапример UTF-8; null читает ранее заданную настройку.
РезультатИмя кодировки либо false.
Версииnull с 8.0.3; опускать codeset можно с 8.4.

Кодировка файла каталога и кодировка возвращаемой PHP строки — разные настройки. bind_textdomain_codeset просит библиотеку выдавать переводы заданного домена в указанной кодировке. Для современного HTML/JSON обычно удобно единообразно использовать UTF-8 на всех этапах.

Примеры с require "gettext-lab.php" используют приложенный учебный каталог: Cart → «Корзина», Checkout → «Оформление заказа», три русские формы слова «товар». Он входит в скачиваемый пример. Нужны расширение gettext и доступная русская locale. Здесь каталог разворачивается из включённой MO-заготовки; в проекте его собирают из PO командой msgfmt, как показано в начале главы. Запуск отдельным CLI-процессом изолирует изменения locale и домена.

Задать и прочитать кодировку результатаСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
echo bind_textdomain_codeset('learning', 'UTF-8'), "\n";
echo bind_textdomain_codeset('learning'), "\n";
echo gettext('Cart'), "\n";
Ожидаемый результат
UTF-8
UTF-8
Корзина

Чтение без второго аргумента возможно с PHP 8.4; для 8.0.3–8.3 передают null. У каталога уже есть корректный заголовок Content-Type.

Передать локализованную подпись в JSONСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/gettext-lab.php';
bind_textdomain_codeset('learning', 'UTF-8');
$payload = ['title' => dgettext('learning', 'Cart')];
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
{"title":"Корзина"}

JSON требует корректного UTF-8. Функция не переводит ключ title и не исправляет чужие некорректные входные байты: здесь она задаёт кодировку конкретного каталога.

Возврат string — имя установленной кодировки, false — отсутствие явной настройки или неудача. Нельзя считать false доказательством «сейчас ASCII»: библиотека может выбирать кодировку по locale. Поддержка конвертации зависит от сборки libintl/iconv; кодировку из пользовательского запроса не подставляют. При неверной декларации исходных байтов в PO нужно исправить сам каталог, а не наслаивать преобразования на уже повреждённую строку. Пустой domain запрещён с PHP 8.4; в PHP 8.5.11 имя домена ограничено 1024 байтами. Функция управляет привязкой домена внутри процесса.

Рядом по задаче: bindtextdomain; gettext; dgettext.

dcgettextПеревести одно сообщение с явными доменом и категорией locale.

PHP 4+ · gettext

dcgettext(string $domain, string $message, int $category): string

Перевести одно сообщение с явными доменом и категорией locale.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
domain / messageНабор и исходный ключ.
categoryОдна доступная категория LC_*, обычно LC_MESSAGES; не LC_ALL.
РезультатПеревод либо исходное message.

dcgettext добавляет к явному домену категорию locale. Для обычных сообщений это LC_MESSAGES; редкий специальный каталог может быть связан с другой категорией. Это не категория товаров и не рубрика сайта, а системная группа настроек локали. Большинству приложений достаточно dgettext.

Контролируемый fallback для одной категорииСамостоятельный пример
<?php
declare(strict_types=1);

setlocale(LC_ALL, 'C');
putenv('LC_ALL=C');
putenv('LANGUAGE=');
echo dcgettext('lesson-no-catalog', 'Invoice', LC_TIME), "\n";
Ожидаемый результат
Invoice

Без каталога вернулся исходный текст. LC_TIME здесь показывает явный параметр категории; функция не форматирует дату и не превращает Invoice в дату.

Перевод из LC_MESSAGES в POSIX-окруженииПример для указанного окружения
<?php
declare(strict_types=1);

// Нужно: gettext, установленная ru_RU.UTF-8 и locale/ru/LC_MESSAGES/learning.mo.
if (!defined('LC_MESSAGES')) throw new RuntimeException('Категория LC_MESSAGES недоступна');
if (setlocale(LC_MESSAGES, 'ru_RU.UTF-8') === false) throw new RuntimeException('Locale не установлена');
putenv('LANGUAGE=ru');
if (bindtextdomain('learning', __DIR__ . '/locale') === false) throw new RuntimeException('Нет каталога');
bind_textdomain_codeset('learning', 'UTF-8');
echo dcgettext('learning', 'Cart', LC_MESSAGES), "\n";
Ожидаемый результат
Корзина

Пример для POSIX-системы с указанным каталогом. В использованной Windows-сборке LC_MESSAGES не определена, поэтому её успешное выполнение здесь не заявляется. Не подменяйте константу числом из чужой ОС.

Не передавать LC_ALL как категорию сообщенияСамостоятельный пример
<?php
declare(strict_types=1);

try { dcgettext('learning', 'Cart', LC_ALL); }
catch (ValueError $error) { echo "Нужна отдельная категория\n"; }
Ожидаемый результат
Нужна отдельная категория

В PHP 8.5.11 LC_ALL явно отклоняется. Эта константа переключает набор категорий в setlocale, но не задаёт единственную категорию каталога.

Для category используют поддерживаемую отдельную LC_* константу платформы. Она определяет категорию поиска; текущий textdomain остаётся прежним. Ограничения domain/message такие же, как у dgettext. Межплатформенность важна: даже наличие функции dcgettext не гарантирует наличие LC_MESSAGES. Если специальные категории не требуются, dgettext оставляет эти детали библиотеке.

Рядом по задаче: dgettext; dcngettext; gettext.

dcngettextВыбрать форму сообщения, явно задав домен и категорию.

PHP 4.2+ · gettext

dcngettext(string $domain, string $singular, string $plural, int $count, int $category): string

Выбрать форму сообщения, явно задав домен и категорию.

Gettext ищет перевод сообщения в каталоге. domain — имя набора переводов, например learning, а не интернет-домен сайта. message — исходная строка-ключ; результат — обычная PHP строка, которую можно сохранить, форматировать и безопасно вывести. Если перевода нет, обычно возвращается исходный текст, поэтому отсутствие исключения ещё не подтверждает исправность настройки.

Часть вызоваЧто означает
domainНепустой набор переводов.
singular / pluralИсходные формы ключа, до 4096 байт каждая в PHP 8.5.11.
countЦелое количество для Plural-Forms.
categoryПоддерживаемая отдельная LC_* категория, не LC_ALL.
РезультатВыбранная строка-шаблон.

dcngettext объединяет явный домен, категорию locale и выбор множественной формы. Это специализированный вариант dngettext, когда приложению действительно нужно управлять категорией поиска. Выбранная форма по-прежнему возвращается шаблоном, а значения подставляет sprintf.

Выбрать исходную форму при явной категорииСамостоятельный пример
<?php
declare(strict_types=1);

setlocale(LC_ALL, 'C');
putenv('LC_ALL=C');
putenv('LANGUAGE=');
foreach ([1, 3] as $count) {
    $pattern = dcngettext('lesson-no-catalog', '%d invoice', '%d invoices', $count, LC_TIME);
    echo sprintf($pattern, $count), "\n";
}
Ожидаемый результат
1 invoice
3 invoices

Демонстрация без MO проверяет передачу аргументов и fallback. LC_TIME не заставляет библиотеку вычислять дату; категория лишь участвует в поиске каталога.

Категория сообщений и русское количество на POSIXПример для указанного окружения
<?php
declare(strict_types=1);

// Нужны русский MO и доступная системная locale, как в начале главы.
if (!defined('LC_MESSAGES')) throw new RuntimeException('Нет LC_MESSAGES');
if (setlocale(LC_MESSAGES, 'ru_RU.UTF-8') === false) throw new RuntimeException('Нет locale');
putenv('LANGUAGE=ru');
if (bindtextdomain('learning', __DIR__ . '/locale') === false) throw new RuntimeException('Нет каталога');
bind_textdomain_codeset('learning', 'UTF-8');
$count = 22;
echo sprintf(dcngettext('learning', '%d item', '%d items', $count, LC_MESSAGES), $count), "\n";
Ожидаемый результат
22 товара

Ожидаемый перевод требует именно учебных трёх форм в MO. В локальной Windows проверен синтаксис этого контекстного примера; его POSIX locale не была эмулирована.

Пустой домен в PHP 8.4+ запрещён. LC_ALL не принимается как отдельная категория и вызывает ValueError. Для negative/fractional счётчиков не пытайтесь угадывать грамматику через принудительное приведение: сначала определите смысл количества. Если категория вам не нужна, у dngettext меньше параметров и меньше платформенных зависимостей. Выбор формы и язык не устраняют необходимость экранировать результат при выводе в HTML.

Рядом по задаче: dngettext; ngettext; dcgettext.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.