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 / titleEn・description / 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。 initial は server で 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 を使う。
cookie 永続 — langFromCookie / langToCookie
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 へフォールバック)