Intl.DisplayNames()を使って言語、地域、文字体系を翻訳する

JavaScriptの標準組み込みオブジェクトに`Intl`オブジェクトがあります。国際化APIの名前空間で、`locale`に依存した数値のフォーマットや分かち書きを行うプロパティを持ちます。

この記事ではそんなプロパティの1つである`DisplayNames`について解説します。

## DisplayNames
`DisplayNames`は言語、地域、文字体系を`locale`に応じて翻訳するためのオブジェクトです。

```ts
const regionNames = new Intl.DisplayNames('ja-JP', { type: 'region' });
// アメリカ合衆国
console.log(regionNames.of('US'));
```

`DisplayNames`を初期化するタイミングで扱いたい`locale`と翻訳する文字列の種類を指定します。
```ts
const regionNames = new Intl.DisplayNames('ja-JP', { type: 'region' });
```

第2引数では翻訳する文字列の種類のほかにオプションを渡せます。
```ts
const regionNames = new Intl.DisplayNames(
  'ja-JP',
  {
    localeMatcher: 'lookup',
    style: 'narrow',
    type: 'language',
    code: 'none',
    languageDisplay: 'standard',
  },
);
```

`localeMathcer`は第一引数の`locale`が配列で複数渡ってきた時の優先順位の付け方を決めます。デフォルトでは`best fit`で他に`lookup`を指定できます。
`best fit`はリクエストやランタイムを用いて最適な`locale`を決定し、`lookup`を指定した場合は[`Lookup`](https://datatracker.ietf.org/doc/html/rfc4647#section-3.4)のアルゴリズムによって決定されます。

`style`は翻訳後の文字の長さを指定します。デフォルトは`long`です。他の選択肢としては`short`と`narrow`が存在します(`narrow`が有効になる例を見つけられませんでした🙏)。

```ts
const languageNames1 = new Intl.DisplayNames(
  'ja-JP',
  {
    type: 'language',
  },
);
const languageNames2 = new Intl.DisplayNames(
  'ja-JP',
  {
    type: 'language',
    style: 'short',
  },
);
const languageNames3 = new Intl.DisplayNames(
  'ja-JP',
  {
    type: 'language',
    style: 'narrow',
  },
);
// 英語（イギリス）
console.log(languageNames1.of('en-GB'));
// 英語（英国）
console.log(languageNames2.of('en-GB'));
// 英語(英国)
console.log(languageNames3.of('en-GB'));
```

`type`は翻訳する文字の種類を指定します。このオプションは指定が必須です。存在する種類は`language`、`region`、`script`、`currency`、`calendar`、`dateTimeField`です。

```ts
const languageNames = new Intl.DisplayNames('ja-JP', { type: 'language' });
// フランス語
console.log(languageNames.of('fr'));

const regionNames = new Intl.DisplayNames('ja-JP', { type: 'region' });
// アメリカ合衆国
console.log(regionNames.of('US'));

const scriptNames = new Intl.DisplayNames(
  'ja-JP',
  {
    type: 'script',
  },
);
// 仮名
console.log(scriptNames.of('Hrkt'));

const currencyNames = new Intl.DisplayNames('ja-JP', { type: 'currency' });
// ユーロ 
console.log(currencyNames.of('EUR'));

const calendarNames = new Intl.DisplayNames('ja-JP', { type: 'calendar' });
// 中華民国暦 
console.log(calendarNames.of('roc'));

const dateTimeFieldNames = new Intl.DisplayNames('ja-JP', { type: 'dateTimeField' });
// 年
console.log(dateTimeFieldNames.of('year'));
```

`fallback`は翻訳先がない時に返す値を決めます。デフォルトでは`code`で受け取った値をそのまま返します。この他には`none`があり、`undefiend`を返します。

```ts
const regionNames1 = new Intl.DisplayNames(
  'ja-JP',
  {
    type: 'region',
  },
);
const regionNames2 = new Intl.DisplayNames(
  'ja-JP',
  {
    type: 'region',
    fallback: 'none',
  },
);

// hk
regionNames1.of('hk');
// undefined
regionNames2.of('hk');
```

`languageDisplay`は`type`が`language`の場合にのみ使えます。デフォルトは`"dialect`で現地での呼ばれ方に翻訳されます。`standard`では一般的な表現になります。

```ts
const languageNames1 = new Intl.DisplayNames('ja-JP', {
  type: 'language',
});
const languageNames2 = new Intl.DisplayNames('ja-JP', {
  type: 'language',
  languageDisplay: 'standard',
});

// アメリカ英語
console.log(languageNames1.of('en-US'));
// 英語（アメリカ合衆国）
console.log(languageNames2.of('en-US'));
```

上記の例からもわかるように`DisplayNames`オブジェクトの作成後は`of`メソッドに渡した文字列が翻訳されます。`type`に対応した文字列を渡せなかった場合はエラーが発生するので注意してください。
`type`に対応する文字列は[ECMAScriptの仕様書](https://tc39.es/ecma402/#sec-canonicalcodefordisplaynames)で定義されています。


## localeが対応していることを確認する
`DisplayNames`オブジェクトに渡す`locale`は静的メソッドである`supportedLocalesOf`を用いて確認できます。引数は`DisplayNames`オブジェクトのコンストラクタと同じです。

```ts
// ['ja', 'ja-JP']
console.log(
  Intl.DisplayNames.supportedLocalesOf(
    ['ja', 'JP', 'ja-JP'],
  )
);
```
第二引数には`localeMatcher`を渡せます。`type`はコンストラクタでは必須ですが、ここでは必要ありません。
```ts
// ['ja', 'ja-JP']
console.log(
  Intl.DisplayNames.supportedLocalesOf(
    ['ja', 'JP', 'ja-JP'],
    {
      localeMatcher: 'lookup',
    }
  )
);
```

## 有効なオプションを確認する
`resolvedOptions`は`DisplayNames`オブジェクトを作成したときに有効となった設定を表示します。
```ts
const languageNames = new Intl.DisplayNames(['ja-JP', 'en-US'], {
  type: 'language',
  languageDisplay: 'standard',
});
/*
 * {
 *   locale: 'ja-JP',
 *   style: 'long',
 *   type: 'language',
 *   fallback: 'code',
 *   languageDisplay: 'standard'
 * }
*/
console.log(languageNames.resolvedOptions());
```
`locale`は渡したままの状態ではなく`localeMatcher`で決められたものが渡されます。