i18n Best Practices for React and JavaScript Projects

Published May 3, 2026 · BeLikeNative Team · 11 min read

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:

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.

Bad
// Hardcoded — impossible to translate
function LoginButton() {
  return <button>Sign In</button>;
}
Good
// 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.

Bad
// Word order is English-specific
const msg = "You have " + count + " new " +
  (count === 1 ? "message" : "messages");
Good
// 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.

Use the platform
// 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.

Provide context
// 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:

  1. Install dependencies: npm install i18next react-i18next i18next-browser-languagedetector
  2. Create translation files: One JSON file per language, typically in src/locales/en.json, src/locales/ja.json, etc.
  3. Initialize i18next: Configure the library with your default language, fallback language, and detection strategy
  4. Wrap your app: Add the I18nextProvider at the root of your component tree
  5. Use the hook: Call useTranslation() in any component that renders text
  6. 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

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