Переводы

Термин “интернационализация” отсылает нас к процессу извлечения строк текста и прочих специфичных для конкретной локали объектов из вашего приложения и перемещения их на некоторый уровень абстракции, где эти элементы могут быть переведены и конвертированы на основании локали пользователя (т.е. в зависимости от языка и страны). Для текста это означает, что его надо передавать в специальную функцию, способную переводить текст (или некое “сообщение”) на язык пользователя:

// этот текст *всегда* будет отображаться на английском
echo 'Hello World';

// текст может быть переведён на язык конечного пользователя или же останется на английском
echo $translator->trans('Hello World');

Note

Термин локаль можно грубо определить как совокупность языка и страны пользователя. Это может быть любая строка, которую ваше приложение сможет использовать для управления переводами и прочими различиями в форматах (например, формат даты или валюты). Рекомендуется использовать стандарт ISO639-1 для языковых кодов, подчерк (_) и затем стандарт ISO3166 для кодов стран (например, получится fr_FR для French/France).

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

  1. Подключить и настроить компонент Symfony - Translation;
  2. Завернуть строки (т.н. “сообщения”) в вызовы Translator‘а;
  3. Создать ресурсы перевода для каждой поддерживаемой локали и после перевести все сообщения в приложении;
  4. Определить, установить и управлять локалью пользователя при помощи сессии.

Настройка

Переводы обрабатываются сервисом (service) Translator, который использует локаль пользователя для поиска и отображения переведённого сообщения. Перед тем как его использовать, подключите Translator в файле конфигурации:

Опция fallback определяет локаль для отката, когда перевод не существует для локали пользователя.

Tip

Когда перевод для локали не существует, переводчик пытается сначала найти перевод для языка (fr если локаль fr_FR, например). Если это также не удаётся, он ищет перевод, используя локаль отката.

Локаль используемая при переводе хранится в сессии пользователя.

Основы переводов

Перевод текста осуществляется сервисом translator (Symfony\Component\Translation\Translator). Для перевода текстового блока (называемого “сообщением”) используйте метод :method:`Symfony\\Component\\Translation\\Translator::trans`. Предположим, например, что вы переводите простое сообщение внутри контроллера:

<?php
// ...
public function indexAction()
{
    $t = $this->get('translator')->trans('Symfony2 is great');

    return new Response($t);
}

При выполнении этого кода, Symfony2 попытается перевести сообщение “Symfony2 is great”, основываясь на локали пользователя. Для этого необходимо указать Symfony2 как необходимо перевести это сообщение при помощи “ресурса для перевода”, который представляет собой набор переведённых сообщений для нужной локали. Этот “словарь” переводов может быть создан в нескольких различных форматах, рекомендуемым же является XLIFF формат:

Теперь, если локалью пользователя будет Французская (например, fr_FR или fr_BE), это сообщение будет переведено как J'aime Symfony2.

Процесс перевода

Для того чтобы перевести сообщение, Symfony2 использует простой процесс:

  • Определяется локаль текущего пользователя, которая хранится в сессии;
  • Загружается каталог переводов сообщений из соответствующего ресурса, определяемого локалью (например, fr_FR), сообщения, соответствующие локали отката (fallback), также загружаются и добавляются к каталогу, если он ещё не загружен. В конечном итоге получается большой “словарь” с переводами. См. также Каталоги сообщений.
  • Если сообщение есть в каталоге, возвращается его перевод. Если же нет, переводчик возвращает оригинал сообщения.

При использовании метода trans() Symfony2 ищет строку целиком в подходящем каталоге и возвращает его (если есть что возвращать).

Заполнители в сообщениях

Иногда, сообщение, которое нужно перевести, содержит переменную:

<?php
// ...
public function indexAction($name)
{
    $t = $this->get('translator')->trans('Hello '.$name);

    return new Response($t);
}

Тем не менее, создание перевода для этой строки невозможно, так как переводчик будет искать строку целиком, включая переменную (например, “Hello Ryan” или “Hello Fabien”). Вместо того, чтобы писать переводы для каждого возможного значения переменной $name, мы можем заменить переменную “заполнителем” (aka “placeholder”):

<?php
// ...
public function indexAction($name)
{
    $t = $this->get('translator')->trans('Hello %name%', array('%name%' => $name));

    new Response($t);
}

Symfony2 теперь будет искать перевод оригинала с заполнителем (Hello %name%) и лишь затем заменять заполнитель его реальным значением. Создание перевода не будет от того, что вы делали ранее:

Note

Заполнители могут иметь любую форму, так как полное сообщение восстанавливается с использованием PHP-функции strtr function. Тем не менее, нотация %var% необходима для использовании шаблонов Twig и, в конечном итоге, более читабельна.

Как вы могли видеть, процесс создания перевода состоит из двух шагов:

  1. Извлечение сообщения, которое нужно перевести, передав его в Translator.
  2. Создание перевода сообщения для каждой локали, которую вы собираетесь поддерживать.

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

Каталоги сообщений

Когда сообщение переводится, Symfony2 собирает каталог сообщений для локали пользователя и ищет в нём его перевод. Каталог сообщений схож со словарём переводов для некоторой локали. Например, каталог для локали fr_FR может содержать такой перевод:

Symfony2 is Great => J’aime Symfony2

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

Tip

Каждый раз, когда вы создаёте новый ресурс переводов (или устанавливаете пакет, который включает переводы), убедитесь, что вы очистили кэш, чтобы Symfony смог найти новые ресурсы для перевода:

php app/console cache:clear

Переводы: расположение в проекте и соглашения по именованию

Symfony2 ищет файлы сообщений (т.е. переводы) в двух местах:

  • Для сообщений внутри пакета, файлы сообщений должны быть распложены в директории Resources/translations/;
  • Для переопределений переводов любого пакета, разместите файлы в директории app/Resources/translations.

Наименование файлов переводов также важно, так как Symfony2 использует соглашение по определению деталей перевода. Каждый файл сообщений должен быть назван в соответствии со следующим шаблоном: domain.locale.loader:

  • domain: Не обязательный путь для структурирования сообщений в группы (например, admin, navigation или же по умолчанию messages) - см. Использование доменов сообщений
  • locale: Локаль, которой соответствует перевод (например, en_GB, en, и т.д.);
  • loader: Как Symfony2 должен загрузить и парсить файл (например, xliff, php или yml).

Loader может быть наименованием любого зарегистрированного загрузчика. По умолчанию в Symfony представлены следующие загрузчики:

  • xliff: XLIFF файл;
  • php: PHP файл;
  • yml: YAML файл.

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

Note

Вы также можете хранить переводы в базе данных, или любом другом хранилище при помощи вашего собственного класса, реализующего интерфейс Symfony\Component\Translation\Loader\LoaderInterface. См. статью в книге рецептов: Пользовательские загрузчики переводов.

Создание переводов

Каждый файл содержит набор пар “id-translation” для заданного домена и локали. Id - это идентификатор единичного перевода и может быть как сообщением на языка базовой локали (например, “Symfony is great”) или же некоторым уникальным идентификатором (например, “symfony2.great” - ниже мы ещё скажем об этом пару слов):

Symfony2 будет находить эти файлы и использовать их при переводе как “Symfony2 is great”, так и “symfony2.great” при использовании французской локали (fr_FR или fr_BE).

Использование доменов сообщений

Как вы уже видели, файлы сообщений структурированы по различным локалям, которым соответствуют их переводы. Файлы сообщений могут быть также структурированы по “доменам”. При создании файлов сообщений, домен - это первая часть имени файла. Домен по умолчанию - messages. Например, предположим, что для лучшей организации файлов переводов они были разделены на три различные домена: messages, admin и navigation. Для французского перевода были созданы следующие файлы сообщений:

  • messages.fr.xliff
  • admin.fr.xliff
  • navigation.fr.xliff

Когда переводится строка не из домена по умолчанию (messages), вы явно должны указать домен третьим аргументом функции trans():

$this->get('translator')->trans('Symfony2 is great', array(), 'admin');

Symfony2 будет теперь искать сообщение в домене admin, соответствующем локали пользователя.

Работа с локалью пользователя

Локаль текущего пользователя хранится в сессии и доступна при помощи сервиса session:

$locale = $this->get('request')->getLocale();

$this->get('request')->setLocale('en_US');

Также возможно хранить локаль в сессии:

$this->get('session')->set('_locale', 'en_US');

Локаль по умолчанию и Локаль для отката

Если локаль в сессии явно не указана, Translator будет использовать параметр fallback_locale. По умолчанию этот параметр установлен в en (см. Настройка).

В качестве альтернативы, вы можете гарантировать, что локаль будет установлена в сессии, если определите параметр default_locale для сервиса сессии:

New in version 2.1: Параметр default_locale был ранее определён в сессии, но начиная с версии 2.1 он был перемещён. Это вызвано тем, что локаль теперь устанавливается в запросе, а не в сессии.

Локаль и URL

Так как локаль пользователя хранится в сессии, возможно вам захочется использовать один и тот же URL для отображения ресурса на любых других языках, основываясь на локали пользователя. Например, http://www.example.com/contact будет отображать контент на английском для одного пользователя, на французском для другого пользователя. К сожалению, это нарушает основополагающее правило Web: каждый URL должен возвращать один и тот же ресурс вне зависимости от пользователя. Для того чтобы усугубить проблему, задумайтесь - какую версию контента должна будет индексироваться поисковиками?

Наилучшим решением является включение локали в URL. Этот метод полностью поддерживается системой маршрутизации при помощи специального параметра _locale:

При использовании в маршруте параметра _locale, соответствующая локаль будет автоматически установлена в пользовательской сессии. Другими словами, если пользователь посещает URI /fr/contact, локаль fr будет автоматически установлена для пользователя в сессии.

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

Множественное число для сообщений

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

(($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);

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

Когда перевод имеет различные формы из-за множественного числа, вы можете предоставить все формы в качестве строки, разделённой вертикальной чертой (|):

'There is one apple|There are %count% apples'

Для того чтобы переводить сообщения с учётом множественного числа, используйте метод:method:Symfony\Component\Translation\Translator::transChoice:

<?php
// ...
$t = $this->get('translator')->transChoice(
    'There is one apple|There are %count% apples',
    10,
    array('%count%' => 10)
);

Второй аргумент (10 в данном примере), это число объектов, которое будет использоваться для определения какой именно перевод будет использован, а также будет замещать %count%.

Основываясь на этом числе, переводчик выберет правильную форму множественного числа. В английском языке, слова в основном имеют форму единственного числа, когда имеется один объект и форму множественного числа для любого другого числа (0, 1, 2...). Итак, если count будет 1, переводчик будет использовать первую строку (There is one apple) в качестве перевода. В противном случае, он будет использовать There are %count% apples.

Французский перевод будет таким:

'Il y a %count% pomme|Il y a %count% pommes'

Даже если эти строки выглядят похожим образом (состоят из двух подстрок, разделённых вертикальной чертой), французское правило отличается: первая форма (единственное число) используется если count равен 0 или 1. Таким образом, переводчик автоматически будет использовать первую строку (Il y a %count% pomme), когда count будет равен 0 или 1.

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

'one: There is one apple|some: There are %count% apples'

'none_or_one: Il y a %count% pomme|some: Il y a %count% pommes'

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

Подробнее о множественности (интервальный метод)

Наиболее простой путь создания множественного числа для сообщения в Symfony2 - использовать встроенную логику для выбора строки на основе данного номера. Иногда вам может потребоваться более полный контроль над переводом множественных чисел или же в особых случаях требуется не стандартный перевод (для числа 0 или же для отрицательных чисел, к примеру). Для таких случаев вы можете использовать интервалы:

'{0} There are no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf] There are many apples'

Эти интервалы следуют нотации ISO 31-11. Строка выше определяет четыре различных интервала: точно 0, точно 1, 2-19, а также 20 и более.

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

'{0} There are no apples|[20,Inf] There are many apples|There is one apple|a_few: There are %count% apples'

Например, для одного яблока будет использовано стандартное правило There is one apple. Для 2-19 - будет использовано второе стандартное правило There are %count% apples.

Класс Symfony\Component\Translation\Interval может представлять конечный набор чисел:

{1,2,3,4}

Или же число в интервале между двумя числами:

[1, +Inf[
]-1,2[

Левая часть разделителя может быть [ (включая) или ] (исключая). Правая часть может быть [ (исключая) or ] (включая). Для бесконечности вы можете использовать -Inf и +Inf.

Переводы в шаблонах

Основную часть времени, переводы появляются в шаблонах. Symfony2 предоставляет поддержку переводов как для Twig так и для PHP шаблонов.

Twig шаблоны

Symfony2 предоставляет специализированные таги для Twig (trans и transchoice) для того чтобы помочь с переводом статических блоков текста:

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

{% transchoice count %}
    {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}

Таг transchoice автоматически получает переменную %count% из контекста и передаёт её переводчику. Этот механизм работает лишь когда вы используете заполнитель в стиле %var%.

Tip

Если вам нужно использовать символ процента (%) в строке, экранируйте его при помощи дублирования: {% trans %}Percent: %percent%%%{% endtrans %}

Вы также можете указать домен для сообщений и передать некоторые дополнительные переменные:

{% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %}

{% trans with {'%name%': 'Fabien'} from "app" into "fr" %}Hello %name%{% endtrans %}

{% transchoice count with {'%name%': 'Fabien'} from "app" %}
    {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}

Фильтры trans и transchoice могут быть использованы для перевода текста переменных и сложных выражений:

{{ message | trans }}

{{ message | transchoice(5) }}

{{ message | trans({'%name%': 'Fabien'}, "app") }}

{{ message | transchoice(5, {'%name%': 'Fabien'}, 'app') }}

Tip

Использование тагов или фильтров для перевода имеет один и тот же эффект, но с одним небольшим отличием: автоматическое экранирование вывода применяется только к переменным, переведённым при помощи фильтра. Другими словами, если вам нужно быть уверенными, что ваша переменная не экранирована, вы должны применять фильтр raw после фильтра trans:

{# текст между тагами никогда не будет экранирован #}
{% trans %}
    <h3>foo</h3>
{% endtrans %}

{% set message = '<h3>foo</h3>' %}

{# переменная переведённая при помощи фильтра экранирована по умолчанию #}
{{ message | trans | raw }}

{# но статическая строка никогда не экранируется #}
{{ '<h3>foo</h3>' | trans }}

PHP Шаблоны

Сервис-переводчик доступен в PHP шаблонах при помощи хелпера translator:

<?php echo $view['translator']->trans('Symfony2 is great') ?>

<?php echo $view['translator']->transChoice(
    '{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
    10,
    array('%count%' => 10)
) ?>

Форсирование локали переводчика

Когда переводится сообщение, Symfony2 использует локаль из сессии пользователя или же fallback локаль, если требуется. Вы также можете вручную указать локаль для перевода:

$this->get('translator')->trans(
    'Symfony2 is great',
    array(),
    'messages',
    'fr_FR',
);

$this->get('translator')->trans(
    '{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
    10,
    array('%count%' => 10),
    'messages',
    'fr_FR',
);

Перевод контента из базы данных

Перевод контента из базы данных должен обрабатываться Doctrine при помощи Translatable Extension. Информацию об этой библиотеке вы можете найти в её документации.

Заключение

При помощи компонента Translation, создание интернациональных приложений больше не требует болезненного процесса и может быть достигнуто при помощи следующих шагов:

  • Извлеките сообщения вашего приложения, завернув каждое в методы :method:`Symfony\\Component\\Translation\\Translator::trans` или :method:`Symfony\\Component\\Translation\\Translator::transChoice`;
  • Переведите каждое сообщение для различных локалей, создав файлы переводов. Symfony2 найдёт и обработает каждый файл так как их имена следуют специфическим соглашениям;
  • Управляйте локалью пользователя, которая хранится в сессии.