Next.js に組み込まれている i18n (Internationalization) サポートを使用します。
Next.js には、国際化 ( i18n ) ルーティングのサポートが組み込まれています。ロケールのリスト、デフォルト ロケール、およびドメイン固有のロケールを指定すると、Next.js が自動的にルーティングを処理します。
Config に設定を追加 Add Config
next.config.js
ファイルに i18n
config を追加します。 locales
にサポートしたい全ての locale を記述します。 defaultLocale
はデフォルトの locale を指定します。
module.exports = {
i18n: {
locales: ["en", "ja"],
defaultLocale: "ja",
},
};
国際化のルーティングには「サブパス ルーティング」と「ドメイン ルーティング」の2通りあります。「サブパス ルーティング」は /en
/fr
となり、「ドメイン ルーティング」は example.com
example.fr
となります。
当サイトではサブパスを選択しました。例えば BLOG ページ( pages/blog.js
) は
/blog
/en/blog
となります。
以上の設定でユーザーのアクセス環境に基づいて、 Next.js は locale を自動的に検出し、ページの locale が切り替わります。
そのほかの設定はNext.js の公式ドキュメント( Internationalized Routing ) で確認できます。
Switcherを作成 Create Switcher
ユーザーが任意で言語の切り替えを行えるようにボタンを設置します。
現在の locale は Next.js の useRouter
で取得します。また、サイトで表示可能な locale のリストも同様に取得できます。
import Link from "next/link";
import { useRouter } from "next/router";
export const LocaleSwitcher = () => {
const { locale, locales, asPath } = useRouter();
return (
<div>
{locales &&
locales.map((localeName) => (
<Link key={localeName} href={asPath} passHref locale={localeName}>
<a className={locale === localeName ? "current" : ""}>
{localeName}
</a>
</Link>
))}
</div>
);
};
サイトやアプリケーション内の好きな場所で読み込みます。
ファイルの構成 File Directory Structure
当サイトは投稿ページ、固定ページを markdown ファイルで生成しています。多言語表示用にファイル名に該当の locale を追加したファイル file-name.locale.md
を用意しました。
src/
└── contents/
├── pages/
| ├── about.en.md
| └── about.md
|
└── posts/
├── blog/
| ├── blog-post.en.md
| └── blog-post.md
|
└── tips/
├── tips-post.en.md
└── tips-post.md
コンポーネントを作成 Create component
当サイトの About ページ ( /src/pages/about.tsx
) を例に作成します。このファイルでは主に以下のことをしています。
getStaticProps
でページ内容を取得getPageData()
で markdown ファイルのデータを取得- 取得した markdown ファイルの内容を HTML に変換して表示( marked を使用)
当サイトのコードの一部抜粋です。 getStaticProps
で locale
を取得し、 markdown ファイルを取得する getPageData()
に渡しています。
import { GetStaticProps, NextPage } from "next";
import { marked } from "marked";
import { getPageData } from "@utils/post";
import { Page } from "@libs/types";
const About: NextPage<{ post: Page }> = ({ post }) => {
return (
<div
className={post.data.slug}
dangerouslySetInnerHTML={{ __html: marked(post.content) }}
/>
)
}
export default About
export const getStaticProps: GetStaticProps = async ({ locale }) => {
const post = await getPageData("pages", "about", locale)
return {
props: {
post,
}
}
}
getPageData()
の例です。 locale === "ja" ? postSlug : postSlug + "." + locale
の部分で、デフォルトの locale (ja) であれば about.md
を locale === “en” であれば about.en.md
が読み込まれるようにしています。
import fs from "fs";
import path from "path";
import matter from "gray-matter";
export function getPageData(
type: string,
postIdentifier: string,
locale: string = "ja"
) {
// .md を除いたファイル名を取得
const postSlug = postIdentifier.replace(/\.md$/, "");
// locale に応じたファイルのパスを取得
const filePath = path.join(
contentsDirectory,
type,
`${locale === "ja" ? postSlug : postSlug + "." + locale}.md`
);
const readFile = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(readFile);
const postData = {
slug: postSlug,
data,
content,
};
return postData;
}
投稿ページや記事の一覧ページも同様に、基本的には getStaticProps
で locale を取得し、 markdown ファイルを取得する function に渡して、 locale に応じたファイルを取得して表示します。
一覧ページ
投稿ページの markdown ファイルに便宜上 locale
を追加しました。
---
title: My Blog Post
locale: en
date: "2022-02-11"
category: "blog"
---
...
一覧ページ用のファイルでは getStaticProps
で locale を取得して、 markdown 取得用の getPosts()
に渡します。
export const getStaticProps: GetStaticProps = async ({ locale }) => {
const tipsPosts = await getTypePosts("tips", locale)
// 日付順に並び替え
const sortedPosts = tipsPosts.sort((postA, postB) => postA.data.date > postB.data.date ? -1 : 1)
return {
props: {
posts: sortedPosts,
},
revalidate: 30
}
}
全ファイルを取得したあとに、 filter()
を使用して markdown ファイル内の locale と現在の locale が一致した場合のみそのファイルを返すようにしています。
export function getPosts(type: string, locale: string = "ja") {
// 指定のディレクトリ内のファイルを取得
const files = getPostsDirectory(type);
// 取得したファイルの locale を含む情報を抽出
const allPosts = files.map((file) => {
return getPostMeta(file, type, locale);
});
let filteredPosts = [];
// 現在の locale に対応するファイルのみに絞り込み
if (locale === "ja") {
filteredPosts = allPosts.filter((post) => post.data.locale === "ja");
} else {
/** en */
filteredPosts = allPosts.filter((post) => post.data.locale === "en");
}
return filteredPosts;
}
投稿ページ
投稿ページの表示ファイルはダイナミックルーティングを使用しています。
src/
└── pages/
└── tips/
└── [slug].tsx
このファイルでは主に以下のことをしています。
getPostPaths()
でgetStaticPaths
に必要なパスのリストを取得getStaticPaths
でレンダリングの必要があるパスのリストを作成getPostByPath()
で markdown ファイルのデータを取得getStaticProps
でページ内容を取得- 取得した markdown ファイルの内容を HTML に変換して表示( marked を使用)
[slug].tsx
内の getStaticPaths
の例です。
export const getStaticPaths: GetStaticPaths = async ({ locales }) => {
// レンダリングの必要があるパスのリストを取得
const postPaths = getPostsPaths("tips")
let paths: any[] = []
// map() で必要な locale を追加したパスリストを生成
postPaths.map((path: string) => {
locales?.map((locale: string) => {
paths.push({
params: { slug: `${path}` },
locale,
})
})
})
return {
paths,
fallback: "blocking"
}
}
paths
の中身は以下のようになります。
{ params: { slug: 'tips-post1' }, locale: 'ja' },
{ params: { slug: 'tips-post1' }, locale: 'en' }
markdown ファイルを取得する getPostByPath()
の例です。
export function getPostByPath(
path: any,
type: string,
locale: string = "ja",
) {
let postData: Post = initialPost;
const files = getPostsDirectory(type)
// 表示するURLに該当するファイルのパスを取得
const filteredFiles = files.filter(file => {
return file.includes(path)
})
filteredFiles.map((file) => {
const tempData = getPost(file, type, locale)
// markdown 内の locale が現在の locale と一致する場合は markdown の内容を取得して返す。
// 一致しない場合は false を返す
if (tempData.data.locale === locale) {
postData = getPost(file, type, locale)
} else {
return false
}
})
return postData
}
[slug].tsx
に戻り、getStaticProps
で locale を取得して getPostByPath()
に渡して markdown の内容を取得して、props を通して表示用コンポーネントに渡します。
export const getStaticProps: GetStaticProps = async ({ locale, params }) => {
const post = await getPostByPath(params!.slug, "tips", locale)
return {
props: {
post: post,
slug: params!.slug,
},
revalidate: 600
}
未翻訳の場合の対応
投稿ページで未翻訳の場合に <meta name="robots" content="noindex" />
が head 内に追加されるようにします。
[slug].tsx
の例です。当サイトは便宜上 markdown ファイルに localized: boolean を追加して、 locale === “en” かつ localized: false の場合は noindex が追加されるようにしています。
また記事本文にも未翻訳である旨のメッセージを表示するようにしました。
import Head from "next/head";
const TipsPost: NextPage<PostProps> = ({ post, slug, tags }) => {
return(
<>
<Head>
{locale === "en" && !post.data.localized && <meta name="robots" content="noindex" />}
</Head>
<article>
<p>This page is not available in English yet.</p>
<article>
</>
)
}
参照
- Advanced Features: Internationalized Routing | Next.js https://nextjs.org/docs/advanced-features/i18n-routing (2023-2-11参照)
- i18n (internationalization) with next.js. and markdown https://medium.com/@albert_barsegyan/i18n-internationalization-with-next-js-and-markdown-6477d818e906(2023-1-14参照)