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

Дерево руководства
Глава 132 · От первой надписи к локализации приложения
Gettext: переводы сайта, каталоги и множественные формы
Все функции Gettext: от первой переведённой кнопки до каталогов PO/MO, доменов модулей, русских форм количества и диагностики окружения.
Зачем локализация магазину: один код, разные надписи
На русском сайте кнопка называется «Оформление заказа», на английском — Checkout. Копировать весь шаблон ради другого языка неудобно: исправление корзины пришлось бы повторять в каждой копии. Gettext хранит надписи отдельно от логики. Код называет исходное сообщение, а каталог переводов возвращает текст нужного языка. Это поиск заранее подготовленного перевода, а не автоматический переводчик и не внешний AI-сервис.
- PHP вызывает gettext("Cart")
- Выбраны язык ru и набор learning
- MO содержит Cart → Корзина
- В переменную вернулась строка Корзина
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 от другой сборки не является установкой.
<?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:en | GNU-список приоритетов переводов, если библиотека его поддерживает. В 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.
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.
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.
- count = 22
- ngettext выбирает форму %d товара
- sprintf подставляет 22
- Покупатель видит 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 |
| Явно управлять категорией locale | dcgettext · 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";
Ожидаемый результат
Здравствуйте, Ира <VIP>!
%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 перевод.
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 сама выбирает язык и ищет файл. Директория должна существовать и быть доступна серверному процессу.
- domain = learning
- directory = /srv/shop/locale
- выбран язык ru
- 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.
<?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.
<?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 в дату.
<?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 не определена, поэтому её успешное выполнение здесь не заявляется. Не подменяйте константу числом из чужой ОС.
<?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 не заставляет библиотеку вычислять дату; категория лишь участвует в поиске каталога.
<?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.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


