ABS+ now speaks 22 languages: how I translated a 4 MB audiobook player and tested every one
ABS+ 1.2.4 translates the whole Audiobookshelf player into 22 languages, with right-to-left Arabic, translated download errors and a clear certificate message. How each language is tested, and what it cost in app size.

On this page
ABS+ 1.2.4, released on October 28, 2026, translates the whole app into 22 languages: English plus 21 more, from Arabic with a proper right-to-left layout to Ukrainian. The app follows the language of your phone, including the download errors and the message you get when your server’s certificate is rejected. The Android APK grew from 3,939,186 to 4,182,014 bytes for it, about 243 KB or 6.2 percent. This post is what I translated, how I checked that every language is complete, and the places where a translation is more than swapping words.
Which languages ABS+ speaks now
ABS+ is a small player for people who run their own Audiobookshelf server, so its users are spread across the world and mostly open the app on a phone that is not set to English. The 22 languages are English, Arabic, German, Spanish, French, Hindi, Indonesian, Italian, Japanese, Korean, Dutch, Polish, Brazilian and European Portuguese, Russian, Swedish, Thai, Turkish, Ukrainian, Vietnamese, and Chinese in both Simplified and Traditional script. On Android each language has its own strings.xml with 137 strings and 9 plural sets. The iPhone app carries the same 22 languages in its string catalog.
The store listings went with it. The Google Play changelog for 1.2.4 exists in 24 locales (the 22, plus British English and Latin American Spanish are separate Play locales), and the App Store listing text, keywords and screenshots are written per market rather than machine-copied from English. The repository has a store-text checker that fails when a locale is missing, so a language can’t quietly ship without its text.
How I check that no language is half done
A translation can look finished and still crash a screen, because a string like “%1$s downloaded %2$d of %3$d” needs its arguments in a language whose word order puts them in a different place. I wrote one test, LocalizationTest, that does four things for every language:
- It compares the set of string names in each language file with English and fails on any that is missing or extra.
- It formats every string with sample arguments matched to the English placeholders and checks that each text argument shows up in the result. A translation that drops
%1$sfails. - It runs every plural through eight sample counts (0, 1, 2, 3, 5, 11, 21 and 100) so a missing plural form can’t hide.
- It fails a language if more than a tenth of its strings are identical to English, which is how an untranslated placeholder file gets caught.
The same test file also checks that the Android locale picker maps phones correctly. A phone set to zh-HK or zh-TW gets Traditional Chinese, zh-CN and zh-Hans-HK get Simplified, es-MX falls back to Spanish, and Indonesian works under both its modern code (id) and Android’s legacy one (in). European and Brazilian Portuguese stay separate, and the test asserts that their sign-in hints differ.
What a translation changes besides the words
Plurals are not “one or many”. English has two plural forms. Russian and Ukrainian need four, and Arabic needs six: zero, one, two, few, many and other. The Arabic file has 54 plural items for the same 9 plural sets that English writes in 18. Android handles this if you supply every form, which is exactly what the test above forces.
Arabic reads right to left. The manifest declares RTL support and the test asserts that an Arabic phone gets a right-to-left layout direction. The playback timeline got its own fix in this release (commit “RTL media controls and localization polish”): the mini player’s progress bar and the seek slider are explicitly left to right, because a timeline moves forward the same way in every language and shouldn’t be mirrored with the rest of the screen. A related detail from the same test: a list of two names reads “Alice and Bob” in English, “Alice и Bob” in Russian, and uses the Arabic conjunction in Arabic, because the app asks the system’s list formatter instead of joining strings with a comma.
File sizes follow the locale. A 5,000,000-byte download shows as “MB” in English and “МБ” in Russian and Ukrainian. Before this release the app built “MB” and “GB” from its own strings. It now asks Android’s size formatter, which picks the unit for the UI language, and the test checks that Russian and Ukrainian show “МБ” and that Arabic doesn’t show the Latin “MB”.
Download errors are stored as reasons, not sentences
The bug I’m gladdest I found is a quiet one. Before 1.2.4, when a download failed, the app stored the error on the download job as a finished sentence, such as “Couldn’t download (HTTP 404)”, built in whatever language was active at that moment. Translating the app is not enough if a stored sentence keeps the old language after you change your phone’s setting.
The fix is a small sealed class in Dl.kt, Halt, with four cases: paused, sign in again, certificate rejected and failed with an HTTP code. The job stores the case, and the text is built from the current language at the moment it is shown. A test renders each case in English and German and asserts they differ, and that a failure with code 404 reads “Couldn’t download (HTTP 404)” in English and uses the German string in German. If you build anything that stores an error for later, store the reason and translate it on the way out.
A certificate problem now says so
The other fix in this release came out of the same work. When a server’s certificate is self-signed or expired, ABS+ used to say it couldn’t reach the server, which sends people to check their Wi-Fi when the real problem is the certificate. Now it says, in your language: “Couldn’t connect securely to the server. Check its certificate.”
The tricky part is not telling the difference when the certificate really is wrong; it is not blaming the certificate when the connection was simply cut. The check walks up to 16 levels of the exception’s cause chain and counts only certificate failures: an untrusted chain, a hostname mismatch or an expired certificate. A TLS connection reset halfway through a transfer, or closed by the peer during the handshake, still reads as “couldn’t reach the server” and the download retries by itself. The walk is capped, and a test covers a cause cycle, so a malformed exception can’t hang the check. The last test signs in against a real local HTTPS server with a self-signed certificate and asserts the message.
Where you can get it
The 1.2.4 APK is on the release page and as absplus.apk on GitHub releases. When I checked the App Store listing on October 30, it still showed 1.2.3 in English only, so the iPhone build with all 22 languages follows once the store publishes it. ABS+ is free, open source and has no ads, analytics or account; it only talks to the Audiobookshelf server you sign in to. You can read about it at absplus.app.
If you want a language added, or you spot a translation that sounds wrong, open an issue in the repository. Every string is a plain text file, which makes corrections cheap to review.
Written October 30, 2026 from the v1.2.4 source and release assets.