i18n Translation in Django

Internationalization and localization for your Django project.

Configuration

in settings.py

USE_I18N = True

# 默認的語言
# 中文的話是 'zh-tw' 或 'zh-cn',不過 Django 1.7 之後要改用 'zh-hant' 或 'zh-hans'
LANGUAGE_CODE = 'zh-tw'

# 支援的語言,不指定的話就會列出全部的語言
LANGUAGES = (
    ('en', 'English'),
    ('zh-cn', 'Simplified Chinese'),
    ('zh-tw', 'Traditional Chinese'),
)

LOCALE_PATHS = (
    os.path.join(BASE_DIR, 'locale/'),
)

# 要記得加上 LocaleMiddleware,才會針對 request headers 使用對應的語言
MIDDLEWARE_CLASSES = (
    ...
    'django.middleware.locale.LocaleMiddleware',
    ...
)

ref:
https://docs.djangoproject.com/en/dev/ref/settings/#languages
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#how-django-discovers-language-preference

in urls.py

urlpatterns = patterns('',
    ...
    url(r'^i18n/', include('django.conf.urls.i18n')),
    ...
)

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#the-set-language-redirect-view

Usage

in models.py

from django.db import models
from django.utils.translation import ugettext_lazy as _

class HighHeels(models.Model):
    name = models.CharField(_('Name'), max_length=255)
    brand = models.CharField(_('Brand'), max_length=255, null=True, blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)

    class Meta:
        verbose_name = _('High Heels')
        verbose_name_plural = _('High Heels')
        get_latest_by = 'created'
        ordering = ('-created', )

    def __unicode__(self):
        return self.name

在 models.py 中通常都會使用 ugettext_lazy() 而不是 ugettext()。因為 gettext_lazy() 其中的值是在被訪問的時候才翻譯,而不是在呼叫 gettext_lazy() 的時候就翻譯。

另外,ugettext()ugettext_lazy() 出來的字串都是 unicode。

pgettext() 的作用跟 ugettext() 一樣,只是多了一個參數可以傳 context 進去。

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#lazy-translation
https://docs.djangoproject.com/en/dev/ref/utils/#django.utils.translation.pgettext

in views.py

from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.translation import ugettext as _

def home(request):
    contexts = {
        'VAR_1': _('string 1'),
        'VAR_2': _('string 2'),
        'VAR_3': 'string 3',
        'VAR_4': 'string 4',
    }

    return render_to_response('home.html', contexts, RequestContext(request))

要在 ugettext 中使用 string format
必須要用以下的形式:

msg = _(u'您有 %(not_ready_count)s 項行程的尚未填寫「司機備忘錄」。') % {'not_ready_count': not_ready_count}

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#internationalization-in-python-code

in base.html

{% load i18n %}

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Heelstagram</title>
  </head>
  <body>
    {% block content %}{% endblock %}

    <form action="/i18n/setlang/" method="POST">
      {% csrf_token %}
      <input name="next" type="hidden" value="{{ redirect_to }}" />
      <select name="language">
      {% get_language_info_list for LANGUAGES as LANGUAGES %}
      {% for language in LANGUAGES %}
        <option value="{{ language.code }}">{{ language.name_local }} ({{ language.code }})</option>
      {% endfor %}
      </select>
      <input type="submit" value="{% trans 'Change Language' %}" />
    </form>
  </body>
</html>

in home.html

{% extends 'base.html' %}

{# 即使 base.html 已經 load i18n 了,在每個 template 還是得在 load 一次 #}
{% load i18n %}

{% block content %}
<p>current language: "{{ LANGUAGE_CODE }}"</p>

{# 在 .po 中會表示為 msgid "string 1" #}
<p>1: {{ VAR_1 }} 會被翻譯</p>

{# 這兩行是等價的,在 .po 中會表示為 msgid "string 3" #}
<p>2: {% trans VAR_3 %} 會被翻譯</p>
<p>3: {% trans "string 3" %} 會被翻譯</p>

{# 在 .po 中會表示為: #}
{# msgctxt "說明的文字" #}
{# msgid "User" #}
<p>4: {% trans 'User' context '說明的文字' %} 會被翻譯</p>

<p>5: {{ VAR_3 }} 不會被翻譯</p>

{# 在 .po 中會表示為 msgid "%(VAR_2)s 這整句會被翻譯,包含 VAR_2 的值" #}
{# VAR_2 也會被翻譯是因為它等於 _('string 2') #}
{# blocktrans 中的變數不能是 artist.name 這種形式,必須用 artist_name 或是 with #}
<p>6: {% blocktrans %}{{ VAR_2 }} 這整句會被翻譯,包含 VAR_2 的值{% endblocktrans %}</p>

{# VAR_4 不會被翻譯是因為它等於 'string 4',只是一個單純的字串 #}
<p>7: {% blocktrans %}{{ VAR_4 }} 這整句會被翻譯,但是不包含 VAR_4 的值{% endblocktrans %}</p>
{% endblock %}

blocktrans 的作用是讓你在翻譯字串中插入 template contexts 變數。

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#trans-template-tag
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#internationalization-in-template-code

Create Language Files

要先安裝 gettext,否則會出現 /bin/sh: xgettext: command not found 的錯誤

# 必須手動建立 locale 目錄(放在 project 或 app 的根目錄)
$ mkdir -p locale

# 第一次執行,必須指定 locale name,不能直接用 `-a` 參數,這樣才會在 locale 底下產生相對應的目錄
# 注意!是 zh_TW 而不是 zh-tw
# 在專案根目錄執行 makemessages 就是對整個專案的所有 apps 產生翻譯檔案
$ ./manage.py makemessages -l en
$ ./manage.py makemessages -l ja
$ ./manage.py makemessages -l zh_CN
$ ./manage.py makemessages -l zh_TW
$ ./manage.py makemessages --all

# .js 的翻譯要額外指定 `-d djangojs` 才會產生 djangojs.po
$ ./manage.py makemessages -l zh_TW -d djangojs --ignore=node_modules

# 如果你的 django.po 裡有些字串被標記為 `#, fuzzy` 的話,要記得刪掉,否則該字串不會被翻譯
$ ./manage.py compilemessages

# 只對特定 app 產生翻譯檔案
$ cd mamba_client/contrib/django
$ django-admin.py makemessages -l zh_CN
$ django-admin.py compilemessages

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files
https://docs.djangoproject.com/en/dev/ref/django-admin/#makemessages

每次 compilemessages 完要記得重啟 server

How Django discovers translations

django-admin.py makemessages -a 這個指令會去收集整個 project 裡的所有 apps 的 locale 字串。
優先權最高的是 LOCALE_PATHS 定義的那個目錄,找不到的話才會去找個別 app 之下的 locale 目錄。

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#how-django-discovers-translations

強制使用某種語言

除了可以用上面那個 /i18n/setlang/ 的 form 表單之外,也可以在 views 裡面這樣寫:

from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.translation import activate
from django.utils.translation import ugettext as _

def common_processor(request):
    contexts = {
        'T1': _('string 1'),
    }

    return contexts

def home(request):
    activate('zh-tw')  # 強制使用正體中文,覆蓋掉 user 和 browser 的設定

    contexts = {
        'T2': _('string 2'),
        'T3': 'string 3',
    }

    return render_to_response('home.html', contexts, RequestContext(request, processors=[common_processor]))

ref:
https://docs.djangoproject.com/en/dev/ref/utils/#module-django.utils.translation

JavaScript

in urls.py

urlpatterns = patterns(
    '',
    ...
    url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog'),
    ...
)

in your_shit.html

<script src="{% url 'django.views.i18n.javascript_catalog' %}"></script>
<script>
    var text = gettext('要被翻譯的字串');
</script>

gettext() 而不是 ugettext()

# 要加上 -d djangojs 才會去 parse .js 裡的字串
$ ./manage.py makemessages --all -d djangojs

for loop in JavaScript

有 index 的 for loop

for (var i = 0; i < 10; i++) {
   console.log(i);
}

for in 用在 object

不能用在 array

var obj = {a:1, b:2, c:3};

for (var prop in obj) {
  console.log("obj." + prop + " = " + obj[prop]);
}

ref:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in

for of 用在 array(或 iterator 物件)

ES6 才有的語法

let iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value);
}

不然也可以用 Array.forEach()

[2, 5, , 9].forEach(function (element, index, array) {
  console.log('a[' + index + '] = ' + element);
});

ref:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of

Python 的參數傳遞是 call-by-assignment

If you pass a mutable object into a method, the method gets a reference to that same object and you can mutate it to your heart's delight, but if you rebind the reference in the method, the outer scope will know nothing about it, and after you're done, the outer reference will still point at the original object.

If you pass an immutable object to a method, you still can't rebind the outer reference, and you can't even mutate the object.

例如 def foo(bar): 這個 function 的參數傳遞,其實是在 foo 的 local namespace 裡幫傳進去的物件綁定了一個叫做 bar 的名字(所謂的 assignment);如果你在 foo 裡 re-assign 了一個新的物件給 bar,實際上是把 bar 這個名字綁定到那個新的物件。

  • mutable objects: list, dict, set 的行為同 call-by-reference
  • immutable objects: boolean, int, float, long, str, unicode, tuple 的行為同 call-by-value

不過如果你把一個 mutable 物件放進 immutable 物件裡,例如把 list 放進 tuple 裡,則修改了 list 之後,那個 tuple 裡的 list 也是會被修改。

a_list = [1, 2, 3]
a_tuple = (a_list, 'a', 'b', 'c')
a_list.append(4)

Assignment is the binding of a name to an object: name = 'Molly'.
Assignment between names doesn't create a new object, both names are simply bound to the same object: nickname = name.

name = 'Mollie'
name = 'Vinta'

所謂的 assign 這個動作,其實是幫 'Mollie' 這個字串取一個名字叫做 name,所以如果你又加上一句 name = 'Vinta',實際上是建立了一個新的物件(字串 'Vinta'),再把這個新字串綁定到 name 這個名字。

# if bar refers to a mutable object
def foo(bar):
    bar.append('new value')
    print(bar)
    # output: ['old value', 'new value']

answer_list = ['old value', ]
foo(answer_list)
print(answer_list)
# output: ['old value', 'new value']

# if bar refers to an immutable object
def foo(bar):
    bar = 'new value'
    print(bar)
    # output: 'new value'

answer_list = 'old value'
foo(answer_list)
print(answer_list)
# output: 'old value'

# if bar refers to a mutable object and re-assign it in foo
def foo(bar):
    bar = ['new value', ]
    print(bar)
    # output: ['new value', ]

answer_list = ['old value', ]
foo(answer_list)
print(answer_list)
# output: ['old value', ]

ref:
http://stackoverflow.com/questions/986006/how-do-i-pass-a-variable-by-reference
https://jeffknupp.com/blog/2012/11/13/is-python-callbyvalue-or-callbyreference-neither/
https://docs.python.org/3/faq/programming.html#how-do-i-write-a-function-with-output-parameters-call-by-reference

scope

只有 module、class、function 才有建立新的 scope,if、while、for 並不會建立新的 scope。

some_list = [
    {
        'id': 1,
        'is_change': False,
    },
    {
        'id': 2,
        'is_change': False,
    },
    {
        'id': 3,
        'is_change': False,
    },
    {
        'id': 4,
        'is_change': False,
    },
]

for item in some_list:
    item['is_change'] = True

some_list 的每個 item 都會被更新,因為是 mutable objects 的行為是 call-by-reference。所以你不需要這樣:

new_list = []
for item in some_list:
    item['is_change'] = True
    new_list.append(item)