ReactとIntl.DisplayNamesで国名や言語名を表示する方法
Intl.DisplayNames を使うと、US のような国・地域のコードを アメリカ合衆国 に、en-US のような言語のコードを アメリカ英語 に変えられます。名前の一覧を自分で持つ必要はありません。日本語、英語、韓国語など、指定した言語での名前が返ります。
表示の言語が決まっているなら、React のコンポーネントの外で1回だけ作れば足ります。言語を切り替える画面では useMemo を使い、切り替えたときだけ作り直します。この記事では、国名の選択肢と言語の切り替えボタンを作り、Node.js と Chromium で返る名前の違いも比べました。
検証環境は Ubuntu 24.04、Node.js 22.23.4(ICU 78.3)、Chromium 153 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2、Playwright 1.63.0 です。.tsx を tsc で型チェックしてから esbuild で変換し、開発用ビルドで動かしました。ブラウザは Playwright で言語を日本語に設定して開いています。Firefox と Safari、本番用ビルドでは試していません。
type を決めて書式を作り、of() にコードを渡す
new Intl.DisplayNames() に、表示に使う言語と type を渡して書式を作り、of() にコードを渡します(MDN の Intl.DisplayNames() コンストラクターの説明)。type は省けません。Node.js で省くと TypeError になりました。
// 書式はモジュールの中で1回だけ作り、使い回す
const regionNames = new Intl.DisplayNames('ja', { type: 'region' });
const languageNames = new Intl.DisplayNames('ja', { type: 'language' });
export const regionName = (code: string) => regionNames.of(code);
export const languageName = (code: string) => languageNames.of(code);
Node.js での結果は次のとおりです。国・地域には2文字のコードのほか、419(ラテンアメリカ)のような3桁の地域のコードも渡せます。
| 渡したコード | regionName |
渡したコード | languageName |
|---|---|---|---|
JP |
日本 |
ja |
日本語 |
US |
アメリカ合衆国 |
en-US |
アメリカ英語 |
GB |
イギリス |
en-GB |
イギリス英語 |
KR |
韓国 |
zh-Hans |
簡体中国語 |
TW |
台湾 |
zh-TW |
中国語 (台湾) |
419 |
ラテンアメリカ |
pt-BR |
ポルトガル語 (ブラジル) |
EU |
欧州連合 |
ain |
アイヌ語 |
type には、ほかに文字の種類の script、通貨の currency、暦の calendar、年・月・曜日などの日時の項目名を返す dateTimeField があります。たとえば script の Hira は ひらがな、currency の JPY は 日本円、calendar の japanese は 和暦 でした。
短い名前や「英語 (アメリカ合衆国)」の形にする
国名が長すぎるときは、style: 'short' を指定します。long(既定)と short で比べると、日本語では次のように変わりました。
| コード | long |
short |
|---|---|---|
US |
アメリカ合衆国 |
アメリカ |
GB |
イギリス |
英国 |
HK |
中華人民共和国香港特別行政区 |
香港 |
MO |
中華人民共和国マカオ特別行政区 |
マカオ |
PS |
パレスチナ自治区 |
パレスチナ |
narrow は、この5つでは short と同じでした。
言語名は、languageDisplay で形を選べます。既定の dialect は、en-US のような地域の付いた言語に専用の名前があれば、それを使います。standard にすると、言語名のあとに括弧で地域や文字を添えます(MDN のコンストラクターの説明にある languageDisplay の項)。
| コード | dialect |
standard |
|---|---|---|
en-US |
アメリカ英語 |
英語 (アメリカ合衆国) |
zh-Hans |
簡体中国語 |
中国語 (簡体字) |
pt-BR |
ポルトガル語 (ブラジル) |
ポルトガル語 (ブラジル) |
専用の名前がない pt-BR では、どちらも同じでした。standard では、英語 (アメリカ合衆国)、英語 (イギリス) のように、どれも言語名から始まります。
名前のないコードは、そのまま返るか例外になる
of() に名前のないコードを渡したときの結果は、コードの形で2つに分かれます(MDN の of() の説明)。
形は合っていて名前がないだけのコード(国・地域なら英字2文字の AA など)は、既定の fallback: 'code' ではコードがそのまま返り、fallback: 'none' では undefined が返りました。形の合わないコードは、fallback の指定にかかわらず RangeError になります。3文字の USA、1文字の J、空の文字列のほか、言語の en_US(_ で区切ったもの)も RangeError でした。
| 渡したコード | fallback: 'code' |
fallback: 'none' |
|---|---|---|
JP |
日本 |
日本 |
AA |
AA |
undefined |
USA |
RangeError |
RangeError |
利用者が入力した値や、外部から受け取った値をそのまま渡すなら、例外も undefined にまとめておくと、画面の側で扱いやすくなります。
// 知らないコードは undefined、形の合わないコードも例外にせず undefined にする
const strictRegionNames = new Intl.DisplayNames('ja', { type: 'region', fallback: 'none' });
export function safeRegionName(code: string): string | undefined {
try {
return strictRegionNames.of(code);
} catch {
return undefined;
}
}
画面では {safeRegionName('AA') ?? '(名前がありません)'} のように書き、名前がないときの表示を決めます。
なお、小文字の jp を渡すと、名前にならずに jp のまま返りました(fallback: 'none' では undefined)。Chromium でも jp のまま返りました。MDN にも、小文字のコードはどこでも確実に動くとは限らないので大文字で渡すように、と書かれています。
TypeScript では、of() の戻り値の型は string | undefined です。string の変数にそのまま入れると、次のエラーになりました。
typecheck/undefined.ts(2,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
国の選択肢を、表示の言語で並べる
国・地域のコードの一覧は、Intl からは取れません。Intl.supportedValuesOf() は通貨や暦の一覧を返しますが、'region' を渡すと RangeError: Invalid key : region になりました(MDN の Intl.supportedValuesOf() の説明)。選択肢に出すコードは、自分で配列にして持ちます。
表示の言語を切り替える画面では、書式を useMemo でキャッシュし、言語か type が変わったときだけ作り直します(公式リファレンスの useMemo の説明)。
import { useMemo } from 'react';
// 表示する言語や種類が変わったときだけ、書式を作り直す
export function useDisplayNames(
locale: string,
type: Intl.DisplayNamesType,
): Intl.DisplayNames {
return useMemo(() => new Intl.DisplayNames(locale, { type }), [locale, type]);
}
選択肢は、表示する名前で並べ替えます。並べ方も表示の言語に合わせるため、Intl.Collator の compare を使います(MDN の Intl.Collator の説明)。
import { useMemo } from 'react';
import { useDisplayNames } from './useDisplayNames';
// 地域コードの一覧は Intl からは取れないので、自分で用意する
const REGION_CODES = ['JP', 'US', 'GB', 'KR', 'TW', 'CN', 'DE', 'FR', 'BR', 'IN', 'AU', 'CA'];
type Props = {
locale: string;
value: string;
onChange: (code: string) => void;
};
export function CountrySelect({ locale, value, onChange }: Props) {
const names = useDisplayNames(locale, 'region');
// 表示する名前で並べ替える。並べ方も表示の言語に合わせる
const options = useMemo(() => {
const collator = new Intl.Collator(locale);
return REGION_CODES.map((code) => ({ code, name: names.of(code) ?? code })).sort((a, b) =>
collator.compare(a.name, b.name),
);
}, [names, locale]);
return (
<select id="country" value={value} onChange={(e) => onChange(e.target.value)}>
{options.map(({ code, name }) => (
<option key={code} value={code}>
{name}
</option>
))}
</select>
);
}
value には国のコードを持たせているので、表示の言語を変えても、選んでいる国はそのままです。Chromium で開いた選択肢の順番は、言語ごとに次のようになりました。
ja: アメリカ合衆国 / イギリス / インド / オーストラリア / カナダ / ドイツ / ブラジル / フランス / 韓国 / 台湾 / 中国 / 日本
en: Australia / Brazil / Canada / China / France / Germany / India / Japan / South Korea / Taiwan / United Kingdom / United States
ko: 대만 / 대한민국 / 독일 / 미국 / 브라질 / 영국 / 오스트레일리아 / 인도 / 일본 / 중국 / 캐나다 / 프랑스
日本語では、カタカナの国名が五十音順に並び、漢字の国名はそのあとに来ました。漢字の国名は読みの順にはなりません。Node.js で 北朝鮮 を足して並べると、韓国 / 台湾 / 中国 / 日本 / 北朝鮮 の順でした。読みの順に並べたいなら、読みを別に持って、それで並べ替えます。
なお、sort() を比べる関数なしで呼ぶと、文字コードの順になり、フランス が ブラジル より前に来ました。
言語の切り替えボタンは、その言語自身の名前で書く
言語の切り替えボタンは、読めない言語で表示されていても見つけられるよう、それぞれの言語自身の名前で書くのが便利です。書式の言語と、of() に渡すコードを同じにすると、その言語での名前が返ります。
import { useDisplayNames } from './useDisplayNames';
const LANGUAGES = ['ja', 'en', 'ko', 'zh-Hans', 'fr'];
type Props = {
locale: string;
onChange: (locale: string) => void;
};
// それぞれの言語の名前を、その言語自身で書く(「English」「한국어」など)。
// 表示の言語によらないので、モジュールの中で1回だけ作っておく
const NATIVE_NAMES = Object.fromEntries(
LANGUAGES.map((code) => [code, new Intl.DisplayNames([code], { type: 'language' }).of(code)]),
);
export function LanguagePicker({ locale, onChange }: Props) {
// いまの表示の言語での名前も添える
const names = useDisplayNames(locale, 'language');
return (
<ul>
{LANGUAGES.map((code) => (
<li key={code}>
<button type="button" id={'lang-' + code} aria-pressed={code === locale} onClick={() => onChange(code)}>
{NATIVE_NAMES[code]}
{code !== locale && <small>({names.of(code)})</small>}
</button>
</li>
))}
</ul>
);
}
NATIVE_NAMES は、ja で 日本語、en で English、ko で 한국어、zh-Hans で 简体中文、fr で français を返しました。括弧の中には、いまの表示の言語での名前が入ります。

useMemo が効いているかは、useDisplayNames の中で new Intl.DisplayNames() を呼んだ回数で確かめました。この2つのコンポーネントを並べた画面では、最初の表示で2回でした。そのあと再描画のボタンを3回押し、国を選び直しても、回数は増えませんでした。表示の言語を英語、韓国語、日本語と順に切り替えると、4回、6回、8回と増えました。切り替えのたびに、国名用と言語名用の2つが作り直されています。
サーバーで描くときは、Node.js とブラウザで名前が違うことがある
同じ言語・同じコードでも、返る名前は実行している環境のデータで決まります。検証環境で、英字2文字のコード AA〜ZZ すべてについて ja の名前を比べたところ、名前が返るコードは Node.js と Chromium でどちらも280個でした。そのうち long では4つの名前が違っていました。
| コード | Node.js | Chromium |
|---|---|---|
HK |
中華人民共和国香港特別行政区 |
香港 |
MO |
中華人民共和国マカオ特別行政区 |
マカオ |
PS |
パレスチナ自治区 |
パレスチナ |
FK |
フォークランド諸島 |
フォークランド諸島 (マルビーナス諸島) |
short で違ったのは FK の1つだけでした。
公式リファレンスの hydrateRoot の説明には、サーバーで描いた内容とブラウザでの最初の描画が同じになる必要がある、と書かれています。食い違いはバグとして直すものとされ、開発モードでは React が警告を出します。
この記事の画面は、選択肢に香港などを入れていません。Node.js の renderToString で描いた HTML と Chromium で描いた画面を比べると、ボタン・選択肢・選んだ国の18か所の文字がすべて同じでした。一方、上の表の地域を選択肢に入れてサーバーで描くと、サーバー(Node.js)で描いた名前と、ブラウザ(Chromium)で描いた名前が食い違います。
また、表示の言語を省くと、実行している環境の既定の言語が使われます。検証環境では、Node.js が en-US、日本語に設定した Chromium が ja-JP でした。サーバーで描く画面では、言語は省かずに指定します。
TypeScript では lib を es2020 以降にする
Intl.DisplayNames の型は、tsconfig.json の lib が es2020 以降で使えます。lib を es2019 にすると、次のエラーになりました。
src/displayNames.ts(2,30): error TS2550: Property 'DisplayNames' does not exist on type 'typeof Intl'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2020' or later.
ただし、@types/node を読み込んでいると、lib が es2019 でもエラーになりませんでした。@types/node の index.d.ts に、es2020 の型を読み込む /// <reference lib="es2020" /> の行があるためです。





