Tornado 国际化(i18n)

以前玩游戏的时候,官方如果不出中文版,经常就寄希望于国内的大神们做汉化版。当时并不了解这种行为是软件的国际化。互联网扩张的几十年,网络服务已经不再是只针对一部分地区的访问用户,通常需要考虑到全球的用户,Goolge,Twitter之类国际化需求显而易见。正好目前有一个项目需要提供国际化版本,即提供给app的API返回的文案需要有中英文两种语言。由于API服务是使用tornado,并且Tornado也支持i18n国际化。

可惜tornado官网对于国际化的文档写得有点随意,使用方式还得边看源码才能实现。Tornado官网中介绍了两种使用国际化的方式,一种是使用po等翻译文件,另外一种是使用csv文件。下面就两种模式,做简单的介绍。文末提供了一些文件的gist地址。

po翻译模式

国际化的内容,通常是软件的文字,即对用户可以的文案。对于web,一种则是api的返回值中的文案,另外一种则是写在模板里,或者动态渲染到模板中的字串。两种场景都需要顾及。

项目结构

新建一个文件夹tornado-i18n,搭建一个简单的tornado项目目录,具体目录如下:

☁  tornado-i18n  tree
.
├── locales
│   └── en_US
│       └── LC_MESSAGES
│           ├── en_US.mo
│           └── en_US.po
├── main.py
└── templates
    └── template.html

4 directories, 4 files

locales文件夹所存放的就是翻译文件,其中en_US.po则是翻译源文件,en_US.mo则是根据en_US.po所编译生成的二进制翻译文件,tornado读取的翻译模板也是mo文件。po文件有一定的攥写规则,编译po文件的工具很多,我使用的是Poedit。除此之外,还有xgettextgettext。看了下文档,略感复杂。

撰写一个翻译po文件

po文件和pot文件都是用于翻译的源文件,后者是一个模板。po文件也是文本文件,大概内容如下:

en_US.po

msgid ""
msgstr ""
"Project-Id-Version: tornado-i18n\n"
"POT-Creation-Date: 2016-09-19 11:45+0000\n"
"PO-Revision-Date: 2016-09-19 10:21+0800\n"
"Last-Translator: Rsj217 <rsj217@gmail.com>\n"
"Language-Team: rsj217 <rsj217@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"X-Generator: Poedit 1.8.9\n"
"X-Poedit-SourceCharset: UTF-8\n"

msgid "你好 世界"
msgstr "Hello world"

msgid "你好,世界"
msgstr "Hello, world"

msgid "登录"
msgstr "Sign in"

msgid表示原文,msgstr表示译文。还有一些文件头,大概就是翻译作者或team的信息。撰写完毕翻译的字符串对之后,使用poedit编译即可生成mo文件。 poedit界面如下:

00.png

设置默认环境和翻译文件

接下来就是Tornado中调用了,tornado提供locales模块,用于读取编译后的mo文件,不同的机器默认的语言环境不一样,这里我们假设默认的为中文环境。使用tornado.locale.set_default_locale('zh_CN')设置为默认的语言。其次通过tornado.locale.load_gettext_translations方法加载翻译文件,该函数的第一个参数为locales文件夹的绝对路径,第二个参数为翻译文件文件名。如果按照前面的目录结构组织文件,tornado就能通过locales文件下的LC_MESSAGES找到en_US.mo文件。

tornado.locale 载入的是mo文件,但是并不用写.mo这个扩展名。

语言选择

接口请求或者模板渲染的时候,具体渲染什么语言,可以通过客户端的请求参数或者服务的部署环境来定义。这里我们使用客户端的请求参数来指定渲染的语言。客户端参数 lang 如果为 zh(或者为空),则提供中文字符,如果为en则提供英文字符。

class BaseHandler(tornado.web.RequestHandler):
    def get_user_locale(self):
        user_locale = self.get_argument('lang', None)
        if user_locale == 'en':
            return tornado.locale.get('en_US')

tornado.web.RequestHandler提供了一个get_user_locale方法,用于返回一个 locale对象,这个对象会读取相应的翻译文件。self.locale.translate方法则可以对字符串进行翻译。通常喜欢使用_来标记翻译,因此将self.locale.translate绑定给_。然后就能使用_(待翻译的字符串)就能翻译啦。

翻译字串

翻译中文的时候,由于python2的编码问题,所有中文都必须使用unicode,不然无法翻译。

class ApiHandler(BaseHandler):
    def get(self, *args, **kwargs):
        _ = self.locale.translate
        text = (u"原文: {} <br /><br />  译文: {} <p></p>"
                u"原文: {} <br /><br />  译文: {} <p></p>"
                u"原文: {} <br /><br />  译文: {} <p></p>").format(u"你好 世界", _(u"你好 世界"),
                                                               u"你好,世界", _(u"你好,世界"),
                                                               u"登录", _(u"登录"))
        self.finish(text)

对于模板中使用也比较简单,直接使用_函数即可:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Template</title>
</head>
<body>
    <p><label>原文</label>:你好 世界</p>
    <p><label>译文</label>:{{ _(u"你好 世界") }}</p>

    <p><label>原文</label>:你好,世界</p>
    <p><label>译文</label>:{{ _(u"你好,世界") }}</p>

    <p><label>原文</label>:登录</p>
    <p><label>译文</label>:{{ _(text) }}</p>
</body>
</html>

完整的代码如下:

main.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os
import tornado.httpserver
import tornado.ioloop
import tornado.locale
import tornado.web


class BaseHandler(tornado.web.RequestHandler):
    def get_user_locale(self):
        user_locale = self.get_argument('lang', None)
        if user_locale == 'en':
            return tornado.locale.get('en_US')

class ApiHandler(BaseHandler):
    def get(self, *args, **kwargs):
        _ = self.locale.translate
        text = (u"原文: {} <br /><br />  译文: {} <p></p>"
                u"原文: {} <br /><br />  译文: {} <p></p>"
                u"原文: {} <br /><br />  译文: {} <p></p>").format(u"你好 世界", _(u"你好 世界"),
                                                               u"你好,世界", _(u"你好,世界"),
                                                               u"登录", _(u"登录"))
        self.finish(text)


class TemplateHandler(BaseHandler):
    def get(self, *args, **kwargs):
        text = u"登录"
        self.render('template.html', text=text)


class Application(tornado.web.Application):
    def __init__(self):
        super(Application, self).__init__(handlers=[
            (r'/api', ApiHandler),
            (r'/template', TemplateHandler),
        ],
                template_path=os.path.join(os.path.dirname(__file__), 'templates'),
                debug=True)


if __name__ == '__main__':
    app = Application()
    
    # 设置语言环境和翻译文件位置
    i18n_path = os.path.join(os.path.dirname(__file__), 'locales')
    tornado.locale.load_gettext_translations(i18n_path, 'en_US')
    tornado.locale.set_default_locale('zh_CN')
    
    server = tornado.httpserver.HTTPServer(app, xheaders=True)
    server.listen(8000)
    tornado.ioloop.IOLoop.current().start()

请求效果如下:

111.jpg

CSV翻译模式

相比与po文件模式,po文件的生成和编译,都比较麻烦,CSV文件要简单很多,而且更灵活。csv的文件名和传给tornado.locale.get的参数一致即可。下面是csv模式的目录

☁  tornado-i18n  tree
.
├── locales    
│   ├── en_US.csv
│   ├── ja_JP.csv
│   └── zh_TW.csv
├── main.py
└── templates
    └── template.html

2 directories, 5 files

csv翻译文件

csv的格式比较熟悉,使用"",进行切分成一个个单元格。翻译文件中,每一行为一个翻译对,第一个单元格为原文,第二个单元格为译文:

en_US.csv

"你好 世界","Hello world"
"你好,世界","Hello,world"
"登录","Sign in"

默认环境配置与翻译环境

与po模式类似,csv模式也需要设置一个默认的语言环境,同时在服务启动的时候,载入指定位置的csv文件。只需要将locales文件夹绝对路径传给tornado.locale.load_translations即可。

if __name__ == '__main__':
    app = Application()
    
    i18n_path = os.path.join(os.path.dirname(__file__), 'locales')
    # tornado.locale.load_gettext_translations(i18n_path, 'en_US')
    tornado.locale.load_translations(i18n_path)
    tornado.locale.set_default_locale('zh_CN')
    
    server = tornado.httpserver.HTTPServer(app, xheaders=True)
    server.listen(8000)
    tornado.ioloop.IOLoop.current().start()

get_user_locale 方法,再添加几个别的语言的选项,就能切换不同的语言了。

    def get_user_locale(self):

        user_locale = self.get_argument('lang', None)
        if user_locale == 'en':
            return tornado.locale.get('en_US')
        elif user_locale == 'tw':
            return tornado.locale.get('zh_TW')
        elif user_locale == 'jp':
            return tornado.locale.get('ja_JP')

具体的使用方式和模板都没有变,只是变更了服务初始化所在于的语言环境而已。完整的实现代码如下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os
import tornado.httpserver
import tornado.ioloop
import tornado.locale
import tornado.web


class BaseHandler(tornado.web.RequestHandler):
    def get_user_locale(self):

        user_locale = self.get_argument('lang', None)
        if user_locale == 'en':
            return tornado.locale.get('en_US')
        elif user_locale == 'tw':
            return tornado.locale.get('zh_TW')
        elif user_locale == 'jp':
            return tornado.locale.get('ja_JP')


class ApiHandler(BaseHandler):
    def get(self, *args, **kwargs):
        _ = self.locale.translate
        text = (u"原文: {} <br /><br />  译文: {} <p></p>"
                u"原文: {} <br /><br />  译文: {} <p></p>"
                u"原文: {} <br /><br />  译文: {} <p></p>").format(u"你好 世界", _(u"你好 世界"),
                                                               u"你好,世界", _(u"你好,世界"),
                                                               u"登录", _(u"登录"))
        self.finish(text)


class TemplateHandler(BaseHandler):

    def get(self, *args, **kwargs):
        text = u"登录"
        self.render('template.html', text=text)


class Application(tornado.web.Application):
    def __init__(self):
        super(Application, self).__init__(handlers=[
            (r'/api', ApiHandler),
            (r'/template', TemplateHandler),
        ],
                template_path=os.path.join(os.path.dirname(__file__), 'templates'),
                debug=True)


if __name__ == '__main__':
    app = Application()
    i18n_path = os.path.join(os.path.dirname(__file__), 'locales')
    # tornado.locale.load_gettext_translations(i18n_path, 'en_US')
    tornado.locale.load_translations(i18n_path)
    tornado.locale.set_default_locale('zh_CN')
    server = tornado.httpserver.HTTPServer(app, xheaders=True)
    server.listen(8000)
    tornado.ioloop.IOLoop.current().start()

最终效果显示如下:

222.jpg

相关文件源码:

翻译文件: en_US.po

po模式: main.py

csv模式: main.py

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 9.2 添加国际化和本地化 Django提供了完整的国际化和本地化支持。它允许你把应用翻译为多种语言,它会处理特定...
    lakerszhy阅读 1,116评论 0 1
  • 译者说 Tornado 4.3于2015年11月6日发布,该版本正式支持Python3.5的async/await...
    TaoBeier阅读 1,949评论 1 7
  • 第一章 引子 苍冥大陆的南方小镇“米多丽”,这里以盛产一种稀有的...
    白月樱阅读 420评论 7 1
  • 一个男人对于女生怎么样才能算是红颜,互相倾尽所有事情这就算知己了吗?有些人说红颜知己当然要长得漂亮能够诉说心事的就...
    世倾简艾阅读 233评论 0 0
  • 在科技加速发展的时代,公司及个人的竞争越来越激烈。最直观的证据是,公司的平均寿命在降低,人们变换职业的频率在增高。...
    马烈视界阅读 108评论 0 0