Как нормализовать идентификаторы локалей до стандартной формы

Преобразуйте идентификаторы локалей в канонический формат с правильным регистром и порядком компонентов

Введение

Идентификаторы локалей можно записывать по-разному, имея в виду один и тот же язык и регион. Например, пользователь может написать EN-us, en-US или en-us — и все три варианта обозначают американский английский. При хранении, сравнении или отображении идентификаторов локалей такие различия приводят к несогласованности.

Нормализация преобразует идентификаторы локалей в стандартную каноническую форму. В этом процессе приводится регистр компонентов, ключевые слова расширений сортируются по алфавиту, и получается единое представление, на которое можно полагаться во всём приложении.

В JavaScript есть встроенные методы для автоматической нормализации идентификаторов локалей. В этом гайде объясняется, что такое нормализация, как её применять в коде и когда нормализованные идентификаторы улучшают вашу логику интернационализации.

Что значит нормализация для идентификаторов локалей

Нормализация преобразует идентификатор локали в его каноническую форму согласно стандарту BCP 47 и спецификациям Unicode. Каноническая форма подразумевает определённые правила для регистра, порядка и структуры.

Нормализованный идентификатор локали соответствует следующим правилам:

  • Коды языков — в нижнем регистре
  • Коды письменности — с заглавной первой буквой
  • Коды регионов — в верхнем регистре
  • Коды вариантов — в нижнем регистре
  • Ключевые слова расширений сортируются по алфавиту
  • Атрибуты расширений сортируются по алфавиту

Эти правила обеспечивают единое стандартное представление для каждой локали. Независимо от того, как пользователь напишет идентификатор локали, нормализованная форма всегда будет одинаковой.

Понимание правил нормализации

Каждый компонент идентификатора локали имеет свой регистр в канонической форме.

Регистр языка

Коды языков всегда пишутся строчными буквами:

en (correct)
EN (incorrect, but normalizes to en)
eN (incorrect, but normalizes to en)

Это относится как к двухбуквенным, так и к трёхбуквенным кодам языков.

Регистр письменности

Коды письменности пишутся в виде title case: первая буква заглавная, остальные три — строчные:

Hans (correct)
hans (incorrect, but normalizes to Hans)
HANS (incorrect, but normalizes to Hans)

К распространённым кодам письменности относятся Latn для латиницы, Cyrl для кириллицы, Hans для упрощённых иероглифов (хань), и Hant для традиционных иероглифов (хань).

Регистр региона

Коды регионов всегда пишутся заглавными буквами:

US (correct)
us (incorrect, but normalizes to US)
Us (incorrect, but normalizes to US)

Это относится к двухбуквенным кодам стран, которые используются в большинстве идентификаторов локалей.

Порядок расширений

Теги расширения Unicode содержат ключевые слова, определяющие форматирование. В канонической форме эти ключевые слова идут в алфавитном порядке по ключу:

en-US-u-ca-gregory-nu-latn (correct)
en-US-u-nu-latn-ca-gregory (incorrect, but normalizes to first form)

Ключ календаря ca идёт раньше ключа системы счисления nu по алфавиту, поэтому ca-gregory появляется первым в нормализованной форме.

Использование Intl.getCanonicalLocales для нормализации

Метод Intl.getCanonicalLocales() нормализует идентификаторы локалей и возвращает их в каноническом виде. Это основной способ нормализации в JavaScript.

const normalized = Intl.getCanonicalLocales("EN-us");
console.log(normalized);
// ["en-US"]

Метод принимает идентификатор локали с любым регистром и возвращает корректную каноническую форму.

Нормализация кодов языка

Метод преобразует коды языка в строчные буквы:

const result = Intl.getCanonicalLocales("FR-fr");
console.log(result);
// ["fr-FR"]

Код языка FR становится fr в результате.

Нормализация кодов письменности

Метод преобразует коды письменности в формат с заглавной первой буквой:

const result = Intl.getCanonicalLocales("zh-HANS-cn");
console.log(result);
// ["zh-Hans-CN"]

Код письменности HANS становится Hans, а код региона cn становится CN.

Нормализация кодов регионов

Метод преобразует коды регионов в верхний регистр:

const result = Intl.getCanonicalLocales("en-gb");
console.log(result);
// ["en-GB"]

Код региона gb становится GB в результате.

Нормализация ключевых слов расширения

Метод сортирует ключевые слова расширения в алфавитном порядке:

const result = Intl.getCanonicalLocales("en-US-u-nu-latn-hc-h12-ca-gregory");
console.log(result);
// ["en-US-u-ca-gregory-hc-h12-nu-latn"]

Ключевые слова меняют порядок с nu-latn-hc-h12-ca-gregory на ca-gregory-hc-h12-nu-latn, потому что ca идёт раньше hc, а hc — раньше nu по алфавиту.

Нормализация нескольких идентификаторов локалей

Метод Intl.getCanonicalLocales() принимает массив идентификаторов локалей и нормализует их все:

const locales = ["EN-us", "fr-FR", "ZH-hans-cn"];
const normalized = Intl.getCanonicalLocales(locales);
console.log(normalized);
// ["en-US", "fr-FR", "zh-Hans-CN"]

Каждая локаль в массиве преобразуется в каноническую форму.

Удаление дубликатов

Метод удаляет дублирующиеся идентификаторы локалей после нормализации. Если несколько входных значений нормализуются в одну и ту же каноническую форму, результат содержит только одну копию:

const locales = ["en-US", "EN-us", "en-us"];
const normalized = Intl.getCanonicalLocales(locales);
console.log(normalized);
// ["en-US"]

Все три входных значения представляют одну и ту же локаль, поэтому на выходе будет только один нормализованный идентификатор.

Такое удаление дубликатов полезно при обработке пользовательского ввода или объединении списков локалей из разных источников.

Обработка некорректных идентификаторов

Если какой-либо идентификатор локали в массиве некорректен, метод выбрасывает RangeError:

try {
  Intl.getCanonicalLocales(["en-US", "invalid", "fr-FR"]);
} catch (error) {
  console.error(error.message);
  // "invalid is not a structurally valid language tag"
}

При нормализации пользовательских списков проверяйте или обрабатывайте ошибки для каждой локали отдельно, чтобы определить, какие именно идентификаторы некорректны.

Использование Intl.Locale для нормализации

Конструктор Intl.Locale также нормализует идентификаторы локалей при создании объектов локали. Получить нормализованную форму можно с помощью метода toString().

const locale = new Intl.Locale("EN-us");
console.log(locale.toString());
// "en-US"

Конструктор принимает идентификатор в любом регистре и возвращает нормализованный объект локали.

Доступ к нормализованным компонентам

Каждое свойство объекта локали возвращает нормализованную форму этого компонента:

const locale = new Intl.Locale("ZH-hans-CN");

console.log(locale.language);
// "zh"

console.log(locale.script);
// "Hans"

console.log(locale.region);
// "CN"

console.log(locale.baseName);
// "zh-Hans-CN"

Свойства language, script и region используют правильный регистр для канонической формы.

Нормализация с опциями

Если вы создаёте объект локали с опциями, конструктор нормализует как основной идентификатор, так и опции:

const locale = new Intl.Locale("EN-us", {
  calendar: "gregory",
  numberingSystem: "latn",
  hourCycle: "h12"
});

console.log(locale.toString());
// "en-US-u-ca-gregory-hc-h12-nu-latn"

Ключевые слова расширения в выводе идут в алфавитном порядке, даже если в объекте опций порядок не задан.

Зачем нужна нормализация

Нормализация обеспечивает единообразие во всём приложении. При хранении, отображении или сравнении идентификаторов локалей использование канонической формы предотвращает скрытые ошибки и повышает надёжность.

Единообразное хранение

При сохранении идентификаторов локалей в базах данных, конфигурационных файлах или локальном хранилище нормализованные формы предотвращают дублирование:

const userPreferences = new Set();

function saveUserLocale(identifier) {
  const normalized = Intl.getCanonicalLocales(identifier)[0];
  userPreferences.add(normalized);
}

saveUserLocale("en-US");
saveUserLocale("EN-us");
saveUserLocale("en-us");

console.log(userPreferences);
// Set { "en-US" }

Без нормализации в наборе будет три записи для одной и той же локали. С нормализацией — только одна.

Надёжное сравнение

Для сравнения идентификаторов локалей требуется нормализация. Два идентификатора, отличающиеся только регистром, обозначают одну и ту же локаль:

function isSameLocale(locale1, locale2) {
  const normalized1 = Intl.getCanonicalLocales(locale1)[0];
  const normalized2 = Intl.getCanonicalLocales(locale2)[0];
  return normalized1 === normalized2;
}

console.log(isSameLocale("en-US", "EN-us"));
// true

console.log(isSameLocale("en-US", "en-GB"));
// false

Прямое сравнение строк без нормализации даёт неверный результат.

Единообразное отображение

При выводе идентификаторов локалей пользователям или в отладочной информации нормализованные формы обеспечивают единый формат:

function displayLocale(identifier) {
  try {
    const normalized = Intl.getCanonicalLocales(identifier)[0];
    return `Current locale: ${normalized}`;
  } catch (error) {
    return "Invalid locale identifier";
  }
}

console.log(displayLocale("EN-us"));
// "Current locale: en-US"

console.log(displayLocale("zh-HANS-cn"));
// "Current locale: zh-Hans-CN"

Пользователи видят корректно отформатированные идентификаторы локалей независимо от исходного формата.

Практические применения

Нормализация решает типичные проблемы при работе с идентификаторами локалей в реальных приложениях.

Нормализация пользовательского ввода

Когда пользователи вводят идентификаторы локалей в формах или настройках, нормализуйте ввод перед сохранением:

function processLocaleInput(input) {
  try {
    const normalized = Intl.getCanonicalLocales(input)[0];
    return {
      success: true,
      locale: normalized
    };
  } catch (error) {
    return {
      success: false,
      error: "Please enter a valid locale identifier"
    };
  }
}

const result = processLocaleInput("fr-ca");
console.log(result);
// { success: true, locale: "fr-CA" }

Это обеспечивает единый формат в вашей базе данных или конфигурации.

Построение таблиц соответствия локалей

При создании таблиц соответствия для переводов или данных, зависящих от локали, используйте нормализованные ключи:

const translations = new Map();

function addTranslation(locale, key, value) {
  const normalized = Intl.getCanonicalLocales(locale)[0];

  if (!translations.has(normalized)) {
    translations.set(normalized, {});
  }

  translations.get(normalized)[key] = value;
}

addTranslation("en-us", "hello", "Hello");
addTranslation("EN-US", "goodbye", "Goodbye");

console.log(translations.get("en-US"));
// { hello: "Hello", goodbye: "Goodbye" }

Оба вызова addTranslation используют один и тот же нормализованный ключ, поэтому переводы хранятся в одном объекте.

Объединение списков локалей

При объединении идентификаторов локалей из разных источников нормализуйте и удаляйте дубликаты:

function mergeLocales(...sources) {
  const allLocales = sources.flat();
  const normalized = Intl.getCanonicalLocales(allLocales);
  return normalized;
}

const userLocales = ["en-us", "fr-FR"];
const appLocales = ["EN-US", "de-de"];
const systemLocales = ["en-US", "es-mx"];

const merged = mergeLocales(userLocales, appLocales, systemLocales);
console.log(merged);
// ["en-US", "fr-FR", "de-DE", "es-MX"]

Метод удаляет дубликаты и нормализует регистр во всех источниках.

Создание интерфейсов выбора локали

При создании выпадающих списков или интерфейсов выбора нормализуйте идентификаторы локалей для отображения:

function buildLocaleOptions(locales) {
  const normalized = Intl.getCanonicalLocales(locales);

  return normalized.map(locale => {
    const localeObj = new Intl.Locale(locale);
    const displayNames = new Intl.DisplayNames([locale], {
      type: "language"
    });

    return {
      value: locale,
      label: displayNames.of(localeObj.language)
    };
  });
}

const options = buildLocaleOptions(["EN-us", "fr-FR", "DE-de"]);
console.log(options);
// [
//   { value: "en-US", label: "English" },
//   { value: "fr-FR", label: "French" },
//   { value: "de-DE", label: "German" }
// ]

Нормализованные значения обеспечивают единообразие идентификаторов при отправке форм.

Валидация конфигурационных файлов

При загрузке идентификаторов локалей из конфигурационных файлов нормализуйте их при инициализации:

function loadLocaleConfig(config) {
  const validatedConfig = {
    defaultLocale: null,
    supportedLocales: []
  };

  try {
    validatedConfig.defaultLocale = Intl.getCanonicalLocales(
      config.defaultLocale
    )[0];
  } catch (error) {
    console.error("Invalid default locale:", config.defaultLocale);
    validatedConfig.defaultLocale = "en-US";
  }

  config.supportedLocales.forEach(locale => {
    try {
      const normalized = Intl.getCanonicalLocales(locale)[0];
      validatedConfig.supportedLocales.push(normalized);
    } catch (error) {
      console.warn("Skipping invalid locale:", locale);
    }
  });

  return validatedConfig;
}

const config = {
  defaultLocale: "en-us",
  supportedLocales: ["EN-us", "fr-FR", "invalid", "de-DE"]
};

const validated = loadLocaleConfig(config);
console.log(validated);
// {
//   defaultLocale: "en-US",
//   supportedLocales: ["en-US", "fr-FR", "de-DE"]
// }

Это позволяет выявлять ошибки конфигурации на раннем этапе и гарантирует использование валидных нормализованных идентификаторов.

Нормализация и сопоставление локалей

Нормализация важна для алгоритмов сопоставления локалей. При поиске наилучшего совпадения локали для предпочтений пользователя сравнивайте нормализованные формы:

function findBestMatch(userPreference, availableLocales) {
  const normalizedPreference = Intl.getCanonicalLocales(userPreference)[0];
  const normalizedAvailable = Intl.getCanonicalLocales(availableLocales);

  if (normalizedAvailable.includes(normalizedPreference)) {
    return normalizedPreference;
  }

  const preferenceLocale = new Intl.Locale(normalizedPreference);

  const languageMatch = normalizedAvailable.find(available => {
    const availableLocale = new Intl.Locale(available);
    return availableLocale.language === preferenceLocale.language;
  });

  if (languageMatch) {
    return languageMatch;
  }

  return normalizedAvailable[0];
}

const available = ["en-us", "fr-FR", "DE-de"];
console.log(findBestMatch("EN-GB", available));
// "en-US"

Нормализация обеспечивает корректную работу логики сопоставления независимо от регистра ввода.

Нормализация не меняет смысл

Нормализация влияет только на представление идентификатора локали. Она не изменяет язык, письменность или регион, которые этот идентификатор обозначает.

const locale1 = new Intl.Locale("en-us");
const locale2 = new Intl.Locale("EN-US");

console.log(locale1.language === locale2.language);
// true

console.log(locale1.region === locale2.region);
// true

console.log(locale1.toString() === locale2.toString());
// true

Оба идентификатора относятся к американскому английскому. Нормализация просто гарантирует, что они записаны одинаково.

Это отличается от операций, таких как maximize() и minimize(), которые добавляют или удаляют компоненты и могут менять специфику идентификатора.

Поддержка браузеров

Метод Intl.getCanonicalLocales() работает во всех современных браузерах. Chrome, Firefox, Safari и Edge полностью его поддерживают.

Node.js поддерживает Intl.getCanonicalLocales(), начиная с версии 9, а полная поддержка реализована с версии 10 и выше.

Конструктор Intl.Locale и его поведение по нормализации работают во всех браузерах, поддерживающих API Intl.Locale. Это включает современные версии Chrome, Firefox, Safari и Edge.

Итоги

Нормализация приводит идентификаторы локалей к каноническому виду, применяя стандартные правила регистра и сортируя ключевые слова расширений. Это создает единообразные представления, которые можно надежно хранить, сравнивать и отображать.

Ключевые моменты:

  • Каноническая форма использует строчные буквы для языков, заглавные для скриптов и прописные для регионов
  • Ключевые слова расширений сортируются по алфавиту в канонической форме
  • Метод Intl.getCanonicalLocales() нормализует идентификаторы и удаляет дубликаты
  • Конструктор Intl.Locale также выдает нормализованный результат
  • Нормализация не меняет смысл идентификатора локали
  • Используйте нормализованные идентификаторы для хранения, сравнения и отображения

Нормализация — это базовая операция для любого приложения, работающего с идентификаторами локалей. Она предотвращает ошибки из-за несогласованного регистра и гарантирует, что ваша логика интернационализации корректно обрабатывает идентификаторы локалей.