Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Рецепт i18n. Основа — Babel, json с кофе и грант с hbs на свой вкус

Anna | 16.06.2014 | нет комментариев
В своем предыдущем посте я писал о том для чего и отчего необходимо было сделать pybabel-hbs, экстрактор строк gettext из образцов handlebars.

Чуть позднее возникла надобность извлекать так же из json.
Так возник pybabel-json.
pip install pybabel-json либо на github

Там применялся лексер джаваскрипта встроенный в babel, но нюансы так же имелись, но пост не об этом, написанное там менее увлекательное, чем было в hbs плагине и вряд ли нуждается в заострении внимания.

Данный пост о том, как же в целом выглядит полный комплект для локализации, от и до, что делать с данными из БД, либо из иного не вовсе неподвижного места.
От и до включает в себя:
(должен подметить — что ни один пункт не является непременным, все это довольно легко подключается к любому приложению только Отчасти и по необходимости)

— Babel. Комплект утилит для локализации приложений.
— Grunt. Администратор задач(task-ов),
— coffeescript. В представлении не нуждается, каждый клиенстский код написан на coffee, и из него тоже необходимо извлекать строки.
— handlebars — темплейты
— json — хранилища строк
— Jed. gettext заказчик для js
— po2json. Утилита для перевода .po файлов в .json формат поддерживаемый Jed-ом

Немножко о gettext и мифах

gettext — первоначально комплект утилит для локализации приложений, сегодня же я бы назвал gettext еще и общепризнанным форматом. (не путать с исключительным)
Минимальную суть дозволено описать так, есть строки на английском, которые проходят через некую функцию gettext и на выходе превращаются в строку на надобном языке, сберегая правила языка касающиеся различного склонения для множественных чисел вероятность указать контекст и домэин.
Значимо подметить, что именно строки, они же ключи, а не константа USER_WELCOME_MESSAGE где-то превращающаяся в текст.

Контекст необходим вдалеке не каждому и в своих плагинах babel-а я его пока что не реализовывал, так как без потребности, пулл реквесты приветствуются
О домэине будет пара слов позднее.
А вот ngettext — штука безоговорочно нужная многим, если не каждому.
И здесь же о мифах.

Нуль яблок.              Zero apples
Одно яблоко.             One apple
Два яблока.              Two apples
Пять яблок.              Five apples

Данный примитивный пример должен показать каждому любителям языковых констант а-ля «USER_WELCOME_MESSAGE», которые потом отдаются на перевод, что все не так легко как кажется на 1-й взор.

За то, какая строка будет выбрана решают правила предопределенные и описанные в babel:
Скажем это для английского:

"Plural-Forms: nplurals=2; plural=(n != 1)n"

А это для русского:

"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)n"

Крупен и Могуч :)
Не необходимо опасаться, в ручную этого писать для, скажем, японского не прийдется.

Так вот, о мифах.
Несколько раз слышал суждение, что дозволено делать стержневой сайт на русском и оборачивать русские же строки в вызовы gettext, а потом добавить английский.
Если у вас свои костыли с применением тех самых языковых констант, у вас нигде нет склоняемых предложений с числами, а применяется уродливый формат в жанре «У вас яблок: 1», то безусловно, дозволено делать основным русский.
Если вы хотите отобразить пользователю чуть больше прекрасные сообщения, как скажем «У вас 1 яблоко», «У вас 7 яблок» то основным языком должен быть английский.

Отчего? Все дело в яблоках.
Множественное число не неизменно в исключительном числе, а исключительное число не неизменно для единицы.
Английский в этом плане примитивен, русский же нет.

ngettext по умолчанию, как ключ ждет именно английский язык. Больше того, ngettext на вход принимает только два параметра — исключительное число и множественное. А не массив множественных чисел.

Таким образом, если вы все таки хотите применять русский по умолчанию вам как минимум прийдется поддерживать файл перевода русский-русский, в котором строка «У вас есть %s яблок» будет превращаться в положительное склонение. Да, дозволено — но это криво.
При изменении необходимо будет помнить, что изменен только ключ, а не строка на русском языке и необходимо пойти и параллельно править файл русского языка. В всеобщем, не необходимо так делать. ngettext максимально совместим именно с английским языком в качестве оригинала.

Кстати, заодно покажу пример, того как выглядят .po файлы для английского и для русского

msgid "You have %(apples_count)d apple"
msgid_plural "You have %(apples_count)d apples"
msgstr[0] "У вас %(apples_count)d яблоко"
msgstr[1] "У вас %(apples_count)d яблока"
msgstr[2] "У вас %(apples_count)d яблок"
msgid "You have %(apples_count)d apple"
msgid_plural "You have %(apples_count)d apples"
msgstr[0] ""
msgstr[1] ""

Т.е кол-во результирующих строк зависит от конфигурации языка. Может быть и есть язык, в котором этак десяток форм множественного числа…

OK, So Where Do I Start?

Все те, у кого до сих пор 3 яблок обязаны быть мотивированы для того что бы начать

pip install babel

Тяжелая часть позади.

Осталось:
— Изменить в коде каждый текст на вызовы gettext
— Натравить babel на код
— На основе полученного .pot файла сделать .po файл соответствующий всякому необходимому языку.

А что собственно переводить?

Вопрос не так примитивен как кажется на 1-й взор:

Часть простая — образцы и код.
Django и flask — есть экстракторы из образцов
Python и javascript поддерживаются babel первоначально
handlebars и json — пришлось сделать, ссылки в начале поста.
Для coffeescript — рецепт дальше
Для каждого остального — гугл в поддержка

Еще раз, часть простая — код, для этого все строки необходимо обернуть в вызовы gettext/ngettext в соответствии с форматом, тот, что требует всякий из экстракторов. Как правило они так же предоставляют вероятность переопределить какую функцию обязаны применять
Скажем, у меня так:

pybabel extract -F babel.cfg -o messages.pot -k "trans" -k "ntrans:1,2" -k "__" .

trans и ntrans указан для джаваскрипта, а __ для питона, в котором эта функция применяется для прозрачной передачи строки(об этом позднее)

Т.е, все
print(«apple») необходимо переделать в print(ngettext(«apple»))
А все
print(«I have %s apples») в print(ngettext(«I have %s apple»,«I have %s apples»,num_of_apples)%num_of_apples)

Здесь должен подметить, чего и каждому хочу, что никогда не использую и не рекомендую применять неименнованные параметры.
В моем случае — только именнованые, то бишь выглядить это должно так:

Python:

print(gettext("I have an apple!"))

print(ngettext(
      "I have %(apples_count)d apple",
      "I have %(apples_count)d apples",
       num_of_apples
).format(apples_count=num_of_apples))

Применяется типовой gettext, для flask и джанго есть свои обертки

Javascript:

console.log(i18n.trans("I have an apple!"))
console.log(i18n.ntrans("I have %(apples_count)d apple","I have %(apples_count)d apples",num_of_apples,{apples_count:num_of_apples}));

Здесь и в кофе применяются прокси для способов Jed отсель:
github.com/tigrawap/pybabel-hbs/blob/master/client_side_usage/i18n.coffee
Параметры передаются в строку засчет встроенного в Jed sprintf

Coffeescript:

console.log i18n.trans "I have an apple!"
console.log i18n.ntrans "I have %(apples_count)d apple", "I have %(apples_count)d apples", num_of_apples, 
        apples_count:num_of_apples

Hadlebars:

{{#trans}}
I have an apple!
{{/trans}}

{{# ntrans num_of_apples apples_count=num_of_apples}}
  I have %(apples_count)d apple
{{else}}
   I have %(apples_count)d apples
{{/ntrans}}

JSON хранилище строк:

{
    "anykey":"I have an apple!",
    "another_any_key":{
           "type":"gettext_string",
           "funcname":"ngettext",
           "content":"I have %(apples_count)d apples",
           "alt_content":"I have %(apples_count)d apples"
    }
}

Оффтоп: Пояснение к этому формату в документации к pybabel-json

Думаю не трудно было подметить, что num_of_apples повторяется всякий вызов два раза.
Повод тому, что один раз он передается в качестве довода для ngettext, по которому решается какая строка применяется, а 2-й раз в качестве параметра для строки, на ряду с другими допустимыми параметрами подставляемыми в эту строку.

— Как я теснее говорил — это простая часть, завернуть присутствующий текст.
Дальше необходимо

1) Изменить все кнопки на которых надписи на кнопки с текстами. Все знают что кнопки с текстом это нехорошо. Но Зачастую это доводится принять, так как так стремительней, а дизайнер хочет именно так :)
— С этим пунктом все должно быть ясно — нудно, но нужно

2)
Куда больше увлекательный пункт, это что делать с как бы бы непрерывными строками, но которые не вовсе непрерывные?
Как пример приведу наш случай — стили к песням. Как бы бы и динамика, в БД хранятся, но по сути — редко меняющаяся статика, которую недурно было бы выдрать и отправить на перевод.

Именно это и стало поводом возникновения pybabel-json.
Это решение так же является решением всякий иной задаче перевода, как скажем — результат об ошибке стороннего сервера сообщением. Дозволено сказать что это статика, но это неподконтрольная нам статика, которую необходимо прекрасно завернуть для перевод.
Все что необходимо — сделать .json файл
errors.json
с содержимым

{
    "from_F_service": [
       "Connection error",
        "Access denied"
],
    "from_T_service":[
        "Oops, it is too long"
]
}

Никаких ключей, чистый массив строк.
Самое страшное что случится если сервис изменил сообщение — пользователь получит непереведенный вариант. Как правило это мелочи

С данными в БД обстановка схожая, в систему билда-пуша-деплоя, что бы то ни было (чай что-то у вас есть)? на том же ярусе, где будут комманды для сборки каждого и каждая babel-ом необходимо перед этими самыми командами добавить скрипт тот, что будет извлекать все надобные данные из БД и собирать сходственный json, запущенный следом babel теснее соберет данные.
Само собой — такие файлы следует добавить в .gitignore либо аналог чего-бы-там ни было, в всеобщем, чтоб в source control не попадало

Все строки, которые получены сходственным образом обязаны проходить через вызов gettext функции
Т.е если это в python, то gettext(), в js Jed либо прокси-способы приведенные ранее

Так же следует подметить, что порой хочется сделать в обратном порядке. Либо нужно сделать в обратном порядке.
Т.е определить в коде что строка должна переводиться, но непринужденно сам перевод будет запущен в ином месте.
Приведу пример на python:

class SomeView(MainView):
      title=gettext("This view title")

Если вы напишите сходственный код, то вы рискуете получить сделанную копию класса в английском исполнении, если класс создался при запуске сервера, либо скажем китайскую версию, если создание было динамическим но кешируемым при первом заходе

В таких случаях хочется подметить для перевода, но перевести в необходимом месте
Необходимое место это создание объекта, а не класса
т.е


def __(string,*k,**kwargs):
    return string

class MainView(SomeParent):
    def __init__(self):
             #....
             self.title=gettext(self._title)
             #....

class SomeView(MainView):
      _title=__("This view title")

Т.е — сборщик строк определит __ как строку для перевода, сама функция не делает ничего, а перевод будет запущен в необходимое время.
Таким образом все в одном месте и выглядит прекрасно.

Это касается многих языков, в том числе coffeescript и джаваскрипт, если вы пишете под node.js.
Для браузера это менее актуально, так как даже в момент создания класса теснее должно быть вестимо для какого языка создавать.

Но в любом случае — положительнее перевести в конструкторе, а не в момент создания класса.

Как бы бы обошел все знаменитые мне вероятности направления перевода, возможен все это сделано.

Склеиваем все совместно

Сейчас дозволено попытаться все это собрать, здесь есть несколько примитивных шагов:
0) Сделать пустой каталог подлинных строк, чтоб не ругался в последующем на неимение файла

touch messages.pot

1) Сделать .po файлы целевых языков Это делается 1 раз и не должно включаться в билд. .po файлы это файлы содержащие как подлинные строки, так и перевод к ним, для всякого языка.

pybabel init -i messages.pot -d path/i18n -l es
#Эта команда сделает .po  для испанского языка в директории path/i18n/es (включая саму директорию i18n если необходимо)
#Повторить для всякого языка, либо за раз: (Кстати может кто подскажет, как это дозволено было сделать без echo?, echo мне кажется костылем) 
echo {es,en,fr,de,ja} | xargs -n1 pybabel init -i messages.pot -d path/i18n -l 

2) Создать/обновить .pot файл — основное хранилище строк Это так же не должно включаться в билд, а необходимо запускать когда нужно получить новые .po файлы, которые будут отправлены на перевод.

python/node/your_language update_translation_jsons 
#Упомянутое ранее обновлении данных из ДБ
pybabel extract -F babel.cfg -o messages.pot -k "trans" -k "ntrans:1,2" -k "__" .
# извлечение новых строк
# trans - для экстрактора из джаваскрипта, ntrans - тоже
#  __ для "прозрачного" экстрактора из питона
# babel.cfg - конфиг babel-а что и откуда брать
pybabel update -i messages.pot -d path/i18n/
#обновление .po файлов для всех языков,

Здесь будет не лишним показать пример babel.cfg файла, это mapping файл, указывающий на то, чем и из каких файлов извлекать строки:


[python: path/backend/notifier.py]
[hbs: path/static/**.hbs]
[json: path/static/i18n/src/**.json]
[javascript: path/static/**.coffee_js]
encoding = utf-8

3) Прогнать все .po файлы через po2json, для приобретения .json, которых и примет Jed.
Вот это дозволено и необходимо включить в build.
Чего невозможно делать — так это пускать в git, им там не место.

Как именно скормить все .po файлу и куда их положить — на совести юзера.
Я же их прогоняю в grunt, как и каждый остальной билд.
grunt-po2json тот, что есть на github и в репозитории гранта поломан, так как не поддерживает rename, а он необходим, так как по мне комфортней, когда все финальные .json файлы идут в одну директорию, локально я это поправил, но необходимо отправить на это дело пулл реквест…

Дозволено безусловно и гораздо проще, позже установки po2json (npm install po2json) включить что-то сходственное в build script:

echo {es,en,fr,de,ja} | xargs -n1 -I {}  po2json /path/i18n/{}/LC_MESSAGES/messages.pot /path/to/build/i18n/{}.json
Не вошедшие в поток мысли, но имеющие толк заострить на них внимание моменты

На протяжении поста несколько раз обещал «об этом позднее», но для позднее подходящего места не нашлось.

Как скажем:
coffeescript не имеет собственного экстрактора, т.к при билде статики coffeescript компилируется(либо транслируется) в javascript.
Следственно довольно запустить сборку .js строк позже перевода в джаваскрипт
В моем случае все даже немножко не так, рядом с всяким файлов coffee лежит файл coffee_js, тот, что создается с поддержкой grunt watch в момент редактирования (и перезапускает дев статику, но это тема для отдельного поста :) ), эти файлы само собой вне гита. Вот из них строки и вытаскиваются

— Еще было припоминание о домэинах.
Домэины в финальном результате это различные файлы, messages.pot/messages.po = домэин messages
Дозволено создавать несколько домэинов, все домэины привязывать к Jed инстансу, либо создавать несколько различных Jed инстанцев и перенаправлять в них
Но для этого необходимо расширять хелперы handlebars либо всякую иную обертку… У меня такой необходимости еще не было никогда, а как правило выбираю не делать ничего лишнего предварительно :)

— Маленькая сноска к тексу во вступительном блоке

Если вы хотите отобразить пользователю чуть больше прекрасные сообщения, как скажем «У вас 1 яблоко», «У вас 7 яблок» то основным языком должен быть английский.

Здесь следует понимать, что в вызове ngettext нужно писать именно «you have %(apples_count)d apples», а не «you have one apple»
Т.к в и в случае одного и в случае 21-ого финальная строка должна быть в первой форме — т.е «У вас %d яблоко»

— Так же будет значимым заострить внимание на одном вопросе, тот, что я еще не поспел решить на механическом ярусе:
babel создает «пустую строку» (конфигурация .po файла, определяющая какой это язык и какие обязаны быть строки для множественного числа) в формате не совместимом с Jed
Jed ждет, что там будет «plural_forms», babel же выдает Plural-Forms
Здесь необходимо будет править либо итог babel, либо вход Jed, либо между ними.
Но для начала поискать в конфигурации обоих.

Если что-то упустил, не описал и т.д. — пишите в комментах, дополню.
Цели разобрать подробно всякую утилиту не стояло, цель была рассказать о существовании оных и о том, как именно и отчего именно так они работают совместно.
Остальному найдется место в комментариях

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB