Skip to main content

Organizing i18n Translation Files: Smart, Scalable, and Maintainable

Organizing i18n Translation Files: Smart, Scalable, and Maintainable
tips#i18n#internationalization#react#nextjs+4 more

Organizing i18n Translation Files: Smart, Scalable, and Maintainable

Translation files starting as chaos? Learn how to structure your i18n files by feature, use clear naming conventions, leverage ICU syntax for plurals and dates, and keep your project maintainable.

Mohammad Alhabil

Author

December 22, 2024
6 min read
~1200 words

Organizing i18n Translation Files: Smart, Scalable, and Maintainable

We all start our projects with just two small, clean translation files (like en.json and ar.json). But over time, the text grows and chaos ensues:

  • Scattered and duplicated strings
  • Unclear key names
  • Inconsistent naming conventions

The result? Wasted time, UI bugs, and maintenance nightmares.

The two most popular translation libraries for React and Next.js:

LibraryBest ForKey Features
i18nextAny environmentFlexible, works with React, Next.js, Node.js, even Vanilla JS
next-intlNext.js (App Router)Built specifically for Next.js with first-class App Router support

Today, let's establish a system for organizing your translation files.

1️⃣ Split Files by Feature or Page

Instead of one massive file per language, create a subfolder for each language with files for each section:

/locales
  /en
    auth.json
    dashboard.json
    common.json
  /ar
    auth.json
    dashboard.json
    common.json

This approach makes it easier to modify and add translations without confusion.

Example Structure

// locales/en/auth.json
{
  "login": {
    "title": "Welcome Back",
    "email": "Email Address",
    "password": "Password",
    "submit": "Sign In",
    "forgotPassword": "Forgot your password?",
    "noAccount": "Don't have an account?",
    "signUp": "Sign up"
  },
  "register": {
    "title": "Create Account",
    "name": "Full Name",
    "email": "Email Address",
    "password": "Password",
    "confirmPassword": "Confirm Password",
    "submit": "Create Account"
  }
}
// locales/en/common.json
{
  "actions": {
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete",
    "edit": "Edit",
    "confirm": "Confirm"
  },
  "status": {
    "loading": "Loading...",
    "error": "Something went wrong",
    "success": "Success!"
  }
}

2️⃣ Use Clear, Organized Key Names

Instead of flat, ambiguous keys:

// ❌ BAD - Unclear context
{
  "submit": "Submit",
  "title": "Title",
  "name": "Name"
}

Use namespaced, contextual keys:

// ✅ GOOD - Clear context
{
  "form.submit": "Submit",
  "profile.title": "Your Profile",
  "profile.name": "Full Name",
  "contact.title": "Contact Us",
  "contact.name": "Your Name"
}

This way, you know which part of the UI each string belongs to and avoid duplication.

Naming Convention Guidelines

PatternExampleUse Case
page.section.elementdashboard.stats.titlePage-specific content
component.elementnavbar.homeReusable components
action.verbactions.saveCommon actions
status.statestatus.loadingStatus messages
error.typeerror.notFoundError messages

3️⃣ Handling Dynamic Text (Interpolation)

When you have variables inside text (like a username), follow your library's syntax:

i18next Syntax

{
  "welcome": "Hello, {{name}}!",
  "items": "You have {{count}} items in your cart"
}
// Usage
t('welcome', { name: 'Mohammad' });  // "Hello, Mohammad!"
t('items', { count: 5 });            // "You have 5 items in your cart"

next-intl Syntax

{
  "welcome": "Hello, {name}!",
  "items": "You have {count} items in your cart"
}
// Usage
t('welcome', { name: 'Mohammad' });  // "Hello, Mohammad!"
t('items', { count: 5 });            // "You have 5 items in your cart"

4️⃣ Leverage ICU Syntax Power

Instead of writing separate keys for each case (singular, plural, etc.):

// ❌ VERBOSE - Multiple keys
{
  "followers_zero": "No followers",
  "followers_one": "1 follower",
  "followers_other": "{count} followers"
}

Write one smart key that handles all cases:

// ✅ SMART - Single ICU key
{
  "followers": "{count, plural, =0 {No followers} one {1 follower} other {# followers}}"
}
// Usage
t('followers', { count: 0 });   // "No followers"
t('followers', { count: 1 });   // "1 follower"
t('followers', { count: 5 });   // "5 followers"

ICU Handles More Than Plurals

Dates

{
  "eventDate": "Event date: {date, date, long}"
}
t('eventDate', { date: new Date() });
// "Event date: December 1, 2024"

Numbers & Currency

{
  "price": "Price: {value, number, ::currency/USD}"
}
t('price', { value: 99.99 });
// "Price: $99.99"

Gender/Select

{
  "greeting": "{gender, select, male {Welcome, sir} female {Welcome, madam} other {Welcome}}"
}
t('greeting', { gender: 'female' });  // "Welcome, madam"
t('greeting', { gender: 'male' });    // "Welcome, sir"
t('greeting', { gender: 'other' });   // "Welcome"

ICU Support by Library

LibraryICU Support
next-intl✅ Built-in, works out of the box
i18next⚠️ Requires i18next-icu plugin

Setting up ICU with i18next

npm install i18next-icu
import i18next from 'i18next';
import ICU from 'i18next-icu';

i18next
  .use(ICU)
  .init({
    // your config
  });

5️⃣ Tools & Tips

i18n Ally VS Code Extension

This extension is a game-changer for working with translations:

  • 📍 Inline Preview: See translations right next to your code
  • ✏️ Quick Edit: Modify translations without leaving the file
  • ⚠️ Missing Keys Detection: Spot missing or duplicate keys instantly
  • 🌍 Multi-language Support: Works with any i18n library
// With i18n Ally, you'll see:
<h1>{t('dashboard.title')}</h1>  // 👁️ "Dashboard" (en) | "لوحة التحكم" (ar)

Automated Key Validation

Use scripts or tools to check for missing keys between languages:

With i18next

npm install -D i18next-parser
// i18next-parser.config.js
module.exports = {
  locales: ['en', 'ar'],
  output: 'locales/$LOCALE/$NAMESPACE.json',
  input: ['src/**/*.{ts,tsx}'],
  sort: true,
};
npx i18next-parser

With next-intl

npm install -D next-intl-scanner
# or
npm install -D intl-watcher

These tools can also auto-generate translation files from your code!

Custom Validation Script

// scripts/validate-translations.ts
import en from '../locales/en/common.json';
import ar from '../locales/ar/common.json';

function findMissingKeys(source: object, target: object, path = ''): string[] {
  const missing: string[] = [];
  
  for (const key of Object.keys(source)) {
    const currentPath = path ? `${path}.${key}` : key;
    
    if (!(key in target)) {
      missing.push(currentPath);
    } else if (typeof source[key] === 'object' && source[key] !== null) {
      missing.push(...findMissingKeys(source[key], target[key], currentPath));
    }
  }
  
  return missing;
}

const missingInAr = findMissingKeys(en, ar);
const missingInEn = findMissingKeys(ar, en);

if (missingInAr.length) {
  console.log('❌ Missing in Arabic:', missingInAr);
}
if (missingInEn.length) {
  console.log('❌ Missing in English:', missingInEn);
}
if (!missingInAr.length && !missingInEn.length) {
  console.log('✅ All translations are in sync!');
}

Quick Reference

TaskSolution
📁 Large projectsSplit by feature/page
🏷️ Naming keysUse section.element pattern
🔢 VariablesUse {{name}} (i18next) or {name} (next-intl)
📊 PluralsUse ICU {count, plural, ...}
📅 DatesUse ICU {date, date, long}
💵 CurrencyUse ICU {value, number, ::currency/USD}
🔍 Missing keysi18n Ally + validation scripts

The Bottom Line

Translation files aren't just text—they're a core part of your project's organization. Getting it right from the start saves time and reduces bugs.


Interfaces you love, code you understand. 💡

Topics covered

#i18n#internationalization#react#nextjs#next-intl#i18next#localization#best-practices

Found this article helpful?

Share it with your network and help others learn too!

Mohammad Alhabil

Written by Mohammad Alhabil

Frontend Developer & Software Engineer passionate about building beautiful and functional web experiences. I write about React, Next.js, and modern web development.