Статьи

Простые многоязычные приложения веток с Gettext

Существует множество подходов для добавления новых языков в пользовательский интерфейс вашего приложения. Хотя некоторые пользовательские решения, такие как symfony / translation , возможно, проще в использовании, они медленнее, чем старый добрый нативный gettext , на порядок величины.

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

Рассматриваемое приложение будет нашим собственным nofw — готовым к использованию скелетным приложением .

Вы говорите на английском?

Начальная загрузка и основы

Мы будем использовать нашу надежную Homestead Improved как всегда, как среду — если вы хотите следовать, пожалуйста, запустите его. В нашей коробке уже установлен и активирован gettext. Мы увидим, как вручную установить его для целей развертывания, в конце этого руководства.

Так как nofw использует Twig , нам понадобится расширение i18n . Чтобы начать проект правильно, вот полный процесс:

git clone https : / / github . com / swader / nofw cd nofw git checkout tags / 2.93 - b 2.93 composer require twig / extensions 

Примечание: вышеприведенные команды клонируют старую версию nofw — без встроенных функций интернационализации — так что читатели могут следовать вместе с учебником.

Это установит как расширения Twig, так и все зависимости проекта. Следуйте процедуре из README, чтобы настроить оставшуюся часть приложения nofw (конец базы данных), затем вернитесь к этому сообщению.

Приложение должно быть запущено и работает.

Nofw работает

Синтаксис для получения переводимой строки: gettext("string") или его псевдоним: _("string") — то есть, _() — это функция, которую мы вызываем, а "string" — это строка, которую мы переводим. Если перевод для "string" не найден, то возвращается оригинальное (которое считается заполнителем) значение. Заполнители обычно представляют собой полные строки на самом популярном для аудитории сайта языке, поэтому, если перевод по какой-то причине не удается, читаемый текст по-прежнему отображается.

Давайте попробуем заставить эту работу работать с фиктивным PHP-файлом, который не поддерживается Twig, просто чтобы убедиться, что все в порядке. Мы будем использовать пример из старой серии статей о gettext . В корне проекта мы i18n.php файл с именем i18n.php и передадим ему содержимое:

 <?php $language = "en_US.UTF-8" ; putenv ( "LANGUAGE=" . $language ) ; setlocale ( LC_ALL , $language ) ; $domain = "messages" ;  // which language file to use bindtextdomain ( $domain , "Locale" ) ; bind_textdomain_codeset ( $domain , 'UTF-8' ) ; textdomain ( $domain ) ; echo _ ( "HELLO_WORLD" ) ; 

В той же папке давайте создадим структуру папок, подобную этой:

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

Описывая приведенный выше код, мы сначала устанавливаем язык среды ОС на американский английский, а затем сохраняем его как переменную среды. Функция setlocale в PHP использует константу LC_ALL для переключения всех контекстов на заданную локаль — поэтому PHP попытается преобразовать даты, числовое форматирование и даже валюту в локаль, которую мы ей предоставили. Естественно, LC_ALL включает в себя и наши собственные переведенные сообщения.

Поле $domain предназначено для указания PHP, какой языковой файл использовать — этот языковой файл будет называться messages.po в его сыром, редактируемом виде и messages.mo в его скомпилированной, машиночитаемой форме. bindtextdomain просто устанавливает путь языкового файла, который, как мы знаем, находится внутри папки Locale , а bind_textdomain_codeset устанавливает языковой набор символов. UTF-8 — довольно универсальная безопасная ставка.

Наконец, textdomain устанавливает активный домен для использования.

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

экстракция

Gettext поставляется с удобным инструментом для извлечения строк-заполнителей из файлов. В корне проекта мы выполним:

 xgettext -- from - code = UTF - 8 - o Locale / messages . pot public / i18n . php 

Выше xgettext будет использовать кодировку UTF-8 для вывода ( -o ) собранных строк из public/i18n.php в данный файл. Проверка полученного файла messages.pot теперь дает:

 # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-10 10:44+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: public/i18n.php:13 msgid "HELLO_WORLD" msgstr "" 

.pot обозначает portable object template . Эти файлы шаблонов используются для создания других языковых файлов. Если мы решим добавить японский в наше приложение позже, файл .pot будет использоваться для создания Locale/ja_JP/LC_MESSAGES/messages.po который, в свою очередь, будет использоваться для создания соответствующего файла messages.mo . Давайте использовать этот подход для генерации файла сообщений en_US сейчас:

 msginit -- locale = en_US -- output - file = Locale / en_US / LC_MESSAGES / messages . po -- input = Locale / messages . pot 

Этот процесс необходимо повторять для каждого нового языка, который мы хотим добавить в приложение.

Файл .po очень похож на файл .pot , только он содержит реальные строки перевода, которые мы можем редактировать:

 # English translations for PACKAGE package. # Copyright (C) 2016 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # vagrant <vagrant@homestead>, 2016. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-10 10:44+0000\n" "PO-Revision-Date: 2016-04-10 10:58+0000\n" "Last-Translator: vagrant <vagrant@homestead>\n" "Language-Team: English\n" "Language: en_US\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ASCII\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: public/i18n.php:13 msgid "HELLO_WORLD" msgstr "HELLO_WORLD" 

После замены значения msgstr для HELLO_WORLD на Howdy мы должны скомпилировать файл .po файл .mo который Gettext может прочитать:

 msgfmt - c - o Locale / en_US / LC_MESSAGES / messages . mo Locale / en_US / LC_MESSAGES / messages . po 

Добавление нового языка

Чтобы убедиться, что все работает, давайте добавим новый язык — hr_HR (хорватский).

  1. Сначала мы устанавливаем новую локаль на ОС:

     sudo locale - gen hr_HR hr_HR . UTF - 8 sudo update - locale sudo dpkg - reconfigure locales 
  2. Затем мы генерируем новые файлы .po из файлов .pot :

     mkdir - p Locale / hr_HR / LC_MESSAGES msginit -- locale = hr_HR -- output - file = Locale / hr_HR / LC_MESSAGES / messages . po -- input = Locale / messages . pot 
  3. Затем мы HELLO_WORLD значение HELLO_WORLD на Zdravo , а затем генерируем файл .mo :

     msgfmt - c - o Locale / hr_HR / LC_MESSAGES / messages . mo Locale / hr_HR / LC_MESSAGES / messages . po 
  4. Наконец, мы изменим настройку языкового стандарта в файле PHP на hr_HR.UTF-8 и протестируем.

Все должно работать нормально.

Примечание. Для очистки кэша gettext может потребоваться перезагрузка веб-сервера и / или PHP-FPM.

прут

Теперь, когда мы знаем, что gettext работает отлично, и мы можем добавлять новые языки по прихоти, давайте посмотрим, как он ведет себя в сочетании с Twig. Сначала добавим следующее в app/config/config_web.php , в самом верху:

 $language = "hr_HR.UTF-8" ; putenv ( "LANGUAGE=" . $language ) ; setlocale ( LC_ALL , $language ) ; $domain = "messages" ;  // which language file to use bindtextdomain ( $domain , __DIR__ . "/../../Locale" ) ; bind_textdomain_codeset ( $domain , 'UTF-8' ) ; textdomain ( $domain ) ; 

Чтобы Twig мог работать с переводимыми строками, ему нужно расширение i18n, которое мы установили в разделе начальной загрузки. Затем в шаблонах мы используем блок trans :

 {% trans %} Hello {{ name }} ! {% endtrans %} 
 

Конечно, gettext не знает, что {{name}} должно означать, поэтому расширение Twig автоматически компилирует это в дружественный к gettext Hello %name%! , Одно предостережение в том, что xgettext не предназначен для извлечения строк веток, поэтому нам нужна альтернатива в соответствии с документами .

Мы скомпилируем наши шаблоны представлений во временную папку системы, а затем скопируем их, как обычные файлы PHP!

Сначала добавим переводимое сообщение в один из файлов. Например, где-то в Standard/Views/home.twig мы можем поместить:

      {% trans %} This is translatable {% endtrans %} 
 

Затем в app/bin мы создадим новый файл: twigcache.php :

 <?php require __DIR__ . '/../../vendor/autoload.php' ; $shared = require __DIR__ . '/../config/shared/root.php' ; $tplDir = dirname ( __FILE__ ) . '/templates' ; $tmpDir = '/tmp/cache/' ; $loader = new Twig_Loader_Filesystem ( $shared [ 'site' ] [ 'viewsFolders' ] ) ; // force auto-reload to always have the latest version of the template $twig = new Twig_Environment ( $loader , [ 'cache' = > $tmpDir , 'auto_reload' = > true , ] ) ; $twig - > addExtension ( new Twig_Extensions_Extension_I18n ( ) ) ; // configure Twig the way you want // iterate over all your templates foreach ( $shared [ 'site' ] [ 'viewsFolders' ] as $tplDir ) { foreach ( new RecursiveIteratorIterator ( new RecursiveDirectoryIterator ( $tplDir ) , RecursiveIteratorIterator : : LEAVES_ONLY ) as $file ) {  // force compilation if ( $file - > isFile ( ) ) { $twig - > loadTemplate ( str_replace ( $tplDir . '/' , '' , $file ) ) ; } } } 

Этот файл извлекает общий root.php конфигурации root.php в котором определены папки представлений, и поэтому нам нужно обновлять их только в одном месте. Выполнение сценария с помощью php app/bin/twigcache.php теперь создает дерево каталогов с файлами PHP-кэша:

 / tmp / cache ├── 1a │ └── 1ad38dfd106734cda72279c3bbd83dd4c64d93ff9c713afb1e74904144018347 . php ├── 1c │ └── 1ca70331199383cea2ce308ab09447cebd7e5e81f2a7f5caa319d577f3a66682 . php . . . ├── df │ └── df75e14ad2cb55315ab205872c8b8590ffde333912ec5c89e44c365479bfe457 . php └── f4 └── f444ff725954cd5a9ec29ceb56a9cbf7eda8a273cea96c542c35a271e0f57c7e . php 

Теперь мы можем использовать xgettext для этой коллекции:

 xgettext - o Locale / messages . pot -- from - code = UTF - 8 - n -- omit - header / tmp / cache / * / * . ph 

Проверка Locale/message.pot теперь показывает совершенно новое содержание:

 #: /tmp/cache/d0/d006e63c5a4c4e6a700d9273d4523dd0cf419105fa4b00cf6b89918c67df4b2b.php:56 msgid "This is translatable" msgstr "" 

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

 msgmerge - U Locale / en_US / LC_MESSAGES / messages . po Locale / messages . pot msgmerge - U Locale / hr_HR / LC_MESSAGES / messages . po Locale / messages . pot 

Команда msgmerge объединяет изменения из messages.pot в определенный файл messages.po . Мы используем msgmerge вместо msginit здесь для удобства, но мы могли бы также использовать msginit для запуска нового языкового файла. Однако Merge имеет дополнительный бонус: поскольку xgettext больше не xgettext переводимые строки в i18n.php из приведенного выше примера, недавно обновленные файлы .po фактически закомментировали ранее использовавшуюся пару строка-значение:

 #: /tmp/cache/d0/d006e63c5a4c4e6a700d9273d4523dd0cf419105fa4b00cf6b89918c67df4b2b.php:56 msgid "This is translatable" msgstr "Yes, this is totally translatable" #~ msgid "HELLO_WORLD" #~ msgstr "Howdy" 

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

Предполагая, что мы изменили некоторые значения перевода, давайте сейчас скомпилируем в .mo и протестируем:

 msgfmt - c - o Locale / hr_HR / LC_MESSAGES / messages . mo Locale / hr_HR / LC_MESSAGES / messages . po msgfmt - c - o Locale / en_US / LC_MESSAGES / messages . mo Locale / en_US / LC_MESSAGES / messages . po 

Рабочий хорватский перевод

Обратите внимание на нашу переведенную строку внизу — все работает как положено!

Конечно, конфигурация, которую мы вставили в верхнюю часть config_web.php может использовать некоторую работу — например, определение нужного языка с помощью маршрутов и т. Д., Но для краткости это работает отлично.

Теперь осталось только найти все строки во всех представлениях и превратить их в блоки {% trans %} !

Бонус: скрипты!

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

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


Мы поместим все эти скрипты в app/bin/i18n/ и сделаем их исполняемыми в командной строке:

 touch app / bin / i18n / { addlang . sh , update - pot . sh , update - mo . sh , config . sh } chmod + x app / bin / i18n / * . sh 

конфиг

 LOCALE_FOLDER = "Locale" REGULAR_USER = "forge" [ [ - f app / bin / i18n / config_local . sh ] ] && source app / bin / i18n / config_local . sh 

Этот сценарий будет включен в другие сценарии, что позволяет пользователям изменять нужную папку для локалей. Кроме того, он содержит имя пользователя, не являющегося пользователем sudo. Поскольку вообще плохая идея иметь много команд sudo внутри скрипта bash, и нам, безусловно, нужно будет выполнять многие из них с привилегиями root, мы решим выполнить весь скрипт с помощью sudo а затем просто отбросим привилегии обычный пользователь по тем командам, которые не нуждаются в sudo . По умолчанию пользователь «подделывает», потому что это настраивает пользователь Laravel Forge .

Этот сценарий также включает другой сценарий конфигурации, если он существует (потому что он находится в .gitignore и не будет существовать на живых серверах), в котором имя пользователя может быть переопределено. Это полезно для местного развития. Например, при использовании Homestead Improved все будет выполняться с точки зрения vagrant пользователя, а пользователь forge не существует.

Новый язык / скрипт обновления языков

 #!/usr/bin/env bash # addlang.sh source app / bin / i18n / config . sh if [ [ $EUID - ne 0 ] ] ; then echo "This script must be run as root" 1 > & 2 exit 1 fi if [ - z " $1 " ] ; then for folder in $ ( find ${LOCALE_FOLDER} - maxdepth 1 - type d | awk - F / '{print $NF }' ) do if [ " ${folder} " ! = ${LOCALE_FOLDER} ] ; then echo "Executing locale-gen ${folder} ${folder} .UTF-8" locale - gen ${folder} ${folder} . UTF - 8 fi done echo "Executing updates..." update - locale dpkg - reconfigure locales fi if [ - n " $1 " ] ; then echo "Executing locale-gen $1 $1 .UTF-8" locale - gen $1 $1 . UTF - 8 echo "Executing updates..." update - locale dpkg - reconfigure locales echo "Creating folder: ${LOCALE_FOLDER} / $1 /LC_MESSAGES" sudo - u ${REGULAR_USER} mkdir - p ${LOCALE_FOLDER} / $1 / LC_MESSAGES fi 

Это немедленно установит любую локаль, переданную в качестве первого аргумента, например так:

 sudo app / bin / i18n / addlang . sh ja_JP 

Это также создаст соответствующую языковую папку в папке Locale .

Если параметр не был передан, то этот сценарий будет искать ожидаемые локали, просматривая папку Locale и автоматически устанавливая каждую Locale в соответствии с именем подпапки.

Locale/hr_HR есть, если есть папки Locale/en_US и Locale/hr_HR , все будет так, как если бы мы запустили sudo app/bin/i18n/addlang.sh en_US и sudo app/bin/i18n/addlang.sh hr_HR . Это помогает автоматически устанавливать локали во время развертывания.

Этот сценарий необходимо запускать от имени пользователя root, поскольку для команд, связанных с локалью, требуются повышенные привилегии.

Обновить скрипт

 #!/usr/bin/env bash # update-potpo.sh source app / bin / i18n / config . sh echo "Regenerating cache" php app / bin / twigcache . php echo "Running xgettext on the cached files" xgettext - o ${LOCALE_FOLDER} / messages . pot -- from - code = UTF - 8 - n -- omit - header / tmp / cache / * / * . php for folder in $ ( find ${LOCALE_FOLDER} - maxdepth 1 - type d | awk - F / '{print $NF }' ) do if [ " ${folder} " ! = ${LOCALE_FOLDER} ] ; then if [ [ - f ${LOCALE_FOLDER} / ${folder} / LC_MESSAGES / messages . po ] ] ; then echo "Merging for ${folder} " msgmerge - U Locale / ${folder} / LC_MESSAGES / messages . po ${LOCALE_FOLDER} / messages . pot else echo "Initializing for ${folder} " msginit -- locale = ${folder} -- output - file = ${LOCALE_FOLDER} / ${folder} / LC_MESSAGES / messages . po -- input = ${LOCALE_FOLDER} / messages . pot fi fi done 

Это восстанавливает кэш представления, xgettext в нем и объединяет результат с текущим файлом .pot , если таковой имеется. Затем он использует обновленный файл .pot для обновления файлов .po . Обратите внимание, что он использует msginit если язык еще не был инициализирован, и msgmerge противном случае.

Перекомпилировать скрипт

 #!/usr/bin/env bash # update-mo.sh source app / bin / i18n / config . sh for folder in $ ( find ${LOCALE_FOLDER} - maxdepth 1 - type d | awk - F / '{print $NF }' ) do if [ " ${folder} " ! = ${LOCALE_FOLDER} ] ; then echo "Compiling .mo for ${folder} " msgfmt - c - o Locale / ${folder} / LC_MESSAGES / messages . mo ${LOCALE_FOLDER} / ${folder} / LC_MESSAGES / messages . po fi done 

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

Развертывание

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

  1. gettext установлен и активирован
  2. необходимые локали были сгенерированы в ОС

В Ubuntu это легко пропустить, если в конце процесса развертывания выполнить следующие команды:

 sudo apt-get install gettext sudo app / bin / i18n / addlang . sh 

Все остальное происходит автоматически, так как файлы .pot , .po и .mo предназначены для фиксации вместе с исходным кодом приложения.

Обратите внимание, что вам нужно изменить как команду установки, так и сценарии оболочки выше, если вы используете что-то другое, чем Ubuntu

Вывод

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

Вы используете gettext? Или ваши приложения используют другой подход? Дайте нам знать об этом в комментариях!