[![hackmd-github-sync-badge](https://hackmd.io/OpDSyIdLTtWQGWU1ac3HwA/badge)](https://hackmd.io/OpDSyIdLTtWQGWU1ac3HwA) ###### `Front-End` `React` `Next.js` `Router` # 【學習筆記】Next.js 實現多國語系:react-i18next & next-i18next [TOC] ![i18n](https://hackmd.io/_uploads/ry0hNlrZA.png) ## 前言 在開發網站時,多國語系功能(i18n)是很常見的需求,能根據使用者需求切換網站顯示的語言。這陣子在 Next.js 專案中意外踩了幾個坑,寫下學習筆記作為紀錄。 本篇分為以下幾個段落: + What is i18n? + 如何實作 + Next.js 內建:路由層級 + 選擇套件:react-i18next & next-i18next + 使用範例 + SSG/SSR:使用 next-i18next + SPA:使用 react-i18next ## What is i18n? i18n 是由 internationalization(國際化)英文縮寫而來,18 代表 i 到 n 之間的字母數量。 透過 i18n 多國語系功能,能夠讓不同語系的使用者,根據需求選定顯示語言和格式,減少在地化的時間成本,達到國際化的目的。 ## 如何實作 ### Next.js 內建:路由層級 在介紹套件之前,先介紹 Next.js 官方[內建 i18n 功能](https://nextjs.org/docs/pages/building-your-application/routing/internationalization),透過路由層級(routing)設定不同語言轉至不同路徑,設定範例如下: ```js= // next.config.js module.exports = { i18n: { locales: ['en', 'zh', 'jp'], defaultLocale: 'en', }, } ``` 根據上述配置,即可搭配 `next/link`、`next/router`,根據路由顯示對應的語言,路徑如下: * `/posts`:預設為 `en` * `/zh/blog` * `/jp/blog` 其中需注意 [Page Router](https://nextjs.org/docs/pages/building-your-application/routing/internationalization) 和 [App Router](https://nextjs.org/docs/app/building-your-application/routing/internationalization) 在路由設定上有所不同,詳細可參考官方提供的範例,以下介紹常見的 i18n 套件作為範例。 > 關於 Page router 和 App router 的差別,可參考之前的筆記:[【學習筆記】Next.js 路由系統:App Router vs Page Router](https://heidiliu2020.github.io/nextjs-app-router/) ### 選擇套件:react-i18next & next-i18next + [i18next/next-i18next](https://github.com/i18next/next-i18next) + 支援 Page Router + 支援 SSG/SSR + [i18next/react-i18next](https://github.com/i18next/react-i18next) + 支援 APP Router,因此 Next.js v13 後的版本建議搭配使用 + 支援 SPA 可參考上述兩種套件的 npm 下載數: ![next-i18n](https://hackmd.io/_uploads/HyMMe1Sb0.png) ## 使用範例 ### SSR:使用 [next-i18next](https://github.com/i18next/next-i18next) (1) 首先是安裝套件: ``` npm install next-i18next --save ``` (2) 在根目錄新增 `next-i18next.config.js` 設定檔: ```typescript= // next-i18next.config.js module.exports = { i18n: { locales: ['en', 'jp', 'zh'], defaultLocale: 'en', }, fallbackLng: { default: ['en'], }, }; ``` (3) 在 `next.config.js` 設定檔引入使用 `next-i18next`: ```typescript= // next.config.js const { i18n } = require('./next-i18next.config') module.exports = { i18n, } ``` (4) 修改 `src/pages/_app.tsx` 檔案,以 `appWithTranslation` 這個 [HOC(高階組件)](https://legacy.reactjs.org/docs/higher-order-components.html)包住整個 App: ```typescript= // src/pages/_app.tsx import { appWithTranslation } from 'next-i18next' const MyApp = ({ Component, pageProps }) => ( <Component {...pageProps} /> ) export default appWithTranslation(MyApp) ``` #### 【補充】Higher-Order Components(HOC,高階組件) + HOC 並不是 React 提供的 API,而是和 JavsScript 中的 [Higher Order Function(高階函式)](https://zh.wikipedia.org/zh-tw/%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0)類似的一個函式,高階函式可代入另一個函式作為參數,最終回傳一個函式作為結果 + 而 HOC 則是可代入元件(Component)作為參數,並回傳一個新的元件 + 目的是將共用邏輯放在 HOC 中,變動的部分由 Component 的 props 和 state 傳入 (5) 在 `public/locales/<locale>/<namespace>.json` 路徑加入多國語系檔案,架構參考如下: ``` . └── public └── locales ├── en | ├── posts.json | └── common.json ├── jp | ├── posts.json | └── common.json └── zh ├── posts.json └── common.json ``` (6) 在頁面引入語系檔案: ```typescript= import { serverSideTranslations } from 'next-i18next/serverSideTranslations' export async function getStaticProps({ locale }) { return { props: { ...(await serverSideTranslations(locale, [ 'common', 'posts', ])), // Will be passed to the page component as props }, } } ``` (7) 即可在頁面引入 `useTranslation` hook 使用,注意需從 `next-i18next` 引用: ```typescript= import { useTranslation } from 'next-i18next' export const Header = () => { const { t } = useTranslation('common') return ( <div> <h1>{t('home-title')}</h1> </div> ) } ``` ### SPA:使用 [react-i18next](https://github.com/i18next/react-i18next) 非 SSR、SSG 環境,只需直接引用 `react-i18next` 套件即可。 (1) 首先是安裝套件: ``` npm install react-i18next i18next --save ``` (2) 建立 `app/i18n` 目錄,放置管理 i18n 的相關檔案(初始 i18n、翻譯文檔),範例如下: + `i18n/index.ts`:初始 i18n 相關設定 ```typescript= // i18n/index.ts import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import resources from './lang-resource'; i18n .use(initReactI18next) // 初始化設定 .init({ resources, // 引入定義語系與對應文字的 json 檔 lng: 'en', // 預設語系為 en fallbackLng: 'en', // 若找不到對應語系則回傳 en defaultNS: 'common', preload: ['en', 'ja', 'zh'], ns: 'common', interpolation: { escapeValue: false, }, parseMissingKeyHandler: () => { return ''; } , react: { useSuspense: false }, }); export default i18n; ``` + `i18n/lang-resource`:定義語系與對應文字的 json 檔 ```jsonld= // i18n/lang-resource const resources = { en: { common: { 'login': 'Login', 'logout': 'Log out', }, }, ja: { common: { 'login': 'ログイン', 'logout': 'ログアウト', }, }, zh: { common: { 'login': '登入', 'logout': '登出', }, }, }; export default resources; ``` (3) 設定完成後,即可在 `app/pages/layout` 引入 `i18n/index.ts` 使用: ```typescript= // app/pages/layout import './i18n' export default function RootLayout() { // ... } ``` 以 `app/pages/login/page.tsx` 為例: ```typescript= // app/pages/login/page.tsx import { useTranslation } from 'react-i18next'; export default function Login() { const { t } = useTranslation(); return ( <div> <p>{t('login')}</p> <p>{t('logout')}</p> <div/> ) } ``` (4) 若要切換語系,可使用 `i18n.changeLanguage` 方法: ```typescript= // app/component/header.tsx import { useTranslation } from 'react-i18next'; export function Header() { const { t, i18n } = useTranslation(); const handleChangeLanguage = () => { i18n.changeLanguage('en') } } ``` ## 結語 其實一開始研究實作 i18n 功能時,看到官方文件整個頭昏眼花,Next.js 內建的路由實作上又稍嫌複雜,React 生態不像 Angular 框架能直接引入內建功能實作,反而有很多種套件能夠選擇使用。 但問題來了,套件引入使用下來卻噴一堆 Error,才發現到由於 Next.js 路由系統架構,需搭配支援 App Router 或 Page Router 的套件使用,其實想成是在實作 React 多國語系功能就單純許多;雖然文中提及的 Page Router 還沒有機會實作,概念上還有點模糊,但還是先寫下筆記留作紀錄。 ## 參考資料 + [一篇文章了解如何在 Next.js 中集成 i18n 国际化(含踩坑及开发配置)](https://juejin.cn/post/7238619890993758263) + [【Next】i18n使用 - JohnShu Blog](https://jiangshuuu.com/docs/React/next-i18n/) + [【DAY20】React Native - 多語系切換 (react-i18next)](https://ithelp.ithome.com.tw/articles/10323017) + [使用 next-i18next 實作中英文多語系 - Modern Next.js Blog 系列 #28](https://ithelp.ithome.com.tw/articles/10308315)