Get Started
@k8ordo/i18n はアプリケーションのロケール軸を持つパッケージです。このページでは、ロケール集合を 1 か所で定義して [locale] 区間に結び、最初の文言を Server Component と Client Component の両方で描くまでを順に進めます。
担当する範囲
どのロケールがあり、どれが既定か。この一覧に依存するものを、defineLocales が返す 1 つの値から導きます。
- ロケール集合と既定のロケール。所属判定と BCP 47 の検査も含みます。
- URL の先頭区間(
/ja/…)。区間を付け外しするlocalize/delocalizeと、[locale]ルートのparamsSchemaを出します。 navigator.languagesやAccept-Languageからの交渉。- 描画中のロケール。サーバーではリクエストが受理した区間、ブラウザでは URL です。
- 文言。
message()で 1 つずつ宣言する関数で、呼ばれた場所のロケールで文字列を返します。
持たないもの
- pathname。ロケール区間より後ろは
@k8ordo/routerのものです。 - 文言の文法。プレースホルダ記法も ICU もありません。値の埋め込みはテンプレートリテラル、複数形は
Intl.PluralRules、日付と数値はIntlの書式化で書きます。 - 読み込み。文言は普通の export なので、どの文言がブラウザに届くかは、各 Client Component が何を import したかからバンドラが決めます。ローダーも名前空間の一覧もありません。
ロケールを URL に置くので、ブラウザ側に同期すべき状態がありません。言語を変えることは、同じ pathname を別の区間の下へ移動することです。文言を関数にするので、引数は TypeScript が検査し、呼ばれない文言はバンドラが落とします。
インストール
ランタイムの依存はありません。React もスキーマライブラリも import しないので、peer dependency は同梱の型定義を読む TypeScript だけです。
npm install @k8ordo/i18n| パッケージ | バージョン | 用途 |
|---|---|---|
typescript | >= 7.0.2 | 同梱の型定義(省略可) |
サーバーでは、描画中のロケールを node:async_hooks の AsyncLocalStorage に載せます。import ではなく process.getBuiltinModule で取り出すので、同じビルドがブラウザでもそのまま動きます(フレームワークの 2 つのモードは Node 24 以降を求めます)。process.getBuiltinModule の無いランタイムではロケールを描画に結び付けられないので、paramsSchema の受理も run も throw します。
ロケール集合を 1 か所で定義する
ロケールの一覧を書くのはこのモジュールだけです。[locale] のスキーマ、静的化のパス展開、言語切替、/ の振り分け、全文言の型は、どれもここから読みます。
// src/i18n.ts
import { defineLocales } from '@k8ordo/i18n';
import type { LocaleOf } from '@k8ordo/i18n';
export const locales = defineLocales(['ja', 'en']);
declare module '@k8ordo/i18n' {
interface Register {
locale: LocaleOf<typeof locales>;
}
}先頭のロケールが既定値です。別のロケールを既定にするときは defineLocales(['en', 'ja'], { default: 'ja' }) と書きます。既定値は、交渉で何も一致しなかったときと、何もロケールを指名していないときに使われます。
Register にロケールを載せるのは 1 回だけです。載せた後は、すべての message() がこのロケールの和集合に照らして検査されます。Register はマージされるための型なので、type ではなく interface で書きます。
[locale] 区間に結ぶ
すべてのページを src/routes/[locale]/ の下に置き、その区間のレイアウトから集合の paramsSchema を export します。ルートの paramsSchema を走らせるのは @k8ordo/static と @k8ordo/server です。スキーマライブラリは要りません。集合自身が Standard Schema の形でスキーマを出します。
// src/routes/[locale]/layout.tsx
import type { ReactNode } from 'react';
import { locales } from '../../i18n';
export const { paramsSchema } = locales;
export default function LocaleLayout({ children }: { children: ReactNode }) {
return children;
}- 一覧に無いロケールは受理されません。
/fr/…はこのパターンが答えない pathname になり、最後はnot-found.tsxが本物の 404 で答えます。 - 受理したロケールは、受理したページの描画の間ずっと現在のロケールになり、ほかのページや 404 には届きません。Server Component も、HTML を作るためにサーバーで走る Client Component も、同じロケールで文言を読みます。
- このファイルに
'use client'を付けてはいけません。Client モジュールから export した値は、スキーマとしてではなく client reference としてハンドラに届くからです。フックを使う枠が必要なら、_parts/の Client Component に分けてレイアウトから描きます。
最初の文言
文言は 1 つずつ export します。値を取る文言は、そのロケールの文を返す関数です。
// src/messages/home.ts
import { message } from '@k8ordo/i18n';
export const title = message({ ja: 'ようこそ', en: 'Welcome' });
export const nameLabel = message({ ja: '名前', en: 'Name' });
export const greeting = message({
ja: (name: string) => `こんにちは、${name}さん`,
en: (name) => `Hello, ${name}`,
});引数の型は ja に注釈した関数から決まり、en もその型に縛られます。どちらかのロケールを書き忘れると、宣言の時点でコンパイルが通りません。
Server Component から描く
呼ぶだけです。サーバーでは、[locale] のスキーマがこのリクエストで受理したロケールの文が返ります。
// src/routes/[locale]/page.tsx
import * as home from '../../messages/home';
import { Greeting } from './_parts/greeting';
export default function HomePage() {
return (
<main>
<h1>{home.title()}</h1>
<Greeting />
</main>
);
}Client Component から描く
書き方は同じです。Provider も hook もありません。ブラウザでは URL の先頭区間がロケールなので、/en/… を開いていれば英語の文が返ります。
// src/routes/[locale]/_parts/greeting.tsx
'use client';
import { useState } from 'react';
import * as home from '../../../messages/home';
export function Greeting() {
const [name, setName] = useState('k8o');
return (
<div>
<label>
{home.nameLabel()}
<input
onChange={(event) => {
setName(event.currentTarget.value);
}}
value={name}
/>
</label>
<p>{home.greeting(name)}</p>
</div>
);
}Server Component から Client Component へ文言を props で渡すときは、呼んだ結果の文字列を渡します。関数は Server Component の境界を越えられません。
保証されること
- 一覧に無いロケールはページに届きません。スキーマが拒みます。
Registerにロケールを載せた後は、ロケールが欠けた文言はコンパイルが通りません。JavaScript から呼んだ場合やasで型を通した場合は、読まれた時点で欠けたロケールと存在するロケールを示して throw し、undefinedを返すことはありません。- 値を取る文言の引数は、関数の型で検査されます。
[locale]の下のページは、URL が示すロケールで描かれます。サーバーではスキーマが受理したロケール、ブラウザでは URL そのものを読みます。