Next + typescript で i18n対応し、SSR、SSGを行うexample
Next + typescript で i18n 対応し、SSR、SSG を行う example
本項では、Next を使用して i18n 対応(SSR, SSG)を方法を解説します。
記事の要約
-
next-i18next
を使うとi18next
を使用して、SSR、SSG を比較的手軽に行う事ができる -
react-i18next
は.d.ts
を使用して、typescript
対応を行うと使い勝手が上がる - i18n 対応時は
next-seo
が便利- i18n 対応していない場合も便利
- i18n 対応時は
alternates
の設定をきちんとする
- 実装例はhttps://github.com/SiroSuzume/typed-i18next-exampleのリポジトリにおいた
- storybook 対応は別途作成中
使用するライブラリについて
以下、本項で使用する主要なライブラリを記載します。
-
Next
- React 用フレームワーク
-
i18next
- 名前が似ているので混乱するかもしれないですが、i18next と Next は直接的な関係はありません。
- React 以外の環境でも動作するライブラリです。
-
react-i18next
- i18next を React で使いやすくするためのライブラリです。hooks ライクな翻訳用関数コンポーネントを提供します。
-
next-i18next
- 更に名前がややこしいですが、react-i18next を、Next の環境で使用するためのライブラリです。
- 翻訳用 JSON を動的に読み込み、SSR、SSG 時に
i18next
で使用できるようにしてくれます。
-
next-seo
- SEO 情報を作成する際に便利なライブラリ
i18next の概要説明
i18next は key を索引、value を翻訳文とした辞書を管理し、索引に対応する翻訳文を返却する Javascript 用ライブラリです。辞書は言語とネームスペースで区切られて管理されています。フォールバックに使用する言語、ネームスペースを指定し、未翻訳の文章に対応するなどの柔軟な使用が可能です。
詳しくは公式サイトを参照してください。i18next.com
今回の記事では競走馬の名前を例に解説します。
翻訳用の JSON ファイルの例
// public/locales/ja/horse-names.json
{
"ステイゴールド": "ステイゴールド",
"ゴールドシップ": "ゴールドシップ"
}
// public/locales/en/horse-names.json
// ゴールドシップの翻訳文が用意されていない状態
{
"ステイゴールド": "Stay Gold"
}
// public/locales/zh/horse-names.json
{
"ステイゴールド": "黄金旅程",
"ゴールドシップ": "黄金船"
}
- 上記の翻訳リソースを使用
- ユーザーの言語が
en
圏 - フォールバックの設定を
ja
にしている場合
の条件ではでi18nextによる翻訳を行うと
ステイゴールド
の翻訳をした場合Stay Gold
。
ゴールドシップ
を翻訳すると、en
に対応した翻訳文がないため、ja
の翻訳文が優先され、結果としてゴールドシップ
が返却されます
react-i18next
の役割
react-i18next
は、上記の処理を React 上で扱いやすくするためのライブラリです。下記の例のように、useTranslation
から返却されたt
を使用して翻訳を行います。
// ネームスペースにhorse-namesを使用
const { t } = useTranslation('horse-names');
return (
<>
<div>{t('ステイゴールド')}</div>
<div>{t('ゴールドシップ')}</div>
</>
);
next-i18next を有効にする
next-i18next
は、Next
のi18n ルーティング設定を元に、ロケールに対応した JSON ファイルをpublic/locales
から取得し、i18next
で使用可能な状態にします。
環境変数の設定
環境変数で対応ロケールを設定すると何かと便利なため、環境変数で対応ロケールを管理する例を記載します。
# デフォルトのロケール
NEXT_PUBLIC_DEFAULT_LOCALE=ja
# 対応ロケール一覧をカンマ区切りで入力
NEXT_PUBLIC_SUPPORTED_LOCALES_SEPARATE_BY_COMMA=ja,en,zh
デフォルトのロケールと、対応ロケールの一覧はクライアント側でも使用するため、NEXT_PUBLIC_
をつけます。
config 関連の設定
以下は、next-i18next 公式の setup の項を参考に作成した、環境変数に対応するnext-i18next.config.js
及びnext.config.js
の例になります
next-i18next.config.js
の例
// @ts-check
/** @type {import('next-i18next').UserConfig} */
module.exports = {
i18n: {
defaultLocale: process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'ja',
locales: (process.env.NEXT_PUBLIC_SUPPORTED_LOCALES_SEPARATE_BY_COMMA || 'ja').split(','),
},
};
next.config.js
の例
const { i18n } = require('./next-i18next.config');
// @ts-check
/** @type {import('next').NextConfig} */
module.exports = {
i18n,
};
pagesフォルダのファイル
next-i18next の appTranslationの項を参考に、_app.tsx
と各ページをtsx
で作る場合の例を記載します。
_app.tsx
の例
_app.tsx
内ではnext-i18next
の提供する、appWithTranslation
を HOC として使用するよう設定します
// src/pages/_app.tsx
import type { AppProps } from 'next/app';
import { appWithTranslation } from 'next-i18next';
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => <Component {...pageProps} />;
export default appWithTranslation(MyApp);
.tsx
ファイルの例
ページ毎の以下は SSG(ISR)でトップページを実装する場合の例になります。getStaticProps
またはgetServerSideProps
内で翻訳リソースを読み込み、props
として渡す事でクライアント上で翻訳する事ができるようになります。
// src/pages/index.tsx
import type { NextPage, GetStaticProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { EntryHorse } from '@/types/entry-horses';
import { mockEntryHorses } from '@/mocks/entry-horse';
// 後述のreact-i18nextの型定義を追加すると、使用可能なネームスペースの名前をユニオン型で使用できる
import { Namespace } from 'react-i18next';
import nextI18NextConfig from '@/../next-i18next.config.js';
/** トップページで使用するネームスペース一覧を列挙する */
const i18nextNameSpaces: Namespace[] = ['seo', 'common', 'horse-names', 'top'];
type TopPageProps = {
entryHorses: EntryHorse[];
};
export const getStaticProps: GetStaticProps<TopPageProps> = async ({ locale }) => {
// 必要なリソースをロードし、propsとして受け渡す
const translations = await serverSideTranslations(locale!, i18nextNameSpaces as string[], nextI18NextConfig);
return { props: { ...translation, entryHorses: mockEntryHorses }, revalidate: 3600 };
};
/** トップページ */
const TopPage: NextPage<TopPageProps> = ({ entryHorses }) => {
return (
<>
<TopTemplate entryHorses={entryHorses} />
</>
);
};
export default TopPage;
react-i18next の typescript 対応
react-i18next
は typescript 向けに、辞書を型定義する方法を提供しています。useTranslation
の引数にネームスペースの名前を使用出来るようになり、指定したネームスペースに対応した翻訳キーをt
の引数として使用可能になります。詳しくはこちらを参考。
react-i18next.d.ts の定義
ルート直下に@types/
フォルダを作り、react-i18next.d.ts
を作成します。以下はcommon
seo
top
horse-names
の 4 つのネームスペースを使用し、デフォルトで使用する言語をja
にする場合の例になります。
// @types/react-i18next.d.ts
import 'react-i18next';
import common from '../public/locales/ja/common.json';
import seo from '../public/locales/ja/seo.json';
import top from '../public/locales/ja/top.json';
import horseNames from '../public/locales/ja/horse-names.json';
declare module 'react-i18next' {
interface CustomTypeOptions {
defaultNS: 'common';
resources: {
common: typeof common;
seo: typeof seo;
top: typeof top;
'horse-names': typeof horseNames;
};
}
}
i18n に対応した SEO 設定
SSR、SSG 時にi18next
が使用出来ることは、SEO の管理の点においても便利です。title
やdescription
、OpenGraph 用の画像といったhead
タグに入れる情報も、t
を使えば言語別に作る必要がなくなります。本項ではnext-seoとreact-i18next
を使用して、SEO 設定を行う方法を記載します。
NextSeo を使用する場合のページ構成
next-seo
ではページ共通のデフォルト設定として定義する<DefaultSeo>
と、DefaultSeo を上書きする、ページ毎の設定である<NextSeo>
をコンポーネントとして提供しています。
- 全てのページで共通して読み込むコンポーネントとして、
DefaultSeo
をラップしたMyDefaultSeo
を用意する - ページ毎に
MyDefaultSeo
との相違点を記載する必要があるため、NextSeo
をラップして作成する。 - 2 つのコンポーネントをページ毎に読み込む。
/** トップページ */
const TopPage: NextPage<TopPageProps> = ({ entryHorses }) => {
return (
<>
{/* 全ページ共通のSEO設定 */}
<MyDefaultSeo />
{/* ページ毎のSEO設定 */}
<TopPageSeo />
<TopTemplate entryHorses={entryHorses} />
</>
);
};
next-seo の使用法
t
をつかってサイトのtitle
description
OG 用の画像 URL を翻訳して使用します。
また、環境変数で定めておいた対応ロケール一覧、デフォルトのロケールからcanonical
やalternates
の設定を行い、他言語の場合の URL を提示し、SEO を強化します。
public/locales/ja/seo.json
にDefaultSeo
用の辞書を作成。
{
"$site-name": "サイト名",
"$site-description": "サイトの説明文章",
"$og-image-path": "/images/ja/og.png"
}
import { DefaultSeo, DefaultSeoProps } from 'next-seo';
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
/** サポートするロケール */
const supportedLocales = (process.env.NEXT_PUBLIC_SUPPORTED_LOCALES_SEPARATE_BY_COMMA ?? 'ja').split(',');
/** デフォルトのロケール */
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'ja';
/** オリジン */
const defaultOrigin = process.env.NEXT_PUBLIC_FRONTEND_ORIGIN ?? 'http://localhost:3000';
/** OG画像幅。Facebook推奨に合わせ1200 */
const ogImageWidth = 1200;
/** OG画像高さ。Facebook推奨に合わせ630 */
const ogImageHeight = 630;
/**
* ロケールとパスからURLを生成する。
* ロケールがデフォルトの場合、ロケールを省略する。
* パスが/の場合も省略する。
*/
function buildLocalizedUrlFromLocale(path: string, locale: string): string {
return `${defaultOrigin}${locale === defaultLocale ? '' : `/${locale}`}${path === '/' ? '' : path}`;
}
/** NextSeoのlanguageAlternatesの型がexportされていないため型計算 */
type LanguageAlternate = Exclude<DefaultSeoProps['languageAlternates'], undefined>[number];
/** 現在のパス、指定の言語、オリジンからhrefLang用オブジェクトを生成 */
function buildLanguageAlternate(bathPath: string, hrefLang: string): LanguageAlternate {
return {
hrefLang,
href: buildLocalizedUrlFromLocale(bathPath, hrefLang),
};
}
/** 現在のパス、指定の言語一覧から、x-default設定を含むhrefLang用オブジェクトを生成 */
function buildLanguageAlternates(bathPath: string, hrefLangs: string[]): LanguageAlternate[] {
const xDefault: LanguageAlternate = {
hrefLang: 'x-default',
href: buildLocalizedUrlFromLocale(bathPath, defaultLocale),
};
return [...hrefLangs.map((hrefLang) => buildLanguageAlternate(bathPath, hrefLang)), xDefault];
}
/** デフォルトのSEO設定。
* 必要な部分をページ毎で上書きして使用する
* @see https://github.com/garmeeh/next-seo
*/
export const MyDefaultSeo = (): JSX.Element => {
const { t } = useTranslation('seo');
// クエリを含める場合はasPathなどを使用する
const { locale, pathname } = useRouter();
const siteName = t('$site-name');
const titleTemplate = `${siteName} | %s`;
const description = t('$site-description');
const ogImageUrl = `${defaultOrigin}${t('$og-image-path')}`;
// 正規化したパス
const canonical = buildLocalizedUrlFromLocale(pathname, locale!);
// 言語毎に正規化したパスを算出する
const languageAlternates = buildLanguageAlternates(pathname, supportedLocales);
return (
<DefaultSeo
defaultTitle={siteName}
titleTemplate={titleTemplate}
description={description}
canonical={canonical}
languageAlternates={languageAlternates}
openGraph={{
title: siteName,
description,
images: [{ url: ogImageUrl, height: ogImageHeight, width: ogImageWidth, alt: siteName }],
type: 'website',
site_name: siteName,
locale,
url: canonical,
}}
/>
);
};
ページ毎の SEO
タイトル等の、ページ毎に変更となる箇所はNextSeo
を使って定義します。
// public/locales/ja/top.json
{
"$top-page-title": "阿寒湖特別"
}
import { NextSeo } from 'next-seo';
import { useTranslation } from 'react-i18next';
/** トップページSEO設定 */
export const TopPageSeo = (): JSX.Element => {
// topのネームスペースにタイトルなどをいれる想定
const { t } = useTranslation('top');
const title = t('$top-page-title');
return <NextSeo title={title} openGraph={{ title }} />;
};
実装例について
GitHub のSiroSuzume/typed-i18next-exampleに、今回の検証に使ったコードをあげています。
また、ja
en
zh
に対応した example を Vercel 上にデプロイしています
-
ja
-
en
-
zh
その他
storybook でもreact-i18next
を有効にする方法について、後日記事にしようと思います
Discussion