Django 多語言教程的實(shí)現(xiàn)(i18n)
最近公司準(zhǔn)備擴(kuò)張海外業(yè)務(wù),所以要給 Django 系統(tǒng)添加 國際化與本土化 支持。國際化一般簡稱 i18n ,代表 Internationalization 中 i 和 n 有 18 個字母;本地化簡稱 L10n ,表示 Localization 中 l 和 n 中有 10 個字母。有趣的一點(diǎn)是,一般會用小寫的 i 和大寫的 L 防止混淆。
簡單來說:i18n 是為國際化搭建框架,L10n 是針對不同地區(qū)的適配。舉個簡單的例子:
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 的范圍非常廣,包括多語言、時區(qū)、貨幣單位、單復(fù)數(shù)、字符編碼甚至是文字閱讀順序(RTL)等等。這篇文章只關(guān)注 i18n 的多語言 方面。
↑ 阿拉伯語的 windows 系統(tǒng),文字甚至界面的方向都與中文版的相反
基本步驟
Django 作為一個大而全的框架,已經(jīng)提供了一套多語言的解決方案,我稍微對比了一下,并沒能找到在 Django 體系下比官方方案還好用的庫。Django 的方案可以簡單分為四步:
- 一些必要的配置
- 在代碼中標(biāo)記需要翻譯的文本
- 使用 makemessages 命令生成 po 文件
- 編譯 compilemessages 命令編譯 mo 文件
下面我們詳細(xì)來看看
第一步:配置
首先在 settings.py 中加入這幾個內(nèi)容
LOCALE_PATHS = ( os.path.join(__file__, 'language'), ) MIDDLEWARE = ( ... 'django.middleware.locale.LocaleMiddleware', ... ) LANGUAGES = ( ('en', 'English'), ('zh', '中文'), )
LOCALE_PATHS
:指定下面第三步和第四步生成文件的位置。老版的 Django 需要手動新建好這個目錄。
LocaleMiddleware
:可以讓 Django 識別并選擇合適的語言。
LANGUAGES
:指定了這個工程能提供哪些語言。
第二步:標(biāo)記文本
之前沒有多語言的需要,所以大家在 AJAX 相應(yīng)代碼中直接寫了中文,比如這樣:
return JsonResponse({"msg": "內(nèi)容過長", "code": 1, "data": None})
現(xiàn)在需要多語言了,就需要告訴 Django 哪些內(nèi)容是需要翻譯的。對于上面的例子來說,就是寫成這樣:
from django.utils.translation import gettext as _ return JsonResponse({"msg": _("內(nèi)容過長"), "code": 1, "data": None})
這里使用 gettext
函數(shù)將原本的字符串包裹起來,這樣的話,Django 就可以根據(jù)當(dāng)前語言返回合適的字符串。一般會使用單個下劃線 _
提高可讀性。
因?yàn)槲宜編缀跛星昂蠖送ㄐ哦际褂?AJAX,所以并沒有怎么用上 Django 的模板功能(順便一提,我司前端使用的多語言工具是 i18next )。不過在這里也一并寫下 Django 模板的標(biāo)記方法:
<title>{% trans "This is the title." %}</title> <title>{% trans myvar %}</title>
其中 trans
標(biāo)簽告訴 Django 需要翻譯這個括號里面的內(nèi)容。更具體的用法可以參考官方文檔。
第三步: makemessages
在執(zhí)行這一步之前,請先通過 xgettext --version
確認(rèn)自己是否安裝了GNU gettext。GNU gettext 是一個標(biāo)準(zhǔn) i18n L10n 庫,Django 和很多其他語言和庫的多語言模塊都調(diào)用了 GNU gettext,所以接下來講的一些 Django 特性實(shí)際上要?dú)w功于 GNU gettext。如果沒有安裝的話可以通過下面的方法安裝:
ubuntu:
$ apt update $ apt install gettext
$ brew install gettext $ brew link --force gettext
安裝完 GNU gettext 后,對 Django 工程執(zhí)行下面的命令
$ python3 manage.py makemessages --local en
之后可以找到生成的文件: language/en/LC_MESSAGES/django.po
。把上面命令中的 en
替換成其他語言,就可以生成不同語言的 django.po
文件。里面的內(nèi)容大概是這樣的:
#: path/file.py:397 msgid "訂單已刪除" msgstr "" ...
Django 會找到被 gettext
函數(shù)包裹的所有字符串,以 msgid
的形式保存在 django.po
。每個 msgid
下面的 msgstr
就代表你要把這個 msgid
翻譯成什么。通過修改這個文件可以告訴 Django 翻譯的內(nèi)容。同時通過注釋說明了這個 msgid
出現(xiàn)在哪個文件的哪一行。
關(guān)于這個文件,發(fā)現(xiàn)幾點(diǎn)有趣的特性:
- Django 會把多個文件中相同的 msgid 歸類在一起?!敢淮尉庉嫞教幏g」
- 如果以后源碼中某個 msgid 被刪了,那么再次執(zhí)行 makemessages 命令后,這個 msgid 和它的 msgstr 會以注釋的形式繼續(xù)保存在 django.po 中。
- 既然源碼中的字符串只是一個所謂的 id,那么我就可以在源碼中寫沒有實(shí)際含義的字符串,比如 _("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
文件后,執(zhí)行下面的命令:
$ python3 manage.py compilemessages --local en
Django 會調(diào)用程序,根據(jù) django.po
編譯出一個名為 django.mo
的二進(jìn)制文件,位置和 django.po
所在位置相同。這個文件才是程序執(zhí)行的時候會去讀取的文件。
執(zhí)行完上面四步后,修改瀏覽器的語言設(shè)置,就可以看到 Django 的不同輸出了。
↑ Chrome 的語言設(shè)置
高級特性
i18n_patterns
有的時候,我們希望可以通過 URL 來選擇不同的語言。這樣做有很多好處,比如同一個 URL 返回的數(shù)據(jù)的語言一定是一致的。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), ])
詳細(xì)的做法可以參考 Django 的官方文檔。
Django 如何決定使用哪種語言
我們之前講過 LocaleMiddleware
可以決定使用何種語言。具體來說, LocaleMiddleware
是按照下面的順序(優(yōu)先級遞減):
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 中,當(dāng)用戶手動選擇語言時,可以讓前端直接修改 Cookies,而不需要請求后臺的某個接口。沒有手動設(shè)置過語言的用戶就沒有這個 Cookies,跟隨瀏覽器設(shè)置。話說 settings.LANGUAGE_COOKIE_NAME
的默認(rèn)值是 django_language
,前端不想在他們的代碼中出現(xiàn) django
,所以我在 settings.py
中添加了 LANGUAGE_COOKIE_NAME = app_language
:joy:。
你也可以通過 request.LANGUAGE_CODE
在 View 中手動獲知 LocaleMiddleware
選用了哪種語言。你甚至可以通過 activate
函數(shù)手動指定當(dāng)前線程使用的語言:
from django.utils.translation import activate activate('en')
ugettext
Python2 時代,為了區(qū)分 unicode strings 和 bytestrings,有 ugettext
和 gettext
兩個函數(shù)。在 Python3 中,由于字符串編碼的統(tǒng)一, ugettext
和 gettext
是等價的。官方說未來可能會廢棄 ugettext
,但是截止到現(xiàn)在(Django 2.0), ugettext
還沒廢棄。
gettext_lazy
這里先用一個例子直觀地看一下 gettext_lazy
和 gettext
的區(qū)別
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
函數(shù)返回的是一個字符串,但是 gettext_lazy
返回的是一個代理對象。這個對象會在被使用的時候,才根據(jù)當(dāng)前線程中語言決定翻譯成什么文字。
這個功能在 Django 的 models 中尤其的有用。因?yàn)?models 中定義字符串的代碼只會執(zhí)行一次。在之后的請求中,根據(jù)語言的不同,這個所謂字符串要有不同的表現(xiàn)。
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 內(nèi)置的 ast
(Abstract syntax tree 語法抽象樹) 模塊 ?;舅悸肥峭ㄟ^ ast
找到工程中的所有字符串,再給這些字符串添加 _( ... )
。最后把修改后的語法樹重新轉(zhuǎn)為代碼。
但是由于 ast
對格式信息的支持不佳,修改代碼后容易造成格式混亂。所以找到了名為 FST (Full Syntax Tree 全面抽象樹) 的改進(jìn)方式。我選擇的 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 上,因?yàn)槭且粋€一次性腳本,寫的比較隨意,大家可以參考。
使用 redbaron
的過程中也發(fā)現(xiàn)了一些問題,一并記錄這里:最大問題是 redbaron
已經(jīng)停止維護(hù) 了!所以不能支持一些新語法,比如 Python3.6 的 f-string。其次是這個庫和 ast
標(biāo)準(zhǔn)庫相比,運(yùn)行速度很慢,每次跑這個腳本我的電腦都發(fā)出了飛機(jī)引擎般的聲音。第三點(diǎn)是會產(chǎn)生一些奇怪的格式:
修改前:
OutStockSheet = { 1: '未出庫', 2: '已出庫', 3: '已刪除' }
修改后( '已刪除'
右邊的括號跑到了下一行):
OutStockSheet = { 1: _('未出庫'), 2: _('已出庫'), 3: _('已刪除' )}
最后一點(diǎn)倒是可以通過格式化工具解決,問題不大。
utf8
vs utf-8
項(xiàng)目中有些 py 文件比較老,在文件開頭使用了 # coding: utf8
的標(biāo)示。對于 Python 來說,utf8 是 utf-8 的別名,所以沒有任何問題。Django 在調(diào)用 GNU gettext 時,會使用參數(shù)指定編碼為 utf-8,但是 GNU 也會讀取文件中的編碼標(biāo)示,而且它的優(yōu)先級更高。不幸的是 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 已經(jīng)默認(rèn)使用 utf-8 編碼了。
總結(jié)
Django (和其背后的 GNU gettext) 的多語言功能非常全面,堪稱博大精深,比如處理單復(fù)數(shù)的ngettext,處理多義詞的pgettext。HTTP 響應(yīng)中使用翻譯后的文本,但是在日志中留下翻譯前文本的gettext_noop。
這篇文章主要講了我在實(shí)踐中用到的功能和遇到的坑,希望可以幫助大家了解 Django 多語言的基本用法。歡迎大家評論:clap:。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
python 二維數(shù)組90度旋轉(zhuǎn)的方法
今天小編就為大家分享一篇python 二維數(shù)組90度旋轉(zhuǎn)的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-01-01python實(shí)現(xiàn)修改xml文件內(nèi)容
這篇文章主要介紹了python實(shí)現(xiàn)修改xml文件內(nèi)容,XML 指可擴(kuò)展標(biāo)記語言,是一種標(biāo)記語言,是從標(biāo)準(zhǔn)通用標(biāo)記語言(SGML)中簡化修改出來的2022-07-07python簡單實(shí)現(xiàn)矩陣的乘,加,轉(zhuǎn)置和逆運(yùn)算示例
這篇文章主要介紹了python簡單實(shí)現(xiàn)矩陣的乘,加,轉(zhuǎn)置和逆運(yùn)算,結(jié)合實(shí)例形式分析了Python針對矩陣的乘,加,轉(zhuǎn)置和求逆等運(yùn)算相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-07-07基于Python的接口自動化unittest測試框架和ddt數(shù)據(jù)驅(qū)動詳解
這篇文章主要介紹了基于Python的接口自動化unittest測試框架和ddt數(shù)據(jù)驅(qū)動詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01python自動化測試Data?Driven?Testing(DDT)用例解析
這篇文章主要為大家介紹了python自動化測試Data?Driven?Testing(DDT)用例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09Python?nonlocal關(guān)鍵字?與?global?關(guān)鍵字解析
這篇文章主要介紹了Python?nonlocal關(guān)鍵字?與?global?關(guān)鍵字解析,nonlocal關(guān)鍵字用來在函數(shù)或其他作用域中使用外層變量,global關(guān)鍵字用來在函數(shù)或其他局部作用域中使用全局變量,更多香瓜內(nèi)容需要的小伙伴可以參考一下2022-03-03解決python中的冪函數(shù)、指數(shù)函數(shù)問題
今天小編就為大家分享一篇解決python中的冪函數(shù)、指數(shù)函數(shù)問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11