@k8ordo/i18n

言語を切り替える

言語を切り替えるのは、今のページと同じpathnameを、別のロケールの区間の下で開き直すことです。このページでは、localizeとdelocalizeで切り替え先のURLを作り、言語の切り替えを組み立てて、選んだ言語をCookieに覚えておくまでを説明します。

このページの内容

切り替え先のURLを作る

今のpathnameからdelocalizeでロケールの区間を外し、localizeで別のロケールの区間を付けます。

ts
locales.delocalize('/ja/products/42');
// { locale: 'ja', pathname: '/products/42' }

locales.localize('/products/42', 'en');
// '/en/products/42'
  • delocalizeは、区間を単位に見ます。/englishの先頭の区間はenglishなので、ロケールとはみなさずにlocale: nullを返します。
  • 先頭の区間がロケールでないとき、delocalizeは既定のロケールを推測しません。nullを返すので、何に落とすかは呼ぶ側が決めます。
  • /enと/en/から区間を外した残りは/です。反対に、localize('/', 'en')は/enを返します。
警告

落とし穴

localizeは、区間がすでに付いているかを確かめません。localize('/en/ui', 'ja')は/ja/en/uiになるので、必ずdelocalizeしたpathnameを渡します。また、/で始まらない値にはTypeErrorを投げます。

Playground

切り替え先のURLを試す

pathnameを入れると、delocalizeの結果と、そこから各ロケールへlocalizeした結果を表示します。集合はこのサイトのlocales(jaとen)です。

delocalize('/ja/products/42')
{ locale: 'ja', pathname: '/products/42' }
localize('/products/42', 'ja')
'/ja/products/42'
localize('/products/42', 'en')
'/en/products/42'

試してみる

  1. 最初に入っている/ja/products/42では、localeがja、pathnameが/products/42になり、enへの切り替え先は/en/products/42です。
  2. /englishに書き換えると、先頭の区間はロケールではないので、localeがnullになります。
  3. /en/に書き換えると、区間を外した残りのpathnameは/になります。
  4. productsのように/で始まらない値に書き換えると、localizeが投げたTypeErrorの文面を表示します。

言語の切り替えを作る

切り替えのリンクは、今いるページのpathnameから作ります。pathnameは@k8ordo/routerのusePathname()で読むので、切り替えはClient Componentにします。

language-switcher.tsx
'use client';

import type { Variants } from '@k8ordo/i18n';
import { usePathname } from '@k8ordo/router';

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

const NAMES: Variants<string> = { ja: '日本語', en: 'English' };

export function LanguageSwitcher() {
  const { pathname } = locales.delocalize(usePathname());
  const current = locales.getLocale();

  return (
    <ul>
      {locales.all.map((locale) => (
        <li key={locale}>
          <a
            aria-current={locale === current ? 'true' : undefined}
            href={locales.localize(pathname, locale)}
            hrefLang={locale}
            lang={locale}
          >
            {NAMES[locale]}
          </a>
        </li>
      ))}
    </ul>
  );
}

locationを読まずにusePathname()を使うのは、サーバーでの描画にはlocationが無いからです。usePathname()はサーバーでもそのページのpathnameを返し、ページを移動するたびに新しいpathnameで描き直します。

素の<a>で書いても、@k8ordo/routerがNavigation APIで移動を受け止めるので、ページ全体を読み込み直すことはありません。移動すればページが描き直され、文言は新しいURLのロケールで読まれます。

型の付いたhref(bindParams)を使わないのは、手元にあるのがパターンではなく、今いるページの具体的なpathnameだからです。

Variants<string>は、Registerに登録したロケールごとに1つずつ値を持つ型です。ロケールを足したのに言語名を書き忘れると、型エラーになります。

情報

メモ

localizeとdelocalizeはpathnameだけを扱うので、クエリ(?より後ろ)は切り替え先に付きません。切り替えたあとも残したいときは、自分で付け足します。

Viteのbaseの下に置くアプリでは、withBase(locales.localize(pathname, locale))のようにbaseを付けます。usePathname()が返すpathnameにも、localizeが返す値にも、baseは付いていないためです。

選んだ言語を覚える

選んだ言語をCookieに書いておくと、次に/を開いたときに使えます。@k8ordo/serverで/に答えるguardが呼ぶnegotiateRequestは、このCookieをAccept-Languageより先に読むからです。

remember-locale.ts
import type { Locale } from './i18n';

const MAX_AGE = 400 * 24 * 60 * 60 * 1000;

export const rememberLocale = (locale: Locale) =>
  cookieStore.set({
    name: 'locale',
    value: locale,
    sameSite: 'lax',
    expires: Date.now() + MAX_AGE,
  });

切り替えのリンクを押したときに、このCookieを書きます。

language-switcher.tsx
<a
  aria-current={locale === current ? 'true' : undefined}
  href={locales.localize(pathname, locale)}
  hrefLang={locale}
  lang={locale}
  onClick={() => {
    void rememberLocale(locale);
  }}
>

Cookie Store APIの既定のままだと、Cookieはブラウザを閉じると消え、SameSite=Strictで書かれます。StrictのCookieは、ほかのサイトのリンクから来た最初のリクエストには付きません。そのためsameSite: 'lax'と遠いexpiresを付けて書きます。ブラウザがCookieを保つのは、長くても400日です。

Cookieの名前はアプリが決めます。読む側のnegotiateRequestにも、同じ名前を{ cookie: 'locale' }のように渡します。読む側の書き方は「最初の言語を選ぶ」で説明します。

情報

メモ

@k8ordo/staticのサイトには、/を開いたときにCookieを読むサーバーがありません。/のページがブラウザでCookieを読む書き方も、「最初の言語を選ぶ」で説明します。

k8ordo

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

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2