2016-01-10

О *зации

Только лишь часового пояса маловато. Есть ещё интересные термины. Локализация, она же localization, она же l10n. Кастомизация, она же customization, она же c10n. Интернационализация, она же internationalization, она же i18n. Обожаю эти милые сокращения, где цифра в середине означает количество пропущенных букв.
i18n
Это всё примерно об одном и том же. Чтобы ваше приложение выглядело и работало прилично и понятно для местных жителей и в США, и в Сибири, и в Китае, и в Арабских Эмиратах, и на дне Марианской впадины, и, кто знает, на орбите Плутона.
Это касается, в первую очередь, но не ограничиваясь, языком, на котором ваше приложение общается с пользователем. Язык, это в первую очередь — текст. Но, само собой, это касается и графики, надписи встречаются и на картинках, и звуков, речь ведь может и звучать, в играх, например.
Локализации подлежит целая куча всего. Чтобы сильно не мучаться, определяют некоторую локаль (locale). И для данной локали задают правила, характерные для данной местности. Какие это правила, можно увидеть, например, в man 5 locale.
  • LC_CTYPE — определения того, какие буквы тут большие, какие маленькие, какие цифры и т.п. Слава богу, с появлением Unicode, тут особых проблем и исключений уже нет.
  • LC_COLLATE — правила сортировки, от А до Я или как-то иначе. Тут всё действительно может быть по-разному. Даже европейские языки, использующие латиницу, любят щеголять всякими умлаутами, которые либо входят в алфавит, а, значит, учитываются при сортировке, либо нет.
  • LC_MONETARY — правила форматирования денежных единиц. С одной стороны, тут всё больше зависит от валюты, чем от страны. То же количество знаков после запятой, то бишь размер «цента», у всех валют разный. И свой знак тоже разный (да здравствует знак рубля). Но вот правила размещения знака валюты: до цифры или после, — а также разделители: дробной части числа и тысячей — в каждой стране могут быть разными.
  • LC_NUMERIC — правила форматирования чисел. Ну, в общем, те же деньги, но без знака валюты. Основное дикое различие — десятичная точка или запятая. Особенно убийственно парсить числа. Есть страны, где запятая является разделителем тысяч. Поэтому нарисовать универсальный парсер для всех возможных национальных вариантов написания чисел не представляется возможным.
  • LC_TIME — правила форматирования дат и времени. Подразумевается, что существуют некоторые национальные стандарты представления времени в «кратком» и «длинном» форматах, например. Но лично мне кажется, что формат представления дат больше зависит от самого приложения, чем от местности. Единственное серьёзное исключение — странные северо-американцы, которые единственные в мире почти везде месяц пишут до числа: MM/DD/YYYY, — и могут не сообразить, когда написано по-человечески: DD.MM.YYYY.
  • LC_MESSAGES — это как раз текстовые сообщения.
Правила сортировки, форматы дат и чисел, как правило, уже как-то зашиты в систему или библиотеки, главное, всегда указывать правильную локаль. А вот тексты надо писать и переводить. Тем более, что большинство программ, даже в XXI веке, остаются текстовыми. Поэтому, когда заботятся о локализации, в первую очередь занимаются переводами.
Translate
С давних времён существует милая библиотека gettext. Её использование предполагает, что в исходных текстах программы все строки, которые надо переводить, остаются как есть, лишь обрамляются специальной функцией:
printf(_("Hello, World!\n"));
С помощью специальных инструментов исходники сканируются и составляется полный перечень всех строк для перевода. Идентификатором строки, получается, является оригинальный текст, содержащийся в конкретном месте исходников. Эти строки обрабатываются переводчиком, перегоняются в бинарные файлы, которые уже поставляются вместе с программой. Всё, в общем, продумано и автоматизировано.
В Java оно заметно попроще. Тут не принято писать в исходниках пространные тексты. Тут принято сразу указывать в коде идентификаторы сообщений. И преобразовывать их в конкретный локализованный текст где-нибудь поближе к view. Никаких свободных средств поиска всех айдишников в исходниках или заметного упрощения труда переводчиков мне не известно. Соответствие идентификаторов и текстов записывается в обычные properties файлы. По одному файлу для каждой локали.
Конечно же, локаль — это не только язык. Обычно к языку добавляют еще и страну. Ибо, например, английский в США и английский в Соединённом Королевстве таки заметно различаются. А в китайском есть две системы иероглифов: упрощённая и традиционная. Упрощённая уже давненько применяется в континентальном Китае, а традиционная всё еще жива на Тайване.
В Java к языку и стране можно добавить некий «вариант». Это когда нам надо пойти дальше и выразить более тонкие отличия, т.е. сделать именно то, что называется кастомизацией.
Но офигенно круто к вопросу локализации подошли в Android. Там все ресурсы, а это и сообщения, и изображения, и разметка GUI, и даже размеры чего угодно, разнесены по папочкам. И разные папочки выбираются исходя из громадной кучи параметров: и язык, и страна, и версия Android, и разрешение, размеры и ориентация экрана, и день/ночь, и нахождение устройства в автомобильном доке, и даже код оператора сотовой сети. Действительно очень круто. А ещё все идентификаторы ресурсов «компилируются» в целочисленные константы, и в коде даётся ссылка уже на эти константы, это позволяет на этапе сборки приложения контролировать наличие всех нужных идентификаторов и связанных с ними ресурсов.
Android Resource
Локализованные сообщения как правило наследуются. Если не нашли строчку для конкретного варианта, ищем для данной страны. Если там нет, находим общий перевод на данный язык. Если и там не нашли, откатываемся к локали по умолчанию. Так как нынче у нас самым распространённым языком является английский, в дефолтную локаль имеет смысл засовывать английские тексты. Если у вас действительно международное приложение.
Из-за наследования сообщений возникает феномен частично переведённых приложений. Когда часть текстов в приложении, скажем, по-русски, а местами, что не перевели — по-английски. С одной стороны — неприятно. С другой стороны — заставить переводчиков перевести всё правильно и успеть к релизу — отдельный неблагодарный труд. К тому же сообщения локализации имеют дурную привычку меняться и добавляться. И переводы должны меняться и обновляться соответственно.
А еще есть парочка милых семитских языков: иврит и арабский, — в которых пишут справа налево. Это отдельная проблема для локализации. Ибо там не только тексты справа налево, там весь интерфейс должен быть справа налево, включая все кнопочки, менюшки, чекбоксы, и даже полосы прокрутки. Именно для поддержки этой локализованной право-левости нынче модно выравнивать элементы интерфейса не по левому и правому краю, а по start и end. Start и end могут означать право или лево в зависимости от текущей локали. Хорошо ещё, что верх и низ пока не заменяют более «толерантными» терминами. Видимо, ещё не нашлось письменности, где пишут снизу вверх. Хотя те же китайцы пишут, на самом деле, не слева направо, а сверху вниз.
Отдельная песня — это смешение в одном и том же тексте символов, которые пишутся справа налево, например, букв на иврите, и символов, которые пишутся слева направо, например, арабских цифр. Для этого есть целый большой кусок Юникода под названием bi-directional или BiDi. Смысл в том, что символы (байтики) хранятся в том порядке, в каком они читаются, а при отображении они могут располагаться как справа налево, так и слева направо. Плюс всё это зависит от контекста, т.е. блока, где отображается текст, который тоже может быть слева-направный или справа-налевный. Адовый ад. Есть даже невидимые символы нулевой ширины, которые переключают направление текста.
Но отдельную нелюбовь я питаю к арабскому языку. Он мало того, что справа-налевный, так в нём еще одна и та же буква может отображаться по-разному в зависимости от того, стоит она в начале, середине, или конце слова. Ну вы представляете, эти милые хвостики на конце каждого слова в арабской вязи. Если в вебе это худо бедно работает, то лет восемь назад совсем не было способа программно сгенерировать корректный PDF файл на арабском языке.
Arabic
Как видим, чаще всего локализуют-переводят сообщения целиком. Т.е. целые абзацы текста, ну или как минимум, отдельные предложения. Но часто текст не должен отображаться как есть, к нему нужно что-то добавить, какие-то данные из приложения. Типичный пример: сообщение «Скопировано Х файлов».
Первое, что делают новички, создают два сообщения: «Скопировано» и «файлов», — а потом делают конкатенацию первого сообщения, числа скопированных файлов и второго сообщения. Не надо так. Потому что в каждом языке — свой порядок слов, часто весьма сильно отличающийся. Нужно создать одно сообщение в виде строки форматирования: «Скопировано %d файлов», и подставить в эту строку форматирования нужное числовое значение.
Но тут возникает другая проблема: формы множественного числа. Это в английском всё просто: либо «file», либо «files». А в русском, если кто не в курсе, три формы множественного числа: «1 файл», «2 файла», но «5 файлов». А ведь есть языки, где этих форм ещё больше (опять арабский побеждает, их там шесть).
Стандартная библиотека Java не предоставляет никаких средств для решения проблемы. Я как-то даже писал костылик для русского языка. А вот gettext поддерживает формы множественного числа. В исходниках указываются две строки: для единственного и множественного числа, а вот уже в переводах можно указать все формы данного языка.
Кстати, у строк форматирования есть свои недостатки. Строка форматирования, пришедшая извне (из-за халатности переводчика?) может использоваться как уязвимость в стиле SQL инъекций. Даже название для этого придумали: printf oriented programming. И даже в Java (и в Android) программа может рухнуть при выводе какой-то одной единственной строки только потому, что выполнение наконец дошло до вывода этой строки, а на этом языке переводчик накосячил с символами форматирования, и они не соответствуют передаваемым параметрам.
Но формы множественного числа — это цветочки. В реальности встречаются и другие случаи согласования данных программы и локализованных сообщений. Ну вспомните хотя бы, сколько падежей в русском языке, и представьте, например, как локализовать цвет, хранящийся где-нибудь в БД, чтобы он в виде текста вставлялся во всех возможных формах в локализованные сообщения. Очевидно, что для каждого значения цвета и для каждой локали нужно где-то задать все возможные грамматические формы слова, обозначающего цвет, и выбирать нужную форму исходя из грамматической формы места в строке форматирования.
Возникает соблазн пойти дальше, и вообще дать грамматическое описание всех понятий, встречающихся в приложении. И конструировать предложения из этих понятий. Мне известна только одна библиотека локализации, которая попыталась пойти так далеко. И она носит говорящее название humanization.
Этот подход подкупает душу программиста потрясающей гибкостью. Но это ж надо каждое сообщение описывать почти что на Ыфкуыле. Надо описывать именно понятия, а не слова, ибо существуют синонимы и омонимы, и угадать, как оно будет в других языках, невозможно. Надо разбираться в грамматике всех языков, для которых осуществляется локализация. Видимо, всё же проще переводить фразы и предложения целиком, при этом меньше требования к филологической грамотности разработчиков и переводчиков.
Web Sites Humanization
Думайте о локализации сразу. Потом добавить её будет сложновато. Не хардкодьте.