DEV Community

Cover image for Setting Up i18n in Your Application: A Developer's Technical Guide
Diogo Heleno
Diogo Heleno

Posted on • Originally published at m21global.com

Setting Up i18n in Your Application: A Developer's Technical Guide

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Your translation files look like this:

// en/common.json
{
  "welcome": {
    "message": "Welcome back, {{name}}!"
  },
  "auth": {
    "signOut": "Sign Out"
  },
  "profile": {
    "lastLogin": "Last login"
  }
}
Enter fullscreen mode Exit fullscreen mode
// de/common.json
{
  "welcome": {
    "message": "Willkommen zurück, {{name}}!"
  },
  "auth": {
    "signOut": "Abmelden"
  },
  "profile": {
    "lastLogin": "Letzter Login"
  }
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Your translation files handle pluralization rules:

{
  "notifications": {
    "count_zero": "No notifications",
    "count_one": "{{count}} notification",
    "count_other": "{{count}} notifications"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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(', ')}`);
      }
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Use TypeScript to enforce translation key correctness:

type TranslationKeys = 
  | 'welcome.message'
  | 'auth.signOut'
  | 'profile.lastLogin';

const t = (key: TranslationKeys, params?: Record<string, any>) => {
  // Implementation
};
Enter fullscreen mode Exit fullscreen mode

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)
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

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)