最近公司準備擴張海外業務,所以要給 django 系統添加 國際化與本土化 支持。國際化一般簡稱 i18n ,代表 internationalization 中 i 和 n 有 18 個字母;本地化簡稱 l10n ,表示 localization 中 l 和 n 中有 10 個字母。有趣的一點是,一般會用小寫的 i 和大寫的 l 防止混淆。
簡單來說:i18n 是為國際化搭建框架,l10n 是針對不同地區的適配。舉個簡單的例子:
i18n:
1
2
|
datetime.now().strftime( '%y/%m/%d' ) # before i18n datetime.now().strftime(timeformat) # after i18n |
l10n:
1
2
3
4
5
6
|
timeformat = { 'cn' : '%y/%m/%d' , 'us' : '%m/%d/%y' , 'fr' : '%d/%m/%y' , ... } |
更加具體的定義可以看https://www.w3.org/international/questions/qa-i18n的解釋。
i18n 的范圍非常廣,包括多語言、時區、貨幣單位、單復數、字符編碼甚至是文字閱讀順序(rtl)等等。這篇文章只關注 i18n 的多語言 方面。
↑ 阿拉伯語的 windows 系統,文字甚至界面的方向都與中文版的相反
基本步驟
django 作為一個大而全的框架,已經提供了一套多語言的解決方案,我稍微對比了一下,并沒能找到在 django 體系下比官方方案還好用的庫。django 的方案可以簡單分為四步:
- 一些必要的配置
- 在代碼中標記需要翻譯的文本
- 使用 makemessages 命令生成 po 文件
- 編譯 compilemessages 命令編譯 mo 文件
下面我們詳細來看看
第一步:配置
首先在 settings.py 中加入這幾個內容
1
2
3
4
5
6
7
8
9
10
11
12
|
locale_paths = ( os.path.join(__file__, 'language' ), ) middleware = ( ... 'django.middleware.locale.localemiddleware' , ... ) languages = ( ( 'en' , 'english' ), ( 'zh' , '中文' ), ) |
locale_paths
:指定下面第三步和第四步生成文件的位置。老版的 django 需要手動新建好這個目錄。
localemiddleware
:可以讓 django 識別并選擇合適的語言。
languages
:指定了這個工程能提供哪些語言。
第二步:標記文本
之前沒有多語言的需要,所以大家在 ajax 相應代碼中直接寫了中文,比如這樣:
1
|
return jsonresponse({ "msg" : "內容過長" , "code" : 1 , "data" : none}) |
現在需要多語言了,就需要告訴 django 哪些內容是需要翻譯的。對于上面的例子來說,就是寫成這樣:
1
2
3
|
from django.utils.translation import gettext as _ return jsonresponse({ "msg" : _( "內容過長" ), "code" : 1 , "data" : none}) |
這里使用 gettext
函數將原本的字符串包裹起來,這樣的話,django 就可以根據當前語言返回合適的字符串。一般會使用單個下劃線 _
提高可讀性。
因為我司幾乎所有前后端通信都使用 ajax,所以并沒有怎么用上 django 的模板功能(順便一提,我司前端使用的多語言工具是 )。不過在這里也一并寫下 django 模板的標記方法:
1
2
|
<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:
1
2
|
$ apt update $ apt install gettext |
1
2
|
$ brew install gettext $ brew link - - force gettext |
安裝完 gnu gettext 后,對 django 工程執行下面的命令
1
|
$ python3 manage.py makemessages - - local en |
之后可以找到生成的文件: language/en/lc_messages/django.po
。把上面命令中的 en
替換成其他語言,就可以生成不同語言的 django.po
文件。里面的內容大概是這樣的:
1
2
3
4
5
|
#: 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:
1
2
|
_( 'today is {month} {day}.' ). format (month = m, day = d) _( 'today is %(month)s %(day)s.' ) % { 'month' : m, 'day' : d} |
po file
1
2
3
4
5
|
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
文件后,執行下面的命令:
1
|
$ python3 manage.py compilemessages - - local en |
django 會調用程序,根據 django.po
編譯出一個名為 django.mo
的二進制文件,位置和 django.po
所在位置相同。這個文件才是程序執行的時候會去讀取的文件。
執行完上面四步后,修改瀏覽器的語言設置,就可以看到 django 的不同輸出了。
↑ chrome 的語言設置
高級特性
i18n_patterns
有的時候,我們希望可以通過 url 來選擇不同的語言。這樣做有很多好處,比如同一個 url 返回的數據的語言一定是一致的。django 的文檔就使用了這種做法:
簡體中文:https://docs.djangoproject.com/zh-hans/2.0/
英文:https://docs.djangoproject.com/en/2.0/
具體的做法是在 url 中添加 <slug:slug>
1
2
3
4
|
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
:joy:。
你也可以通過 request.language_code
在 view 中手動獲知 localemiddleware
選用了哪種語言。你甚至可以通過 activate
函數手動指定當前線程使用的語言:
1
2
3
|
from django.utils.translation import activate activate( 'en' ) |
ugettext
python2 時代,為了區分 unicode strings 和 bytestrings,有 ugettext
和 gettext
兩個函數。在 python3 中,由于字符串編碼的統一, ugettext
和 gettext
是等價的。官方說未來可能會廢棄 ugettext
,但是截止到現在(django 2.0), ugettext
還沒廢棄。
gettext_lazy
這里先用一個例子直觀地看一下 gettext_lazy
和 gettext
的區別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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 中定義字符串的代碼只會執行一次。在之后的請求中,根據語言的不同,這個所謂字符串要有不同的表現。
1
2
3
4
5
6
7
8
9
10
11
12
|
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
。核心的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
|
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
標準庫相比,運行速度很慢,每次跑這個腳本我的電腦都發出了飛機引擎般的聲音。第三點是會產生一些奇怪的格式:
修改前:
1
2
3
4
5
|
outstocksheet = { 1 : '未出庫' , 2 : '已出庫' , 3 : '已刪除' } |
修改后( '已刪除'
右邊的括號跑到了下一行):
1
2
3
4
5
|
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 編碼,然后在遇到中文字符時報錯(真笨!):
1
2
3
4
|
$ 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:。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://juejin.im/post/5b3efc36e51d45197136eb09