Чуть позже появилась необходимость извлекать так же из 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», которые потом отдаются на перевод, что все не так просто как кажется на первый взгляд.
За то, какая строка будет выбрана решают правила предопределенные и описанные в 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 файл соответствующий каждому нужному языку.
А что собственно переводить?
Вопрос не так прост как кажется на первый взгляд:
Часть простая — шаблоны и код.
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, по которому решается какая строка используется, а второй раз в качестве параметра для строки, на ряду с другими возможными параметрами подставляемыми в эту строку.
— Как я уже говорил — это простая часть, завернуть существующий текст.
Далее нужно
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, либо между ними.
Но для начала поискать в конфигурации обоих.
Если что-то упустил, не описал и т.д. — пишите в комментах, дополню.
Цели разобрать детально каждую утилиту не стояло, цель была рассказать о существовании оных и о том, как именно и почему именно так они работают вместе.
Остальному найдется место в комментариях
ссылка на оригинал статьи http://habrahabr.ru/post/199226/
Добавить комментарий