Organizing i18n Translation Files: Smart, Scalable, and Maintainable

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
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.
Popular i18n Libraries
The two most popular translation libraries for React and Next.js:
| Library | Best For | Key Features |
|---|---|---|
| i18next | Any environment | Flexible, works with React, Next.js, Node.js, even Vanilla JS |
| next-intl | Next.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
| Pattern | Example | Use Case |
|---|---|---|
page.section.element | dashboard.stats.title | Page-specific content |
component.element | navbar.home | Reusable components |
action.verb | actions.save | Common actions |
status.state | status.loading | Status messages |
error.type | error.notFound | Error 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
| Library | ICU 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
| Task | Solution |
|---|---|
| 📁 Large projects | Split by feature/page |
| 🏷️ Naming keys | Use section.element pattern |
| 🔢 Variables | Use {{name}} (i18next) or {name} (next-intl) |
| 📊 Plurals | Use ICU {count, plural, ...} |
| 📅 Dates | Use ICU {date, date, long} |
| 💵 Currency | Use ICU {value, number, ::currency/USD} |
| 🔍 Missing keys | i18n 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
Found this article helpful?
Share it with your network and help others learn too!

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.
Related Articles
View all
Core Web Vitals: Optimize Your Site Before Google and Users Hate It
These metrics aren't just numbers—they're how Google understands your user experience. Learn how to optimize CLS, LCP, and INP to keep both Google and your users happy.

The .vscode Folder: Your Team's Secret Weapon for Consistent Development
Every teammate using different VS Code settings? That means messy code, weird bugs, and a confused team. Learn how the .vscode folder can unify your development environment.