DEV Community

Fedar Haponenka
Fedar Haponenka

Posted on

When to Switch to a Third-Party Localization Library

When starting a new project, my instinct is to keep things lean: minimal dependencies, maximum control, and code I fully understand. Localization often seems like the perfect candidate for a custom solution. After all, what's simpler than some JSON files and a locale switcher? But there comes a tipping point where simple becomes painful. Here's how to know when it's time to switch to a professional library.

The Simple Custom Solution

Let's start with what a basic custom implementation looks like:

// locales/en.json
{
  "welcome": "Welcome to our application",
  "login": {
    "title": "Sign In"
  },
  "user": {
    "greeting": "Hello, {name}!"
  }
}

// locales/ru.json
{
  "welcome": "Добро пожаловать в наше приложение",
  "login": {
    "title": "Войти"
  },
  "user": {
    "greeting": "Привет, {name}!"
  }
}

// localization.ts
interface Translations {
  [key: string]: string | Translations;
}

class Localization {
  private currentLocale: string = 'en';
  private translations: Map<string, Translations> = new Map();

  async loadLocale(locale: string): Promise<void> {
    const response = await fetch(`locales/${locale}.json`);
    const data = await response.json();
    this.translations.set(locale, data);
  }

  t(key: string, params?: Record<string, unknown>): string {
    const keys = key.split('.');
    let value: string | Translations | undefined = this.translations.get(this.currentLocale);

    for (const k of keys) {
      value = (value as Translations)?.[k];
      if (!value) {
        return key; // Fallback to key
      }
    }

    // Simple parameter replacement
    if (params && typeof value === 'string') {
      return Object.entries(params).reduce((str, [param, val]) => {
        return str.replace(new RegExp(`{${param}}`, 'g'), String(val));
      }, value);
    }

    return typeof value === 'string' ? value : key;
  }

  setLocale(locale: string): void {
    this.currentLocale = locale;
  }
}
Enter fullscreen mode Exit fullscreen mode

This works perfectly at first. Then reality hits.

Pluralization Complexity

While English has only two forms, in Russian, nouns after numerals are declined (have different endings). This can be problematic, as it requires writing complex checks that aren't always obvious.

pluralize(count: number, forms: string[]): string {
    if (this.currentLocale === 'ru') {
        if (count <= 20 && count > 10) {
            return forms[2];
        }

        if (count % 10 === 1) {
            return forms[0];
        }

        if ([2, 3, 4].includes(count % 10)) {
            return forms[2];
        }

        return forms[2];
    }

    // English is easy
    return count === 1 ? forms[0] : forms[1];
}
Enter fullscreen mode Exit fullscreen mode

Even these conditions don't cover all cases. What happens when you need to add another language?

Formatting Everything

Suddenly you need to format:

  • Dates (12/25/2023 vs 25.12.2023)
  • Times (2:30 PM vs 14:30)
  • Numbers (1,234.56 vs 1 234,56)
  • Currencies (€1,234.56 vs 1.234,56 € vs 1 234,56€)

Your simple utility grows to hundreds of lines of locale-specific formatting rules.

Now migration to a specialized localization library is becoming inevitable.

Security Considerations

This is where many developers hesitate. "But npm packages have vulnerabilities!". True, but consider:

Custom solution vulnerabilities:

  • XSS through unescaped interpolation
  • Path traversal in locale loading (../../../etc/passwd)
  • ReDoS in your regex-based replacement
  • Memory leaks from poor caching
  • No security team reviewing your code

Library vulnerabilities:

  • Dedicated security teams
  • Regular audits
  • CVEs with patches within days
  • Community scrutiny

The truth: Your custom code is likely more vulnerable than well-maintained, widely-used libraries. You're one regex mistake away from an XSS vulnerability that nobody will find until it's exploited.

When to Make the Switch

The decision to switch from a custom solution to a library isn't just about lines of code. It's about recognizing when you're solving problems that have already been solved better. That moment often arrives silently. One day you're adding a simple translation, the next you're debugging why your Russian pluralization doesn't work correctly. Or you discover that your custom middleware doesn't properly handle locale detection for search engine crawlers, hurting your international SEO.

The true cost of custom i18n becomes apparent when you consider opportunity cost. Every hour spent maintaining your own solution is an hour not spent building features that differentiate your product. When localization becomes a recurring headache rather than a solved foundation, when new team members struggle to understand your bespoke system, or when adding a new language feels like a multi-day engineering task rather than a simple translation update.
Here's your decision framework:

Switch NOW if:

  • You support 3+ languages with different plural rules
  • You need date/number/currency formatting
  • You're expanding to right-to-left languages (Arabic, Hebrew)
  • You need SSR/SSG support

Stay Custom if:

  • You only support English (maybe +1 similar language like Spanish)
  • No complex formatting needed
  • Bundle size is absolutely critical (<1KB budget)
  • You're building a prototype (will rebuild later)

Top comments (0)