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 похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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

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