Почти каждый уважающий себя разработчик мечтает написать свой собственный проект…
Кто-то для себя. Кто-то для того, чтобы нести пользу и радость людям вокруг. А кто-то хочет вложить свое время, знания и таланты в продукт, который объединит в себе все вышесказанное и, как бонус, будет приносить доход.
Вот и я, наконец, закрыла свой гештальт.
Уже много лет меня не покидала мысль о том, как было бы здорово объединить свои знания в области психологии и умения программирования в одном проекте.
Я долго наблюдала за знакомыми, анализировала общие закономерности их поведения, глубже изучала тему мотивации, постановки целей и дисциплины. И пришла к следующим выводам:
🧠 Лень — это миф. Нет ленивых людей, есть только недостаток мотивации.
📢 Социальный капитал решает. Публичное обещание или челлендж мотивируют в разы сильнее, чем тихое решение «начать с понедельника».
👥 Вместе проще. Формировать сложные привычки в компании единомышленников гораздо легче, чем пробираться в одиночку.
🥷 Приватность — тоже сила. При этом далеко не всем подходит публичность. Для многих ощущение «за мной следят», давление группы и страх чужой оценки создают лишний стресс. Им нужен безопасный, свой личный путь без посторонних глаз и сравнений.
🧗 Масштаб пугает. Люди часто ставят гигантские цели, но спотыкаются, потому что не разбивают их на маленькие, понятные шаги и бросают всё при первой же неудаче.
И в своем приложении я постаралась учесть все эти моменты.
Ignite — это геймифицированная платформа, которая превращает процесс работы над собой и своими привычками в полноценный RPG-квест.
В приложении есть 2 варианта миссий: ЕЖЕДНЕВНЫЙ РИТУАЛ и ЭПИЧЕСКИЙ КВЕСТ. Каждая из этих миссий может быть как публичной, так и приватной.
Ежедневный ритуал создается на определенное количество дней и подразумевает ежедневное (либо через день) его выполнение. “Читать книгу 15 минут в день; Ходить 10 000 шагов или Делать утреннюю йогу”. Даже просто ”Начинать каждое утро со стакана воды”. Любое действие, которое вы хотите ввести в привычку, заслуживает место в списке ваших ежедневных миссий.
Более того, к любому публичному ритуалу можно присоединиться и делиться своими успехами или сложностями с другими участниками. И, конечно же, поддерживать друг друга.
Ежедневные ритуалы - идеальный инструмент для личных и групповых челленджей.
Эпический квест работает по-другому. Здесь вы ставите определенную конечную цель, путь к которой разбиваете на любое количество небольших шагов. Выполнение каждого из шагов и приближает вас к глобальной цели. “Причитать 50 книг; Подготовиться и пробежать марафон; Научиться готовить 20 новых блюд” или любая другая вдохновляющая вас цель может стать Эпическим квестом.
У квеста есть дата триумфа и награда, которую вы обещаете себе после успешного его завершения.
Так как, чаще всего, у каждого человека свои глобальные цели, то к квестам присоединяться нельзя. Но, если квест публичный, то за ним можно следить и поддерживать автора на его пути к успеху.
И приятным бонусом к основной функциональности - список небольших дел (шагов дня) на сегодня/завтра, которые слишком малы для ежедневного ритуала или просто должны буть выполнены однократно. Например: записаться к стоматологу или забрать посылку. Куда же без нашего любимого To-Do листа? :)
За выполнение миссий и шагов дня герои получают очки опыта, благодаря которым поднимаются вверх по рангам и Искры - внутреннюю валюту приложения, за которые можно покупать заморозку стрика, сохранять неразрывным прогресс в ритуалах или продлять путь в завершившихся, но таких приятных и полезных ритуалах.
Сейчас проект находится на стадии активного тестирования, и мне очень важен ваш фидбэк!
Буду рада и бесконечно благодарна, если вы попробуете Ignite-me.app, поделитесь своими впечатлениями, подсветите баги и подскажете, что, на ваш взгляд, можно было бы улучшить.
Так как приложение написано как PWA, его не нужно скачивать, а можно легко и быстро добавить как ярлык на телефон, будь у вас Android или iOS. Это позволит получать уведомления от приложения (только в том случае, если вы их хотите видеть и включите в настройках профиля) и всегда иметь его под рукой.
На относительно простом примере показываю как можно сделать программу «снова великой».
*разумеется это нейросетевая Дженна а не реальная, которая про рефакторинг ничего не знает.
Исходный код отрефакторенной версии выложен на Github.
Задача
Допустим есть некий софт, созданный еще «при царе Горохе» неким гордым но умным одиночкой, которого с тех пор никто не видел. Софт живой и с пользователями, которые приносят прибыль, поэтому его надо как‑то развивать и поддерживать.
В попытке расширить команду разработки, вы начинаете нанимать новых разработчиков, но раз за разом происходит одна и та же ситуация:
проработав месяц-два, нанятые программисты в ужасе убегают в закат.
Кто‑то при увольнении намекает на причины такого поступка, в диапазоне от «ваш проект попахивает» до надо «срочно все переписать». После примерно десятого убежавшего программиста, вы наконец начинаете задумываться, что возможно с проектом действительно что‑то не так и стоит провести этот самый «рефакторинг».
Так это обычно начинается.
Образец
В качестве образца для этой статьи был взят один интересный но малоизвестный широкой публике проект JPC:
The fast x86 PC emulator in 100% pure Java
Самый настоящий эмулятор старого x86-компьютера, реализованный без всяких нативных частей — на чистой Java!
Не очень большой (~6500 файлов с исходным кодом), но имеет стадию «внутренней генерации» — часть исходного кода создана не вручную а путем запуска кодогенератора по метаданным, что довольно часто встречается у больших проектов с историей, причем на любом языке.
Еще к сожалению JPC немного заброшен, что для статьи только в плюс поскольку добавляет реалистичности — именно в таком состоянии чаще всего пребывают проекты, которые просят «привести в чувство».
Текущее состояние
После переезда проекта на Github, список коммитов выглядит следующим образом:
Мягко говоря негусто.
Никаких тестов в проекте нет, по всей видимости тестировалось все вручную с помощью молитвы, еще судя по исходному коду — далеко не весь функционал является рабочим, что также характерно для проектов в «пред‑рефакторинговом» состоянии.
Думаю теперь очевидно почему для этой статьи был выбран именно JPC — несмотря на редкость решаемой задачи, для рефакторинга это самый типичный клиент.
Собирается сей чудо-проект с помощью.. Makefile, что для мира Java является дичью и извращением редкостью:
make application
Примерно как собирать проект на QT с помощью Gradle или (еще лучше) — sbt.
Предложите как-нибудь коллегам и посмотрите на реакцию, некоторые точно перестанут с вами здороваться за руку.
Для сборки необходимо указать путь к JDK в переменной PATH, при этом сборка проходит успешно даже с последними версиями (автор использовал OpenJDK 21).
Готовое приложение JPCApplication.jar появится в корне проекта после завершения сборки, но на этом хорошие новости заканчиваются:
Собранное приложение отказывается запускаться, что мы исправим чуть ниже.
Стадия первая: новый скелет
Первым делом, как и в реальном боевом проекте, необходимо избавиться от любого «самопала», задействованного при сборке. Причина, почему этот шаг критически важен на самом деле не так очевидна:
статические анализаторы — главный иструмент рефакторинга, крепко привязаны к структуре проекта и стандартным средствам сборки
Разумеется существуют варианты и с произвольной структурой проекта, но эффективность рефакторинга будет заметно ниже. Поэтому автор сделал стандартный (для своей практики) «финт ушами»:
перевел сборку проекта на Apache Maven, максимально широко поддерживаемый средствами анализа кода, CI-системами и средами разработки.
Реализовать такую миграцию в данном случае оказалось очень просто, поскольку JPC совсем не использует внешние библиотеки. Все что я сделал — раскидал ресурсы и исходный код в стандартную для Maven структуру каталогов:
Исходный код был перенесен из каталога src в src/main/java, ресурсы — в src/main/resources. Также был добавлен очень простой pom.xml, описывающий минимальные шаги сборки проекта:
Это было убрано, поскольку точно такой же параметр запуска зашит еще и в код:
Наследие «былых времен», которое также достаточно часто встречается в устаревших проектах — во времена Java 1.5 и апплетов было модным использовать собственные атрибуты в манифесте.
Стадия вторая: удаление ненужного
Как в практически любом долгоживущем проекте, в JPC есть свои «внутренние утилиты» — отдельные программы, написанные для задач внутренней автоматизации.
Это та самая «грязная рабочая поверхность», которую не видит конечный пользователь.
При проведении рефакторинга, трогать внутренние утилиты стоит в последнюю очередь и в самом крайнем случае, поскольку правильность их работы проверять тяжело (ни тестов ни документации для внутренних утилит обычно нет в природе), зато они сильно влияют на общую работоспособность проекта.
В JPC внутренние утилиты реализованы в виде отдельных классов в пакете «tools» и нескольких шелл-скриптов в корне проекта.
И то и другое я просто не стал переносить в новую версию, также я убрал часть исходного кода эмулятора, отвечающего за отладку (пакет org.jpc.debugger) — по той же самой причине.
был убран импорт класса org.jpc.debugger.LinearMemoryViewer а используемая статичная функция translateLinearAddressToInt перенесена в класс PC.
Все эти действия позволили сократить кодовую базу проекта практически вдвое, что сильно упростило следующий шаг рефакторинга.
Стадия третья: критические проблемы
Наконец мы подошли непосредственно к самому рефакторингу, который я буду проводить с помощью среды разработки Intellij Idea.
Первый запуск анализатора дает следующий результат:
24 критических ошибки и ~ 37 тысяч предупреждений — не так уж плохо, по сравнению с тем что бывает на свете.
Смотрим глубже и видим, что все 24 ошибки — действительно самые критичные, поскольку из-за них проект может перестать собираться в самом ближайшем будущем:
Так что эти места стоит рефакторить в первую очередь, пока проект хотя-бы собирается из исходников.
Есть и хорошая новость:
Как видно из скриншота выше, большая часть критичных ошибок гнездится в классе JPCApplet, который используется для запуска приложения в режиме Java-апплета — ныне устаревшей технологии, когда-то работавшей с помощью плагина для браузера.
Поскольку плагин более официально не поддерживается — вся технология приказала долго жить и у обычных пользователей не встречается, так что класс можно удалить.
Но все несколько сложнее, поскольку еще есть вложенные классы, один из которых используется снаружи (org.jpc.j2se.JPCApplication):
JPCApplet.PlayPausePanel pp = new JPCApplet.PlayPausePanel(this);
Я просто перенес этот класс по месту использования, что позволило наконец удалить JPCApplet из проекта целиком.
Получилось минус 13 критических ошибок.
Еще один источник проблем — класс LinkBorder также можно удалить, поскольку он использовался лишь из удаленного JPCApplet.
Что дало еще минус три критических ошибки.
Дальше смотрим класс org.jpc.emulator.peripheral.Mixer, который забит предупреждениями от анализатора буквально через каждую строчку, однако вносить массовые правки пока не стоит — «всемогущая» Idea временами ошибается и это именно такой случай.
Анализатор ругается (в первую очередь) на конструктор new Float(), поскольку его прямое использование объявлено устаревшим, а в новых версиях Java стоит использовать Float.valueOf() в качестве замены.
Но как только вы замените конструктор, анализатор подскажет еще несколько оптимизаций, так что конечный вариант будет достаточно сильно отличаться:
Ругается анализатор на уникальный метод stop(), который был отмечен как устаревший еще до того как я начал писать на Java:
'stop()' is deprecated since version 1.2 and marked for removal
Примерно до версии 1.8 использование данного метода еще можно было как‑то оправдать наличием устаревших библиотек, в нынешних реалиях этот метод — просто еще один способ «выстрелить себе в ногу»:
Stopping a thread causes it to unlock all the monitors that it has locked.
Так что в коде использование этого метода точно стоит заменить на стандартный .interrupt() :
if (runner.isAlive()) { runner.interrupt(); }
Блок try-catch также можно спокойно убрать, поскольку SecurityException не выбрасывается в новых версиях Java при попытке остановки нити.
На этом все критические проблемы в проекте решены и получен минимальный практический смысл от всей затеи:
убраны места, которые могут сломать сборку проекта в новых версиях Java
Стадия четвертая: ошибки выполнения
Пришло время наконец попробовать запустить нашего «франкенштейна».
Сборка разумеется завершится успешно (не зря же старались), но при запуске будет выбрасываться все та же ошибка поиска ресурсов:
В оригинальной версии JPC, часть ресурсов (например образы биоса) загружались только из jar‑файла, часть (образы дисков) — только снаружи, из каталога resources, при этом каталог с ресурсами был общим.
Сию дичь необходимо пресечь и сделать в более адекватном стиле, например как это реализовано в движке знаменитого Quake:
сначала ищем внешний файл, если не найден — ищем в ресурсах, если не найден в ресурсах — падаем с ошибкой
За чтение образа BIOS отвечает вот такой метод в классе org.jpc.emulator.motherboard.Bios:
private staticfinalbyte[] getBiosData(String image) throws IOException { InputStream in = Bios.class.getResourceAsStream(image); if (in == null) { thrownew IOException("resource not found: " + image); } try { ByteArrayOutputStream bout = new ByteArrayOutputStream();
while (true) { int ch = in.read(); if (ch < 0) { break; } bout.write((byte) ch); }
В принципе за такую реализацию уже можно начинать бить, спасает лишь факт, что столь идиотсткое побайтовое чтение работает исключительно с ресурсами, которые уже находятся в памяти.
File f = new File(image); if (f.exists() && f.isFile() && f.canRead()) return Files.readAllBytes(f.toPath());
f = new File("resources",image); if (f.exists() && f.isFile() && f.canRead()) return Files.readAllBytes(f.toPath());
final URL u = Bios.class.getResource(image); if (u == null) thrownew IOException("resource (bios) not found: %s".formatted(image));
try (InputStream in = u.openStream()) { return in.readAllBytes(); } }
Логика переделана на возможности современной Java 17, поэтому кода стало сильно меньше, также были добавлены проверки на наличие ресурса:
по полному пути,
по частичному (предполагается что файл находится в каталоге resources),
поиск внутри jar приложения.
Но при следующей попытке запуска получаем еще одно исключение, уже в другом месте:
Причиной является искусственная проверка:
if (!(cl instanceof URLClassLoader)) thrownew IllegalStateException();
Когда-то давно системный загрузчик классов действительно наследовался от URLClassLoader, так что проверка бы отработала.
Несмотря на то, что подобные искусственные проверки служат вообщем‑то хорошей цели раннего обнаружения проблем, временами разработчики перебарщивают и пытаются контролировать то что контролю не поддается.
Однако одним лишь удалением проверки дело не ограничилось — необходимо почистить еще один метод, реализующий «закат солнца вручную»:
if (!dir.equals(thisDir)) continue; resources.add(name); }
jarStream.close(); } catch (IOException e) { e.printStackTrace();} } InputStream stream = context.getResourceAsStream(directory); try { if (stream != null) { Reader r = new InputStreamReader(stream); StringBuilder sb = newStringBuilder(); char[] buffer = newchar[1024]; try { while (true) { int length = r.read(buffer); if (length < 0) { break; } sb.append(buffer, 0, length); } } finally { r.close(); }
for (String s : sb.toString().split("\n")) { if (context.getResource(directory + s) != null) { resources.add(s); } } } } catch (IOException e) { LOGGING.log(Level.INFO, "Exception reading images directory stream", e); }
return resources.iterator(); }
Тут происходит поиск доступных образов дисков путем последовательного перебора всех файлов внутри .jar с приложением.
С учетом того что .class файлов внутри ~6500 — такое решение мягко говоря «не оптимально».
Вообще говоря любой поиск ресурсов через перебор во время работы приложения является медленным, это и есть основная причина медленного запуска любого приложения на (например) Spring Boot.
Поскольку в проекте используется очень небольшое количество образов диска и нет вариантов по резкому увеличению их количества, я просто зашил названия в код:
privatestatic Iterator<String> getResources(String directory) { final List<String> resources = new ArrayList<String> (Arrays.stream(IMAGES).toList()); final File f = new File(directory);
if (!f.exists() || !f.isDirectory()) { return resources.iterator(); }
final File[] files = f.listFiles();
if (files == null) { return resources.iterator(); } for (File ff: files) { resources.add(directory + ff.getName()); }
return resources.iterator(); }
Метод getResources() используется для отображения списка доступных образов дисков через меню приложения, все внутренние образы (зашитые в.jar) добавляются в этот список автоматически.
После столь примитивной правки, приложение стало запускаться визуально быстрее даже на мощном современном ноутбуке, так что не стоит недооценивать силу простых решений ;)
Хотя всех правок выше оказалось недостаточно, следующая остановка — класс org.jpc.support.ArrayBackedSeekableIODevice, который (внезапно) играет ключевую роль в проекте.
Метод configure() отвечает непосредственно за загрузку образов дисков и дискет:
File f = new File(spec); if (f.exists() && f.isFile() && f.canRead()) { imageData = Files.readAllBytes(f.toPath()); length = imageData.length; return; }
f = new File("resources",spec); if (f.exists() && f.isFile() && f.canRead()) { imageData = Files.readAllBytes(f.toPath()); length = imageData.length; return; } final URL u = ArrayBackedSeekableIODevice.class.getResource(spec); if (u == null) thrownew IOException("resource (image) not found: %s" .formatted(spec));
На этой стадии была проведена самая настоящая «коммерческая оптимизация» — доведен до ума функционал актуальный конечным пользователям.
Это уже не стандартные сказки про «технический долг» и «плохую архитектуру», а вполне себе осязаемый результат, который можно потрогать.
Так что вас за такое-то скотство, проведенное с рабочим проектом уже точно не уволят ;)
Стадия пятая: массовые правки
Все описанное выше — обязательные базовые части, без которых рефакторинг вообще не может состояться как согласованный с бизнесом и оплаченный процесс. Но можно зайти дальше — в действительно рисковую зону, где ваши действия могут иметь не всегда предсказуемые последствия:
массовые и сквозные правки исходного кода, во всем проекте целиком
Рабочая область выглядит как-то так:
Собственно все «желтенькое» на скриншоте ниже — места для рефакторинга, заботливо подсказанные средой разработки:
К сожалению на практике все несколько сложнее чем подсказывает Idea и просто нажимать «Alt + Shift + Enter» на каждую подсказку не стоит:
Все потому, что в проекте активно используется Reflection API для загрузки и обращения к методам класса необычными способами:
Что сводит анализаторы исходного кода с ума, поэтому примерно половина методов в проекте десктоп-приложения, не использующего никакие IoC-контейнеры отмечено как неиспользуемые:
Скотство?
Конечно скотство, но и в реальных больших и старых проектах такое тоже будет в обязательном порядке — когда‑то использование Reflection API считалось модным и молодежным явлением, убрать которое "под капот" смогли только те самые IoC‑контейнеры вроде Spring.
Следующим примером кода, нуждающегося в массовой зачистке является использование анонимных классов:
Лямбды появились еще в Java 8 и с тех пор уже нет никакого здравого смысла их игнорировать — они здорово сокращают объем кода:
В любом legacy-проекте, особенно если это приложение для десктопа такого будет очень и очень много:
Следующий повод для массовых правок — прямой результат ручной разработки, без использования средств проверки и анализа кода:
Хороший пример, подсказанный анализатором:
Разумеется это не является критической проблемой, поскольку эти модификаторы ничего не делают, но таких мест очень много и в сумме они дают ненужное увеличение объема кодовой базы.
Следующие две проблемы — также частые гости устаревших проектов:
Точно также как и с ненужными модификаторами в интерфейсе, всего лишь занимают место и увеличивают объем кода.
Хотя пример выше это совсем уж старый код, поскольку метод Arrays.asList () появился еще в Java 7 — былинные времена далекого прошлого, как можно было его сохранить до сих пор — загадка.
Эпилог
Если вы никогда не видели JPC то стоит посмотреть, поскольку он в свое время несколько расширил мнение о возможности Java, в первую очередь в плане производительности — тема о которой много и сильно шутили еще 10 лет назад.
Ну а если перед вами стоит задача провести подобный рефакторинг — стадии с первой по четвертую фактически являются руководством к действию.
Массовые правки я бы с ходу делать не рекомендовал — очень уж высокие риски, что что‑то пойдет не так.
Еще в реальном проекте процесс рефакторинга скорее всего сильно затянется, поэтому вам придется делать промежуточные срезы и синхронизировать ваш рефакторинг с обычной разработкой, о чем стоит помнить до начала всего действа.
Рассказываю как мы сделали самые крутые визитки на Диком Западе в отечественной ИТ-индустрии.
Внимание на код - он полностью рабочий!
Все началось когда автор наткнулся на одну интересную статью где эксперт по 3D-технологиям вместил специально оптимизированный и обфусцированный код рейтрейсера на C++ в размеры своей визитки.
Мы позеленели от зависти тоже захотели себе что-то такое, но поскольку занимаемся все же серверами а не 3D-графикой и больше Java, чем C++ — решили что будет круто уместить на обратной стороне нашей визитки простейший HTTP-сервер на Java.
Вместе с запуском и компиляцией.
Еще при наличии графического окружения будет запущен браузер.
Плюс немного криптографии для защиты от подделки.
Весь код уместился в 18 строк, выровненных по ширине так чтобы влезть в размеры визитки:
Вбиваете код с визитки в любимый редактор, сохраняете файл как vcard.sh и запускаете:
Локально запустится простейший HTTP-сервер, который отдаст текстовую страничку с нашими контактами. При наличии GUI — запустится еще и браузер по-умолчанию, с автоматическим открытием страницы этого сервера.
И все это в 18 строк кода.
Да, еще будет нужен любой Linux/BSD/MacOS/Solaris и любая версия JDK начиная с 1.8 на машине.
Поддержку запуска на Windows делать не стал (хотя это и возможно технически), но можно спокойно запустить в WSL .
Чтобы вы не мучились с вводом кода с картинки, вот текстовая версия:
Тут используется связка из заголовочного shell-скрипта и слегка обфусцированного кода на Java. Еще я не стал кодировать весь блок на Java полностью в HEX-строку, чтобы визуально оставалось ощущение исходного кода.
Это shebang, стандартное для Unix указание на используемый интерпретатор, про него и так все знают. Дальше происходит создание временного каталога в /tmp и присваивание его имени переменной в скрипте:
t=$(mktemp -d);
Затем получение скриптом собственного имени с полным путем:
e=$(realpath $0);
Чтение скриптом самого себя, с отрезанием первых 4х строк - чтобы получить блок кода на Java:
sed '1,4d' $0
Дальше начинается pipe, в котором результат предыдущей команды передается на вход следующей:
Результат всех преобразований записывается в файл Yo.java, в том самом временном каталоге.
Малоизвестная опция -XDignore.symbol.file отключает предупреждение об использовании системных классов JDK (com.sun.net.httpserver.*) в проекте — в 1.8 версии классы встроенного в JDK HTTP-сервера еще считались системными.
Запуск с передачей полного пути оригинального скрипта для последующего его чтения из Java-кода:
java -cp . Yo $e
Сам код после деобфускации и форматирования выглядит уже вот так:
Тут уже большая часть логики вполне очевидна, поэтому раскрою лишь два самых сложных фрагмента.
Криптография
Когда я только начинал думать над реализацией этой штуки, уже было ясно что нужен какой-то неочевидный контроль целостности:
исходный код очевидно будут пересылать через сообщения, в виде постов или по почте, что легко его сломает.
Поэтому хотелось хоть какую-то защиту от подделки содержимого, чтобы компьютерные дети не добавили патч Брамина в самое интересное место, а индийский паренек не подменил авторство и мои контакты на свои, ради строчки в резюме.
Задачу усложнял факт передачи открытых исходников и ограничение по размерам, но видимо получилось:
static String ED = "1AtzGU0uq7J7DHPdjdJJ5JJDiwQi8mElIDOjuRK0DEU=";
На каждую попытку как-то подменить содержимое (включая заголовок) будет выдаваться вот такая ошибка:
Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. at java.base/com.sun.crypto.provider.CipherCore.unpad(CipherCore.java:981) at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1062) at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853) at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446) at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202) at Yo.main(Yo.java:6)
Получается код сам себя защищает от подделки.
0x7f000001
Вторым неочевидным моментом является вот такой странный адрес хоста:
String h = "0x7f000001";
Который используется при формировании ссылки для открытия браузером:
Desktop.getDesktop().browse(new URI("http://" + h + ":" + p));
Такое применение однозначно говорит о том что адрес очень даже стандартный, поскольку проходит как стадию валидации на стороне Java при формировании объекта URI, так и валидацию на стороне запускаемого браузера.
Это просто нотация, вариант написания IP-адреса 127.0.0.1, обозначающего loopback (петлю) — внутренний интерфейс, к которому можно подключиться локально, а не из сети.
Вот тут больше примеров различных вариантов написания IP-адресов, уверен — удивит даже бывалых админов.
P.S.
Статья была опубликована на Хабре, более фривольный оригинал статьи находится в нашем блоге, где мы подробно рассказываем об ужасах разработки, вгоняя в краску даже опытных и бывалых.
Главной премьерой вечера стал огромный 116-дюймовый телевизор Hisense 116UX. Удивляли гостей не только размеры экрана — компания решила отказаться от привычной презентации.
Технологии на языке искусства
Новую линейку телевизоров компания представила в формате арт-галереи. Вместо привычных стендов — тематические инсталляции, вместо списка характеристик — пространство, где технологии можно увидеть и услышать.
Все это — чтобы показать, насколько яркими и живыми могут быть цвета, как выглядит глубокий черный и на что способна встроенная акустика. Над экранами разместили большие художественные полотна, которые продолжали происходящее на дисплеях.
Новые модели от Hisense
На презентации продемонстрировали несколько моделей новой линейки — UR8S, UR9S и флагманский Hisense 116UX.
Особенность новинок — технология RGB MiniLED. Теперь телевизор точнее управляет цветом и светом, поэтому изображение выглядит ярче, контрастнее и естественнее.
Менеджер по продуктам ТВ- и аудиокатегории Hisense Шон Ли:
«Запуск технологии RGB Mini-LED на российском рынке является новым этапом в развитии Hisense. Мы задали новый стандарт визуального опыта, который уже получил признание на глобальном уровне».
Больше всего внимания собрал Hisense 116UX. Это телевизор с трехметровой диагональю. На таком экране особенно хорошо заметны детали: яркие сцены остаются насыщенными, темные — глубокими, а изображение выглядит объемным и живым. За качество картинки отвечает фирменный процессор Hisense, а встроенная многоканальная акустика создает эффект домашнего кинотеатра без дополнительных колонок.
Вместо лекций — живое общение
Для любителей игр организовали отдельную зону. К телевизорам подключили консоли, чтобы гости могли проверить, как новинки ведут себя в динамике.
Организаторы отказались и от длинных технических презентаций. Гости переходили между зонами, сравнивали модели, задавали вопросы специалистам и тестировали телевизоры в удобном для себя темпе.
Линейка уже поступила в продажу. Доступны модели UR8S в нескольких размерах экрана, серия UR9S и флагманский 116UX. Телевизоры можно приобрести у крупных российских ритейлеров и на популярных маркетплейсах.
Доброе утро! Сегодня продолжил изучение книги Стивена Праты и изучил cin и теперь cin и cout не абстрактные методы а именно ввод и вывод, также я поработал с fstream и очень удивлён лаконичностью работы с файлами в сравнении с питоном, ещё поработал с chrono ( модуль для работы со временем ) и получилось пока что написать что то такое:
вот что получаем на выходе программы:
и вот что содержится в текстовом файле:
также сегодня покастомизировал свою виртуальную машину и теперь у меня вместо вот такого:
вот такое:
я заменил foot на alacrity что было довольно сложно и пришлось с нейрокой фиксить очень многое и добавил конфигурацию zproger с красивой темой, результатом более чем доволен и пока что останусь на alacritty Fish
Доброе утро. На данный момент начал прочтение книги Язык программирования C++ (Стивен Прата), вспомнил строгую типизацию ( был опыт с Arduino ), разобрался что за int main():
int main() это обязательная функция которая возвращает строго int и на сколько я понял, число которая она возвращает указывает на статус выполнения, в ней есть необязательный return 0; который ещё может вызываться неявно. Также узнал у существовании cout в пространстве имён std и вывел свой первый текст ( не особо понял в чём такая великая сложность вызвать функцию и в место print("Some text") написать std::cout << "Some text";)
Вот пока что код который отражает все полученные мной знания в данном языке, компилировал с помощью g++:
высылаю скрином из за некорректного отображения кода текстом в Пикабу
Буднично рассказываю как локализовать обычное корпоративное приложение на нечеловеческие языки: Клингонский и Р’льех.
На этом скриншоте куда больше реального приложения чем кажется на первый взгляд.
Эээ.. думаю стоит начать с демонстрации результата — той самой нереальной локализации, ради которой все это и затевалось, чтобы всем сразу "все стало понятно".
Так выглядит версия на клингонском:
Обратите внимание на даты — это настоящий Stardate.
А вот так выглядит версия на Р'льех:
«Cthulhu fhtagn!» на JSF, CDI и JPA. Сложно сказать какая часть предложения напугает сильнее.
Ну и наконец банальный английский:
Вот так выглядит в работе переключение локализации:
Да, это самое обычное веб-приложение на Java, работающее в обычном браузере.
Но только с локализацией на клингонский и Р'льех.
Матчасть
Чтобы вы смогли оценить сложность задачи «локализации на язык которого нет», стоит для начала рассказать как происходит обычная локализация — на обычные человеческие языки.
Возьмем для примера классику в виде русско‑английской локализации, вот что необходимо реализовать в этом случае:
Определение текущей локали
Переключение локали
Хранение локализованных строк
Отображение локализованных данных
Данный функционал подразумевается как минимальный, когда речь заходит о локализации ПО, причем большая часть всей этой логики уже реализована в любом современном инструментарии и все что нужно сделать для поддерживаемых языков — «включить и использовать».
Вот так например выглядит хранение локализованных строк:
Это абсолютно стандартный способ, поддерживаемый как самим JDK так и всем прикладным ПО на Java
Также легко и просто оперировать обычным человеческим языком со стороны прикладного кода, например вот так выглядит получение локали из кодового названия:
Locale locale = Locale.forLanguageTag("en_US");
Где en — это указание на английский а US — на страну США.
Не менее легко происходит и переключение между языками (в данном случае в Jakarta Faces):
Но вся эта благодать быстро заканчивается, стоит только выйти за границу реальности поддерживаемых локалей и попытаться использовать «то чего нет».
Язык которого нет
Символы несуществующих фантастических языков предсказуемо отсутствуют в официальной таблице символов Unicode, их нет в списке поддерживаемых средствами разработки и нет в браузере.
Что означает невозможность какой-либо работы «из коробки» с таким языком — без специальных шагов.
Но прежде хотелось бы немного рассказать о самих фантастических языках, выбранных для локализации — чтобы у вас появилось некоторое представление куда может завести фанатизм и любовь к хардкору.
В мире где настоящие человеческие языки отмирают по сотне в день по мере ухода из жизни последних носителей, кто-то специально учит вымышленный!
Поскольку большинство фанатов клингонского — самые разнообразные гики, хорошо дружащие с техникой и матчастью, было и есть множество попыток протащить вымышленный язык куда только можно.
Например в ядро 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
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.
Дословный перевод — «ктулхский» или «р'льехский», что (да простят меня подводные боги) показалось не очень благозвучным.
Поэтому я использовал термин Р'льех, который на самом деле означает иное:
Н'ЯРЛАФОТЕП — отличное название для нового проекта, не находите?
Доступные 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+), забираем проект из репозитория:
Готовое приложение будет находиться в каталоге target:
В каталоге liberty находится распакованный сервер приложений Open Liberty, с установленным внутрь нашим приложением — за все эти радости отвечает специальный плагин (см. ниже).
Запуск
Как уже упоминалось выше, наш замечательный проект предназначен для запуска и работы на сервере приложений IBM Open Liberty.
Разумеется вы можете сходить по ссылке выше, прокрутить страницу вниз до раздела Releases, скачать версию 24.0.0.6+ с профилем Jakarta EE 10, развернуть и затем установить туда наше приложение.
Для настоящего развертывания в корпоративной среде обычно и делают. По крайней мере делали до эры докера.
Но поскольку у нас тут технологическое демо, я посчитал что все эти шаги по развертыванию будут слишком сложными и добавил в сборку специальный плагин для автоматического развертывания и запуска.
Одной командой:
mvn liberty:dev
Произойдет скачивание IBM Open Liberty, распаковка, настройка, установка внутрь нашего приложения и немедленный запуск.
Вот так это выглядит из среды разработки Intellj Idea:
Начну с самого главного вопроса — с отображения символов несуществующего фантастического языка. Взгляните:
Нет это не галлюцинации или фотошоп, это установленный правильный 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 FOSSfont 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‑области.
Вот так выглядит процесс редактирования отдельного символа:
Имейте ввиду что это долгий и утомительный процесс, особенно если речь про разработку шрифта с нуля.
А вот так для сравнения выглядит шрифт для Р'льех:
Как видите тут не используется PUA и заменяются символы ASCII, с самого начала таблицы.
Для полного погружения, вот так выглядит редактирование одного из этих стильных глифов:
И ведь кто-то сидел и рисовал это. Воистину воля подводных богов безгранична.
Разумеется, можно было потратить какое‑то время и перенести глифы Р'льех в PAU‑область, что позволило бы использование шрифта по аналогии с клингонским — параллельно с другими языками.
Но к сожалению я не верю в Ктулху обладаю достаточным запасом времени и сил, так что оставил как есть.
На самом деле есть еще одна важная причина — показать вам два подхода к локализации, а не один:
второй вариант реализации шрифта с полной заменой всех символов на безумные иероглифы чем-то фантастическим (без использования PAU-области) встречается куда чаще.
Его точно стоит учитывать, поскольку скорее всего именно с таким шрифтом вы и столкнетесь, пытаясь работать с фантастическими языками.
Отображение в браузере
Отдельно опишу как происходит отображение этих фантастических языков в браузере — поскольку мы используем веб, а не отдельное десктоп-приложение.
Все современные браузеры поддерживают регистрацию и использование пользовательских шрифтов на странице — это мягко говоря не новость.
Регистрация TTF‑шрифта происходит путем использования CSS‑стиля и специальной директивы font‑face:
Сложно выглядящая директива #resource[''] на самом деле уже часть парсера страниц JSF — EL-выражение, преобразующее относительный путь к указанному ресурсу в полный.
А вот так выглядит задание отдельных стилей для использования наших фантастических шрифтов:
.klingon { font-family: 'Klingon'; }
Эти стили применяются выборочно, для включения фантастического шрифта при включенной перекодировке у сообщения:
Если сообщение было написано на клингонском pIqaD — оно будет пропущено через транслятор (см. ниже) и при отображении будет использован клингонский TTF‑шрифт.
Таким образом сохраняется обратная совместимость с другими языками и остается возможность ввода на обычном английском.
Но это решение только для отдельных блоков сообщений, ведь есть еще глобальное переключение выбранной локали:
Для решения этой задачи, используется вот такая логика:
Звездочка (*) означает что указанный шрифт должен быть применен ко всем элементам на странице, что и дает вот такой эффект глобальной локализации всего:
Также тут задается фоновая картинка в немного странном формате:
klingon.jpg.xhtml
На самом деле файл называется klingon.jpg и находится в каталоге webapp/resources, а постфикс .xhtml — особенность работы ресурсов в JSF, он нужен для правильной работы, хотя и выглядит полной дичью.
Переходим к следующей важной теме.
Транслятор
При локализации на несуществующий и неподдерживаемый язык существует еще одна проблема:
необходимо как-то работать с локализованным на такой язык текстом из стандарного окружения.
Конечно можно попробовать ставить шрифты, поддерживающие ваш фантастический язык в каждый используемый редактор, каждый терминал и среду разработки — да, это будет работать (см. ниже).
Но с точки зрения промышленной разработки это плохой путь — любая ошибка приведет к тому что вы не сможете увидеть локализованный текст вообще, либо он будет отображаться неправильно.
Если очень повезет, то пойдя этим путем можно получить что-то такое:
Круто, но слишком сложно и не подходит для массовой разработки — когда задействовано много разработчиков.
Есть способ лучше. Дело в том что ни один, даже трижды фантастический язык не существует в вакууме — для него в обязательном порядке создается:
Транслитера́ция (лат. trans- «через; пере-» + littera — «буква») — точная передача знаков одной письменности знаками другой письменности[1][2], при которой каждый знак (или последовательность знаков) одной системы письма передаётся соответствующим знаком (или последовательностью знаков) другой системы письма.
Даже если речь про например дотракийский — выдуманный сценаристами язык кхала Дрого из «Игры Престолов», к нему все равно в качестве приложения идет транслитерация на английском — актерам надо как-то учить произношение.
Более того, такая транслитерация существует и для самих человеческих языков, причем видимо для всех (исключений пока не встречал).
Например есть широко известный вариант написания кириллицы с помощью символов латиницы:
Нет людей в рунете старше 30ти, которые бы его никогда не видели.
Собственно транслит встречается до сих пор — стоит только сломаться мультиязычному вводу на вашем компьютере или телефоне и все — вам тоже придется его использовать.
Именно транслитерацию в латинские символы мы и будем использовать.
Да это «Гамлет» на клингонском — а что вы знаете о фанатизме?
pIqaD
Вариант написания клингонского латинскими символами называется pIqaD, конечно же он куда более широко распространен и популярен чем те сложные клингонские иероглифы, которые я с таким трудом отображал выше.
Думаю не стоит упоминать, что при такой популярности есть и устоявшиеся правила транслитерации и (что куда более важно) — готовые наработки. Очень быстро были найдены и готовые трансляторы, самый популярный (из открытых) выглядит вот так:
На основе его исходного кода (на Javascript) была написана моя реализация на Java, с помощью которой вот такие строковые ресурсы:
Превращаются во время работы приложения в те самые фантастические иероглифы:
Как видите тут происходит достаточно простая замена символов согласно таблице подстановки, с латинских на Unicode из PAU‑области — все внешне сложное, на самом деле устроено очень просто.
Один из немногих оригиналов документов на Р'льех.
К сожалению (или к счастью — в зависимости от контекста), фантастический язык Р’льех из миров Лавкрафта куда менее популярен, поэтому получилось найти всего один рабочий транслятор:
Using the digital serpent's package, you can translate english to the language of the "old ones" Spread aimgr'luh
Занимается им некий китайский DevOps-инженер (надеюсь не в рамках должностных обязанностей), сам транслятор и написан на Python:
Важным моментом является другой принцип работы — вместо транслитерации символов происходит подстановка слов или даже целых фраз:
Вся логика была портирована в мой проект, мою реализацию транслятора для Р'льех можно посмотреть вот тут. Разумеется с таким подходом в виде зашитого и очень небольшого словаря, нет возможности реализовать перевод технических терминов:
у меня честно нет идей как могут выглядеть слова «Авторизация», «Назад» или «Сохранить» на языке древних.
Поэтому транслятор Р’льех используется только для ввода текста — чтобы найти истинных последователей показать как это работает.
Но перейдем к следующей интересной теме.
Нереальная локаль
Следующей проблемой при работе с фантастическими языками является их регистрация в системе — в том языке, платформе или фреймворке, который вы используете.
Это нужно в первую очередь для того, чтобы как‑то сигнализировать внутри приложения о том что используется такой фантастический язык и проводить соответствующую подстройку — например вызывать тот самый транслятор, описанный выше.
Тут может быть огромное количество вариантов, проблем и подводных камней, поскольку такой разработкой мы выходим за рамки обыденного поддерживаемого. И при возникающих проблемах вам скорее всего никто не поможет — кроме нас разумеется.
Но для Java весь процесс более-менее отработан, описан и предсказуем:
в Java у локалей есть поддержка т. н. «variant» — специальной вариации языка, которая может быть сколь угодно нестандартной.
Сама локаль остается системной (в данном случае — английской), но при этом к ней добавляется специальный постфикс, означающий что используется «вариация»:
Поскольку такие 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> */ publicclass KlingonedResourceBundle extends ResourceBundle { public KlingonedResourceBundle() { setParent(ResourceBundle.getBundle("i18n.messages", FacesContext.getCurrentInstance().getViewRoot().getLocale())); } @override publicfinalvoid setParent(ResourceBundle parent) { super.setParent(parent); } @override protectedObject handleGetObject(@Nonnull String key) { // here will be extracted and substituted value finalObject v = parent.getObject(key); if (!(v instanceofString 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);
Основное действие происходит в методе handleGetObject() ,сейчас разберу логику этого метода по шагам, благо она будет повторяться и в других местах.
Первым шагом происходит вызов такого же метода, но из родительского класса — для получения еще не перекодированного текстового шаблона:
final Object v = parent.getObject(key);
Затем происходит отбраковка по возвращаемому типу — мы работаем только со строками и все остальные варианты пропускаем:
if (!(v instanceofString 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»:
Но это еще не все интесное и необычное, что хотелось бы раскрыть в рамках статьи.
Валидация данных
Как каша без масла протеина или водка без закуски — не бывает корпоративных приложений без валидации данных.
В Jakarta EE (как и в ее предшественнике JavaEE) для автоматической валидации входных данных используются механизмы из спецификации JSR 303 «Bean Validation».
В самом простом случае это выглядит как аннотирование полей класса:
.. @size(min = 3, max = 255) privateString title; // a title @NotBlank(message = "{validation.message.not-blank}") @Lob @column(length = Integer.MAX_VALUE) privateString message; // message, stored as CLOB in database, //so size is almost unlimited @size(min = 3, max = 30) @email privateString author; // author's email ..
Когда такой класс попадает в качестве входящего аргумента метода класса, управляемого CDI‑окружением, срабатывает автоматическая валидация и в интерфейсе появляются сообщения об ошибках:
Если ошибка имеет привязку к конкретному полю, за ее отображение отвечает отдельный блок:
<h:message for="f_message" errorClass="msg" />
если нет — она отображается через «глобальную свалку»:
Вместо текста сообщения, тут указан некий код, который автоматически заменяется на текст из специального ResourceBundle:
Согласно спецификации JSR303 название для бандла должно быть именно ValidationMessages.
Вся эта логика является частью спецификации 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> */ publicclass JSR303KlingonMessageInterpolator implements MessageInterpolator { // we need to have existing MessageInterpolator, // to being used as parent privatefinal MessageInterpolator delegate; public JSR303KlingonMessageInterpolator() { // take default implementation from JSR303 configuration this.delegate = Validation.byDefaultProvider() .configure().getDefaultMessageInterpolator(); } @Override publicString 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 publicString interpolate(String string, Context cntxt, Locale locale) { LOG.log(Level.INFO, "interpolating {0} with locale: {1}", newObject[]{string, locale.toLanguageTag()}); // here will be extracted and substituted value finalString 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);
Основная магия логика заключается вот в этих строках:
.. finalString 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-файле:
Который находится в файле src/main/resources/META-INF/validation.xml
Последней интересной темой, достойной освещения в рамках статьи про локализацию будут фантастические даты.
Заметьте — не просто фантастический формат отображения а целый календарь!
Фантастические даты
Никогда не задумывались какой смысл закладывается в дату?
Что такое на самом деле 2024й год?
Фактически это означает что прошло 2024 года с рождения Иисуса Христа (по новому летоисчислению), что возможно не очевидно некоторым представителям молодого поколения, но вполне достаточно для жизни и работы цивилизации.
А что если вам надо использовать альтернативную систему расчета времени?
Миллион лет от последнего динозавра?
40 000 лет бесконечной войны?
Озадачившись данным вопросом, я решил что неплохо было бы реализовать для фантастического языка еще и фантастическое летоисчисление. И использовать его для обычного корпоративного приложения, да.
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; importjava.util.Date; import java.util.Locale; /** * A custom converter for StarDate * @author alex0x08 */ @FacesConverter(value = "stardateConverter") publicclass 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 publicString 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‑шрифт для вашего языка и проверить его отображение в системе и браузере — если планируется веб‑разработка.
Следующим шагом необходимо реализовать либо взять готовые правила транслитерации вашего фантастического языка символами существующего — кириллицей, латиницей и так далее. И написать соответствующий транслятор символов.
Вся дальнейшая работа сведется к включению транслятора в ключевых местах проекта.
Я планирую начать учиться на программиста, но не знаю что лучше купить, макбук или windows ноутбук (планирую им пользоваться минимум 4 года*). У меня айфон и айпад, поэтому я склоняюсь к макбуку, но я предполагаю, что большинство программ для кодинга создано под windows. Вот лучшие варианты, которые я нашел до 800 долларов (60-70к рублей): Apple MacBook Neo 13’ 2026 (RAM 8 GB, SSD 512 GB, A18 Pro, MacOS) и Lenovo Lecoo Pro 14’ 2025 (RAM 32 GB, SSD 1024 GB, Intel Core Ultra 5-125H, Intel Arc Graphics, Windows Home). По поводу мышки - logitech mx anywhere 3s или logitech mx master 3s или logitech mx master 4 - но не знаю какая между ними разница, кроме цены + коврик logitech mx. Также посоветуйте какое направление программирования (или хотя бы какой язык) самое актуальное для обучения для высокооплачиваемой работы в будущем как в России, так и за границей.
В 2026 году в React появился хук use(), который меняет подход к работе с асинхронными данными и контекстом. Он входит в состав React 19 и уже доступен в стабильной версии.
Проблема, которую решает use()
Раньше загрузка данных в компоненте требовала написания однотипного кода:
useState для хранения данных
useState для состояния загрузки
useState для ошибки
useEffect для выполнения запроса
Ручное обновление всех состояний
Этот подход работал, но создавал много шаблонного кода и размазывал логику по разным хукам.
Как работает use()
use() -это хук, который принимает промис и «разворачивает» его прямо в теле компонента. Если промис ещё не завершился, React приостанавливает рендеринг компонента. Когда промис резолвится, компонент перерендеривается с полученными данными.
import { use, Suspense } from 'react'; function UserProfile({ userId }) { const user = use(fetchUser(userId)); return <div>{user.name}</div>; }
Здесь fetchUser(userId) возвращает промис. use() блокирует рендеринг до тех пор, пока этот промис не будет разрешён.
Роль Suspense
Для корректной работы use() необходимо использовать компонент Suspense. Он отлавливает состояние «приостановки» дочернего компонента и показывает fallback-интерфейс.
function App()
{ return
( <Suspense fallback={<div>Загрузка...</div>}>
<UserProfile userId="123" />
</Suspense> ); }
Пока UserProfile ожидает данные, пользователь видит сообщение «Загрузка...». После завершения запроса fallback заменяется на готовый UI.
Обработка ошибок
Ошибки, возникающие внутри use(), не обрабатываются автоматически. Для их перехвата используется ErrorBoundary — компонент, который ловит ошибки в дочерних компонентах.
<ErrorBoundary
fallback={<div>Ошибка загрузки</div>}>
<Suspense fallback={<div>Загрузка...</div>}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
Это стандартный для React подход к обработке ошибок.
Детали реализации
use() не создаёт новый запрос при каждом рендере. Если переданный промис уже был разрешён, use() возвращает данные мгновенно.
use() можно вызывать условно. В отличие от useContext, use() не требует, чтобы хук вызывался всегда в одном и том же порядке.
use() можно использовать не только с промисами. Он также работает с контекстом, что делает его универсальным инструментом для чтения данных в компоненте.
Код сокращается в 3–4 раза, логика становится декларативной.
use() — это новый инструмент в экосистеме React, который делает код более читаемым и предсказуемым. В сочетании с Suspense и ErrorBoundary он позволяет строить компоненты, которые описывают что должно отображаться, а не как это должно загружаться. Это шаг в сторону декларативной модели, к которой React стремится с момента своего появления.
Если вам интересна тема веб-разработки, я также публикую разборы других кейсов в своем Telegram-канале и на Максе. Буду рад единомышленникам.