Мультиязычное приложение Phonegap
Выбор техники перевода
Приложение Phonegap представляет собой обычную html-страницу и JavaScript файл. Многоязычность можно достигнуть несколькими путями.1. Например, можно сделать по одной страничке html на каждый язык. Что-то типа index-ru.html, index-en.html и подгружать нужную из главной index.html.
Минусы такого подхода
- Очень сложно будет вносить одинаковые изменения в разные файлы по мере развития программы.
- Невозможно одновременно работать программисту и переводчику (если это разные люди).
- Очень сложно будет делать изменения по текстам, к которым уже сделан перевод.
Плюс такого подхода
- Переводчику будет легко переводить и сразу видеть изменения (в браузере).
Минусы подхода
- Очень легко где-то ошибиться программисту и тогда в нужном месте html-страницы будет либо пусто, либо не тот перевод.
- Переводчику будет очень сложно работать. Например, будет эталонный языковой файл (английский). Переводчик его берёт, копирует и делает перевод непосредственно в файле. Пока всё легко и гладко. Потом программист изменяет эталонный языковой файл. Переводчику нужно скачать новый файл, понять, что изменилось, найти уже переведённое, как-то всё объединить в новый перевод. И так каждый раз...
Плюс подхода
- Программист и переводчик могут работать одновременно.
3. Чтобы избежать всех этих неудобств был придуман и годами утвердился метод переводов Gettext. Помимо устранения всех перечисленных выше трудностей, этот способ решает проблемы с формами единственного и множественного числа (одно яблоко; два яблока; пять яблок) - которая в разных языках отличается. Смысл методики следующий. Программист пишет программу, где весь текст оборачивается в специальные конструкции, например {{_ "My text"}}. Специальная программа (стандартная xgettext) натравливается на файлы с такими обёрнутыми текстами и собирает их в один файл .PO. Например, en.po для английского языка. Переводчик берёт себе этот файл и работает с ним привычными средствами (есть много программ для работы с этим форматом). При изменении программы генерируется новый файл .PO, который учтёт уже имеющиеся переводы! Этот формат в конечном счёте можно преобразовать в какой-то другой для удобного встраивания в программу.
Минусы подхода
- Стандартными средствами этот метод не работает с файлами HTML и JavaScript.
Плюсы подхода
- Программист и переводчик могут работать одновременно, не мешая друг другу.
- Легко вносить изменения в переводы.
- Нет проблем с формами множественного числа.
- В худшем случае, если нет перевода, текст программа покажет на английском языке.
Изобретаем велосипед
Я предлагаю подход к интернационализации приложения, задействующий целый стек технологий. Само приложение пишется на HTML и JavaScript и затем компилируется под ту или иную платформу (Android, Windows и т.п.) с помощью PhoneGap. Чтобы упростить себе жизнь и не писать всё с нуля, задействован очень элегантный фреймворк Framework7. В рамках этого фреймворка используется шаблонизатор Template7. В шаблонизаторе задействован механизм помощников (helpers), который позволяет мне обёрнутые текстовые конструкции из файла html перевести с помощью популярной библиотеки Jed. Языковые файлы jed использует в формате JSON. Чтобы сгенерировать эти файлы, нужно сначала по методике Gettext собрать исходные тексты из html/javascript и сформировать файлы .po, с которыми будет работать переводчик в программе PoEdit (или любой другой, поддерживающей Gettext). После того, как перевод готов, файлы .po преобразуются в .json с помощью утилиты po2json.Вся загвоздка в этой расписанной схеме кроется в том, что родная утилита xgettext не ищет тексты для переводов в файлах .html, .js (Poedit с версии 1.7 начал понимать JavaScript вслед за GNU Gettext версии 0.18.3) и .json. Видимо, когда создавали этот инструмент, не предвидели, что кому-то понадобится создавать приложение на HTML. Но прогресс не стоит на месте и это стало востребовано, а вот инструмент заточить не подоспели. Приходится идти на ухищрения.
Сбор текстов для переводов из файлов .js и .json
Для генерации файлов переводов будем использовать программу Poedit. Для тех, у кого номер версии Poedit менее 1.7 потребуются следующие ухищрения (для более новых версий ничего дополнительно делать не требуется). В программе заходим в меню [Правка/Установки], переходим на последнюю вкладку "Парсеры". Преобразовываем настройки парсера для Perl, чтобы он искал нам тексты в JavaScript файлах. Для этого дополним список расширений до следующего состояния: "*.pl;*.js;*.json". Остальные настройки оставляем без изменений:Сбор текстов для переводов из файлов .html
С файлами HTML придётся помучится больше. Сначала установим парсер xgettext для Handlebars. Пропишем его в настройках PoEdit, как в инструкции на странице разработчика. Получим ещё один парсер:Чтобы этот парсер стал собирать тексты из HTML, придётся все расширения .html на момент обработки изменить на .hbs... Или применить доп настройку (которую любезно подсказал автор утилиты):
список список расширений дополняем до
*.hbs ; *.html
xgettext-template --force-po -o %o %C %K %F -L Handlebars
Готовим файлы к переводу
Для тестирования того, что тексты для переводов собираются из файлов нормально, подготовим тестовый файл HTML test.html:<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <p id="pText">{{_ myText}}</p>
    <button id="btnSign">{{_ "Sign in"}}</button>
    <p id="pPlural">{{ngettext "There is one translation." "There are %d translations." count}}</p>
  </body>
</html> 
{"exerciseType":[
  {
    "id": 0,
    "name": {{_ "Exercise 1"}},
    "exercises": [
      {
        "name": {{_ "WG pull down"}}
      },
      {
        "name": {{_ "NG pull down"}}
        ]
      },
      {
        "name": {{_ "Chin up"}}
      }
    ]
  }
]}
Но тогда JSON файл будет не правильной структуры (ожидаются определённые простые типы значений параметров) и программа его просто не поймёт. Можно сделать в таком формате файл, парсером собирать с него тексты для перевода, а перед сборкой программы убирать все конструкции "{{_" и "}}", а затем в программе переводить полученные данные с помощью Jed: i18n.gettext(). Это нужно будет проделывать каждый раз при сборке программы - не удобно. Поэтому отказываемся от идеи хранить тексты к переводу в .json.
Открываем программу Poedit и создаём в ней первый проект по переводу. Для начала пусть это будет английский язык. Все тексты в программе и так на английском, но для примера перевод на английский в данном случае также делаем. В меню нажимаем [Файл/Создать каталог...] и заполняем форму. Для параметра "Формы множественного числа" внизу окошка приведена ссылка на шпаргалку. Либо можно про всё это по-подробнее прочитать в GNU первоисточнике. Для английского языка эта магическая строка выглядит так:
nplurals=2; plural=n != 1;Переходим на следующую вкладку и вручную (?!) заполняем путь до каталога, где лежат наши тестовые файлы:
Переходим на следующую вкладку и заполняем все ключевые слова. У меня будут использоваться следующие: "_", "ngettext:1,2" (для множественного числа) и "gettext" (для файлов .js). В новых версиях Poedit (а может и в старых) можно тут вообще ничего не указывать, т.к. для каждого языка парсер уже знает предопределённые ключевые слова.
Нажимаем "OK" и создаём файл en.po. После этого моментально наши файлы просканируются и будет выдан результат - все найденные тексты для перевода (с учётом JSON, который я потом переделал):
Всё. Теперь сгенерированный файл en.po можно отдавать переводчику, который всё в той же программе Poedit (или в какой-нибудь другой) сможет перевести все строки. Допустим, переводчик перевёл одну фразу. Посмотрим, что получилось внутри файла en.po:
msgid ""
msgstr ""
"Project-Id-Version: Example\n"
"POT-Creation-Date: 2015-01-14 16:40+0300\n"
"PO-Revision-Date: 2015-01-14 16:44+0300\n"
"Last-Translator: Oleg Ekhlakov <subspam@mail.ru>\n"
"Language-Team:  <subspam@mail.ru>\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.5.4\n"
"X-Poedit-KeywordsList: _;ngettext\n"
"X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-SearchPath-0: /home/oleg/Загрузки/testi18/hbs\n"
 
#: /home/oleg/ÐагÑÑзки/testi18/hbs/text.json:4
msgid "Exercise 1"
msgstr "First exercise" 
 
#: /home/oleg/ÐагÑÑзки/testi18/hbs/text.json:7
msgid "WG pull down"
msgstr "" 
 
#: /home/oleg/ÐагÑÑзки/testi18/hbs/text.json:10
msgid "NG pull down"
msgstr "" 
 
#: /home/oleg/ÐагÑÑзки/testi18/hbs/text.json:14
msgid "Chin up"
msgstr "" 
 
#: /home/oleg/ÐагÑÑзки/testi18/hbs/test.hbs:8
msgid "Sign in"
msgstr "" 
 
#: /home/oleg/ÐагÑÑзки/testi18/hbs/test.hbs:9
msgid "country"
msgstr ""
#: /home/oleg/ÐагÑÑзки/testi18/hbs/test.hbs:10
msgid "There is one translation."
msgid_plural "There are %d translations."
msgstr[0] ""
msgstr[1] "" Всё, что осталось без перевода будет по-умолчанию заменено на исходный текст.
Для русского языка по аналогичной схеме создаём файл ru.po. Для начала пересохраним текущий перевод: [Файл/Сохранить как] и укажем имя ru.po. Откроем свойства [Каталог/Свойства...]. В форму множественного языка попадёт более великая и могучая формула:
nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);Конвертируем файлы перевода для применения в программе
В программе специально подготовленные переводы будет обрабатывать библиотека Jed, поэтому нужно под неё подстроиться. Конвертируем .po в .json. Для этого применим утилиту po2json. Установим её по инструкции через npm. Можно запускать в консоли (переходим в консоли в рабочий каталог):po2json ru.po ru.json --format=jed --prettyЯ предварительно перевёл некоторые строки в русском файле и сохранил:
Вот что получается после конвертации в JSON:
{
   "domain": "messages",
   "locale_data": {
      "messages": {
         "": {
            "domain": "messages",
            "plural_forms": "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);",
            "lang": "ru"
         },
         "Exercise 1": [
            null,
            "Первое упражнение"
         ],
         "WG pull down": [
            null,
            ""
         ],
         "NG pull down": [
            null,
            ""
         ],
         "Chin up": [
            null,
            ""
         ],
         "Sign in": [
            null,
            "Зарегистрироваться"
         ],
         "There is one translation.": [
            "There are %d translations.",
            "Один перевод",
            "%d перевода",
            "%d переводов"
         ]
      }
   }
}--format=jed1.xТак что пока поправим готовый файл вручную (в надежде на то, что в будущем всё поправят). Для этого в полученном JSON файле нужно убрать все строки "null,", а также преобразовать форму множественного числа, удалив первый из четырёх вариантов. Должно получиться так:
{
   "domain": "messages",
   "locale_data": {
      "messages": {
         "": {
            "domain": "messages",
            "plural_forms": "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);",
            "lang": "ru"
         },
         "Exercise 1": [
            "Первое упражнение"
         ],
         "WG pull down": [
            ""
         ],
         "NG pull down": [
            ""
         ],
         "Chin up": [
            ""
         ],
         "Sign in": [
            "Зарегистрироваться"
         ],
         "There is one translation.": [
            "Один перевод",
            "%d перевода",
            "%d переводов"
         ]
      }
   }
}Оживляем приложение
Чтобы увидеть переведённые тексты в своём HTML приложении, нужно дописать обработку JavaScript, которая будет подменять все шаблоны на их перевод. Так как я применяю фреймворк, то все примеры будут с учётом подгрузки необходимых ему библиотек.В таком приложении библиотеки задействуют возможности друг друга, поэтому их нужно загружать в определённом порядке. Ранее я для этого пробовал использовать библиотеку Modernizr, но потом отказался от этого - дико тормозит.
Внутри тега <head> добавим загрузку Jed и файлов стилей Framework7. Перед закрывающим тегом </body> добавим поочерёдную загрузку Framework7 и своего скрипта. Получается следующее:
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
    <link rel="stylesheet" href="css/framework7.min.css">
    <script src="js/jed.min.js"></script>
  </head>
  <body>
    <p id="pText">{{_ myText}}</p>
    <button id="btnSign">{{_ "Sign in"}}</button>
    <p id="pPlural">{{ngettext "There is one translation." "There are %d translations." count}}</p>
    <script src="js/framework7.min.js"></script>
    <script src="js/my-app.min.js"></script>
  </body>
</html>
var myApp = new Framework7({
  init: false
});
i18n = new Jed(response);
Template7.registerHelper('_', function(msgid) {
  return i18n.gettext(msgid);
});
Template7.registerHelper('ngettext', function(msgid, plural, count) {
  return i18n.translate(msgid).ifPlural(count, plural).fetch(count);
});
Теперь можно инициализировать приложение и хелперы можно будет применять:
myApp.init();
var template = $$('#pText').html();
var compiledTemplate = Template7.compile(template);
var context = {
  myText: 'My super text!!!'
};
var html = compiledTemplate(context);
document.getElementById("pText").innerHTML = html;
var btnTemplate = $$('#btnSign').html();
var compiledBtnTemplate = Template7.compile(btnTemplate);
var html2 = compiledBtnTemplate();
document.getElementById("btnSign").innerHTML = html2
var plurTemplate = $$('#pPlural').html();
var compiledPlurTemplate = Template7.compile(plurTemplate);
var context = {
  count: 0
}
var html3 = compiledPlurTemplate(context);
document.getElementById("pPlural").innerHTML = html3;
var myApp = new Framework7({
  init: false
});
var $$ = Dom7;
var lang = 'ru';
var fLang;
var langData;
var i18n;
if (lang === 'ru') {
  fLang = './ru.json';
} else if (lang === 'en') {
  fLang = './en.json';
}
$$.getJSON(fLang, function(response) {
    i18n = new Jed(response);
  Template7.registerHelper('_', function(msgid) {
    return i18n.gettext(msgid);
  });
  
  Template7.registerHelper('ngettext', function(msgid, plural, count) {
    return i18n.translate(msgid).ifPlural(count, plural).fetch(count);
  });
  myApp.init();
  
  var template = $$('#pText').html();
  var compiledTemplate = Template7.compile(template);
  var context = {
    myText: 'My super text!!!'
  };
  var html = compiledTemplate(context);
  document.getElementById("pText").innerHTML = html;
  
  var btnTemplate = $$('#btnSign').html();
  var compiledBtnTemplate = Template7.compile(btnTemplate);
  var html2 = compiledBtnTemplate();
  document.getElementById("btnSign").innerHTML = html2;
 
  var plurTemplate = $$('#pPlural').html();
  var compiledPlurTemplate = Template7.compile(plurTemplate);
  var context = {
    count: 0
  }
  var html3 = compiledPlurTemplate(context);
  document.getElementById("pPlural").innerHTML = html3;
});
myText: 'My super text!!!' myText: i18n.gettext('My super text!!!')Теперь все простые строковые варианты перевода работают.
В реальном приложении нет необходимости каждую строку "компилировать" отдельно. Можно сделать перевод всех строк текста приложения вызовом одной функции. Например, такой:
function translate(fLang) {
  $$.getJSON(path + fLang, function(response) {
    console.log('Загрузили новый языковой файл! fLang = ' + fLang);
    i18n = new Jed(response);
    console.log('i18n = ' + JSON.stringify(i18n));
    // Переводим все шаблоны текстов в html на нужный язык
    var template = $$('.app-text').each(function() {
      //console.log('Переводим очередную строку');
      var compiledTemplate = Template7.compile($$( this ).text());
      //console.log('compiledTemplate = ' + compiledTemplate);
      var htmlText = compiledTemplate();
      //console.log('New htmlText = ' + htmlText);
      $$( this ).text(htmlText);
    });
  });
} 










 
 
Хорошая статья
ОтветитьУдалитьСпасибо на добром слове! Надеюсь, статья кому-то поможет в нелёгком деле переводов программ на другие языки.
Удалить