Back to Blog
Guide

i18n Best Practices: 15 Rules for Internationalization in 2024

Master internationalization with these 15 essential best practices. From code architecture to translation management, build apps that scale globally.

IntlPull Team
IntlPull Team
Engineering
October 25, 202412 min read

Introduction

After helping thousands of teams internationalize their apps, we've identified the practices that separate smooth, scalable i18n implementations from painful, buggy ones.

Here are 15 rules to follow for successful internationalization.

Code Architecture

1. Extract All User-Facing Strings from Day One

Don't wait until "we need to translate." By then, you'll have thousands of hardcoded strings.

// Bad - hardcoded string
<button>Submit</button>

// Good - extracted from start
<button>{t('form.submit')}</button>

Tip: Use IntlPull's CLI scanner to find missed strings:

npx intlpull scan --dry-run

2. Never Concatenate Strings

Different languages have different word orders. Concatenation breaks translations.

// Bad - breaks in German, Japanese, Arabic...
const message = "Hello " + name + ", you have " + count + " messages";

// Good - uses interpolation
const message = t('greeting', { name, count });
// en: "Hello {name}, you have {count} messages"
// de: "Hallo {name}, Sie haben {count} Nachrichten"
// ja: "{name}さん、{count}件のメッセージがあります"

3. Use ICU Message Format for Complex Strings

ICU handles plurals, gender, and selection elegantly:

{count, plural,
  =0 {No messages}
  one {# message}
  other {# messages}
}

{gender, select,
  male {He liked your post}
  female {She liked your post}
  other {They liked your post}
}

4. Keep Translation Keys Semantic

Keys should describe content, not location:

// Bad - tied to UI location
"homepage_header_title"
"sidebar_menu_item_3"

// Good - describes content
"welcome.title"
"navigation.settings"
"error.network_unavailable"

5. Organize Keys by Feature, Not File

Structure translations by feature domain:

/locales/en.json
{
  "auth": {
    "login": { ... },
    "signup": { ... }
  },
  "dashboard": {
    "overview": { ... },
    "settings": { ... }
  },
  "checkout": {
    "cart": { ... },
    "payment": { ... }
  }
}

Formatting

6. Use Native Formatting APIs

Never hardcode number, date, or currency formats:

// Bad - US-only format
const date = `${month}/${day}/${year}`;
const price = `$${amount}`;

// Good - locale-aware
const date = new Intl.DateTimeFormat(locale).format(dateObj);
const price = new Intl.NumberFormat(locale, {
  style: 'currency',
  currency: userCurrency
}).format(amount);

Locale differences:

USGermanyJapan
Date12/31/202431.12.20242024/12/31
Number1,234.561.234,561,234.56
Currency$99.9999,99 €¥100
Date12/31/202431.12.20242024/12/31
Number1,234.561.234,561,234.56
Currency$99.9999,99 €¥100
Date12/31/202431.12.20242024/12/31
Number1,234.561.234,561,234.56
Currency$99.9999,99 €¥100
Number1,234.561.234,561,234.56
Currency$99.9999,99 €¥100
Currency$99.9999,99 €¥100

7. Handle Pluralization Properly

Different languages have different plural rules:

LanguagePlural Forms
English2 (one, other)
French2+ (one, other, many)
Russian3 (one, few, many)
Arabic6 (zero, one, two, few, many, other)
English2 (one, other)
French2+ (one, other, many)
Russian3 (one, few, many)
Arabic6 (zero, one, two, few, many, other)
English2 (one, other)
French2+ (one, other, many)
Russian3 (one, few, many)
Arabic6 (zero, one, two, few, many, other)
French2+ (one, other, many)
Russian3 (one, few, many)
Arabic6 (zero, one, two, few, many, other)
Russian3 (one, few, many)
Arabic6 (zero, one, two, few, many, other)
Arabic6 (zero, one, two, few, many, other)
// Use ICU plural format
t('items', { count: 5 })
// Config handles the rules per language

8. Support RTL Languages

Arabic, Hebrew, Persian, and Urdu read right-to-left:

/* Use logical properties */
.card {
  margin-inline-start: 1rem;  /* Not margin-left */
  padding-inline-end: 1rem;   /* Not padding-right */
}

/* Let dir attribute handle layout */
[dir="rtl"] .icon {
  transform: scaleX(-1);
}
<html dir={isRTL ? 'rtl' : 'ltr'}>

Translation Quality

9. Provide Context for Translators

Translators need to know where and how strings are used:

{
  "save": {
    "value": "Save",
    "context": "Button to save user profile changes, appears at bottom of form",
    "maxLength": 10,
    "screenshot": "save-button.png"
  }
}

10. Use a Glossary

Maintain consistent terminology across translations:

EnglishSpanishGermanContext
DashboardPanel de controlDashboardMain user interface
SettingsConfiguraciónEinstellungenUser preferences
SubmitEnviarAbsendenForm submission button
DashboardPanel de controlDashboardMain user interface
SettingsConfiguraciónEinstellungenUser preferences
SubmitEnviarAbsendenForm submission button
DashboardPanel de controlDashboardMain user interface
SettingsConfiguraciónEinstellungenUser preferences
SubmitEnviarAbsendenForm submission button
SettingsConfiguraciónEinstellungenUser preferences
SubmitEnviarAbsendenForm submission button
SubmitEnviarAbsendenForm submission button

11. Leverage Translation Memory

Don't translate the same phrase twice. TM catches:

  • Exact matches: "Save changes" = "Save changes"
  • Fuzzy matches: "Save changes" ≈ "Save all changes"
  • ROI: 30-60% reduction in translation costs over time.

    Process

    12. Automate String Extraction

    Manual extraction misses strings and wastes time:

    # Scan codebase weekly in CI
    npx intlpull scan --push
    
    # Transform found strings automatically
    npx intlpull scan --apply

    13. Use Continuous Localization

    Don't batch translations. Localize continuously:

    # GitHub Action example
    on:
      push:
        branches: [main]
        paths:
          - 'src/**'
          - 'locales/en.json'
    
    jobs:
      sync:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          - run: npx intlpull push
          - run: npx intlpull pull
          - uses: stefanzweifel/git-auto-commit-action@v4

    14. Test with Pseudo-Localization

    Before real translations, test with pseudo-translations:

    "Welcome" → "[Ẃéĺćőḿé !!!!!!]"

    Pseudo-localization reveals:

  • Hardcoded strings (not transformed)
  • Layout issues (text expansion)
  • Truncation problems
  • Character encoding issues
  • 15. Implement OTA Updates for Mobile

    Don't wait for app store releases to fix translations:

    // With OTA, translation updates are instant
    let message = IntlPull.t("welcome.message")
    // Change in dashboard → users see it immediately

    Without OTA: 1-2 week delay for any translation change

    With OTA: Instant updates, no app release needed

    Anti-Patterns to Avoid

    Don't: Use String Concatenation

    // Never do this
    "Welcome, " + name

    Don't: Hardcode Formatting

    // Never do this
    date.toLocaleDateString('en-US')

    Don't: Split Sentences

    // Never do this
    <span>{t('click')}</span>
    <Link>{t('here')}</Link>
    <span>{t('to_continue')}</span>

    Don't: Assume Language = Country

    // Never do this
    if (country === 'US') language = 'en';
    // Spanish speakers in US, Portuguese in US, etc.

    Don't: Forget About Text Expansion

    /* Never do this */
    .button {
      width: 100px; /* Breaks in German */
    }

    Checklist

    Before launching in a new language:

  • [ ] All strings extracted and translated
  • [ ] Pluralization tested
  • [ ] Date/number/currency formatting correct
  • [ ] RTL layout works (if applicable)
  • [ ] Images and icons localized
  • [ ] SEO metadata translated
  • [ ] Error messages translated
  • [ ] Email templates translated
  • [ ] Legal pages translated
  • [ ] Tested by native speakers
  • Tools That Help

    NeedTool
    Translation ManagementIntlPull
    String ExtractionIntlPull CLI
    Code TransformationIntlPull CLI
    AI TranslationIntlPull (GPT-4, Claude)
    OTA UpdatesIntlPull SDK
    Pseudo-localizationIntlPull
    Translation ManagementIntlPull
    String ExtractionIntlPull CLI
    Code TransformationIntlPull CLI
    AI TranslationIntlPull (GPT-4, Claude)
    OTA UpdatesIntlPull SDK
    Pseudo-localizationIntlPull
    Translation ManagementIntlPull
    String ExtractionIntlPull CLI
    Code TransformationIntlPull CLI
    AI TranslationIntlPull (GPT-4, Claude)
    OTA UpdatesIntlPull SDK
    Pseudo-localizationIntlPull
    String ExtractionIntlPull CLI
    Code TransformationIntlPull CLI
    AI TranslationIntlPull (GPT-4, Claude)
    OTA UpdatesIntlPull SDK
    Pseudo-localizationIntlPull
    Code TransformationIntlPull CLI
    AI TranslationIntlPull (GPT-4, Claude)
    OTA UpdatesIntlPull SDK
    Pseudo-localizationIntlPull
    AI TranslationIntlPull (GPT-4, Claude)
    OTA UpdatesIntlPull SDK
    Pseudo-localizationIntlPull
    OTA UpdatesIntlPull SDK
    Pseudo-localizationIntlPull
    Pseudo-localizationIntlPull

    Conclusion

    Good i18n is about:

  • Clean architecture - extract strings early, use interpolation
  • Proper formatting - let APIs handle locale differences
  • Quality translations - context, glossaries, TM
  • Smart process - automation, continuous flow, testing
  • Follow these 15 practices and you'll build apps that scale globally without the typical i18n pain.

    Ready to level up your i18n? Try IntlPull free and implement these best practices with the right tools.

    i18n
    best-practices
    internationalization
    guide
    tips
    Share:

    Ready to simplify your i18n workflow?

    Start managing translations with IntlPull. Free tier included.