Как создать многоязычное приложение: демо с PHP и Gettext
Опубликовано: 2022-03-11Независимо от того, создаете ли вы веб-сайт или полноценное веб-приложение, чтобы сделать его доступным для более широкой аудитории, часто требуется, чтобы оно было доступно на разных языках и в разных регионах.
Фундаментальные различия между большинством человеческих языков делают это далеко не легким. Различия в грамматических правилах, языковых нюансах, форматах дат и многом другом делают локализацию уникальной и сложной задачей.
Рассмотрим этот простой пример.
Правила множественного числа в английском языке довольно просты: у вас может быть форма единственного числа слова или форма множественного числа слова.
Однако в других языках, таких как славянские, есть две формы множественного числа в дополнение к форме единственного числа. Вы даже можете найти языки с четырьмя, пятью или шестью формами множественного числа, например, словенский, ирландский или арабский.
То, как организован ваш код, как спроектированы ваши компоненты и интерфейс, играет важную роль в определении того, насколько легко вы сможете локализовать свое приложение.
Интернационализация (i18n) вашей кодовой базы помогает гарантировать, что ее можно будет относительно легко адаптировать к различным языкам или регионам. Интернационализация обычно выполняется один раз, предпочтительно в начале проекта, чтобы избежать необходимости внесения значительных изменений в исходный код в будущем.
Как только ваша кодовая база интернационализирована, локализация (l10n) становится вопросом перевода содержимого вашего приложения на определенный язык/локаль.
Локализация должна выполняться каждый раз, когда требуется поддержка нового языка или региона. Кроме того, всякий раз, когда часть интерфейса (содержащая текст) обновляется, становится доступным новый контент, который затем необходимо локализовать (т. е. перевести) для всех поддерживаемых локалей.
В этой статье мы узнаем, как интернационализировать и локализовать программное обеспечение, написанное на PHP. Мы рассмотрим различные варианты реализации и различные инструменты, доступные в нашем распоряжении, чтобы упростить процесс.
Инструменты интернационализации
Самый простой способ интернационализировать программное обеспечение PHP — использовать файлы массивов. Массивы будут заполнены переведенными строками, которые затем можно будет найти в шаблонах:
<h1><?=$TRANS['title_about_page']?></h1>
Это, однако, вряд ли рекомендуется для серьезных проектов, так как это определенно создаст проблемы с обслуживанием в будущем. Некоторые проблемы могут возникнуть даже в самом начале, например, отсутствие поддержки интерполяции переменных или множественного числа существительных и так далее.
Одним из наиболее классических инструментов (часто используемым в качестве эталона для i18n и l10n) является инструмент Unix под названием Gettext.
Несмотря на то, что он был создан в 1995 году, он по-прежнему представляет собой комплексный инструмент для перевода программного обеспечения, который также прост в использовании. Хотя с ним довольно легко начать работу, он по-прежнему имеет мощные вспомогательные инструменты.
Gettext — это то, что мы будем использовать в этом посте. Мы представим отличное приложение с графическим интерфейсом, которое можно использовать для простого обновления исходных файлов l10n, тем самым избегая необходимости иметь дело с командной строкой.
Библиотеки для упрощения работы
Существуют основные веб-фреймворки и библиотеки PHP, поддерживающие Gettext и другие реализации i18n. Некоторые из них легче установить, чем другие, или они имеют дополнительные функции или поддерживают разные форматы файлов i18n. Хотя в этом документе мы сосредоточимся на инструментах, поставляемых с ядром PHP, вот список некоторых других, о которых стоит упомянуть:
oscarotero/Gettext: поддержка Gettext с объектно-ориентированным интерфейсом; включает улучшенные вспомогательные функции, мощные экстракторы для нескольких форматов файлов (некоторые из них изначально не поддерживаются командой
gettext
). Также можно экспортировать в форматы, выходящие за рамки файлов .mo/.po, что может быть полезно, если вам нужно интегрировать файлы перевода в другие части системы, например в интерфейс JavaScript.symfony/translation: Поддерживает множество различных форматов, но рекомендует использовать подробные XLIFF. Не включает вспомогательные функции или встроенный экстрактор, но поддерживает заполнители с использованием
strtr()
внутри.zend/i18n: поддерживает массивы и файлы INI или форматы Gettext. Реализует уровень кэширования, чтобы избежать необходимости каждый раз читать файловую систему. Также включает помощники по представлениям, входные фильтры и валидаторы с учетом локали. Однако у него нет экстрактора сообщений.
Другие фреймворки также включают модули i18n, но они недоступны вне их кодовых баз:
Laravel: поддерживает файлы базовых массивов; не имеет автоматического экстрактора, но включает помощник
@lang
для файлов шаблонов.Yii: поддерживает массив, Gettext и перевод на основе базы данных, а также включает экстрактор сообщений. Поддерживается расширением
Intl
, доступным с PHP 5.3 и основанным на проекте ICU. Это позволяет Yii выполнять мощные замены, такие как прописывание чисел, форматирование дат, времени, интервалов, валюты и порядковых номеров.
Если вы решите использовать одну из библиотек, не предоставляющих экстракторов, вы можете использовать форматы Gettext, чтобы вы могли использовать исходную цепочку инструментов Gettext (включая Poedit), как описано в оставшейся части главы.
Установка Геттекста
Возможно, вам потребуется установить Gettext и соответствующую библиотеку PHP с помощью диспетчера пакетов, например apt-get или yum. После установки включите его, добавив extension=gettext.so
(Linux/Unix) или extension=php_gettext.dll
(Windows) в файл php.ini
.
Здесь мы также будем использовать Poedit для создания файлов перевода. Вы, вероятно, найдете его в менеджере пакетов вашей системы; он доступен для Unix, Mac и Windows, а также может быть бесплатно загружен на его веб-сайте.
Типы файлов Gettext
Есть три типа файлов, с которыми вы обычно имеете дело при работе с Gettext.
Основными из них являются файлы PO (Portable Object) и MO (Machine Object), первый из которых представляет собой список читаемых «переведенных объектов», а второй — соответствующий двоичный файл (который должен интерпретироваться Gettext при выполнении локализации). Существует также файл POT (шаблон PO), который просто содержит все существующие ключи из ваших исходных файлов и может использоваться в качестве руководства для создания и обновления всех файлов PO.
Файлы шаблонов не являются обязательными; в зависимости от инструмента, который вы используете для выполнения l10n, вам будет достаточно только файлов PO/MO. У вас будет одна пара файлов PO/MO для каждого языка и региона, но только одна POT для каждого домена.
Разделение доменов
В некоторых случаях в больших проектах вам может понадобиться разделить переводы, когда одни и те же слова имеют разное значение в разных контекстах.
В таких случаях вам нужно разделить их на разные «домены», которые в основном представляют собой именованные группы файлов POT/PO/MO, где имя файла является указанным доменом перевода .
Малые и средние проекты обычно для простоты используют только один домен; его имя произвольное, но мы будем использовать «main» для наших примеров кода.
Например, в проектах Symfony домены используются для разделения перевода сообщений проверки.
Код локали
Локаль — это просто код, который идентифицирует одну версию языка. Он определяется в соответствии со спецификациями ISO 639-1 и ISO 3166-1 alpha-2: две строчные буквы для языка, за которыми может следовать знак подчеркивания и две прописные буквы, обозначающие код страны или региона.
Для редких языков используются три буквы.
Для некоторых говорящих часть страны может показаться избыточной. На самом деле некоторые языки имеют диалекты в разных странах, например австрийский немецкий (de_AT) или бразильский португальский (pt_BR). Вторая часть используется для различения этих диалектов — когда ее нет, она рассматривается как «общая» или «гибридная» версия языка.
Структура каталогов
Чтобы использовать Gettext, нам нужно будет придерживаться определенной структуры папок.
Во-первых, вам нужно выбрать произвольный корень для ваших файлов l10n в исходном репозитории. Внутри у вас будет папка для каждой необходимой локали и фиксированная папка «LC_MESSAGES», которая будет содержать все ваши пары PO/MO.
Формы множественного числа
Как мы уже говорили во введении, разные языки могут иметь разные правила множественного числа. Однако Gettext избавляет нас от этой проблемы.
При создании нового файла .po вам нужно будет объявить правила множественного числа для этого языка, и переведенные части, чувствительные к множественному числу, будут иметь другую форму для каждого из этих правил.
При вызове Gettext в коде вам нужно будет указать число, относящееся к предложению (например, для фразы «У вас есть n сообщений.» вам нужно будет указать значение n), и он выработает правильную форму использовать - даже используя подстановку строк, если это необходимо.
Множественные правила состоят из необходимого количества правил с булевой проверкой для каждого правила (проверка не более чем для одного правила может быть опущена). Например:
Японский:
nplurals=1; plural=0;
nplurals=1; plural=0;
- одно правило: нет форм множественного числаАнглийский:
nplurals=2; plural=(n != 1);
nplurals=2; plural=(n != 1);
- два правила: используйте форму множественного числа только тогда, когда n не равно 1, в противном случае используйте форму единственного числа.Бразильский португальский:
nplurals=2; plural=(n > 1);
nplurals=2; plural=(n > 1);
- два правила, используйте форму множественного числа только тогда, когда n больше 1, в противном случае используйте форму единственного числа.
Для более глубокого объяснения есть информативный учебник LingoHub, доступный онлайн.
Gettext определит, какое правило использовать, на основе предоставленного числа и будет использовать правильную локализованную версию строки. Для строк, в которых необходимо обрабатывать множественное число, вам нужно будет включить в файл .po отдельное предложение для каждого определенного правила множественного числа.
Пример реализации
После всей этой теории давайте перейдем к практике. Вот выдержка из файла .po (пока не слишком беспокойтесь о синтаксисе, а вместо этого просто получите представление об общем содержании):
msgid "" msgstr "" "Language: pt_BR\n" "Content-Type: text/plain; charset=UTF-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" msgid "We're now translating some strings" msgstr "Nos estamos traduzindo algumas strings agora" msgid "Hello %1$s! Your last visit was on %2$s" msgstr "Ola %1$s! Sua ultima visita foi em %2$s" msgid "Only one unread message" msgid_plural "%d unread messages" msgstr[0] "So uma mensagem nao lida" msgstr[1] "%d mensagens nao lidas"
Первый раздел работает как заголовок с пустыми msgstr
msgid
Он описывает кодировку файла, формы множественного числа и некоторые другие вещи. Второй раздел переводит простую строку с английского на бразильский португальский, а третий делает то же самое, но использует замену строки из sprintf
, позволяя переводу содержать имя пользователя и дату посещения.
Последний раздел представляет собой пример форм множественного числа, отображающих версию единственного и множественного числа как msgid
на английском языке и их соответствующие переводы как msgstr
0 и 1 (после числа, заданного правилом множественного числа).
Там также используется замена строки, поэтому число можно увидеть непосредственно в предложении, используя %d
. Формы множественного числа всегда имеют два msgid
(единственное и множественное число), поэтому рекомендуется не использовать сложный язык в качестве источника перевода.
Ключи локализации
Как вы могли заметить, мы используем настоящее английское предложение в качестве идентификатора источника. Этот msgid
используется во всех ваших файлах .po, что означает, что другие языки будут иметь тот же формат и те же поля msgid
, но переведенные строки msgstr
.
Говоря о ключах перевода, здесь есть два стандартных «философских» подхода:
1. msgid как настоящее предложение
Основными преимуществами такого подхода являются:
Если есть части программного обеспечения, не переведенные на какой-либо язык, отображаемый ключ все равно будет иметь какое-то значение. Например, если вы знаете, как переводить с английского на испанский, но вам нужна помощь в переводе на французский, вы можете опубликовать новую страницу с отсутствующими французскими предложениями, и вместо этого часть веб-сайта будет отображаться на английском языке.
Переводчику гораздо проще понять, что происходит, и сделать правильный перевод на основе
msgid
.Он дает вам «бесплатный» l10n для одного языка — исходного.
С другой стороны, основным недостатком является то, что если вам нужно изменить фактический текст, вам нужно заменить один и тот же msgid
в нескольких языковых файлах.
2. msgid как уникальный структурированный ключ
Это будет структурировано описывать роль предложения в приложении, включая шаблон или часть, в которой находится строка, а не ее содержимое.
Это отличный способ организовать код, отделив текстовое содержимое от логики шаблона. Однако это может создать проблемы для переводчика, который упустит контекст.

Файл исходного языка потребуется в качестве основы для других переводов. Например, в идеале у разработчика должен быть файл «en.po», который переводчики будут читать, чтобы понять, что писать в «fr.po».
Отсутствующие переводы отображали бы бессмысленные клавиши на экране («top_menu.welcome» вместо «Привет, пользователь!» на упомянутой непереведенной французской странице).
Это хорошо, потому что перевод должен быть завершен до публикации, но плохо, потому что проблемы с переводом будут действительно ужасными в интерфейсе. Некоторые библиотеки, тем не менее, включают возможность указать данный язык как «резервный», имея поведение, аналогичное другому подходу.
В руководстве по Gettext предпочтение отдается первому подходу, так как в целом это проще для переводчиков и пользователей в случае возникновения проблем. Этот подход мы будем использовать и здесь.
Однако следует отметить, что документация Symfony поддерживает перевод на основе ключевых слов, что позволяет вносить независимые изменения во все переводы, не затрагивая при этом шаблоны.
Повседневное использование
В обычном приложении вы будете использовать некоторые функции Gettext при написании статического текста на своих страницах.
Затем эти предложения появлялись в файлах .po, переводились, компилировались в файлы .mo, а затем использовались Gettext при рендеринге фактического интерфейса. Учитывая это, давайте свяжем то, что мы обсуждали до сих пор, в пошаговом примере:
1. Пример файла шаблона, включая несколько различных вызовов gettext
<?php include 'i18n_setup.php' ?> <div> <h1><?=sprintf(gettext('Welcome, %s!'), $name)?></h1> <!-- code indented this way only for legibility → <?php if ($unread): ?> <h2> <?=sprintf( ngettext('Only one unread message', '%d unread messages', $unread), $unread )?> </h2> <?php endif ?> </div> <h1><?=gettext('Introduction')?></h1> <p><?=gettext('We\'re now translating some strings')?></p>
gettext()
просто переводитmsgid
в соответствующийmsgstr
для данного языка. Также есть сокращенная функция_()
, которая работает так же.ngettext()
делает то же самое, но с множественными правиламиТакже есть
dgettext()
иdngettext()
, которые позволяют переопределить домен для одного вызова (подробнее о конфигурации домена в следующем примере).
2. Пример установочного файла (i18n_setup.php, как указано выше), выбор правильной локали и настройка Gettext.
Использование Gettext включает в себя немного стандартного кода, но в основном это настройка каталога локалей и выбор соответствующих параметров (локали и домена).
<?php /** * Verifies if the given $locale is supported in the project * @param string $locale * @return bool */ function valid($locale) { return in_array($locale, ['en_US', 'en', 'pt_BR', 'pt', 'es_ES', 'es'); } //setting the source/default locale, for informational purposes $lang = 'en_US'; if (isset($_GET['lang']) && valid($_GET['lang'])) { // the locale can be changed through the query-string $lang = $_GET['lang']; //you should sanitize this! setcookie('lang', $lang); //it's stored in a cookie so it can be reused } elseif (isset($_COOKIE['lang']) && valid($_COOKIE['lang'])) { // if the cookie is present instead, let's just keep it $lang = $_COOKIE['lang']; //you should sanitize this! } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // default: look for the languages the browser says the user accepts $langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); array_walk($langs, function (&$lang) { $lang = strtr(strtok($lang, ';'), ['-' => '_']); }); foreach ($langs as $browser_lang) { if (valid($browser_lang)) { $lang = $browser_lang; break; } } } // here we define the global system locale given the found language putenv("LANG=$lang"); // this might be useful for date functions (LC_TIME) or money formatting (LC_MONETARY), for instance setlocale(LC_ALL, $lang); // this will make Gettext look for ../locales/<lang>/LC_MESSAGES/main.mo bindtextdomain('main', '../locales'); // indicates in what encoding the file should be read bind_textdomain_codeset('main', 'UTF-8'); // if your application has additional domains, as cited before, you should bind them here as well bindtextdomain('forum', '../locales'); bind_textdomain_codeset('forum', 'UTF-8'); // here we indicate the default domain the gettext() calls will respond to textdomain('main'); // this would look for the string in forum.mo instead of main.mo // echo dgettext('forum', 'Welcome back!'); ?>
3. Подготовка перевода к первому запуску
Одним из больших преимуществ Gettext по сравнению с настраиваемыми пакетами i18n является обширный и мощный формат файлов.
Возможно, вы думаете: «О, чувак, это довольно сложно понять и отредактировать вручную, простой массив был бы проще!» Не заблуждайтесь, такие приложения, как Poedit, здесь, чтобы помочь - много. Вы можете получить программу с их веб-сайта, она бесплатна и доступна для всех платформ. Это довольно простой инструмент, к которому можно привыкнуть, и в то же время очень мощный — он использует все функции, доступные в Gettext. Мы будем работать с последней версией Poedit 1.8.
При первом запуске вы должны выбрать «Файл > Создать…» из меню. Вас спросят о языке; выберите/отфильтруйте язык, на который вы хотите перевести, или используйте формат, который мы упоминали ранее, например en_US
или pt_BR
.
Теперь сохраните файл, используя ту же структуру каталогов, о которой мы упоминали. Затем вы должны нажать «Извлечь из источников», и здесь вы настроите различные параметры для задач извлечения и перевода. Вы сможете найти все это позже через «Каталог > Свойства»:
Исходные пути: Включите все папки из проекта, где
gettext()
(и одноуровневые) — обычно это папка (папки) с вашими шаблонами/представлениями. Это единственная обязательная настройка.Свойства перевода:
- Название и версия проекта, команда и адрес электронной почты команды: полезная информация, которая содержится в заголовке файла .po.
- Формы множественного числа: это правила, о которых мы упоминали ранее. В большинстве случаев вы можете оставить его с параметром по умолчанию, поскольку Poedit уже включает удобную базу данных правил множественного числа для многих языков.
- Кодировки: желательно UTF-8.
- Кодировка исходного кода: кодировка, используемая вашей кодовой базой — вероятно, тоже UTF-8, верно?
Исходные ключевые слова: базовое программное обеспечение знает, как
gettext()
и подобные вызовы функций выглядят на нескольких языках программирования, но вы также можете создавать свои собственные функции перевода. Именно здесь вы добавите эти другие методы. Об этом позже в разделе «Советы».
После установки этих свойств Poedit просканирует ваши исходные файлы, чтобы найти все вызовы локализации. После каждого сканирования Poedit будет отображать сводку того, что было найдено и что было удалено из исходных файлов. Новые записи будут пустыми в таблице перевода, что позволит вам ввести локализованные версии этих строк. Сохраните его, и файл .mo будет (повторно) скомпилирован в ту же папку, и вуаля! Ваш проект будет интернационализирован!
Poedit также может предложить общие переводы из Интернета и из предыдущих файлов. Это удобно, так что вам нужно только проверить, имеют ли они смысл, и принять их. Если вы не уверены в переводе, вы можете пометить его как нечеткий, и он будет отображаться желтым цветом. Синие записи — это те, у которых нет перевода.
4. Перевод строк
Как вы, возможно, заметили, есть два основных типа локализованных строк: простые и во множественном числе.
Простые имеют только два поля: исходную и локализованную строку. Исходная строка не может быть изменена, так как Gettext/Poedit не позволяют изменять ваши исходные файлы; скорее, вам нужно будет изменить сам источник и повторно отсканировать файлы. ( Совет: если щелкнуть правой кнопкой мыши строку перевода, отобразится подсказка с исходными файлами и строками, в которых используется эта строка.)
Строки форм множественного числа включают два поля для отображения двух исходных строк и вкладки, позволяющие настраивать различные окончательные формы.
Пример строки с формой множественного числа в Poedit, показывающей вкладку перевода для каждой из них.
Всякий раз, когда вы изменяете файлы исходного кода и вам нужно обновить переводы, просто нажмите «Обновить», и Poedit повторно просканирует код, удалив несуществующие записи, объединив те, которые были изменены, и добавив новые.
Poedit также может попытаться угадать некоторые переводы, основываясь на других сделанных вами переводах. Эти предположения и измененные записи получат маркер «Нечеткий», указывающий на то, что они нуждаются в пересмотре, отображаемый в списке желтым цветом.
Это также полезно, если у вас есть команда переводчиков, и кто-то пытается написать что-то, в чем он не уверен: просто отметьте это как «Нечеткое», и кто-то другой проверит это позже.
Наконец, рекомендуется оставить отмеченным «Просмотр > Непереведенные записи», так как это поможет вам не забыть какие-либо записи. Из этого меню вы также можете открывать части пользовательского интерфейса, которые позволяют при необходимости оставлять контекстную информацию для переводчиков.
Советы и хитрости
Веб-серверы могут кэшировать ваши файлы .mo.
Если вы используете PHP как модуль на Apache (mod_php), у вас могут возникнуть проблемы с кешированием файла .mo. Это происходит при первом чтении, а затем, чтобы обновить его, вам может потребоваться перезапустить сервер.
На Nginx и PHP5 обычно требуется всего пара обновлений страницы, чтобы обновить кеш перевода, а на PHP7 это требуется редко.
Библиотеки предоставляют вспомогательные функции для сокращения кода локализации.
Многие предпочитают использовать _()
вместо gettext()
. Многие пользовательские библиотеки i18n из фреймворков также используют что-то похожее на t()
, чтобы сделать переведенный код короче. Однако это единственная функция, которая имеет ярлык.
Возможно, вы захотите добавить в свой проект некоторые другие, такие как __()
или _n()
для ngettext()
, или, может быть, причудливую _r()
, которая объединит вызовы gettext()
и sprintf()
. Другие библиотеки, такие как Gettext от oscarotero, также предоставляют подобные вспомогательные функции.
В таких случаях вам нужно указать утилите Gettext, как извлекать строки из этих новых функций. Не бойтесь, это очень легко. Это просто поле в файле .po или экран настроек в Poedit (в редакторе эта опция находится внутри «Каталог > Свойства > Ключевые слова источников»).
Помните: Gettext уже знает функции по умолчанию для многих языков, поэтому не беспокойтесь, если этот список кажется пустым. Вам необходимо включить в этот список спецификации новых функций в следующем формате:
Если вы создаете что-то вроде
t()
, которое просто возвращает перевод строки, вы можете указать его какt
. Gettext будет знать, что единственным аргументом функции является переводимая строка;Если функция имеет более одного аргумента, вы можете указать, в каком из них находится первая строка, и, при необходимости, форму множественного числа. Например, если сигнатура нашей функции
__('one user', '%d users', $number)
, спецификация будет__:1,2
, что означает, что первая форма является первым аргументом, а вторая форма является второй аргумент. Если вместо этого в качестве первого аргумента используется ваше число, спецификация будет__:2,3
, что указывает на то, что первая форма является вторым аргументом, и так далее.
После включения этих новых правил в файл .po новое сканирование выведет ваши новые строки так же легко, как и раньше.
Сделайте свое PHP-приложение многоязычным с помощью Gettext
Gettext — очень мощный инструмент для интернационализации вашего PHP-проекта. Помимо гибкости, позволяющей поддерживать большое количество языков, написанных человеком, поддержка более 20 языков программирования позволяет легко перенести свои знания об использовании его с PHP на другие языки, такие как Python, Java или C#.
Кроме того, Poedit может помочь сгладить путь между кодом и переведенными строками, делая процесс более простым и легким для понимания. Он также может оптимизировать общие усилия по переводу благодаря интеграции с Crowdin.
По возможности учитывайте другие языки, на которых могут говорить ваши пользователи. Это в основном важно для неанглоязычных проектов: вы можете увеличить доступ пользователей, если выпустите его на английском языке, а также на своем родном языке.
Конечно, не все проекты нуждаются в интернационализации, но намного проще запустить i18n в зачаточном состоянии проекта, даже если он изначально не нужен, чем делать это позже, если впоследствии это станет обязательным требованием. А с такими инструментами, как Gettext и Poedit, это проще, чем когда-либо.