Введение.
Изначально хотелось написать побольше упомянув в статье роуты и борьбу с ошибками, но тема интернационализации довольно обширна оказалась поэтому было решено посвятить пост исключительно ей. Мы будем пользоваться распространенным инструментом gettext в рамках фреймворка.
Все переводы можно разделить на две части:
a) Те что непосредственно в файлах с расширением .py — с ними все просто.
b) И те что в шаблонах — к ним придется написать небольшой велосипед :).
Ну и далее нужно автоматизировать процесс управления переводами.
Итак добро пожаловать под кат.
Перевод фраз из питоновских файлов
Для того чтоб функионировала система перевода мы импортируем функцию которая занимается переводом, в качестве аргумента передаем в нее название модуля в котом осуществляем перевод и все. Далее просто вызываем эту функцию как _() и передаем как аргумент переводимую фразу.
import core.union; _ = core.union.get_trans('module')
_('text')
Перевод фраз из шаблонов
Вставляем в функцию render_templ, которая занимается рендерингом шаблонов, следующий фрагмент для передачи переводящей функции в шаблоны:
p['gettext'] = get_trans(module)
p['_'] = get_trans(module)
В итоге получаем:
def render_templ(template, **kwarg):
template = jinja.get_template(template)
module = split_templ_name( template)[0]
kwarg['gettext'] = get_trans(module)
kwarg['_'] = get_trans(module)
kwarg['context'] = context()
return template.render(**kwarg)
Также в шаблоны можно передать контекст окружения, чтоб не передавать его в каждой функции.
Дальше идут функции, которые занимаются переводами.
langs = {}
def load_lng(path, module_name, lang):
""" Подгружает модули с языками """
if not module_name in langs[lang]: langs[lang][module_name] = []
path = os.path.join( path, module_name, 'locale') if module_name else os.path.join (path, 'locale')
if os.path.isdir(path):
t = gettext.translation('_', path, [cur_lang()], codeset='UTF-8')
langs[lang][module_name].append(t)
def get_lng(module):
""" Возвращает объекты перевода для компонента. """
lang = cur_lang()
if not lang in langs: langs[lang] = {}
if not module in langs[lang]:
langs[lang][module] = []
load_lng(os.path.join (settings.lib_path,'app'), module, lang)
load_lng(os.path.join (os.getcwd(),'app'), module, lang)
if not module: load_lng(os.path.join (os.getcwd()), '', lang)
return langs[lang][module]
def trans(module, s):
""" принимает имя компонента и строчку, которую надо перевести
и непосредственно переводит"""
if type(s) == str: s = s.decode('UTF-8')
translated = s
lng = get_lng(module)
if lng:
for i in reversed(lng):
translated = i.gettext(s).decode('UTF-8')
# если удалось перевести то translated отличается от оригинала и дальше не надо искать.
if s != translated: break
return translated
def get_trans(module): # возвращает функцию которая переводит саму фразу
return lambda s: trans(module, s)
Работа с gettext
Теперь нам предстоит автоматизировать перевод после того как мы написали в шаблоне {{_('text')}}. Мы должны получить в папках /app/module/ru/LS_MESSAGES файлики _.mo, _.po.
_.mo — скомпилированный файлик откуда gettext потом читает переводы.
_.po — файл с исходниками для переводчиков по такому образцу:
#: /path/modul/templ/base.tpl:25
msgid "text"
msgstr "" # сюда текст перевода вписывают уже переводчики.
Работа с этими файлами ведется стандартными командами из консоли:
xgettext – собирает по файлам строчки для перевода.
msginit – создает файл перевода для конкретного языка _.po.
msgfmt – компилирует в бинарный файл _.mo.
msgmerge – обновление файлов переводов.
Но во первых хотелось бы все автоматизировать и не писать каждый раз несколько команд, а во вторых xgettext не умеет работать с шаблонами а только с файлами *.py по крайней мере такая возможность у него найдена не была.
Поэтому мы напишем небольшую утилиту которая одной командой выполняла бы за нас все эти действия.
# Список языков которые собираемся подерживать.
list_lang = ['ru_RU', 'en_US']
Получение аргументов из командной строки.
s = str(sys.argv)
s = s[1:-1]; app = []
for word in s.split(", "):
app.append(word)
# путь к папке с фреймворком
lib_path = '/path'
# путь к каталогу с сайтом
app_path = app[1][1:-1]
def iter_trans(dir, is_app=True):
""" Идем по компонентам ищем там локаль и превращаем файлик с переводами в бинарник."""
if is_app: #тут бежим по компонентам
for res in os.listdir(dir):
path_ = os.path.join(dir, res, 'locale')
if os.path.isdir(path_):
for res in os.listdir(path_):
path = os.path.join(path_, res, 'LC_MESSAGES')
if os.path.isdir(path):
po_f = os.path.join(path, '_.po')
mo_f = os.path.join(path, '_.mo')
os.popen("msgfmt -o %s %s" % (mo_f, po_f )).read()
else: # тут идем по переводам из проекта.
path_ = os.path.join(dir, 'locale')
if os.path.isdir(path_):
for res in os.listdir(path_):
path = os.path.join(path_, res, 'LC_MESSAGES')
if os.path.isdir(path):
po_f = os.path.join(path, '_.po')
mo_f = os.path.join(path, '_.mo')
os.popen("msgfmt -o %s %s" % (mo_f, po_f )).read()
def iter_mo(dir, is_app=True):
""" Идем по шаблонам """
if is_app:
for res in os.listdir(dir):
path = os.path.join(dir, res, 'templ')
if os.path.isdir(path): iter_templ(path)
else:
path = os.path.join(dir, 'templ')
if os.path.isdir(path): iter_templ(path)
pot_header = """# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: %s\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
""" % (time.strftime("%Y-%m-%d %H:%M%z"))
def iter_templ(dir):
""" Из шаблонов извлекаем строчки для переводов и закидуем их в pot файлы.
А затем создаем по списку языков необходимые _.po файлы или сливаем с уже существующими.
"""
out_f = os.path.join(dir, '..', 'locale', '_.pot')
file_o = open(out_f, 'w')
# Записываем в файл заголовок.
file_o.write(pot_header)
for name in os.listdir(dir):
if name.endswith('.tpl'):
load_translation(os.path.join(dir, name), file_o)
file_o.close()
for res in list_lang:
lang = res[:2]
po_path = os.path.join(dir, '..', 'locale', lang)
if not os.path.isdir(po_path): os.makedirs(po_path, 0755)
po_path = os.path.join(po_path, 'LC_MESSAGES')
if not os.path.isdir(po_path): os.makedirs(po_path, 0755)
po_f = os.path.join(po_path, '_.po')
if not os.path.isfile(po_f): os.popen("msginit --no-translator -i %s -o %s -l %s" % ( out_f, po_f, res+'.UTF-8')).read()
else: os.popen("msgmerge %s %s -o %s" % (po_f, out_f, po_f)).read()
def load_translation(in_f, file):
""" Извлекаем строки из шаблона и записуем их в файл. """
with open(in_f, 'r') as f: l = f.read().split('n')
n = 0; r = {}
for rs in l:
n += 1
# находим подчеркивание со скобочками
aa = re.findall(r'_([^)]+)', rs)
for res in aa:
# вырезаем саму строчку без подчеркивания скобок и кавычек
res = res[3:-2]
# смотрим нет ли еще такой строчки
if not res in r: r[res] = []
# Добавляем номер строки
r[res].append(n)
for res, nums in r.iteritems():
file.write('#: '+in_f+':'+','.join([str(x) for x in nums])+'n')
file.write('msgid "'+res+'"n')
file.write('msgstr ""nn')
# теперь если у нас стоит аргумент 'cpl', то компилируем, если нет, то просто собираем строчки.
itr = iter_trans if len(app) > 2 and app[2][1:-1] == 'cpl' else iter_mo
for res in [lib_path + '/', False, lib_path +'/app/', app_path +'/app/', app_path + '/', False]: itr(res)
Резюме
Теперь в фреймворке есть модульная структура, шаблонизация, и интернализация. Для создания работоспособного каркаса нам осталось добавить отладку, работу с роутами и статикой. После этого можно будет приступать к созданию основных компонентов: админка, представление данных и тд.
Пока все.
Используемые материалы
О природе gettext
Введение в gettext в python
gettext и jinja2
Документация по python и gettext
Автор: Alex10