Обзор компонентов Symfony2 : Translation

Компонент Translation

Современное приложение должно поддерживать использование разных языков, чтобы люди из любого уголка Земли могли полноценно его использовать. Интернационализация — i18n — процесс разработки приложения с возможностью его перевода на разные языки без изменения структуры программы. Локализация — l10n — процесс адаптации уже переведенного приложения под специфический регион или область, например добавление определенного формата даты или валюты. Компонент Translation предоставляет большое количество способов интернационализации, но не локализации.

Компонент разбит на три основные части:

  • Каталоги (Catalogues) — это коллекции значений типа «ключ-значение». Компонент просто находит нужное значение для определенного ключа.
  • Загрузчики (Loaders) — конвертируют переводы из форматов YAML, XLIFF or JSON в массив PHP.
  • Экспорт (Dumpers) — экспорт каталога перевода в нужный формат.

Компонент изначально поддерживает несколько загрузчиков и способов экспорта для таких форматов как PHP, XLIFF, JSON, YAML, PO, MO, INI и CSV. Так же он способен загружать PHP массивы.

Простой пример

Давайте начнем с простого примера. Попробуем перевести пару фраз на испанский и португальский языки. Взгляните на следующий код:

<?php
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Translator;
include_once __DIR__. '/vendor/autoload.php';
$translator = new Translator('es_ES');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array(
    'hello world!' => '¡hola mundo!',
    'hello %name%!' => '¡hola %name%!'
), 'es_ES');
$translator->addResource('array', array(
    'hello world!' => 'Olá mundo!',
    'hello %name%!' => 'Olá %name%!'
), 'pt');
var_dump($translator->trans('hello world!'));
var_dump($translator->trans('hello %name%!', array('%name%' => 'Raul')));
var_dump($translator->trans('hello %name%!', array('%name%' => 'Raul'), null, 'pt_PT'));

Мы создали объект $translator, который является экземпляром класса Translator, в качестве локализации по-умолчанию установили «es_ES», которая представляет из себя комбинацию кода ISO 639-1 для языка иISO 3166-1 alpha-2 для страны.

Затем, добавили ArrayLoader для загрузки перевода в PHP (пока обойдёмся без файлов перевода). При помощи ArrayLoader мы регистрируем переведённые выражения для «es_ES» и «pt».

Наконец, мы вызываем метод trans() из объекта переводчика и получаем переведённые строковые значения. Первая строка выдаст «¡hola mundo!», так как «es_ES» — наша локаль по-умолчанию. Следующий вызов вернет «¡hola Raul!», так как мы используем заполнители (placeholders), а последний вызов вернет «Olá Raul!», потому что сначала переводчик ищет перевод для локали «pt_PT», а затем для «pt».

<?php
string(13) "¡hola mundo!"
string(12) "¡hola Raul!"
string(10) "Olá Raul!"

В качестве идентификатора для строки перевода рекомендуется использовать ключи с нижним подчёркиванием, а не полные строковые значения (hello_world вместо hello world!):

<?php
$translator->addResource('array', array(
    'hello_world' => '¡hola mundo!',
    'hello_name!' => '¡hola %name%!'
), 'es_ES');

Загрузка перевода из отдельного файла

Обычно используются файлы перевода вместо массивов PHP. Загрузка таких файлов подразумевает регистрацию загрузчика нужного формата и загрузка самого файла. Например, чтобы загрузить следующий файл:

messages:
    good_morning: "Buenos días"
errors:
    invalid_email: "Email no válido"

Мы должны зарегистрировать загрузчик YAML, с указанием параметра yaml и установить абсолютный путь к файлу перевода.

<?php
use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Translator;
include_once __DIR__. '/vendor/autoload.php';
$translator = new Translator('es_ES');
$translator->addLoader('yaml', new YamlFileLoader());
$translator->addResource('yaml', __DIR__ . '/translations/es_ES.yml' , 'es_ES');
var_dump($translator->trans('messages.good_morning'));
var_dump($translator->trans('errors.invalid_email'));

Ожидаемый вывод:

<?php
string(12) "Buenos días"
string(16) "Email no válido"

Помните, что все загрузчики зависят от компонента Конфигурации (Config) и загрузчик Yaml зависит от компонента YAML.

Разбивка перевода по доменам

В предыдущем примере мы использовали формат Yaml для перевода сообщений о возникновении ошибки и об удачном вводе данных. Переводы могут быть вложеными и мы используем следующую нотацию ключей: «messages.good_morning» и «errors.invalid_email». Но гораздо удобнее разбивать сообщения по разным файлам, в зависимости от смысловой нагрузки. Чаще всего различают обычные сообщения, сообщения об ошибках, сообщения для форм и сообщения для проверки данных.

В нашем примере мы можем разбить сообщения на два файла с указанием домена. Если мы не укажем домен, то его значение по-умолчанию — «messages» (домен это что-то вроде пространства имён для переводов).

<?php
use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Translator;
include_once __DIR__. '/vendor/autoload.php';
$translator = new Translator('es_ES');
$translator->addLoader('yaml', new YamlFileLoader());
$translator->addResource('yaml', __DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');
$translator->addResource('yaml', __DIR__ . '/translations/errors.es_ES.yml' , 'es_ES', 'errors');
var_dump($translator->trans('good_morning'));
var_dump($translator->trans('invalid_email', array(), 'errors'));

Множественное число

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

Например, чтобы отобразить сообщение «There is 1 cat», если существует один кот и «There are X cats», где X от двух и более котов, нужно воспользоваться заполнителями (placeholders)

cats: "There is 1 cat|There are %number% cats" 

Всю работу берет на себя метод transChoice(). Он получает ключ для перевода (cats), переменную количества котов и необязательное значение заполнителя %numbers% (placeholder):

<?php
var_dump($translator->transChoice('cats', 1));
var_dump($translator->transChoice('cats', 2, array('%number%' => 2)));

Вывод предыдущего примера:

<?php
string(14) "There is 1 cat"
string(16) "There are 2 cats"

В документации Symfony2 вы можете более подробно прочитать о генерации множественного числа.

Экспорт сообщений

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

Следующий пример загружает перевод в формате YAML и делает экспорт в JSON при помощи JsonFileDumper:

<?php
use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Dumper\JsonFileDumper;
include_once __DIR__. '/vendor/autoload.php';
$loader = new YamlFileLoader();
$catalogue = $loader->load(__DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');
$dumper = new JsonFileDumper();
$dumper->dump($catalogue, array('path' => __DIR__.'/dumps'));

Скрипт создаст файл ./dumps/messages.es_ES.yml со следующим содержанием в формате JSON:

{
    "good_morning": "Buenos d\u00edas",
    "good_night": "Buenas noches",
    "welcome": "Bienvenido"
}

Создание специфических загрузчиков

Всё это отлично, но как быть, если мы работаем на устаревшей версии symfony2, а нам необходимо использовать современные форматы перевода. Мы вполне можем использовать компонент Translator. Нам всего лишь надо создать свой загрузчик.

Допустим мы используем следующий формат для хранения наших переводов:

(welcome)(Bienvenido)
(goodbye)(Adios)
(hello)(Hola)

Для создания своего загрузчика нам надо реализовать интерфейс LoaderInterface, в котором объявлен один метод load(). В нашем загрузчике этот метод открывает файл перевода и парсит его в массив PHP. После чего создает и возвращает каталог.

<?php
namespace RaulFraile\Loader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Loader\LoaderInterface;
class CustomLoader implements LoaderInterface
{
    public function load($resource, $locale, $domain = 'messages')
    {
        $messages = array();
        $lines = file($resource);
        foreach ($lines as $line) {
            if (preg_match('/\(([^\)]+)\)\(([^\)]+)\)/', $line, $matches)) {
                $messages[$matches[1]] = $matches[2];
            }
        }
        $catalogue = new MessageCatalogue($locale);
        $catalogue->add($messages, $domain);
        return $catalogue;
    }
}

Как вы сами видите, создать собственный загрузчик не так уж и трудно. И мы легко можем его использовать вместо стандартного:

<?php
use Symfony\Component\Translation\Translator;
use RaulFraile\Loader\CustomLoader;
include_once __DIR__. '/vendor/autoload.php';
$translator = new Translator('es_ES');
$translator->addLoader('custom', new CustomLoader());
$translator->addResource('custom', __DIR__.'/translations/messages.txt', 'es_ES');
var_dump($translator->trans('hello'));

Вывод:

string(4) "Hola"

В этом и заключается хорошо спроектированное программное обеспечение, расширение функционала сравнимо с детской игрой.

Так же можно легко создать экспорт для любого формата. Для этого необходимо создать класс реализирующий интерфейс DumperInterface. Чтобы упростить эспорт в файл достаточно расширить класс FileDumper.

<?php
namespace RaulFraile\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Dumper\FileDumper;
class CustomDumper extends FileDumper
{
    public function format(MessageCatalogue $messages, $domain = 'messages')
    {
        $output = '';
        foreach ($messages->all($domain) as $source => $target) {
            $output .= sprintf("(%s)(%s)\n", $source, $target);
        }
        return $output;
    }
    protected function getExtension()
    {
        return 'txt';
    }
}

Метод format генерирует выходные данные, а метод dump сохраняет их в файл. Такой класс можно использовать в качестве стандартного экспорта. В следующем примере мы экспортируем данные из формата YAML в только что придуманный нами формат.

<?php
use Symfony\Component\Translation\Loader\YamlFileLoader;
use RaulFraile\Dumper\CustomDumper;
include_once __DIR__. '/vendor/autoload.php';
$loader = new YamlFileLoader();
$catalogue = $loader->load(__DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');
$dumper = new CustomDumper();
$dumper->dump($catalogue, array('path' => __DIR__.'/dumps'));

Заключение

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