Django своими руками часть 2: Интернационализация

в 18:07, , рубрики: framework, jinja2, python, интернационализация, метки: , , ,

Часть 1.

Введение.

Изначально хотелось написать побольше упомянув в статье роуты и борьбу с ошибками, но тема интернационализации довольно обширна оказалась поэтому было решено посвятить пост исключительно ей. Мы будем пользоваться распространенным инструментом 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js