---
title: asaloader i18n 與 l10n 處理
tags: asaloader, mvmc-lab
---
# asaloader i18n 與 l10n 處理
## i18n 與 l10n
W3C 將 i18n 與 l10n 的概念及對應工作描述的很到位。
> Internationalization is the design and development of a product, application or document content that enables easy localization for target audiences that vary in culture, region, or language.
>
> Localization refers to the adaptation of a product, application or document content to meet the language, cultural and other requirements of a specific target market (a locale).”
>
> [name= W3C] https://www.w3.org/International/questions/qa-i18n
「程式國際化」簡稱 I18N,其意為 InternationalizatioN 一字中頭尾字母 "I" 與 "N" 中間夾 18 個英文字母,故名。其代表將一個產品做一些處理,讓該產品能夠簡單的進行下一步在地化的工作,包括語言、文化等。像提供可翻譯的文檔,提供翻譯的管道,皆是i18n對應的工作。
「資料本土化」簡稱 L10N,其意為 LocalizatioN 一字中頭尾字母 "L" 與 "N"中間夾 10 個英文字母,故名。其代表將一個產品中的部分內容替換,以符合特定市場或地區的語言、文化等。
對asaloader來說,須要做國際化及在地化的地方便是CLI介面,讓不同語言的使用者皆可以使用自己的語言來判讀CLI介面的輸出。所以在設計程式時,便要考慮如何判斷使用者的語言、載入各語言對應的文字的解決方法。並要能提供一個好個翻譯方式,讓不懂程式的譯者能翻譯此程式。
## GetText
asaloader 使用 GetText 來解決 i18n 的問題。
GetText 是一整套 i18n 解決方案,並面向程式開發,現由 GNU 所開發維護。其目標是解決 GNU 在開發程式模組時處理各語言翻譯所遇到的困難,其提供了函式庫,並建議了程式的撰寫方式。讓開發程式時,不需要再耗費心力去實現i18n的框架,也不需要因翻譯問題而改變太多程式的撰寫方式。
- GetText 官方介紹:https://www.gnu.org/software/gettext/
- GetText 官方文檔:https://www.gnu.org/software/gettext/manual/gettext.html
- wiki gettext: https://zh.wikipedia.org/wiki/Gettext
以下部分內容節錄及修改自wiki。
### 開發
程式原始碼需要進行修改以回應GNU gettext請求。多數程式語言均已通過字元封裝的方式實現了對其的支援。為了減少輸入量和代碼量,此功能通常以標記別名「\_」的形式使用,所以例如以下python語言代碼:
```python
print(gettext("My name is %s.\n").format(my_name));
```
應當寫作:
```python
print(_("My name is %s.\n").format(my_name));
```
gettext使用其中的字串尋找對應的其他語言翻譯,若沒有可用翻譯則返回原始內容。
### .pot 檔案 翻譯模板檔
副檔名 `.pot`(portable object template) 檔案是翻譯模板檔,翻譯人員可透過閱讀此檔案,並將檔案中文字翻譯,生成`.po`翻譯檔案。
翻譯模板可透過 GNU 所提供的 xgettext 程式來生成,xgettext 透過讀取原始碼中,偵測以`_()`所框住之文字來生成,使用方式如[連結](https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html)所示。
pot 的格式如下:
```
#: src/__main__.py:36
msgid "My name is %s.\n"
msgstr ""
```
### .po 檔案 翻譯文字檔
副檔名 `.po`(portable object) 檔案是翻譯文字檔,翻譯人員藉由翻譯`.pot`檔案,來產生`.po`檔案。
翻譯人員複製`.pot`檔案成`.po`檔案,並將`msgid`對應的文字填入`msgstr`中即可,範例如下。
```
#: src/__main__.py:36
msgid "My name is %s.\n"
msgstr "我的名子是%s。\n"
```
現在亦有許多工具可以協助翻譯,例如Poedit、gtranslator、OmegaT等,也有現成的服務可以自動生成不同語言的翻譯檔案。個人推薦使用Poedit軟體,很好比對目前的翻譯進度,並有一天10比的免費線上翻譯。付費版本也只需要1000台幣左右,對常用的人價錢真的滿划算的。
### .mo檔案 翻譯索引檔
副檔名 `.mo` 檔案是翻譯索引檔,此檔案是經由`.po`檔案編譯而成,加快文字查找的速度。最後gettext會自此檔案中查找文字對應的翻譯。
編譯`.mo`時,通常使用GNU所提供的`msgfmt`來完成,其使用方式如[連結](https://www.gnu.org/software/gettext/manual/html_node/msgfmt-Invocation.html)所示。
## POSIX locale
https://www.csie.ntu.edu.tw/~r95053/samples/collection/backup/locale2.html
## asaloader 如何完成 i18n
asaloadr 主要使用 locale 模組來完成 i18n 相關工作。locale 模組提供了作業系統語言查找的功能,並提供與 gettext 互動的API。
### 系統語言偵測
```python
def get_shell_lc() -> str:
"""取得當前shell的語言代碼。
Returns:
str: 當前shell的語言代碼。
"""
loc = locale.getdefaultlocale()
lc = loc[0]
return lc
```
語言偵測的方式如上面程式碼,使用`locale.getdefaultlocale()`取得當前shell的`locale`設定,回傳一個Tuple物件,格式為`(language code, encoding)`。第一個元素是語言代碼,第二個元素為編碼格式,通常為`UTF-8`。我們將第一個元素取出作為函式`get_shell_lc`的回傳,供後續使用。
語言代碼為ISO 639-1為語言分類的設計的一套標準代碼,每種語言都被分配了兩個字母,若有不同地區上的差異,則會在後方加上底線`_`以及國家代碼以區分。像`zh_TW`、`zh_CN`,`zh`代表中文,而其後國家代碼`TW`台灣、`CN`中國,則代表兩個不同的國家使用的中文。
可以透過`locale`指令,查看shell的地區設置。
```
> locale
LANG=zh_TW.UTF-8
LC_CTYPE="zh_TW.UTF-8"
LC_NUMERIC="zh_TW.UTF-8"
LC_TIME="zh_TW.UTF-8"
LC_COLLATE="zh_TW.UTF-8"
LC_MONETARY="zh_TW.UTF-8"
LC_MESSAGES="zh_TW.UTF-8"
LC_ALL=
```
可參考:
- [wiki 國家代碼 ISO_3166-1](https://en.wikipedia.org/wiki/ISO_3166-1)
- [wiki 語言代碼 ISO_639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
- [wiki 文字編碼](https://en.wikipedia.org/wiki/Character_encoding)
### 設定語言
asaloader透過函式`update_lc`來更新輸出所使用的語言,程式碼如下。
```python
def update_lc() -> None:
"""依據語言代碼更新當前應用的語言
此函式會解析 sys.argv 傳入的參數,取得設定的語言,
去更新字串翻譯函式`_`。
若 sys.argv 沒有,則使用 shell 的語言代碼,
去更新字串翻譯函式`_`。
"""
# 翻譯索引搜索位置
package_dir = path.abspath(path.dirname(__file__))
locale_dir = path.join(package_dir, '../locale')
lc = None
# 如果有透過CLI設定語言,則使用該語言
if '--lc' in sys.argv:
i = sys.argv.index('--lc')
# 判斷 --lc 其後是否有尾隨參數
if len(sys.argv) > i + 1:
lc = sys.argv[i + 1]
# 如果沒有,取得系統語言
if lc is None:
lc = get_shell_lc()
global _
# 如果找的到語言對應的翻譯索引,則使用該檔案
if gettext.find('asaloader', locale_dir, [lc]):
translate = gettext.translation('asaloader', locale_dir, [lc])
_ = translate.gettext
# 如果找不到,則使函式`_`回傳輸入字串
else:
translate = None
def _(s): return s
```
1. 翻譯索引搜索位置
`locale`模組預設會搜索指定資料夾中的翻譯索引,翻譯索引`.mo`要放在規定的相對位置`locale/<語言代碼>/LC_MESSAGES`中,而翻譯樣板`.pot`則通常放在`locale`資料夾下。
`package_dir`會利用變數`__file__`來取得模組的安裝位置,若使用pip安裝則會在`site-packages`資料夾中。
之後便可以利用變數`package_dir`,來取得資料夾`locale`的絕對位置了,以供後續使用。
2. 設定語言
解析CLI傳入的參數,並取得語言設定參數`--lc`的設定值。這邊因為要在一開始就進行語系設定,在讓轉換完成的字串輸入到CLI參數解析器中,所以這邊不能透過CLI參數解析器來取得參數。必須要手動解析`sys.argv`,並要自行判斷是否合法,判斷使用者沒有輸入參數。
如果使用者沒有設定參數`--lc`,則使用`get_shell_lc`來取得shell設定的參數。
3. 取得翻譯索引,應用到 gettext 中
最後利用函式`gettext.find`來判斷資料夾`locale_dir`中是否有該語言的翻譯索引。如果有的話,便使用該翻譯索引生成的`gettext`取代當前的字串翻譯函式`_`。如果沒有,則讓字串翻譯函式`_`回傳輸入得字串,不要進行任何翻譯。
可參考:
- [locale 模組 gettext 設定方式(官方文件)](https://docs.python.org/3/library/gettext.html#gettext.NullTranslations.install)
註:
第一,網路上很多方法使用函式`gettext.install`來完成這件事情,該函式已經被棄用了,之前測試也無法使用。
第二,網路上的方法大多使用locale模組的GNU gettext API的方式。在測試的時候,許多方式皆無法使用,本人沒有詳細研究,也忘記當時追蹤到的原因了。後來則改為使用官方推薦的Class-based API,並手動更改函式`_`。
> The gettext module defines the following API, which is very similar to the GNU gettext API. If you use this API you will affect the translation of your entire application globally. Often this is what you want if your application is monolingual, with the choice of language dependent on the locale of your user. If you are localizing a Python module, or if your application needs to switch languages on the fly, you probably want to use the class-based API instead.
> [name= python gettext document]
### 語言更新的時機
以上函式皆放在`asaloader/locale.py中`,如下程式碼所示。
```python
...
__all__ = ['_']
def _(s: str) -> str:
...
def update_lc() -> None:
...
update_lc()
```
在要使用字串翻譯函式`_`時,便需要從此檔案中引用函式`from asaloader.locale import _`,這時便會執行函式`update_lc()`來更新語言了。
所以從CLI介面操作時,首先進入`__main__.py`,最開頭的引用便會依據設定將語言更新,之後才會進入到主函式`run`中來生成CLI解析器。如此一來便可以在解析器生成之前便完成語言更新的動作,讓使用者可以看到對應的語言。
## 生成翻譯文字檔
這邊使用poedit作為範例,首先利用poedit來開啟asaloader的翻譯模板`locale/asaloader.pot`,點選「建立新譯文」,然後在「譯文語言:」欄位輸入要翻譯的語言代碼,或直接透過列表選擇。這邊選擇日文作為範例,語言代碼為`ja`。
![](https://i.imgur.com/FZuz7HY.png)
這時可以看到左上方會顯示對應的`語言代碼.mo`,我們先進行最重要的動作,存檔!將檔案放到`locale/<語言代碼>/LC_MESSAGES`中,並取名為`asaloader.po`。以這邊為例,翻譯文字檔案的相對路徑就會為`locale/ㄇㄨja/LC_MESSAGES`。
![](https://i.imgur.com/z6tDIgs.png)
接著就可以進行翻譯的動作了。如果有付費的話,可以先選擇前置翻譯,從線上資料庫中進行自動翻譯。如果沒有,則需要手動一條一條翻譯,在譯文處輸入文字即可。若有不確定的地方,可以先點選待校閱,軟體會以高亮提醒此譯文還需要確認,存檔前也會進行提醒,相當好用。
![](https://i.imgur.com/IQpWLMt.png)
完成翻譯後,便可以使用「驗證」來查看是否有文法或格式錯誤,如果有錯誤便會有橘色驚嘆號提醒。在圖片中可以看到翻譯文字最前面的空白漏掉了,補上後便不會在有警告。
![](https://i.imgur.com/UF1ZOj6.png)
翻譯完可以看看統計資料,看自己翻譯了多少文字,或是否有缺漏。
![](https://i.imgur.com/wNOWEoE.png)
最後,記得儲存,commit,push!
## 依據 pot 檔進行更新
若程式的輸出訊息有進行更動,翻譯模板也會更著更新。這時可以利用「從 pot/po 檔案新增」功能,來選擇新的翻譯模板。軟體便會提示差異位置,協助你一步步完成修改,而不需要從翻譯一次。
![](https://i.imgur.com/XZDVhN2.png)
## assloader 更新翻譯模板及生成翻譯索引
執行檔案`utils/l10n.py`會去生成新的翻譯模板,並將現有的`.po`檔案,也一併轉換成`.mo`檔案。此檔案也會在模組安裝的時候執行,因為模組發佈時,並沒有連同.mo一起發佈。使用者透過pip->setup.py安裝時,才會執行此檔案來生成翻譯索引。