How to translate a web application's user interface

How to translate a web application's user interface

Last updated: 12 September 2026

To translate a web application's interface you have two real options. Extract every string into locale files that ship with the app, or run a layer over the rendered DOM that swaps text as it appears. Most teams end up with some of both, and which half does more work depends on how much of your UI is product copy and how much is content.

This guide is about app screens rather than landing pages: dashboards, settings, forms, buttons, empty states, error messages and the emails your app sends. The failure modes are different, and most advice written about "translating a website" does not survive contact with a real product UI.

It is worth the effort. CSA Research surveyed 8,709 consumers across 29 countries and found that 40% will never buy from websites in other languages. Its Chief Research Officer Dr. Donald A. DePalma put it plainly: "if a company chooses to not localize the buying experience they risk losing 40% or more of the total addressable market."

An app interface behaves nothing like a marketing page

A landing page is a fixed block of text sitting in the HTML when it arrives. An app is a machine that produces text. A validation error exists only after someone submits a bad form. A toast exists for four seconds. A modal's contents are not in the document until a click builds them. An empty state appears only for accounts with no data, which usually means never on your own machine.

That matters because every approach has to answer one question: at what moment does a string become text? Locale files answer it at build time. A runtime layer answers it whenever the DOM changes. Anything that answers it only on page load will miss most of your product.

Grammar breaks before layout does

Counts fail first. English has two plural forms and a lot of code quietly assumes that is universal. Unicode CLDR defines six plural categories: zero, one, two, few, many and other. Arabic uses all six for cardinal numbers, Russian and Polish use four, Japanese and Korean use one. A string like {count} items cannot be patched with a trailing "s", and getting it wrong makes a UI read as broken rather than merely foreign.

Dates and numbers follow. The same order total is 1,299.50 in the US, 1.299,50 in Germany and 1 299,50 in France. You do not need a library for this, because Intl.NumberFormat and Intl.DateTimeFormat are built into every current browser. Formatting is the one part of app i18n the platform genuinely solved for you.

Translated labels do not fit the buttons you designed

Text expansion is a layout bug that only shows up in production. The W3C's guidance on text size in translation reproduces IBM's expansion table, and its shape is the opposite of what most people expect: source strings up to ten characters long should be expected to expand by 200 to 300 percent, while strings over seventy characters settle down to around 130 percent.

Read that in the context of a UI. Your longest paragraphs are the safest thing on the page. Your buttons, tab labels and menu items are where the damage lands. The same W3C page measures the word "views" as 2.8 times longer in German than in English. Direction is the other half: the W3C counts twelve right-to-left scripts across 215 languages, and RTL is a CSS problem, not a translation one.

Option one: a build-time i18n library

You wrap every user-facing string in a function call, extract them into JSON per locale, and ship them. i18next is the default choice in JavaScript, with roughly 16 million weekly npm downloads for the core package and another 11.6 million for react-i18next (npm, week of 5 September 2026). Its own documentation is accurate about the scope: "i18next goes beyond just providing the standard i18n features (such as plurals, context, interpolation, format)."

That is the real argument for it. You get plural categories, interpolation, context variants, lazy-loaded namespaces and, crucially, translations that live outside the browser, so emails, PDFs and API errors share one catalogue. The cost is engineering time: someone has to wrap the strings and stop untranslated copy landing in every pull request.

Option two: a runtime translation layer

The other approach leaves your code alone. A script watches the rendered DOM, collects visible text, and replaces it as nodes appear. Because it reacts to mutations rather than page loads, it catches modals, toasts and rows that arrive from an API after the fact.

The honest pitch is speed. You get a translated UI in an afternoon without touching a component, which suits internal tools, admin panels and early-stage products. It also handles user-generated content, which locale files structurally cannot, because no build step knows what your customers will type.

The honest limits matter more. It cannot translate a string that never reaches the DOM, so transactional emails, push notifications and API responses are untouched. It adds a network round trip on first render, so plan for a flash of untranslated content.

Where to draw the line between them

In practice the line is drawn by ownership, not by technology. If a string is part of the product specification, with wording that gets reviewed and signed off, it belongs in locale files. Mozilla's Project Fluent makes that case better than anyone: "Translators should be able to use the entire expressive power of their language without asking developers for permission." You cannot get that from a system that only sees rendered English.

Everything else is content: help text, changelogs, in-app marketing, and anything users wrote themselves. That is the half WeLocale is built for, with one script tag, AI-powered translation applied to the live DOM including content that loads after the page, and an editor for fixing any string the machine got wrong. WeLocale is genuinely the wrong tool if your translated strings are part of your product specification.

The gotchas that surface in week two

Set the lang attribute, and change it when the language changes. Screen readers take pronunciation rules from it. WCAG 2.2 success criterion 3.1.2 requires that "the human language of each passage or phrase in the content can be programmatically determined", and mixed-language app screens are exactly the case it was written for.

Ignore SEO for anything behind a login. Crawlers never see those pages, so hreflang and indexable language URLs are wasted work there. They matter only on your public surfaces.

Cache deliberately. Locale bundles should be immutable and fingerprinted, and runtime translations cached locally so returning users do not refetch them. And write a test that renders your longest German string into your narrowest button.

FAQ

Can I translate a React or Vue app without rewriting the components? Yes, with a runtime layer that observes the DOM and replaces text after your framework renders it. It covers modals, toasts and data that arrives from an API. It cannot cover strings that never render in the browser, such as transactional emails or API error messages.

What about text that users type themselves? Locale files cannot help, because no build step knows what a user will write. User-generated content has to be translated at runtime, either by a DOM-level layer or by calling a translation API from your own code before you display it.

Do I need hreflang tags for an app dashboard? No. Pages behind a login are never crawled, so hreflang and indexable language URLs do nothing there. Add them to your public pages instead: marketing site, documentation, pricing and the login screen itself.

Want this on your own site? Add 50+ languages with one snippet, no code changes.

Try WeLocale free