手書き i18n:3 言語サイトでルーティングを自作する代償
このサイトの i18n は Astro 組み込みの設定を使わず自作しました。その理由と、代償が実際に現れた場所——2 回の 404 と、半分しか翻訳されていなかったブログについて書きます。
Astro の i18n 設定は使わず、言語まわりは全部自分で書きました。フレームワークが悪いからではありません。このサイトでは、多言語は描画の問題である前にコンテンツ管理の問題だからです。文言を英語に差し替えるのは簡単な半分で、3 言語の構造を揃え続けるのが難しい半分です。
自作したもの
3 つだけです。
[lang]の動的ルート 1 本。ページファイルは 1 つでzh/en/jaを兼ねます。- 言語別のコンテンツディレクトリ:
src/content/blog/<lang>/<slug>.md。言語は entry id の接頭辞から取ります。 - 文言辞書 1 つ:
src/lib/i18n.tsのLOCALES/LOCALE_META/t(locale, key, vars)。
ルート生成の唯一の真実は src/server/paths.ts です。ページは export const getStaticPaths = xxxPaths するだけ。
フレームワークの i18n との違い
| フレームワークの i18n | 自作 | |
|---|---|---|
| ルートの接頭辞 | 設定から生成 | こちらの [lang] で明示 |
| 言語判定のリダイレクト | フレームワーク任せ | / で自作 |
| 翻訳が無いとき | 既定言語へ黙ってフォールバック | その言語には記事が存在しない |
| 型 | フレームワークの locale 型 | 自前の Locale、コンパイル時に絞れる |
| 壊れたときに在る場所 | フレームワークの中 | 自分のコードの中 |
3 行目が決め手でした。「既定言語にフォールバック」は親切に聞こえますが、実際には英語の読者が開いた記事が中国語で出てくるという意味です。読者に「押し間違えたかな」と思わせるより、空欄のほうがましです。
黙ったフォールバックはコンテンツの欠落を隠します。欠けているものは、欠けていると分かる形にすべきです。
代償 1:パス生成は 1 か所だけ
自作 i18n で一番よくやる失敗は、言語の接頭辞を「都合のいい場所で連結する文字列の一部」として扱うことです。
実際にやりました。appHref(kind, slug) が /tools/convert を返していました——接頭辞なし。本当のルートは /[lang]/[kind]/[slug]/ なので、ホームと 2 つの一覧ページにあるアプリへのリンクが全部 404 でした。しかも中国語版は正常に見えます。その経路では別の場所が接頭辞を足していたからです。英語に切り替えて初めて露見しました。
直し方は「接頭辞を忘れない」ではなく、パス生成を 1 つの関数に閉じ込めることでした。
// src/server/meta.ts
export function href(locale: string, path = '/'): string {
const normalized = path.startsWith('/') ? path : '/' + path;
return '/' + locale + (normalized === '/' ? '/' : normalized);
}
今はどのページもコンポーネントも URL を連結していません。ルートを足すときは server/paths.ts に xxxPaths を足します。
代償 2:3 言語揃いは統率であってコードではない
1 言語 1 ファイルなので、「翻訳が追いついていない」はビルド時に完全に合法です。ビルドは通り、ページも出て、ただ英語版に記事が数本足りないだけ。
実際の状態は中国語 3 本に対して英語 1 本、日本語 1 本でした。一覧・アーカイブ・タグ・RSS はすべてその言語の記事配列から生えるので、英語の読者には 1 本だけのブログが見えていました。中国語版は完璧に見えます。
これに技術的な解決策はありません。ルールを書き残すだけです。自分の場合は、agent 向けの執筆仕様に書きました。
zh/x.mdを足したらen/x.mdとja/x.mdも同時に足す。1 言語でも欠けると、その言語の一覧・アーカイブ・タグ・RSS にその記事は出ない。
タグ語彙を言語ごとに持っているのも同じ理由です。タグは言語間で揃えないので、放っておくと performance / パフォーマンス / 性能 が 1 本ずつを指す状態になります。
今の形
- 言語を足す:
LOCALES・LOCALE_META・辞書を増やし、content/blog/にディレクトリを足す。 - 記事を足す:ファイル 3 つ。それ以外は何もしない。
- ナビの文言を変える:辞書の 3 言語分を 1 行ずつ。
代償は約束事を自分で覚え続けることで、フレームワークは何も検査してくれません。見返りは、すべての挙動が読めるコードの中にあり、制御できないフォールバック層が無いことです。
i18n の価値は「多言語に対応していること」ではなく、欠けたときにどれだけ正直に振る舞うかにあります。

コメント
…