flask 国际化

Flask-Babel简介

它同Jinja2的i18n扩展一样,可以翻译Jinja2模板中的内容,以及Flask代码中的文字内容。同时它还可以翻译日期格式等等。它也是基于Babel和gettext等组件实现,有着非常简单友好的API接口,便于我们使用。

安装和启用

建议通过pip安装,简单方便:

pip install Flask-Babel

我们可以采用下面的方法初始化一个Flask-Babel的实例:

from flask_babel import Babel,gettext as _
babel=Babel()
babel.init_app(app)

设置语言和时区

Flask-Babel提供了两个Flask应用配置项:

  1. BABEL_DEFAULT_LOCALE: 应用默认语言,不设置的话即为en
  2. BABEL_DEFAULT_TIMEZONE: 应用默认时区,不设置的话即为UTC
BABEL_DEFAULT_LOCALE='en'

当程序里没指定时,就会采用这些默认设置。那么如何在程序里指定呢?Flask-Babel提供了两个装饰器localeselectortimezoneselector,分别用来设置语言和时区:

@babel.localeselector
def get_locale():
    return 'zh'

@babel.timezoneselector
def get_timezone():
    return 'UTC'

这里的设置将会覆盖应用配置项中”BABEL_DEFAULT_LOCALE”和”BABEL_DEFAULT_TIMEZONE”。上面的程序不是个好例子,常见的情况是从当前用户会话中,或者从服务器环境中获取语言/时区设置。

@babel.localeselector
def get_locale():
    return request.accept_languages.best_match(app.config['LANGUAGES'])

这里我使用了Flask中request对象的属性accept_languages。 request对象提供了一个高级接口,用于处理客户端发送的带Accept-Language头部的请求。 该头部指定了客户端语言和区域设置首选项。 该头部的内容可以在浏览器的首选项页面中配置,默认情况下通常从计算机操作系统的语言设置中导入。 大多数人甚至不知道存在这样的设置,但是这是有用的,因为应用可以根据每个语言的权重,提供优选语言的列表。 为了满足你的好奇心,下面是一个复杂的Accept-Languages头部的例子:

Accept-Language: da, en-gb;q=0.8, en;q=0.7

这表示丹麦语(da)是首选语言(默认权重= 1.0),其次是英式英语(en-GB),其权重为0.8,最后是通用英语(en),权重为0.7。

要选择最佳语言,你需要将客户请求的语言列表与应用支持的语言进行比较,并使用客户端提供的权重,查找最佳语言。 这样做的逻辑有点复杂,但它已经全部封装在best_match()方法中了,该方法将应用提供的语言列表作为参数并返回最佳选择。

装饰器localeselectortimezoneselector修饰的函数,被调用一次后就会被缓存,也就是不会被多次调用。但是有时候,当切换用户时,我们想从新用户会话中重新获取语言/时区设置,此时可以在登录请求中调用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/')
def translate(num=None):
    if num is None:
        return gettext(u'No users')
    return ngettext(u'%(num)d user', u'%(num)d users', num)

关于gettextngettext的区别,大家可以参考下这篇文章的”单/复数支持”部分。

此外,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()方法,更简单的我们可以用_()方法代替:


{{ _('Test Sample') }}

{{ _('Hello World!') }}

在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_,webassets.ext.jinja2.AssetsExtension

这个文件告诉”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”。 

flask 国际化_第1张图片

生成的文件名要和.po文件对应和一致 否则会出错导致翻译不准确

    • 参数-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 国际化_第2张图片

格式化日期

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_numberformat_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的方法,这里就不一一介绍了。

命令行增强

pybabel命令有点长,难以记忆。 利用这个机会向你展示如何创建与flask命令集成的自定义命令。 到目前为止,你已经看到我使用Flask-Migrate扩展提供的flask runflask shell和几个flask db子命令。 将应用特定的命令添加到flask实际上也很容易。 所以我现在要做的就是创建一些简单的命令,并用这个应用特有的参数触发pybabel命令。 我要添加的命令是:

  • flask translate init LANG用于添加新语言
  • flask translate update用于更新所有语言存储库
  • flask translate compile用于编译所有语言存储库

babel export步骤不会设置为一个命令,因为生成messages.pot文件始终是运行initupdate命令的先决条件,因此这些命令的执行将会生成翻译模板文件作为临时文件。

Flask依赖Click进行所有命令行操作。 像translate这样的命令是几个子命令的根,它们是通过app.cli.group()装饰器创建的。 我将把这些命令放在一个名为app/cli.py的新模块中:

app/cli.py:翻译命令组

from app import app

@app.cli.group()
def translate():
    """Translation and localization commands."""
    pass

该命令的名称来自被装饰函数的名称,并且帮助消息来自文档字符串。 由于这是一个父命令,它的存在只为子命令提供基础,函数本身不需要执行任何操作。

updatecompile很容易实现,因为它们没有任何参数:

app/cli.py:更新子命令和编译子命令:

import os

# ...

@translate.command()
def update():
    """Update all languages."""
    if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
        raise RuntimeError('extract command failed')
    if os.system('pybabel update -i messages.pot -d app/translations'):
        raise RuntimeError('update command failed')
    os.remove('messages.pot')

@translate.command()
def compile():
    """Compile all languages."""
    if os.system('pybabel compile -d app/translations'):
        raise RuntimeError('compile command failed')

请注意,这些函数的装饰器是如何从translate父函数派生的。 这似乎令人困惑,因为translate()是一个函数,但它是Click构建命令组的标准方式。 与translate()函数相同,这些函数的文档字符串在--help输出中用作帮助消息。

你可以看到,对于所有命令,运行它们并确保返回值为零(这意味着命令没有返回任何错误)。 如果命令错误,那么我会引发一个RuntimeError,这会导致脚本停止。 update()函数在同一个命令中结合了extractupdate步骤,如果一切都成功的话,它会在更新完成后删除messages.pot文件,因为当再次需要这个文件时,可以很容易地重新生成 。

init命令将新的语言代码作为参数。 这是其执行流程:

app/cli.py:Init子命令。

import click

@translate.command()
@click.argument('lang')
def init(lang):
    """Initialize a new language."""
    if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
        raise RuntimeError('extract command failed')
    if os.system(
            'pybabel init -i messages.pot -d app/translations -l ' + lang):
        raise RuntimeError('init command failed')
    os.remove('messages.pot')

该命令使用@click.argument装饰器来定义语言代码。 Click将命令中提供的值作为参数传递给处理函数,然后将该参数并入到init命令中。

启用这些命令的最后一步是导入它们,以便注册命令。 我决定在顶级目录的microblog.py文件中执行此操作:

microblog.py:注册命令。

from app import cli
  • 这里我唯一需要做的就是导入新的cli.py模块,不需要做任何事情,因为导入操作会导致命令装饰器运行并注册命令。

此时,运行flask --help将列出translate命令作为选项。 flask translate --help将显示我定义的三个子命令:

(venv) $ flask translate --help
Usage: flask translate [OPTIONS] COMMAND [ARGS]...

  Translation and localization commands.

Options:
  --help  Show this message and exit.

Commands:
  compile  Compile all languages.
  init     Initialize a new language.
  update   Update all languages.

所以现在工作流程就简便多了,而且不需要记住长而复杂的命令。 要添加新的语言,请使用:

flask translate init 
  • 在更改_()_l()语言标记后更新所有语言:
flask translate update
  • 在更新翻译文件后编译所有语言:
flask translate compile

你可能感兴趣的:(web,python)