Чуть позже появилась необходимость извлекать так же из 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 и на выходе превращаются в строку на нужном языке, сохраняя правила языка касающиеся разного склонения для множественных чисел + возможность указать контекст и домэин.
Важно заметить, что именно строки, они же ключи, а не константа 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] ""
Т.е кол-во результирующих строк зависит от конфигурации языка. Может быть и есть язык, в котором этак десяток форм множественного числа…
Все те, у кого до сих пор 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, либо между ними.
Но для начала поискать в конфигурации обоих.
Если что-то упустил, не описал и т.д. — пишите в комментах, дополню.
Цели разобрать детально каждую утилиту не стояло, цель была рассказать о существовании оных и о том, как именно и почему именно так они работают вместе.
Остальному найдется место в комментариях
This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:
- Massacres That Matter - Part 1 - 'Responsibility To Protect' In Egypt, Libya And Syria
- Massacres That Matter - Part 2 - The Media Response On Egypt, Libya And Syria
- National demonstration: No attack on Syria - Saturday 31 August, 12 noon, Temple Place, London, UK
Комментариев нет:
Отправить комментарий