gettext: рецепт жаркое из антилопы в Javascript


    При разработке CMF я столкнулся с необходимостью грамотно реализовать i18n (мультиязычность), и стал рассматривать различные варианты…

    Сначала, исходя из прошлого опыта, я хотел сделать «языковые константы» и мытарствах с хаками для числительных. Но потом к счастью остановил свой выбор на GNU gettext, на мощном и популярном (в Unix-среде) инструменте. Вскоре я понял что нет смысла излишне нагружать сервер переводами фраз, которые не индексируются поисковиками, и что в ряде случаев лучше переводить на клиенте. Однако, необходима была унифицированная система, позволяющая делать переводы в едином формате.
    Прогуглив gettext javascript, я увидел несколько реализаций.

    Первой попалась — code.google.com/p/gettext-js
    Плюс в том что она не требует дополнительной переконвертации исходного po-файла, минус — нету ngettext.

    Потом я нашел plugins.jquery.com/project/gettext
    Её я и решил использовать. Однако, для работы плагина требуется подготовка специального JSON-файла из MO-файла.

    Для конвертации приводится функция на Python:
    import simplejson as enc
    import gettext
    def gettext_json(domain, path, lang = [], indent = False):
        try:
            tr = gettext.translation(domain, path, lang)
            # for unknown reasons, instead of having plural entries like 
            # key: [sg, pl1...]
            # tr._catalog has (key, n): pln, 
            keys = tr._catalog.keys()
            keys.sort()
            ret = {}
            for k in keys:
                v = tr._catalog[k]
                if type(k) is tuple:
                    if k[0] not in ret:
                        ret[k[0]] = []
                    ret[k[0]].append(v)
                else:
                    ret[k] = v
            return enc.dumps(ret, ensure_ascii = False, indent = indent)
        except IOError:
            return None
    


    Пришлось потратить… дцать минут на гуглеж и изучение доки, чтоб поправить код и заставить работать. В результате родил нормальную Unix-программу.

    gettext2json
    #!/usr/bin/python
    
    import sys
    import simplejson as enc
    import gettext
        
    def gettext_json(domain, path, lang = [], indent = False):
        try:
            tr = gettext.translation(domain, path, lang)
            # for unknown reasons, instead of having plural entries like 
            # key: [sg, pl1...]
            # tr._catalog has (key, n): pln, 
            keys = tr._catalog.keys()
            keys.sort()
            ret = {}
            for k in keys:
                v = tr._catalog[k]
                if type(k) is tuple:
                    if k[0] not in ret:
                        ret[k[0]] = []
                    ret[k[0]].append(v)
                else:
                    ret[k] = v
            return enc.dumps(ret, ensure_ascii = True, indent = indent)
        except IOError as (errno, strerror):
    			print "I/O error({0}): {1}".format(errno, strerror)	
            
    print gettext_json(sys.argv[1],sys.argv[2],[sys.argv[3]], True)
    


    Также, решил автоматизировать процесс создания бинарных MO-файлов из текстовых PO-файлов:

    BuildLocales:
    #!/usr/bin/php -q
    <?php
    chdir(__DIR__);
    $lcPath = './locale';
    $jsPath = './static/locale';
    
    foreach (glob($lcPath.'/*/LC_MESSAGES/*.po') as $poFile) {
    	
    	$locale = pathinfo(dirname(dirname($poFile)), PATHINFO_FILENAME);
    	$domain = pathinfo($poFile, PATHINFO_FILENAME);
    	
    	$moFile = dirname($poFile).'/'.$domain.'.mo';
    	$jsFile = $jsPath.'/'.$locale.'/'.$domain.'.json';
    	shell_exec('mkdir -p '.escapeshellarg($jsPath.'/'.$locale));
    	shell_exec('msgfmt -o '.escapeshellarg($moFile).' '.escapeshellarg($poFile));
    	$cmd = 'gettext2json '.escapeshellarg($domain).' '.escapeshellarg($lcPath ).' '.escapeshellarg($locale).' > '.escapeshellarg($jsFile);
    	shell_exec($cmd);
    }
    

    Таким образом, для подготовки всех файлов достаточно лишь создать/изменить текстовый .po файлы в папке locale и запустить скрипт BuildLocales.

    Для подключения gettext к Javascript необходимо указать атрибут lang у тега html, добавить в head элемент link с путем до json-файла и подгрузить jquery.gettext.js.

    Начало HTML-кода страницы будет выглядеть примерно так:

    <!DOCTYPE html> 
    <html lang="ru"> 
    ...
    <link href="/locale/ru/mydomain.json" lang="ru" rel="gettext"/>
    <script type="text/javascript" src="/js/jquery.gettext.js" />
    ...
    


    Затем можно вызывать функцию _(«Hello world!») и наслаждаться. Упс! Не работает!

    Придется кое-что поправить в jquery.gettext.js, накладываем patch:

    63,66c63,70
    <                                     try {
    <                                       var messages = eval('(' + data + ')');
    <                                     } catch(e) {
    <                                       return;
    ---
    >                                     if (typeof(data) == 'object') {
    >                                       var messages = data;
    >                                     } else {
    >                                       try {
    >                                               var messages = eval('(' + data + ')');
    >                                       } catch(e) {
    >                                               return;
    >                                       }
    
    
    

    Те кому лень накладывать патч берут jquery.gettext.js.

    Надеюсь жаркое вам понравилось, спасибо за внимание.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 43

    • UFO just landed and posted this here
        +1
        так если они на одном сайте у них и должен быть один ПР. Разве нет?
        • UFO just landed and posted this here
            +1
            Это только про Pr речь идет, тиц везде постоянен.
            • UFO just landed and posted this here
          +1
          На одном урле в каждом поисковике будет одна копия страницы. Наверное можно ее сделать двуязычной, но вес в каждом языке от этого уменьшиться.
          –1
          Пользовался gettext при разработке проекта на php. Старые добрые массивы оказались гораздо удобнее в плане хранения переводов. После каждого изменения .po файла генерирование .mo файла сродни компиляции — на одну операцию больше. Плюс к этому gettext кэширует перевод после первого обращения, и нету адекватного способа почистить кэш (кроме как назначить другое имя для .mo файла). Возможно это недостаток только php библиотеки gettext.
            0
            1. Напишите скрипт в несколько строк который будет проверять дату модификации файлов каждую секунду и перекомпилировать .po-файл. Если руки прямые, то inotify.

            2. Куда кеширует? ;-)
              0
              1. зачем?
              2. в память, вероятно :)
              +1
              редакторы сами перегенерируют .mo файлы. в чем проблема? а про кеш вообще не понятно
                –1
                Про кэш. Редакторы может и генерят, только не вижу смысла (и удобства как такового) использовать для редактирования перевода специальный редактор.
                  0
                  Специальный редактор, вообще-то для специалистов по переводам, которым не в радость копаться в ваших исходниках.
                    0
                    и к тому же можно красиво поднастроить этот самый редактор и выглебать переводимые строки из кода автоматом и соответственно зачищать ненужные. в ручную это наверное аццкий труд поддерживать все в нормальном состоянии
                • UFO just landed and posted this here
                    0
                    не гоните — это только проблема Windows, в *NIX .mo файл подхватывается на лету
                        0
                        И? я вам говорю, что нормальный сервер под линукс не требует рестарта при замене .mo файла. Наймите нормального админа. Под виндой да, нет толкового решения. А переименовывать файлы, это бред, нафига забивать память кэшем строк гет текста? К тому же в нормальном проекте строки в .po меняются редко — чего вы там постоянно меняете?
                          +1
                          Сервер не имеет к этому никакого отношения. gettext.so загружается в память один раз. После подгрузки *.mo файла в кэш, он не проверяет, изменяется ли этот файл на диске или нет и все запросы удовлетворяет из кэша. Кэш будет обнулен только при перезапуске процесса, подгрузившего gettext.so. Если PHP работает в любом из cgi режимов — проблем гораздо меньше. Но если он работает, например, как модуль Апача, а сам апач — в воркер режиме, будут проблемы. Потому что кэш никак не обнулить без перезапуска процесса воркера апача.
                        0
                        проблема есть, решается переименовыванием .mo файла или рестартом апача.

                        ИМХО лучший способ i18n на PHP — комбинированный. На тестовом сервере работаем с массивами, при выгрузке на продакшен кладем все в .po и .mo.
                      –2
                      Вскоре я понял что нет смысла излишне нагружать сервер переводами фраз, которые не индексируются поисковиками, и что в ряде случаев лучше переводить на клиенте.


                      Это такой хитрый способ убрать слова-паразиты?
                        0
                        тоже занимаюсь велосипедом
                        и вот сейчас задумываюсь, как бы так реализовать тоже самое, но на ХСЛТ
                          +1
                          ;-) А я написал jquery.i18n. Скоро напишу статью.
                          +1
                          Вскоре я понял что нет смысла излишне нагружать сервер переводами фраз, которые не индексируются поисковиками

                          Такое поведение обусловлено спецификой проекта? Просто интересно по какой причине вы отказались от серверного перевода в пользу JS.
                            0
                            За автора не отвечу. А мы пользуемся gettext для javascript в приложении, у которого интерфейс построен на ExtJS. Он один раз загружается вместе со всеми переводами, и дальше все надписи на кнопках и контролах переводятся в браузере. От серверного перевода при этом, конечно, не отказываемся — сообщения бизнес-логики приезжают в браузер уже переведёнными.
                              0
                              Да, в вашем случае это оправдано. Если переводы используются активно, то всё время перегружать ExtJS накладно. Но везде требования к сервисам разные. Например, не представляю как можно заставить поисковик нормально проиндексировать многоязычный сайт без уникальных урлов и приходящих с сервера переводов без Ajax. А в 90% случаев требования к проектам именно такие.
                            0
                            Не очень хороший подход… а если у вас json-массив (gettext) будет весить больше 1мб? Браузер то закеширует у клиента его, но первоначально будет загружать полностью… Разбивать на страницы каждый json файл? зачем новый велосипед?
                              0
                              Предположим что суммарный объем языкового файла равен 1 мегабайту, значит размер JS-скрипта, который использует этот языковой файл будет заведомо намного больше 1 мегабайта, т.к. вызовы аля _(«text») составляют незначительную часть всего кода.
                              Пойдем в рассуждениям дальше. Если сравнивать эту ситуацию с той при которой для каждого языка генерируется отдельный JS-код, объем докачки при смене языка сильно возрастёт.

                              > Разбивать на страницы каждый json файл?
                              JSON файл не надо разбивать. Надо для каждой логически обособленной сущности (компонента) делать свой файл.
                              0
                              КО, где же ты?
                              Объясни, пожалуйста, смысл таких извращений!
                                –1
                                Жди картинки троллейбуса из хлеба. Ваш К.О.
                                0
                                В django все уже изпадкаробки.
                                  0
                                  крутую вы «нормальную unix программу» родили
                                    0
                                    gettext — жуткий англоцентрический формат. Там же английский вариант является ключём в итоге куча проблем:
                                    1. Некоторые разработчики сначала делают программу на своём языке, а потом уже переводят.
                                    2. Одна строка на английском не всегда соответствует одной строке на других языках (например, глагол и существительное в англ. — одно и то же слово, а русском — два).
                                    3. Если меняется английский текст, то надо менять его во всех переводах.

                                    Да и что с плюрализацией (1 робот, 2 робота, 5 роботов)? В gettext её синтаксис и так ужасен, а в исходнике я вообще не нашёл, что можно менять плюрализацию для разных языков (а англ. плюрализация сильно отличается от русской).
                                      +2
                                      LOL. Всегда поражала воинствующая невежественность.

                                      1. Ключем может являться любой вариант, просто принято написать программы на английском, но это не требование.

                                      2. Ну и сделайте два варианта, с комментами о контексте.

                                      3. А вы хотели чтоб изменения в переводы вносила фея?

                                      4. С плюрализацией всё замечательно. Ну, то что вы не нашли, говорит только о том что плохо искали.
                                        0
                                        Так может для перевод лучше использовать иерархический формат типа JSON или YAML с кодом в ключе?
                                          0
                                          Да, не спорю, я мог пропустить код плюрализции. Но меня больше всего смущает формат плюрализации в gettext. В YAML интереснее:
                                          robots: !!pl
                                            0: нет роботов
                                            1: %1 робот
                                            2: %1 робота
                                            n: %1 роботов
                                          
                                          0
                                          Но всё же, может укажите мне, где в этом проекте задаются правила плюрализации для каждого языка?
                                          0
                                          Что посоветуете в качестве альтернативы? Mozilla например все устраивает :)
                                            0
                                            Мне нравится YAML формат, как, например, в Rails. Главное, чтобы ключем был какой-то код.
                                              0
                                              YAML все равно надо в PHP компилировать ведь. Кроме того, YAML формат — это не средство локализации. Как у него с плюрализацией, падежными формами?
                                                +1
                                                Ну я говорю о формате хранения, а не библиотеке. Собственно, в Ruby on Rails его не надо переконвертировать (компилировать), так как перевод загружается только один раз.
                                                Плюрализация для всех языков есть (сразу из коробки). Формат:
                                                robots: !!pl
                                                  0: нет роботов
                                                  1: %1 робот
                                                  2: %1 робота
                                                  n: %1 роботов
                                                

                                                Падежные формы — это обычно отдельная библиотека (в основном используется сервис от Яндекс), но я не встречал необходимости в нём (максимум, имена менять).
                                                  0
                                                  Ruby становится стандартом :) Разлюбить PHP, что ли? :))
                                                    0
                                                    Поиграться всегда можно :).

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