Django 多语言教程 (i18n)

原文连接:ocavue.com/django_tran…html

概览

最近公司准备扩张海外业务,因此要给 Django 系统添加 国际化与本土化 支持。国际化通常简称 i18n,表明 Internationalization 中 i 和 n 有 18 个字母;本地化简称 L10n,表示 Localization 中 l 和 n 中有 10 个字母。有趣的一点是,通常会用小写的 i 和大写的 L 防止混淆。前端

简单来讲:i18n 是为国际化搭建框架,L10n 是针对不一样地区的适配。举个简单的例子:vue

i18n:node

datetime.now().strftime('%Y/%m/%d')  # before i18n
datetime.now().strftime(timeformat)  # after i18n
复制代码

L10n:python

timeformat = {
    'cn': '%Y/%m/%d',
    'us': '%m/%d/%Y',
    'fr': '%d/%m/%Y',
    ...
}
复制代码

更加具体的定义能够看 W3C 的解释。git

i18n 的范围很是广,包括多语言、时区、货币单位、单复数、字符编码甚至是文字阅读顺序(RTL)等等。这篇文章只关注 i18n 的多语言 方面。github

使用阿拉伯语的 windows 系统,来源

↑ 阿拉伯语的 windows 系统,文字甚至界面的方向都与中文版的相反(图片来源django

基本步骤

Django 做为一个大而全的框架,已经提供了一套多语言的解决方案,我稍微对比了一下,并没能找到在 Django 体系下比官方方案还好用的库。Django 的方案能够简单分为四步:ubuntu

  1. 一些必要的配置
  2. 在代码中标记须要翻译的文本
  3. 使用 makemessages 命令生成 po 文件
  4. 编译 compilemessages 命令编译 mo 文件

下面咱们详细来看看windows

第一步:配置

首先在 settings.py 中加入这几个内容

LOCALE_PATHS = (
    os.path.join(__file__, 'language'),
)
MIDDLEWARE = (
    ...
    'django.middleware.locale.LocaleMiddleware',
    ...
)
LANGUAGES = (
    ('en', 'English'),
    ('zh', '中文'),
)
复制代码

LOCALE_PATHS:指定下面第三步和第四步生成文件的位置。老版的 Django 须要手动新建好这个目录。

LocaleMiddleware:可让 Django 识别并选择合适的语言。

LANGUAGES:指定了这个工程能提供哪些语言。

第二步:标记文本

以前没有多语言的须要,因此你们在 AJAX 相应代码中直接写了中文,好比这样:

return JsonResponse({"msg": "内容过长", "code": 1, "data": None})
复制代码

如今须要多语言了,就须要告诉 Django 哪些内容是须要翻译的。对于上面的例子来讲,就是写成这样:

from django.utils.translation import gettext as _

return JsonResponse({"msg": _("内容过长"), "code": 1, "data": None})
复制代码

这里使用 gettext 函数将本来的字符串包裹起来,这样的话,Django 就能够根据当前语言返回合适的字符串。通常会使用单个下划线 _ 提升可读性。

由于我司几乎全部先后端通讯都使用 AJAX,因此并无怎么用上 Django 的模板功能(顺便一提,我司前端使用的多语言工具是 i18next)。不过在这里也一并写下 Django 模板的标记方法:

<title>{% trans "This is the title." %}</title>
<title>{% trans myvar %}</title>
复制代码

其中 trans 标签告诉 Django 须要翻译这个括号里面的内容。更具体的用法能够参考官方文档

第三步:makemessages

在执行这一步以前,请先经过 xgettext --version 确认本身是否安装了 GNU gettext。GNU gettext 是一个标准 i18n L10n 库,Django 和不少其余语言和库的多语言模块都调用了 GNU gettext,因此接下来说的一些 Django 特性实际上要归功于 GNU gettext。若是没有安装的话能够经过下面的方法安装:

ubuntu:

$ apt update
$ apt install gettext
复制代码

macOS:

$ brew install gettext
$ brew link --force gettext
复制代码

windows

安装完 GNU gettext 后,对 Django 工程执行下面的命令

$ python3 manage.py makemessages --local en
复制代码

以后能够找到生成的文件:language/en/LC_MESSAGES/django.po。把上面命令中的 en 替换成其余语言,就能够生成不一样语言的 django.po 文件。里面的内容大概是这样的:

#: path/file.py:397
msgid "订单已删除"
msgstr ""

...
复制代码

Django 会找到被 gettext 函数包裹的全部字符串,以 msgid 的形式保存在 django.po。每一个 msgid 下面的 msgstr 就表明你要把这个 msgid 翻译成什么。经过修改这个文件能够告诉 Django 翻译的内容。同时经过注释说明了这个 msgid 出如今哪一个文件的哪一行。

关于这个文件,发现几点有趣的特性:

  • Django 会把多个文件中相同的 msgid 归类在一块儿。「一次编辑,处处翻译」
  • 若是之后源码中某个 msgid 被删了,那么再次执行 makemessages 命令后,这个 msgid 和它的 msgstr 会以注释的形式继续保存在 django.po 中。
  • 既然源码中的字符串只是一个所谓的 id,那么我就能够在源码中写没有实际含义的字符串,好比 _("ERROR_MSG42"),而后将 "ERROR_MSG42" 同时翻译成中文和英文。
  • 这个文件中会保留模板字符串的占位符,好比可使用命名占位符作到在不一样语言中使用不一样占位符顺序的功能,下面给出了一个例子:

py file:

_('Today is {month} {day}.').format(month=m, day=d)
_('Today is %(month)s %(day)s.') % {'month': m, 'day': d}
复制代码

po file:

msgid "Today is {month} {day}."
msgstr "Aujourd'hui est {day} {month}."

msgid "Today is %(month)s %(day)s."
msgstr "Aujourd'hui est %(day)s %(month)s."
复制代码

第四步:compilemessages

修改好 django.po 文件后,执行下面的命令:

$ python3 manage.py compilemessages --local en
复制代码

Django 会调用程序,根据 django.po 编译出一个名为 django.mo 的二进制文件,位置和 django.po 所在位置相同。这个文件才是程序执行的时候会去读取的文件。

执行完上面四步后,修改浏览器的语言设置,就能够看到 Django 的不一样输出了。

Chrome 的语言设置

↑ Chrome 的语言设置

高级特性

i18n_patterns

有的时候,咱们但愿能够经过 URL 来选择不一样的语言。这样作有不少好处,好比同一个 URL 返回的数据的语言必定是一致的。Django 的文档就使用了这种作法:

简体中文:https://docs.djangoproject.com/zh-hans/2.0/

英文:https://docs.djangoproject.com/en/2.0/

具体的作法是在 URL 中添加 <slug:slug>

urlpatterns = ([
    path('category/<slug:slug>/', news_views.category),
    path('<slug:slug>/', news_views.details),
])
复制代码

详细的作法能够参考 Django 的官方文档

Django 如何决定使用哪一种语言

咱们以前讲过 LocaleMiddleware 能够决定使用何种语言。具体来讲,LocaleMiddleware 是按照下面的顺序(优先级递减):

  • i18n_patterns
  • request.session[settings.LANGUAGE_SESSION_KEY]
  • request.COOKIES[settings.LANGUAGE_COOKIE_NAME]
  • request.META['HTTP_ACCEPT_LANGUAGE'],即 HTTP 请求中的 Accept-Language header
  • settings.LANGUAGE_CODE

我司选择把语言信息放到 Cookies 中,当用户手动选择语言时,可让前端直接修改 Cookies,而不须要请求后台的某个接口。没有手动设置过语言的用户就没有这个 Cookies,跟随浏览器设置。话说 settings.LANGUAGE_COOKIE_NAME 的默认值是 django_language,前端不想在他们的代码中出现 django,因此我在 settings.py 中添加了 LANGUAGE_COOKIE_NAME = app_language 😂。

你也能够经过 request.LANGUAGE_CODE 在 View 中手动获知 LocaleMiddleware 选用了哪一种语言。你甚至能够经过 activate 函数手动指定当前线程使用的语言:

from django.utils.translation import activate

activate('en')
复制代码

ugettext

Python2 时代,为了区分 unicode strings 和 bytestrings,有 ugettextgettext 两个函数。在 Python3 中,因为字符串编码的统一,ugettextgettext 是等价的。官方说将来可能会废弃 ugettext,可是截止到如今(Django 2.0),ugettext 还没废弃。

gettext_lazy

这里先用一个例子直观地看一下 gettext_lazygettext 的区别

from django.utils.translation import gettext, gettext_lazy, activate, get_language

gettext_str = gettext("Hello World!")
gettext_lazy_str = gettext_lazy("Hello World!")

print(type(gettext_str))
# <class 'str'>
print(type(gettext_lazy_str))
# <class 'django.utils.functional.lazy.<locals>.__proxy__'>

print("current language:", get_language())
# current language: zh
print(gettext_str, gettext_lazy_str)
# 你好世界! 你好世界!

activate("en")

print("current language:", get_language())
# current language: en
print(gettext_str, gettext_lazy_str)
# 你好世界! Hello World!
复制代码

gettext 函数返回的是一个字符串,可是 gettext_lazy 返回的是一个代理对象。这个对象会在被使用的时候,才根据当前线程中语言决定翻译成什么文字。

这个功能在 Django 的 models 中尤为的有用。由于 models 中定义字符串的代码只会执行一次。在以后的请求中,根据语言的不一样,这个所谓字符串要有不一样的表现。

from django.utils.translation import gettext_lazy as _

class MyThing(models.Model):
    name = models.CharField(help_text=_('This is the help text'))

class YourThing(models.Model):
    kind = models.ForeignKey(
        ThingKind,
        on_delete=models.CASCADE,
        related_name='kinds',
        verbose_name=_('kind'),
    )
复制代码

使用 AST / FST 修改源码

因为我司工程很是庞大,人力给每一个字符串添加 _( ... ) 过于繁琐。因此我试图寻找一种自动化的方式。

一开始选择的是 Python 内置的 ast (Abstract syntax tree 语法抽象树) 模块 。基本思路是经过 ast 找到工程中的全部字符串,再给这些字符串添加 _( ... )。最后把修改后的语法树从新转为代码。

可是因为 ast 对格式信息的支持不佳,修改代码后容易形成格式混乱。因此找到了名为 FST (Full Syntax Tree 全面抽象树) 的改进方式。我选择的 FST 库是 redbaron。核心的代码以下:

root = RedBaron(original_code)

for node in root.find_all("StringNode"):
    if (
        has_chinese_char(node)
        and not is_aleady_gettext(node)
        and not is_docstring(node)
    ):
        node.replace("_({})".format(node))

modified_code = root.dumps()
复制代码

我把完整的代码放到了 Gist 上,由于是一个一次性脚本,写的比较随意,你们能够参考。

使用 redbaron 的过程当中也发现了一些问题,一并记录这里:最大问题是 redbaron 已经中止维护了!因此不能支持一些新语法,好比 Python3.6 的 f-string。其次是这个库和 ast 标准库相比,运行速度很慢,每次跑这个脚本个人电脑都发出了飞机引擎般的声音。第三点是会产生一些奇怪的格式:

修改前:

OutStockSheet = {
    1: '未出库',
    2: '已出库',
    3: '已删除'
}
复制代码

修改后('已删除' 右边的括号跑到了下一行):

OutStockSheet = {
    1: _('未出库'),
    2: _('已出库'),
    3: _('已删除'
)}
复制代码

最后一点却是能够经过格式化工具解决,问题不大。

utf8 vs utf-8

项目中有些 py 文件比较老,在文件开头使用了 # coding: utf8 的标示。对于 Python 来讲,utf8 是 utf-8 的别名,因此没有任何问题。Django 在调用 GNU gettext 时,会使用参数指定编码为 utf-8,可是 GNU 也会读取文件中的编码标示,并且它的优先级更高。不幸的是 utf8 对 GNU gettext 来讲是一个未知编码,因而 GNU gettext 会降级使用 ASCII 编码,而后在遇到中文字符时报错(真笨!):

$ python3 manage.py makemessages --local en
...
xgettext: ./path/filename.py:1: Unknown encoding "utf8". Proceeding with ASCII instead.
xgettext: Non-ASCII comment at or before ./path/filename.py:26.
复制代码

因此我须要把 # coding: utf8 改为 # coding: utf-8,或者干脆删掉这行,反正 Python3 已经默认使用 utf-8 编码了。

总结

Django (和其背后的 GNU gettext) 的多语言功能很是全面,堪称博大精深,好比处理单复数的 ngettext,处理多义词的 pgettext。HTTP 响应中使用翻译后的文本,可是在日志中留下翻译前文本的 gettext_noop

这篇文章主要讲了我在实践中用到的功能和遇到的坑,但愿能够帮助你们了解 Django 多语言的基本用法。欢迎你们评论👏。


知识共享协议

本文采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

相关文章
相关标签/搜索