所谓“国际化”是指抽象字符串的过程,它从你的应用程序中取出其它特定的语言环境部分并放入一个可以基于用户所在语言环境(如语言和国家)将其翻译和转换的层。对于文本,这意味着可以通过将文本(或“消息”)翻译成用户语言的函数来进行封装:
- // 文本将*总是*用英文输出
- echo 'Hello World';
- // 文本可以被翻译成最终用户的语言,缺省是英文
- echo $translator->trans('Hello World');
所谓语言环境基本上是指用户语言和国家。它可以是字符串,然后你的应用程序可以用它来管理翻译和其它不同的格式(如货币格式)。我们建议使用ISO639-1语言代码,加上一个下划线(_),然后再加上ISO3166国家代码(如:fr_FR对应法语/法国)。
在本章,我们将学会如何让应用程序支持多语言环境,然后如何为多语言环境创建翻译。总得来说,这个过程通常有以下几步:
翻译通过Translator服务来处理,该服务使用用户的语言环境去查找并返回翻译消息。在使用它之前,在你的配置文件中启动翻译:
- # app/config/config.yml
- framework:
- translator: { fallback: en }
fallback选项定义一个回退语言环境,这样当用户语言环境对应的翻译不存在时使用该回退语言环境。
当一个语言环境的翻译不存在时,翻译器首先尝试找针对语言的翻译(如语言环境是fr_FR的话,就是fr)。如果这也失败的话,它查找使用回退语言环境的翻译。
在翻译中使用的语言环境被保存在用户会话(Session)中。
文本的翻译是通过翻译服务(Translator)来实现的。要翻译一段文本(被称为消息),使用trans()方法。举个例子,假设我们打算从内部控制器中翻译一段简单的消息:
- public function indexAction()
- {
- $t = $this->get('translator')->trans('Symfony2 is great');
- return new Response($t);
- }
当代码执行时,Symfony2将尝试基于用户的语言环境去翻译"Symfony2 is greate"消息。为了代码能正常工作,我们需要告诉Symfony2如何通过“翻译资源”去翻译消息。翻译资源是指定语言环境的翻译消息集。翻译的“词典”可以通过几种不同的格式创建,XLIFF是推荐的格式:
- <!-- messages.fr.xliff -->
- <?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>Symfony2 is great</source>
- <target>J'aime Symfony2</target>
- </trans-unit>
- </body>
- </file>
- </xliff>
现在如果用户的语言环境语言是法语(如:fr_FR或fr_BE),那么该消息会被翻译成J'aime Symfony2。
要实际翻译消息,Symfony2使用一个简单的过程:
当使用trans()方法时,Symfony2在相应的消息目录中查找正确的字符串,并返回它(如果它存在的话)。
有时,消息包含一个需要被翻译的变量:
- public function indexAction($name)
- {
- $t = $this->get('translator')->trans('Hello '.$name);
- return new Response($t);
- }
然而,为该字符串创建翻译是不可能的,因为翻译器将尝试去查找正确的、包括变量部分的消息(如:"Hello Ryan" 或 "Hello Fabien"),我们可以给变量放置一个“占位符”,而不是为每个$name变量可能的值而重复地翻译:
- public function indexAction($name)
- {
- $t = $this->get('translator')->trans('Hello %name%', array('%name%' => $name));
- new Response($t);
- }
Symfony2现在可以查找消息(Hello %name%)的翻译,然后用变量的值来替换占位符了。在此之前创建翻译来实现:
- <!-- messages.fr.xliff -->
- <?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的strtr函数重构。然而,在Twig模板中翻译时%var%标记也被要求,总体上要遵循一个合理的约定。
正如我们所见,创建翻译有两步:
每二步是通过创建消息目录来实现的,该目录为不同的语言环境中定义翻译。
当消息被翻译时,Symfony2为用户语言环境编译一个消息目录,并在其中查找消息的翻译。消息目录就象特定语言环境的翻译词典一样。例如,语言环境 fr_FR 的目录可能包含以下翻译:
- Symfony2 is Great => J'aime Symfony2
创建这些翻译是国际化应用程序开发者(或翻译者)的责任。翻译被保存在文件系统中,并且被Symfony2发现,感谢这些约定。
每次你创建一个新的翻译资源(或安装一个包含翻译资源的Bundle)时,请确保清空你的缓存,以便Symfony2可以发现新的翻译资源:
- $ php app/console cache:clear
Symfony2在两个位置查找消息文件(如翻译):
翻译的文件名也是重要的,因为Symfony2可以根据约定来确定翻译的细节。每个消息文件必须按照domain.locale.loader这种模式命名:
loader可以是任何被注册的引导器。缺省状态下,Symfony2提供以下引导器:
选择使用哪个引导器完全是你个人的爱好。
你也可以将翻译保存在数据库中,或者其它被实现LoaderInterface接口的自定义类提供的其它存储中。参见下面的自定义翻译引导器一节,学会如何注册自定义引导器。
每个给定domain和locale的文件由一系统基于id的翻译对组成。id是单个翻译的标识,可以是你应用程序主语言环境中的消息(如:"Symfony is great"),也可以是唯一的标识(如:"symfony2.great"):
- <!-- src/Acme/DemoBundle/Resources/translations/messages.fr.xliff -->
- <?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>Symfony2 is great</source>
- <target>J'aime Symfony2</target>
- </trans-unit>
- <trans-unit id="2">
- <source>symfony2.great</source>
- <target>J'aime Symfony2</target>
- </trans-unit>
- </body>
- </file>
- </xliff>
当要将"Symfony2 is great"或"symfony2.great"翻译成法语语言环境(如:fr_FR或fr_BE)时,Symfony2将找到并使用这些文件。
使用真实或关键词消息
本例在创建消息翻译时使用了两个不同的方式:
- $t = $translator->trans('Symfony2 is great');
- $t = $translator->trans('symfony2.great');
第一种方式,消息用缺省的语言环境语言(本例是英语)来写,然后在创建翻译时做为该消息的"id";
第二种方式,消息用表达消息内容的“关键词”来写,然后关键词消息被所有翻译当做"id"。在本例中,必须为缺省的语言环境创建翻译(如要把symfony2.great翻译成 Symfony2 is great)。
第二种方式是可移植的。因为当我们觉得消息在缺省语言环境中其实应该写成"Symfony2 is really great"时,消息的关键词不需要在任何翻译文件中改变。
选择使用哪种方法完全取决于你,但“关键词”格式常被推荐。
另外,PHP和YAML文件格式支持嵌套id,以当避免你使用关键词来替代真实文本做为你的id时的重复:
- symfony2:
- is:
- great: Symfony2 is great
- amazing: Symfony2 is amazing
- has:
- bundles: Symfony2 has bundles
- user:
- login: Login
将多层结构扁平化成单个id/翻译对,可以通过在各层间添加点号(.)来完成。因此上面的示例与下面的等效:
- symfony2.is.great: Symfony2 is great
- symfony2.is.amazing: Symfony2 is amazing
- symfony2.has.bundles: Symfony2 has bundles
- user.login: Login
正如我们所见,消息文件被组织成所翻译的语言环境,消息文件也可以进一步被组织成”域“。当创建消息文件时,域是文件名的第一部分。缺省域是messages。例如,假设为了组织方便,翻译被分为三个不同的域:messages、admin和navigation。法语翻译将拥有以下消息文件:
当翻译字符串没有在缺省域(messages)时,你必须在trans()中指定域作为第三个参数:
- $this->get('translator')->trans('Symfony2 is great', array(), 'admin');
Symfony2现在在用户语言环境的admin域中查找消息。
当前用户的语言环境被保存在会话(Session)中,并且可以通过会话服务访问:
- $locale = $this->get('session')->get语言环境();
- $this->get('session')->set语言环境('en_US');
如果语言环境没有在会话中明确设置,fallabck_locale配置参数将被Translator使用。该参数缺省为en(参见Configuration)。
另外,你可以通过在会话服务中定义default_locale来确保locale在用户会话中设置:
- # app/config/config.yml
- framework:
- session: { default_locale: en }
因为用户的语言环境被保存在会话中,它也许会尝试基于用户语言环境,使用同一URL来显示不同语言的资源。如http://www.example.com/contact可以对一个用户用英语,而对另一个用户用法语来显示相关内容。不幸地是,这违反了Web的基本规则:特定的URL返回相同的资源,而无关用户。进一步的问题,搜索引擎要索引哪个版本的内容呢?
更好地策略是将语言环境包含在URL中。这可以通过在路由系统中使用指定的_locale参数来得到完全的支持:
- contact:
- pattern: /{_locale}/contact
- defaults: { _controller: AcmeDemoBundle:Contact:index, _locale: en }
- requirements:
- _locale: en|fr|de
在路由中使用指定的_locale参数,被匹配的语言环境将自己在用户会话中设置。换句话说,如果用户访问/fr/contact的URI,fr将自动在用户会话中作为语言环境被设置。
你现在可以使用用户的语言环境来创建导向应用程序中其它翻译页面的路由。
消息多元化是个很难的课题,因为规则可以非常复杂。例如,这里有一个俄语多元化规则在数学上的表现:
- (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
如你所见,在俄语中你可以有三种不同复数形式,每种都给了0、1或2的索引。对于每种形式,复数是不同的,因此翻译也是不同的。
当翻译由于多元化存在不同形式时,你可以提供所有的形式,并将它们在一个字符串用管道符(|)分开:
- 'There is one apple|There are %count% apples'
为了翻译多元化的消息,使用transChoice()方法:
- $t = $this->get('translator')->transChoice(
- 'There is one apple|There are %count% apples',
- 10,
- array('%count%' => 10)
- );
第二个参数(本例中是10),是对象数量的描述,并且被用来决定使用哪个翻译,同时填充%count%占位符。
根据所给的数字,翻译选择正确的复数形式。在英语中,大多数词有单数形式(只有一个对象)和复数形式(除1外的其它数字0, 2, 3...)。因此,如果count是1,翻译器将使用第一个字符串(There is one apple),否则将使用There are %count% apples。
这是法语的翻译:
- 'Il y a %count% pomme|Il y a %count% pommes'
即使字符串看上去相同(它由被管道符分开的两个子字符串组成),法语的语法是不同的:第一个形式(不是复数)用于count为0或1时,因此,当count为0或1时,翻译器将自动使用第一个字符串(Il y a %count% pomme)。
每个语言环境都有它自己的规则集,其中的一些在数字映射复数形式的复杂规则之下,有着多达6种不同的复数形式。对于英语和法语来说规则非常简单,但对于俄语,你也许需要去示意哪个规则匹配哪个字符串。为了帮助翻译器,你可以对每个字符串使用可选的"标签":
- '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使用内部逻辑去根据所给数字选择哪个字符串。有时,你需要更多的控制或者想特定的情况下(如count为0或负数)的不同翻译。因此,你需要使用明确的数字区间:
- '{0} There is 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或更多。
你也可以合并数学规则和标准规则。在这种情况下,如果count不被特定区间匹配的话,那么标准规则将明确规则之后起作用:
- '{0} There is 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将会起作用。
区间可以表示有限的数字集:
- {1,2,3,4}
或者两个数中间的数:
- [1, +Inf[
- ]-1,2[
左定界符可以是 [ (包括) or ] (不包括),右定界符可以是 [ (不包括) or ] (包括)。除了数字,你还可以使用-Inf和+Inf来代表无穷大。
大多数情况下,翻译发生在模板。Symfony2在Twig和PHP模板中提供了本地化支持。
Symfony2提供特定的Twig标签(trans和transchoice)来帮助静态区块文本的翻译:
- {% trans %}Hello %name%{% endtrans %}
- {% transchoice count %}
- {0} There is no apples|{1} There is one apple|]1,Inf] There are %count% apples
- {% endtranschoice %}
transchoice标签会自动从当前上下文中得到%count%变量,并将其发送到翻译器。这个机制只是你使用的占位符遵循%var%模式的时间才会工作。
如果你需要在字符串中使用百分号(%),那么使用%%来进行输出清理:
- {% trans %}Percent: %percent%%%{% endtrans %}
你也可以指定消息域并发送一些附加的变量:
- {% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %}
- {% transchoice count with {'%name%': 'Fabien'} from "app" %}
- {0} There is 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') }}
使用翻译标签或过滤器效果是一样的,但它们有着细微的不同:自动输出清理只有使用过滤器时才会应用到可变翻译中。换句话说,如果你需要确定你的翻译变量没有输出清理,你必须需要在翻译过滤器中使用raw过滤器:
- {# text translated between tags is never escaped #}
- {% trans %}
- <h3>foo</h3>
- {% endtrans %}
- {% set message = '<h3>foo</h3>' %}
- {# a variable translated via a filter is escaped by default #}
- {{ message | trans | raw }}
- {# but static strings are never escaped #}
- {{ '<h3>foo</h3>' | trans }}
翻译服务在PHP模板中可以通过翻译器助手函数来访问:
- <?php echo $view['translator']->trans('Symfony2 is great') ?>
- <?php echo $view['translator']->transChoice(
- '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
- 10,
- array('%count%' => 10)
- ) ?>
翻译消息时,Symfony2使用来自用户会话的语言环境或必要时使用回退语言环境。你也可以手工指定语言环境用于翻译:
- $this->get('translation')->trans(
- 'Symfony2 is great',
- array(),
- 'messages',
- 'fr_FR',
- );
- $this->get('translation')->trans(
- '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
- 10,
- array('%count%' => 10),
- 'messages',
- 'fr_FR',
- );
数据库内容的翻译应该由Doctrine通过Translatable Extension来处理。更多信息参见该库的文档。
使用Symfony2翻译组件,创建一个国际化应用程序不再需要经历痛苦的过程,而只归纳几个基本步骤: