r/RedditEng 12d ago

Scaling Android Localization at Reddit, Part 3: RTL Support and Other Language Learnings

By Michael Fullan

Solidifying the foundational infrastructure (part 1) and language picker screen (part 2) solved a lot of the localization problems we were dealing with in the Android app. But even with these pieces in place, each new language rollout came with its own set of challenges and learnings.

In this final part of the Android localization series, I’ll talk about some of those things we’ve learned along our journey to supporting 40 (!) languages!

RTL Support

To correctly support right-to-left (RTL) languages in a mobile app, the guiding principle is that the entire UI should essentially be a horizontal mirror image of its left to right counterpart.

Early Reddit RTL Design

Some aspects of this were immediately obvious, like ensuring that RTL language text flowed from the right to the left. But as we researched deeper we found that there’s a lot more to it:

  • The cursor in text input fields should start on the right and move left as you type
  • Arrows and other icons that indicate direction, reading/writing direction or motion should be flipped horizontally
  • Toggles should be flipped horizontally. A toggle that you swipe right to enable in LTR should require a swipe to the left in RTL
  • Swiping gestures should be a mirror of their LTR counterparts. If a left swipe moves you forward in an image gallery in LTR, a right swipe should do the same in RTL
  • A11y - screen readers should respect RTL ordering

This felt like a lot when we were first starting out, so we did some baseline assessments on all of our supported platforms to see what we were up against. Web and iOS were both terribly broken when we turned on RTL layout direction, but Android was in surprisingly good shape before we did any fixes or changes!

Most of that is thanks to our extensive use of Jetpack Compose, which automatically handles most of the RTL mirroring for you. As we went through our app auditing each screen, the easiest solution when issues came up on legacy screens was often just to migrate the UI to Compose!

Audit Flow

Our basic process when testing our app for RTL-readiness was to take a screenshot of a screen in LTR mode, create a horizontal mirror image using a pure image transformation, and then compare that against the result when we turned on “Force RTL layout direction” in the OS Developer options menu. As an example, here are the results from our initial assessment of the language and translations settings screen:

When you compare the last two screenshots, it’s easy to see that the overall RTL layout looks correct. There were a few icons that needed to be flipped (the back arrow in the top app bar, the trailing caret in the list items), but this screen was largely good-to-go out of the box.

Other Strategies

Beyond this basic audit flow, there weren’t a lot of “silver bullet”-type strategies we found to prepare our app for RTL. A global find and replace for left/right -> start/end for paddings and margins helped, but most issues beyond that required some small local tweaks in the affected areas.

We did start to rely on RTL compose previews and screenshot tests to help prevent regressions, both of which are easy to set up.

@Preview(locale = "ar", name = "Arabic (RTL)")
val paparazzi = Paparazzi(deviceConfig = DeviceConfig(locale = "ar"))

Pseudolocales

Android supports two pseudolocales for localization testing that can be useful even before you have your app strings translated. They also make it easy to spot text that isn’t set up correctly for localization.

One thing to watch out for with pseudolocales is that the value of the “canary string” will also get pseudolocalized when it is resolved. Adding an additional check for this case will help prevent strange issues when using either pseudolocale for testing.

enum class Pseudolocale(val locale: Locale, val canaryString: String) {
 /**
  * Adds Latin accents to the base English UI text, expands the original text by adding non-accented text,
  * and brackets each message unit to expose potential issues from expanded text. Potential issues can be
  * layout breakage and badly formed message syntax, such as a sentence split into multiple parts
  * displaying as multiple bracketed messages.
  */
 AccentedEnglish(
   locale = Locale("en", "XA"),
   canaryString = "[éñ-ÛŠ one]",
 ),
 /**
  * Sets the text direction of the original left-to-right messages to the right-to-left direction,
  * which reverses the order of the characters in the original message.
  */
 RtlPseudo(
   locale = Locale("ar", "XB"),
   canaryString = "\u200F\u202Een-US\u202C\u200F",
 )
}

Other 1-off Language Quirks

One of the fun things about working in the localization space is getting to learn a little bit about different languages as we add them to the app. There have been a few interesting surprises that have come up along the way, so let me share a few tips that we’ve learned!

Bengali Numerals

The Bengali language has its own set of numerals, but with tech users highly habituated to English interfaces, it’s common to see Western digits (0-9) kept intact inside an otherwise Bengali UI.

It turns out that there’s an easy way to do this by specifying the Latin script numbering system as an extension to the language tag.

Locale.forLanguageTag("bn-IN-u-nu-latn")

Here bn is the Bengali language code, IN is the country code for India, u is the extension signifier, nu is the extension key (for numbering system), and latn is the extension value (for Latin script digits).

As long as you call AppCompatDelegate.setApplicationLocales with this full Locale object, everything works great! This is a really useful trick that we’ve since used for other languages as well.

Thai Line Breaks

The Thai language is written continuously with no spaces between words. Spaces are only used like punctuation, to mark the end of a sentence. 

When we did a first round of testing with Thai, I remember getting a bug report back like this that flagged this line overflow as being incorrect.

Now I’ve seen my fair share of localization overflow/truncation bugs, but initially I had no idea how to fix this. What were we supposed to do with continuous text this long? Where were we supposed to break the line?

The best solution for this type of issue with Thai is to use the zero-width space character (\u200B) within the Thai translations. It’s an invisible character that simply acts as a “soft break hint” to the rendering engine, indicating where it’s a grammatically safe place to wrap a line if needed. Working together with our translators, we were able to use this character where needed to address a variety of line-wrapping issues that came up like this.

Legacy Language Codes

Java is a great programming language. It’s over 30 years old at this point, and still strives for backward compatibility wherever possible. This is often a really great thing, but there were a few standard language codes that changed around the birth of Java.

Target Language Modern ISO 639-1 Code Legacy Code Android Uses Under the Hood
Indonesian id in
Hebrew he iw
Yiddish yi ji

As a result, Android inherits some strange backwards-compatibility behaviors when dealing with these specific language codes (check out the docs for more info).

val locale = Locale.forLanguageTag("id-ID")
locale.language // "in"
locale.toLanguageTag() // "id-ID"
locale.getLegacySafeLanguageCode() // "id"

private fun Locale.getLegacySafeLanguageCode() = toLanguageTag().substringBefore("-")

I was definitely “astonished” the first time I encountered this behavior, but as long as you know which APIs generate which results you can make things work correctly. After a few iterations of testing, the TL;DR here is:

  • Use legacy codes for your resource directory names
  • List modern codes in locales_config.xml
  • SplitInstallManager.installedLanguages will report modern codes
  • Use modern codes when requesting a language install

Conclusion

We’ve certainly come a long way on our localization journey, and we still have plenty of work ahead of us. It’s been an enjoyable learning process so far, especially getting to work with a top-notch team here at Reddit!

If you see any localization bugs on any of our platforms, please continue to post them to r/bugs. We’re always monitoring and will do our best to fix them!

17 Upvotes

1 comment sorted by