🌏
Next.jsでi18nをやるんだが、言語ごとの翻訳ファイルを作りたくない
TL;DR
こんな感じの、翻訳テキストがIDごとに1箇所にまとまったファイルでi18n対応してみました。
translationTexts.js
export default {
// オブジェクトのkeyがテキストID的に働く
person: {
// IDの入れ子可能
name: {
en: 'Name',
ja: '名前'
},
// 各言語で同じテキストを使う場合は言語コードを指定しないでもOK
hp: 'HP',
// 入れ子の階層は任意
status: {
power: {
en: 'power',
ja: '力'
},
speed: {
en: 'speed',
ja: '素早さ'
}
},
// 関数でもOK
money: {
en: (num:number) => `$ ${num}`,
ja: (num:number) => `${num}円`,
},
// 配列を使用可能
parts: [{
en: 'head',
ja: '頭'
},{
en: 'body',
ja: '体'
}],
}
}
背景
Nextjs i18n
とかでググるとたくさんの有益情報がHitするんですが、そのどれもが
「それでは、言語ごとの翻訳ファイルを作っていきましょう」
みたいな感じでした。
なんとなくそっちの方がちゃんとしてるのはわかる気がするんですが、僕みたいに一人で小規模のものを作ってる場合には、こっちの構造の方が見通し良くて好きだな〜と思っての蛮行です。
手順
サブパスルーティングを設定
next.config.js
module.exports = {
// ...中略
i18n: {
locales: ["en", "ja"],
defaultLocale: "ja",
},
}
これで、
https://example.com/
およびhttps://example.com/ja
ではja版のページを
https://example.com/en
ではen版のページを表示する準備が整いました。
翻訳ファイルを作成
記事冒頭のやつです。再掲します。
translationTexts.js
export default {
// オブジェクトのkeyがテキストID的に働く
person: {
// IDの入れ子可能
name: {
en: 'Name',
ja: '名前'
},
// 各言語で同じテキストを使う場合は言語コードを指定しないでもOK
hp: 'HP',
// 入れ子の階層は任意
status: {
power: {
en: 'power',
ja: '力'
},
speed: {
en: 'speed',
ja: '素早さ'
}
},
// 関数でもOK
money: {
en: (num:number) => `$ ${num}`,
ja: (num:number) => `${num}円`,
},
// 配列を使用可能
parts: [{
en: 'head',
ja: '頭'
},{
en: 'body',
ja: '体'
}],
}
}
useTranslations hookを作成
useTranslations.ts
import { useRouter } from 'next/router'
import { useMemo } from 'react'
import texts from "../constants/translationTexts"
export const useTranslations = () => {
const { locale } = useRouter()
const result = useMemo<{[key:string]:any}>(() => {
const t = locale || 'ja'
// localeが一致するデータを再帰的にpick
const cherryPick = (obj: {[key:string]:any}|any[], key:string):{}|[] => {
if(Array.isArray(obj)) {
return obj.map(o => cherryPick(o, key))
}
else if(typeof obj === 'object') {
if(key in obj) return obj[key]
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, cherryPick(v, key)]))
}
return obj
}
return cherryPick(texts, t)
}, [locale])
return { locale, t:result };
}
使用
import { useTranslations } from 'useTranslations'
const Person = () => {
const { t:{person} } = useTranslations()
return (<>
<p>{person.name}</p>
<p>{person.hp}</p>
<p>{person.status.power}</p>
<p>{person.status.speed}</p>
<p>{person.money(100)}</p>
<ul>{person.parts.map((t:string)=>(<li>{t}</li>))}</ul>
</>)
}
export default Person
おまけ
言語切り替えリンク
import { useRouter } from 'next/router';
const LanguageSelector = () => {
const router = useRouter()
return (<>
<Link href={router.asPath} locale='ja'>JP</Link>
<span> | </span>
<Link href={router.asPath} locale='en'>EN</Link>
</>)
}
export default LanguageSelector
Discussion