Symfony 框架实战教程——第六天:模板重载与翻译

Symfony 框架实战教程——第六天:模板重载与翻译

Chris Yue 2 comments
Posts

昨天我们已经完成了首页和新闻列表页的外观改造,剩下的新闻详情页,就可以留给大家当作业自己实践了。今天我们要改造的是登录页。

在未登录状态下点击“+发表新闻”按钮,也就是 /news/new 链接,会转跳到 /oauth/login/ 链接,也就是第三方登录的链接(如果只实现了本地用户登录的同学,去的应该是另外一个界面,不过没关系,重载模板的原理都一样)。目前第三方登录页面只有一个可怜巴巴的“QQ”这个链接,让我们也给它加上页头页尾。

不过,这个模板文件很明显不是我们自己添加的,是第三方 Bundle 里自带的,难道我们要去修改第三方 Bundle 的代码吗?库文件随便修改这样好吗?当然不好,好消息是 Symfony 框架实际是提供了框架模板重载机制的。

模板重载

在重载之前,我们需要知道模板的路径。那么如何找路径呢?很简单,只要先找到对应的路径对应的路由,然后找到对应的控制器,然后通过控制器代码就能知道加载的模板是……你以为我会跟你说这么傻的方法吗?其实你只需要点一下调试工具栏的这个像秒表的按钮:

symfony screenshot time profiler button

是否看到某个 .html.twig 文件了呢?(友情提示:万一你电脑超级牛逼渲染模板连 1ms 都不到,是看不到模板渲染时间的,这个时候你可以将页面上方的 Threshold 改成 0.1 甚至更小)

sf time profiler

得知模板的名称为 HWIOAuthBundle:Connect:login.html.twig,我们就可以据此创建重载文件 app/Resources/HWIOAuthBundle/views/Connect/login.html.twig。创建好了之后刷新一下登录页,因为我们什么都没写,如果重载成功,会返回一个空白页面。注意文件的命名规律,必须按此规律才能实现在 app 下的第三方模板重载

我们可以参考一下本身的模板代码,然后加上我们自己的 base.html.twig 的布局模板:

{# app/Resources/HWIOAuthBundle/views/Connect/login.html.twig %}
{% extends 'base.html.twig' %}

{% block body %}
    {% if error is defined and error %}
        <span>{{ error }}</span>
    {% endif %}
    {% for owner in hwi_oauth_resource_owners() %}
        <a href="{{ hwi_oauth_login_url(owner) }}">{{ owner | trans({}, 'HWIOAuthBundle') }}</a> <br />
    {% endfor %}
{% endblock body %}

光有一个链接其实依然很丑,我想把他做成一个带企鹅图标的按钮。我虽然不是设计师,但还好有开源素材给我用。fontawesome 便是这么一个开源的图标库。它是以字体的方式存在的,这就意味着你可以不需要设计师做图,而是用 CSS 的方式让这些“图片”像字一样,随意变大,缩小,改变颜色,设置半透明效果,添加动画……只要 CSS 能做到的都能实现,而且因为字体是矢量的,所以放得再大也不会失真。

现在把 fontawesome 引入到项目中来:

{# app/Resources/views/base.html.twig #}
...
<link rel="stylesheet" href="//cdn.bootcss.com/font-awesome/4.3.0/css/font-awesome.min.css">
...

然后将之前的链接替换成创建按钮:

{# app/Resources/HWIOAuthBundle/views/Connect/login.html.twig #}
...
<a class="btn btn-default" href="{{ hwi_oauth_login_url(owner) }}">{{ owner | trans({}, 'HWIOAuthBundle') }}</a>
...

当然,我们可以顺手把“qq”变成大写的(使用 twig 的 upper filter,像这样 {{ owner | tarns({}, 'HWIOAuthBundle') | upper }}),不过这么做并不好,因为要是以后添加了微博,那按钮岂不会变成 WEIBO

翻译

其实 HWIOAuth 已经帮我们想到了这一点,所以用了一个叫 trans 的 Twig filter。 trans 即translate即翻译的意思,我们如何实现翻译呢?首先我们需要一个文件,来定义翻译源和翻译结果的对应关系。比如目前我们的翻译源是 qq,翻译结果是……为了效果明显点,就打算叫 腾讯 QQ 吧。这样 trans 就会读取这个文件,如果发现翻译源是一个叫 qq 的字符串,他就会帮我们用 腾讯qq 来替换它(注意必须是只有 qq 的字符串,this is qq 这样的字符串里的 qq 是不会被替换的)。

虽然翻译文件也可以有各种格式,比如最省事儿的 YAML 格式来做翻译,不过翻译也已经有业界标准,通常都是使用 xliff 格式。这个文件的存放位置,如果是第三方 Bundle 本身自带的翻译文件,放在 XxxBundle/Resources/translations/yyy.zh_CN.xliff,但如果是项目自身的翻译,按官方的 最佳实践 的说法,放在 app/Resources/translations/yyy.zh_CN.xliff。至于这个 yyy 具体是什么文件名,得看 trans 的第二个参数,比如这里是 HWIOAuthBundle,那翻译文件名就是 HWIOAuthBundle.zh_CN.xliff。如果没有第二个参数,默认是 messages,对应文件名就是 messages.zh_CN.xliff

使用 xliff 或者说用一种业界普遍使用的格式有一个最大的好处:相关的辅助软件多如牛毛,随便网上一搜就是一大把,不过这里我们先不用其他的软件来做翻译。

另外,文件名中间的 zh_CN 表示的是“中文-大陆”,也就是简体中文。语言代码一般由 语言 + 区域 的方式构成,比如 en_USen_GB,但如果有些语句不严格区分区域的话,区域代码也是可以省略的,比如 en。Symfony 翻译的顺序是先找“语言 + 区域”文件,再查找“仅语言”文件,再查找默认语言文件,至于默认语言文件如何设置,后面会提到。小提示:以后千万记得中文应该是 zh 而不是 cn,cn 指的是中国,而不是中文。

那么我们就来创建翻译文件吧,且慢!像翻译这种常见的需求……好吧不多说,接下来我们将用到简化翻译的 Bundle:JMSTranslationBundle

一如既往安装 Bundle

$ composer require jms/translation-bundle

注册 Bundle

// in AppKernel::registerBundles()
$bundles = array(
    // ...
    new JMS\TranslationBundle\JMSTranslationBundle(),
    // ...
);

然后我们就可以让 JMSTranslationBundle 自动帮我们创建 xliff 文件了

$ php app/console translation:extract zh_CN --dir=app/Resources --output-dir=app/Resources/translations

好嘞,让我们去看看生成文件:打开 app/Resources/translations/……说好的 xliff 文件呢?为啥连根毛都没有啊?因为,模板文件里被翻译的是一个变量(那个 owner),JMSTranslationBundle 依然不知道翻译源是啥。

为了让 JMSTranslationBundle 知道有一待翻译短语叫 qq,那我们就手工创建一个好了!(好吧其实我也是为了演示 JMSTranslationBundle 的用法,文件完全可以自己创建,不过最好还是不要手写 xliff,一是内容多,写起来麻烦,二是手写容易出错)。在 app/Resources/views 下的随便什么模板文件,在某个block里输入下面的内容:

{{ 'qq'|trans({}, 'HWIOAuthBundle') }}

然后再重新跑一次命令,我们便可以得到 HWIOAuthBundle.zh_CN.xliff 文件了:

<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:jms="urn:jms:translation" version="1.2">
  <file date="2015-04-01T01:13:48Z" source-language="en" target-language="zh_CN" datatype="plaintext" original="not.available">
    <header>
      <tool tool-id="JMSTranslationBundle" tool-name="JMSTranslationBundle" tool-version="1.1.0-DEV"/>
      <note>The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.</note>
    </header>
    <body>
      <trans-unit id="bed4eb698c6eeea7f1ddf5397d480d3f2c0fb938" resname="qq">
        <jms:reference-file line="9">views/Connect/login.html.twig</jms:reference-file>
        <source>qq</source>
        <target state="new">qq</target>
      </trans-unit>
    </body>
  </file>
</xliff>

到此,之前为了生成文件而随便添加的代码可以删除了。

这个文件如果不看 xliff 相关文档,你敢随便改吗?没关系,JMSTranslationBundle 还有另外一个给力的功能:带 Web UI的翻译工具。

因为在生产环境我们是不可能向公众开放翻译工具的,所以以下配置应该加到 routing_dev.yml 里:

JMSTranslationBundle_ui:
    resource: @JMSTranslationBundle/Controller/
    type:     annotation
    prefix:   /_trans

因为 JMSTranslationBundle 的 WebUI 使用了 JMSDiExtra 以及 TwigExtensions 两个库,我们也把它们加载进来:

$ composer require 'jms/di-extra-bundle'
$ composer require 'twig/extensions'
// in AppKernel::registerBundles()
$bundles = array(
    // ...
    new JMS\AopBundle\JMSAopBundle(),
    new JMS\DiExtraBundle\JMSDiExtraBundle($this),
    // ...
);

注意:因为 TwigExtensions 库不是 Bundle,所以不需要进行注册工作。
另外 JMSAopBundle 是 JMSDiExtraBundle 需要的 Bundle,虽然在安装过程中已经作为 JMSDiExtraBundle 的依赖自动被安装,但是注册还是需要我们手动的。

这个时候访问 /_trans 路径,还是会报错的,因为我们还有一些必要的设置没做,首先是 JMSDiExtraBundle 的设置,这个 Bundle 作用其实就一个:可以用 annotation 的方式来注册 Service。其实还是挺有用的,大家如果有兴趣可以看看 JMSTranslationBundle 的 TranslateController,看看它是如何用 annotation 的方式来注入一些服务到 controller 里的。其他不多说,先写配置:

jms_di_extra:
    locations:
        all_bundles: false
        bundles: [JMSTranslationBundle]

另 JMSTranslationBundle 也需要通过配置文件得知一些信息,我们来将其配置上,比如我们要翻译成什么语言,在哪个路径里找翻译源,在哪个路径找翻译文件

jms_translation:
    configs:
        app:
            dirs: ['%kernel.root_dir%/Resources', '%kernel.root_dir%/../src']
            output_dir: '%kernel.root_dir%/Resources/translations'

设置好所有配置后,再次访问 /_trans,虽然还有报错,但是报错不一样了。这一次是 twig 的错误,说是找不到 truncate 的 twig filter。那我们如何去注册一个 twig filter 呢?

注册自定义Twig Filter

在定义 Symfony 的服务的时候,有些服务可能在 Symfony 初始化的时候,就需要被运行,而不是普通服务那样,调用它的时候才运行,比如 Twig 的 Filter 服务,他需要在创建好服务对象之后,立马被 Twig Enviroment 注册,这样才能在 twig 模板中使用定义好的 filter。而要实现此种目的,Symfony 的服务容器是通过给服务定义 tag 来实现的。比如我们接下来要做的配置:

# app/config/services.yml
services:
    twig.text_extension:
        class: Twig_Extensions_Extension_Text
        tags:
            - name: twig.extension

注意到我们加了一个名字为 twig.extension 的 tag,只要有此 tag 的服务,Symfony 就会在服务创建好之后,立马将其注册到 Twig Environment 中。另外类似的还有 Listener (tag 名字为 kernel.event_listener)等,只要初始化好服务,立马就被 Event Dispatcher 注册。

另外如果有兴趣,也可以看看 Twig_Extension_Extension_Text 是怎么定义Filter的。主要是 getFilters 里的 new Twig_SimpleFilter('truncate', 'twig_truncate_filter', array('needs_environment' => true)) 那句,其中 Twig_SimpleFilter 构造函数的第二个参数可以是任意的 callback,而 callback 的第一个参数,就是我们需要处理的字符串。

回到正题,加上上面的配置之后,访问 /_trans,就不会报错了,只不过样式根本就没有,一个连样式都没有功能还好意思发布成,你们信吗?当然不信。JMSTranslationBundle 是自带了样式的,其实还包括 JS 文件。我们需要做的是将第三方 Bundle 中的这些静态文件,链接到 web 目录下:

$ php app/console assets:install --symlink

此命令将所有 Bundle 里的静态文件(位于XxxBundle/Resources/public),都创建了一个软链接(类似ln -s命令)到 web/bundle 目录下。

有些系统可能不支持软链接,可以尝试去掉 --symlink 参数,直接复制到 webbundles 目录下。

我们再次刷新 /_trans 页面,可以看到这样的页面:

symfony screenshot jms translation bundle ui

修改完中文翻译(没有确认按钮,只要输入框失去焦点就已经保存了),我们再刷新 /oauth/login 页面,按钮文字依然没有被翻译,这是因为我们还没有设置项目的默认语言,这需要在 config.yml 文件中指定:

framework:
    ...
    # 注意以下设置的区别:
    translator:
        fallbacks: ["%locale%"] # 如果某个语言没有对应的翻译,就显示fallbacks里的语言翻译

    default_locale: "%locale%" # 假如用户没有明确表示想要看哪个语言的页面
                               # 就认为当前用户是想看%locale%指定语言的页面
                               # 用户可通过HTTP请求中的`accept_language`
                               # 或者路由里的设置比如`domain.com/zh_CN/home`来指定,可以配置

最后,修改 parameters.yml 文件

locale: zh_CN

再刷新页面,翻译已经完成

symfony screenshot translation

里世界的第六天

Symfony 框架实战教程——第六天:模板重载与翻译 by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

写作累,服务器还越来越贵
求分担,祝愿好人一生平安
天使打赏人

2 Comments

ccna30

四月 3, 2015 在 12:49 下午

每次文章都能带给我们惊喜,尤其是楼主找的这些bundle,都是精品,请问应该到哪里去找bundle呢?

    Chris Yue

    四月 3, 2015 在 12:57 下午

    专门找还真找不着,这些都是在以前使用过程中各种尝试挑剩留下来的好东东

     

发表评论

35 + = 36