ロケール
defineLocales がアプリケーションのロケール集合を宣言します。一覧に依存するもの、つまり所属判定・交渉・URL の区間・[locale] のスキーマ・静的化のパス・描画中のロケールは、どれもこの戻り値のメンバーです。
defineLocales(all, options?)
ロケールのタグを並べた配列と、省略できる default を受け取ります。タグはリテラル型のまま推論されるので、as const は要りません。
// src/i18n.ts
import { defineLocales } from '@k8ordo/i18n';
import type { LocaleOf } from '@k8ordo/i18n';
export const locales = defineLocales(['en-US', 'en-GB', 'ja'], {
default: 'ja',
});
export type Locale = LocaleOf<typeof locales>;
declare module '@k8ordo/i18n' {
interface Register {
locale: Locale;
}
}default を省くと先頭のタグが既定値です。default の型は一覧の和集合なので、一覧に無いタグはコンパイルで落ちます。
誤った一覧は、使われた時点ではなく定義した時点で TypeError を投げます。重複したタグと BCP 47 ではないタグは型では検出されないので、検出するのはこの検査だけです。一覧に無い default はコンパイルでも落ちますが、JavaScript からの呼び出しや as で通した値のために実行時にも確かめます。
| 呼び出し | 投げるエラーの文面 |
|---|---|
defineLocales(['ja', 'en'], { default: 'fr' }) | defineLocales: the default "fr" is not in ["ja","en"] |
defineLocales(['ja', 'ja']) | defineLocales: a locale is listed twice in ["ja","ja"] |
defineLocales(['ja', 'not a tag']) | defineLocales: "not a tag" is not a BCP 47 language tag |
アプリケーションに集合は 1 つ
defineLocales は値を返すだけでなく、message() が読む既定値と所属判定を登録します。message() が集合を引数に取らないのは、取ると宣言がバンドラに副作用と見なされ、使われない文言を落とせなくなるからです。
登録は後勝ちです。開発サーバーがロケールを足したモジュールを評価し直したとき、次に呼ばれた文言が新しい集合を読むためです。その反面、テストやデモや補助関数の中で別の集合を defineLocales すると、その時点からすべての文言がその集合を読みます。集合は 1 つのモジュールで定義して export し、ほかの場所では import してください。
集合のメンバー
戻り値は Locales<L, D> 型です。L はタグの和集合、D は既定値のタグです。
| メンバー | 内容 |
|---|---|
all | 一覧のタグ。書いた順のままです。 |
default | 既定のロケール。交渉で何も一致しなかったときと、何もロケールを指名していないときに使われます。 |
is(value) | 所属判定の型ガード。完全一致で、大文字小文字を区別します(is('EN') は false)。 |
negotiate(requested) | 希望の並びに対して、対応する最良のロケールを返します(下の「交渉」)。 |
localize(pathname, locale) | pathname の前にロケールの区間を付けます。'/ui' は '/en/ui'、'/' は '/en' になります。 |
delocalize(pathname) | 逆向きです。'/en/ui' は { locale: 'en', pathname: '/ui' }、ロケールで始まらない pathname は locale: null です。 |
paths(patterns) | @k8ordo/static の paths オプション。/:locale 区間を持つパターンをロケールの数だけ展開します。 |
paramsSchema | [locale] 区間のスキーマ(Standard Schema)。受理したロケールが、受理したページの描画のロケールになります。 |
getLocale() | 描画中のロケール。hook ではないので、どこからでも呼べます。 |
run(locale, fn) | サーバー専用。fn と、そこから始まる処理を locale のもとで実行します。 |
URL に関わるメンバー(localize・delocalize・paths・paramsSchema・getLocale・run)の使い方は、別のページにまとめています。
型
| 型 | 内容 |
|---|---|
Locales<L, D> | defineLocales の戻り値の型。 |
LocaleOf<typeof locales> | 集合のタグの和集合。Register の locale にもこれを書きます。 |
LocalesOptions<D> | 第 2 引数の型({ default?: D })。 |
Delocalized<L> | delocalize の戻り値({ locale: L | null; pathname: string })。pathname は空にならず、少なくとも / です。 |
LocaleParamsSchema<L> | paramsSchema の型。スキーマライブラリに依存しないよう、Standard Schema の形をこのパッケージ自身が宣言しています。 |
BCP 47 のタグ
タグは Intl.Locale が受け付けるかどうかで検査します。en_US のような区切りの誤りや *** は BCP 47 ではありません。
集合に書いた綴りがそのまま URL の区間になります。en-US と書けば /en-US/… です。URL に出したい綴りで書いてください。
交渉
negotiate(requested) は、希望の並び(navigator.languages や、Accept-Language を解析した結果)から、対応する最良のロケールを 1 つ返します。
- 希望のタグを先頭から 1 つずつ見ます。
- そのタグと完全に一致するロケールがあれば、それを返します(大文字小文字は区別しません)。
- 無ければ、同じ言語を話す最初の対応ロケールを返します。
- BCP 47 ではないタグは、throw せずに飛ばします。希望の並びは利用者の入力だからです。
- 最後まで一致が無ければ、既定値を返します。
- 交渉の完全一致は大文字小文字を区別しません(
en-gbはen-GBに一致し、集合側の綴りで返ります)。isとdelocalizeは区別します。 - 言語での一致は、言語サブタグ(
Intl.Localeのlanguage)だけを比べます。用字や地域は比べないので、['zh-Hans', 'zh-Hant']にzh-Hant-TWを求めると、先に書いたzh-Hansが選ばれます。同じ言語のロケールが複数あるときは、書いた順が言語一致で選ばれる順です。
全タグの完全一致を先に探さず、タグごとに言語まで見てから次へ進むのは、利用者の 1 番目の希望を優先するためです。['en-US', 'ja'] を送る人は英語を最も望んでいるので、集合に en があれば、2 番目の ja が完全一致でも en を返します。
引数は Iterable<string> なので、navigator.languages も配列も Set もそのまま渡せます。
// src/preferred-locale.ts
import { parseAcceptLanguage } from '@k8ordo/i18n';
import { locales } from './i18n';
import type { Locale } from './i18n';
export const fromBrowser = (): Locale => locales.negotiate(navigator.languages);
export const fromHeaders = (headers: Headers): Locale =>
locales.negotiate(parseAcceptLanguage(headers.get('accept-language')));例
集合 defineLocales(['ja', 'en', 'en-GB']) に対する結果です。
| 希望 | 結果 | 理由 |
|---|---|---|
['en-GB'] | en-GB | 完全一致 |
['EN-gb'] | en-GB | 完全一致(大文字小文字は区別しない) |
['en-US', 'en-GB'] | en | en-US の言語で、先に en が見つかる |
['en-US', 'ja'] | en | 1 番目の希望の言語が、2 番目の完全一致より先 |
['ja-JP', 'en'] | ja | ja-JP の言語で一致 |
['***', 'en'] | en | *** は BCP 47 ではないので飛ばす |
['fr', 'de'] | ja | 一致なし。既定値 |
[] | ja | 空の並び。既定値 |
parseAcceptLanguage(header)
Accept-Language ヘッダーを、negotiate に渡せる希望の並びにします。サーバーでヘッダーから交渉するときに使います。
qの重みの大きい順に並べます。qの無いタグの重みは 1 です。- 重みが同じタグは、ヘッダーに書かれた順を保ちます。
- 重みが 0 以下のタグ(空の
q=は0と読みます)と*は落とします。*は「何でもよい」という意味で、それは既定値がすでに表しています。 - ヘッダーが無い(
null)か空白だけなら、空の並びを返します。 - 空白、
Q=のような大文字の引数名、知らない引数は許容します。q=abcのように数値にならないqは無視され、重みは 1 のままです。 - タグ自体は検査しません。BCP 47 ではないタグは
negotiateが飛ばします。
| ヘッダー | 結果 |
|---|---|
parseAcceptLanguage('en-US;q=0.8, ja, en;q=0.9, fr;q=0.8') | ['ja', 'en', 'en-US', 'fr'] |
parseAcceptLanguage('*;q=0.5, en;q=0, ja') | ['ja'] |
parseAcceptLanguage(' en ; foo=bar ; Q=0.5 ,ja') | ['ja', 'en'] |
parseAcceptLanguage(null) | [] |
交渉を試す
ヘッダーを書き換えると、parseAcceptLanguage が作る並び、各タグが集合のどのロケールに一致したか、negotiate の答えがその場で変わります。集合はこのサイト自身の locales(ja と en、既定は ja)です。説明用に別の集合を定義しないのは、上で書いたとおり、2 つ目の defineLocales がこのページの文言が読む集合を置き換えてしまうからです。
parseAcceptLanguage が返す並びfr-CH一致なしfr一致なしen-US言語が一致 → enja見ない(すでに決まった)
negotiate の答えen