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

    Часть 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.
    #
    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 ""\n\n')
    
    # теперь если у нас стоит аргумент '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

    Продолжение

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 8

      0
      Почему-то думал что этого достаточно, без велосипедов…
        –2
        Извиняюсь, не понял изначально про, что статья…
        +1
        Поскольку у вас уже есть Jinja2 в зависимостях, то можно воспользоваться его расширением gettext.
        Тут можно посмотреть на него.
        Я его и с Django пользую так:
        ... JINJA_CONFIG = { 'extensions': [ 'jinja2.ext.i18n', .... ], } ...
          0
          jinja2 все равно не может знать о местонахождении locale, поэтому ей нужно указывать откуда брать переводы.
          А строчки:
          jinja = Environment( extensions=['jinja2.ext.i18n']) jinja.install_gettext_translations(translations)
          и
          kwarg['_'] = get_trans(module)
          Выполняют примерно одинаковую задачу. Но с небольшой разницей, get_trans() «знает» что такое модули, и каждому модулю предоставляет именно его файл переводов, исключая конфликты и пр.
          +3
          Инициализация ненужных переменных, названия переменных в стиле n, r, l, s, вложенные на 5 уровней циклы. Зачем вы делаете это с питоном?
            –1
            Я человек от питона пока что достаточно далёкий. Скажите, в чём зло пяти уровней?
            Что вместо вложенности циклов принято в питоне? Как можно по другому написать?
            Ну и локальные переменные, используемые один раз в соседней строке — в чём зло кратких названий? Как принято в питоне?
            Заранее спасибо.
            0
            почему не babel?
              0
              Если честно то я мало иследовал плюсы babel относительно gettext, при том что gettext более распространен. Насколько я понимаю фрагмент:
              import gettext; gettext.translation(domain, path, ['ru'], codeset='UTF-8')
              Для babel примерный аналог:
              from babel.support import Translations Translations.load(dirname=resource_filename(__name__, 'locale'), locales=['ru'], domain='messages')
              Разница не очень большая.

            Only users with full accounts can post comments. Log in, please.