Как построить многоязычное приложение: пример на PHP и Gettextперевод

Не важно разрабатываете вы небольшой сайт или полноценное Web-приложение, вы хотите чтобы все это было доступно как можно большей аудитории. Для этого, как минимум, нужно обеспечить доступность сайта на разных языках.

Различия в языках, такие как грамматика, формат даты и другие нюансы, делают эту задачу достаточно сложной.

Рассмотрим простой пример.

Правила построения множественных форм слов. В английском языке все просто, есть единственное число и есть множественное. В других языках, например, славянских, множественных форм две. А где-то их может быть больше, например, в арабском.

Организация кода и проектирование интерфейсов играет важную роль в том на сколько просто будет проходить локализация.

internationalization, i18n

После того, как код прошел интернационализацию(i18n), локализация (l10n) становится вопросом перевода контента на разные языки. Каждый раз, когда добавляется новый язык в поддержку или какая-то часть интерфейса, содержащая текст, меняется, нужно править локализацию.

В этой статье мы рассмотрим вариант реализации многоязычности на PHP.

Инструменты для интернационализации.

Самый простой способ, это использовать массивы. Массив заполняется строками с переводом, который потом используется в шаблонах.

<h1><?=$TRANS['title_about_page']?></h1>

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

Но, это не самый лучший способ для крупных проектов, т.к. в будущем возникнут проблемы с поддержкой такого решения. Что-то может возникнуть сразу же. Например, отсутствие плюрализации. Классическое решение, это инструмент Gettext, пришедший из Unix. Хотя и разработан в 1995 году, он по прежнему является отличным инструментом для перевода ПО.

Gettext это как раз то, что будет использоваться в этой статье. Мы опишем отличный GUI для работы с l10n файлами, без необходимости открывать командную строку.

Библиотеки, которые облегчат вашу жизнь

Существуют различные фрэймворки и библиотеки, которые поддерживают Gettext и другие реализации i18n. Какие-то проще использовать, у некоторых есть полезные функции или поддержка различных i18n форматов. Мы обратим внимание на инструменты, доступные в ядре PHP.

oscarotero/Gettext: даёт поддержку Gettext; содержит полезные функции, механизмы для работы с различными форматами файлов. Также может подготавливать .mo/.po файлы, которые могут понадобиться в других частях системы.

symfony/translation: поддерживает множество различных форматов, но рекомендуется использовать XLIFF. Нет вспомогательных функций.

zend/i18n: поддерживает массивы и INI файлы или формат Gettext. Поддерживает кеширование. Содержит вспомогательные функции для представлений фильтрации и валидации.

Некоторые фреймворки содержат i18n модули, но они не могут быть отделены от самого фреймворка.

Laravel: поддерживает работу с массивами. Автоматической подстановки нет, но есть @lang хелпер.

Yii: работает с массивами, Gettext и БД. Может автоматически находить сообщения в тексте. Под капотом расширение Intl, доступное с PHP 5.3 и базирующееся на проекте ICU. Всё это даёт Yii мощные возможности по замете строк, переводу чисел в строку, форматированию дат, времени, валюты и т.д.

Если будете использовать библиотеку, которая сам не ищет сообщения для перевода, то можно воспользоваться специальными инструментами для работы с Gettext (включая Poedit). Всё это будет описано ниже.

Установка Gettext

Вы можете установить Gettext и все необходимые расширения через менеджер пакетов, такой как apt-get или yum. После установки нужно убедиться, что они подключены в php.ini: extension=gettext.so (Linux/Unix) или extension=php_gettext.dll (Windows)

Также мы будем использовать Poedit для создания файлов с переводом. Это приложение моет быть найдено через ваш менеджер пакетов. Приложение доступно для Unix, Mac и Windows и может использоваться бесплатно.

Типы фалов Gettext

Существует 3 типа файлов, с которыми обычно работают.

Основные это PO (Portable Object) и MO (Machine Object). PO содержит переводимые объекты, а MO бинарный файл, который интерпретируется Gettext-ом. Также имеется POT (PO template) файл, который содержит все доступные ключи из ваших исходников и используется для создания и обновления PO файлов.

POT файлы не обязательны. Всё зависит от инструмента, который вы используете для формирования l10n. Достаточно только пары PO/MO файлов. Будет по паре PO/MO файлов на каждый язык и регион, но только один POT файл на часть проекта.

Раздельные части проекта

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

В таких случаях, вам нужно разделить переводы по областям, которые состоят из групп POT/PO/MO файлов.

В небольших проектах используется только одна группа. Название может быть произвольным, но мы будет использовать «main» в наших примерах.

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

Код локали

Локаль это код, который определяет версию языка. Он определён в спецификациях ISO 639-1 и ISO 3166-1 alpha-2: две строчные буквы, определяющие язык, символ подчеркивания и две заглавные буквы, указывающие на страну или регион.

Для некоторых редких языков используются коды из 3х символов.

Может показаться, что часть с указанием страны лишняя. Но на самом деле, некоторые языки имеют диалекты. Например, Австрийский Немецкий (de_AT) или Бразильский Португальский (pt_BR). Вторая часть кода нужна чтобы точно определить диалект. Если страна/регион не указаны, то применяется значение по-умолчанию.

Структура проекта

Чтобы использовать Gettext нужно придерживаться определённой структуры папок.

Во-первых, нужно определить корневую папку для l10n файлов. В ней нужно создать папки для каждой локали и папку «LC_MESSAGES», в которой будут находиться все ваши пары PO/MO файлов.

Множественные формы

Как было сказано в самом начале, языки могут иметь разные правила формирования множественных форм. Но Gettext спасает от этой проблемы.

Когда создаёте .po файл, нужно определить правила формирования множественных форм для этого языка.

Когда вызываете Gettext в коде, нужно указать число, которое относится к переводимой фразе (например, для фразы «У вас n сообщений» нужно указать n), что даст указание какую форму перевода использовать.

Правила формирования множественных форм состоят из количества форм и тестового выражения с булевым результатом. Например:

Японский: nplurals=1; plural=0; - одно правило: нет множественных форм

Английский: nplurals=2; plural=(n != 1); - два правила: использовать множественную форму, если n не 1, иначе единственное число.

Бразильский португальский: nplurals=2; plural=(n > 1); - два правила: множественная форма, если n больше 1, иначе единственное число.

Пример реализации

Посмотрим на .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 "Nós estamos traduzindo algumas strings agora"

msgid "Hello %1$s! Your last visit was on %2$s"
msgstr "Olá %1$s! Sua última visita foi em %2$s"

msgid "Only one unread message"
msgid_plural "%d unread messages"
msgstr[0] "Só uma mensagem não lida"
msgstr[1] "%d mensagens não lidas"

Первая часть – что-то вроде заголовка, в котором msgid и msgstr - пустые строки.

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

Последний блок это пример работы с множественными формами. Msgid – строка на английском, а msgstr[n] – перевод для n.
Количество может использоваться непосредственно в переводе через %d. Множественные формы всегда имеют два msgid (для единственного числа и множественного). Поэтому не рекомендуется использовать сложные языки как исходные.

Ключи

Мы используем предложения на английском как исходные ID. Эти msgid используются во всех .po файлах. В других языках msgid будет таким же, а msgstr будет содержать перевод.

Говоря о ключах, есть два философских подходах:

1. msgid реальное предложение

Главное достоинство такого подхода:

Если некоторые части приложения не переведены на какой-то язык, ключ всё равно будет иметь осмысленное значение. Например, если у вас есть перевод с английского на испанский, но нет перевода на французский, вы всё-таки можете выложить новую страницу сайта, а непереведённые строки будут выводиться на английском.

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

И в самом начале у вас уже будет l10n для одного из языков – исходного.

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

2. msgid уникальный и структурированный ключ

Такой подход описывает роль строки в приложении и включает в себя указатель на шаблон или часть приложения, где эта строка будет использоваться.

Это, конечно, здорово, когда код структурирован, а контент отделён от логики приложения. Но это может лишить переводчика контекста, что может привести к ошибкам в переводе.

В этом случае понадобится базовый файл с исходными значениями строк, на который будут опираться переводчики. Например, будет файл en.po, на который будут использовать переводчики для подготовки fr.po.

А вот непереведённый текст будет отображаться как бессмысленная строка («top_menu.welcome» вместо «Hello, User!»). Но некоторые библиотеки позволяют указать язык, который нужно использовать в случае отсутствия нужного.

Мануал Gettext предполагает использование первого варианта, т.к. он проще в использовании и поддержке.

Как использовать

1. Простой шаблон с вызовами gettext

 

<?php include 'i18n_setup.php' ?>
<div id="header">
    <h1><?=sprintf(gettext('Welcome, %s!'), $name)?></h1>
    <?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. Пример файла настройки 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//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. Preparing translation for the first run

Одним из главных преимуществ Gettext для пользовательских i18n фреймворков является мощный формат файлов.

Может возникнуть мысль, что очень сложно работать с такими файлами, гораздо проще использовать массивы. Poedit вам в помощь. Бесплатное приложение, доступное для скачивания на сайте разработчика.

При первом запуске, выберите «File -> New» в главном меню. Нужно будет указать целевой язык.

New

Language for translation

Сохранить файл используя структуру папок как было описано ранее. Затем выберите «Extract from sources» и настройте работу с исходниками. В будущем эти настройки доступны в разделе «Catalog -> Properties»:

Source paths: укажите путь до каталога, в котором находятся файлы, для которых требуется перевод. Обычно это папка с шаблонами.

Translation properties: название проекта, команда и e-mail адрес. Полезная информация, которая указывается в заголовке .po файла.

Plural forms: здесь настраиваются правила для множественных форм, о которых говорили ранее. В большинстве случаев можно оставить значение по-умолчанию, т.к. в Poedit уже есть правила для многих языков.

Charsets: utf-8

Source code charset: кодировка исходных кодов.

Source keywords: вообще приложение знает как выглядят вызовы gettext() во многих языках программирования. Но, в принципе, можно указать что-то своё.

После всех необходимых настроек, Poedit просканирует все ваши исходники и найдёт вызовы функций для локализации. После каждого сканирования, Poedit покажет отчет, в котором можно будет увидеть то, что добавлено и что удалено. Новые строки будут иметь пустое значение в таблице. Сохраняете и .mo файл скомпилируется и заменит старый.

Poedit также может подсказывать перевод.

Translation

4. Перевод строк

Есть два основных типа строк: простые и с множественными значениями.

Для простых строк есть два поля: исходник и перевод. Исходник не может быть изменён, т.к. Gettext/Poedit не может менять исходные коды вашего приложения. Если нужно внести изменения, то вам придется самостоятельно внести правки в код и запустить сканирование повторно. Кстати, если кликнуть правой кнопкой по строке в таблице, то можно посмотреть подсказку с указанием файла и строки, где находится вызов локализационной функции.

Для множественных форм есть два поля для исходных строк и закладки для настройки конечных форм.

Перевод строк

Советуем включить «View -> Untranslated entries first», чтобы точно не забыть перевести текст.



Комментарии

добавить
Комментариев пока нет. Будете первым?
Чтобы комментировать, нужно авторизоваться

Советуем почитать


Федеральная система
Сергей 0

Федеральная система "Город" читать далее

В прошлый раз описал процесс работы с платёжной системой Cyberplat, теперь хочу поделиться опытом работы с ФСГ (Федеральная система город).

Разработано сие чудо ЦФТ. Старались делать все по ГОСТ, поэтому произвести интеграцию не так просто, как хотелось бы (рассматриваем PHP).

0 11.07.2016 17:40:15

PHP Управление строками
Максим 0

PHP Управление строками читать далее

Мало кто из разработчиков задумывается о том, как устроено ядро PHP и что происходит «под капотом». Действительно, на практике большинству редко бывают нужны подобные знания, тем не менее обладать ими будет полезно. Статья рассказывает о том, как устроены строки в PHP и о различиях работы с ними в PHP 5 и 7.


Это мой первый перевод подобной статьи, тем более технически не самой простой. Обо всех неточностях пишите в комментариях или лично мне.

0 04.05.2016 23:28:31