@k8ordo/i18n

静的に書き出す

@k8ordo/staticは、パラメータを持つページのpathnameを、ビルドの時点で知る必要があります。ロケールの区間はどのページでも同じ値を取るので、その一覧はロケールの集合が作ります。このページでは、locales.pathsの渡し方と、ほかのパラメータがあるときの書き方、404ページの言語を説明します。

このページの内容

ロケールの数だけページを書き出す

frameworkのpathsオプションにlocales.pathsを渡します。/:localeの区間を持つパターンが、ロケールの数だけのpathnameになります。

vite.config.ts
import { framework } from '@k8ordo/static';
import { defineConfig } from 'vite';

import { locales } from './src/i18n';

export default defineConfig({
  plugins: [framework({ paths: locales.paths })],
});
ts
locales.paths(['/:locale', '/:locale/about']);
// ['/ja', '/en', '/ja/about', '/en/about']

置き換えは区間ごとに行います。そのため、/:localeCodeのように名前がlocaleで始まるだけのパラメータには触れません。

書き出すpathnameは、1つずつ別のリクエストとして描かれます。ページごとにスキーマがロケールを受け付けるので、文言はそのページのロケールで書かれます。ビルドは複数のページを同時に描きますが、あるページのロケールがほかのページに漏れることはありません。

ほかのパラメータもあるとき

/:locale/blog/:slugのように、ほかのパラメータも持つパターンは、locales.pathsを通しても/ja/blog/:slugのように:slugが残ります。

ビルドはパラメータの残ったpathnameを書き出せないので、static build needs pathnames for /:locale/blog/:slug — supply them with the "paths" optionというエラーで止まります。

vite.config.ts
import { framework } from '@k8ordo/static';
import { defineConfig } from 'vite';

import { locales } from './src/i18n';
import { readSlugs } from './src/posts';

const expandSlugs = async (paths: string[]) => {
  const slugs = await readSlugs();
  return paths.flatMap((path) =>
    path.includes('/:slug')
      ? slugs.map((slug) => path.replace('/:slug', `/${slug}`))
      : [path],
  );
};

export default defineConfig({
  plugins: [
    framework({
      paths: (patterns) => expandSlugs(locales.paths(patterns)),
    }),
  ],
});

先にlocales.pathsでロケールを展開し、残ったパラメータを同じ関数の中で展開します。pathsにはPromiseを返す関数も渡せるので、記事の一覧を読み込んでから答えられます。

404ページの言語

静的なホストは、知らないURLすべてに1つの404.htmlを返します。このファイルはビルドの番兵の区間で1回だけ描かれるので、文言は既定のロケールで書かれます。

番兵の区間はスキーマが受け付けないため、ビルドがそれまでにどのロケールのページを描いていても、404.htmlは既定のロケールになります。1つのファイルを、訪問者ごとの言語に合わせることはできません。

ブラウザは、このファイルをハイドレーションしません。別のURLのために描かれたものなので、ハイドレーション中に読んだ文言と食い違うからです。代わりに訪問者のURLで描き直すので、Client Componentはそこで訪問者のロケールになります。

そのため、not-found.tsxはClient Componentにして文言を描きます。JavaScriptが動かない訪問者には、既定のロケールのまま届きます。

routes/[locale]/not-found.tsx
'use client';

import * as m from '../../messages';

export default function NotFound() {
  return (
    <main>
      <h1>{m.notFound.title()}</h1>
      <p>{m.notFound.description()}</p>
    </main>
  );
}

<html lang>は、ルートレイアウトがサーバーで書いた既定のロケールのまま残ります。このサイトでは、[locale]のレイアウトが描くClient Componentが、effectの中でdocument.documentElement.langを訪問者のロケールに直しています。

情報

メモ

開発サーバーと@k8ordo/serverでは、404は訪問者のURLで描かれます。URLの区間が集合のロケールなら、サーバーが書いたHTMLの時点でそのロケールです。

k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2