Давно хотелось раскрыть интересную тему локализации ПО, но так чтобы не повторяться и не цитировать прописные истины.
Поэтому рассказываю как локализовать обычное корпоративное Java-приложение на.. несуществующие фантастические языки: Клингонский и Р’льех.
Думаю начать стоит с демонстрации конечного результата — той самой нереальной локализации, ради которой все это и затевалось.
Версия на клингонском:
Версия на Р'льех:
Ну и наконец самый банальный английский:
Вот так выглядит в работе переключение локализации:
Да, это самое обычное веб-приложение на Java, работающее в обычном браузере.
Но только с локализацией на клингонский и Р'льех.
Матчасть
Компьютеры прошли длинный путь до момента появления самой возможности локализации, тем более до сегодняшних реалий и технологий, которые я буду вам демонстрировать.
Но стоит иметь ввиду, что вся эта область под названием «локализация и интернационализация ПО» — сложна и обширна, рассказать обо всех ее нюансах ни в рамках одной статьи ни даже книги у меня просто нехватит сил.
Вполне возможно, что вы дорогой читатель также владеете темой и знаете некоторые аспекты этого процесса лучше меня, либо найдете недочеты в названиях терминов — это нормально и предсказуемо.
С интересом послушаю о вашем опыте и боевых практиках.
Чтобы вы смогли оценить сложность задачи «локализации на язык которого нет», стоит для начала рассказать как происходит обычная локализация — на обычные человеческие языки.
Возьмем для примера классику в виде русско‑английской локализации, вот что необходимо реализовать в этом случае:
Определение текущей локали
Переключение локали
Хранение локализованных строк
Отображение локализованных данных
Данный функционал подразумевается как минимальный, когда речь заходит о локализации ПО, причем большая часть всей этой логики уже реализована в любом современном инструментарии и все что нужно сделать для поддерживаемых языков — «включить и использовать».
Для 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
и наслаждаемся.
Отображение
Начну с самого главного вопроса — с отображения символов несуществующего фантастического языка.
Взгляните:
На скриншоте стандартный 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
Редактор мощный и доступный практически для любых ОС — его возможностей точно хватит с запасом, по крайней мере для стадии прототипирования и любительской работы со шрифтами.
Вот так выглядит клингонский шрифт, открытый в этом редакторе:
Вот так выглядит процесс редактирования отдельного символа:
А вот так для сравнения выглядит шрифт для Р'льех:
Для полного погружения, вот так выглядит редактирование одного из этих стильных глифов:
Разумеется, можно было потратить какое‑то время и перенести глифы Р'льех в 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 и вообщем-то отлично работает без вашего участия — до тех пор пока не появляется необходимость сотворить какую-нибудь дичь.
К сожалению поддержка несуществующих языков в текстах сообщений об ошибках является именно такой дичью:
Поэтому придется немного подумать.
После долгих поисков и изучения документации, все же был найден способ вклиниться в процесс получения текстов сообщений с ошибками валидации:
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
Мы небольшая команда ветеранов ИТ‑индустрии, создаем и дорабатываем самое разнообразное программное обеспечение, наш софт автоматизирует бизнес‑процессы на трех континентах, в самых разных отраслях и условиях.
Оживляем давно умершее, чиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.