How to Add Multi-Language Support to an AI-Built App
The hard part of translating an AI-built app is not translation. It is that the generator wrote your text into the components, glued sentences together, and formatted dates inline.
To add multi-language support to an AI-built app, do it in this order: extract every hardcoded string into a message catalogue, fix the sentences the generator built by concatenation, move dates, numbers and currency to locale-aware formatting, then add locale routing with proper URLs, and only then send anything for translation. Translating first is the expensive mistake, because you pay per word for text you are about to restructure.
Nothing here is specific to any one builder. It is specific to how generated code tends to look.
Why multi-language support is harder in an AI-built app
A generator writes the most direct code that satisfies your prompt. You asked for a booking page, so it wrote a booking page, with the words in it. That produces four patterns you now have to undo:
Text lives inside components. Every label, button, error and empty state is a literal, spread across dozens of files rather than collected anywhere.
Sentences are built by concatenation.
'You have ' + n + ' bookings'reads fine in English and cannot be translated correctly into a language where word order or pluralisation differs.Dates and numbers are formatted inline. A hand-rolled
formatDatethat emitsMM/DD/YYYYis a bug in most of the world.Layout assumes English width. German runs 30 percent longer, and a button sized to fit "Save" will not fit "Speichern".
The good news is that all four are mechanical, which makes them unusually good work to hand back to an agent.
Step 1: audit before you touch anything
Find out how big this is. A crude grep for quoted strings containing a space gets you most of the way:
# rough count of user-facing string literals
rg -o "[\"'][A-Z][a-z]+ [^\"']{3,}[\"']" src/ | wc -l
# which files carry the most
rg -c "[\"'][A-Z][a-z]+ [^\"']{3,}[\"']" src/ | sort -t: -k2 -rn | head -20Under 200 strings is an afternoon. Over 800 and you should plan it as a proper piece of work with a branch and a test pass. Note the number now; you will use it to sanity-check the extraction later.
Step 2: pick the approach before extracting
Whatever framework you are on has a standard i18n library, and the standard one is almost always the right choice: it will have the pluralisation rules, the interpolation syntax and the tooling. What matters more than the library is the shape you extract into.
Use nested keys that describe location and purpose, not the English text:
{
"booking": {
"form": {
"submit": "Confirm booking",
"errorPastDate": "Pick a date in the future"
},
"list": {
"empty": "No bookings yet",
"count": "{count, plural, one {# booking} other {# bookings}}"
}
}
}Keys named after the English string, like confirmBooking, break the moment the English copy changes and leave you with keys that lie about their content.
Step 3: extract with the agent, file by file
This is exactly the shape of task a coding agent is good at, provided you bound it. One directory at a time, with an explicit instruction not to change behaviour:
Extract every user-facing string in src/components/booking/ into
locales/en.json using nested keys under booking.*.
Rules:
- Replace each literal with the t() call, do not change any logic
- Do not touch strings used as object keys, test ids, or CSS class names
- Do not touch console messages or thrown error strings
- Leave any string containing a template variable alone and list it
at the end instead, I will handle those manuallyThat last rule matters. Interpolated strings are where the real decisions live, and you want them in a list to review rather than silently converted. Run your typecheck and test suite after each directory, and commit per directory so a bad batch is one revert rather than a bisect. The same reasoning applies here as anywhere else you give an AI agent a bounded task with a checkable definition of done.
Step 4: fix the concatenated sentences
Now work through that list. Every one of these is a small correctness decision:
// what the generator wrote
const msg = 'You have ' + count + ' booking' + (count === 1 ? '' : 's');
// what survives translation
const msg = t('booking.list.count', { count });The s suffix trick assumes English pluralisation. Several languages have three or more plural forms, and some have none. Push the rule into the message catalogue where translators can express it, rather than into your JavaScript where they cannot.
The same applies to sentences assembled from clauses. If your code builds "Cancelled by" plus a name plus "on" plus a date, give translators the whole sentence with named placeholders instead, so they can reorder it.
Step 5: dates, numbers and currency
Delete every hand-written formatter. The platform has this built in, and the Intl API covers dates, numbers, currency, relative times, lists and plural rules in every browser and runtime you care about.
// generated code, wrong outside the US
const when = `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
// locale-aware
const when = new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(d);
const price = new Intl.NumberFormat(locale, {
style: 'currency', currency: 'CHF'
}).format(amount);Two traps worth naming. Currency is not a language: a French speaker in Canada wants French text and Canadian dollars, so keep locale and currency as separate settings. And storing money as a float will eventually cost you a cent, which is a different bug that translation work tends to expose. Store minor units as integers.
Step 6: locale routing, and getting the URLs right
Language must be in the URL. If every language serves from the same address, search engines see one page that changes unpredictably, and users cannot share a link in the language they read.
Pattern | Example | Use when |
|---|---|---|
Path prefix | example.com/de/booking | Default choice, simplest to host |
Subdomain | de.example.com/booking | Separate teams or separate content |
Country domain | example.de/booking | Genuinely separate market presence and budget |
Whichever you pick, emit hreflang tags on every page listing every other language version plus an x-default, and make sure each localised page is reachable by a crawler without JavaScript-based redirects. Detecting a browser language and redirecting is fine as a convenience, but it must never be the only route to a locale, or crawlers see one language and users lose the ability to switch.
Step 7: right-to-left, if you need it
Arabic, Hebrew, Persian and Urdu need layout mirroring, not just translated text. Modern CSS makes this far less painful than it used to be, provided the generated code has not hardcoded physical directions.
/* generated: breaks in RTL */
.card { margin-left: 1rem; padding-right: 0.5rem; text-align: left; }
/* logical properties: mirrors automatically */
.card { margin-inline-start: 1rem; padding-inline-end: 0.5rem; text-align: start; }Set dir="rtl" on the html element for those locales and audit anything with an arrow, a chevron, or a progress indicator, since those need mirroring too. This is another good agent task: converting physical to logical properties across a stylesheet is mechanical and verifiable by eye.
Step 8: translate, then test in the ugliest language you support
Only now is the catalogue worth sending out. Machine translation is a reasonable first pass for interface text if a fluent speaker reviews it; it is not acceptable for anything legal, medical or financial, and it is not acceptable for marketing copy you care about.
Before launch, run the whole app in German or Finnish and look for clipped buttons, wrapped navigation and overflowing tables. English is the shortest language you will ship and therefore the least useful one to test in. A pseudo-localisation mode that pads every string by 40 percent catches most of this without waiting for real translations.
What breaks after launch
Untranslated strings appearing later. Add a build check that fails when a key exists in
en.jsonand is missing from another locale.New features shipping English-only. The extraction habit has to survive into normal development or you rebuild this debt in three months.
Emails and PDFs. They live outside the component tree and get forgotten every single time.
Search. Multilingual content changes how you index and match, which is worth planning if you have added search to the app.
Deploys. More locales mean more routes and more build output, so check your deployment setup still handles the page count.
Several of these, especially the deploy and search failures, tend to surface as a live incident rather than a slow-burning gap, so it is worth having a plan for what to do when your AI-built app breaks in production ready before the second locale ships.
Common questions
Can I just ask the AI builder to make the app multi-language?
For a small app, sometimes. The risk is that it translates the strings in place rather than extracting them, which looks correct and leaves you with the same problem in five languages instead of one. Ask for extraction first and translation second, and check the catalogue exists.
How many languages should I start with?
Two. The second language is where all the structural problems surface; the third through tenth are mostly cost. Adding one language proves nothing, because you will not notice everything you hardcoded.
Is machine translation good enough now?
For interface labels and short functional text, usually, with review. For anything where a wrong word creates liability or embarrassment, no. The failure mode is confident fluent nonsense, which reviewers who do not speak the language cannot catch.
Where should the user's language preference be stored?
In the URL as the source of truth, with a cookie or profile setting to decide where to send someone who arrives at the root. Storing it only in local state means a shared link opens in the wrong language. The wider structure of this decision is covered in how to build an app with AI.
Once your app supports the audience it needs to, re-engagement is the next problem. See how to add push notifications to an AI-built app for a working web push walkthrough.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


