?????, или добрый день по-японски.
Как бы не был популярен английский язык, всё же пользователям комфортнее и привычнее в родной языковой среде.
Поэтому далее мы пошагово рассмотрим процесс адаптации консольного приложения для Linux на Kotlin/Native к русской и английской локали.
Поможет нам в этом старый-добрый GNU gettext.
В итоге убедимся, что это совсем не страшно.
Заодно посмотрим интеграцию с библиотеками на C, которая значительно расширяет возможности Kotlin/Native.
Что напишем: переводчик количественных числительных на японский язык.
Что ожидается от читателя: знание языков программирования Kotlin, C, базовый уровень знакомства с ОС Linux (в частности Bash).
Что понадобится в процессе: любой дистрибутив Linux, любая версия IntelliJ IDEA, установленный пакет gettext-devel или аналогичный.
Мотивация
Собственно почему Kotlin/Native и причём тут японский
Где-то полгода медленно и печально изучаю японский язык по учебнику “Minna no Nihongo”: частью ради перевода текста песен, частью просто из интереса к культуре.
Позже решил перейти с системной разработки на прикладную, с десктопа на мобильные платформы, соответственно с C++/Qt/STL на Kotlin/JVM/Android SDK.
Теперь хочу два этих занятия совместить, написав для себя программы для помощи в изучении японского. Конечно, уже много готовых, но NIH-синдром ведь не что-то плохое, правда?
Раньше я не задумываюсь использовал бы связку Qt/QML/C++: она позволяет быстро и эффективно решать в общем-то любые задачи и на любой платформе.
Однако Qt всё больше поворачивается спиной к Open Source, вот и решил пора валить попробовать что-то другое.
И тут в процессе изучения Kotlin узнал про Kotlin/Native.
Соответственно, первая программа-помощник будет именно на нём.
Что такое Kotlin/Native
Изначально Kotlin выступал в качестве "более лучшей" (С) Java, и целиком опирался на платформу JVM.
Однако затем компания JetBrains, разработчик Kotlin, решила адаптировать набирающий популярность язык и под другие платформы.
В частности, под веб (Kotlin/JS) и под "натив" (Kotlin/Native).
Kotlin/Native появился в марте 2017 года, кстати ровно четыре года назад.
Он позволяет компилировать код на Kotlin в нативный код с помощью LLVM, без зависимости от виртуальной машины JVM и других библиотек.
Далее мои субъективные впечатления по опыту разработки демонстрационного приложения, так что буду рад исправлениям и дополнениям.
Из плюсов:
исходный код под пермиссивной открытой лицензией (Apache-2.0 license)
работа на всех поддерживаемых LLVM платформах, в частности iOS и десктопы
генерирует единственный исполняемый файл без сторонних зависимостей
низкое потребление системных ресурсов
доступны все базовые "вкусности" Kotlin вроде коллекций и функционального программирования
есть прозрачный interop с языком C
как следствие: доступны low-level функции платформы, в частности POSIX
Из минусов:
стандартная библиотека весьма бедная по сравнению с огромной JVM, многое придётся писать с нуля (пример: оставьте только пункт Native тут)
нет стабильной библиотеки для GUI, хотя есть некоторые привязки через C interop (GTK, libui)
слабый инструментарий по сравнению с той же Android Studio, например нет той же локализации
относительно долгое время сборки
Больше о Kotlin/Native можно почитать тут, а примеры доступны на Github.
Терминология
Интернационализация (i18n): подготовка приложения к локализации, обычно выполняется разработчиками.
Локализация (l18n): Процесс перевода и адаптации контента приложения для конкретных языков, обычно выполняется переводчиками.
Важно: "контент" тут это не только строки, но и направление текста, формат даты, чисел и так далее. В данной статье ограничусь только строками.
Что такое GNU gettext
Этот пакет из нескольких утилит, библиотек и регламентов.
Является частью GNU Translation Project.
Состоит из:
правила оформления исходного кода для последующей интернационализации
утилиты для генерации текстовых файлов с локализуемыми строками
кроссплатформенная нативная библиотека для извлечения переводов в runtime
правила дистрибуции бинарных файлов с переводами
Почему GNU gettext
Интернационализация в Kotlin/JVM для Android использует средства Android SDK, в частности строковые ресурсы, и завязана на JVM.
Поэтому для Kotlin/Native эти средства недоступны.
В Qt есть собственный инструментарий, но его не получится использовать вне Qt, тем более с отличным от C++ языком.
Поэтому остаётся GNU gettext:
универсальный (поддерживается множество языков программирования)
кроссплатформенный (Win/Mac/Linux, есть Android/iOS версия)
стабильный в силу почтенного возраста
с подробной документацией
со вспомогательными приложениями
Демонстрационный проект
Суть: консольная программа пока только под Linux, чтобы не переусложнять код.
Функционал: читает натуральное число из аргумента командной строки или stdin, и переводит его в количественное числительное на японском.
Число может содержать специфичные для локали разделители тысяч, которая программа выводит при запуске.
Скачать проект можно на Github и затем открыть в IntelliJ IDEA.
Характеристики исполняемых файлов
Для начала время полной сборки: ~18 сек на конфигурации Ryzen 3900X + 32GB DDR4-3600 + NVM-E SSD. На мой взгляд многовато для такого маленького проекта и такой конфигурации.
Тут можно вспомнить о преимуществах скриптовых языков, которые компилировать не надо.
Теперь посмотрим свойства исполняемого файла для отладочной и релизной конфигураций:
Размеры скомпилированных исполняемых файлов
$ file build/bin/native/debugExecutable/JapaneseNumeralTranslator.kexe
build/bin/native/debugExecutable/JapaneseNumeralTranslator.kexe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.16, BuildID[xxHash]=a0971dbf76e9db60, with debug_info, not stripped
$ ldd build/bin/native/debugExecutable/JapaneseNumeralTranslator.kexe
linux-vdso.so.1 (0x00007fff890d7000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f348e47a000)
libm.so.6 => /lib64/libm.so.6 (0x00007f348e334000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f348e312000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f348e2f7000)
libc.so.6 => /lib64/libc.so.6 (0x00007f348e12c000)
/lib64/ld-linux-x86-64.so.2 (0x00007f348e4a0000)
libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f348e112000)
libutil.so.1 => /lib64/libutil.so.1 (0x00007f348e10b000)
libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007f348e0d1000)
librt.so.1 => /lib64/librt.so.1 (0x00007f348e0c6000)
$ file build/bin/native/releaseExecutable/JapaneseNumeralTranslator.kexe
build/bin/native/releaseExecutable/JapaneseNumeralTranslator.kexe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.16, BuildID[xxHash]=c76aff5e0db3fdae, not stripped
$ ldd build/bin/native/releaseExecutable/JapaneseNumeralTranslator.kexe
linux-vdso.so.1 (0x00007ffff69c2000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f41ad9dd000)
libm.so.6 => /lib64/libm.so.6 (0x00007f41ad897000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f41ad875000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f41ad85a000)
libc.so.6 => /lib64/libc.so.6 (0x00007f41ad68f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f41ada03000)
libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f41ad675000)
libutil.so.1 => /lib64/libutil.so.1 (0x00007f41ad66e000)
libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007f41ad634000)
librt.so.1 => /lib64/librt.so.1 (0x00007f41ad629000)
$ ls -lh build/bin/native/debugExecutable/JapaneseNumeralTranslator.kexe
-rwxr-xr-x. 1 eraxillan eraxillan 1.8M Mar 7 13:24 build/bin/native/debugExecutable/JapaneseNumeralTranslator.kexe
$ ls -lh build/bin/native/releaseExecutable/JapaneseNumeralTranslator.kexe
-rwxr-xr-x. 1 eraxillan eraxillan 529K Mar 7 13:24 build/bin/native/releaseExecutable/JapaneseNumeralTranslator.kexe
Тут всё в порядке, бинарники скромные по размеру и без каких-либо сторонних зависимостей.
Отладка проекта
Она не работает, по крайней мере в Community Edition.
Просто игнорируются точки останова, хотя судя по выводу команды file отладочные символы в исполняемом файле есть.
Насколько я понял, соответствующий плагин доступен только в платной редакции IDE, которая пока для меня избыточна.
Прошу поправить, если ошибаюсь.
Так что для написания демо пришлось обходиться printf-driven отладкой, ну мне не привыкать после Android AOSP.
Интернационализация
В случае нашего проекта нужно лишь все локализуемые строки "обернуть" в вызов функции gettext().
Для краткости можно сделать синоним этой функции, например tr(), это общепринятая практика.
import kotlinx.cinterop.*
import platform.linux.*
import platform.posix.*
fun tr(key: String): String = gettext(key)?.toKString() ?: ""
Понадобится ещё служебный код для работы с POSIX-локалями, он находится в файле Locale.kt.
Также мы должны заранее определиться с количеством поддерживаемых языков: для демо это будут только английский и русский, плюс числительные на японском для любого языка.
Кстати, посмотреть названия локалей можно с помощью команды locale -a.
Перевод локализуемых строк
Я написал Bash-скрипты для генерации с нуля и обновления файлов gettext:
Далее вкратце опишу основные шаги, которые они выполняют.
Генерируем pot-файл ("шаблон"), который содержит базовую информацию о программе и собственно строки нуждающиеся в переводе.
# Extract all tr() wrapped strings to po/jnt.pot file
xgettext --keyword=tr --language=java --add-comments --sort-output --copyright-holder='Alexander Kamyshnikov <axill777@gmail.com>' --package-name='Japanese numeral translator' --package-version='1.0' --msgid-bugs-address='axill777@gmail.com' -o po/jnt.pot --files-from=KT_FILES
Генерируем po-файл (текстовый перевод):
# Generate locale sources
# NOTE: --no-translator option is a workaround to supress email input request
msginit --no-translator --input=po/jnt.pot --locale=en_US.UTF-8 --output po/en_US/jnt.po
msginit --no-translator --input=po/jnt.pot --locale=ru_RU.UTF-8 --output po/ru_RU/jnt.po
Генерируем mo-файл (бинарный перевод):
# Generate locale binary files
msgfmt --output-file=po/en_US/jnt.mo po/en_US/jnt.po
msgfmt --output-file=po/ru_RU/jnt.mo po/ru_RU/jnt.po
Развертывание бинарных файлов с переводами
К сожалению, Kotlin/Native не поддерживает ресурсы, так что "упаковать" mo-файлы в исполняемый файл пока не выйдет.
На это есть соответствующий баг.
Думаю, функционал ресурсов можно реализовать и вручную дополнительной задачей в Gradle, это пожалуй тема для отдельной статьи.
Для тестового проекта положим mo-файлы рядом с приложением, где не нужны права суперпользователя и где gettext сможет их найти.
Для релиза приложение следует упаковать в RPM/DEB-пакет, а mo-файлы установить в директорию /usr/share/locale
.
Итог
Как видите, процесс несложен, по крайней мере при наличии готового кода и скриптов.
В процессе разработки нужно лишь периодически вызывать update_localization.sh, переводить новые строки, и снова вызывать этот скрипт для генерации mo-файлов.
????? ?????, или спасибо за внимание!
Источники
Определения взяты из документации Django
Почему интернационализация и локализация имеют значение
Kotlin Native: следите за файлами
DxGetText — GNU Gettext for Delphi and C++ Builder
P.S.: дальше планирую использовать Kotlin/Native уже в рамках кроссплатформенных библиотек. Если будет интерес, могу доработать демо, например портировать на Windows.