Django 多语言教程的实现(i18n)


Posted in Python onJuly 07, 2018

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

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

i18n:

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

L10n:

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

更加具体的定义可以看https://www.w3.org/International/questions/qa-i18n的解释。

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

Django 多语言教程的实现(i18n)

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

基本步骤

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

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

下面我们详细来看看

第一步:配置

首先在 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 出现在哪个文件的哪一行。

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

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

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 的不同输出了。

Django 多语言教程的实现(i18n)

↑ 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 是按照下面的顺序(优先级递减):

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

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

你也可以通过 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 多语言的基本用法。欢迎大家评论:clap:。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
利用python生成一个导出数据库的bat脚本文件的方法
Dec 30 Python
Python实现获取磁盘剩余空间的2种方法
Jun 07 Python
Python简单定义与使用字典dict的方法示例
Jul 25 Python
Scrapy框架CrawlSpiders的介绍以及使用详解
Nov 29 Python
Anaconda2下实现Python2.7和Python3.5的共存方法
Jun 11 Python
python3去掉string中的标点符号方法
Jan 22 Python
Python3几个常见问题的处理方法
Feb 26 Python
Django项目使用CircleCI的方法示例
Jul 14 Python
python中eval与int的区别浅析
Aug 11 Python
Python进程,多进程,获取进程id,给子进程传递参数操作示例
Oct 11 Python
pytorch实现保证每次运行使用的随机数都相同
Feb 20 Python
python实现画图工具
Aug 27 Python
python利用requests库进行接口测试的方法详解
Jul 06 #Python
python生成密码字典的方法
Jul 06 #Python
Python 3.x 判断 dict 是否包含某键值的实例讲解
Jul 06 #Python
使用python中的in ,not in来检查元素是不是在列表中的方法
Jul 06 #Python
python 实现将字典dict、列表list中的中文正常显示方法
Jul 06 #Python
Python判断中文字符串是否相等的实例
Jul 06 #Python
Python中使用Counter进行字典创建以及key数量统计的方法
Jul 06 #Python
You might like
ThinkPHP权限认证Auth实例详解
2014/07/22 PHP
帝国cms常用标签汇总
2015/07/06 PHP
PHP创建/删除/复制文件夹、文件
2016/05/03 PHP
laravel框架学习笔记之组件化开发实现方法
2020/02/01 PHP
一文看懂PHP进程管理器php-fpm
2020/06/01 PHP
javascript document.images实例
2008/05/27 Javascript
原生JavaScript生成GUID的实现示例
2014/09/05 Javascript
jQuery模拟新浪微博首页滚动效果的方法
2015/03/11 Javascript
轻松使用jQuery双向select控件Bootstrap Dual Listbox
2015/12/13 Javascript
Bootstrap三种表单布局的使用方法
2016/06/21 Javascript
JavaScript Ajax实现异步通信
2016/12/14 Javascript
js实现复选框的全选和取消全选效果
2017/01/03 Javascript
jQuery动态产生select option下拉列表
2017/03/15 Javascript
关于Vue实现组件信息的缓存问题
2017/08/23 Javascript
微信小程序template模版的使用方法
2019/04/13 Javascript
微信小程序收货地址API兼容低版本解决方法
2019/05/18 Javascript
使用 node.js 模仿 Apache 小部分功能
2019/07/07 Javascript
在 Vue 中编写 SVG 图标组件的方法
2020/02/24 Javascript
Node.js API详解之 dgram模块用法实例分析
2020/06/05 Javascript
[02:33]DOTA2亚洲邀请赛趣味视频之吐真话筒
2018/03/31 DOTA
python使用装饰器和线程限制函数执行时间的方法
2015/04/18 Python
MySQL最常见的操作语句小结
2015/05/07 Python
Python实现代码统计工具(终极篇)
2016/07/04 Python
python的pip安装以及使用教程
2018/09/18 Python
python实现连续图文识别
2018/12/18 Python
python实现集中式的病毒扫描功能详解
2019/07/09 Python
python实现数据分析与建模
2019/07/11 Python
face++与python实现人脸识别签到(考勤)功能
2019/08/28 Python
使用 pytorch 创建神经网络拟合sin函数的实现
2020/02/24 Python
三陽商会官方网站:Sanyo iStore
2019/05/15 全球购物
俄罗斯卫浴采暖及维修用品超级市场:Dkrussia
2020/05/12 全球购物
暑期培训班招生方案
2014/08/26 职场文书
违规违纪检讨书范文
2015/05/06 职场文书
Python中的min及返回最小值索引的操作
2021/05/10 Python
python中mongodb包操作数据库
2022/04/19 Python
Python实现双向链表基本操作
2022/05/25 Python