Solo i18n: 1993 keys, 6 languages, 9 LLM passes
Key takeaways: Internationalizing a mobile app with 1993 translation keys in 6 languages is achievable solo thanks to LLM automation. The npm run translate script detects the delta, translates only new keys via OpenRouter, and maintains near real-time synchronization for a few cents per pass. The result: a 6x increase in acquisition channels without continuous human effort.
TAMSIV was born in French. Every string, every error message, every placeholder — everything was hardcoded in JSX. When I decided to support 6 languages (FR, EN, DE, ES, IT, PT), I didn't realize it would represent 1993 translation keys spread across 35 files. And even less that I would need 9 successive passes to achieve a stable result.
Here's the full story of this i18n adventure, from the first t('key') to the automated pipeline running today.
Why internationalize a solo indie app?
The short answer: user acquisition. An app available only in French deprives itself of 95% of the global market. Even in Europe, limiting your audience to French means ignoring Germany (83M inhabitants), Spain (47M), Italy (60M), Portugal (10M), and of course, the entire English-speaking world.
But beyond the market, there's a technical reason: stores (Google Play, App Store) index metadata in each language. A listing translated into 6 languages means 6 times more organic discovery surface. This is a lever recommended by Google itself.
For a solo developer like me, it's also a competitive advantage against large apps that neglect localization beyond English. If a German user searches for "Aufgaben-App mit Spracheingabe" and TAMSIV is the only one with a German listing, it's a win.
How to extract 1993 translation keys without losing your mind?
First step: replace each hardcoded French string with a t('key') call. In 35 files. Manually. There's no reliable shortcut for this step — each string has a context, and the key naming must reflect that context.
I structured the keys by screen and section:
feed.empty_state— the message when the feed is emptyagenda.filter.participating— the "Participating" filter in the agendaprofile.settings.language— the language selector in the settingsdictaphone.recording.title— the title of the recording screengroups.hierarchy.depth_limit— the message for group depth limit
This screen.section.element convention is crucial. Without it, you quickly end up with keys like button_text_3 that mean nothing three months later. I followed i18next recommendations on hierarchical naming.
Practical tip: I started with the most visited screens (Dictaphone, Feed, Agenda) before tackling settings and secondary screens. This allows for quick testing of translations on main user flows.
Which automatic translation tool to choose when you're alone?
Manually translating 1993 keys into 5 languages? That's almost 10,000 individual translations. Impossible for a solo developer. Classic services like Google Translate API or DeepL lack application context — "Memo vocal" would become "Vocal memo" instead of "Voice memo".
I wrote an npm run translate script that sends French keys to OpenRouter (Gemini 2.5 Flash as the main model). The LLM understands the application context and produces natural translations:
- "Memo vocal" → "Voice memo" (EN), "Sprachnotiz" (DE), "Nota de voz" (ES)
- "Ajouter une tache" → "Add a task", "Aufgabe hinzufugen", "Agregar una tarea"
- "Tout voir" → "See all", "Alle anzeigen", "Ver todo"
The script sends keys in batches of 50, with the application context in the system prompt: "You are translating strings for a voice-controlled AI task management mobile application." This context makes all the difference in quality.
Why 9 passes and not just one?
The first pass translated the bulk of the corpus — about 1600 keys. But a living app evolves. Each new feature adds keys:
- Pass 2: gamification — 47 new keys (levels, badges, streaks)
- Pass 3: agenda — 62 keys (events, filters, recurrence)
- Pass 4: collaborative groups — 89 keys (hierarchy, permissions, assignments)
- Pass 5-7: contextual corrections and adjustments after user feedback
- Pass 8: onboarding and first-use screens
- Pass 9: website (tamsiv.com) — a separate second translation corpus
The script detects missing keys by comparing fr.json (the reference) with each target file. It only re-translates the delta — new or modified keys. No token waste, no risk of overwriting an already validated translation.
How does the translation pipeline work in practice?
The complete pipeline follows 4 steps:
- Diff: the script compares
fr.jsonwithen.json,de.json,es.json,it.json,pt.json. Each key present in FR but absent in a target is marked as "to translate". - Batch: keys to be translated are grouped into batches of 50 (to stay within LLM token limits).
- Translation: each batch is sent to OpenRouter with a structured prompt. The LLM returns valid JSON with the translations.
- Merge: translations are merged into the target files, preserving alphabetical order and existing structure.
The cost? Approximately 0.02 to 0.05 EUR per pass with Gemini 2.5 Flash via OpenRouter. For 5 languages. This is almost free compared to professional translation services (which charge 0.10-0.20 EUR per word).
How to handle automatic language detection on first launch?
On first launch, TAMSIV detects the system language via React Native's getLocales(). If the language is supported (FR, EN, DE, ES, IT, PT), it's used directly. Otherwise, it falls back to English.
The choice is then saved in the database (Supabase) in the user profile. This allows the preference to be retrieved even when changing phones. And it avoids the classic problem: the user changes their OS language for reason X, and all their apps switch without warning.
I also added a language selector in the settings, so the user can choose independently of the system language. It's a detail, but it makes a difference for bilinguals or expats.
What are the pitfalls of LLM-based automatic translation?
LLM translation is not perfect. Here are the pitfalls I encountered:
- False friends: "Classeur" translated as "Binder" instead of "Folder" — the LLM lacked application context at first.
- Grammatical gender: in German, "die Aufgabe" (feminine) vs "der Memo" (masculine) — articles must match.
- Plurals: some languages have complex plural rules (Portuguese, German). Singular/plural forms must be provided explicitly.
- Length: German words are 30-50% longer than French. "Einstellungen" vs "Parametres". This breaks layouts if the UI design isn't flexible.
- Special characters: quotation marks vary by language (" " in English, « » in French, „ “ in German).
The solution: a detailed system prompt specifying the application domain, examples of validated few-shot translations, and an automatic proofreading pass where the LLM checks the consistency of its own translations.
How to maintain synchronization over time?
The hardest part isn't the initial translation — it's maintenance. Every modification in French must be propagated. If I change "Ajouter une tache" to "Creer une tache", the other 5 languages must follow.
My synchronization system:
- Change detection: an SHA256 hash of each FR value is stored. If the hash changes, the key is marked for retranslation.
- Automatic CI: via GitHub Actions, on each push to
mainthat modifiesfr.json, the script runs and creates a commit with updated translations. - Manual verification: I review translation diffs in the PR to detect obvious errors.
6 languages maintained in near real-time for a few cents per pass. The effort/impact ratio is unbeatable.
What is the concrete impact on user acquisition?
The numbers speak for themselves. After internationalization:
- Discovery surface x6: the Play Store listing is indexed in 6 languages, multiplying the covered search queries.
- Store conversion rate: a user who sees a description in their language is 2 to 3 times more likely to install.
- Retention: the in-app experience in the native language drastically reduces 7-day churn.
- Website SEO: the tamsiv.com website is available in 6 languages with localized URLs (
/fr/,/en/,/de/, etc.), which boosts international SEO.
For a solo developer, this is the acquisition lever with the best cost/effectiveness ratio. No ad budget, no obscure growth hacking — just intelligent automated translation.
How to apply this approach to your own project?
If you're developing an app and hesitating to internationalize, here's my advice: do it early. The longer you wait, the more strings there are to extract. Here are the key steps:
- Structure your keys from the start with an
screen.section.elementconvention. - Choose your target languages based on your market. For Europe, FR/EN/DE/ES/IT/PT cover the majority.
- Automate translation with an LLM via API (OpenRouter, OpenAI, etc.). The cost is negligible.
- Integrate into your CI so that synchronization is automatic with each push.
- Test with real native users if possible — even a quick glance is better than nothing.
The build in public journey of TAMSIV shows that even solo, you can achieve a level of localization comparable to large teams.
FAQ
How long does it take to internationalize a React Native app?
For TAMSIV, the initial extraction of 1993 keys took about 3 days of focused work. Setting up the automatic translation script took half a day. Subsequent passes take a few minutes each thanks to automation. The bulk of the work is in extraction — not translation.
Is OpenRouter reliable for automatic translation?
Yes, with the right precautions. The system prompt must include the application context, examples of validated translations, and tone instructions. Gemini 2.5 Flash via OpenRouter produces higher quality translations than Google Translate for interface strings, because it understands the context. Fallback between models ensures availability.
Should user-generated content be translated?
No. Tasks, memos, and events remain in the user's language. Only the interface (buttons, labels, system messages, notifications) is translated. Translating user content would be both costly and confusing.
How to handle languages with different alphabets (Arabic, Japanese)?
TAMSIV currently focuses on Latin languages. RTL languages (Arabic, Hebrew) or ideograms (Chinese, Japanese) require additional layout adaptations (text direction, fonts, spacing). This is planned for a later phase, but each alphabet is a project in itself.
Can the translation script be reused on another project?
Absolutely. The script is generic: it takes a source JSON file, a list of target languages, and an LLM endpoint. You just need to adapt the system prompt to your application's context. The delta-only pattern (only translating new keys) works for any project using JSON translation files.