i18n Best Practices for React and JavaScript Projects
Internationalization, abbreviated as i18n (because there are 18 letters between the "i" and the "n"), is the process of designing your application so it can be adapted to different languages and regions without engineering changes. It is one of those things that is ten times easier to do from the start than to retrofit later.
If your app has more than a handful of users, some of them do not speak English natively. Even if you are only shipping in English today, building with i18n in mind means you can add languages later without rewriting your UI layer. And even for English-only apps, proper i18n practices improve code quality by separating content from logic.
What i18n Actually Involves
Most developers think i18n is just "wrapping strings in a translate function." That is roughly 30% of the work. Full i18n covers:
- String externalization: Moving user-visible text out of component code into translation files
- Plural handling: English has two forms (singular/plural), but Arabic has six, and some languages have different rules for numbers ending in 2, 3, or 4
- Date and time formatting: MM/DD/YYYY vs DD/MM/YYYY vs YYYY-MM-DD. Time zones. 12-hour vs 24-hour clocks
- Number formatting: 1,000.50 vs 1.000,50 vs 1 000,50
- Currency formatting: Symbol placement, decimal rules, currency-specific precision
- Text direction: Left-to-right vs right-to-left (Arabic, Hebrew)
- Text expansion: German translations are typically 30% longer than English. Your UI needs to handle that
The 7 Most Common i18n Mistakes
1. Hardcoded Strings in Components
The most basic and most common mistake. Every user-visible string should come from a translation file, not from your JSX.
// Hardcoded — impossible to translate
function LoginButton() {
return <button>Sign In</button>;
}
// Externalized — ready for any language
function LoginButton() {
const { t } = useTranslation();
return <button>{t('auth.signIn')}</button>;
}
2. String Concatenation for Sentences
This is a subtle but critical mistake. Different languages have different word orders, so you cannot build sentences by concatenating fragments.
// Word order is English-specific
const msg = "You have " + count + " new " +
(count === 1 ? "message" : "messages");
// ICU MessageFormat handles plurals for every language
// en.json: "inbox.count": "You have {count, plural,
// one {# new message} other {# new messages}}"
const msg = t('inbox.count', { count });
3. Assuming Text Length
If your button is exactly wide enough for "Submit," it will break when the German translation is "Einreichen" or the French is "Soumettre le formulaire." Design your layouts to accommodate text expansion of at least 40%.
4. Hardcoded Date and Number Formats
Never format dates with string manipulation. Use Intl.DateTimeFormat and Intl.NumberFormat, which are built into every modern browser and Node.js.
// Automatically uses the user's locale
new Intl.DateTimeFormat(locale, {
year: 'numeric', month: 'long', day: 'numeric'
}).format(date);
new Intl.NumberFormat(locale, {
style: 'currency', currency: 'EUR'
}).format(amount);
5. Forgetting About RTL
If you ever plan to support Arabic, Hebrew, Persian, or Urdu, your layout needs to flip. Use CSS logical properties (margin-inline-start instead of margin-left) and the dir attribute on your HTML root.
6. Translating Without Context
The English word "Post" can be a noun (a blog post), a verb (to post a comment), or a prefix (post-mortem). Without context, translators will guess wrong. Always provide context comments in your translation files.
// en.json
{
"post.verb": "Post", // As in "Post a comment"
"post.noun": "Post", // As in "Read this post"
"post.action": "Publish" // Button to publish content
}
7. Not Testing with Pseudo-Localization
Pseudo-localization replaces English text with accented characters and padding (e.g., "Submit" becomes "[~Ssuubbmmiitt~]") to visually verify that your UI handles longer text, special characters, and bidirectional text without breaking.
Step-by-Step: Setting Up i18n in React
The two most popular i18n libraries for React are react-i18next (built on i18next) and react-intl (from FormatJS). Both are production-ready. Here is the setup with react-i18next, which has a slightly simpler API:
- Install dependencies:
npm install i18next react-i18next i18next-browser-languagedetector - Create translation files: One JSON file per language, typically in
src/locales/en.json,src/locales/ja.json, etc. - Initialize i18next: Configure the library with your default language, fallback language, and detection strategy
- Wrap your app: Add the
I18nextProviderat the root of your component tree - Use the hook: Call
useTranslation()in any component that renders text - Extract strings: Systematically replace every hardcoded string with a
t()call
The string extraction step is the most labor-intensive part, especially in an existing codebase. This is where automated detection becomes valuable.
Automated Detection of Hardcoded Strings
Manually scanning thousands of components for hardcoded strings is error-prone and tedious. The bln-i18n-checker GitHub Action automates this by scanning your JSX and TypeScript files for user-visible strings that are not wrapped in translation functions. It runs on every pull request and flags new hardcoded strings before they get merged.
The checker is smart enough to distinguish between strings that need translation (button labels, error messages, headings) and strings that do not (CSS class names, event names, API endpoints). This reduces false positives and keeps the signal-to-noise ratio high.
i18n Libraries Compared
- react-i18next: Most popular, flexible, supports namespaces, lazy loading, and SSR. Good for most projects.
- react-intl (FormatJS): Uses ICU MessageFormat, strong plural/gender support, built-in formatters. Better if you need advanced plural rules.
- LinguiJS: Compile-time extraction, small runtime, ICU syntax. Good for performance-critical apps.
- next-intl: Purpose-built for Next.js with App Router support. Best choice if you are already on Next.js.
All four are actively maintained and have large communities. The best choice depends on your framework, your team's familiarity, and whether you need features like namespaced translations or compile-time extraction.
Catch Hardcoded Strings Before They Ship
Run automated i18n checks on every pull request. No more manually scanning for missing translations.
i18n Checker Action