Как обрабатывать резервные локали, если предпочтительная локаль недоступна
Автоматический выбор поддерживаемых языков, если пользователь выбрал неподдерживаемую локаль
Введение
Не каждое веб-приложение поддерживает все языки мира. Если пользователь предпочитает язык, который ваше приложение не поддерживает, нужен механизм резервного выбора, чтобы показывать контент на наиболее подходящем языке, а не ошибки или непереведённый текст.
Резервная локаль — это процесс выбора альтернативной локали, если предпочтительная недоступна. В JavaScript API Intl делает это автоматически: принимает несколько вариантов локалей и выбирает первую из поддерживаемых. Благодаря этому ваше приложение всегда отображает корректно отформатированный контент, даже если точная локаль пользователя недоступна.
В этом уроке объясняется, как работает резервная локаль в JavaScript, как её эффективно реализовать и как построить собственную логику резервирования для приложений с особыми требованиями к поддержке локалей.
Проблема неподдерживаемых локалей
Когда вы передаёте идентификатор локали в Intl API, среда выполнения JavaScript должна поддерживать эту локаль, чтобы корректно форматировать контент. Если вы запрашиваете форматирование для норвежского (нюношк), а поддерживается только норвежский (букмол), форматтеру нужно корректно обработать такую ситуацию.
Без резервирования приложения не смогут отображать контент или будут показывать непереведённый текст при встрече с неподдерживаемыми локалями. Пользователи из регионов с редкими языковыми вариантами столкнутся с некорректным интерфейсом.
Представьте пользователя, говорящего на канадском французском. Если ваше приложение поддерживает только европейский французский, вы хотите, чтобы форматтер использовал именно его, а не просто выдавал ошибку. Это не идеально, но гораздо лучше, чем отсутствие локализации.
Как Intl API автоматически обрабатывает резервные языки
Каждый конструктор Intl принимает либо одну строку с локалью, либо массив строк локалей. Если передать массив, среда выполнения будет проверять локали по порядку и использовать первую поддерживаемую.
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
Среда выполнения просматривает массив слева направо. Если поддерживается канадский французский, он будет выбран. Если нет — пробуется европейский французский. Если ни один вариант французского не доступен, происходит откат к американскому английскому.
Такой автоматический откат означает, что вам не нужно вручную проверять поддержку или обрабатывать ошибки при запросе конкретных локалей. Intl API гарантирует, что будет выбрана поддерживаемая локаль или системная по умолчанию.
Как указать несколько вариантов локалей
Самый простой способ реализовать откат — передать массив локалей в порядке предпочтения. Это работает со всеми конструкторами Intl, включая DateTimeFormat, NumberFormat, Collator и другими.
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
Такой подход обеспечивает плавную деградацию: пользователь получает контент на предпочтительном диалекте, если он доступен, затем на более общем варианте языка, а затем на языке-резерве.
Порядок важен. Среда выполнения выбирает первую поддерживаемую локаль, поэтому самые специфичные и предпочтительные локали должны быть в начале.
Как работает сопоставление локалей
Если вы указываете несколько локалей, JavaScript использует алгоритм сопоставления локалей, чтобы выбрать наилучший доступный вариант. Этот алгоритм сравнивает ваши запрошенные локали с набором поддерживаемых локалей среды выполнения.
Если запрошенная локаль полностью совпадает с поддерживаемой, она будет выбрана сразу. Если точного совпадения нет, среда выполнения может выбрать родственную локаль на основе кода языка и региона.
Например, если вы запрашиваете en-AU (австралийский английский), а среда выполнения поддерживает только en-US и en-GB, будет выбран один из этих вариантов английского, а не совершенно другой язык.
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
Метод resolvedOptions() возвращает фактическую локаль, которую использует форматтер. Это позволяет проверить, какая локаль была выбрана после применения fallback.
Проверка поддерживаемых локалей
Статический метод supportedLocalesOf() проверяет, какие локали из списка поддерживаются определённым Intl-конструктором. Этот метод возвращает массив, содержащий только поддерживаемые локали.
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
Этот метод фильтрует запрошенные локали, чтобы показать, какие из них среда выполнения может использовать без fallback к значениям по умолчанию. Неподдерживаемые локали удаляются из возвращаемого массива.
Вы можете использовать этот метод, чтобы проверить поддержку до создания форматтеров или чтобы показать пользователям, какие языковые опции доступны.
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
У каждого Intl-конструктора есть свой метод supportedLocalesOf(), потому что поддержка локалей может отличаться для разных функций интернационализации. Например, среда выполнения может поддерживать французский для форматирования чисел, но не для сегментации текста.
Построение цепочек fallback из идентификаторов локалей
Если вы знаете, что ваше приложение поддерживает определённые локали, вы можете построить цепочку fallback, которая постепенно становится менее специфичной. Этот подход начинается с полного идентификатора локали и поочерёдно убирает компоненты, пока не найдётся совпадение.
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
Эта функция создаёт цепочку fallback, извлекая языковой код из идентификатора локали и добавляя в конце fallback на английский. Вы можете расширить эту логику, чтобы добавить более сложные правила fallback в зависимости от поддерживаемых локалей вашего приложения.
Для приложений, поддерживающих несколько вариантов одного языка, может быть полезно сначала использовать родственные диалекты, прежде чем переходить на английский.
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
Такой подход гарантирует, что пользователи увидят контент на близком диекте их языка, прежде чем будет показан английский.
Выбор алгоритма сопоставления локали
Intl API поддерживает два алгоритма сопоставления локалей: lookup и best fit. Вы можете указать, какой алгоритм использовать, через опцию localeMatcher при создании форматтеров.
Алгоритм lookup следует спецификации BCP 47 Lookup. Он выполняет строгое сопоставление, систематически сравнивая идентификаторы локалей и выбирая первое точное совпадение.
const locales = ["de-DE", "en-US"];
const formatter = new Intl.NumberFormat(locales, {
localeMatcher: "lookup"
});
console.log(formatter.resolvedOptions().locale);
// Uses strict lookup matching rules
Алгоритм best fit позволяет среде выполнения выбирать локаль с помощью собственной логики сопоставления. Этот алгоритм может принимать более умные решения о том, какая локаль лучше всего подходит пользователю, даже если это не точное совпадение.
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
По умолчанию используется алгоритм best fit. В большинстве случаев рекомендуется использовать его, так как он даёт лучшие результаты в разных JavaScript-окружениях. Используйте lookup только если нужен предсказуемый и строгий режим сопоставления.
Использование языковых предпочтений браузера для автоматического выбора локали
Свойство navigator.languages возвращает массив языков, которые пользователь предпочитает, в порядке приоритета. Этот массив можно напрямую передать в конструкторы Intl для автоматического выбора локали на основе настроек браузера.
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
Такой подход позволяет браузеру полностью управлять логикой выбора локали. Если первый язык пользователя не поддерживается, Intl API автоматически попробует второй, затем третий и так далее.
Этот подход отлично подходит, если вы хотите учитывать все языковые предпочтения пользователя, не создавая вручную цепочки резервных языков.
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
API Intl проверяет каждый язык в массиве и выбирает первый поддерживаемый, что делает этот способ надёжным для работы с разными пользовательскими предпочтениями.
Совмещение поддержки приложения с предпочтениями пользователя
Если приложение поддерживает ограниченное количество локалей, вы можете отфильтровать пользовательские предпочтения, чтобы оставить только поддерживаемые локали перед созданием форматтеров. Это гарантирует, что вы будете использовать только те локали, которые поддерживает ваше приложение.
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
Эта функция находит первое совпадение между предпочтениями пользователя и поддерживаемыми локалями, а также пробует языковые коды без региональных для более широкого поиска.
Для более сложного сопоставления можно проверить, какие из пользовательских предпочтений поддерживаются API Intl в этом рантайме.
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);
Такой подход гарантирует, что выбранная локаль поддерживается и вашим приложением, и средой выполнения JavaScript.
Обработка случаев, когда нет совпадений локалей
Если ни одна из запрошенных локалей не поддерживается, API Intl использует системную локаль по умолчанию. Эта локаль зависит от среды и определяется операционной системой, браузером или настройками Node.js.
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
Системная локаль по умолчанию гарантирует, что форматтеры всегда будут работать, даже если список локалей полностью некорректен или не поддерживается. Ваше приложение не выдаст ошибку из-за проблем с локалями.
Чтобы задать конкретную резервную локаль вместо системной по умолчанию, всегда добавляйте в конец массива локалей такую широко поддерживаемую локаль, как en или en-US.
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
Этот подход делает поведение вашего приложения более предсказуемым в разных средах.
Практические паттерны для production-приложений
При разработке production-приложений комбинируйте несколько стратегий резервирования, чтобы обеспечить надёжную работу с локалями для разных групп пользователей.
Обычно используется паттерн, при котором форматтеры создаются с полной цепочкой резервирования: учитываются предпочтения пользователя, поддерживаемые приложением локали и гарантированный финальный 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
Этот класс инкапсулирует логику fallback по локали и обеспечивает единое поведение по всему приложению.
Для приложений, которым нужно реагировать на смену локали пользователя во время работы, объединяйте менеджер локалей с обработчиками событий.
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
Этот паттерн создаёт форматтеры, которые всегда синхронизированы с языком браузера, чтобы приложение отображало контент в соответствии с текущими предпочтениями.
Итоги
Fallback по локали гарантирует, что ваше приложение будет показывать корректно отформатированный контент даже если пользователь выбрал локаль, которую вы явно не поддерживаете. Intl API автоматически обрабатывает fallback, принимая массивы предпочтительных локалей и выбирая первый поддерживаемый вариант.
Ключевые моменты:
- Передавайте массивы локалей в конструкторы Intl для автоматического fallback
- Во время выполнения выбирается первая поддерживаемая локаль из массива
- Используйте
supportedLocalesOf(), чтобы проверить доступные локали - Стройте цепочки fallback от более специфичных к более общим локалям
- Опция
localeMatcherуправляет алгоритмом сопоставления - Передавайте
navigator.languagesнапрямую для автоматической обработки пользовательских предпочтений - Всегда добавляйте широко поддерживаемый финальный fallback, например, английский
- Если ни одна локаль не подходит, во время выполнения используется системная по умолчанию
Для большинства приложений используйте автоматический fallback с массивами локалей. Реализуйте свою логику fallback, если нужно точно контролировать порядок попыток локалей.