翻译

3.3 版本
维护中的版本

“国际化”(internationalization,常被简写为i18n),是指将字符串和其他一些具有区域特征的片段从你的程序中提取(abstract)出来并基于用户所在区域(比如语言、国家)而将其置于一个能被翻译和转化的层的过程。对于文本来说(text),这意味着用一个能够把它(或“信息”)翻译成用户所需语言的函数,来剥离文本的每一部分:

1
2
3
4
5
// 文本始终以英语输出
echo 'Hello World';
 
// 文本将以用户指定语言或默认英语输出
echo $translator->trans('Hello World');

locale的意思,单纯来说就是用户的语言和国家。在程序中它可以是任何一个字符串,用来管理翻译(translation)和其他格式信息(比如币种)。推荐使用以ISO 639-1语言代码,加一个下划线(_),再跟一个ISO 3166-1 alpha-2国家代码(比如fr_FR这个locale是指“法国法语”French/France)。

在本章,你将学习如何使用Symfony框架中的translation组件。你可以阅读翻译组件来了解更多。整体上,翻译的过程有如下几步:

  1. 开启和配置Symfony的翻译服务;

  2. 将字符串抽象出来(如“xxxx”),这是通过调用Translator去剥离它们来实现的 ;(参考 翻译基础)

  3. 针对每个被支持的locale,创建翻译资源/文件,用于翻译程序中每一个待译字串;

  4. 针对request(请求)和可选的 基于用户整个session过程,来 确定、设置和管理用户的locale信息

配置 

翻译的过程是通过translator服务来处理的,该服务使用用户指定的locale来查找并返回翻译过的信息。使用translator之前,在配置文件中开启它:

1
2
3
# app/config/config.yml
framework:
translator: { fallbacks: [en] }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:framework="http://symfony.com/schema/dic/symfony"
           xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
 
    <framework:config>
        <framework:translator>
            <framework:fallback>en</framework:fallback>
        </framework:translator>
    </framework:config>
</container>
1
2
3
4
// app/config/config.php
$container->loadFromExtension('framework', array(
'translator' => array('fallbacks' => array('en')),
));

关于 fallbacks关键字 以及Symfony在找不到翻译语种时如何处理,请参考 翻译时的Locales回滚 以了解细节。

翻译时要用到的locale信息被存于request对象中。一般在路由中被设为 _locale 属性。请参考 The Locale and the URL

翻译基础 

translator 服务负责完成对文本的翻译。为了翻译一个文本块(被称为“message”,以下称作“信息”),使用trans()方法。例如,你要在controller中翻译一个简单的信息:

1
2
3
4
5
6
7
8
9
// ...
use Symfony\Component\HttpFoundation\Response;
 
public function indexAction()
{
$translated = $this->get('translator')->trans('Symfony is great');
 
return new Response($translated);
}

上述代码被执行后,Symfony就尝试基于用户的 locale 来翻译 “Symfony is great”信息。为了让这个过程实现,你需要告诉Symfony如何通过一个“翻译源(translation resource)”来执行翻译。一般来说翻译源是一个文件,包含有成组的翻译信息,对应某一指定的locale。它就像个翻译时的“字典”,可被创建为多种格式,但推荐使用XLIFF(译注:就是后缀不同的xml格式):

1
2
3
4
5
6
7
8
9
10
11
12
<!-- messages.fr.xlf -->
<?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>
        </body>
    </file>
</xliff>
1
2
# messages.fr.yml
Symfony is great: J'aime Symfony
1
2
3
4
// messages.fr.php
return array(
'Symfony is great' => 'J\'aime Symfony',
);

关于这类文件的存放位置等信息,参考 翻译源/文件名与位置

现在,如果用户的locale是法语(比如 fr_FRfr_BE ),那么前面的信息将被翻译为 J'aime Symfony。你也可以在 模板(templates) 中完成翻译。

翻译的处理过程 

为了能翻译一条信息,Symfony执行以下简明流程:

  • 先确定request对象中所存储的当前用户的 locale 信息;

  • 某一目录(一大组message)的已经翻译好的信息,将从由 locale(比如 fr_FR )所决定的翻译源加载。如果locale不存在,则由 fallback locale 所决定的翻译信息,也将被加载并且合并到目录之中。最终结果就是生成了一个“翻译大词典”;

  • 如果能够从目录中找到待翻信息,翻译结果将被返回。否则, translator返回原始信息。

当使用trans()方法时,Symfony从对应的信息目录中,寻找准确的字符串,然后返回它(如果存在的话)。

译注:message catalog

此处的目录,原文是catalog,是指Symfony在处理“翻译”的过程中,从翻译源提取出来的已经翻译好的待用信息集合,也就是一个包含了很多条message的xliff文件。参考后面“翻译源/文件的命名和位置”中的domain部分)

信息占位符 

有时,一条包含变量的信息,需要被翻译:

1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpFoundation\Response;
 
public function indexAction($name)
{
$translated = $this->get('translator')->trans('Hello '.$name);
 
return new Response($translated);
}
 

可是,对于这样一个字符串,创建相应的翻译是不可能的,因为translator始终在尝试寻找“确定信息”,包括变量值本身(比如“Hello Ryan”和“Hello Fabien”在translator看来是两条不同的信息)。

对于这种情形,请参考组件文档中的 信息占位符/Message Placeholders。同样情况在模板中的处理方法,参考 Twig Templates

复数处理 

另一个复杂场面,则是你在翻译中要面对基于某些变量的“复数状况”:

1
2
There is one apple.
There are 5 apples.

要处理这个,应使用 transChoice() 方法,或者用模板中的transchoice标签/调节器,请 参考这里

更多信息,参考Translation组件文档的 复数处理章节

模板中的翻译 

多数情况下,翻译发生在模板中,对于Twig和PHP模板,Symfony提供了原生支持。

Twig模板 

Symfony提供了特殊的Twig标签(transtranschoice),用于对“静态文本块”信息提供翻译帮助。

1
2
3
4
5
{% 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%变量,并将其传给translator。这种机制,只在你使用%var%这种格式的占位符时生效。

在Twig模板中使用 trans/transchoice 标签进行翻译时,%var% 占位符注释是必须要提供的。

如果你需要在字符串中使用百分号%,要写两次来为它转义:

1
{% trans %}Percent: %percent%%%{% endtrans %}

你也可以指定信息域(message domain),并传递一些附加的变量进来:

1
2
3
4
5
6
7
{% 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} %name%, there are no apples|{1} %name%, there is one apple|]1,Inf[ %name%, there are %count% apples
{% endtranschoice %}

trans和transchoice调节器,可用于翻译变量文本和复杂表达式:

1
2
3
4
5
6
7
{{ message|trans }}
 
{{ message|transchoice(5) }}
 
{{ message|trans({'%name%': 'Fabien'}, "app") }}
 
{{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }}

在翻译时无论使用标签还是调节器,效果是相同的,但有一个微小区别:自动输出转义功能仅对调节器有效。换言之,如果你需要翻译出来的信息“不被转义”,则必须在translation调节器后面再跟一个raw调节器:

1
2
3
4
5
6
7
8
9
10
{# 标签中被翻译的文本从不被转义#}
{% trans %}
<h3>foo</h3>
{% endtrans %}
 
{% set message = '<h3>foo</h3>' %}
 
{# 变量调节器翻译的字符串和变量,默认将被转义 #}
{{ message|trans|raw }}
{{ '<h3>bar</h3>'|trans|raw }}

你可以通过单一标签,为整个twig模板设置一个翻译域(translation domain):

1
{% trans_default_domain "app" %}

注意这时仅会影响到当前模板,而不包括任何“被包容(included)”的模板(为的是减少副作用)。

PHP模板 

translator也可以在PHP模板中通过translator helper来使用:

1
2
3
4
5
6
7
<?php echo $view['translator']->trans('Symfony 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)
) ?>

翻译源/文件的命名和位置 

Symfony在以下位置寻找信息文件(即是translations/翻译信息):

  • app/Resources/translations 目录;

  • app/Resources/<bundle name>/translations 目录;

  • 任何bundle下的 Resources/translations/ 目录

上面的位置是按照“高优先权在前”的顺序排列的。这意味着,你可以用前面两个目录之一,来覆写某个bundle中的翻译信息。

覆写机制基于键等级(key level)而执行:只有被覆写的键需要被列在高优先级的信息文件中。当一个键没有在信息文件中被找到时,translator将自动回滚到低优先级的信息文件中。

信息文件的文件名也很重要,每一个信息文件必须按下列命名路径来命名:domain.locale.loader:

  • domain: 这是一个可选项,用于组织信息文件成为群组(例如admin, navigation 或default messages)。参阅 使用翻译信息的域 Using Message Domains

  • locale: 这是翻译信息的locale (例如 en_GB, en, 等等);

  • loader: 这是Symfony如何来加载和解析信息文件 (也就是 xlf, php, yml 等文件后缀).

加载器(loader)可以是任何已注册加载器的名称。Symfony默认提供了许多加载器,包括:

  • xlf: 加载XLIFF文件;

  • php: 加载PHP文件;

  • yml: 加载YAML文件;

使用何种加载器的选择权完全在你,随你喜好。推荐使用xlf作为翻译的信息文件。更多选择,参考 加载信息目录 Loading Message Catalogs

你也可以将翻译信息存在数据库中,或任何其他介质,只要提供一个自定义的类去实现 LoaderInterface 接口即可。参考 translation.loader 标签,以了解更多。

你可以在配置文件中通过paths选项添加一个目录:

1
2
3
4
5
# app/config/config.yml
framework:
translator:
paths:
- '%kernel.root_dir%/../translations'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:framework="http://symfony.com/schema/dic/symfony"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-Instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services
    http://symfony.com/schema/dic/services/services-1.0.xsd
    http://symfony.com/schema/dic/symfony
    http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
>
 
    <framework:config>
        <framework:translator>
            <framework:path>%kernel.root_dir%/../translations</framework:path>
        </framework:translator>
    </framework:config>
</container>
1
2
3
4
5
6
7
8
// app/config/config.php
$container->loadFromExtension('framework', array(
'translator' => array(
'paths' => array(
'%kernel.root_dir%/../translations',
),
),
));

每次你创建一个新的翻译源(translation resource)或安装了一个包含翻译源的bundle时,一定要清除缓存,这样Symfony才能发现这个新的翻译源。

1
$  php app/console cache:clear

翻译时Locale的回滚 

假设一名用户的locale信息是 fr_FR,而你正翻译的键是 Symfony is great。为了找到法语信息,Symfony切实地检查若干locale的翻译源:

  1. 首先,Symfony在一个 fr_FR 的翻译源(例如messages.fr_FR.xlf)寻找翻译信息;

  2. 如果没找到,Symfony在一个 fr 翻译源(比如messages.fr.xlf)继续寻找翻译信息;

  3. 如果仍然没找到,Symfony使用fallbacks这个配置参数, 它被默认设为 en (参阅 FrameworkBundle配置信息)。

2.6版Symfony引入了将缺少的翻译信息写入日志的能力。

当Symfony无法找到给定locale的翻译信息时,它会把缺少的翻译信息给添加到日志文件中。参阅 logging

翻译数据库内容 

翻译数据库内容时要用到Doctrine扩展中的 Translatable ExtensionTranslatable Behavior(PHP 5.4+)。更多信息请参考相应文档。

译注:使用Doctrine扩展有两种方式

一个是类库方式,一个是bundle方式。此处的Translatable Behavior,是指用bundle安装之后,翻译数据库内容时所应参考的用法)

翻译约束信息 

参考 如何翻译验证约束消息 以了解更多。

处理用户的Locale 

翻译的过程是取决于用户的locale的。阅读 如何操作用户的Locale 以了解如何处理。

对翻译进行调试 

debug:translation 命令行语句从Symfony 2.5起引入。 Symfony 2.6之前,这一命令是translation:debug。

当你在不同语言的大量翻译信息中操作时,要跟踪到丢失了哪条信息以及哪条信息没有被使用,是很困难的。阅读 如何找到丢失或未使用的翻译信息 以了解如何发现这一类翻译信息。

总结 

使用Symfony的翻译组件创建一个国际化的应用程序将不再是一个“痛苦过程”,而是归结于以下简单步骤:

  • 从程序中抽象出待翻译的信息,把每一条信息用 trans()transChoice() 方法替换(通过 使用Translator一文了解更多);

  • 通过创建信息文件(translation message file)将待译信息翻译成多个locale语种。Symfony能够找到并处理每一个文件,因为这些文件的名字遵循指定的命名约定;

  • 管理好用户的locale,它可以存在request中,但是也可以存在用户的session中。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)