Существует множество подходов для добавления новых языков в пользовательский интерфейс вашего приложения. Хотя некоторые пользовательские решения, такие как 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 (конец базы данных), затем вернитесь к этому сообщению.
Приложение должно быть запущено и работает.
Синтаксис для получения переводимой строки: 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 <[email protected]>\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 (хорватский).
-
Сначала мы устанавливаем новую локаль на ОС:
sudo locale - gen hr_HR hr_HR . UTF - 8 sudo update - locale sudo dpkg - reconfigure locales
-
Затем мы генерируем новые файлы
.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
-
Затем мы
HELLO_WORLD
значениеHELLO_WORLD
наZdravo
, а затем генерируем файл.mo
:msgfmt - c - o Locale / hr_HR / LC_MESSAGES / messages . mo Locale / hr_HR / LC_MESSAGES / messages . po
-
Наконец, мы изменим настройку языкового стандарта в файле 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 или что-то еще полностью. В любом случае, прежде чем мы попробуем наши сценарии выше, нам нужно убедиться, что:
- gettext установлен и активирован
- необходимые локали были сгенерированы в ОС
В Ubuntu это легко пропустить, если в конце процесса развертывания выполнить следующие команды:
sudo apt-get install gettext sudo app / bin / i18n / addlang . sh
Все остальное происходит автоматически, так как файлы .pot
, .po
и .mo
предназначены для фиксации вместе с исходным кодом приложения.
Обратите внимание, что вам нужно изменить как команду установки, так и сценарии оболочки выше, если вы используете что-то другое, чем Ubuntu
Вывод
В этом руководстве мы рассмотрели добавление функций интернационализации в существующее приложение на базе Twig. Мы продемонстрировали использование gettext в фиктивном файле no-Twig, убедились, что все работает, и затем пошагово интегрировались с Twig. Наконец, мы написали несколько быстрых скриптов, которые могут очень помочь при совместном использовании проекта или его развертывании в рабочей среде.
Вы используете gettext? Или ваши приложения используют другой подход? Дайте нам знать об этом в комментариях!