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

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

Введение

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

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

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

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

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

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

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

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

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

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

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

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

en (правильно)
EN (неправильно, но нормализуется в en)
eN (неправильно, но нормализуется в en)

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

Регистр написания скриптов

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

Hans (правильно)
hans (неправильно, но нормализуется до Hans)
HANS (неправильно, но нормализуется до Hans)

Общие коды скриптов включают Latn для латиницы, Cyrl для кириллицы, Hans для упрощённых иероглифов Хань и Hant для традиционных иероглифов Хань.

Регистр написания регионов

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

US (правильно)
us (неправильно, но нормализуется до US)
Us (неправильно, но нормализуется до US)

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

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

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

en-US-u-ca-gregory-nu-latn (правильно)
en-US-u-nu-latn-ca-gregory (неправильно, но нормализуется до первой формы)

Ключ календаря 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 `Текущая локаль: ${normalized}`;
  } catch (error) {
    return "Недопустимый идентификатор локали";
  }
}

console.log(displayLocale("EN-us"));
// "Текущая локаль: en-US"

console.log(displayLocale("zh-HANS-cn"));
// "Текущая локаль: zh-Hans-CN"

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

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

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

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

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

function processLocaleInput(input) {
  try {
    const normalized = Intl.getCanonicalLocales(input)[0];
    return {
      success: true,
      locale: normalized
    };
  } catch (error) {
    return {
      success: false,
      error: "Пожалуйста, введите допустимый идентификатор локали"
    };
  }
}

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("Неверная локаль по умолчанию:", 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("Пропуск недопустимой локали:", 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 также создает нормализованный вывод
  • Нормализация не изменяет значение идентификатора локали
  • Используйте нормализованные идентификаторы для хранения, сравнения и отображения

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