原文連接: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 系統,文字甚至界面的方向都與中文版的相反(圖片來源)django
Django 做爲一個大而全的框架,已經提供了一套多語言的解決方案,我稍微對比了一下,並沒能找到在 Django 體系下比官方方案還好用的庫。Django 的方案能夠簡單分爲四步:ubuntu
makemessages
命令生成 po 文件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
複製代碼
$ brew install gettext
$ brew link --force gettext
複製代碼
安裝完 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
出如今哪一個文件的哪一行。
關於這個文件,發現幾點有趣的特性:
msgid
歸類在一塊兒。「一次編輯,處處翻譯」msgid
被刪了,那麼再次執行 makemessages
命令後,這個 msgid
和它的 msgstr
會以註釋的形式繼續保存在 django.po
中。_("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 的語言設置
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 的官方文檔。
咱們以前講過 LocaleMiddleware
能夠決定使用何種語言。具體來講,LocaleMiddleware
是按照下面的順序(優先級遞減):
i18n_patterns
request.session[settings.LANGUAGE_SESSION_KEY]
request.COOKIES[settings.LANGUAGE_COOKIE_NAME]
request.META['HTTP_ACCEPT_LANGUAGE']
,即 HTTP 請求中的 Accept-Language
headersettings.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')
複製代碼
Python2 時代,爲了區分 unicode strings 和 bytestrings,有 ugettext
和 gettext
兩個函數。在 Python3 中,因爲字符串編碼的統一,ugettext
和 gettext
是等價的。官方說將來可能會廢棄 ugettext
,可是截止到如今(Django 2.0),ugettext
還沒廢棄。
這裏先用一個例子直觀地看一下 gettext_lazy
和 gettext
的區別
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'),
)
複製代碼
因爲我司工程很是龐大,人力給每一個字符串添加 _( ... )
過於繁瑣。因此我試圖尋找一種自動化的方式。
一開始選擇的是 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 中國大陸許可協議進行許可。