?????, или добрый день по-японски.

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

Поэтому далее мы пошагово рассмотрим процесс адаптации консольного приложения для 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.