How to localize the app into 55 languages and not go crazy?
From selecting which languages to target, to translating into every language, and to RTL support, there are many challenges when it comes to internalization. Here's how we approached them.
I don't think anyone will be surprised when I say I love to travel. Seeing new places, experiencing new cultures, and learning about local history are among my favorite things in the world. This passion goes hand in hand with learning foreign languages, especially when I can understand bits and pieces of a new language. When we first visited Portugal, I was surprised at how much written Portuguese I could understand (thanks to my Spanish studies). While the spoken language was quite different, I could make sense of many written words.
This wasn't the case when we visited Budapest, Hungary's capital. Before the trip, I knew practically nothing about Hungary or Hungarian, but for some unknown reason I had this naive feeling that being a native Russian speaker, I might find some connection to Slavic languages. Lol, just how wrong I was about that. I couldn't understand a single word—the signs were completely incomprehensible. Then, drawing from my programming background, I remembered something called "hungarian notation" and decided to look up whether it had any connection to this confusing language.
I then went down the rabbit hole to learn that Hungarian belongs to the same language family as Finnish (of all languages!), but this fascinating story isn't why you're reading this newsletter.
Speaking of languages, apparently navigating Hungarian wasn't challenging enough, so I decided to localize my language learning app Native Pal into 55 languages. That was fun (if your definition of fun involves a bit of masochism). That's why today I want to tell you about the challenges we encountered on this journey.
#1 How did we even come up with 55 languages?
For those new to Native Pal, it's an AI chat-based language learning app where you chat with characters, receive tips, error analysis, suggestions, and audio messages—with more exciting features coming soon. We initially launched with 9 languages, including some popular ones, a few requested by my Instagram followers, and Latvian (since I'm from Latvia). The base language was English (yeah... teaching English through English wasn't my brightest idea 😅).
We chose OpenAI as our initial AI provider, and since we planned to implement text-to-speech and speech-to-text features, we decided to support all languages that OpenAI offered. The more the merrier, right?
However, this decision somewhat backfired when we implemented text-to-speech. While the models can technically "speak" these languages, they're optimized for English and often have strong accents—which defeats the purpose of learning from a "native" pal. But that's a story for another newsletter 😁
#2 Which flags to show for multi-country languages?
Dealing with countries is complex due to political sensitivities. When implementing country selection for custom pals, we discovered that while most countries were straightforward to include, some had complicated recognition status or disputed territories. Given these challenges, we chose to temporarily skip this feature altogether.
With languages though, we had to make a choice. For example, we decided to show the Spain and Portugal flags for Spanish and Portuguese, rather than flags from LATAM countries. This is because we don't yet differentiate between regional variants of these languages, though we plan to add this support in the future.
Arabic presented another challenge since it's spoken across many countries. I researched how other apps handle this, including Duolingo, and noticed they all use the Saudi Arabian flag. So I did the same. However, my native speaker friend pointed out that this wasn't very inclusive. She told me about a better alternative—a symbol that represents the Arabic language itself: a letter called "dhad" that has a unique sound found only in Arabic. I'm excited to meet her in person soon to hear how it's pronounced! 😁
Learning about cultural nuances like this is one of the perks of working on a language-learning app! If you notice anything in Native Pal that could be improved from a cultural perspective, please let me know—I'll be happy to fix it.
#3 In what language to display… the language name?
With 9 supported languages, the display order isn't crucial, especially with a single localization. However, when an app supports 55 languages and is localized into all of them, users need an efficient way to find their desired language. We considered several options:
Display each language in its native form (e.g., "English, Español, Latviešu") with a static list. Can be an issue if you don’t know how your target language is spelled in its native script, for example if you’re a complete beginner.
Translate and sort language names alphabetically in each native language. This requires extensive translation work.
Use a combination approach: show both the native language name and its translation (e.g., "Spanish (Español)").
I think all of these approaches would work perfectly fine, for me it felt more consistent to have everything in the native language and sort alphabetically, so I went with option #2. But I think it’s mostly a matter of preference.
Here is how it looks in my app:
#4 How to localize the app into 55 languages and not go crazy?
There are 2 aspects to language support:
The target language that users are learning. We could have simply expanded from 9 to 55 languages with English as the base, but that would be too easy, right? Many potential users want to learn a new language but don't speak English—including those wanting to learn English itself.
The base language. This doesn't necessarily need to be your native language. For instance, though Russian is my native language, I prefer learning Spanish through English. This base language serves for tips, error analysis, and the app's interface.
From the technical perspective, I kept it simple and followed the official documentation. This approach has worked well so far, though time will tell how it holds up. The real adventure began with the actual translations.
I remember the good bad old days when we'd copy strings to the client's Excel file for translation. They would use the built-in Google Translate function (which, let's be honest, is more of a placeholder than a real solution for full sentences) and copy everything back to the project files. This process was tedious even with just 5 languages—now imagine 55! We later explored services like Weblate and Lokalise, and even considered building our own translation service to save money (typical developer thinking: "I'd rather build it than pay for it" 😂).
For Native Pal, I knew from the start that I'd use LLMs for translations since they're quite good at it, but I dreaded having to copy-paste 55 files back and forth to ChatGPT—and for every new feature 🤪 I considered automating it with a script, but then I remembered everyone on the internet was raving about Cursor, an AI-powered IDE. And honestly, it's been a game-changer. You simply select the code you need, tell it to translate, and it does so inline. No back-and-forth needed, and you can select your model (I use GPT since I know it supports my languages). It's super fast, almost seamless, and the translations are much more natural and appropriate than Google Translate. Of course, it's not perfect—being literal translations from English, they can sometimes sound awkward even if grammatically correct. But I'm also just an indie dev and can't hire 55 native speaker translators (yet) 😁 What a time to be alive!
I've tried Cursor for other boilerplate tasks—it works well and saves time—but I can't bring myself to use it regularly since I'm an IntelliJ girlie for life and Cursor is VSCode-based 🥲 For translations though, it's perfect since I only need to switch to it for that specific task.
#5 What if Material Localizations Don't Support a Language?
After the fascinating task of translating everything, the next step is just as thrilling—testing whether everything works (or doesn't). This includes fixing text overflows you might have missed, checking line count limitations, and discovering issues like this:
Material Components like tooltips and dialogs use built-in MaterialLocalizations
for localization. According to the docs, MaterialLocalisations
support many languages, but Maori isn't among them, which causes an error (or a million of them). While it can be fixed by providing custom localizations for an unsupported locale, I decided to simply disable Maori as a native language for now (it's still available as a target language). Sometimes you have to pick your battles, and at this point, I was pretty done with the whole localization business.
#6 How to handle remote localizations?
For custom pals, you can select their interests and characteristics.
These values are dynamic and come from the backend, allowing us to update them anytime without an app update. Since they're stored in the database as entries rather than static app strings, the Cursor hack wasn't an option. Instead, we created a script that translates the strings and writes them directly to the database. Just be VERY careful not to send the history of previous translations with every request—unless you want some surprising bills 😅
Another challenge was deciding which language to display the chips in—base or target? Initially, we planned to show them in the target language since users create pals in that language. However, we realized beginner learners might struggle to understand all the words. Our solution was to show them in the base language for beginner and intermediate learners, and in the target language for advanced learners. We might be overcomplicating things and maybe it makes sense to show it all in base language so that it’s the same as the interface language, but I’ll think again about this later.
Things get especially tricky when users are learning multiple languages, as they might prefer different base languages for different target languages. This raises questions about the appropriate interface language—should it follow the device locale? Should users select a base language whenever starting a new chat? We're still working on finding the best answers to these challenges and may adjust our approach based on user feedback.
#7 RTL support, but level hard (learning an LTR language in RTL interface and vice versa)
We support Arabic, Hebrew, and Urdu as native languages, which requires localizing the interface. These languages are written right-to-left (RTL). While Flutter handles RTL well out of the box, some extra work is needed. For example, the Row
and Column
widgets support RTL by using axis alignments like start
and end
(relative to layout direction) instead of left
and right
. However, not all widgets use this system, so you'll need to fix those. I followed this great article by LeanCode to implement RTL. For instance, widgets like Positioned
that use left
and right
params have directional alternatives like PositionedDirectional
. Even though my app isn't huge, refactoring from default widgets to directional ones affected many files. My advice? Build in this support from the start, especially with directional widgets. In my apps, I use a design system with custom widgets—like NPText
(NP for Native Pal, my preferred naming convention). I did the same for directional widgets, creating wrappers like NPPositioned
to help me remember to use the correct widget. For icons, you'll need to decide whether they should be mirrored by setting the matchTextDirection
property. I'm still not 100% sure I've done everything right, but I'm trying 🤪
While this required some work, it wasn't the end of the story. Everything works smoothly if your native language is LTR and you're learning an LTR language, or if both are RTL. But what about when your interface is in LTR and you're learning an RTL language? For me, this mainly involved handling text display (messages and tips) from the pal, plus the typing direction in a TextField
. Here's how I solved it:
For static texts, I'm using the
AutoDirection
widget that I took from here. It detects the text directionality with the help of BiDi functions from theintl
library and wraps it in aDirectionality
widget.For dynamic texts, I preset the directionality by checking the target language (
iso
) parameter:
TextField(
textDirection: LanguageMapper.isRTL(iso) ?
TextDirection.rtl : TextDirection.ltr
)
Here's how it looks in the end:
Since I'm not learning these languages myself and don’t know how well this works, I welcome any feedback on whether this implementation is correct and user-friendly. If you're a native speaker or learner of these languages, I'd love to hear your thoughts! 😅
These were the main challenges in what started as a "quick" addition of native language support. Had I known the full scope upfront, I might have postponed this 😂 But I'm glad I took it on—I learned so much along the way. Plus, now my baby brothers who don't speak English have no excuse not to use Native Pal 😁
As I said, the translation by AI is not always perfect. If you’re a native speaker and notice anything cringe-worthy in the app in your language, I’d appreciate your feedback to fix it!
I hope you enjoyed this newsletter! If you did, don’t hesitate to let me know by liking / re-stacking / commenting / subscribing :)
I have many things to talk about in the upcoming issues, including how supporting TTS (text-to-speech) of 55 languages was another very interesting challenge 😁 Until then, talk to you on Twitter and Bluesky!
Don’t forget to check out my apps here :)
-Daria 💙