Setting Up i18n in Your Application: A Developer's Technical Guide
You've decided to expand your app to new markets. Your PM mentions "localization" and "translation" in the same breath, but as a developer, you need to understand what actually needs to be built before any translator touches your code.
Internationalization (i18n) is the technical foundation that makes everything else possible. Without it, you'll end up with fragile, hard-to-maintain code that breaks every time you add a new language. Here's how to implement i18n properly from the start.
Why Hard-Coded Strings Will Bite You Later
Consider this seemingly innocent React component:
function UserProfile({ user }) {
return (
<div>
<h1>Welcome back, {user.name}!</h1>
<button onClick={logout}>Sign Out</button>
<p>Last login: {user.lastLogin}</p>
</div>
);
}
This works fine for English, but what happens when you need German? German text typically expands 20-30% beyond English length. "Sign Out" becomes "Abmelden" (manageable), but "Welcome back" becomes "Willkommen zurück" (50% longer). Your carefully crafted mobile layout breaks.
More critically, you'll need to hunt through your entire codebase, extract every string manually, and modify components one by one. For a medium-sized application, this retrofit can take weeks.
Externalizing Strings: The Foundation
The first step is moving all user-facing text into external resource files. Here's that same component, properly internationalized:
import { useTranslation } from 'react-i18next';
function UserProfile({ user }) {
const { t } = useTranslation();
return (
<div>
<h1>{t('welcome.message', { name: user.name })}</h1>
<button onClick={logout}>{t('auth.signOut')}</button>
<p>{t('profile.lastLogin')}: {formatDate(user.lastLogin)}</p>
</div>
);
}
Your translation files look like this:
// en/common.json
{
"welcome": {
"message": "Welcome back, {{name}}!"
},
"auth": {
"signOut": "Sign Out"
},
"profile": {
"lastLogin": "Last login"
}
}
// de/common.json
{
"welcome": {
"message": "Willkommen zurück, {{name}}!"
},
"auth": {
"signOut": "Abmelden"
},
"profile": {
"lastLogin": "Letzter Login"
}
}
Handling Dynamic Content and Pluralization
Static strings are the easy part. Dynamic content requires more thought:
// Bad: English-centric logic
function NotificationCount({ count }) {
return <span>{count} notification{count !== 1 ? 's' : ''}</span>;
}
// Good: Language-agnostic
function NotificationCount({ count }) {
const { t } = useTranslation();
return <span>{t('notifications.count', { count })}</span>;
}
Your translation files handle pluralization rules:
{
"notifications": {
"count_zero": "No notifications",
"count_one": "{{count}} notification",
"count_other": "{{count}} notifications"
}
}
Libraries like react-i18next handle plural forms automatically based on language-specific rules. Polish has 5 plural forms; Arabic has 6. Your code doesn't need to know this.
Date, Number, and Currency Formatting
Don't reinvent regional formatting. Use the browser's built-in Intl API:
function formatPrice(amount, currency, locale) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
}).format(amount);
}
function formatDate(date, locale) {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}
// Usage
formatPrice(1234.56, 'EUR', 'de-DE'); // "1.234,56 €"
formatPrice(1234.56, 'USD', 'en-US'); // "$1,234.56"
CSS Considerations for Text Expansion
German and Finnish text can be 50% longer than English. Arabic and Hebrew read right-to-left. Your CSS needs to accommodate this:
/* Flexible containers */
.button {
padding: 8px 16px;
min-width: 120px; /* Prevents cramping */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* RTL support */
.container {
direction: ltr;
}
[dir="rtl"] .container {
direction: rtl;
}
/* Logical properties work better than left/right */
.sidebar {
margin-inline-start: 20px; /* Adapts to text direction */
}
Building a Translation-Friendly Development Workflow
Set up your build process to catch missing translations early:
// webpack plugin to validate translation completeness
const TranslationValidatorPlugin = {
apply(compiler) {
compiler.hooks.emit.tap('TranslationValidator', (compilation) => {
const enKeys = getTranslationKeys('./src/i18n/en/');
const deKeys = getTranslationKeys('./src/i18n/de/');
const missing = enKeys.filter(key => !deKeys.includes(key));
if (missing.length > 0) {
console.warn(`Missing German translations: ${missing.join(', ')}`);
}
});
}
};
Use TypeScript to enforce translation key correctness:
type TranslationKeys =
| 'welcome.message'
| 'auth.signOut'
| 'profile.lastLogin';
const t = (key: TranslationKeys, params?: Record<string, any>) => {
// Implementation
};
Server-Side Considerations
Don't forget your API responses and error messages:
// Express.js example
app.use((req, res, next) => {
req.locale = req.headers['accept-language']?.split(',')[0] || 'en';
next();
});
app.post('/api/users', (req, res) => {
try {
// Process request
} catch (error) {
res.status(400).json({
error: getLocalizedErrorMessage(error.code, req.locale)
});
}
});
What Comes Next
Once you've internationalized your application, the localization process becomes manageable. Translators work with clean JSON files instead of digging through your source code. Your layouts flex properly. New languages become a configuration change, not a development project.
The technical foundation covered here enables everything that follows: professional translation, cultural adaptation, and market-specific features. Without it, you'll spend more time on workarounds than on building features.
For a deeper dive into how i18n fits into the broader localization process, check out this comprehensive breakdown of internationalization, localization, and translation differences.
Quick Implementation Checklist
- [ ] All user-facing strings moved to external files
- [ ] Translation library integrated (react-i18next, vue-i18n, etc.)
- [ ] Pluralization rules configured
- [ ] Date/number formatting uses Intl API
- [ ] CSS handles text expansion and RTL
- [ ] Build process validates translation completeness
- [ ] TypeScript types enforce translation key correctness
- [ ] Server-side error messages localized
Start with string externalization. Everything else builds from there.
Top comments (0)