言語を切り替える
言語を切り替えるのは、今のページと同じpathnameを、別のロケールの区間の下で開き直すことです。このページでは、localizeとdelocalizeで切り替え先のURLを作り、言語の切り替えを組み立てて、選んだ言語をCookieに覚えておくまでを説明します。
このページの内容
切り替え先のURLを作る
今のpathnameからdelocalizeでロケールの区間を外し、localizeで別のロケールの区間を付けます。
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を投げます。
切り替え先の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'
試してみる
- 最初に入っている
/ja/products/42では、localeがja、pathnameが/products/42になり、enへの切り替え先は/en/products/42です。 /englishに書き換えると、先頭の区間はロケールではないので、localeがnullになります。/en/に書き換えると、区間を外した残りのpathnameは/になります。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.tsimport 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を読む書き方も、「最初の言語を選ぶ」で説明します。