Разработка

индекс
203,40

Идея: функция форматирования для удобной локализации строк

Проблема: при переводе приложений на другие языки (чаще мы сталкиваемся с русификацией англоязычных продуктов) чаще всего страдает поддержка множественных форм чисел. Например, «1 заметка, 2 заметки, 5 заметки» или якобы универсальное «1 file(s)» и т.д. Дело в том, что во всех языках программирования обычно дело не идет дальше варианта sprintf() или какого-то шаблонизатора, а поддержку множественных форм надо каждый раз программировать ручками: если N = 1, то «1 заметка», иначе «N заметок». А делать это каждый раз лениво. Эту задачу частично решает фреймворк gettext, где есть понятие нескольких вариантов локализованной строки, но это не сильно облегчает жизнь, ибо в одной строке, показываемой пользователю, может быть несколько частей, зависящих от чисел («Найдено 23 файла в 3 папках»), а значит куски строк надо потом все равно склеивать между собой.

Поэтому мне пришла следующая идея: почему бы не придумать какой-то общий формат, микроязык (по аналогии с вполне устоявшимся форматом функции format() или sprintf()), чтобы адресовать эту проблему и упростить в дальнейшем написание локализуемого кода.

Как известно, в разных языках разное количество множественных форм числа. В английском их два («1 file», «many files»). В русском — три («1 файл», «2 файла», «много файлов»). В арабском их вообще, как говорит нам Pootle, аж 6 штук. Поэтому нам нужно иметь возможность задавать прямо в строке набор из нескольких подстрок и параметр, от которого зависит выбор этих подстрок.

Предлагаемый формат подстроки с множественными вариантами:
{%COUNTER%|FORM0|FORM1|FORM2[|FORM3][|FORM4][|...]}

где
%COUNTER% — это некое имя переменной, которое может иметь значения [0,1,2,3,...]
FORM0 — это версия строки для значения COUNTER = 0 (это особый случай, обычно требующий отдельного сообщения)
FORM1, FORM2 и т.д. — это альтернативные варианты текста для каждой множественной формы данного языка; для английского это будет два варианта, для русского — три и т.д.

Пример на английском языке:
{%F%|No files|1 file|%F% files} found in {%D%|0 folders|1 folder|%D% folders}.

Это даст нам следующие конечные варианты строки на выходе при подстановке различных значений %F% и %D%:
%F% = 0, %D% = 1 =>No files found in 1 folder.
%F% = 1, %D% = 2 => 1 file found in 2 folders.

Тот же пример строки, переведенной на русский:
{%F%|Не найдено файлов|1 файл найден|%F% файла найдено|%F% файлов найдено} в {%D%|0 папках|1 папке|%D% папках|%D% папках}.

Это даст нам следующие конечные варианты строки на выходе при подстановке различных значений %F% и %D%:
%F% = 0, %D% = 1 => Не найдено файлов в 1 папке.
%F% = 1, %D% = 2 => 1 файл найден в 2 папках.

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

Было бы здорово иметь реализацию подобного микроязыка на разных языках программирования.

Надеюсь, кому-нибудь идея пригодится.

UPD: Результат не заставил себя долго ждать: в данном топике хабраюзер webdew делится реализацией функции на C#, за что ему большое спасибо.
+21
23 марта 2009, 17:56
26

комментарии (40)

+1
iv_s #
Мне идея очеь понравилась, буду использовать в своих проектах. Спасибо.
+1
tom #
Вы переизобретаете gettext? :)
0
tom #
Виноват. Признаюсь, не прочитал целиком, потому что подумал что если вы знаете про gettext, то у вас не должно появляться такого вопроса. И да, gettext действительно не очень удобен, когда несколько числительных идут в одной фразе, хотя такая ситуация и встречается не очень часто.
0
afan #
gettext и для одного числительного не очень хорош, т.к. либо все равно требует некоторых лишних телодвижений с точки зрения программирования, либо избыточного дублирования текста. Пример:

Когда строка короткая, то все хорошо:
ngettext("%d comment", "%d comments", $n)

Когда строка длинная, то это приводит либо к избыточной работе со стороны переводчика:
ngettext("Do you really want to delete %d comment? Operation can't be undone.", "Do you really want to delete %d comments? Operation can't be undone.", $n)

либо начинаются пляски с разбиением законченного сообщения на отдельные составляющие, что тоже не всегда полезно:
ngettext("Do you really want to delete %d comment?", "Do you really want to delete %d comments?", $n) + gettext("Operation can't be undone.")

Поэтому я и полагаю, чтоу уместнее было бы всю логику формирования строки засунуть в саму строку, а программисту ограничиться единичным взызовом типа
localize("string", ('param1' => 'value1', 'param2' => 'value2'))

0
tom #
В принципе исходя из своего опыта работы с геттекстом, я тоже не мог бы назвать его удачным решением проблемы, однако многие переводчики уже привыкли работать именно с ним и приходится с этим мириться.

Когда я размышлял о вопросе локализации текстов, то мне на ум приходил вариант, который я бы назвал объектно-ориентированным. Зачастую переводить нужно не только надписи на кнопках и простые фразы, состоящие из одного-двух приложений, а целые письма, состоящие из нескольких абзацев, содержащие медиа-вложения и прочее.

Хотелось бы чтобы для всего этого использовался один подход (хотя бы идеологически). Сама собой приходит идея выделения всех переводных сущностей из текста и некий механизм привязки этих сущностей к тексту. Пока только к сожалению ничего конкретного не удалось придумать, т.к. хватает других задач.

Предложенный вами подход в целом интересен, для простых проектов использовать его может быть целесообразно. Еще раз прошу прощения, что невнимательно прочитал топик в первый раз :)
0
afan #
Ну переводчики-то работают не с самим gettext, а с .po-файлами. И ничто не мешает нам использовать эти .po файлы как формат хранения строк для перевода и для отдачи переводчикам. А строка внутри .po уже может быть какой угодно, с какими угодно включениями. Надо только объяснить переводчикам про этот микроязык и как им пользоваться.
+1
kolyan #
Думается, для русского языка будет больше трех вариантов для вашего примера. Например 21 файл в 11 папках
0
afan #
В примере русской строки ошибка была — поправил.
+3
eschava #
java класс ChoiceFormat делает тоже самое
и выглядит формат почти так же :)
{0, choice, 1#is negative| 0#is zero or fraction | 1#is one |1.0> 1+ |2#is two |2<is more than 2}
0
afan #
О, спасибо, посмотрю.
+2
Mercury13 #
Я думал в точности о таком. И даже придумал тупой язык скриптов для этого. Имеет четыре оператора.
% x; — остаток от деления
= x { op } op — условный оператор, проверка на равенство
in x y { op } op — условный оператор, проверка на принадлежность диапазону
take x; — взять вариант x

Русский
; ворона|вороны|ворон
_PluralRule = { %100; in 5 20 { take 3; } %10; =1 { take 1; } in 2 4 { take 2; } }

Английский
; crow|crows
_PluralRule = { =1 { take 1; } }

Если не прошёл ни один take, автоматически берётся последнее, т.е. «ворон» или, соответственно, «crows».

Сейчас я бы то же самое сделал на XML или подобном языке иерархического описания данных — тогда в распоряжении были только INI-файлы.

Кстати, почему именно «вороны» — из-за фразы «считать ворон».
0
Goodkat #
Про остальные падежи не забывайте: воронам, воронах, и т.п.
Это только в английском всё так просто :)
0
Mercury13 #
Именительный: ворона|вороны|ворон
Родительный: вороны|ворон|ворон
Дательный: вороне|воронам|воронам
Винительный: ворону|ворон|ворон
Творительный: вороной|воронами|воронами
Предложный: вороне|воронах|воронах

Так что вроде всё в порядке, и если не использовать один текст дважды (стандартное правило интернационализатора), проблем не будет.
–5
brutaler #
когда все уже хабракат ставить научатся
0
Ueasley #
Я написал маленький плагин для Smarty, в итоге все выглядит так:

{decline num=8 words=«сумка, сумки, сумок»}

А что бы не писать кучу раз одно и то же, связал это с системой языков. Можно написать и так:

{decline num=8 word=«сумка»}, и он сам возьмет из языковых пакетов нужные варианты.

0
kulakowka #
поделитесь плагином?
0
Ueasley #
Отрезал все лишнее, вот:

function smarty_function_decline($params, &$smarty) {
$titles = split(",", $params['titles']);
return Decliner::declOfNum($params['num'], $titles);
}


Decliner::declOfNum

static function declOfNum($number, $titles) {
$cases = array (2, 0, 1, 1, 1, 2);
return $titles[($number % 100 > 4 && $number % 100 < 20) ? 2 : $cases[min($number % 10, 5)]];
}
0
kulakowka #
спасибо большое.
Люблю обмен опытом
0
Ueasley #
Случайно отрезал проверки на сущестсвование индексов/массива =\
0
egorinsk #
Ы))

А я написал в свое время модификатор, работает на sprintf() и используется примерно так:

{$vacancies_count|incline_word:«Всего %d вакансия»:«Всего %d вакансии»:«Всего %d вакансий»:«Нет вакансий»} (последний параметр необязательный)

Имхо удобнее)

0
Bal #
Аналогично, только синтаксис такой (и без проверки на ноль, я стараюсь по мере возможности практиковать «thinking forth»):

Всего {$vacancies_count} ваканси{$vacancies_count|sklon:'я, и, й'}

:)
0
egorinsk #
У вас неудобно когда 2 слова подряд, например «2 хороших вакансии». Но вообще это все самодеятельность и на будущее стоит посматривать в строону gettext или чего-то такого.
0
Bal #
В случае двух слов подряд будет просто «{$vacancies_count} {$vacancies_count|sklon:'хорошая вакансия, хороших вакансии, хороших вакансий'}».

>и на будущее стоит посматривать в строону gettex

Как только он появится на всех хостингах — так сразу :) А пока приходится ориентироваться на хостинг массовый.
0
kai #
gettext вроде-бы прекрасно может работать с plural forms?

blog.gate.lv/2008/04/23/gettext-plurals/
0
afan #
Ну вот мне кажется, что это «прекрасно» несколько преувеличено (см. мои комментарии выше).
0
crocodile2u #
Для геттекста есть poedit, геттекст — де-факто стандарт (ок, наряду с еще несколькими программами), А чем может похвастать ваша система?

ЗЫ. Это не к тому, что ваше начинание плохое. Всегда хорошо иметь альтернативы. Но, если посмотреть правде в глаза: приобрести хоть сколько-нибудь заметную популярность ваше решение сможет вряд ли :(
0
afan #
Ничто не мешает продолжать использовать формат .po, все написанные для него утилиты типа poedit или того же Pootle, про который я упомянул выше. Но в строках могут содержаться любые строки, в т.ч. и в предложенном мной формате. Переводчик точно также как и раньше осуществляет перевод, мы достаем из скомпилированного .mo локализованный вариант строки, а затем пропускаем через некую функцию (по аналогии со sprintf), которая уже формирует готовую строку на основе данных ей параметров.
0
hawange #
Вроде как XLIFF может что-то похожое…
0
afan #
Знаю про XLIFF, но это немного из другой оперы — никто им не будет пользоваться как форматом хранения строк в программе (для этого у всех уже существуют свои методы properties-файлы в Java, rc-файлы в VC, strings-файлы на Маке и т.д. Нужно нечто, что любой бы использовал здесь и сейчас — строки по прежнему хранятся в ресурсах, специфичных для данной платформы/языка, но вместо всяких макросов вида %s или {0} использовался бы простой и удобный микроязык, позволяющий несколько строк в ресурсах «схлопнуть» до одной. Это упрощает и кодирование и локализацию.
+1
Paulus #
И до кучи: форматное выражение должно учитывать порядок слов в разных языках, не факт, что «21 файл в 11 папках» не окажется в китайском «в 11 папках 21 файл». В MSDN про это статьи есть неплохие
0
afan #
Именно! gettext эту задачу опять решить не в силах, перекладывая все на разработчиков, а в предложенном варианте это решается на ура — внутри строки все можно переставить как надо:
В {%D%|0 папках|1 папке|%D% папках|%D% папках} {%F%|файлов не найдено|найден 1 файл|найдено %F% файла|найдено %F% файлов}.
+1
jerlen #
вот два сайта, где подобные проблемы с формами для множественного и единственного числа решаются простыми формулами, учитывая языковые особенности.

Список формул 1
Список формул 2
0
afan #
Эти формулы требуются и в случае gettext, и в моем случае — от них, разумеется, никуда не деться. Эти формулы как раз должны использоваться внутри той функции, которая быдет делать выбор подстроки по заданному числу.
0
Goodkat #
В английском один падеж и одна форма множественного числа: 1 coder, 2 coders. Остальные падежи реализуюся предлогами.
В русском языке шесть падежей (иногда и больше), и к тому-же падеж зависит от предстоящего числительного: 1 стол — именительный., 2 стола — уже родительный, ед. число. 5 столов — род. множ. число.
Нужно как-то и это учитывать для универсальности решения.

0
afan #
Множественных форм числа всего три (максимум) в любом случае, иногда формы одинаковы при склонении слова:
Кто? Что? — 1 стол, 2 стола, 5 столов
Кого? Чего? — 1 стола, 2 столов, 5 столов
Кому? Чему? — 1 столу, 2 столам, 5 столам
Кого? Что? — 1 стол, 2 стола, 5 столов
Кем? Чем? — 1 столом, 2 столами, 5 столами
О ком? О чем? — 1 столе, 2 столах, 5 столах

Так что тут все работает.

0
tenshi #
«ни одного файла найдено»
НЛО прилетело и опубликовало эту надпись здесь
0
afan #
> Ну и вторая мысль: надо делать рекурсивную обработку строк, т.е., например у нас есть {%F%a|b|c}, вот в строках a, b и c мы тоже
> можем писать блоки форматирования.

Да, вложенность могла бы пригодиться. Будут ли только эти вложенные конструкции понятны простому переводчику?..
0
afan #
> Вроде того, что когда функция видит %F%, она вставляет значение F в это место.
> Когда видит {%F%...} вставляет подходящий текст, но не само число.

Вроде так я и описывал.

0
afan #
Упс. Не туда ушло.

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