Использование Translator

Представьте, что вы хотите перевесит строку "Symfony прекрасен" на французский:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\ArrayLoader;

$translator = new Translator('fr_FR');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array(
    'Symfony is great!' => 'Symfony est super !',
), 'fr_FR');

var_dump($translator->trans('Symfony is great!'));

В этом примере, сообщение "Symfony прекрасен!" будет переведено на локаль, установленную в конструкторе (fr_FR), если сообщение существует в одном из каталогов сообщений.

Заполнители сообщений

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

1
2
3
4
// ...
$translated = $translator->trans('Hello '.$name);

var_dump($translated);

Однако, создание перевода для этой строки невозможно, так как переводчик будет пытаться найти точное сообщение, включая его переменные части (например, "Привет, Райан" или "Привет, Фабиен"). Вместо того, чтобы писать перевод для каждой возможной итерации переменной $name, вы можете замениь переменную "заполнителем":

1
2
3
4
5
6
7
// ...
$translated = $translator->trans(
    'Hello %name%',
    array('%name%' => $name)
);

var_dump($translated);

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

  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="1">
                    <source>Hello %name%</source>
                    <target>Bonjour %name%</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • PHP
    1
    2
    3
    return array(
        'Привет %name%' => 'Bonjour %name%',
    );
    
  • YAML
    1
    'Привет %name%': Bonjour %name%
    

Note

Заполнители могут принимать любую форму, так как полное сообщение реконструируется используя PHP strtr function. Однако рекомендуется форма %...% для избежания проблем при использовании Twig.

Как вы видели, создание перевода - это двухшаговый процесс:

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

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

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

Создание файлов перевода - это важная часть "локализации" (часто используемая аббревиатура L10n). Файлы переводов состоят из серии пар id - перевод для заданного домена и локали. Источник является идентификатором для индивидуального перевода, и может быть сообщением в главной локали вашего приложения (например, "Symfony прекрасен"), или уникальным идентификатором (например, symfony.great - см. сноску ниже).

Файлы перевода могут быть созданы в нескольких разных форматах, в то время как XLIFF является рекомендуемым. Эти файлы анализирются одним из классов загрузчика.

  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="symfony_is_great">
                    <source>Symfony is great</source>
                    <target>J'aime Symfony</target>
                </trans-unit>
                <trans-unit id="symfony.great">
                    <source>symfony.great</source>
                    <target>J'aime Symfony</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • YAML
    1
    2
    Symfony is great: J'aime Symfony
    symfony.great:    J'aime Symfony
    
  • PHP
    1
    2
    3
    4
    return array(
        'Symfony is great' => 'J\'aime Symfony',
        'symfony.great'    => 'J\'aime Symfony',
    );
    

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

1
2
3
$translator->trans('Symfony is great');

$translator->trans('symfony.great');

В первом методе сообщения написаны на языке локали по умолчанию (в этом случае, на английском). Это сообщение потом используется как "id" при создании переводов.

Во втором методе сообщения на самом деле являются "ключевыми словами", которые передают идею сообщения. Сообщение с ключевым словом потом используется как "id" для любых переводов. В этом случае, переводы должны быть сделаны для локали по умолчанию (т.е., чтобы перевести symfony.great в Symfony is great).

Второй метод удобен, так как ключ сообщения не нужно менять в каждом файле переводов, если вы решите, что сообщение должно выглядеть как "Symfony is really great" (Symfony действительно прекрасен) в локали по умолчанию.

Какой метод использовать - зависит только от вас, но формат "ключевых слов" часто рекомендуется.

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    symfony:
        is:
            great: Symfony is great
            amazing: Symfony is amazing
        has:
            bundles: Symfony has bundles
    user:
        login: Login
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    array(
        'symfony' => array(
            'is' => array(
                'great'   => 'Symfony is great',
                'amazing' => 'Symfony is amazing',
            ),
            'has' => array(
                'bundles' => 'Symfony has bundles',
            ),
        ),
        'user' => array(
            'login' => 'Login',
        ),
    );
    

Множественные уровни объединяются в единственные пары id / перевод, путём добавления точки (.) между каждым уровнем, поэтому примеры выше эквивалентны следующему:

  • YAML
    1
    2
    3
    4
    symfony.is.great: Symfony is great
    symfony.is.amazing: Symfony is amazing
    symfony.has.bundles: Symfony has bundles
    user.login: Login
    
  • PHP
    1
    2
    3
    4
    5
    6
    return array(
        'symfony.is.great'    => 'Symfony is great',
        'symfony.is.amazing'  => 'Symfony is amazing',
        'symfony.has.bundles' => 'Symfony has bundles',
        'user.login'          => 'Login',
    );
    

Плюрализация

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

1
2
3
4
5
6
7
8
9
(($number % 10 == 1) && ($number % 100 != 11))
    ? 0
    : ((($number % 10 >= 2)
        && ($number % 10 <= 4)
        && (($number % 100 < 10)
        || ($number % 100 >= 20)))
            ? 1
            : 2
);

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

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

1
'Есть одно яблоко|Есть %count% яблок'

Чтобы переводить сообщения с множественным числом, используйте метод transChoice():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// заполнитель %count% приписывается второму аргументу...
$translator->transChoice(
    'Есть одно яблоко|Есть %count% яблок',
    10
);

// ...но вы можете определить больше заполнителей по необходимости
    $translator->transChoice(
    'Поспеши, %name%! Осталось одно яблоко.|Осталось %count% яблок.',
    10,
    // здесь нет необходимости добавлять %count%; Symfony делает это за вас
    array('%name%' => $user->getName())
);

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

Основываясь на заданном числе, перевод выбирает правильную множественную форму. В английском, большинство слов имеют форму единственного числа, когда есть один объект, и множественную форму со всеми другими числами (0, 2, 3...). Поэтому, если count - 1, переводчик будет использовать первую строку (Есть одно яблоко) в качестве перевода. В других случаях он будет использовать Есть %count% яблок.

Вот французский перевод:

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

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

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

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

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

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

Tip

Так как теги опциональны, переводчик не использует их (переводчик просто получит строку, основанную на его положении в строке).

Ясная интервальная плюрализация

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

1
'{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. Строка сверху указывает 4 разных интервала: ровно 0, ровно 1, 2-19, и 20 и больше.

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

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

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

Interval может представлять ограниченный набор цифр:

1
{1,2,3,4}

Или числа между двумя другими числами:

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$translator->trans(
    'Symfony is great',
    array(),
    'messages',
    'fr_FR'
);

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

Note

Начиная с Symfony 3.2, третий аргумент transChoice() опционален, если единиственный используемый заполнитель - %count%. В предыдущих версиях Symfony вам всегда нужно было его определять:

1
2
3
4
5
6
7
$translator->transChoice(
    '{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
    10,
    array('%count%' => 10),
    'messages',
    'fr_FR'
);

Извлечение каталога сообщений

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

1
2
3
4
5
$catalogue = $translator->getCatalogue('fr_FR');
$messages = $catalogue->all();
while ($catalogue = $catalogue->getFallbackCatalogue()) {
    $messages = array_replace_recursive($catalogue->all(), $messages);
}

Переменная $messages теперь будет иметь следующую структуру:

1
2
3
4
5
6
7
8
9
array(
    'messages' => array(
        'Hello world' => 'Bonjour tout le monde',
    ),
    'validators' => array(
        'Value should not be empty' => 'Valeur ne doit pas être vide',
        'Value is too long' => 'Valeur est trop long',
    ),
);

Добавление заметок к содержанию перевода

Иногда переводчикам требуется дополнительный контекст, чтобы решить, как лучше перевести некоторое содержание. Такой контекст можно предоставить в виде заметок, которые являются набором комментариев, используемых для хранения читаемой информации конечного пользователя. Единственный формат, поддерживающий загрузку и сброс заметок - XLIFF версии 2.0.

Если документ XLIFF 2.0 содержит узлы <notes>, то они автоматически загружаются или сбрасываются при использовании этого компонента внутри приложения Symfony:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0"
       srcLang="fr-FR" trgLang="en-US">
  <file id="messages.en_US">
    <unit id="LCa0a2j" name="original-content">
      <notes>
        <note category="state">new</note>
        <note category="approved">true</note>
        <note category="section" priority="1">user login</note>
      </notes>
      <segment>
        <source>original-content</source>
        <target>translated-content</target>
      </segment>
    </unit>
  </file>
</xliff>

При использовании компонента Translation отдельно, вызовите метод каталога setMetadata() и передайте заметки в качестве массивов. Вот, к примеру, код, необходимый для генерирования предудыщего XLIFF-файла:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Dumper\XliffFileDumper;

$catalogue = new MessageCatalogue('en_US');
$catalogue->add([
    'original-content' => 'translated-content',
]);
$catalogue->setMetadata('original-content', ['notes' => [
    ['category' => 'state', 'content' => 'new'],
    ['category' => 'approved', 'content' => 'true'],
    ['category' => 'section', 'content' => 'user login', 'priority' => '1'],
]]);

$dumper = new XliffFileDumper();
$dumper->formatCatalogue($catalogue, 'messages', [
    'default_locale' => 'fr_FR',
    'xliff_version' => '2.0'
]);

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.