[Python] Internationalization (i18n) of Python Application by GNU gettext Tools


GNU gettext tools are great for creating i18n (web) applications. In this post, assume that PO and MO files are ready and we will use the PO and MO files to let Python applications speak local languages. If you do not know what PO or MO files are, or you do not know how to create them by gettext tools, please read my previous post [1] first. If you want to know how to render HTML by Jinja2 template engine with the translated texts in PO and MO files in your Python web application, please refer to [9].

PO and MO files

I will show how to use MO and PO files by example. In the example, we support two locale, zh_TW (Traditional Chinese) and vi_VN (Vietnamese). The zh_TW PO file are located at locale/zh_TW/LC_MESSAGES/messages.po and vi_VN PO file are located at locale/vi_VN/LC_MESSAGES/messages.po.

zh_TW PO file locale/zh_TW/LC_MESSAGES/messages.po:

messages.po | repository | view raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Chinese translations for PACKAGE package.
# Copyright (C) 2013 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Automatically generated, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-06-04 10:20+0800\n"
"PO-Revision-Date: 2013-03-10 05:19+0800\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

msgid "Home"
msgstr "首頁"

msgid "Canon"
msgstr "經典"

msgid "About"
msgstr "關於"

msgid "Setting"
msgstr "設定"

msgid "Translation"
msgstr "翻譯"

vi_VN PO file locale/vi_VN/LC_MESSAGES/messages.po:

messages.po | repository | view raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Vietnamese translations for PACKAGE package.
# Copyright (C) 2013 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Automatically generated, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-06-06 23:05+0800\n"
"PO-Revision-Date: 2013-06-06 22:50+0800\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: vi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"

msgid "Home"
msgstr "Trang chính"

msgid "Canon"
msgstr "Kinh điển"

msgid "About"
msgstr "Giới thiệu"

msgid "Setting"
msgstr "Thiết lập"

msgid "Translation"
msgstr "Dịch"

Generate corresponding MO files by msgfmt. Put MO files together with PO files. So we have two PO files and two MO files:

locale/zh_TW/LC_MESSAGES/messages.po
locale/zh_TW/LC_MESSAGES/messages.mo
locale/vi_VN/LC_MESSAGES/messages.po
locale/vi_VN/LC_MESSAGES/messages.mo

Source Code

Now we can let Python application speak local language. The application-independent code is put in the following module:

i18n.py | repository | view raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env python
# -*- coding:utf-8 -*-

"""
References:
http://docs.python.org/2/library/gettext.html
http://jinja.pocoo.org/docs/extensions/
http://webpy.org/cookbook/i18n_support_in_template_file
http://webpy.org/cookbook/runtime-language-switch
https://code.google.com/p/webapp-improved/source/browse/webapp2_extras/i18n.py
http://docs.python.org/2/library/threading.html
"""

import os
import gettext
import threading

localedir = os.path.join(os.path.dirname(__file__), 'locale')
domain = 'messages'
threadLocalData = threading.local()
threadLocalData.locale = 'en_US'

# find out all supported locales in locale directory
locales = []
for dirpath, dirnames, filenames in os.walk(localedir):
  for dirname in dirnames:
    locales.append(dirname)
  break

AllTranslations = {}
for locale in locales:
  AllTranslations[locale] = gettext.translation(domain, localedir, [locale])

def gettext(message):
  return AllTranslations[ threadLocalData.locale ].gettext(message)

def ugettext(message):
  return AllTranslations[ threadLocalData.locale ].ugettext(message)

def ngettext(singular, plural, n):
  return AllTranslations[ threadLocalData.locale ].ngettext(singular, plural, n)

def ungettext(singular, plural, n):
  return AllTranslations[ threadLocalData.locale ].ungettext(singular, plural, n)

def setLocale(locale):
  if locale in locales:
    threadLocalData.locale = locale


if __name__ == '__main__':
  # for test purpose
  for dirpath, dirnames, filenames in os.walk(localedir):
    for dirname in dirnames:
      print(dirname)
    break

Use above module to get translated texts:

demo.py | repository | view raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import i18n


if __name__ == '__main__':
  i18n.setLocale("zh_TW")
  print(i18n.gettext("Home"))
  print(i18n.gettext("Canon"))
  print(i18n.gettext("About"))
  print(i18n.gettext("Setting"))
  print(i18n.gettext("Translation"))
  i18n.setLocale("vi_VN")
  print(i18n.gettext("Home"))
  print(i18n.gettext("Canon"))
  print(i18n.gettext("About"))
  print(i18n.gettext("Setting"))
  print(i18n.gettext("Translation"))

Note

The domain in this case is messages. If you name your PO and MO file as hello, the domain becomes hello.

locale/zh_TW/LC_MESSAGES/hello.po
locale/zh_TW/LC_MESSAGES/hello.mo
locale/vi_VN/LC_MESSAGES/hello.po
locale/vi_VN/LC_MESSAGES/hello.mo