What is i18n?
Internationalization (i18n) is the process of designing software so it can be adapted to different languages and regions without changing your code. The “i18n” nickname comes from the fact that there are 18 letters between the “i” and the “n” in internationalization.
Think of i18n as laying the groundwork for:
- Localization (l10n): Translating and adapting your content for a specific market.
- Globalization (g11n): Coordinating product readiness and operations across markets.
In practice, i18n is about making sure your app can handle:
- Text in any language
- Dates, times, numbers, and currencies in the right format
- Pluralization rules that match local grammar
- Right-to-left layouts (like Arabic or Hebrew)
Why i18n matters
Investing in i18n upfront brings tangible benefits to your engineering and product teams:
- Lower localization costs: Extracting and replacing strings is easier when translation logic is built in from the start.
- Faster development: Shared infrastructure for managing strings reduces future rework.
- Better UX: Users see content in their language, formatted in ways that feel natural.
- Scalability: Apps can be launched in new markets with fewer code changes.
i18n implementation patterns in modern apps
Below are some common patterns for implementing i18n in production-grade apps. We’ll walk through React (frontend), iOS, and Android.
No matter the platform, translations usually come from a translation management system (TMS) like Smartling as JSON or resource files. These files are then loaded by your app at runtime through an i18n framework or native localization API, ensuring that the correct strings are displayed for the user’s language or locale.
Frontend (React)
Modern React apps typically use a dedicated i18n library. Two of the most popular are i18next and FormatJS, which are both framework-agnostic. In React, their respective implementations are react-i18next (function and hook-based) and react-intl (component- and hook-based, built on ICU MessageFormat). Both integrate cleanly with enterprise pipelines and handle pluralization, variables, and locale-aware formatting.
1. React with i18next + react-i18next
i18next is one of the most popular JavaScript i18n libraries, and when paired with the react-i18next bindings, it gives React apps a powerful, hook-friendly API for loading and rendering translations.
In the example below, we’ll walk through a common enterprise i18n pattern. We’ll define translation files, initialize i18next at the app’s entry point, and then render translations in components. This approach works across frameworks, integrates cleanly with most translation management systems (TMS), and scales well as your app grows—whether you’re displaying static strings or dynamic, variable-driven messages like counts.
Example i18n pattern: i18next + reacti18next
You’ll probably receive content as JSON files from your translation management system. The JSON file example below shows how translations are stored for English. Each entry has a unique key, and plural forms are defined separately so the library can choose the right one based on the count you pass. Your TMS will generate a matching file for every supported language.
{
"welcome": "Welcome",
"visits_one": "You’ve visited {{count}} time.",
"visits_other": "You’ve visited {{count}} times."
}
Next, you'll see an example of how to configure i18next so it knows what languages your app supports, where to find the translations, and what to do if a translation is missing. This setup file is run once when your app starts (often in the entry point file like index.js or main.tsx) so that translations are ready before any UI renders. Centralizing configuration in one place keeps your localization logic consistent across your app.
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './locales/en/translation.json';
import fr from './locales/fr/translation.json';
const locale = 'fr'; // in production, resolve dynamically
i18next
.use(initReactI18next)
.init({
lng: locale,
fallbackLng: 'en',
resources: {
en: { translation: en },
fr: { translation: fr }
},
interpolation: { escapeValue: false }
});
export default i18next;
Once i18next is initialized, you can use it anywhere in your app. In the below example, the useTranslation hook retrieves the t function, which takes a key and optional variables to render the right string. When you pass count as a variable, i18next automatically selects the correct plural form based on the translation file.
import { useTranslation } from 'react-i18next';
import './i18n';
export default function App() {
const { t } = useTranslation();
const count = 3;
return (
<>
<h1>{t('welcome')}</h1>
<p>{t('visits', { count })}</p>
</>
);
}
2. React with FOrmatJS + react-intl
react-intl is part of the FormatJS ecosystem and provides a component-based approach to i18n in React. It’s built on the ICU MessageFormat standard, so you get built-in pluralization, date/number formatting, and locale fallback.
In the example below, we’ll set up translation files, wrap the app in an IntlProvider, and render localized text using FormattedMessage. This approach is well-suited for teams that want a declarative, component-driven style for handling i18n in React.
Example i18n pattern: FormatJS + react-intl
The JSON file below contains translations using ICU MessageFormat syntax, which puts plural logic inside the string itself. This keeps all language rules in one place and lets translators fully control grammar without developer intervention. Your TMS manages these per-locale files.
{
"welcome": "Welcome",
{% raw %} "visits": "You’ve visited {count, plural, one {# time} other {# times}}."{% end_raw %}Next, you’ll see an example of wrapping your app in the IntlProvider component. This is a one-time setup, usually done in a root component like Root.tsx or index.jsx. It makes the active locale and its messages available throughout your app so any component can use them without extra imports.
import { IntlProvider } from 'react-intl';
import en from './locales/en.json';
import fr from './locales/fr.json';
const MESSAGES = { en, fr };
const locale = 'fr';
export default function Root() {
return (
<IntlProvider locale={locale} messages={MESSAGES[locale]}>
<App />
</IntlProvider>
);
}
Lastly, read below to see how the FormattedMessage component looks up a translation by its ID and handles pluralization automatically. All you need to pass is the count, and the library applies the correct language rules from your JSON file.
import { FormattedMessage } from 'react-intl';
export default function App() {
const count = 3;
return (
<>
<h1><FormattedMessage id="welcome" defaultMessage="Welcome" /></h1>
<p><FormattedMessage id="visits" values={{ count }} /></p>
</>
);
}
A few additional tips for production use:
- Locale source: In real apps, determine the locale centrally (e.g., from a URL like /fr/*, the user’s profile, or a server-provided setting) and pass it to <IntlProvider>.
- Message organization: For scale, break message files down by domain or feature (e.g., auth.json, dashboard.json) and merge them for the active locale.
- Fallbacks: defaultMessage is your safety net during rollout—keep it in your source language.
- Async loading (optional): For large bundles, dynamically import message files per locale (code-split) before rendering <IntlProvider>.
iOS
iOS gives you solid localization tools out of the box, but scaling cleanly to many locales requires thoughtful implementation of i18n best practices. Otherwise, without a clear structure, you can end up with duplicated keys, inconsistent formatting, and translation files that become a headache to maintain. The key is to make a few structural decisions early so your localization stays organized and easy to extend as new markets are added.
Tip 1: Organize resources in a way that scales
A good place to start is with String Catalogs (.xcstrings) in Xcode 15 and later, or .strings/.stringsdict files if you’re on an older setup. These work well with a TMS, which will usually send you translations in XLIFF format. You can import those straight into your catalog, and the system will handle the heavy lifting of merging and managing them.
You’ll probably find it easier to keep things tidy if you organize catalogs by feature or module. Smaller, targeted files make it simpler to find keys, review changes, and hand work off to translators. Here’s an example of how you might want to organize them:
Resources/
i18n/
Auth.xcstrings
Checkout.xcstrings
Profile.xcstrings
Tip 2: Handle pluralization in the catalog, not in code
Pluralization is another place where structure pays off. It’s better to define all plural rules in the catalog instead of in Swift, so your code just passes variables and the right phrase is picked automatically for each language. Here’s what that might look like in the catalog:
Key: unread_messages
Format: Plural
One: "%d unread message"
Other: "%d unread messages"
And here’s how you might use it in Swift:
let unreadCount = 3
let format = String(localized: "unread_messages")
let message = String.localizedStringWithFormat(format, unreadCount)
// "3 unread messages"
This way, you’re not hardcoding grammar in code, and translators can get the details right for each language.
Tip 3: Centralize formatting for numbers, dates, and currencies
You’ll also want to centralize number, date, and currency formatting so every part of the app feels consistent. A shared utility or service can help with that. Here’s a simple example using Swift’s modern FormatStyle API:
let price = 19.99
let display = price.formatted(.currency(code: "EUR"))
// "€19.99" or "19,99 €" depending on locale
By keeping your string resources organized, handling pluralization in the catalog, and centralizing all your locale-aware formatting, you set your iOS app up to grow without creating extra maintenance work. Once those practices are in place, the process for adding new languages becomes far more predictable—and much less stressful. Now let’s look at Android, which offers its own built-in localization tools.
Android
Android’s resource system is already designed with localization in mind, but keeping it maintainable across many languages takes some planning. If you keep everything in one big file or scatter grammar rules in code, it can get messy fast. Instead, prioritize file segmented organization, define all grammar rules in XML, and make sure your formatting and layouts work for every writing system you plan to support.
Tip 1: Keep resources organized by feature
For most teams, translations will come from a TMS as XLIFF files. You import these into the res/values directories for each locale, and Android takes care of matching the right strings to the user’s language.
Breaking your resources into separate files by feature is a simple way to make life easier. Smaller files make it quicker to review changes and help avoid merge conflicts.
app/src/main/res/
values/strings_auth.xml
values/strings_checkout.xml
values/plurals_checkout.xml
Tip 2: Define grammar rules in resources, not code
As in iOS, pluralization is one of those things that’s best handled in resources so translators can adapt it per language without you changing code. Here’s an example of a plural resource in English:
<plurals name="checkout_cart_items_count">
<item quantity="one">%1$d item</item>
<item quantity="other">%1$d items</item>
</plurals>
And here’s how you’d use it in Kotlin:
val msg = resources.getQuantityString(
R.plurals.checkout_cart_items_count, count, count
)
This way, our code stays clean, and Android automatically picks the right form based on the locale.
Tip 3: Use locale-aware formatting
For layouts, it’s a good habit to use start and end instead of left and right so they’ll adapt for right-to-left languages like Arabic or Hebrew:
<LinearLayout
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</LinearLayout>
And when formatting numbers or currencies, pass in the app’s current locale so everything looks consistent:
val appLocale = LocaleListCompat.getAdjustedDefault()[0] ?: Locale.getDefault()
val price = NumberFormat.getCurrencyInstance(appLocale).format(1234.56)
// "$1,234.56" or "1 234,56 €"
In the end, getting Android i18n right is mostly about letting the platform do the heavy lifting. By keeping all text in resources, structuring those resources with locale-specific folders, and building in RTL and locale-aware formatting from day one, you avoid the common traps that make localization brittle. Many of these principles echo iOS best practices, but Android’s resource qualifier system means you can often support new languages by simply adding the right files. Done well, scaling to new locales becomes a predictable, low-effort process.
Common i18n pitfalls
Unfortunately, even well-built systems sometimes run into avoidable problems. The table below calls out some of the most common missteps, why they cause trouble, and how to prevent them. Consider this a quick reference you can use to check your own setup before shipping.
Mistake |
How to avoid it |
Hardcoded strings |
Extract all user-facing text into resource files or translation keys. |
Assuming all text is left-to-right |
Test layouts with right-to-left languages like Arabic or Hebrew. |
Neglecting pluralization |
Use libraries that support plural rules (e.g., ICU format, i18next). |
Unlocalized units or formats |
Use locale-aware formatting (e.g., Intl.NumberFormat, Intl.DateTimeFormat). |
Skipping text expansion checks |
Test with pseudo-locales to simulate longer translations. |
Incomplete string extraction |
Use pseudo-locales in staging to surface missing or untagged strings. |
Preparing for localization at scale
Once your app is set up for internationalization, the next step is making localization as efficient and low-maintenance as possible. A few automation tools and workflows can take you from “we can translate” to “we can roll out new languages quickly, without extra development load.” Consider:
- Using a Translation Management System (TMS) with an enterprise-grade API, like Smartling, to sync translation files and manage review workflows.
- Automating string extraction and delivery using your CI/CD pipeline.
- Using AI tools (like Smartling’s AI Hub) for fast first-pass translations with built-in quality checks like fallback handling and hallucination mitigation.
Dernières réflexions
Internationalization is a foundational practice for any product going global, and the earlier you implement it, the fewer headaches you’ll have later. Combine solid i18n practices with translation automation and testing workflows, and your team will be well-equipped to ship international-ready software faster and more confidently.
Want to go deeper? Check out Smartling’s Global Ready Conference webinar on i18n strategy in the age of AI.