How we built Localazy CLI: Kotlin MPP and Github Actions

Search for a command to run...

No comments yet. Be the first to comment.
Are you looking for a translation API to use in your new project? We curated a list of the 18 most popular translation service providers’ APIs available on the market. Maybe you’ll find a match! Translating your content online hasn’t ever been easier...

Learn why Spanish is an important language to consider and why it might be one of the first to choose when you start with the localization of your product. There’s no wonder Spanish is the most popular language on our platform! 📖 Introduction Before...

Learn why idioms are difficult to translate correctly, how to identify them in the text, and figure out their meaning to come up with the best translation in this article. Translators know very well that idioms aren't a piece of cake to translate. 🧁...

This short tutorial will help you set up a scalable Nuxt 3 boilerplate for small and large projects. Find out more about the configuration that Localazy uses for their website and build your next project on proven and solid ground. Nuxt 3 is nearing ...

Localazy can take complete care of your translation process with the built-in translation services available inside the platform. Forget the hassle of managing translation projects forever. All you need to do is choose the language and service. Learn...

vaclavhodek's blog
94 posts
Entrepreneur, idea maker, developer, saas & mobile enthusiast. Building developer-friendly localization solution at localazy.com.
How we built Localazy CLI for our developer-friendly software localization tool for smart & lazy developers :-).
Initially, Localazy supported only Android through our simple to use but potent Gradle plugin that automatically handles everything for the developer. Our vision of developer-friendly software localizations proves to be the right way to go. We started to receive a lot of requests to support other platforms too.
Our accurate shared memory ShareTM, proactive review, and intuitive interface were compelling reasons to use Localazy also for developers outside the Android ecosystem, even if the integration would be less sophisticated than the Gradle plugin.
We thought about the right solution that would enable other developers to use Localazy on all platforms. We wanted to keep the developer-friendly / developer-centric approach and CLI, aka the command-line interface, was the logical answer.
We wanted Localazy CLI to be supported on all major platforms. Ideally, we wanted no dependency on Java or Node as those usually mean a huge binary or giant virtual machine. Small and fast binary for a given operating system looked like a perfect solution.
Kotlin is already heavily used at Localazy, and it powers the whole backend. And so Kotlin Multiplatform was a logical choice to look at for this task. It allowed us to write a single codebase for native Linux, macOS, Windows and Java in a programming language we know well.
Almost all the code is shared between platforms and written in pure Kotlin. There were only two platform-dependent features - filesystem access and HTTP communication.
The filesystem access is quite simple, and we just create an actual implementation for Java (using java.io.File and streams), Linux/macOS (using POSIX) and Windows (slightly modified POSIX as there are small differences on Windows).
For HTTP communication, good old HTTPUrlConnection is used for Java and ktor client for Linux, macOS and Windows.
The prototype of CLI was ready in a couple of days, and with a bit of fine-tuning, we were able to compile it to Java’s JAR and native binary for Linux, macOS and Windows.
Everything seemed to be great, except it wasn’t…
Well, everything worked well, but the ktor client introduced a dependency on libcurl. It’s not a big deal on Linux or macOS as you either already have libcurl installed or you can install it with a single line through APT, YUM or Brew.
On Windows, however, it was necessary to include almost 20 DLL files in the same directory where the CLI binary is placed. Ough, that’s not good, and we didn't want that for sure.
Create an installer that installs DLL files to the system folder. No! This doesn’t seem to be the simple developer-friendly solution in a spirit download and go, and I didn’t like the idea of installing anything on the system level.
Statically link libcurl to the binary. Several days lost trying to get this work. Partially, we were able to do so, but the resulting binary was huge, and it still needed some dependencies. We even recompiled the libcurl from the source, so it didn’t depend on OpenSSL and other libraries and rather use Windows native subsystems. No luck.
Rewrite the whole HTTP connection part to use WinInet - Windows native communication stack. Well, it meant a lot of work and another implementation, but it’s worth it as the resulting binary is small and has no dependencies. And so we did.
We tweaked Gradle build script, so it works in two different modes - when IntelliJ is active and when the final build process runs.
When IntelliJ is active, only the selected platform based on the host OS is used. This setup allows us to develop the CLI easily, and there is only one compilation target with no additional operations.
When the full build is run, the binary for the respective platform is compiled, packaged, renamed and placed into the correct folder. Additional operations are performed for optimising the binary size. For the Linux host, not only the native binary but also the Java version is built, a binary for Docker image is prepared, and DEB and RPM packages are generated.
The big drawback of Kotlin MPP is cross-compilation. It’s currently possible to build some targets only on specific host platforms. Windows binary can only be built on the Windows host. The same applies to macOS.
At that moment, we already planned to use Github Actions for compilation, but as we had our own Linux based runner, we wanted to cover as much of the build process as possible using it. Even without our own runner, Linux actions are cheaper.
The second reason for cross-compilation was that I simply didn’t have Windows and cross-compilation allowed me to check that everything is alright and I could still be able to test binary with virtualized Windows. Technically, I could build the binary in the virtual machine but for the reason above, I was more interested in some kind of cross-compilation.
There is no solution for macOS. It simply needs the macOS host.
For Windows, however, there is a solution. Install Wine in Ubuntu-based docker and unpack the Windows version of OpenJDK, so it’s accessible from Wine. Be sure to set JAVA_HOME and PATH too. Invoking ./gradlew.bat fails due to missing TTY, but there is a simple workaround:
faketty () {
script -qfec "$(printf "%q " "$@")"
}
faketty wine cmd /c gradlew.bat --no-daemon --console=plain clean buildWindows
And that’s it! The Windows binary can be built on the Linux host.
Once the binaries for all platforms were built, we needed to make them available for you.
We have two Actions - one that is run on macOS and one that is run on our self-hosted Linux runner. Actions are bound to a release, so whenever we want to release a new version, we just create a tag and release on Github. The name of the release is automatically used as a version identifier.
So, we change the code, test it, commit, push and create a release on Github. And that’s it. Everything else is taken care about automatically including changes on the website. Less manual work and repetitive tasks, less human errors :-).
Personally, I love how a few minutes after creating a release on Github, invoking apt update && apt upgrade makes sure that you are running the latest version of the Localazy CLI ;-).
Even we don’t remember all details about our configuration file localazy.json that is used by the CLI. Thankfully, my colleague Jan spent quite some time tweaking JSON schema for the file and so you can enjoy smart completion in supported IDEs. We tested on Visual Studio Code and IntelliJ IDEA and both work like a charm.
It needed some time to study it, create it and distribute it, but it simply makes our tool more developer-friendly and that’s important. We really strive to bring you a great tool and not only to fill some missing gap. Details matter.
We are now in the process of obtaining a certificate from Comodo, so we can sign the Windows binary and get rid of warnings about a possibly dangerous unsigned app.
It seems that we can sign the Windows binary on Linux, so it’s going to be part of the Github Action once we have the certificate available.
Kotlin MPP is a viable solution for building CLI apps - it only needs some tweaks to support all platforms - and with Github Actions, it’s possible to automate everything.
It's important to have as much of the logic as possible in the common module.
However, thanks to JetBrains for Kotlin, it was fun to write the CLI, and it works smoothly on all platforms.
This article has been originally published as How we built Localazy CLI: Kotlin MPP and Github Actions on localazy.com.