ガイド 第16

多言語 i18n

pickLang の2形対応・LangProvider/useLang・4S languages.en fallback

@event/i18n

UI 言語は「1 個の純関数(pickLang)+ 1 個の client context(LangProvider)」に集約する。 翻訳フレームワークは足さない。文字列選択のセマンティクスは session-card の engine/text.ts と揃え、 EN が空なら base(ja) にフォールバック する 4S languages.en の規則をそのまま保つ。外部依存は React のみ。

pickLang — 2 形に対応する純関数

pickLang(localized, lang, fallback?) は React 非依存の純関数で、ローカライズ済みオブジェクトの 2 つの形 を 1 引数で受ける:

挙動
value/valueEn 対{ value: title, valueEn: titleEn }lang === "en" かつ valueEn 非空なら valueEn、他は value(= base/ja)
lang-keyed record{ ja, en, fr, … }record[lang] →(空なら)record[fallback](既定 "ja")→ 最初の非空値

4S の Session.title / titleEndescription / descriptionEn は前者の対にそのまま写せる。 どちらの形でも trim 済みで空判定するので、EN 切替中でも未翻訳エンティティが空欄にならない。

pickLang({ value: s.title, valueEn: s.titleEn }, "en");   // titleEn 空なら title
pickLang({ ja: "日本語", en: "English" }, "en", "ja");     // record 形

Localized = LangRecord | ValuePair で型付けし、実行時は value/valueEn キーの有無で分岐する。 言語判定の補助として hasCJK(text) / CJK_RE(session-card と同一の CJK 範囲)も export する。

LangProvider / useLang — client context

<LangProvider initial={initialLang} fallback="ja" cookieName="starter_lang">
  {children}
</LangProvider>

useLang(){ lang, setLang, fallback }LangState)を返す。provider 外での呼び出しは throw。 initialserver で cookie から解決して渡す(下記)ことで SSR と初期描画を一致させる。 cookieName を渡した時だけ setLang が言語を cookie に永続化する。

useT / <T> — provider 言語で pickLang

context の現在言語を毎回書かずに済むよう、pickLang の薄いラッパを 2 つ用意する:

const t = useT();
t({ value: news.title, valueEn: news.titleEn });   // 現在言語で 1 文字列
<T of={{ value: news.title, valueEn: news.titleEn }} />;   // それを描画

useT()(localized, fallback?) => string を返し、fallback 省略時は provider の fallback を使う。

langFromCookie(raw, cookieName?) は寛容パーサで、server(cookies().get(name)?.value)でも client(document.cookie)でも 使える。生の value でも "a=b; starter_lang=en" 形のヘッダでも受ける。 langToCookie(lang, cookieName?) は client 専用(document 無い環境では no-op = SSR 安全)。cookie 名は イベント固有にできる(既定 DEFAULT_LANG_COOKIE)。

配線の要点(starter)

  • app/layout.tsx(server)で langFromCookie((await cookies()).get(LANG_COOKIE)?.value) ?? "ja"

initial として LangProvider に渡し、<html lang={initialLang}> も同じ値にする

  • 言語スイッチャは useLang().setLang を叩くだけの client component。cookieName 指定済みなので選択は cookie に残る
  • news の { title, titleEn }useT(){ value, valueEn } として渡すと、切替で見出しが ja/en に追従する

(EN 空なら ja へフォールバック)