ロケール識別子を標準形式に正規化する方法

ロケール識別子を正しい大文字小文字と構成要素の順序を持つ正規形式に変換する

はじめに

ロケール識別子は、同じ言語と地域を指す場合でも、さまざまな方法で記述することができます。ユーザーは EN-usen-US、または en-us と書くかもしれませんが、これら3つはすべてアメリカ英語を表しています。ロケール識別子を保存、比較、または表示する際、これらのバリエーションは一貫性のない状態を生み出します。

正規化はロケール識別子を標準的な正規形式に変換します。このプロセスでは、コンポーネントの大文字小文字を調整し、拡張キーワードをアルファベット順に並べ、アプリケーション全体で信頼できる一貫した表現を生成します。

JavaScriptには、ロケール識別子を自動的に正規化するための組み込みメソッドが用意されています。このガイドでは、正規化の意味、コードでの適用方法、および正規化された識別子がどのように国際化ロジックを改善するかについて説明します。

ロケール識別子における正規化の意味

正規化は、BCP 47標準とUnicode仕様に従って、ロケール識別子をその正規形式に変換します。正規形式には、大文字小文字、順序、および構造に関する特定のルールがあります。

正規化されたロケール識別子は、次の規則に従います:

  • 言語コードは小文字
  • 文字体系コードはタイトルケースで最初の文字が大文字
  • 地域コードは大文字
  • バリアントコードは小文字
  • 拡張キーワードはアルファベット順にソート
  • 拡張属性はアルファベット順にソート

これらのルールにより、各ロケールに対して単一の標準表現が作成されます。ユーザーがロケール識別子をどのように記述しても、正規化された形式は常に同じです。

正規化ルールの理解

ロケール識別子の各コンポーネントには、正規形式における特定の大文字小文字の規則があります。

言語の大文字小文字

言語コードは常に小文字を使用します:

en (正しい)
EN (不正確、ただし en に正規化される)
eN (不正確、ただし en に正規化される)

これは2文字と3文字の両方の言語コードに適用されます。

スクリプトの大文字小文字の規則

スクリプトコードはタイトルケースを使用します。最初の文字は大文字で、残りの3文字は小文字です:

Hans (正しい)
hans (誤り、ただしHansに正規化される)
HANS (誤り、ただしHansに正規化される)

一般的なスクリプトコードには、ラテン文字のLatn、キリル文字のCyrl、簡体字漢字のHans、繁体字漢字のHantなどがあります。

地域コードの大文字小文字の規則

地域コードは常に大文字を使用します:

US (正しい)
us (誤り、ただしUSに正規化される)
Us (誤り、ただしUSに正規化される)

これは、ほとんどのロケール識別子で使用される2文字の国コードに適用されます。

拡張子の順序

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"]

スクリプトコードHANSHansになり、地域コードcnCNになります。

リージョンコードの正規化

このメソッドはリージョンコードを大文字に変換します:

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 の前に来て、hcnu の前に来るためです。

複数のロケール識別子の正規化

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"]

配列内の各ロケールはその正規形式に変換されます。

重複の削除

このメソッドは正規化後に重複するロケール識別子を削除します。複数の入力値が同じ正規形式に正規化される場合、結果には1つのコピーのみが含まれます:

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

3つの入力はすべて同じロケールを表すため、出力には単一の正規化された識別子が含まれます。

この重複排除は、ユーザー入力を処理したり、複数のソースからロケールリストをマージしたりする際に役立ちます。

無効な識別子の処理

配列内のロケール識別子のいずれかが無効な場合、メソッドは 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"

languagescriptregionプロパティはすべて、正規形式に対して正しい大文字小文字を使用します。

オプションによる正規化

オプションを指定してロケールオブジェクトを作成すると、コンストラクタは基本識別子とオプションの両方を正規化します:

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" }

正規化がなければ、セットには同じロケールに対して3つのエントリが含まれることになります。正規化により、正しく1つのエントリだけが含まれます。

信頼性の高い比較

ロケール識別子の比較には正規化が必要です。大文字小文字のみが異なる2つの識別子は同じロケールを表します:

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はバージョン9からIntl.getCanonicalLocales()をサポートし、バージョン10以降で完全にサポートしています。

Intl.Localeコンストラクタとその正規化動作は、Intl.Locale APIをサポートするすべてのブラウザで動作します。これには、Chrome、Firefox、Safari、およびEdgeの最新バージョンが含まれます。

要約

正規化は、標準的な大文字小文字のルールを適用し、拡張キーワードをソートすることにより、ロケール識別子を正規形式に変換します。これにより、確実に保存、比較、表示できる一貫した表現が作成されます。

主要な概念:

  • 正規形式では、言語には小文字、スクリプトにはタイトルケース、地域には大文字を使用します
  • 拡張キーワードは正規形式ではアルファベット順にソートされます
  • Intl.getCanonicalLocales()メソッドは識別子を正規化し、重複を削除します
  • Intl.Localeコンストラクタも正規化された出力を生成します
  • 正規化はロケール識別子の意味を変更しません
  • 保存、比較、表示には正規化された識別子を使用してください

正規化は、ロケール識別子を扱うあらゆるアプリケーションの基本的な操作です。一貫性のない大文字小文字の使用によって引き起こされるバグを防ぎ、国際化ロジックがロケール識別子を確実に処理することを保証します。