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.
    Писал чисто под свои нужды, возможно, вам чего-то не хватает или что-то работает не так. Готов править, улучшать!
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 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

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