The Flask Mega-Tutorial 之 Chapter 13: I18n and L10n

小引

  • 利用 Flask-Babel,实现多语言支持,即 I18n 和 L10n
  • 定制自己的 command line extensions (to flask command)

Introduction to Flask-Babel

1、引入扩展 Flask-Babel

(venv) $ pip install flask-babel


2、配置 supported languages

config.py: supported languages list.

class Config(object):
    # ...
    LANGUAGES = ['en', 'zh_CN', 'zh']

注:

  • flask-babel 不接受 hyphen (如 en-US),所以 zh-CN 变为 zh_CN
  • zh-cnmoment.js 需要的:zh-cn for China; zh-hk for HK, zh-tw for Taiwan. (moment.js 对大小写及下划线或短横线无要求,如zh-cn,ZH_CN 均可)


3、初始化设置 babel

app / __init__.py: Flask-Babel instance.

# ...
from flask import request
from flask_babel import Babel

app = Flask(__name__)
# ...
babel = Babel(app)

# ...

@babel.localeselector
def get_locale():
    return request.accept_languages.best_match(app.config['LANGUAGES'])
  • browser 每次发送 request 请求,其携带的 request headers 里面都有一个 Accept-Language。当前 chrome 发送request时,其中的 request headers 包含以下:
    这里写图片描述

Marking Texts to Translate In Python Source Code

设置好 supported languages 和 初始化 babel 后,将待翻译的 text 标注出来。
待标注的文本,大致分2类: codetemplate

  • 标出 code 中需要翻译的 text,作为参数传给 _() 函数
from flask_babel import _


  • 有些 code 中包含的 string literal 是 request 之外赋值的(如各种 form.py 中包含的各种 form),这类 string literals 往往在 app 启动时,就需要赋值,但此时尚不知待用的语言。为此引入:
from flask_babel import lazy_gettext as _l

This new function wraps the text in a special object that triggers the translation to be performed later, when the string is used.

注: lazy_gettext() 是 _() 的封装,速度略慢。

需要更改的文件是 app / **.py,共须修改 4 个文件

__init__.py
email.py
errors.py (无)
forms.py
models.py(无)
routes.py


  • 更改 redirect 时的 默认flash 信息

app / __init__.py

login = LoginManager(app)
login.login_view = 'login'
login.login_message = _l('Please log in to access this page.')

The Flask-Login extension flashes a message any time it redirects the user to the login page. This message is in English and comes from the extension itself. To make sure this message also gets translated, I’m going to override the default message and provide my own, wrapper with the _l() function for lazy processing:



Marking Texts to Translate In Templates

标注 templates 中的 待翻译文本

app / templates/ **.html

email/~ (未)
404.html
500.html
_post.html
base.html
edit_profile.html
index.html
login.html
register.html
reset_password.html
reset_password_request.html
user.html

注:

  • 常规采用 {{ _() }}{{ _l() }} 包裹 string literal, 否则会被模板视为 literal 而无法完成 evaluation。

  • dynamic component 的语句,如 <h1>Hi, {{ current_user.username }}!</h1>
    改为类似占位符的格式:

    <h1>{{ _('Hi, %(username)s!', username=current_user.username) }}</h1>


  • _post.html ,利用 {% set user_link %},将 user_link 转为 链接后,再作为参数传入 translation func
        {% set user_link %}
            <a href="{{ url_for('user', username=post.author.username) }}">
                {{ post.author.username }}
            </a>
        {% endset %}
        {{ _('%(username)s said %(when)s',
            username=user_link, when=moment(post.timestamp).fromNow()) }}


  • user.html 中 原来的计数风格为<p>{{ user.followers.count() }} followers, {{ user.followed.count() }} following.</p>
    改为 %(count)d 的格式
<p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}</p>



Extracting Text to Translate

**.py**.html 中已标注好的文本(translatable texts),提取出来。

1、配置文件,规定 PyBabel 要扫描的文件
microblog / babel.cfg: PyBabel configuration file.

[python: app/**.py]
[jinja2: app/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

The first two lines define the filename patterns for Python and Jinja2 template files respectively.
The third line defines two extensions provided by the Jinja2 template engine that help Flask-Babel properly parse template files.

2、开始扫描文件,提取到 messages.pot
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .

  • -F, 指定 extract 扫描所依据的配置文件
  • -k _l, 指明 _l() 标注的 texts 也在扫描之列 (默认的 text marker 为 _()
  • -o,输出文件名
  • .,扫描所有文件(command dir 下)

上述命令,会在根目录下(命令目录)即 microblog / ,生成 messages.pot 文件。
pybabel extract

注: messages.pot 无需版本控制(source control),因为容易取得,并且仅作为初始文件使用。

This is a file that can be easily regenerated any time it is needed, simply by running the command above again. So there is no need to commit this file to source control.


Generating a Language Catalog

1、初始化 pybabel init (两个版本)
(venv) $ pybabel init -i messages.pot -d app/translations -l zh_CN
(venv) $ pybabel init -i messages.pot -d app/translations -l zh

  • -i,指定输入的 .pot 文件
  • -d,指定输出 dir (flask babel 默认 app / translations 下,存储所有语言的 repos )
  • -l,指明语言(须为 str,故 zh-CN is unaccepted,改为 zh_CN

上述命令,会在 app / translations / zh_CN / LC_MESSAGES / 下生成 messages.po 文件
这里写图片描述


2、编辑 messages.po 文件,并完成译文翻译

  • 使用 poedit 编辑翻译 messages.po 文件
    poedit

3、编译 pybabel compile

所有语言的 po 文件,都会一起 compile 到对应的 mo 文件。

(venv) $ pybabel compile -d app/translations
compiling catalog app/translations/es/LC_MESSAGES/messages.po to
app/translations/es/LC_MESSAGES/messages.mo

pybabel compile

这里写图片描述

4、应用测试

  • 可以调整 chromelanguage settingAccept-Language: zh-CN,zh;q=0.9,en;q=0.8
    注意:chrome 选择 中文(简体) 时,会自动在 zh-CN 后,加上 zh (权重 0.9)。
    这里写图片描述

  • 或者直接更改 localeselector 函数,使返回的 language code 恒定。
    (不能用 hyphen,因为 flask-babel 不是吧)

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

登录
login

首页
index

探索
explore

个人主页
profile


Updating the Translations

1、需要更新的情形:

  • 在 translation files 尚不完整时使用,后续完毕后更新
  • text marker (_()_l() 标注)标注遗漏,补充标注后须更新
  • text 内容变动,后续须标注且更新

2、更新命令

(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .
(venv) $ pybabel update -i messages.pot -d app/translations

注:update 更新是 增量修改update 会依据新生成 .pot 文件,将修改智能地合并到项目相关的所有 .po 文件中。智能合并(smart merge),旧的 texts 不变,只有 .pot 中新增/删除的条目才会更新合并。

The update call takes the new messages.pot file and merges it into all the messages.po files associated with the project. This is going to be an intelligent merge, in which any existing texts will be left alone, while only entries that were added or removed in messages.pot will be affected.

3、更新后,重新编译,并应用测试

可以在所有语言版本都更新完毕后,一次性编译完成,然后应用测试。


Translating Dates and Times

  • Moment.js 对于中文,提供三类 locale 支持:zh-cn,zh-hk,zh-tw(大小写,短横线 / 下划线,均可)
  • 为符合moment.js 的要求,简体中文选择 zh-cn,则要求下面设定的 g.locale 返回的是 zh-cn 的一种(无论大小写,短横线/下划线),即 _init_.py 中的 get_locale() 函数返回相应的 zh-cn
  • get_locale 在 _init_.py 中定义,其作用是拿 request 的请求语言(Accept-Language) 比对 app.config[‘LANGUAGES’] 中的 supported languages ,找到 app 配置中最合适的版本,然后返回 locale 对象。
    The decorated function is invoked for each request to select a language translation to use for that request:
# __init__.py

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

对于 request, **flask-babel** 通过 `get_locale()` 返回选定的语言 & locale。
1、将 **locale** 绑定到 **flask.g** 上,这样才能从 **base template** 获得它。 `app / routes.py`
# ...
from flask import g
from flask_babel import get_locale

# ...

@app.before_request
def before_request():
    # ...
    g.locale = str(get_locale())
    # print('g.locale= {}'.format(g.locale))
通过打印验证,chrome 设置为 `Accept-Language: zh-CN,zh;q=0.9,en;q=0.8` 时,返回的 g.locale 为 **zh_Hans_CN** ![这里写图片描述](https://img-blog.csdn.net/20180622163511554?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0t1bmdyZXll/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 查看 **zh_CN** 下的 **po** 文件,发现 g.locale 绑定的是 **Language Team** 的名称。
# Chinese (Simplified, China) translations for PROJECT.
#...

"Language: zh_CN\n"
"Language-Team: zh_Hans_CN <LL@li.org>\n"

#...
如果将 chrome 的语言权重调整,使 **zh** 为 1, ‘zh-CN’ 0.9, 验证发现 g.locale 绑定 的 “Language Team” 变为 :
# Chinese translations for PROJECT.

"Language: zh\n"
"Language-Team: zh <LL@li.org>\n"

无论是 zh,还是zh-CN,两者返回的 **g.locale** 都`不`符合 **moment.js** 的要求,为此,将上述代码调整如下:
@app.before_request
def before_request():
    if current_user.is_authenticated:
        current_user.last_seen = datetime.utcnow()
        db.session.commit()
    g.locale = 'zh-cn' if str(get_locale()).startswith('zh') else str(get_locale())   
    #print(f'g.locale = {g.locale}')
如果 **get_locale()** 返回的是中文,则统一改为 **zh-cn**; 如果不是中文,则保留原语种 **str(get_locale())**。 经过这样的调整,**dates and times** 可以正常显示。
2、在 **base.html** 中 添加 `{{ moment.lang(g.locale) }}`

{% block scripts %}
    {{ super() }}
    {{ moment.include_moment() }}
    {{ moment.lang(g.locale) }}
{% endblock %}
![这里写图片描述](https://img-blog.csdn.net/20180622164742595?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0t1bmdyZXll/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

Command-Line Enhancements

1、已有的 flask 命令
flask run
flask shell
flash db ( migrate、 upgrade)
2、定制 **pybabel** 命令
  • flask translate init LANG , add a new language
  • flask translate update , update all language repositories
  • flask translate compile ,compile all language repositories

babel export ,即 pybabel extract,是 initupdatepre-requisite,所以不单独列出。


3-1、创建 parent method: def translate()

import os
import click
from app import app

@app.cli.group()
def translate():
    """Translation and localization commands."""
    pass
  • 引入 click
  • @app.cli.group() 建立 parent function 或者 root function
  • sub_commands 会依据 root func def translate()
  • command 名词,取自被修饰的 func 名称
  • help 信息,取自 doctring
  • 本函数只作 base,用于建立 sub-commands, 所以不执行仅 pass


3-2、初始化 flask translate init

@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('lang'),传入参数
  • 如果 pybabel extractpybabel init 执行正常,则 返回 0;如果执行异常,则返回值不为零,报错,同时停止程序
  • 如果一切正常,则删除messages.pot ,因为此文件仅用于后续翻译使用,需要的时候可以很容易生成。


3-3、更新 pybabel update

@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')
  • 类似 flask translate init
  • pybabel extractpybabel update 执行正常,则返回 0;如果异常,则报错,同时停止程序
  • 如果一切正常,最后删除 messages.pot


3-4、 编译 pybabel compile

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

过程不涉及生成messages.pot,故无删除操作。


3-5、配置

microblog.py: Register command-line commands.

from app import cli


查看 flask 命令 flask --help
这里写图片描述


查看 flask translate 命令 flask translate --help
这里写图片描述


以后可以直接使用:

(venv) $ flask translate init <language-code>
(venv) $ flask translate update
(venv) $ flask translate compile

小结

  1. 本节内容,涉及 不同 languages 对应的 internationalization & localization
  2. Dates and Times 由于涉及 Moment.js 对中文的支持格式,难以与 chrome 的 language setting 完美协调,只好自己判断一下,统一转为 zh-cn

更多 zh-cn 问题,关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值