11 июля 2014 в 12:04

po.js — супер простая утилита для i18n

Когда я разрабатываю системы на Zend Framework, то всегда использую gettext и Zend_Translate. Всё лаконично просто и обычно не возникает никаких проблем с переводом даже больших проектов. Для каждого языка генерируются свои файлы .po и .mo, переводы пляшут от дефолтного языка, ключи тоже на этом же языке. Переводчикам удобно передать эти файлы, которые они могут открыть в POEdit и удобно всё перевести. Так вот, на стороне сервера всё очень просто, но часто нужно переводить какие-то сообщения «на лету» в JavaScript, а он не понимает ваши .mo файлы. Но хотелось бы пользоваться именно ими, чтобы не разделять перевод одного проекта на 2 части (backend, frontend). И я начал искать. В Интернете существует достаточно большое количество таких решений, но все они почему-то обрастают зависимостями:

code.google.com/p/gettext-js (Prototype)
angular-gettext.rocketeer.be (Angular)
github.com/jakob-stoeck/jquery-gettext (jQuery)

А хотелось иметь именно «pure-js» решение. Ок, напишем своё.

Первым делом я искал, как же в JS прочитать PO-файлы. Можно парсить, но это лишняя нагрузка, поэтому я решил не насиловать JavaScript и отдавать ему уже готовый JSON. Поэтому первое, что нам предстоит сделать, -это сконвертировать PO в JSON. Советую воспользоваться этим конвертером.

Далее алгоритм простой, сохраняем себе на сервер JSON-файл, а передаем ссылку на него в pojs. Конечно, подключив перед этим po.min.js на страницу.

<script src="po.min.js"></script>
<script>
    pojs.init('/ru.json');
</script>


Если текущий язык дефолтный, то не нужно передавать ссылку на JSON.

Все переводы возвращаются после вызова функции с передачей в нее ключа. Если ключ не найден, то будет возвращен сам ключ.

pojs._('Hello world');


Также в po.js присутствует еще одна супер-мини фича, немного похожая на sprintf.

pojs._('My name is %s, and I am %s years old', ['Sasha', 24]);


Если JSON не закэширован, то он будет получен асинхронно, а это значит, что мы не сможем использовать pojs._() сразу же после инициализации. Оберните код, где используются переводы:

pojs.ready(function() {
    pojs._('Hello world');
});


Стоит отметить какие-то плюсы po.js, иначе не было бы смысла всё это делать:

1. Нано-размер: ~0.7KB
2. Не нуждается в сторонних зависимостях, таких как jQuery, Prototype, Angular …
3. JSON кэшируется в localStorage. Поэтому будьте осторожны, если у вас очень большие файлы переводов. Сбросить кэш можно просто добавив "?1" к ссылке на JSON-файл (да, вот такой old school)

po.js на GitHub

p.s.
Писал чисто под свои нужды, возможно, вам чего-то не хватает или что-то работает не так. Готов править, улучшать!
Плютов Александр @plutov
карма
7,0
рейтинг 0,0
Самое читаемое Разработка

Комментарии (18)

  • +3
    Немного риторики — как дела с доменами, контекстами, множественными числами? (Хотя из кода вижу, что никак) Ну и конвертить PO онлайн — должно быть очень удобно для билда проектов.
    • 0
      Я же и прокомментировал в конце статьи, что жду от вас всех пожеланий и нужд. По поводу конвертации, я всё равно перед тем как залить на сервер запускаю POEdit, и он создает .mo файл. Но видел решения автоматической генерации перевода на сервере. Думаю, туда несложно добавить автоматическое создание JSON-файла. Просто относится ли это к этой библиотеке или нет? Если вы считаете, что да, то как вы видите это процесс? Спасибо.
      • 0
        Вы можете просто сделать эту фичу отдельный модулем. Кому будет нужно — тот подключит.
        • 0
          Всё верно, я так уже и планирую. Просто в каком виде? Это будет bash скрипт или php скрипт (второе больше по душе).
          • –3
            PHP, наверное, будет лучше тем, что более кроссплатформенно получится, чем баш.
            • –3
              т.е. господа минусующие хотят сказать, что баш скрипт проще будет запустить под Windows, чем PHP?
              • 0
                Скорее всего да. Bash поставить на Windows — раз плюнуть (и если вы более-менее серьезный разработчик, то он у вас уже есть), а разбираться с подключением PHP — это отдельный гемор если у вас серверная часть на другом языке.
      • 0
        Joshua I. Miller в свое время написал отличнейший порт GNU gettext-a, тут есть его копия — phpxref.pagelines.com/nav.html?dms/editor/js/Gettext.js.source.html. Там можете найти много интересного. У себя на проекте используем именно этот модуль.

        С файлами локализации у нас работают документаторы, через POEdit. На выходе получаем .po файлы, которые вышеупомянутой библиотекой конвертируем в .json, во время билда. В клиент подтягиваем уже скомпиленый .json, который внедряется в код урезанной версией Gettext-а.

        При этом сохранены все доступные методы GNU.
  • 0
    >Первым делом я искал, как же в JS прочитать PO-файлы. Можно парсить, но это лишняя нагрузка, поэтому я решил не насиловать JavaScript и отдавать ему уже готовый JSON.
    Я бы всё-таки подумал о том, чтобы добавить парсер po->json в библиотеку. Во многих случаях вполне допустимо потратить сотню миллисекунд на парсинг po-файла, особенно если он происходит уже после DOM Ready.
    Имхо, это тот случай, когда можно разгрузить сервер и отдать работу клиенту.
    Обычно po-файлы меняются не так часто, но не у всех настроены всякие билд-скрипты, которые автоматически смогут перевести po во json при изменении перевода, и возможность прямой загрузки po может помочь некоторым будущим пользователям вашей библиотеки.
    • 0
      Для более менее серьезных проектов файл .po может быть немаленьким. И конвертация на клиенте может занять неприятное время. Но я согласен с вами, парсинг нужно добавить, но он должен происходить или во время билда, или на сервере при загрузке страницы. Дело в том, что хотелось бы какое-то универсальное решение. Я могу написать парсер на PHP, но что, если проект на Python или Ruby?
      • 0
        поэтому я и говорю — парсер на js на клиенте. Укажите в документации, что гораздо лучше иметь заранее созданный json на сервере, но дайте и возможность автоматического подключения po-файла. Не все из тех, что хочет переводы, имеют инфраструктуру на сервере, и им придётся вручную запускать создание json-а.
        Но смотрите сами. Вы просили улучшения, я считаю, что количество пользователей эта функция увеличит. Но решать вам. Если вы считаете архитектурно неправильным парсить на клиенте, то так и быть. Вон, твиттер пытался сделать шаблонизацию на клиенте, а потом отказался и стал с сервера выдавать готовый html.
      • 0
        Небольшая подсказка — phantomjs. Его можно запустить из любой среды, скормить ему парсер на JS, и источник .po, на выходе получить .json. Весь код запуска сведется к одной строке.
      • 0
        Без поддержки числительных и контекста это несерьезно, в любом сколько-либо крупном проекте это требуется. Ну и интерфейс не-gettext совместимый уже сейчас.
        Солидарен с комментатором выше по поводу кода: зачем дикой частоты интервал, там где всего один раз должен сработать коллбек после загрузки файла? Да и односимвольные переменные (как и underscore-префикс для всего подряд) не добавляют читаемости, оставьте эту работу минимизатору.

        (Промахнулся веткой комментариев, хотел на первом уровне написать)
  • 0
    Гдето с месяц назад тоже искал (js based) i18n тулзу для своего проекта. В общем в финальном раунде сравнения победил i18next. Среди участников также были

  • +1
    Отличный метод…

        ready: function(cb) {
            var _t = this, i = setInterval(function() {
                if (_t._r) {
                    cb();
                    clearInterval(i);
                }
            }, 10);
        },
    


    Да еще и несоблюдение JS стандартов.
    • +1
      Я понимаю, что вы гуру и вам достаточно написать саркастическое словосочетание. Но знаете что, отвергая, предлагай. Не все мы пишем идеальный код. На вашем месте я бы лучше поделился опытом и объяснил, что в этой функции не так.
      • +2
        Писал с iPad, ночью, по-этому так «сухо». Сейчас попробую все разжевать.

        Во-первых, не i18n (internationalization), а всего лишь i10n (localization). Не путайте.

        Во-вторых, на GitHub у вас написанно:
        Super-simple gettext translation in pure JS
        и, хочу я сказать, что Gettext тут не при чем.

        В-третьих, у инструментов, перечисленных вверху поста, нет зависимостей. Библиотека/фреймворк, под которую написан плагин/дополнение не может быть зависимостью, а только ноборот.

        Код вашей библиотеки с моими комментариями
        // Неправильное определение переменной: нет ключевого
        // слова "var" - несоблюдение строгих стандартов JS.
        pojs = {
            // Непонятное имя свойства
            _l: null,
            // Непонятное имя свойства
            _r: false,
            // Непонятное имя свойства
            _p: {},
            // Непонятное имя параметра
            init: function(l) {
                this._l = l;
                this._load();
            },
            _: function(key, args) {
                // Избыточное выражение.
                // Проще:
                //  this._p[key] || key
                //
                // Неправильное форматирование кода. Не только здесь, повсеместно.
                // Рекомендую к прочтению JS Style Guide: https://github.com/BR0kEN-/javascript
                //
                // В данном случае лучше так:
                //  var t = this._p[key] || key,
                //      a;
                var t = this._p.hasOwnProperty(key) ? this._p[key] : key, a;
                // Неправильная проверка. "args" может быть строкой и удовлетворять условие.
                // Пример:
                //  var args = 'string';
                //
                //  if (args) {
                //    for (var arg in args) {
                //      // args[arg] = s
                //      // args[arg] = t
                //      // args[arg] = r
                //      // args[arg] = i
                //      // args[arg] = n
                //      // args[arg] = g
                //    }
                //  }
                if (args) {
                    for (a in args) {
                        t = t.replace(/%s/, args[a]);
                    }
                }
                return t;
            },
            // Неправильный подход. Необходимо использовать
            // событие "load" для XMLHttp объекта.
            //
            // Отсутствие проверки на тип.
            // Пример:
            //  pojs.ready('fucking code');
            //  Uncaught TypeError: string is not a function
            ready: function(cb) {
                var _t = this, i = setInterval(function() {
                    if (_t._r) {
                        cb();
                        clearInterval(i);
                    }
                }, 10);
            },
            _load: function() {
                // "'localStorage' in window" и "window['localStorage'] !== null" - одно и то же.
                var _t = this, x, lsa = 'localStorage' in window && window['localStorage'] !== null, cache = null;
                if (_t._l) {
                    if (lsa) {
                        cache = localStorage.getItem('pojs_' + _t._l);
                        if (cache) {
                            _t._p = JSON.parse(cache);
                            _t._r = true;
                            return;
                        }
                    }
        
                    x = new XMLHttpRequest();
                    x.onreadystatechange = function() {
                        // Избыточная вложенность и неправильное обращение к объекту.
                        //
                        // Выражение записывается так:
                        // if (this.readyState === 4 && this.status === 200) {
                        //   // actions
                        // }
                        //
                        // Или же еще проще:
                        // if (this.response) {
                        //   // actions
                        // }
                        //
                        // Помимо этого, в IE8, данный код обрастет ошибками:
                        //  - отсутствие объекта XMLHttpRequest;
                        //  - отсутствие объекта console.
                        if (x.readyState === 4) {
                            if (x.status === 200) {
                                // Выше, почему-то, есть проверка на существование
                                // localStorage, а тут подразумевается что объект
                                // будет во что бы то ни стало?
                                localStorage.setItem('pojs_' + _t._l, x.responseText);
                                _t._p = JSON.parse(x.responseText);
                            } else {
                                console.error('Can not load JSON from ' + _t._l);
                            }
                            _t._r = true;
                        }
                    };
                    x.open('GET', this._l, true);
                    x.send();
                } else {
                    _t._r = true;
                }
            }
        };
        

        P.S. Создал pull request на GitHub.
  • 0
    Как это обычно бывает, все уже написано, оттестировано, проверено на продакшене. github.com/socialabs/puttext

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.