Flask扩展系列(三)–国际化I18N和本地化L10N
在Jinja2系列中,我们曾经介绍过Jinja2模板的i18n扩展,它可以实现模板中内容的本地化翻译。这里,我们要介绍一个Flask扩展,Flask-Babel。它同Jinja2的i18n扩展一样,可以翻译Jinja2模板中的内容,以及Flask代码中的文字内容。同时它还可以翻译日期格式等等。它也是基于Babel和gettext等组件实现,有着非常简单友好的API接口,便于我们使用。
系列文章
- Flask扩展系列(一)-Restful
- Flask扩展系列(二)-Mail
- Flask扩展系列(三)-国际化I18N和本地化L10N
- Flask扩展系列(四)-SQLAlchemy
- Flask扩展系列(五)-MongoDB
- Flask扩展系列(六)-缓存
- Flask扩展系列(七)-表单
- Flask扩展系列(八)-用户会话管理
- Flask扩展系列(九)-HTTP认证
- Flask扩展系列-自定义扩展
安装和启用
建议通过pip安装,简单方便:
$ pip install Flask-Babel
我们可以采用下面的方法初始化一个Flask-Babel的实例:
from flask import Flask
from flask.ext.babel import Babel
app = Flask(__name__)
babel = Babel(app)
设置语言和时区
Flask-Babel提供了两个Flask应用配置项:
- BABEL_DEFAULT_LOCALE: 应用默认语言,不设置的话即为
en
- BABEL_DEFAULT_TIMEZONE: 应用默认时区,不设置的话即为
UTC
app.config.update(
DEBUG=True,
BABEL_DEFAULT_LOCALE='zh'
)
当程序里没指定时,就会采用这些默认设置。那么如何在程序里指定呢?Flask-Babel提供了两个装饰器localeselector
和timezoneselector
,分别用来设置语言和时区:
@babel.localeselector
def get_locale():
return 'zh'
@babel.timezoneselector
def get_timezone():
return 'UTC'
这里的设置将会覆盖应用配置项中”BABEL_DEFAULT_LOCALE”和”BABEL_DEFAULT_TIMEZONE”。上面的程序不是个好例子,常见的情况是从当前用户会话中,或者从服务器环境中获取语言/时区设置。
装饰器localeselector
和timezoneselector
修饰的函数,被调用一次后就会被缓存,也就是不会被多次调用。但是有时候,当切换用户时,我们想从新用户会话中重新获取语言/时区设置,此时可以在登录请求中调用refresh()
方法清缓存:
from flask.ext.babel import refresh
@app.route('/login')
def login():
... # Get new user locale and timezone
refresh()
... # Render response
在视图和模板中使用翻译
Flask-Babel封装了Python的gettext()
方法,你可以在视图函数中使用它:
from flask.ext.babel import gettext, ngettext
@app.route('/trans')
@app.route('/trans/<int:num>')
def translate(num=None):
if num is None:
return gettext(u'No users')
return ngettext(u'%(num)d user', u'%(num)d users', num)
关于gettext
和ngettext
的区别,大家可以参考下这篇文章的”单/复数支持”部分。
此外,Flask-Babel还提供了lazy_gettext()
方法,它的作用同gettext()
类似,区别是它在文字被使用时才会被翻译,所以可以用来在上下文环境外定义要翻译的文字,比如:
from flask.ext.babel import lazy_gettext
hello = lazy_gettext(u'Hello World')
@app.route('/lazy')
def lazy():
return unicode(hello)
同样在模板中,我们也可以使用gettext()
方法,更简单的我们可以用_()
方法代替:
<!doctype html>
<title>{{ _('Test Sample') }}</title>
<h1>{{ _('Hello World!') }}</h1>
在Flask请求中,我们来渲染此模板:
@app.route('/')
def index():
return render_template('hello.html')
现在让我们启动应用,访问上面的视图,验证下程序是否正常运行。大家应该可以看到gettext()
方法里的文字被显示出来了,目前还没有被翻译。
创建本地化翻译文件
在介绍Jinja2模板的i18n扩展时,我们曾使用Python源码中的”pygettext.py”来创建翻译文件。这里,我们用个更方便的,就是Babel中的”pybabel”命令。步骤如下:
首先让我们创建一个Babel的配置文件,文件名任意,这里我们取名为”babel.cfg”
[python: **.py] [jinja2: **/templates/**.html] extensions=jinja2.ext.autoescape,jinja2.ext.with_
这个文件告诉”pybabel”要从当前目录及其子目录下所有的*.py
文件,和templates目录及其子目录下所有的*.html
文件里面搜寻可翻译的文字,即所有调用gettext()
,ngettext()
和_()
方法时传入的字符串。同时它告诉”pybabel”,当前Jinja2模板启用了”autoescape”和”with”扩展。
接下来,在当前目录下,生成一个名为”messages.pot”的翻译文件模板
$ pybabel extract -F babel.cfg -o messages.pot .
打开”messages.pot”,你会发现,上例中”No users”, “Test Sample”等文字都出现在
msgid
项中了,很强大吧。- 参数
-F
指定了Babel配置文件; - 参数
-o
指定了输出文件名。 如果你在程序中用到了
lazy_gettext()
方法,那么你需要加上参数-k lazy_gettext
来提醒”pybabel”要搜索该方法的调用:$ pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .
- 参数
修改翻译文件模板
首先记得将”messages.pot”中的#, fuzzy
注释去掉,有这个注释在,将无法编译po文件。然后修改里面的项目信息内容如作者,版本等。创建”.po”翻译文件
$ pybabel init -i messages.pot -d translations -l zh
上面的命令就可以创建一个中文的po翻译文件了,文件会保存在当前目录下的”translations/zh/LC_MESSAGES”下,文件名为”messages.po”。
- 参数
-i
指定了翻译文件模板; - 参数
-d
指定了翻译文件存放的子目录,上例中我们放在”translations”子目录下; - 参数
-l
指定了翻译的语言,同样也是第二级子目录的名称”zh”。
- 参数
编辑”.po”翻译文件
打开刚才生成的中文po翻译文件,将我们要翻译的内容写入msgstr
项中,并保存:#: flask-ext3.py:31 msgid "No users" msgstr "没有用户" #: flask-ext3.py:32 msgid "%(num)d user" msgid_plural "%(num)d users" msgstr[0] "%(num)d个用户" #: templates/hello.html:2 msgid "Test Sample" msgstr "测试范例" #: templates/hello.html:3 msgid "Hello World!" msgstr "世界,你好!"
最后一步,编译po文件,并生成
*.mo
文件$ pybabel compile -d translations
-d
指定了翻译文件存放的子目录。该命令执行后,”translations”目录下的所有po文件都会被编译成mo文件。
如果我们当前的语言”locale”已经设置为zh
了,再次启动应用,访问根视图或者”/trans”视图,你会看到我们的文字都已经是中文的了。
之后,如果代码中的待翻译的文字被更改过,我们需要重新生成”messages.pot”翻译文件模板。此时,要是再通过pybabel init
命令来创建po文件的话,会丢失之前已翻译好的内容,这个损失是很大的,其实我们可以通过下面的方法来更新po文件:
$ pybabel update -i messages.pot -d translations
-i
和-d
参数就不用再解释了。执行pybabel update
后,原先的翻译会被保留。不过要注意,因为有些字条”pybabel”无法确定,会将其标为”fuzzy”,你要将”fuzzy”注释去掉才能使其起效。
- 最后的最后,提醒下大家,translations目录必须是跟你Flask的app应用对象在同一目录下,如果你的app对象是放在某个包里,那translations目录也必须放在那个包下。
格式化日期
Flask-Babel不仅可以翻译文字,还可以自动翻译日期格式,运行下面的例子:
from flask.ext.babel import format_datetime
from datetime import datetime
@app.route('/now')
def current_time():
return format_datetime(datetime.now())
假设当前系统时间是”2016-3-20 11:38:32”,在”locale”是en
的情况下,会显示”Mar 20, 2016, 11:39:59 AM”;而在”locale”是zh
的情况下,会显示”2016年3月20日 上午11:38:32”。
format_datetime()
方法还可以带第二个参数指定输出格式,如”full”, “short”, “yyyy-MM-dd”等。详细的日期输出格式可参阅Babel的官方文档。
格式化数字
Flask-Babel提供了format_number
和format_decimal
方法来格式化数字,使用方法同上例中的format_datetime
非常类似,只需传入待格式化的数字即可:
from flask.ext.babel import format_decimal
@app.route('/num')
def get_num():
return format_decimal(1234567.89)
上面的数字,在”locale”是en
的情况下,会显示”1,234,567.90”;而在”locale”是de
的情况下,会显示”1.234.567,89”。
格式化货币
既然可以格式化数字,自然也少不了货币格式化显示的功能了。我们可以使用format_currency
方法,它同format_decimal
的区别是它必须传入两个参数,第二个参数指定了货币类型:
from flask.ext.babel import format_currency
@app.route('/currency')
def currency():
return format_currency(1234.5, 'CNY')
上面的数字”1234.5”,在类型(即第二个参数)是CNY
的情况下,会显示”¥1,234.50”;而在类型是USD
的情况下,会显示”US$1,234.50”。
Flask-Babel还提供了格式化百分数format_percent
,和格式化科学计数format_scientific
的方法,这里就不一一介绍了。
关于Babel的详细内容可以参考其官方网站
本篇中的示例参考了Flask-Babel的官方文档和Flask-Babel的源码。
本篇的示例代码可以在这里下载。