Давно хотелось раскрыть интересную тему локализации ПО, но так чтобы не повторяться и не цитировать прописные истины.

Поэтому рассказываю как локализовать обычное корпоративное Java-приложение на..  несуществующие фантастические языки: Клингонский и Р’льех.

На этом скриншоте куда больше реального приложения чем кажется на первый взгляд.
На этом скриншоте куда больше реального приложения чем кажется на первый взгляд.

Думаю начать стоит с демонстрации конечного результата — той самой нереальной локализации, ради которой все это и затевалось.

Версия на клингонском:

Обратите внимание на даты - это настоящий Stardate.
Обратите внимание на даты — это настоящий Stardate.

Версия на Р'льех:

"Cthulhu fhtagn!" на JSF, CDI и JPA. Сложно сказать какая часть предложения напугает сильнее.
«Cthulhu fhtagn!» на JSF, CDI и JPA. Сложно сказать какая часть предложения напугает сильнее.

Ну и наконец самый банальный английский:

Вот так выглядит в работе переключение локализации:

Да, это самое обычное веб-приложение на Java, работающее в обычном браузере.

Но только с локализацией на клингонский и Р'льех.

Матчасть

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

Но стоит иметь ввиду, что вся эта область под названием «локализация и интернационализация ПО» — сложна и обширна, рассказать обо всех ее нюансах ни в рамках одной статьи ни даже книги у меня просто нехватит сил.

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

С интересом послушаю о вашем опыте и боевых практиках.

Чтобы вы смогли оценить сложность задачи «локализации на язык которого нет», стоит для начала рассказать как происходит обычная локализация — на обычные человеческие языки.

Возьмем для примера классику в виде русско‑английской локализации, вот что необходимо реализовать в этом случае:

  • Определение текущей локали

  • Переключение локали

  • Хранение локализованных строк

  • Отображение локализованных данных

Данный функционал подразумевается как минимальный, когда речь заходит о локализации ПО, причем большая часть всей этой логики уже реализована в любом современном инструментарии и все что нужно сделать для поддерживаемых языков — «включить и использовать».

Для Java уж точно.

Вот так например выглядит хранение локализованных строк:

Это абсолютно стандартный способ, поддерживаемый как самим JDK так и всем прикладным ПО на Java
Это абсолютно стандартный способ, поддерживаемый как самим JDK так и всем прикладным ПО на Java

Также легко и просто оперировать обычным поддерживаемым языком со стороны прикладного кода, например вот так выглядит получение локали из кодового названия:

Locale locale = Locale.forLanguageTag("en_US");

где en — это указание на английский а US — на страну США.

Не менее просто происходит и переключение между поддерживаемыми языками (в данном случае в Jakarta Faces):

FacesContext.getCurrentInstance().getViewRoot().setLocale(locale);

Но вся эта «благодать» быстро заканчивается, стоит только выйти за границу реальности поддерживаемых локалей и попытаться использовать «то чего нет».

Язык которого нет

Символы несуществующих фантастических языков предсказуемо отсутствуют в официальной таблице символов Unicode, их нет в списке поддерживаемых средствами разработки (в первую очередь в JDK) и нет в браузере.

Что означает невозможность какой-либо работы «из коробки» с таким языком — без специальных шагов.

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

Клингонский

Фантастический, полностью выдуманный сценаристами сериала Star Trek язык расы инопланетян еще в 70х оказался невероятно популярным.

Популярным настолько, что ныне существует целый институт, посвященный изучению вымышленного языка:

Институт клингонского языка (англ. the Klingon Language Institute, KLI) — независимая организация в Пенсильвании, США. Её цели — поддержка и развитие клингонского языка и клингонской культуры, представленных в вымышленной вселенной киносериала «Звёздный путь». Она поддерживается Paramount Pictures.

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

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

В ядро Linux:

In September 1997, Michael Everson made a proposal for encoding KLI pIqaD in Unicode, based on the Linux kernel source code. The Unicode Technical Committee rejected the Klingon proposal in May 2001

В официальный набор символов Unicode (линк):

September 1997: first Unicode proposal for pIqaD.1
May 2001: Rick McGowan submits Proposal to Reject Klingon
May 2001: Proposal to reject Klingon adopted by UTC (minutes)
November 2016: New Proposal for Encoding Klingon, showing lots of examples of usage
July 2020: Another New Proposal for Encoding Klingon. This one uses the correct “Klingon” names for the letters.
August 2021: Request to Remove Klingon from Non-Approval List, made in accordance with Ken Whistler’s suggestion from 2016, linked above.

Как видите фанаты Star Trek крайне упертые товарищи, которые уже второй десяток лет продолжают упорно осаждать двери офиса по адресу:

611 Gateway Blvd.
Suite 120

в Сан‑Франциско CA 94 080, где и располагается «The Unicode Consortium». Кстати вы также можете позвонить в консорциум Unicode на их офисный номер:

+1-408-401-8915

и поинтересоваться почему клингонский до сих пор не включен в официальный набор символов — дело же важное.

Удивительно (или нет), но в Microsoft тоже любят клингонский, настолько что добавили его поддержку в свой онлайн-переводчик:

Именно его я использовал для клингонского перевода проекта.
Именно его я использовал для клингонского перевода проекта.

При таком интересе технически продвинутой общественности, очень быстро появились готовые TTF-шрифты, использующие PUA область:

Since then several fonts using that encoding have appeared, and software for typing in pIqaD has become available

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

Вот так выглядит клингонский алфавит:

Обратите внимание на соответствие одного глифа клингонского сразу нескольким на английском — это влияет на реализацию транслятора (см. ниже).
Обратите внимание на соответствие одного глифа клингонского сразу нескольким на английском — это влияет на реализацию транслятора (см. ниже).

Р'льех

С языком древних Р’льех все обстоит куда проще — этот также полностью выдуманный язык, приверженцы которого живут под водой и к счастью мало интересуются продвижением своего фантастического языка в широкие массы. 

С названием есть небольшая неточность:

 Cthuvian, which is also called R'lyehian, is a fictional language created by H. P. Lovecraft in "The Call of Cthulhu" and expanded upon by various authors.

Дословный перевод — «ктулхский» или «р'льехский», что (да простят меня подводные боги) показалось не очень благозвучным.

Поэтому я использовал термин Р'льех, который на самом деле означает иное:

Р’льех или Р'лайх (англ. R’lyeh) — вымышленный город, впервые упомянутый Говардом Филлипсом Лавкрафтом в рассказе «Зов Ктулху» (1928)[1]. С тех пор Р’льех стал неотъемлемой частью мифологии Лавкрафта и Мифов Ктулху. Р’льех описан в «Некрономиконе» Лавкрафта и «Cthaat Aquadingen» Брайана Ламли.

Алфавит выглядит как-то так:

Н'ЯРЛАФОТЕП - отличное название для нового проекта, не находите?
Н'ЯРЛАФОТЕП — отличное название для нового проекта, не находите?

Доступные TTF-шрифты для Р'льех не используют PUA-область Unicode, поэтому применение такого шрифта превратит все символы в месиво:

Обратите внимание на поле ввода - текст в нем визуально на Р'льех, хотя введены символы английского.
Обратите внимание на поле ввода - текст в нем визуально на Р'льех, хотя введены символы английского.

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

В этом и заключается главная сила PUA-области и ее главная фишка.

Будете создавать локализацию на древнеегипетский или руническое письмо викингов — обязательно используйте шрифт с PUA-областью.

Так выглядит проект из среды разработки.
Так выглядит проект из среды разработки.

Тестовый проект

Для статьи был специально выбран самый «тру‑энтерпрайз» стек, чтобы показать насколько далеко продвинулись технологии локализации.

Это не какие-то околонаучные экспериментальные языки или малоизвестные специализированные фреймворки и не дикий «low level» с песьеголовыми программистами на С, это самый настоящий технологический мейнстрим — тот вид разработки и набор технологий, с которыми вы (если конечно занимаетесь разработкой) сталкиваетесь каждый день:

представьте любимый клиент-банк с локализацией на клингонском.

Разумеется будет много специфики именно для Java и выбранных технологий, но описанные идеи и подходы очень даже применимы и для большинства других языков и решений.

Вот что в меню:

JakartaEE 10, который в девичестве назывался JavaEE а в далеком детстве J2EE.

В качестве сервера приложений был взят IBM OpenLiberty — современный открытый потомок большой IBM Websphere Application Server, который IBM ныне продвигает в светлое корпоративное будущее как платформу для разработки микросервисов.

Технически тестовый проект представляет собой веб‑приложение (WAR), которое разворачивается на сервере приложений и по полной использует его ресурсы — все как в золотые годы JavaEE.

Но чтобы не загонять читателей в классические мытарства с установкой и развертыванием — был добавлен автозапуск приложения с автоматическим развертыванием (как в Spring Boot).

Внутри классика корпоративной разработки:

JPA, CDI, JSF и новое Servlet API 6 — уже полностью на аннотациях.

Все прямо как на настоящей работе в банке, где деньги платят.

И сейчас мы будем локализовывать все это на выдуманный язык из фантастического сериала 1970х.

Но прежде опишу стандатное — сборку и запуск.

Сборка

Для сборки используется обычный Apache Maven и последняя версия JDK (22+), забираем проект из репозитория:

git clone https://github.com/alex0x08/javaee-klingon.git

и запускаем сборку:

mvn clean package

Готовое приложение будет находиться в каталоге target:

В каталоге liberty находится распакованный сервер приложений Open Liberty, с установленным внутрь нашим приложением — за все эти радости отвечает специальный плагин (см. ниже).

Запуск

Как уже упоминалось выше, наш замечательный проект предназначен для запуска и работы на сервере приложений IBM Open Liberty.

Разумеется вы можете сходить по ссылке выше, прокрутить страницу вниз до раздела Releases, скачать версию 24.0.0.6+ с профилем Jakarta EE 10, развернуть и затем установить туда наше приложение.

Для настоящего развертывания в корпоративной среде обычно и делают. По крайней мере делали до эры докера.

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

Одной командой:

mvn liberty:dev

Произойдет скачивание IBM Open Liberty, распаковка, настройка, установка внутрь нашего приложения и немедленный запуск.

Вот так это выглядит из среды разработки Intellj Idea:

После запуска открываем страницу:

http://localhost:9080/kligonweb-1.0.1-RELEASE/guestbook.xhtml

и наслаждаемся.

Вот эти ребята. USS Voyager c командой.
Вот эти ребята. USS Voyager c командой.

Отображение

Начну с самого главного вопроса — с отображения символов несуществующего фантастического языка.

Взгляните:

Нет это не галлюцинации или фотошоп, это установленный правильный TTF-шрифт клингонского в системе.
Нет это не галлюцинации или фотошоп, это установленный правильный TTF-шрифт клингонского в системе.

На скриншоте стандартный gedit, в настройках которого был задан клингонский шрифт для отображения основной части. Как видите использование PUA‑области Unicode в шрифте позволяет неплохо дружить символы обычного и фантастического языков.

Если приглядитесь — увидите сглаживание, работающее даже для глифов клингонского.

К сожалению для Р'льех не нашлось шрифта, использующего PUA‑область Unicode, поэтому при отображении происходит замена всех символов глифами Р'льех:

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

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

Для клингонского:

Klingon pIqaD Mandel takes the Klinzhai or Mandel font glyphs (really a different alphabet from the KLI’s Standard pIqaD) and refits them for use as pIqaD.

и еще один для Р'льех:

I created this font based on the description by H.P. Lovecraft. Click here to download the Rlyehian font package, which includes two version of the font and a guide to understanding its use.

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

FontForge

Уже достаточно давно и успешно существует отличный открытый редактор шрифтов:

FontForge is a FOSS font editor which supports many common font formats. Developed primarily by George Williams until 2012, FontForge is free software and is distributed under a mix of the GNU General Public License Version 3 and the 3-clause BSD license.[2] It is available for operating systems including Linux, Windows,[3] and macOS,[4] and is localized into 12 languages

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

Вот так выглядит клингонский шрифт, открытый в этом редакторе:

Обратите внимание на фразу "Private Use Area" - она означает что глифы клингонского расположены именно в PUA-области.
Обратите внимание на фразу «Private Use Area» — она означает что глифы клингонского расположены именно в PUA‑области.

Вот так выглядит процесс редактирования отдельного символа:

Имейте ввиду что это долгий и утомительный процесс, особенно если речь про разработку шрифта с нуля.
Имейте ввиду что это долгий и утомительный процесс, особенно если речь про разработку шрифта с нуля.

А вот так для сравнения выглядит шрифт для Р'льех:

Как видите тут не используется PUA и заменяются символы ASCII, с самого начала таблицы.
Как видите тут не используется PUA и заменяются символы ASCII, с самого начала таблицы.

Для полного погружения, вот так выглядит редактирование одного из этих стильных глифов:

И ведь кто-то сидел и рисовал это. Воистину воля п̴͎͚̱̤̂о̸̮̣͇̣̉̽̂͛͝д̶̼̃͛в̵̣̒́о̵̣͗͝д̶̛͓̄н̵͔̦̳͍̜͒ы̴̡͉͙̤̅̓̕ͅх̸̹̲̱̖̊̀ ̸̲̪̣̦̩̀б̷̤͓̯̃͊̇͋͝о̴̹͂͝г̴͍̋͑̂̚о̷̼̩͕́̈́в̵̝̘̿̋̓̕ безгранична.
И ведь кто-то сидел и рисовал это. Воистину воля п̴͎͚̱̤̂о̸̮̣͇̣̉̽̂͛͝д̶̼̃͛в̵̣̒́о̵̣͗͝д̶̛͓̄н̵͔̦̳͍̜͒ы̴̡͉͙̤̅̓̕ͅх̸̹̲̱̖̊̀ ̸̲̪̣̦̩̀б̷̤͓̯̃͊̇͋͝о̴̹͂͝г̴͍̋͑̂̚о̷̼̩͕́̈́в̵̝̘̿̋̓̕ безгранична.

Разумеется, можно было потратить какое‑то время и перенести глифы Р'льех в PAU‑область, что позволило бы использование шрифта по аналогии с клингонским — параллельно с другими языками.

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

На самом деле есть еще одна важная причина — показать вам два подхода к локализации, а не один:

второй вариант реализации шрифта с полной заменой всех символов на безумные иероглифы чем-то фантастическим (без использования PAU-области) встречается куда чаще.

Его точно стоит учитывать, поскольку скорее всего именно с таким шрифтом вы и столкнетесь, пытаясь работать с фантастическими языками.

Отображение в браузере

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

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

Регистрация TTF‑шрифта происходит путем использования CSS‑стиля и специальной директивы font‑face:

 @font-face {
               font-family: 'Klingon';
               src: url("#{resource['Klingon-pIqaD-Mandel.ttf']}");
            }
@font-face {
              font-family: 'Rlyeh';
              src: url("#{resource['Rlyehian.ttf']}");
}          

Сложно выглядящая директива #resource[''] на самом деле уже часть парсера страниц JSF — EL-выражение, преобразующее относительный путь к указанному ресурсу в полный.

А вот так выглядит задание отдельных стилей для использования наших фантастических шрифтов:

.klingon {
   font-family: 'Klingon';
}          

Эти стили применяются выборочно, для включения фантастического шрифта при включенной перекодировке у сообщения:

<p class="card-text">
<h:outputText value="#{record.message}"
styleClass="#{record.translateKlingon ? 'klingon' : ''}"/>
</p>

Если сообщение было написано на клингонском pIqaD — оно будет пропущено через транслятор (см. ниже) и при отображении будет использован клингонский TTF‑шрифт.

Таким образом сохраняется обратная совместимость с другими языками и остается возможность ввода на обычном английском.

Но это решение только для отдельных блоков сообщений, ведь есть еще глобальное переключение выбранной локали:

Для решения этой задачи, используется вот такая логика:

<h:outputStylesheet  
    name="style-klingon.css"  
    rendered="#{i18n.locale.variant eq 'KLINGON'}"/>
<h:outputStylesheet  
    name="style-rlyeh.css"  
    rendered="#{i18n.locale.variant eq 'RLYEH'}"/>

Суть ее в том что в зависимости от «variant» выбранной локали (см. ниже) подгружается тот или иной глобальный стиль:

* {
    font-family: 'Klingon', sans-serif;
}
body {
    background-image: url('klingon.jpg.xhtml');
}

Звездочка (*) означает что указанный шрифт должен быть применен ко всем элементам на странице, что и дает вот такой эффект глобальной локализации всего:

Также тут задается фоновая картинка в немного странном формате:

klingon.jpg.xhtml

На самом деле файл называется klingon.jpg и находится в каталоге webapp/resources, а постфикс .xhtml — особенность работы ресурсов в JSF, он нужен для правильной работы, хотя и выглядит полной дичью.

Переходим к следующей важной теме.

Транслятор

При локализации на несуществующий и неподдерживаемый язык существует еще одна проблема:

необходимо как-то работать с локализованным на такой язык текстом из стандарного окружения.

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

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

Если очень повезет, то пойдя этим путем можно получить что-то такое:

Круто, но слишком сложно и не подходит для массовой разработки - когда задействовано много разработчиков.
Круто, но слишком сложно и не подходит для массовой разработки — когда задействовано много разработчиков.

Есть способ лучше

Дело в том что ни один, даже трижды фантастический язык не существует в вакууме — для него в обязательном порядке создается:

Транслитера́ция (лат. trans- «через; пере-» + littera — «буква») — точная передача знаков одной письменности знаками другой письменности[1][2], при которой каждый знак (или последовательность знаков) одной системы письма передаётся соответствующим знаком (или последовательностью знаков) другой системы письма.

Даже если речь про например дотракийский — выдуманный сценаристами язык кхала Дрого из «Игры Престолов», к нему все равно в качестве приложения идет транслитерация на английском — актерам надо как-то учить произношение.

Более того, такая транслитерация существует и для самих человеческих языков, причем видимо для всех (исключений пока не встречал).

Например есть широко известный вариант написания кириллицы с помощью символов латиницы:

kotorii nazyvayetsa 'translit'

Нет людей в рунете старше 30ти, которые бы его никогда не видели.

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

Именно транслитерацию в латинские символы мы и будем использовать.

Да это "Гамлет" на клингонском - а что вы знаете о фанатизме?
Да это «Гамлет» на клингонском — а что вы знаете о фанатизме?

pIqaD

Вариант написания клингонского латинскими символами называется pIqaD, конечно же он куда более широко распространен и популярен чем те сложные клингонские иероглифы, которые я с таким трудом отображал выше.

Настолько популярен, что есть даже «Гамлет» в переводе на клингонский, где используется pIqaD-транслитерация:

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

Очень быстро были найдены и готовые трансляторы, самый популярный (из открытых) выглядит вот так:

На основе его исходного кода (на Javascript) была написана моя реализация на Java, с помощью которой вот такие строковые ресурсы:

превращаются во время работы приложения в те самые фантастические иероглифы:

Полный код реализации можно посмотреть вот тут.

Как видите тут происходит достаточно простая замена символов согласно таблице подстановки, с латинских на Unicode из PAU‑области — все внешне сложное, на самом деле устроено очень просто.

Один из немногих оригиналов документов на Р'льех.
Один из немногих оригиналов документов на Р'льех.

К сожалению (или к счастью — в зависимости от контекста), фантастический язык Р’льех из миров Лавкрафта куда менее популярен, поэтому получилось найти всего один рабочий транслятор:

Using the digital serpent's package, you can translate english to the language of the "old ones" Spread a̶͙̓̓̓͛̿̓͘ḯ̵̡̲̟̼͎̩͉̬̙̈̀͆͜m̴̨̺̖͇͔̝̤̖͊̏̌̅̔̿͜͜͝ģ̶̺͚̬̣̣̜͉̃̒͜ŗ̷͖͇͖̘͍̹̳̈̑͐͌̇̆͘͜͝ͅ'̴̢͉͎͇͔̬̖̽̈̕͜ļ̷̛̥̹̰͎̤͉̫̱̗͗̈́͗͆̾͒̄̅͠ű̸̖̼͇̏̈́̉̊̌̃̕ḩ̷̧̲̬͔̉ͅ

Занимается им некий китайский DevOps-инженер (надеюсь не в рамках должностных обязанностей), сам транслятор и написан на Python:

python test.py -t "I pray to the mother of skin"

Важным моментом является другой принцип работы — вместо транслитерации символов происходит подстановка слов или даже целых фраз:

Вся логика была портирована в мой проект, мою реализацию транслятора для Р'льех можно посмотреть вот тут.

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

у меня честно нет идей как могут выглядеть слова «Авторизация», «Назад» или «Сохранить» на языке древних.

Поэтому транслятор Р’льех используется только для ввода текста — чтобы найти истинных последователей показать как это работает.

Но перейдем к следующей интересной теме.

Нереальная локаль

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

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

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

Но для Java весь процесс более-менее отработан, описан и предсказуем:

в Java у локалей есть поддержка т. н. «variant» — специальной вариации языка, которая может быть сколь угодно нестандартной.

Сама локаль остается системной (в данном случае — английской), но при этом к ней добавляется специальный постфикс, означающий что используется «вариация»:

<h:form>
    <h:selectOneMenu styleClass="form-select" style="width: 12em;"
               value="#{i18n.language}" onchange="submit()">
        <f:selectItem itemValue="en" itemLabel="English" />
        <f:selectItem itemValue="en-US-KLINGON" itemLabel="Klingon" />
        <f:selectItem itemValue="en-US-RLYEH" itemLabel="Rlyeh" />
    </h:selectOneMenu>
</h:form>

Поскольку такие variants являются частью официального API, они поддерживаются всем прикладным ПО и библиотеками (за редкими исключениями).

В том числе они используются в механизме работы ResourceBundle:

Если включить «variant» в название файла с ресурсами — он будет найден и загружен при выборе локали с таким «variant».

К сожалению стандартной реализации ResourceBundle оказалось недостаточно — хотелось получить перекодированный клингонский сразу из ресурсов, поэтому я сделал свою:

package com.Ox08.experiments.kligon;
import jakarta.annotation.Nonnull;
import jakarta.faces.context.FacesContext;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
 * Extended resource bundle, used to inject Klingon glyphs if Klingon locale
 * used
 *
 * @author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a>
 */
public class KlingonedResourceBundle extends ResourceBundle {
    public KlingonedResourceBundle() {
        setParent(ResourceBundle.getBundle("i18n.messages",
                FacesContext.getCurrentInstance().getViewRoot().getLocale()));
    }
    @Override
    public final void setParent(ResourceBundle parent) {
        super.setParent(parent);
    }
    @Override
    protected Object handleGetObject(@Nonnull String key) {
        // here will be extracted and substituted value
        final Object v = parent.getObject(key);
        if (!(v instanceof String vstring)) {
            return v;
        }
        LOG.log(Level.INFO, "handleGetObject : {0}", vstring);
        // current locale
        final Locale l = FacesContext.getCurrentInstance().getViewRoot().getLocale();
        // check if its Klingon  and transliterate to glyphs 
        if ("KLINGON".equals(l.getVariant()))
            return KlingonTranslator.transliterate(vstring);
        // .. and for Rlyeh
        if ("RLYEH".equals(l.getVariant()))
            return RlyehTranslator.translate(vstring);

        // otherwise - just respond 'as-is'
        return v;
    }
    @Override
    @Nonnull
    public Enumeration<String> getKeys() {
        return parent.getKeys();
    }
    private static final Logger LOG = Logger.getLogger("BUNDLE-KLINGON");
}

Основное действие происходит в методе handleGetObject() ,сейчас разберу логику этого метода по шагам, благо она будет повторяться и в других местах.

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

final Object v = parent.getObject(key);       

Затем происходит отбраковка по возвращаемому типу — мы работаем только со строками и все остальные варианты пропускаем:

 if (!(v instanceof String vstring)) {
            return v;
  }

Дальше происходит получение текущей локали пользователя:

final Locale l = FacesContext.getCurrentInstance()
                                .getViewRoot().getLocale();   

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

Затем в зависимости от значения «variant» вызывается перекодировщик для клингонского:

if ("KLINGON".equals(l.getVariant()))
            return KlingonTranslator.transliterate(vstring);

или для Р'льех:

 if ("RLYEH".equals(l.getVariant()))
            return RlyehTranslator.translate(vstring);

Регистрация кастомной реализации ResourceBundle задается в файле с настройками Jakarta Faces (webapp/WEB-INF/faces-config.xml):

..
<resource-bundle>
 <!--
      Note that 'base name' points to specific class, 
      not to .properties file
 -->
 <base-name>com.Ox08.experiments.kligon.KlingonedResourceBundle</base-name>
 <var>msgs</var>
</resource-bundle>
..

Там же указывается список поддерживаемых локалей, с учетом «variants»:

..
<locale-config>
         <default-locale>en</default-locale>
         <supported-locale>en_US_KLINGON</supported-locale>
          <supported-locale>en_US_RLYEH</supported-locale>
</locale-config>
..

Наконец хранение выбранной пользователем локали происходит в отдельном сессионном бине:

package com.Ox08.experiments.kligon;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.SessionScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.Locale;
import java.util.logging.Logger;
/**
 * This bean stores selected locale, attached to user's session
 *
 * @author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a>
 */
@Named("i18n")
@SessionScoped
public class LocaleBean implements Serializable {
    @Inject
    private transient Logger log;
    // current locale
    private Locale locale;
    /**
     * Initializes current locale value
     */
    @PostConstruct
    void init() {
        // take current locale from request
        locale = FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
        log.log(java.util.logging.Level.INFO,
                "Current locale {0} , variant: {1}",
                new Object[]{locale.toLanguageTag(), locale.getVariant()});
    }
    public Locale getLocale() {
        return locale;
    }
    public String getLanguage() {
        return locale == null ? null : locale.toLanguageTag();
    }
    public void setLanguage(String language) {
        // get Locale object from language tag
        locale = Locale.forLanguageTag(language);
        // set it to current view root
        FacesContext.getCurrentInstance().getViewRoot().setLocale(locale);
        log.log(java.util.logging.Level.INFO,
                "Switched locale to  {0} , variant: {1}",
                new Object[]{locale.toLanguageTag(), locale.getVariant()});
    }
}

Поле «locale» из данного бина используется со стороны XHTML-страницы:

 ..
 <f:view xmlns="http://www.w3.org/1999/xhtml"
            xmlns:h="http://xmlns.jcp.org/jsf/html"
            ..
            locale="#{i18n.locale}">
 ..

Но это еще не все интесное и необычное, что хотелось бы раскрыть в рамках статьи.

Валидация данных

Как каша без масла протеина или водка без закуски — не бывает корпоративных приложений без валидации данных.

В Jakarta EE (как и в ее предшественнике JavaEE) для автоматической валидации входных данных используются механизмы из спецификации JSR 303 «Bean Validation».

В самом простом случае это выглядит как аннотирование полей класса:

..
    @Size(min = 3, max = 255)
    private String title; // a title
    @NotBlank(message = "{validation.message.not-blank}")
    @Lob
    @Column(length = Integer.MAX_VALUE)
    private String message; // message, stored as CLOB in database, 
    //so size is almost unlimited
    @Size(min = 3, max = 30)
    @Email
    private String author; // author's email
..  

Когда такой класс попадает в качестве входящего аргумента метода класса, управляемого CDI‑окружением, срабатывает автоматическая валидация и в интерфейсе появляются сообщения об ошибках:

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

<h:message for="f_message" errorClass="msg" />

если нет — она отображается через «глобальную свалку»:

<p>    
  <h:messages globalOnly="true" infoClass="msg" errorClass="msg" />
</p>

Теперь обратите внимание вот на эту строчку:

@NotBlank(message = "{validation.message.not-blank}")

Вместо текста сообщения, тут указан некий код, который автоматически заменяется на текст из специального ResourceBundle:

Согласно спецификации JSR303 название для бандла должно быть именно ValidationMessagesВся эта логика является частью спецификации JSR303 и вообщем-то отлично работает без вашего участия — до тех пор пока не появляется необходимость сотворить какую-нибудь дичь.
Согласно спецификации JSR303 название для бандла должно быть именно ValidationMessages.

Вся эта логика является частью спецификации JSR303 и вообщем-то отлично работает без вашего участия — до тех пор пока не появляется необходимость сотворить какую-нибудь дичь.

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

Текст красненьким - та самая валидация JSR303. На клингонском.
Текст красненьким — та самая валидация JSR303. На клингонском.

Поэтому придется немного подумать.

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

Message interpolators are used by the validation engine to create user readable error messages from constraint message descriptors.

В итоге была написана собственная реализация такого «интерполятора»:

package com.Ox08.experiments.kligon;
import jakarta.validation.MessageInterpolator;
import jakarta.validation.Validation;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
 * Custom JSR 303 Message Interpolator, used to inject Klingon glyphs 
 *  into JSR303 validation
 *
 * @author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a>
 */
public class JSR303KlingonMessageInterpolator 
                       implements MessageInterpolator {
    // we need to have existing MessageInterpolator, 
    // to being used as parent
    private final MessageInterpolator delegate;
    public JSR303KlingonMessageInterpolator() {
        // take default implementation from JSR303  configuration
        this.delegate = Validation.byDefaultProvider()
                .configure().getDefaultMessageInterpolator();
    }
    @Override
    public String interpolate(String string, Context cntxt) {
        LOG.log(Level.INFO, "interpolating {0}", string);
        // without specified locale - just pass interpolation to delegate
        return delegate.interpolate(string, cntxt);
    }
    @Override
    public String interpolate(String string, 
                              Context cntxt, Locale locale) {
        LOG.log(Level.INFO,
                "interpolating {0} with locale: {1}",
                new Object[]{string, locale.toLanguageTag()});
        // here will be extracted and substituted value
        final String result = delegate.interpolate(string, cntxt, locale);
        // check for Klingon locale and transliterate to glyphs 
        if ("KLINGON".equals(locale.getVariant())) 
            return KlingonTranslator.transliterate(result);
        
        if ("RLYEH".equals(locale.getVariant())) 
            return RlyehTranslator.translate(result);
        
        return result;
    }
    private static final Logger LOG = Logger.getLogger("JSR303-KLINGON");
}

Основная магия логика заключается вот в этих строках:

 ..
final String result = delegate.interpolate(string, cntxt, locale);
// check for Klingon locale and transliterate to glyphs 
if ("KLINGON".equals(locale.getVariant())) 
        return KlingonTranslator.transliterate(result);
     
if ("RLYEH".equals(locale.getVariant())) 
        return RlyehTranslator.translate(result);
     
return result;

Как видите, локаль поступает на вход метода в готовом виде — ее не надо определять из контекста JSF, а вот в этом месте происходит получение оригинальной строки из файла с текстовыми строками:

final String result = delegate.interpolate(string, cntxt, locale);

Дальше в зависимости от наличия «variant» у локали, текст либо пропускается через транслятор либо отдается «как есть».

Регистрация кастомного интерполятора также имеет свою специфику — она происходит в отдельном XML-файле:

<?xml version="1.0" encoding="UTF-8"?>
<validation-config
    xmlns="https://jakarta.ee/xml/ns/validation/configuration"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration
            https://jakarta.ee/xml/ns/validation/configuration/validation-configuration-3.0.xsd"
    version="3.0">
    <!-- register custom interpolator, used to retrieve i18n validation messages -->
   <message-interpolator>com.Ox08.experiments.kligon.JSR303KlingonMessageInterpolator</message-interpolator>  
</validation-config>

который находится в:

src/main/resources/META-INF/validation.xml

Последней интересной темой, достойной освещения в рамках статьи про локализацию будут фантастические даты.

Заметьте — не просто фантастический формат отображения а целый календарь.

Фантастические даты

Никогда не задумывались какой смысл закладывается в дату?

Что такое на самом деле 2024й год?

Фактически это означает что прошло 2024 года с рождения Иисуса Христа (по новому летоисчислению), что возможно не очевидно некоторым представителям молодого поколения, но вполне достаточно для жизни и работы цивилизации.

А что если вам надо использовать альтернативную систему расчета времени?

Миллион лет от последнего динозавра?

40 000 лет бесконечной войны?

Озадачившись данным вопросом, я решил что неплохо было бы реализовать для фантастического языка еще и фантастическое летоисчисление. И использовать его для обычной корпоративной разработки.

Вселенная сериала Star Trek оказалась настолько продуманной и проработанной что имела собственную систему летоисчисления:

A stardate is a fictional system of time measurement developed for the television and film series Star Trek. In the series, use of this date system is commonly heard at the beginning of a voice-over log entry, such as "Captain's log, stardate 41153.7.

Именно ее поддержку я и решил реализовать:

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

Финальную реализацию можно посмотреть вот тут.

Но одной только реализации кастомного календаря оказалось мало — нужен еще один класс-конвертер, реализующий непосредственно конвертацию дат с этим календарем:

package com.Ox08.experiments.kligon;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;
/**
 * A custom converter for StarDate 
 * @author alex0x08
 */
@FacesConverter(value = "stardateConverter")
public class StarDateConverter implements Converter<Date> {
    @Override
    public Date getAsObject(FacesContext fc, 
                            UIComponent uic, String string) {
        final Locale l = fc.getViewRoot().getLocale();
        if ("KLINGON".equals(l.getVariant()))
            return StarDate.parseStarDate(string).getDate();

        return Date.from(ZonedDateTime.parse(string, 
                DateTimeFormatter.ISO_DATE_TIME
                       .withZone(ZoneId.systemDefault())).toInstant());
   }
   @Override
   public String getAsString(FacesContext fc, UIComponent uic, Date t) {
        final Locale l = fc.getViewRoot().getLocale();
        if ("KLINGON".equals(l.getVariant()))
            return StarDate.newInstance(t).toString();
        return DateTimeFormatter.ISO_DATE_TIME
                    .withZone(ZoneId.systemDefault())
                    .format(t.toInstant());
    }
}

Активируется этот конвертер автоматически благодаря наличию аннотации:

@FacesConverter(value = "stardateConverter")

и автоматически же применяется для всех полей с типом Date, проходящих через бины, управляемые CDI.

Внутри уже традиционная логика получения текущей локали:

final Locale l = fc.getViewRoot().getLocale();

Затем при наличии клингонского «variant» происходит либо преобразование из объекта в строку с учетом кастомного календаря:

 if ("KLINGON".equals(l.getVariant()))
            return StarDate.parseStarDate(string).getDate();

либо из строки в объект (также с учетом StarDate):

if ("KLINGON".equals(l.getVariant()))
            return StarDate.newInstance(t).toString();

На этом красивая история о нереальном подходит к концу, подведем итоги.

Итоги и выводы

В современных реалиях и с использованием современных инструментов нет серьезных препятствий для локализации на любые неведомые языки — искусственные или настоящие.

Отсутствие официальной поддержки «из коробки» в инструментах разработки и даже отсутствие символов в таблице символов Unicode — не является проблемой для настоящего джедая.

Первым шагом необходимо разработать или найти готовый TTF‑шрифт для вашего языка и проверить его отображение в системе и браузере — если планируется веб‑разработка.

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

Вся дальнейшая работа сведется к включению транслятора в ключевых местах проекта.

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

P.S. Это сильно облагороженная и цензурированная версия статьи, расширенный оригинал которой доступен в нашем блоге.

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

Пишите если найдете ошибки или неточности.

0x08 Software

Мы небольшая команда ветеранов ИТ‑индустрии, создаем и дорабатываем самое разнообразное программное обеспечение, наш софт автоматизирует бизнес‑процессы на трех континентах, в самых разных отраслях и условиях.

Оживляем давно умершеечиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.

Комментарии (21)


  1. Keeper10
    24.06.2024 08:20
    +6

    Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn!


  1. OldFisher
    24.06.2024 08:20

    Но в чём может быть смысл перевода сайтов на Р'Льехский? Много ли чего вы сможете выразить, не открыв окна в иные демонические измерения у себя в офисе или прямо в голове, не призвав Великих Древних или хотя бы не отрастив жабры и щупальца в самых неудобных местах?


    1. alex0x08 Автор
      24.06.2024 08:20
      +5

      Напомню что только в РФ живет 130 разных народов, у каждого из которых есть свой язык.

      А еще есть Индия и целый африканский континент, где на каждую деревню по своему языку.

      Замените клингонский на например 'удмуртский' и все поймете — принципы локализации одинаковы.


      1. IvanSTV
        24.06.2024 08:20
        +1

        Если смотреть с глобальной точки зрения менее затратно обучить 100500 индусов, у которых в каждой деревне свой язык, английскому на уровне понимания интерфейса со словарем, чем пилить 100500 локализаций для каждой деревни, учитывая, что придется их пилить в 100500 приложений. Развитие обычно идет по самому энергоэкономному пути. Индусам и африканцам все равно придется переходить на один какой-то язык, это объективный исторический процесс, который в эпоху индустриализации, урбанизации и глобализации невозможно остановить, потому что управление предприятием с 50000 работников невозможно, если они говорят даже на трех языках, а когда их больше. и они малораспространенные, то проще такой интернационал не нанимать. Попытки писать локализации - это занятие исключительно ВРЕМЕННОЕ. Мне на работе нахрен не нужен удмурт, который 1Ску знает только с удмуртским интерфейсом, даже удаленная поддержка такого работника невозможна. Админ сидит в Москве, и вот присылает ему работник скрин ошибки, а он на удмуртском :)
        Было бы интересно, если бы нейросеть всосала тексты на каком-то языке, а потом локализовала, при этом создавая на ходу шрифты и раскладки клавиатуры, а так - не очень.


        1. alex0x08 Автор
          24.06.2024 08:20
          +5

           менее затратно обучить 100500 индусов, у которых в каждой деревне свой язык,

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

          Поэтому задача локализации и перевода на редкие языки никогда не потеряет своей актуальности.


          1. IvanSTV
            24.06.2024 08:20
            +2

            есть вполне себе бесценные вещи вроде сохранения культурного наследия, ради которого и создаются все эти шрифты и локализации.

            проблема в том, что это используется фактически не для сохранения культурного наследия (которым занимаются научные этнографические институты), а для воспроизводства этнической и языковой разобщенности, консервирования патриархальных укладов, дополнительный фактор экономической неэффективности производства (таки удмуртский 1С потребует дополнительно удмуртского админа), препятствования процессам глобализации и развития. Рассадить этносы по национальным нужничкам, закуклив в своем языке и культуре - это ВЕРНЕЙШИЙ СПОСОБ ИХ ДЕРЖАТЬ В НЕВЕЖЕСТВЕ И НИЩЕТЕ.

            Я уже не говорю о том, что эти стимулирует производство тонн говнопродукта масскультуры - теперь говнорэпчик и на языке мумба-юмба о том, как главным мачо деревни вдул всем телкам, ограбил белого человека на мобильник и нажрался местного самогона. Наблюдал "национальное наследие" в Чувашии в виде наипримитивнейшей чувашском попсы и тонны чувашского блатняка. Как бы вы понимаете, что когда ученый-этнограф собирает фольклорный эпос, который оттачивался веками сказителей - это одно, а когда вы даете примитивному колхознику синтезатор - это совсем другое? Или нет?

            То есть, декларируя о "сохранении культуры" адепты "культурного наследия малых народов" фактически ПРЕПЯТСТВУЮТ цивилизации этих народов. В советское время пытались северным народам "сохранить традиционный уклад". Эти северные народы в 80-х плевались от этих оленей, молодежь вся массово мечтала уехать нахрен от этого традиционного уклада и родной язык забыть вместе с оленями.


            1. kenomimi
              24.06.2024 08:20

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

              Я бы выбрал так называемый английский международный: нет странных и комбинированных букв (хотя есть th, например), нет надстрочных/подстрочных/... символов, нет тонов, нет родов, минимум падежей, склонений и прочей дребедени, мало похожих букв. На нем уже говорит треть мира - сопротивление внедрению уже пробито, и если начать его массово учить везде - особого горения не будет, какое было бы с вводом искуственного языва типа эсперанто.


              1. IvanSTV
                24.06.2024 08:20
                +1

                С одним но - не нужно вести борьбу с национальными языками. Нужно мягко принуждать учить один общий, 

                Какая борьба с национальными языками? Достаточно только не гальванизировать этот труп. А локализации - это и есть попытка сделать так, чтобы дохлая лягушка лапкой подергала...


  1. CrazyOpossum
    24.06.2024 08:20

    Очень важная простыня статья, спасибо. А что насчёт языков с нестандартными направлениям записи - сверху вниз или вообще по спиралям?


    1. alex0x08 Автор
      24.06.2024 08:20

      Ну скажу честно что озадачил только вариант с «по спиралям» (такое есть?), остальное врядли представляет какую‑либо техническую сложность.

      Очевидно что будет использоваться точно такая же транслитерация во всех местах кроме непосредственного отображения пользователю, а смена направления записи легко управляется на стадии отображения, даже если речь не про веб а что-то настольное.

      Определенную проблему представляют «спецэффекты», когда например пользователь вводит арабский текст в поле ввода и он автоматически меняет направление ввода.

      Но если обойтись без них — направление ввода легко поменять на стадии обработки ввода.


      1. CrazyOpossum
        24.06.2024 08:20

        Ну скажу честно что озадачил только вариант с «по спиралям» (такое есть?), остальное врядли представляет какую‑либо техническую сложность.

        В художке такие упоминались, но полез гуглить реальные примеры. Нашёл "Бустрофедон" - строки пишутся змейкой.


        1. alex0x08 Автор
          24.06.2024 08:20

          Нашёл "Бустрофедон" - строки пишутся змейкой.

          Boustrophedon (/ˌbuːstrəˈfiːdən/[1]) is a style of writing in which alternate lines of writing are reversed, with letters also written in reverse, mirror-style. This is in contrast to modern European languages, where lines always begin on the same side, usually the left.

          Да легко, это точно не представляет технической проблемы при отображении.

          Но например для распознавания такого текста точно будут нужны особые правила.


      1. Keeper10
        24.06.2024 08:20

        вариант с «по спиралям» (такое есть?)

        Фестский диск же.


        1. alex0x08 Автор
          24.06.2024 08:20

          Не поверите но как минимум попытка реализовать «круговой интерфейс» уже была:

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


      1. event1
        24.06.2024 08:20

        вариант с «по спиралям» (такое есть?)

        Бустрофедоном записаны тексты Ронго-Ронго — языка острова Пасхи.