Локализация интерфейса сайта с использованием PHP, Smarty и Gettext

    Все началось с того, что к существующему сайту понадобилось добавить русскую локализацию. После того как я изучал в течение нескольких дней эту тему и наступил множественные грабли, я в конце-концов получил рабочий сайт. А теперь хочу поделиться рецептом многоязычной локализации.
    Проверено и отработано на PHP 5.3.3 (Linux)/PHP 5.3.1 (Windows) + Smarty 3.0.7. В данном случае для существующего сайта на английском создавалась русскоязычная версия.
    Я не провожу ликбеза на тему «как это работает» (он есть на phpclub), но предлагаю простую инструкцию и описание возможных проблем, с которыми я сталкивался во время реализации.

    Итак,
    1. Качаем плагин Smarty Gettext (обязательно версии 1.0b1, а не 0.9.1, которую предлагают!): скачать, почитать больше про плагин (рекомендуется)
    2. Забираем оттуда файл block.t.php и кладем его в директорию smarty/plugins
    3. Создаем в корне сайта папку locale (можно и в другом месте, но только следите за путями), а в ней папку ru
    4. В папке ru создаем папку LC_MESSAGES — здесь будут храниться языковые файлы для русского языка
    5. После чего необходимо пройтись по всем файлам *.tpl и окружить все строки, которые должны быть переведены тэгом {t}, вот так:

      {t}Members{/t}
      {t 1=$user}Here is your payment for %1{/t}


      Пр формат тэга {t} можно почитать в документации к плагину Smarty Gettext
    6. Теперь, используя утилиту tsmarty2c.php из плагина Smarty Gettext, создается база строк, которую необходимо будет переводить на другие языки. Изначально предполагалось запускать утилиту из командной строки, передавая ей параметрами имена папок/файлов, но я предлагаю модифицированную версию, которая ищет *.tpl файлы в папке ./templates, парсит в них строк и кладет их в файл ./locale/langfrases.ctsmarty2c.phps
    7. Для создания языковых файлов я предлагаю использовать Poedit — кросс-платформенный редактор языковых файлов.
      В нем следует создать новый каталог (Файл-Создать каталог), указав на вкладке «Пути» путь к папке locale, в которой уже должен находиться файл langfrases.c (будьте осторожны — Poedit категорически не любит лишних пробелов в конце пути!). Каталог следует сохранить в папке LC_MESSAGES под именем messages.po. После чего редактор просканирует указанный путь на предмет файлов, содержащих строки и предложит их перевести:



      В результате после сохранения каталога в папке LC_MESSAGES у вас должны получиться два файла — messages.mo и messages.po, содержащие переводы строк на русский язык
    8. Теперь, когда у нас есть языковой файл с переводами строк, необходимо его подключить к сайту. Предположим, что имеется два языка — Английский и Русский. В этом случае используется две локали — en_US и ru_RU.utf8. Для того, чтобы использовать его в PHP нам понадобится следующий код (скачать):

      $lang = 'ru_RU.utf8';
      
      if (!defined('LC_MESSAGES')) define('LC_MESSAGES', 5); // в Windows эта константа может быть не определена
      setlocale(LC_MESSAGES, $lang); // устанавливаем локаль
          
      if  (!isset ($_COOKIE['lang'])) setcookie('lang', $lang, 1640995200); // сохраняем язык в Cookie
      bind_textdomain_codeset("messages", 'UTF8'); // устанавливаем кодировку файла messages.mo
      
      if ($lang == 'ru_RU.utf8') {
          // подключаем файлы русской локализации
          bindtextdomain("messages", "./locale");
          textdomain("messages");
      }
      else {
          // возвращаем английский язык
          bindtextdomain("messages", "");
      }
      
    После этого, при входе на страницу, вместо строк, окруженных тэгом {t} будут показаны переведенные строки из языкового файла. Если же в языковом файле они отсутствуют, будет показан не измененный текст.

    Проблемы, которые могут возникнуть:
    • Файл messages.mo в Windows кэшируется, и изменения в нем видны только после перезагрузки Apache.
    • Если передать функции setlocale значение отличное от LC_MESSAGES, то возможно возникновение проблем, связанных с тем, что в русском языке дробная часть отделяется запятой, а в английском — точкой. Так как локаль начинает влиять и на представления чисел в PHP, то при запросах к MySQL дробная часть теряется.
    • Локали ru_RU.utf8 и ru_RU могут отличаться на сервере. Если указывать просто ru_RU, то есть шанс получить вопросительные знаки вместо букв.
    Осталась нерешенной проблема склонения существительных с числами и их множественного числа.
    Утилита tsmarty2c умеет обрабатывать формы множественного числа и использовать вызовы ngettext, а вот Poedit у меня отказался их принимать.

    Надеюсь, что этот топик поможет вам сэкономить свое время и подтолкнет к созданию еще лучших, качественных сайтов =)
    Share post

    Similar posts

    Comments 39

      0
      А что там с падежами?
        +3
        gettext не умеет понимать грамматику и синтаксис языка.
        Переводимой единицей для gettext является не слово, а фраза целиком.

        Можно сделать из плагина block.t.php модификатор, а потом использовать что-то вот такое — http://thelogin.ru/wiki/Падежи_с_числами_без_gettext передавая в качестве one, two и many строку, обработанную модификатором.

        Тут помогает, что слово с пробелом в конце — это другое слово для gettext.

        Т.е. 'record', 'record ', 'record ' можно перевести как «запись», «записи», «записей» — пробел в английском не виден и можно переводить строки в обычном порядке через Poedit.
          +1
          В gettext все отлично с переводом множественных чисел, нужно лишь указать правило формирования в заголовке .po-файле. Для русского языка что-то вроде:

          "Plural-Forms: nplurals=3; plural=((((n%10)==1)&&((n%100)!=11))?(0):(((((n%10)>=2)&&((n%10)<=4))&&(((n%100)<10)||((n%100)>=20)))?(1):2));\n"

          Т.е. у нас три формы множественного числа:
          1, 21, 31… день
          2, 3, 4, 22, 23, 24, 32, 33, 34… дня
          0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 26, 27, 28, 29, 30, 35, 36… дней

          И потом переводим для каждого из трех случаев:

          msgid "%d day ago"
          msgid_plural "%d days ago"
          msgstr[0] "%d день назад"
          msgstr[1] "%d дня назад"
          msgstr[2] "%d дней назад"
          0
          Подскажите, как в gettext решается проблема такого плана: в разных контекстах одна и та же фраза переводится на русский по-разному. Допустим, order = 'заказ', order='порядок'
            0
            Сам думал над тем, чтобы в проблемных местах в файле локализации указывать при необходимости имя шаблона, для которого применять перевод фразы, но это уродство какое-то, да и в рамках одного шаблона могут быть подобные проблемы
              0
              Можно переводить с русского на английский.
              0
              использовать независимые от языка ключи.
                0
                Потребует явного введения второй локали. Хотя вариант, да.
                0
                Как выше уже писали «Переводимой единицей для gettext является не слово, а фраза целиком.»
                а для отдельных слов приходится делать что нибудь типа sort order, но всё таки это редкость
                  0
                  gettext поддерживает контексты:
                  www.gnu.org/software/hello/manual/gettext/Contexts.html
                  www.php.net/manual/en/book.gettext.php#89975

                  Это может быть полезно и в качестве ответа на вопрос о падежах.
                  0
                  На freebsd для gettext еще переменную среды пришлось выставлять (кажется «lang»).
                  Да и вообще стоит пояснить, что локаль используется системная, поэтому, чтобы на какой-нибудь китайский перевести, нужно будет в систему ставить китайскую локаль.
                    0
                    Есть гипотеза, что раз уж вы переводите на китайский — локаль у вас в системе есть ;-)
                      0
                      Это почему? ) я имел ввиду систему на сервере. Просто у нас были проблемы что пришлось доставлять локали для не очень популярных языков и с freebsd админам пришлось помучиться.
                        0
                        А, ну да, пардон, притупил немного ;-)
                    +4
                    А также вы не решили весьма важные проблемы:
                    1) порядок слов в разных языках разный, поэтому фраза «visit a {link} for info» и «для получения информации посетите {link}» требует ну тупого перевода фразы, а подстановки аргумента (аналог printf в каком-то смысле)
                    2) использование полных фраз вместо ключей часто смущает разработчиков, поэтому они не задумываясь изменяют знак препинания или лишний пробел, после чего перевод для этой фразы оказывается недоступен
                    3) mo — бинарный формат, поэтому отслеживать изменения посредством систем контроля версий не получится
                    4) существуют форматы форматирования чисел (decimal point), которые в вашем примере никак не рассмотрены
                    5) существуют данные для ввода (опять же, float значения), валидация которых никак не затронута
                    6) и наконец никак не решается проблема падежей и единственных/множественных чисел.

                    Это я все не из пальца высосал, мы делали точно так же, без смарти правда, но подход был таким же. И наткнулись на массу проблем, поэтому я такой подход считаю в корне неправильным.
                      0
                      сорри, аргументы просмотрел, мой пункт номер 1 отменяется
                        +5
                        вы просто не проявили смекалку
                        кто мешает использовать синтаксис того же printf? You have %d messages. У вас %s сообщений.
                        Использование ключей уместно лишь в некоторых случаях, а использование фраз крайне удобно. Программисту раз и на всегда надо сказать, что после коммита и определенной итерации все ошибки, в том числе и орфографические должны исправляться переводчиком или редактором в PO файле. Это нормальная практика работы. Если же фраза требует кординальной преработки, очевидно, что ее снова надо переводить.
                        Что касается склонений и падежей, то тут ваши слова звучат как «хочу чтобы было супер». Это полезный и грамотный инструмент для много язычности, а не волшебник. Прекрасно эта проблема решается методом plural_form, реализаций которого предостаточно. Что касается SVN, то у вас для этого есть .PO файл, который не является бинарным и вообще, зачем вам отслеживать изменения в этом файле? достаточно при выкладки компилировать PO файл в MO. 4 и 5 пункт вообще не очень понятент относительно многоязычности.

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

                        Я бы назвал тако2й подход интуитивным и от части нативным. Все остальные более-менее разумные методы — это попытка реализовать тот же GETTEXT только через другое место, не совсем логичное.
                          0
                          3) mo — бинарный формат, поэтому отслеживать изменения посредством систем контроля версий не получится

                          Предлагаю посмотреть в сторону transifex. Можно воспользоваться как хостингом, так и установить его у себя локально. Очень удобная штука, на мой взгляд, особенно если у вас несколько файлов переводов, достаточно много поддерживаемых языков и более одного переводчика.
                          В случае же когда у вас 2 языка (английский и родной), переводят девелоперы, то .po в vcs и poedit достаточно.
                          0
                          Файл messages.mo в Windows кэшируется, и изменения в нем видны только после перезагрузки Apache.

                          Он кешируется везде. Он читается 1 раз на PHP процесс. Если PHP работает как cgi, это не проблема. Но если как модуль апача — внесение изменений в этот файл не приводит к его автоматической перезагрузке из кеша gettext.
                            0
                            Если у вас шаред хостинг, перезагрузить апач нет прав, а изменения из .mo файла так и не появляются на сайте можно переименовать .mo файл.
                            У меня был простенький скрипт который из админки перекомпилировал .mo файл с текущей датой в имени, работало отлично.
                              +6
                              Ненавижу блядь смарти!
                                +3
                                А я вот уже не один год его постоянно использую в веб-проектах и доволен!
                                  –1
                                  Чем? Ещё одним эгоизмом разработчиков создать собственный язык?
                                0
                                А зачем вообще нужен gettex?
                                Я обычно все делаю намного проще — используя вот такую конструкцию в шаблонах, к примеру
                                {if $lang eq «ru»} {config_load file=«language.cfg» section=«ru»}
                                {elseif $lang eq «en»} {config_load file=«language.cfg» section=«en»}
                                тут language.cfg содержаться все преводы.

                                  0
                                  При использовании gettext — добавление нового языка — тривиальная задача, по крайней мере — по сравнению с вашим вариантом
                                    +1
                                    GETTEXT сопровождается рядом редакторов хороших и нативных для виндовс и линукс, например, и даже макос, а вот как вы рискуете отдавать ваши непонятные файлики переводчику на Китайский, скажем, мне не понятно. Также непонятно, как он у вас их берет? Это ж сплошной гемор. А если он какую-то кавычку сотрет случайно или еще чего? Даже уже по этому данный способ уступает GETTEXT.
                                    +3
                                    Буквально давече реализовал систему локализации на базе PHP + MySQL + Google Spreadsheet (для расшаренного редактирования таблицы переводов с уведомлением по Email & подсветкой непереведённых строк). Работает по принципу: PHP парсит исходники проектов на факт наличия строки вида {\w+} всё найденное выкладывает в Google Spreadsheet таблицу, далее переводчики переводят Google Spreadsheet таблицу. Модератор проверят, что всё ок и вызывает скрипт, которые сливает все данные таблицы в MySQL. Ну и там уже PHP парсит весь OUTPUT на факт наличия {\w+} строк, дёргает одним запросом из MySQL переводы и делает соответствующие замены.

                                    Работает достаточно быстро. Очень удобно редактирование таблицы переводов в Google Spreadsheet т.к. там есть history/revision документа, есть подсветка пустых ячеек, есть подписка на уведомление по почте о новых строках.

                                    Если кому интересно могу выложить исходник и оформить в виде поста.
                                      0
                                      Интересно! Часто приходиться работать с мультиязычными проектами — думаю это автоматизирует работу над переводами ?!
                                        0
                                        Интересно! Выкладывайте.
                                          +1
                                          Было бы очень интересно почитать.
                                            0
                                            Присоединюсь, несмотря на то, что уже прошло много времени с даты публикации комментария.
                                            0
                                            В своих проектах использую связку PHP + MySQL + Smarty. В коде шаблона это выглядит следующим образом:
                                            {v prefix=«contacts» key=«title» lang=«ru»}
                                            Есть таблица languages в которой хранится список языков, используемых на сайте. Также есь таблицы  prefixs и, связанная с ней связью 1..n, таблица keys. Таблица prefixs используется для группировки контента по разделам. У каждого префикса есть свой список ключей. Значения ключей для каждого языка хранятся в четвертой таблице values. Для языковой расширяемости в коде шаблона имеется переменная $current_language, которая позволяет без труда использовать 1..n языков на сайте:
                                            {v prefix=«messages» key=«thank» lang=$current_language} 
                                              0
                                              Еще нужно помнить что локаль для gettext устанавливается на процесс.
                                              Если вы используете php режиме тредов, о можете получить интересные сложно повторимые баги
                                                –2
                                                Мне кажется, для локализации (да и шаблонизации XML/HTML документов) ничего лучше XSLT до сих пор не придумали.
                                                1) XSL шаблон содержит скелет разметки страницы, 2) несколько подключаемых однородных XML файлов содержат тексты на нужном языке (если хотите, то и что угодно другое), 3) в итоге преобразования всё это формирует один конечный документ (при том гарантированно синтаксически валидный).

                                                Приведу один real-life пример из своего проекта.

                                                Фрагмент XSL шаблона, использующий языковые вставки:
                                                <xsl:template match="site">
                                                	<div class="box" id="SiteSummary">
                                                		<h1><xsl:value-of select="/page/nls/text[@id='SiteTitle']"/><xsl:value-of select="@domain"/>!</h1>
                                                		<p><xsl:value-of select="/page/nls/text[@id='SiteSubtitle']"/></p>
                                                	</div>
                                                	...
                                                </xsl:template>


                                                Пример структуры языкового XML файла:
                                                <?xml version="1.0" encoding="utf-8"?>
                                                <language id="ru">
                                                	<text id="SiteTitle"><![CDATA[Заголовок]]></text>
                                                	<text id="SiteSubtitle"><![CDATA[Подзаголовок]]></text>
                                                	...
                                                </language>


                                                PHP код импортирования данных языкового файла в общий DOM с данными, необходимыми для формирования конечной страницы (собственно, на нём весь процесс локализации и заканчивается):
                                                private function load_nls()
                                                {
                                                	$xml_nls = new DOMDocument('1.0', 'utf-8');
                                                	if (!$xml_nls->load(BASE_DIR . '/' . STATIC_DIR . 'language_ru.xml'))
                                                		die('unable to load NLS data');
                                                	$root = $xml_nls->documentElement;
                                                	if ($root->nodeName != 'language')
                                                		die('invalid NLS XML data');
                                                
                                                	$nls_storage = $this->template->data->createElement('nls');
                                                	foreach ($root->getElementsByTagName('text') as $e_text)
                                                		$nls_storage->appendChild($this->template->data->importNode($e_text, TRUE));
                                                	$this->template->data->documentElement->appendChild($nls_storage);
                                                }


                                                Ну и grand finale — генерация готовой страницы:
                                                	$xsl = new DOMDocument('1.0', 'utf-8');
                                                	$xslt = new XSLTProcessor;
                                                	$xsl->loadXML($xsl_contents, LIBXML_NOCDATA);
                                                	$xslt->importStyleSheet($xsl);
                                                	echo $xslt->transformToXML($this->data);


                                                Никакого gettext и Smarty, ура. :)
                                                  0
                                                  последняя фраза вас выдала =) Хотя идея понятна.
                                                    0
                                                    Товарищ старался и получил в репу минусы.
                                                    Всё бы ничего, но зачем кидаться такими словами: «ничего лучше XSLT до сих пор не придумали».
                                                    Сам использую Zend_Translate, в качестве обёртки над Gettext.

                                                    По поводу Smarty могу сказать следующее: Это как сандалики, из которых надо вырасти. А кто-то ходит в них до конца дней, что уже пальцы вылазят. Никак не хочу обосрать эту библиотеку, пусть юзают те, кому нравится. Иногда попадаются на доработку проекты, использующие её в качестве шаблонизатора. Порой взглянув на умение пользоваться Smarty предыдущими программистами вызывает смех сквозь слёзы.
                                                      0
                                                      На минусы, честно говоря, пофиг. Не ради этого стараемся.

                                                      но зачем кидаться такими словами: «ничего лучше XSLT до сих пор не придумали»

                                                      А чего бы не кинуться, если я такого мнения придерживаюсь? ;) Хорошо бы и другие, прежде чем лепить минус, хотя бы попробовали и сравнили…
                                                      0
                                                      А как формируется языковой XML?
                                                      Как переводчику понять, где используется ключ SiteTitle?
                                                      Как за 7 дней научить переводчика писать валидный XSL.
                                                      Что со склонением, множественным числом и переводом параметризируемых фраз?
                                                      И как, черт возьми, быть с тем фактом, что 82% разработчиков не понимают XSL?
                                                        0
                                                        А как формируется языковой XML?

                                                        Руками в «блокноте» или в XML редакторе. Любая удобная структура, с которой потом сможете работать из XSL.

                                                        Как переводчику понять, где используется ключ SiteTitle?

                                                        Метки с подстановкой конкретных текстов должен расставлять UI дизайнер или верстальщик, я так понимаю. Можете вместо кратких меток использовать и полный текст (<xsl:value-of select="/page/nls/text[@id='Это заголовок сайта']"/>), но тут потребуется некоторая доработка кода, хотя и незначительная.

                                                        Как за 7 дней научить переводчика писать валидный XSL.

                                                        Переводчикам и не надо его писать. Им надо писать XML для конкретных языков. Как вариант — в любом другом удобном формате, из которого вы потом сможете сгенерировать XML.

                                                        Что со склонением, множественным числом и переводом параметризируемых фраз?

                                                        Ничего, это домашнее задание для разработчиков. Чудес как бы никто не обещал. ;)

                                                        И как, черт возьми, быть с тем фактом, что 82% разработчиков не понимают XSL?

                                                        А 82% разработичков понимают Smarty, gettext или какие-то другие шаблонизаторы и средства локализации? Что-то сомневаюсь…

                                                    Only users with full accounts can post comments. Log in, please.