How to handle locale fallback when preferred locale is unavailable

Automatically select supported languages when users prefer unsupported locales

Introduction

Not every web application supports every language in the world. When a user prefers a language your application does not support, you need a fallback mechanism to display content in the next best language instead of showing errors or untranslated text.

Locale fallback is the process of selecting an alternative locale when the preferred locale is unavailable. JavaScript's Intl API handles this automatically by accepting multiple locale options and selecting the first one it supports. This ensures your application always displays properly formatted content, even when the exact preferred locale is not available.

This lesson explains how locale fallback works in JavaScript, how to implement it effectively, and how to build custom fallback logic for applications with specific locale support requirements.

The problem with unsupported locales

When you pass a locale identifier to an Intl API, the JavaScript runtime must support that locale to format content correctly. If you request formatting for Norwegian Nynorsk but the runtime only supports Norwegian Bokmål, the formatter needs a way to handle this gracefully.

Without fallback, applications would fail to display content or show untranslated text when encountering unsupported locales. Users from regions with less common language variants would experience broken interfaces.

Consider a user who speaks Canadian French. If your application only supports European French, you want the formatter to use European French conventions rather than failing completely. While not perfect, this provides a better experience than no localization at all.

How the Intl API handles fallback automatically

Every Intl constructor accepts either a single locale string or an array of locale strings. When you pass an array, the runtime evaluates each locale in order and uses the first one it supports.

const locales = ["fr-CA", "fr-FR", "en-US"];
const formatter = new Intl.DateTimeFormat(locales);

const date = new Date("2025-03-15");
console.log(formatter.format(date));
// Uses fr-CA if available
// Falls back to fr-FR if fr-CA is not available
// Falls back to en-US if neither French variant is available

The runtime examines the array from left to right. If it supports Canadian French, it uses that. If not, it tries European French. If neither French variant is available, it falls back to American English.

This automatic fallback means you do not need to check support manually or handle errors when requesting specific locales. The Intl API guarantees it will select a supported locale or fall back to the system default.

Providing multiple locale options

The simplest way to implement fallback is to pass an array of locales in order of preference. This works with all Intl constructors including DateTimeFormat, NumberFormat, Collator, and others.

const locales = ["es-MX", "es-ES", "es", "en"];
const numberFormatter = new Intl.NumberFormat(locales, {
  style: "currency",
  currency: "USD"
});

console.log(numberFormatter.format(1234.56));
// Uses Mexican Spanish if available
// Falls back to European Spanish
// Falls back to generic Spanish
// Falls back to English as final option

This pattern provides a graceful degradation path. Users get content in their preferred dialect if available, then a broader variant of their language, then a common fallback language.

The order matters. The runtime selects the first locale it supports, so place the most specific and preferred locales first.

Understanding how locale matching works

When you provide multiple locales, the JavaScript runtime uses a locale matching algorithm to select the best available option. This algorithm compares your requested locales against the set of locales the runtime supports.

If a requested locale exactly matches a supported locale, the runtime uses it immediately. If no exact match exists, the runtime may select a related locale based on language and region codes.

For example, if you request en-AU (Australian English) but the runtime only supports en-US and en-GB, it will select one of those English variants rather than falling back to a completely different language.

const locales = ["en-AU", "en"];
const formatter = new Intl.DateTimeFormat(locales);

const resolvedLocale = formatter.resolvedOptions().locale;
console.log(resolvedLocale);
// Might show "en-US" or "en-GB" depending on the runtime
// The runtime selected a supported English variant

The resolvedOptions() method returns the actual locale the formatter is using. This lets you verify which locale was selected after fallback.

Checking which locales are supported

The supportedLocalesOf() static method checks which locales from a list are supported by a specific Intl constructor. This method returns an array containing only the supported locales.

const requestedLocales = ["fr-CA", "fr-FR", "de-DE", "ja-JP"];
const supportedLocales = Intl.DateTimeFormat.supportedLocalesOf(requestedLocales);

console.log(supportedLocales);
// Output depends on runtime support
// Example: ["fr-FR", "de-DE", "ja-JP"]
// Canadian French was not supported, others were

This method filters the requested locales to show which ones the runtime can use without falling back to defaults. Unsupported locales are removed from the returned array.

You can use this method to check support before creating formatters, or to display which language options are available to users.

const availableLocales = ["en-US", "es-MX", "fr-FR", "de-DE", "ja-JP"];
const supported = Intl.NumberFormat.supportedLocalesOf(availableLocales);

console.log("This runtime supports:", supported);
// Shows which of your application locales work in this environment

Each Intl constructor has its own supportedLocalesOf() method because locale support can vary between different internationalization features. A runtime might support French for number formatting but not for text segmentation.

Building fallback chains from locale identifiers

When you know your application supports specific locales, you can build a fallback chain that progressively becomes less specific. This pattern starts with a full locale identifier and removes components until finding a match.

function buildFallbackChain(locale) {
  const chain = [locale];

  const parts = locale.split("-");
  if (parts.length > 1) {
    chain.push(parts[0]);
  }

  chain.push("en");

  return chain;
}

const fallbacks = buildFallbackChain("zh-Hans-CN");
console.log(fallbacks);
// ["zh-Hans-CN", "zh", "en"]

const formatter = new Intl.DateTimeFormat(fallbacks);
// Tries Simplified Chinese for China
// Falls back to generic Chinese
// Falls back to English

This function creates a fallback chain by extracting the language code from a locale identifier and adding a final English fallback. You can expand this logic to include more sophisticated fallback rules based on your application's supported locales.

For applications that support multiple variants of a language, you might want to fall back to related dialects before jumping to English.

function buildSmartFallbackChain(locale) {
  const chain = [locale];

  if (locale.startsWith("es-")) {
    chain.push("es-MX", "es-ES", "es");
  } else if (locale.startsWith("fr-")) {
    chain.push("fr-FR", "fr-CA", "fr");
  } else if (locale.startsWith("zh-")) {
    chain.push("zh-Hans-CN", "zh-Hant-TW", "zh");
  }

  const parts = locale.split("-");
  if (parts.length > 1 && !chain.includes(parts[0])) {
    chain.push(parts[0]);
  }

  if (!chain.includes("en")) {
    chain.push("en");
  }

  return chain;
}

const fallbacks = buildSmartFallbackChain("es-AR");
console.log(fallbacks);
// ["es-AR", "es-MX", "es-ES", "es", "en"]
// Tries Argentinian Spanish
// Falls back to Mexican Spanish
// Falls back to European Spanish
// Falls back to generic Spanish
// Falls back to English

This approach ensures users see content in a related dialect of their language before falling back to English.

Selecting a locale matching algorithm

The Intl API supports two locale matching algorithms: lookup and best fit. You can specify which algorithm to use through the localeMatcher option when creating formatters.

The lookup algorithm follows the BCP 47 Lookup specification. It performs strict matching by comparing locale identifiers systematically and selecting the first exact match.

const locales = ["de-DE", "en-US"];
const formatter = new Intl.NumberFormat(locales, {
  localeMatcher: "lookup"
});

console.log(formatter.resolvedOptions().locale);
// Uses strict lookup matching rules

The best fit algorithm allows the runtime to select a locale using its own matching logic. This algorithm can make intelligent decisions about which locale best serves the user's needs, even if it is not an exact match.

const locales = ["de-DE", "en-US"];
const formatter = new Intl.NumberFormat(locales, {
  localeMatcher: "best fit"
});

console.log(formatter.resolvedOptions().locale);
// Uses runtime's best fit algorithm
// Might select a related locale more intelligently

The default algorithm is best fit. Most applications should use the default because it provides better results across different JavaScript runtimes. Use lookup only when you need predictable, strict matching behavior.

Using browser language preferences for automatic fallback

The navigator.languages property returns an array of the user's preferred languages in order of preference. You can pass this array directly to Intl constructors to implement automatic fallback based on browser settings.

const formatter = new Intl.DateTimeFormat(navigator.languages);

const date = new Date("2025-03-15");
console.log(formatter.format(date));
// Automatically uses user's preferred supported language

This approach lets the browser handle all fallback logic. If the user's first preference is not supported, the Intl API automatically tries their second preference, then third, and so on.

This pattern works well when you want to respect all of the user's language preferences without manually building fallback chains.

console.log(navigator.languages);
// ["fr-CA", "fr", "en-US", "en"]

const numberFormatter = new Intl.NumberFormat(navigator.languages, {
  style: "currency",
  currency: "USD"
});

console.log(numberFormatter.format(1234.56));
// Tries Canadian French first
// Falls back through the user's complete preference list

The Intl API evaluates each language in the array and selects the first one it supports, making this a robust solution for handling diverse user preferences.

Combining application support with user preferences

For applications with limited locale support, you can filter user preferences to match your supported locales before creating formatters. This ensures you only attempt to use locales your application can handle.

const supportedLocales = ["en-US", "es-MX", "fr-FR", "de-DE"];

function findBestLocale(userPreferences, appSupported) {
  const preferences = userPreferences.map(pref => {
    const parts = pref.split("-");
    return [pref, parts[0]];
  }).flat();

  for (const pref of preferences) {
    if (appSupported.includes(pref)) {
      return pref;
    }
  }

  return appSupported[0];
}

const userLocale = findBestLocale(navigator.languages, supportedLocales);
const formatter = new Intl.DateTimeFormat(userLocale);

console.log(formatter.resolvedOptions().locale);
// Selected locale matches both user preference and app support

This function finds the first match between user preferences and supported locales, including trying language codes without region codes for broader matching.

For more sophisticated matching, you can check which of the user's preferences are supported by the Intl API in this runtime.

const supportedLocales = ["en-US", "es-MX", "fr-FR", "de-DE"];

function findBestSupportedLocale(userPreferences, appSupported) {
  const runtimeSupported = Intl.DateTimeFormat.supportedLocalesOf(appSupported);

  for (const pref of userPreferences) {
    if (runtimeSupported.includes(pref)) {
      return pref;
    }

    const lang = pref.split("-")[0];
    const match = runtimeSupported.find(s => s.startsWith(lang));
    if (match) {
      return match;
    }
  }

  return runtimeSupported[0] || "en";
}

const userLocale = findBestSupportedLocale(navigator.languages, supportedLocales);
const formatter = new Intl.DateTimeFormat(userLocale);

This approach ensures the selected locale is supported by both your application and the JavaScript runtime.

Handling cases where no locale matches

If none of the requested locales are supported, the Intl API falls back to the system's default locale. This default varies by environment and is determined by the operating system, browser, or Node.js configuration.

const unsupportedLocales = ["non-existent-locale"];
const formatter = new Intl.DateTimeFormat(unsupportedLocales);

console.log(formatter.resolvedOptions().locale);
// Shows the system default locale
// Might be "en-US" or another locale depending on the system

The system default ensures formatters always work, even with completely invalid or unsupported locale lists. Your application will not throw errors due to locale issues.

To ensure a specific fallback instead of relying on the system default, always include a widely supported locale like en or en-US at the end of your locale array.

const locales = ["xyz-INVALID", "en"];
const formatter = new Intl.DateTimeFormat(locales);

console.log(formatter.resolvedOptions().locale);
// Will use "en" since the first locale is invalid
// Guaranteed fallback to English instead of system default

This pattern makes your application's behavior more predictable across different environments.

Practical patterns for production applications

When building production applications, combine multiple fallback strategies to ensure robust locale handling across diverse user bases.

A common pattern creates formatters with a complete fallback chain that includes user preferences, application-supported locales, and a guaranteed final fallback.

class LocaleManager {
  constructor(supportedLocales) {
    this.supportedLocales = supportedLocales;
    this.defaultLocale = "en-US";
  }

  buildLocaleChain(userPreference) {
    const chain = [];

    if (userPreference) {
      chain.push(userPreference);

      const lang = userPreference.split("-")[0];
      if (lang !== userPreference) {
        chain.push(lang);
      }
    }

    chain.push(...navigator.languages);
    chain.push(...this.supportedLocales);
    chain.push(this.defaultLocale);

    const unique = [...new Set(chain)];
    return unique;
  }

  createDateFormatter(userPreference, options = {}) {
    const locales = this.buildLocaleChain(userPreference);
    return new Intl.DateTimeFormat(locales, options);
  }

  createNumberFormatter(userPreference, options = {}) {
    const locales = this.buildLocaleChain(userPreference);
    return new Intl.NumberFormat(locales, options);
  }
}

const manager = new LocaleManager(["en-US", "es-MX", "fr-FR", "de-DE"]);
const dateFormatter = manager.createDateFormatter("pt-BR");

console.log(dateFormatter.resolvedOptions().locale);
// Uses comprehensive fallback chain
// Tries Portuguese for Brazil
// Falls back to Portuguese
// Falls back through navigator.languages
// Falls back through supported locales
// Guaranteed to use en-US if nothing else matches

This class encapsulates locale fallback logic and ensures consistent behavior across your application.

For applications that need to respond to user locale changes at runtime, combine the locale manager with event listeners.

class LocaleAwareFormatter {
  constructor(supportedLocales) {
    this.supportedLocales = supportedLocales;
    this.updateFormatters();

    window.addEventListener("languagechange", () => {
      this.updateFormatters();
    });
  }

  updateFormatters() {
    const locales = [...navigator.languages, ...this.supportedLocales, "en"];

    this.dateFormatter = new Intl.DateTimeFormat(locales);
    this.numberFormatter = new Intl.NumberFormat(locales);
  }

  formatDate(date) {
    return this.dateFormatter.format(date);
  }

  formatNumber(number) {
    return this.numberFormatter.format(number);
  }
}

const formatter = new LocaleAwareFormatter(["en-US", "es-MX", "fr-FR"]);
console.log(formatter.formatDate(new Date()));
// Automatically updates when user changes language preferences

This pattern creates formatters that stay synchronized with browser language changes, ensuring your application always displays content according to current preferences.

Summary

Locale fallback ensures your application displays properly formatted content even when users prefer locales you do not explicitly support. The Intl API handles fallback automatically by accepting arrays of locale preferences and selecting the first supported option.

Key concepts:

  • Pass arrays of locales to Intl constructors for automatic fallback
  • The runtime selects the first locale it supports from the array
  • Use supportedLocalesOf() to check which locales are available
  • Build fallback chains that progress from specific to general locales
  • The localeMatcher option controls the matching algorithm
  • Pass navigator.languages directly for automatic user preference handling
  • Always include a widely supported final fallback like English
  • The runtime falls back to system defaults when no locales match

Use automatic fallback with locale arrays for most applications. Implement custom fallback logic when you need specific control over which locales are attempted in which order.