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

ロケール識別子を正しい大文字小文字の使い分けとコンポーネントの順序で正規形式に変換する

はじめに

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

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

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

ロケール識別子の正規化の意味

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

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

  • 言語コードは小文字
  • 文字体系コードは最初の文字を大文字にしたタイトルケース
  • 地域コードは大文字
  • 変種コードは小文字
  • 拡張キーワードはアルファベット順にソート
  • 拡張属性はアルファベット順にソート

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

正規化ルールの理解

ロケール識別子の各コンポーネントは、正規形式において特定の大文字小文字の使い分け規則を持ちます。

言語コードの大文字・小文字表記

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

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

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

文字体系コードの大文字・小文字表記

文字体系コードはタイトルケースを使用し、最初の文字は大文字、残りの3文字は小文字になります:

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)

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

拡張機能の順序

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

スクリプトコード 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 に並び替えられます。これは cahc より前に来て、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つの入力はすべて同じロケールを表すため、出力には正規化された識別子が1つだけ含まれます。

この重複排除は、ユーザー入力を処理する場合や、複数のソースからロケールリストをマージする場合に便利です。

無効な識別子の処理

配列内のロケール識別子が無効な場合、このメソッドは 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コンストラクタも正規化された出力を生成
  • 正規化はロケール識別子の意味を変更しない
  • 保存、比較、表示には正規化された識別子を使用

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