Next.js (SSG) でハマったロケール対応
9/30/2021
NextJS
React
TypeScript
GithubPages
Hosting
SSG

Next.js でロケール対応すると SSG 化できない事象に出会した。

主な特徴は下記 2 つ。

  • Next.js 11 最新を使っている
  • Github Pages にデプロイしている (SSG)

順当に考えれば next-i18next を選択する訳だが、これが SSG にとって不都合だった。

i18n support is not compatible with next export.

Issue も立てられていたようです。

i18n with next export calls getStaticProps for each defined lang, but then errors #18318

ただこちらは close されており、今後対応される可能性も低いです。

i18next を使う

Next.js 用のプラグインとして提供されている訳では無いものの i18next を一から使ってみた。

pages/_app.tsx でロケール設定を読み込むために i18n.init.js を作成する必要がある。

import i18next from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'

import { languages, defaultLanguage } from './i18n.config'

const locales = Object.assign(
  {},
  ...Object.keys(languages).map((index) => {
    return {
      [languages[index]]: {
        translations: require('./public/locales/' + languages[index] + '/common.json'),
      },
    }
  }),
)

const detection = {
  order: [
    'querystring',
    'cookie',
    'localStorage',
    'sessionStorage',
    'navigator',
    'htmlTag',
    'path',
    'subdomain',
  ],
  lookupCookie: 'lng',
  lookupLocalStorage: 'lng',
  lookupFromPathIndex: 0,
  lookupFromSubdomainIndex: 0,
  caches: ['localStorage', 'cookie'],
  excludeCacheFor: ['cimode'],
  cookieOptions: { path: '/', sameSite: 'strict' },
}

i18next.use(LanguageDetector).init({
  detection: detection,
  fallbackLng: defaultLanguage,
  resources: locales,
  ns: ['translations'],
  defaultNS: 'translations',
  returnObjects: true,
  debug: false,
  interpolation: {
    escapeValue: false,
  },
  react: {
    wait: true,
  },
})

export default i18next

新たに作成する i18n.config.js で対応言語、初期言語を設定する。

export const languages = ['en', 'ja']

export const defaultLanguage = 'en'

pages/_app.tsx で読み込む。

import i18next from 'i18next'
import '../i18n.init'

あと翻訳ワードとそれぞれに対する ID を管理するため JSON ファイルを public/locales 下に置く必要がある。

ルートページを作成する

ルートパスでサブパスからルーティングを判断する。

import React from 'react'
import { useRouter } from 'next/router'
import i18next from 'i18next'

export default function Home() {
  const router = useRouter()

  React.useEffect(() => {
    const { pathname } = router
    if (pathname == '/') {
      router.push('/' + i18next.language.substring(0, 2))
    }
  })

  return null
}

pages/[lang].tsx でサブページを読み込む

対応言語に合わせてルーティングを設定する。

getStaticPathsgetStaticProps を組み合わせることで、導入予定の (導入の可能性が高い) 全ての言語を網羅させる。

import * as React from 'react'
import i18next from 'i18next'
import { defaultLanguage, languages } from '../../../i18n.config'

export default function Home() {
  return (
    {/*省略*/}
  )
}

export async function getStaticPaths() {
  return {
    paths: languages.map((lang) => {
      return { params: { lang: lang } }
    }),
    fallback: false,
  }
}

export async function getStaticProps({ params }) {
  return {
    props: {
      language: languages.includes(params.lang) ? params.lang : defaultLanguage,
    },
  }
}

使いたい箇所に導入するだけ

i18next を適宜インポートして、該当の ID を指定すれば OK。

import i18next from 'i18next'

const Section = () => {
  return (
    <section>
      <h1>{i18next.t('title')}</h1>
    </section>
  )
}

結果として遠回りになったものの、国際化対応でき安堵。

あわせてよみたい..