はじめに
Next.js(v14時点)では標準でi18nがあるもののStatic Exportsでは利用できないようです。
参考: Routing: Internationalization | Next.js (2024-04-27)
martinkr/next-export-i18n というライブラリもありますが、Sub-path Routingしたかったのと外部ライブラリ利用をなるべく控えたかったので、オレオレで自作してみました。意外と簡単だった & 現時点ではこういった記事が少なそうだったのでまとめました。
今回のSub-path Routingの方針
i18nはSub-path Routingで対応しました。pathに en
や ja
を含めるやり方です。
また、Sub-path Routingでもさらに以下の方針があるかと思います。両方作ってみたので以降で両方紹介します。
- 単にアクセスしたページにアクセスする
- LocalStorageに言語を保持して、言語が異なるpathならredirectする
実装
今回紹介するコードはこちらにもpushしてます。
playground_next_ssg/hosting at main · koshitake2m2/playground_next_ssg
完成イメージはこちらです。
/lang/en |
/lang/ja |
---|---|
共通の準備
directory構成
.
├── app
│ └── lang
│ ├── [lang]
│ │ └── page.tsx
│ ├── page.tsx
│ └── switch-lang.tsx
├── i18n
│ ├── i18n-type.ts
│ ├── i18n.en.ts
│ ├── i18n.ja.ts
│ └── i18n.ts
└── public
└── lang
└── redirect.js
※みやすさ重視でいくらか省いてます
Static Exports
const nextConfig = {
output: "export",
...
};
module.exports = nextConfig;
i18nのオブジェクトの準備
デフォルトの言語を en
とします。
export type LangType = "en" | "ja";
export type I18nType = {
lang: LangType;
greeting: string;
welcome: string;
say: (words: string) => string;
a: {
b: string;
};
};
import { I18nType } from "./i18n-type";
export const i18nEn = {
lang: "en",
greeting: "Hello",
welcome: "Welcome",
say: (words: string) => `say ${words}`,
a: {
b: "ab",
},
} as const satisfies I18nType;
import { I18nType } from "./i18n-type";
export const i18nJa = {
lang: "ja",
greeting: "こんにちは",
welcome: "ようこそ",
say: (words: string) => `「${words}」と言う`,
a: {
b: "abだよ",
},
} as const satisfies Partial<I18nType>;
import { I18nType, LangType } from "./i18n-type";
import { i18nEn } from "./i18n.en";
import { i18nJa } from "./i18n.ja";
export const langPathParams: { lang: LangType }[] = [
{ lang: "en" },
{ lang: "ja" },
];
export const i18nMap: Record<LangType, I18nType> = {
en: i18nEn,
ja: { ...i18nEn, ...i18nJa }, // i18nJaを書き漏れたらi18nEnが出る
};
上記の実装であれば以下のような嬉しさがあります。
- リネームしやすい
- 利用箇所を特定しやすい
- 利用時に補完が効く
- 階層を表現できる
- 関数を利用できるのでコンポーネントから任意の文字を渡せる
また、お好みで以下のようにしてもいいでしょう
-
I18nType
を定義しない -
type I18nType = typeof i18nEn
で定義する - デフォルト以外の言語も型で縛る
1. 単にアクセスしたページにアクセスする版
挙動
-
/lang
- アクセスしたらブラウザの言語をもとに
/lang/en
か/lang/ja
にリダイレクトする
- アクセスしたらブラウザの言語をもとに
-
/lang/en
- 単に英語ページ
-
/lang/ja
- 単に日本語ページ
画面準備
/lang
const browserLang = navigator.language;
const path = browserLang.startsWith("ja") ? "/lang/ja" : "/lang/en";
window.location = path;
import Script from "next/script";
export default function LangPage() {
return (
<>
<Script src={`/lang/redirect.js`}></Script>
</>
);
}
/lang/[lang]
import Link from "next/link";
import { i18nMap, langPathParams } from "../../../i18n/i18n";
import { SwitchLang } from "../switch-lang";
import { LangType } from "../../../i18n/i18n-type";
export function generateStaticParams() {
return langPathParams;
}
export default function LangPage({ params }: { params: { lang: LangType } }) {
const { lang } = params;
const i18n = i18nMap[lang];
return (
<>
<h1>lang</h1>
<p>lang: {lang}</p>
<p>welcome: {i18n.welcome}</p>
<Link href={`/lang/${lang}/hello`}>hello page</Link>
<SwitchLang />
</>
);
}
言語を変えるボタン
"use client";
import { usePathname, useRouter } from "next/navigation";
import { LangType } from "../../i18n/i18n-type";
export function SwitchLang() {
const router = useRouter();
const pathname = usePathname();
const onClickSwitchLang = (lang: LangType) => {
const newPath = pathname.replace(/^\/lang\/[^/]*/, `/lang/${lang}`);
router.push(newPath);
};
return (
<>
<h1>switch lang</h1>
<button onClick={() => onClickSwitchLang("en")}>en</button>
<button onClick={() => onClickSwitchLang("ja")}>ja</button>
</>
);
}
2. LocalStorageに言語を保持して、言語が異なるpathならredirectする版
挙動
- 各ページ共通
- LocalStorageに言語が保存されてる場合
- 該当言語のページにリダイレクトする
- LocalStorageに言語が保存されてない場合
- ブラウザの言語をもとに該当言語のページにリダイレクトする
- LocalStorageに言語が保存されてる場合
-
/lang
- アクセスしたら
/lang/en
か/lang/ja
にリダイレクトする
- アクセスしたら
-
/lang/en
- 単に英語ページ
-
/lang/ja
- 単に日本語ページ
/lang
単に空のページ
export default function LangLocalStoragePage() {
return <></>;
}
各ページ共通のリダイレクト用のscriptを準備
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Script src={`/lang/redirect.js`}></Script> {children}
</>
);
}
LocalStorageやブラウザの言語を取得してリダイレクトする処理。ゴリゴリです。
/**
* @param {string} lang
*/
const redirectLang = (lang) => {
const newPath = location.pathname.replace(
/^\/lang\/[^/]*/,
`/lang/${lang}`
);
window.location = newPath;
};
const savedLang = localStorage.getItem("lang");
if (savedLang && ["en", "ja"].includes(savedLang)) {
const pathLang = location.pathname.match(
/^\/lang\/([^/]*)/
)?.[1];
if (pathLang !== savedLang) {
redirectLang(savedLang);
}
} else {
const browserLang = navigator.language;
const newLang = browserLang.startsWith("ja") ? "ja" : "en";
localStorage.setItem("lang", newLang);
const pathLang = location.pathname.match(
/^\/lang\/([^/]*)/
)?.[1];
if (pathLang !== newLang) {
redirectLang(newLang);
}
}
import Link from "next/link";
import { i18nMap, langPathParams } from "../../../i18n/i18n";
import { SwitchLangLocalStorage } from "../switch-lang";
import { LangType } from "../../../i18n/i18n-type";
export function generateStaticParams() {
return langPathParams;
}
export default function LangPage({ params }: { params: { lang: LangType } }) {
const { lang } = params;
const i18n = i18nMap[lang];
return (
<>
<h1>lang</h1>
<p>lang: {lang}</p>
<p>welcome: {i18n.welcome}</p>
<Link href={`/lang/${lang}/hello`}>hello page</Link>
<SwitchLangLocalStorage />
</>
);
}
言語切り替えボタン。LocalStorageに言語をsetする。
"use client";
import { usePathname, useRouter } from "next/navigation";
import { LangType } from "../../i18n/i18n-type";
export function SwitchLangLocalStorage() {
const router = useRouter();
const pathname = usePathname();
const onClickSwitchLang = (lang: LangType) => {
const newPath = pathname.replace(
/^\/lang\/[^/]*/,
`/lang/${lang}`
);
localStorage.setItem("lang", lang);
router.push(newPath);
};
return (
<>
<h1>switch lang</h1>
<button onClick={() => onClickSwitchLang("en")}>en</button>
<button onClick={() => onClickSwitchLang("ja")}>ja</button>
</>
);
}
補足
- App Routerで書いてますが、pages directoryでも同様にできます
- 今回は
/lang/en
にしてますが、/en
のようにトップのpathにすることもできます - redirect用の処理をゴリゴリにjsで書いてますが、もっとスマートな書き方があればゆるぼです🙏
参考
同じく自作でi18n対応してる方や別ライブリ利用の記事を見つけましたので共有です。