---
createdAt: 2025-11-01
updatedAt: 2025-11-01
title: next-i18nextã䜿ã£ãNext.jsã¢ããªã±ãŒã·ã§ã³ã®åœéåæ¹æ³
description: next-i18nextã§i18nãèšå®ããæ¹æ³ïŒå€èšèªå¯Ÿå¿ã®Next.jsã¢ããªåãã®ãã¹ããã©ã¯ãã£ã¹ãšSEOã®ãã³ããåœéåãã³ã³ãã³ãã®æŽçãæè¡çãªã»ããã¢ãããã«ããŒã
slugs:
- blog
- nextjs-internationalization-using-next-i18next
applicationTemplate: https://github.com/aymericzip/next-i18next-template
history:
- version: 7.0.6
date: 2025-11-01
changes: åç
---
# 2025幎ç next-i18nextã䜿ã£ãNext.jsã¢ããªã±ãŒã·ã§ã³ã®åœéåæ¹æ³
## ç®æ¬¡
<TOC/>
## next-i18nextãšã¯ïŒ
**next-i18next**ã¯ãNext.jsã¢ããªã±ãŒã·ã§ã³åãã®äººæ°ã®ããåœéåïŒi18nïŒãœãªã¥ãŒã·ã§ã³ã§ããå
ã
ã®`next-i18next`ããã±ãŒãžã¯Pages Routeråãã«èšèšãããŠããŸããããæ¬ã¬ã€ãã§ã¯ãææ°ã®**App Router**ã§`i18next`ãš`react-i18next`ãçŽæ¥äœ¿çšããŠi18nextãå®è£
ããæ¹æ³ã玹ä»ããŸãã
ãã®ã¢ãããŒãã«ããã以äžãå¯èœã«ãªããŸãïŒ
- **åå空éã䜿ã£ãç¿»èš³ã®æŽç**ïŒäŸïŒ`common.json`ã`about.json`ïŒã«ããã³ã³ãã³ã管çã®åäžã
- **å¿
èŠãªåå空éã®ã¿ãããŒãžããšã«èªã¿èŸŒãããšã§ç¿»èš³ãå¹ççã«ããŒã**ãããã³ãã«ãµã€ãºãåæžã
- **ãµãŒããŒã³ã³ããŒãã³ããšã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãã®äž¡æ¹ããµããŒã**ããé©åãªSSRãšãã€ãã¬ãŒã·ã§ã³ãå®çŸã
- **TypeScriptãµããŒãã®ç¢ºä¿**ã«ãããåå®å
šãªãã±ãŒã«èšå®ãšç¿»èš³ããŒãå®çŸã
- **é©åãªã¡ã¿ããŒã¿ããµã€ãããããrobots.txtã®åœéåã«ããSEOãæé©å**ã
> 代æ¿ãšããŠã[next-intlã¬ã€ã](https://github.com/aymericzip/intlayer/blob/main/docs/blog/ja/i18n_using_next-intl.md)ããçŽæ¥[Intlayer](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/intlayer_with_nextjs_16.md)ãåç
§ããããšãã§ããŸãã
> æ¯èŒã¯[ next-i18next vs next-intl vs Intlayer](https://github.com/aymericzip/intlayer/blob/main/docs/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md)ãã芧ãã ããã
## å®è£
åã«å®ãã¹ããã©ã¯ãã£ã¹
å®è£
ã«å
¥ãåã«ã以äžã®ãã©ã¯ãã£ã¹ãå®ã£ãŠãã ããïŒ
- **HTMLã®`lang`ãš`dir`屿§ãèšå®ãã**
ã¬ã€ã¢ãŠãå
ã§ã`getLocaleDirection(locale)` ã䜿çšã㊠`dir` ãèšç®ããé©åãªã¢ã¯ã»ã·ããªãã£ãšSEOã®ããã« `<html lang={locale} dir={dir}>` ãèšå®ããŸãã
- **åå空éããšã«ã¡ãã»ãŒãžãåå²ãã**
ããŒãããå¿
èŠã®ãããã®ã ããèªã¿èŸŒãããã«ããã±ãŒã«ãšåå空éããšã«JSONãã¡ã€ã«ãæŽçããŸãïŒäŸïŒ`common.json`ã`about.json`ïŒã
- **ã¯ã©ã€ã¢ã³ãã®ãã€ããŒããæå°åãã**
ããŒãžã§ã¯ãå¿
èŠãªåå空éã®ã¿ã `NextIntlClientProvider` ã«éä¿¡ããŸãïŒäŸïŒ`pick(messages, ['common', 'about'])`ïŒã
- **éçããŒãžãåªå
ãã**
ããã©ãŒãã³ã¹ãšSEOã®åäžã®ããã«ãã§ããã ãéçããŒãžã䜿çšããŸãã
- **ãµãŒããŒã³ã³ããŒãã³ãã§ã®åœéå**
ããŒãžã `client` ãšããŒã¯ãããŠããªããã¹ãŠã®ã³ã³ããŒãã³ãã®ãããªãµãŒããŒã³ã³ããŒãã³ãã¯éçã§ããããã«ãæã«ããªã¬ã³ããªã³ã°ã§ããŸãããã®ãããç¿»èš³é¢æ°ããããããšããŠæž¡ãå¿
èŠããããŸãã
- **TypeScriptã®åãèšå®ãã**
ã¢ããªã±ãŒã·ã§ã³å
šäœã§åã®å®å
šæ§ã確ä¿ããããã«ããã±ãŒã«çšã®TypeScriptåãèšå®ããŸãã
- **ãªãã€ã¬ã¯ãçšã®ãããã·**
ãããã·ã䜿çšããŠãã±ãŒã«ã®æ€åºãšã«ãŒãã£ã³ã°ãåŠçãããŠãŒã¶ãŒãé©åãªãã±ãŒã«æ¥é èŸä»ãURLã«ãªãã€ã¬ã¯ãããŸãã
- **ã¡ã¿ããŒã¿ããµã€ãããããrobots.txtã®åœéå**
Next.jsãæäŸãã`generateMetadata`颿°ã䜿çšããŠãã¡ã¿ããŒã¿ããµã€ãããããrobots.txtãåœéåãããã¹ãŠã®ãã±ãŒã«ã§æ€çŽ¢ãšã³ãžã³ã«ããããè¯ãæ€åºã確ä¿ããŸãã
- **ãªã³ã¯ã®ããŒã«ã©ã€ãº**
`Link`ã³ã³ããŒãã³ãã䜿ã£ãŠãªã³ã¯ãããŒã«ã©ã€ãºãããŠãŒã¶ãŒãé©åãªãã±ãŒã«æ¥é èŸä»ãURLã«ãªãã€ã¬ã¯ãããŸããããã¯ãã¹ãŠã®ãã±ãŒã«ã§ããŒãžã®æ€åºã確å®ã«ããããã«éèŠã§ãã
- **ãã¹ããšç¿»èš³ã®èªåå**
ãã¹ããšç¿»èš³ã®èªååã¯ãå€èšèªã¢ããªã±ãŒã·ã§ã³ã®ã¡ã³ããã³ã¹ã«ãããæéãåæžããã®ã«åœ¹ç«ã¡ãŸãã
> åœéåãšSEOã«é¢ããŠç¥ã£ãŠããã¹ããã¹ãŠããŸãšããããã¥ã¡ã³ãã¯ãã¡ããã芧ãã ãã: [next-intlã«ããåœéå (i18n)](https://github.com/aymericzip/intlayer/blob/main/docs/blog/ja/internationalization_and_SEO.md)ã
---
## Next.jsã¢ããªã±ãŒã·ã§ã³ã§i18nextãã»ããã¢ããããã¹ããããã€ã¹ãããã¬ã€ã
<iframe
src="https://stackblitz.com/github/aymericzip/next-i18next-template?embed=1&ctl=1&file=src/app/i18n.ts"
className="m-auto overflow-hidden rounded-lg border-0 max-md:size-full max-md:h-[700px] md:aspect-16/9 md:w-full"
title="ã㢠CodeSandbox - Intlayerã䜿ã£ãã¢ããªã±ãŒã·ã§ã³ã®åœéåæ¹æ³"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
loading="lazy"
> GitHubã®[Application Template](https://github.com/aymericzip/next-i18next-template)ãã芧ãã ããã
ããããäœæãããããžã§ã¯ãæ§æã¯ä»¥äžã®éãã§ãïŒ
```bash
.
âââ i18n.config.ts
âââ src # srcã¯ãªãã·ã§ã³ã§ã
âââ locales
â âââ en
â â âââ common.json
â â âââ about.json
â âââ fr
â âââ common.json
â âââ about.json
âââ types
â âââ i18next.d.ts
âââ app
â âââ proxy.ts
â âââ i18n
â â âââ server.ts
â âââ [locale]
â âââ layout.tsx
â âââ (home) # / ïŒããŒã ã¡ãã»ãŒãžã§å
šããŒãžãæ±æããªãããã®ã«ãŒãã°ã«ãŒãïŒ
â â âââ layout.tsx
â â âââ page.tsx
â âââ about # /about
â âââ layout.tsx
â âââ page.tsx
âââ components
âââ I18nProvider.tsx
âââ ClientComponent.tsx
âââ ServerComponent.tsx
```
### ã¹ããã 1: äŸåé¢ä¿ã®ã€ã³ã¹ããŒã«
å¿
èŠãªããã±ãŒãžãnpmã䜿ã£ãŠã€ã³ã¹ããŒã«ããŸãïŒ
```bash packageManager="npm"
npm install i18next react-i18next i18next-resources-to-backend
```
```bash packageManager="pnpm"
pnpm add i18next react-i18next i18next-resources-to-backend
```
```bash packageManager="yarn"
yarn add i18next react-i18next i18next-resources-to-backend
```
- **i18next**: 翻蚳ã®èªã¿èŸŒã¿ãšç®¡çãè¡ãåœéåã®ã³ã¢ãã¬ãŒã ã¯ãŒã¯ã§ãã
- **react-i18next**: i18nextã®Reactãã€ã³ãã£ã³ã°ã§ãã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãåãã«`useTranslation`ã®ãããªããã¯ãæäŸããŸãã
- **i18next-resources-to-backend**: 翻蚳ãã¡ã€ã«ã®åçèªã¿èŸŒã¿ãå¯èœã«ãããã©ã°ã€ã³ã§ãå¿
èŠãªåå空éã ããããŒãã§ããŸãã
### ã¹ããã 2: ãããžã§ã¯ãã®èšå®
ãµããŒããããã±ãŒã«ãããã©ã«ããã±ãŒã«ãããã³URLã®ããŒã«ã©ã€ãºçšãã«ããŒé¢æ°ãå®çŸ©ããèšå®ãã¡ã€ã«ãäœæããŸãããã®ãã¡ã€ã«ã¯i18nèšå®ã®åäžã®çå®ã®æ
å ±æºãšããŠæ©èœããã¢ããªã±ãŒã·ã§ã³å
šäœã§åå®å
šæ§ãä¿èšŒããŸãã
ãã±ãŒã«èšå®ãäžå
åããããšã§äžæŽåãé²ããå°æ¥çã«ãã±ãŒã«ã®è¿œå ãåé€ã容æã«ããŸãããã«ããŒé¢æ°ã¯SEOãã«ãŒãã£ã³ã°ã®ããã«äžè²«ããURLçæãä¿èšŒããŸãã
```ts fileName="i18n.config.ts"
// åå®å
šã®ããã«ãµããŒããããã±ãŒã«ãconsté
åãšããŠå®çŸ©
// 'as const'ã¢ãµãŒã·ã§ã³ã«ãããTypeScriptã¯string[]ã§ã¯ãªããªãã©ã«åãæšè«ããŸã
export const locales = ["en", "fr"] as const;
// localesé
åããLocaleåãæœåº
// ããã«ãããŠããªã³å "en" | "fr" ãäœæãããŸã
export type Locale = (typeof locales)[number];
// ãã±ãŒã«ãæå®ãããŠããªãå Žåã«äœ¿çšãããããã©ã«ãã®ãã±ãŒã«ãèšå®
export const defaultLocale: Locale = "en";
// å³ããå·Šãžã®ããã¹ãæ¹ååŠçãå¿
èŠãªèšèª
export const rtlLocales = ["ar", "he", "fa", "ur"] as const;
// ãã±ãŒã«ãRTLïŒå³ããå·ŠïŒããã¹ãæ¹åãå¿
èŠãšãããã©ããããã§ãã¯
// ã¢ã©ãã¢èªãããã©ã€èªããã«ã·ã£èªããŠã«ãã¥ãŒèªãªã©ã«äœ¿çš
export const isRtl = (locale: string) =>
(rtlLocales as readonly string[]).includes(locale);
// æå®ããããã±ãŒã«ãšãã¹ã«åºã¥ããŠããŒã«ã©ã€ãºããããã¹ãçæ
// ããã©ã«ããã±ãŒã«ã®ãã¹ã«ã¯ãã¬ãã£ãã¯ã¹ããªãïŒäŸ: "/about" 㯠"/en/about" ã§ã¯ãªãïŒ
// ãã®ä»ã®ãã±ãŒã«ã«ã¯ãã¬ãã£ãã¯ã¹ãä»ãïŒäŸ: "/fr/about"ïŒ
export function localizedPath(locale: string, path: string) {
return locale === defaultLocale ? path : `/${locale}${path}`;
}
// 絶察URLã®ããŒã¹URLïŒãµã€ãããããã¡ã¿ããŒã¿ãªã©ã§äœ¿çšïŒ
const ORIGIN = "https://example.com";
// ãã±ãŒã«ãã¬ãã£ãã¯ã¹ä»ãã®çµ¶å¯ŸURLãçæ
// SEOã¡ã¿ããŒã¿ããµã€ãããããã«ããã«ã«URLã§äœ¿çš
export function absoluteUrl(locale: string, path: string) {
return `${ORIGIN}${localizedPath(locale, path)}`;
}
// ãã©ãŠã¶ã§ãã±ãŒã«ã¯ãããŒãèšå®ããããã«äœ¿çš
export function getCookie(locale: Locale) {
return [
`NEXT_LOCALE=${locale}`,
"Path=/",
`Max-Age=${60 * 60 * 24 * 365}`, // 1幎
"SameSite=Lax",
].join("; ");
}
```
### ã¹ããã3: 翻蚳ããŒã ã¹ããŒã¹ã®éäžç®¡ç
ã¢ããªã±ãŒã·ã§ã³ãå
¬éãããã¹ãŠã®namespaceã®åäžã®çå®ã®ãœãŒã¹ãäœæããŸãããã®ãªã¹ããåå©çšããããšã§ããµãŒããŒãã¯ã©ã€ã¢ã³ããããã³ããŒã«ã®ã³ãŒããåæããã翻蚳ãã«ããŒã®åŒ·åãªåä»ããå¯èœã«ãªããŸãã
```ts fileName="src/i18n.namespaces.ts"
export const namespaces = ["common", "about"] as const;
export type Namespace = (typeof namespaces)[number];
```
### ã¹ããã4: TypeScriptã§ç¿»èš³ããŒã匷ãåä»ããã
`i18next`ãæ¡åŒµããŠãæšæºã®èšèªãã¡ã€ã«ïŒéåžžã¯è±èªïŒãæãããã«ããŸããããã«ããTypeScriptã¯namespaceããšã®æå¹ãªããŒãæšè«ãã`t()`ã®åŒã³åºãããšã³ãããŒãšã³ãã§æ€èšŒãããŸãã
```ts fileName="src/types/i18next.d.ts"
import "i18next";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "common";
resources: {
common: typeof import("@/locales/en/common.json");
about: typeof import("@/locales/en/about.json");
};
}
}
```
> ãã³ã: ãã®å®£èšã¯ `src/types` ãã©ã«ãå
ã«ä¿åããŠãã ããïŒååšããªãå Žåã¯ãã©ã«ããäœæããŠãã ããïŒãNext.js ã¯ãã§ã« `tsconfig.json` ã« `src` ãå«ããŠããããããã®æ¡åŒµã¯èªåçã«èªèãããŸããããèªèãããªãå Žåã¯ã`tsconfig.json` ã«ä»¥äžã远å ããŠãã ããïŒ
```json5 fileName="tsconfig.json"
{
"include": ["src/types/**/*.ts"],
}
```
ããã«ããããªãŒãã³ã³ããªãŒããã³ã³ãã€ã«æã®åãã§ãã¯ãå©çšå¯èœã«ãªããŸãïŒ
```tsx
import { useTranslation, type TFunction } from "react-i18next";
const { t } = useTranslation("about");
// OKãåä»ãæžã¿: t("counter.increment")
// ãšã©ãŒãã³ã³ãã€ã«ãšã©ãŒ: t("doesNotExist")
export type AboutTranslator = TFunction<"about">;
```
### ã¹ããã 5: ãµãŒããŒãµã€ãã® i18n åæåãèšå®ãã
ãµãŒããŒã³ã³ããŒãã³ãã®ããã«ç¿»èš³ãèªã¿èŸŒããµãŒããŒãµã€ãåæå颿°ãäœæããŸãããã®é¢æ°ã¯ãµãŒããŒãµã€ãã¬ã³ããªã³ã°çšã«å¥ã®i18nextã€ã³ã¹ã¿ã³ã¹ãäœæããã¬ã³ããªã³ã°åã«ç¿»èš³ãèªã¿èŸŒãŸããŠããããšãä¿èšŒããŸãã
ãµãŒããŒã³ã³ããŒãã³ãã¯ã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ããšã¯ç°ãªãã³ã³ããã¹ãã§åäœãããããç¬èªã®i18nextã€ã³ã¹ã¿ã³ã¹ãå¿
èŠã§ãããµãŒããŒã§ç¿»èš³ãäºåã«èªã¿èŸŒãããšã§ãæªç¿»èš³ã®ã³ã³ãã³ããäžç¬è¡šç€ºããããã©ãã·ã¥ãé²ããæ€çŽ¢ãšã³ãžã³ã翻蚳æžã¿ã®ã³ã³ãã³ããèªèã§ããããSEOãåäžããŸãã
```ts fileName="src/app/i18n/server.ts"
import { createInstance } from "i18next";
import { initReactI18next } from "react-i18next/initReactI18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { defaultLocale } from "@/i18n.config";
import { namespaces, type Namespace } from "@/i18n.namespaces";
// i18nextã®åçãªãœãŒã¹èªã¿èŸŒã¿ãèšå®
// ãã®é¢æ°ã¯ãã±ãŒã«ãšããŒã ã¹ããŒã¹ã«åºã¥ããŠç¿»èš³JSONãã¡ã€ã«ãåçã«ã€ã³ããŒãããŸã
// äŸ: locale="fr", namespace="about" -> "@/locales/fr/about.json"ãã€ã³ããŒã
const backend = resourcesToBackend(
(locale: string, namespace: string) =>
import(`@/locales/${locale}/${namespace}.json`)
);
const DEFAULT_NAMESPACES = [
namespaces[0],
] as const satisfies readonly Namespace[];
/**
* ãµãŒããŒãµã€ãã¬ã³ããªã³ã°çšã«i18nextã€ã³ã¹ã¿ã³ã¹ãåæåãã
*
* @returns ãµãŒããŒãµã€ãã§äœ¿çšå¯èœãªåæåæžã¿i18nextã€ã³ã¹ã¿ã³ã¹
*/
export async function initI18next(
locale: string,
ns: readonly Namespace[] = DEFAULT_NAMESPACES
) {
// æ°ãã i18next ã€ã³ã¹ã¿ã³ã¹ãäœæïŒã¯ã©ã€ã¢ã³ãåŽã®ã€ã³ã¹ã¿ã³ã¹ãšã¯å¥ïŒ
const i18n = createInstance();
// React çµ±åãšããã¯ãšã³ãããŒããŒã§åæå
await i18n
.use(initReactI18next) // React ããã¯ã®ãµããŒããæå¹å
.use(backend) // åçãªãœãŒã¹èªã¿èŸŒã¿ãæå¹å
.init({
lng: locale,
fallbackLng: defaultLocale,
ns, // ããã©ãŒãã³ã¹åäžã®ããæå®ãããåå空éã®ã¿ãèªã¿èŸŒã
defaultNS: "common", // æå®ããªãå Žåã®ããã©ã«ãåå空é
interpolation: { escapeValue: false }, // HTML ãšã¹ã±ãŒãããªãïŒReact ã XSS ä¿è·ãåŠçïŒ
react: { useSuspense: false }, // SSR äºæã®ãã Suspense ãç¡å¹å
returnNull: false, // ããŒãèŠã€ãããªãå Žå㯠null ã§ã¯ãªã空æåãè¿ã
initImmediate: false, // ãªãœãŒã¹ãèªã¿èŸŒãŸãããŸã§åæåãé
å»¶ïŒé«éãªSSRã®ããïŒ
});
return i18n;
}
```
### ã¹ããã6: ã¯ã©ã€ã¢ã³ããµã€ãã®i18nãããã€ããŒãäœæãã
i18nextã³ã³ããã¹ãã§ã¢ããªã±ãŒã·ã§ã³ãã©ããããã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ããããã€ããŒãäœæããŸãããã®ãããã€ããŒã¯ãµãŒããŒããäºåã«èªã¿èŸŒãŸãã翻蚳ãåãåããæªç¿»èš³ã³ã³ãã³ãã®ãã©ãã·ã¥ïŒFOUCïŒãé²ããéè€ãã§ãããåé¿ããŸãã
ã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãã¯ãã©ãŠã¶ã§åäœããç¬èªã®i18nextã€ã³ã¹ã¿ã³ã¹ãå¿
èŠã§ãããµãŒããŒããäºåã«èªã¿èŸŒãŸãããªãœãŒã¹ãåãå
¥ããããšã§ãã·ãŒã ã¬ã¹ãªãã€ãã¬ãŒã·ã§ã³ãä¿èšŒããã³ã³ãã³ãã®ãã©ãã·ã¥ãé²ããŸãããã®ãããã€ããŒã¯ãã±ãŒã«ã®å€æŽãåå空éã®åçèªã¿èŸŒã¿ã管çããŸãã
```tsx fileName="src/components/I18nProvider.tsx"
"use client";
import { useEffect, useState } from "react";
import { I18nextProvider } from "react-i18next";
import { createInstance, type ResourceLanguage } from "i18next";
import { initReactI18next } from "react-i18next/initReactI18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { defaultLocale } from "@/i18n.config";
import { namespaces as allNamespaces, type Namespace } from "@/i18n.namespaces";
// ã¯ã©ã€ã¢ã³ããµã€ãã®åçãªãœãŒã¹èªã¿èŸŒã¿ãèšå®
// ãµãŒããŒãµã€ããšåããã¿ãŒã³ã ãããã®ã€ã³ã¹ã¿ã³ã¹ã¯ãã©ãŠã¶ã§åäœãã
const backend = resourcesToBackend(
(locale: string, namespace: string) =>
import(`@/locales/${locale}/${namespace}.json`)
);
type Props = {
locale: string;
namespaces?: readonly Namespace[];
// ãµãŒããŒããäºåã«èªã¿èŸŒãŸãããªãœãŒã¹ïŒFOUC - æªç¿»èš³ã³ã³ãã³ãã®ãã©ãã·ã¥ã鲿¢ïŒ
// ãã©ãŒããã: { namespace: translationBundle }
resources?: Record<Namespace, ResourceLanguage>;
children: React.ReactNode;
};
/**
* ã¯ã©ã€ã¢ã³ããµã€ãã®i18nãããã€ããŒã§ãã¢ããªãi18nextã³ã³ããã¹ãã§ã©ããããŸã
* ãµãŒããŒããäºåã«èªã¿èŸŒãŸãããªãœãŒã¹ãåãåãã翻蚳ã®åååŸãé²ããŸã
*/
export default function I18nProvider({
locale,
namespaces = [allNamespaces[0]] as const,
resources,
children,
}: Props) {
// useStateã®é
å»¶åæåã䜿ã£ãŠi18nã€ã³ã¹ã¿ã³ã¹ãäžåºŠã ãäœæ
// ããã«ãããã¬ã³ããŒããšã«äœæãããã®ãé²ããŸã
const [i18n] = useState(() => {
const i18nInstance = createInstance();
i18nInstance
.use(initReactI18next)
.use(backend)
.init({
lng: locale,
fallbackLng: defaultLocale,
ns: namespaces,
// ãªãœãŒã¹ãæäŸãããŠããå ŽåïŒãµãŒããŒããïŒãã¯ã©ã€ã¢ã³ãåŽã§ã®åååŸãé¿ããããã«ããã䜿çšããŸã
// ããã«ããFOUCïŒFlash of Unstyled ContentïŒãé²ããåæèªã¿èŸŒã¿ã®ããã©ãŒãã³ã¹ãåäžããŸã
resources: resources ? { [locale]: resources } : undefined,
defaultNS: "common",
interpolation: { escapeValue: false },
react: { useSuspense: false },
returnNull: false, // undefinedã®å€ãè¿ãããã®ãé²ããŸã
});
return i18nInstance;
});
// localeããããã£ã倿Žããããšãã«èšèªãæŽæ°ããŸã
useEffect(() => {
i18n.changeLanguage(locale);
}, [locale, i18n]);
// ã¯ã©ã€ã¢ã³ãåŽã§å¿
èŠãªãã¹ãŠã®namespaceãèªã¿èŸŒãŸããŠããããšã確èªããŸã
// é
åãæ£ããæ¯èŒããããã«join("|")ãäŸåé¢ä¿ãšããŠäœ¿çšããŠããŸã
useEffect(() => {
i18n.loadNamespaces(namespaces);
}, [namespaces.join("|"), i18n]);
// Reactã³ã³ããã¹ããéããŠãã¹ãŠã®åã³ã³ããŒãã³ãã«i18nã€ã³ã¹ã¿ã³ã¹ãæäŸ
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}
```
### ã¹ããã7: åçãã±ãŒã«ã«ãŒãã®å®çŸ©
ã¢ããªãã©ã«ãå
ã« `[locale]` ãã£ã¬ã¯ããªãäœæããŠããã±ãŒã«ã®åçã«ãŒãã£ã³ã°ãèšå®ããŸããããã«ãããNext.jsã¯åãã±ãŒã«ãURLã®ã»ã°ã¡ã³ããšããŠæ±ãããšãã§ããããã«ãªããŸãïŒäŸïŒ`/en/about`ã`/fr/about`ïŒã
åçã«ãŒãã䜿çšããããšã§ãNext.jsã¯ãã«ãæã«ãã¹ãŠã®ãã±ãŒã«ã®éçããŒãžãçæã§ããããã©ãŒãã³ã¹ãšSEOãåäžããŸããã¬ã€ã¢ãŠãã³ã³ããŒãã³ãã¯ãã±ãŒã«ã«åºã¥ããŠHTMLã® `lang` ãš `dir` 屿§ãèšå®ããã¢ã¯ã»ã·ããªãã£ãšæ€çŽ¢ãšã³ãžã³ã®çè§£ã«éèŠãªåœ¹å²ãæãããŸãã
```tsx fileName="src/app/[locale]/layout.tsx"
import type { ReactNode } from "react";
import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";
// åçãã©ã¡ãŒã¿ãç¡å¹å - ãã¹ãŠã®ãã±ãŒã«ã¯ãã«ãæã«æ¢ç¥ã§ããå¿
èŠããããŸã
// ããã«ããããã¹ãŠã®ãã±ãŒã«ã«ãŒãã§éççæãä¿èšŒãããŸã
export const dynamicParams = false;
/**
* ãã¹ãŠã®ãã±ãŒã«ã«å¯ŸããŠãã«ãæã«éçãã©ã¡ãŒã¿ãçæ
* Next.jsã¯ããã§è¿ãããåãã±ãŒã«ã®ããŒãžãäºåã¬ã³ããªã³ã°ããŸã
* äŸ: [{ locale: "en" }, { locale: "fr" }]
*/
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
/**
* ãã±ãŒã«åºæã®HTML屿§ãåŠçããã«ãŒãã¬ã€ã¢ãŠãã³ã³ããŒãã³ã
* lang屿§ãšããã¹ãæ¹åïŒltr/rtlïŒããã±ãŒã«ã«åºã¥ããŠèšå®ããŸã
*/
export default function LocaleLayout({
children,
params,
}: {
children: ReactNode;
params: { locale: string };
}) {
// URLãã©ã¡ãŒã¿ãããã±ãŒã«ãæ€èšŒ
// ç¡å¹ãªãã±ãŒã«ãæäŸãããå Žåã¯ããã©ã«ããã±ãŒã«ã«ãã©ãŒã«ããã¯
const locale: Locale = (locales as readonly string[]).includes(params.locale)
? (params.locale as any)
: defaultLocale;
// ãã±ãŒã«ã«åºã¥ããŠããã¹ãã®æ¹åãæ±ºå®
// ã¢ã©ãã¢èªã®ãããªRTLèšèªã¯é©åãªããã¹ã衚瀺ã®ããã«dir="rtl"ãå¿
èŠ
const dir = isRtl(locale) ? "rtl" : "ltr";
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
);
}
```
### ã¹ããã8: 翻蚳ãã¡ã€ã«ãäœæãã
åãã±ãŒã«ãšåå空éããšã«JSONãã¡ã€ã«ãäœæããŸãããã®æ§é ã«ããã翻蚳ãè«ççã«æŽçããåããŒãžã§å¿
èŠãªãã®ã ããèªã¿èŸŒãããšãã§ããŸãã
åå空éïŒäŸïŒ`common.json`ã`about.json`ïŒããšã«ç¿»èš³ãæŽçããããšã§ãã³ãŒãåå²ãå¯èœã«ãªãããã³ãã«ãµã€ãºãåæžã§ããŸããããã«ãããåããŒãžã«å¿
èŠãªç¿»èš³ã®ã¿ãèªã¿èŸŒããããããã©ãŒãã³ã¹ãåäžããŸãã
```json fileName="src/locales/en/common.json"
{
"appTitle": "Next.js i18n App",
"appDescription": "Example Next.js application with internationalization using i18next"
}
```
```json fileName="src/locales/fr/common.json"
{
"appTitle": "Application Next.js i18n",
"appDescription": "Exemple d'application Next.js avec internationalisation utilisant i18next"
}
```
```json fileName="src/locales/en/home.json"
{
"title": "Home",
"description": "Home page description",
"welcome": "Welcome",
"greeting": "Hello, world!",
"aboutPage": "About Page",
"documentation": "Documentation"
}
```
```json fileName="src/locales/fr/home.json"
{
"title": "Accueil",
"description": "Description de la page d'accueil",
"welcome": "Bienvenue",
"greeting": "Bonjour le monde!",
"aboutPage": "Page à propos",
"documentation": "Documentation"
}
```
```json fileName="src/locales/en/about.json"
{
"title": "About",
"description": "About page description",
"counter": {
"label": "Counter",
"increment": "Increment",
"description": "Click the button to increase the counter"
}
}
```
```json fileName="src/locales/fr/about.json"
{
"title": "Ã propos",
"description": "Description de la page à propos",
"counter": {
"label": "Compteur",
"increment": "Incrémenter",
"description": "Cliquez sur le bouton pour augmenter le compteur"
}
}
```
### ã¹ããã9: ããŒãžã§ç¿»èš³ãå©çšãã
i18nextããµãŒããŒäžã§åæåãã翻蚳ããµãŒããŒã³ã³ããŒãã³ããšã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãã®äž¡æ¹ã«æž¡ãããŒãžã³ã³ããŒãã³ããäœæããŸããããã«ãããã¬ã³ããªã³ã°åã«ç¿»èš³ãèªã¿èŸŒãŸããã³ã³ãã³ãã®ãã©ãã·ã¥ã鲿¢ã§ããŸãã
ãµãŒããŒãµã€ãã®åæåã¯ãããŒãžãã¬ã³ããªã³ã°ãããåã«ç¿»èš³ãèªã¿èŸŒã¿ãSEOã®åäžãšFOUCïŒFlash of Unstyled ContentïŒã®é²æ¢ã«åœ¹ç«ã¡ãŸããäºåã«èªã¿èŸŒãã ãªãœãŒã¹ãã¯ã©ã€ã¢ã³ããããã€ããŒã«æž¡ãããšã§ãéè€ãããã§ãããé¿ããã¹ã ãŒãºãªãã€ãã¬ãŒã·ã§ã³ãå®çŸããŸãã
```tsx fileName="src/app/[locale]/about/index.tsx"
import I18nProvider from "@/components/I18nProvider";
import { initI18next } from "@/app/i18n/server";
import type { Locale } from "@/i18n.config";
import { namespaces as allNamespaces, type Namespace } from "@/i18n.namespaces";
import type { ResourceLanguage } from "i18next";
import ClientComponent from "@/components/ClientComponent";
import ServerComponent from "@/components/ServerComponent";
/**
* i18nã®åæåãåŠçãããµãŒããŒã³ã³ããŒãã³ãããŒãž
* ãµãŒããŒã§ç¿»èš³ãäºåã«èªã¿èŸŒã¿ãã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãã«æž¡ã
*/
export default async function AboutPage({
params: { locale },
}: {
params: { locale: Locale };
}) {
// ãã®ããŒãžã§å¿
èŠãªç¿»èš³ã®ããŒã ã¹ããŒã¹ãå®çŸ©
// åå®å
šãšãªãŒãã³ã³ããªãŒãã®ããã«äžå€®ç®¡çããããªã¹ããåå©çš
const pageNamespaces = allNamespaces;
// å¿
èŠãªããŒã ã¹ããŒã¹ã§ãµãŒããŒäžã§i18nextãåæå
// ããã«ãã翻蚳JSONãã¡ã€ã«ããµãŒããŒåŽã§èªã¿èŸŒãŸãã
const i18n = await initI18next(locale, pageNamespaces);
/// "about" åå空éã®åºå®ç¿»èš³é¢æ°ãååŸ
// getFixedT ã¯åå空éãåºå®ãããããt("about:title") ã§ã¯ãªã t("title") ãšããŠäœ¿ãã
const tAbout = i18n.getFixedT(locale, "about");
// i18n ã€ã³ã¹ã¿ã³ã¹ãã翻蚳ãã³ãã«ãæœåº
// ãã®ããŒã¿ã¯ I18nProvider ã«æž¡ãããã¯ã©ã€ã¢ã³ãåŽã® i18n ããã€ãã¬ãŒããã
// FOUCïŒæªç¿»èš³ã³ã³ãã³ãã®ã¡ãã€ãïŒãé²ããéè€ãã§ãããåé¿
const resources = Object.fromEntries(
pageNamespaces.map((ns) => [ns, i18n.getResourceBundle(locale, ns)])
) satisfies Record<Namespace, ResourceLanguage>;
return (
<I18nProvider
locale={locale}
namespaces={pageNamespaces}
resources={resources}
>
<main>
<h1>{tAbout("title")}</h1>
<ClientComponent />
<ServerComponent t={tAbout} locale={locale} count={0} />
</main>
</I18nProvider>
);
}
```
### ã¹ããã10: ã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãã§ã®ç¿»èš³ã®äœ¿çš
ã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãã§ã¯ã`useTranslation` ããã¯ã䜿çšããŠç¿»èš³ã«ã¢ã¯ã»ã¹ã§ããŸãããã®ããã¯ã¯ç¿»èš³é¢æ°ãši18nã€ã³ã¹ã¿ã³ã¹ãžã®ã¢ã¯ã»ã¹ãæäŸããã³ã³ãã³ãã®ç¿»èš³ããã±ãŒã«æ
å ±ã®ååŸãå¯èœã«ããŸãã
ã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãã¯ç¿»èš³ã«ã¢ã¯ã»ã¹ããããã«Reactããã¯ãå¿
èŠãšããŸãã`useTranslation` ããã¯ã¯i18nextãšã·ãŒã ã¬ã¹ã«çµ±åããããã±ãŒã«ã倿Žãããéã«ãªã¢ã¯ãã£ããªæŽæ°ãæäŸããŸãã
> ããŒãžããããã€ããŒã«ã¯å¿
èŠãªåå空éã®ã¿ãå«ããããã«ããŠãã ããïŒäŸïŒ`about`ïŒã
> Reactã®ããŒãžã§ã³ã19æªæºã®å Žåã¯ã`Intl.NumberFormat`ã®ãããªéããã©ãŒããã¿ãŒãã¡ã¢åããŠãã ããã
```tsx fileName="src/components/ClientComponent.tsx"
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
/**
* 翻蚳ã®ããã®Reactããã¯ã䜿çšããã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãã®äŸ
* useStateãuseEffectãuseTranslationãªã©ã®ããã¯ã䜿çšå¯èœ
*/
const ClientComponent = () => {
// useTranslationããã¯ã¯ç¿»èš³é¢æ°ãši18nã€ã³ã¹ã¿ã³ã¹ãžã®ã¢ã¯ã»ã¹ãæäŸ
// "about"åå空éã®ç¿»èš³ã®ã¿ãèªã¿èŸŒãããã«æå®
const { t, i18n } = useTranslation("about");
const [count, setCount] = useState(0);
// ãã±ãŒã«å¯Ÿå¿ã®æ°å€ãã©ãŒããã¿ãŒãäœæ
// i18n.languageã¯çŸåšã®ãã±ãŒã«ãæäŸïŒäŸ: "en", "fr"ïŒ
// Intl.NumberFormatã¯ãã±ãŒã«ã®æ
£ç¿ã«åŸã£ãŠæ°å€ããã©ãŒããã
const numberFormat = new Intl.NumberFormat(i18n.language);
return (
<div className="flex flex-col items-center gap-4">
{/* ãã±ãŒã«åºæã®ãã©ãŒãããã§æ°å€ã衚瀺 */}
<p className="text-5xl font-bold text-white m-0">
{numberFormat.format(count)}
</p>
<button
type="button"
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
aria-label={t("counter.label")}
onClick={() => setCount((c) => c + 1)}
>
{t("counter.increment")}
</button>
</div>
);
};
export default ClientComponent;
```
### ã¹ããã11: ãµãŒããŒã³ã³ããŒãã³ãã§ã®ç¿»èš³ã®äœ¿çš
ãµãŒããŒã³ã³ããŒãã³ãã¯Reactã®ããã¯ã䜿çšã§ããªãããã芪ã³ã³ããŒãã³ãããpropsçµç±ã§ç¿»èš³ãåãåããŸãããã®æ¹æ³ã«ããããµãŒããŒã³ã³ããŒãã³ãã¯åæçã«ä¿ãããã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãå
ã«ãã¹ãããããšãå¯èœã«ãªããŸãã
ã¯ã©ã€ã¢ã³ãå¢çå
ã«ãã¹ããããå¯èœæ§ã®ãããµãŒããŒã³ã³ããŒãã³ãã¯åæçã§ããå¿
èŠããããŸãã翻蚳æžã¿ã®æååãšãã±ãŒã«æ
å ±ãpropsãšããŠæž¡ãããšã§ãéåææäœãé¿ããé©åãªã¬ã³ããªã³ã°ãä¿èšŒããŸãã
```tsx fileName="src/components/ServerComponent.tsx"
import type { TFunction } from "i18next";
type ServerComponentProps = {
// 芪ã®ãµãŒããŒã³ã³ããŒãã³ãããæž¡ãããç¿»èš³é¢æ°
// ãµãŒããŒã³ã³ããŒãã³ãã¯ããã¯ã䜿ããªãããã翻蚳ã¯propsçµç±ã§åãåã
t: TFunction<"about">;
locale: string;
count: number;
};
/**
* ãµãŒããŒã³ã³ããŒãã³ãã®äŸ - 翻蚳ã¯propsãšããŠåãåã
* ã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãïŒéåæãµãŒããŒã³ã³ããŒãã³ãïŒã®äžã«ãã¹ãå¯èœ
* Reactããã¯ã¯äœ¿çšã§ããªãããããã¹ãŠã®ããŒã¿ã¯propsãŸãã¯éåææäœããååŸããå¿
èŠããã
*/
const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
// ãã±ãŒã«ã䜿ã£ãŠãµãŒããŒåŽã§æ°å€ããã©ãŒããã
// SSRäžã«ãµãŒããŒã§å®è¡ãããåæããŒãžããŒããæ¹å
const formatted = new Intl.NumberFormat(locale).format(count);
return (
<div className="flex flex-col items-center gap-4">
<p className="text-5xl font-bold text-white m-0">{formatted}</p>
{/* propsã§æž¡ãããç¿»èš³é¢æ°ãäœ¿çš */}
<div className="flex flex-col items-center gap-2">
<span className="text-xl font-semibold text-white">
{t("counter.label")}
</span>
<span className="text-sm opacity-80 italic">
{t("counter.description")}
</span>
</div>
</div>
);
};
export default ServerComponent;
```
---
### ïŒãªãã·ã§ã³ïŒã¹ããã12ïŒã³ã³ãã³ãã®èšèªã倿Žãã
Next.jsã§ã³ã³ãã³ãã®èšèªã倿Žããæšå¥šæ¹æ³ã¯ããã±ãŒã«æ¥é èŸä»ãã®URLãšNext.jsã®ãªã³ã¯ã䜿çšããããšã§ãã以äžã®äŸã§ã¯ãçŸåšã®ãã±ãŒã«ãã«ãŒãããèªã¿åãããã¹åãããããåãé€ããå©çšå¯èœãªåãã±ãŒã«ããšã«ãªã³ã¯ãã¬ã³ããªã³ã°ããŸãã
```tsx fileName="src/components/LocaleSwitcher.tsx"
"use client";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { useMemo } from "react";
import { defaultLocale, getCookie, type Locale, locales } from "@/i18n.config";
export default function LocaleSwitcher() {
const params = useParams();
const pathname = usePathname();
const activeLocale = (params?.locale as Locale | undefined) ?? defaultLocale;
const getLocaleLabel = (locale: Locale): string => {
try {
const displayNames = new Intl.DisplayNames([locale], {
type: "language",
});
return displayNames.of(locale) ?? locale.toUpperCase();
} catch {
return locale.toUpperCase();
}
};
const basePath = useMemo(() => {
if (!pathname) return "/";
const segments = pathname.split("/").filter(Boolean);
if (segments.length === 0) return "/";
const maybeLocale = segments[0] as Locale;
if ((locales as readonly string[]).includes(maybeLocale)) {
const rest = segments.slice(1).join("/");
return rest ? `/${rest}` : "/";
}
return pathname;
}, [pathname]);
return (
<nav aria-label="èšèªã»ã¬ã¯ã¿ãŒ">
{(locales as readonly Locale[]).map((locale) => {
const isActive = locale === activeLocale;
const href =
locale === defaultLocale ? basePath : `/${locale}${basePath}`;
return (
<Link
key={locale}
href={href}
aria-current={isActive ? "page" : undefined}
onClick={() => {
document.cookie = getCookie(locale);
}}
>
{getLocaleLabel(locale)}
</Link>
);
})}
</nav>
);
}
```
### ïŒãªãã·ã§ã³ïŒã¹ããã13ïŒããŒã«ã©ã€ãºãããLinkã³ã³ããŒãã³ãã®äœæ
ã¢ããªå
šäœã§ããŒã«ã©ã€ãºãããURLãåå©çšããããšã§ãããã²ãŒã·ã§ã³ã®äžè²«æ§ãä¿ã¡ãSEOã«ã广çã§ãã`next/link`ãã©ããããå
éšã«ãŒãã«ã¯ã¢ã¯ãã£ããªãã±ãŒã«ããã¬ãã£ãã¯ã¹ãšããŠä»ããå€éšURLã¯ãã®ãŸãŸã«ããå°ããªãã«ããŒãäœæããŸãããã
```tsx fileName="src/components/LocalizedLink.tsx"
"use client";
import NextLink, { type LinkProps } from "next/link";
import { useParams } from "next/navigation";
import type { ComponentProps, PropsWithChildren } from "react";
import {
defaultLocale,
type Locale,
locales,
localizedPath,
} from "@/i18n.config";
const isExternal = (href: string) => /^https?:\/\//.test(href);
type LocalizedLinkProps = PropsWithChildren<
Omit<LinkProps, "href"> &
Omit<ComponentProps<"a">, "href"> & { href: string; locale?: Locale }
>;
export default function LocalizedLink({
href,
locale,
children,
...props
}: LocalizedLinkProps) {
const params = useParams();
const fallback = (params?.locale as Locale | undefined) ?? defaultLocale;
const normalizedLocale = (locales as readonly string[]).includes(fallback)
? ((locale ?? fallback) as Locale)
: defaultLocale;
const normalizedPath = href.startsWith("/") ? href : `/${href}`;
const localizedHref = isExternal(href)
? href
: localizedPath(normalizedLocale, normalizedPath);
return (
<NextLink href={localizedHref} {...props}>
{children}
</NextLink>
);
}
```
> ãã³ã: `LocalizedLink` ã¯ããããã€ã³çœ®æãªã®ã§ãã€ã³ããŒããå·®ãæ¿ããŠã³ã³ããŒãã³ãã«ãã±ãŒã«åºæã®URLåŠçãä»»ããåœ¢ã§æ®µéçã«ç§»è¡ã§ããŸãã
### ïŒãªãã·ã§ã³ïŒã¹ããã14: ãµãŒããŒã¢ã¯ã·ã§ã³å
ã§ã¢ã¯ãã£ããªãã±ãŒã«ã«ã¢ã¯ã»ã¹ãã
ãµãŒããŒã¢ã¯ã·ã§ã³ã§ã¯ãã¡ãŒã«éä¿¡ããã°èšé²ããµãŒãããŒãã£é£æºã®ããã«çŸåšã®ãã±ãŒã«ãå¿
èŠã«ãªãããšãå€ãã§ãããããã·ã§èšå®ããããã±ãŒã«ã¯ãããŒãšããã©ãŒã«ããã¯ãšããŠã® `Accept-Language` ããããŒãçµã¿åãããŠäœ¿çšããŸãã
```ts fileName="src/app/actions/get-current-locale.ts"
"use server";
import { cookies, headers } from "next/headers";
import { defaultLocale, locales, type Locale } from "@/i18n.config";
const KNOWN_LOCALES = new Set(locales as readonly string[]);
const normalize = (value: string | undefined): Locale | undefined => {
if (!value) return undefined;
const base = value.toLowerCase().split("-")[0];
return KNOWN_LOCALES.has(base) ? (base as Locale) : undefined;
};
export async function getCurrentLocale(): Promise<Locale> {
const cookieLocale = normalize(cookies().get("NEXT_LOCALE")?.value);
if (cookieLocale) return cookieLocale;
const headerLocale = normalize(headers().get("accept-language"));
return headerLocale ?? defaultLocale;
}
// çŸåšã®ãã±ãŒã«ã䜿çšãããµãŒããŒã¢ã¯ã·ã§ã³ã®äŸ
export async function stuffFromServer(formData: FormData) {
const locale = await getCurrentLocale();
// ãã±ãŒã«ã«åºã¥ããå¯äœçšïŒã¡ãŒã«ãCRMãªã©ïŒã«ãã±ãŒã«ã䜿çš
console.log(`ãã±ãŒã« ${locale} ã§ãµãŒããŒããã®åŠç`);
}
```
> ãã®ãã«ããŒã¯ Next.js ã®ã¯ãããŒãšããããŒã«äŸåããŠãããããRoute HandlersãServer Actionsããã®ä»ã®ãµãŒããŒå°çšã³ã³ããã¹ãã§åäœããŸãã
### ïŒãªãã·ã§ã³ïŒã¹ããã15ïŒã¡ã¿ããŒã¿ã®åœéå
ã³ã³ãã³ãã®ç¿»èš³ã¯éèŠã§ãããåœéåã®äž»ãªç®çã¯ããªãã®ãŠã§ããµã€ããäžçã«ããèŠããããã«ããããšã§ããI18n ã¯é©å㪠SEO ãéããŠãŠã§ããµã€ãã®å¯èŠæ§ãåäžãããããã®åŒ·åãªææ®µã§ãã
é©åã«åœéåãããã¡ã¿ããŒã¿ã¯ãæ€çŽ¢ãšã³ãžã³ãããŒãžã§å©çšå¯èœãªèšèªãçè§£ããã®ã«åœ¹ç«ã¡ãŸããããã«ã¯ãhreflang ã¡ã¿ã¿ã°ã®èšå®ãã¿ã€ãã«ã説æã®ç¿»èš³ãåãã±ãŒã«ã«å¯ŸããŠæ£ããã«ããã«ã« URL ã®èšå®ãå«ãŸããŸãã
å€èšèª SEO ã«é¢ãããã¹ããã©ã¯ãã£ã¹ã®ãªã¹ãã¯ä»¥äžã®éãã§ãïŒ
- `<head>`ã¿ã°å
ã«hreflangã¡ã¿ã¿ã°ãèšå®ããŠãæ€çŽ¢ãšã³ãžã³ãããŒãžã§å©çšå¯èœãªèšèªãçè§£ã§ããããã«ããŸã
- `http://www.w3.org/1999/xhtml` XMLã¹ããŒãã䜿çšããŠãsitemap.xmlã«ãã¹ãŠã®ããŒãžç¿»èš³ããªã¹ãããŸã
- ãã¬ãã£ãã¯ã¹ä»ãããŒãžãrobots.txtããé€å€ããã®ãå¿ããªãã§ãã ããïŒäŸïŒ`/dashboard`ã`/fr/dashboard`ã`/es/dashboard`ïŒ
- ã«ã¹ã¿ã Linkã³ã³ããŒãã³ãã䜿çšããŠãæãããŒã«ã©ã€ãºãããããŒãžã«ãªãã€ã¬ã¯ãããŸãïŒäŸïŒãã©ã³ã¹èªã§ã¯`<a href="/fr/about">à propos</a>`ïŒ
éçºè
ã¯ãã°ãã°ãã±ãŒã«éã§ããŒãžãé©åã«åç
§ããããšãå¿ããã¡ã§ãããããä¿®æ£ããŸãããïŒ
```tsx fileName="src/app/[locale]/about/layout.tsx"
import type { Metadata } from "next";
import {
locales,
defaultLocale,
localizedPath,
absoluteUrl,
} from "@/i18n.config";
/**
* åãã±ãŒã«ããŒãžã§ã³ã®ããŒãžã®SEOã¡ã¿ããŒã¿ãçæãã
* ãã®é¢æ°ã¯ãã«ãæã«åãã±ãŒã«ããšã«å®è¡ãããŸã
*/
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = params;
// ãã®ãã±ãŒã«ã®ç¿»èš³ãã¡ã€ã«ãåçã«ã€ã³ããŒã
// ã¡ã¿ããŒã¿ã®ã¿ã€ãã«ãšèª¬æã®ç¿»èš³ãååŸããããã«äœ¿çš
const messages = (await import(`@/locales/${locale}/about.json`)).default;
// ãã¹ãŠã®ãã±ãŒã«ã®hreflangãããã³ã°ãäœæ
// æ€çŽ¢ãšã³ãžã³ãèšèªã®ä»£æ¿ãçè§£ããã®ã«åœ¹ç«ã€
// ãã©ãŒããã: { "en": "/about", "fr": "/fr/about" }
const languages = Object.fromEntries(
locales.map((locale) => [locale, localizedPath(locale, "/about")])
);
return {
title: messages.title,
description: messages.description,
alternates: {
// ãã®ãã±ãŒã«ããŒãžã§ã³ã®æ£èŠURL
canonical: absoluteUrl(locale, "/about"),
// SEOã®ããã®èšèªä»£æ¿ïŒhreflangã¿ã°ïŒ
// "x-default"ã¯ããã©ã«ãã®ãã±ãŒã«ããŒãžã§ã³ãæå®
languages: {
...languages,
"x-default": absoluteUrl(defaultLocale, "/about"),
},
},
};
}
export default async function AboutPage() {
return <h1>About</h1>;
}
```
### ïŒãªãã·ã§ã³ïŒã¹ããã16ïŒãµã€ããããã®å€èšèªå¯Ÿå¿
ãã¹ãŠã®ãã±ãŒã«ããŒãžã§ã³ã®ããŒãžãå«ããµã€ãããããçæããŸããããã«ãããæ€çŽ¢ãšã³ãžã³ããã¹ãŠã®èšèªããŒãžã§ã³ã®ã³ã³ãã³ããæ€åºããã€ã³ããã¯ã¹åãããããªããŸãã
é©åã«å€èšèªå¯Ÿå¿ããããµã€ããããã¯ãæ€çŽ¢ãšã³ãžã³ããã¹ãŠã®èšèªããŒãžã§ã³ã®ããŒãžãèŠã€ããŠã€ã³ããã¯ã¹åã§ããããã«ããåœéçãªæ€çŽ¢çµæã§ã®å¯èŠæ§ãåäžãããŸãã
```ts fileName="src/app/sitemap.ts"
import type { MetadataRoute } from "next";
import { defaultLocale, locales } from "@/i18n";
const origin = "https://example.com";
const formatterLocalizedPath = (locale: string, path: string) =>
locale === defaultLocale ? `${origin}${path}` : `${origin}/${locale}${path}`;
/**
* ãã¹ãŠã®ãã±ãŒã«ãšãã®ããŒã«ã©ã€ãºããããã¹ã®ããããååŸãã
*
* åºåäŸ:
* {
* "en": "https://example.com",
* "fr": "https://example.com/fr",
* "es": "https://example.com/es",
* "x-default": "https://example.com"
* }
*/
const getLocalizedMap = (path: string) =>
Object.fromEntries([
...locales.map((locale) => [locale, formatterLocalizedPath(locale, path)]),
["x-default", formatterLocalizedPath(defaultLocale, path)],
]);
// ãã¹ãŠã®ãã±ãŒã«ããªã¢ã³ããå«ããµã€ãããããçæããSEOãåäžãããŸã
// alternatesãã£ãŒã«ãã¯æ€çŽ¢ãšã³ãžã³ã«èšèªããŒãžã§ã³ãç¥ãããŸã
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: formatterLocalizedPath(defaultLocale, "/"),
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1.0,
alternates: { languages: getLocalizedMap("/") },
},
{
url: formatterLocalizedPath(defaultLocale, "/about"),
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
alternates: { languages: getLocalizedMap("/about") },
},
];
}
```
### ïŒãªãã·ã§ã³ïŒã¹ããã17: robots.txtã®å€èšèªå¯Ÿå¿
ä¿è·ãããã«ãŒãã®ãã¹ãŠã®ãã±ãŒã«ããŒãžã§ã³ãé©åã«åŠçããrobots.txtãã¡ã€ã«ãäœæããŸããããã«ãããæ€çŽ¢ãšã³ãžã³ã管çè
ããŒãžãããã·ã¥ããŒãããŒãžãã©ã®èšèªã§ãã€ã³ããã¯ã¹ããªãããã«ããŸãã
ãã¹ãŠã®ãã±ãŒã«ã«å¯ŸããŠrobots.txtãé©åã«èšå®ããããšã§ãæ€çŽ¢ãšã³ãžã³ãæ©å¯ããŒãžãã©ã®èšèªã§ãã€ã³ããã¯ã¹ããã®ãé²ããŸããããã¯ã»ãã¥ãªãã£ãšãã©ã€ãã·ãŒã®ããã«éåžžã«éèŠã§ãã
```ts fileName="src/app/robots.ts"
import type { MetadataRoute } from "next";
import { defaultLocale, locales } from "@/i18n";
const origin = "https://example.com";
// ãã¹ãŠã®ãã±ãŒã«ã®ãã¹ãçæïŒäŸ: /admin, /fr/admin, /es/adminïŒ
const withAllLocales = (path: string) => [
path,
...locales
.filter((locale) => locale !== defaultLocale)
.map((locale) => `/${locale}${path}`),
];
const disallow = [...withAllLocales("/dashboard"), ...withAllLocales("/admin")];
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: ["/"], disallow },
host: origin,
sitemap: `${origin}/sitemap.xml`,
};
}
```
### ïŒãªãã·ã§ã³ïŒã¹ããã18ïŒãã±ãŒã«ã«ãŒãã£ã³ã°ã®ããã®ããã«ãŠã§ã¢èšå®
ãŠãŒã¶ãŒã®å¥œã¿ã®ãã±ãŒã«ãèªåçã«æ€åºããé©åãªãã±ãŒã«æ¥é èŸä»ãURLã«ãªãã€ã¬ã¯ããããããã·ãäœæããŸããããã«ããããŠãŒã¶ãŒã¯èªåã®å¥œã¿ã®èšèªã§ã³ã³ãã³ããé²èЧã§ãããŠãŒã¶ãŒäœéšãåäžããŸãã
ããã«ãŠã§ã¢ã¯ããŠãŒã¶ãŒããµã€ãã蚪ããéã«èªåçã«å¥œã¿ã®èšèªã«ãªãã€ã¬ã¯ãããããã«å°æ¥ã®èšªåã®ããã«ãã®èšèªèšå®ãã¯ãããŒã«ä¿åããŸãã
```ts fileName="src/proxy.ts"
import { NextResponse, type NextRequest } from "next/server";
import { defaultLocale, locales } from "@/i18n.config";
// æ¡åŒµåãæã€ãã¡ã€ã«ã«ãããããæ£èŠè¡šçŸïŒäŸïŒ.jsã.cssã.pngïŒ
// ãã±ãŒã«ã«ãŒãã£ã³ã°ããéçã¢ã»ãããé€å€ããããã«äœ¿çš
const PUBLIC_FILE = /\.[^/]+$/;
/**
* Accept-Language ããããŒãããã±ãŒã«ãæœåº
* "fr-CA"ã"en-US" ãªã©ã®åœ¢åŒã«å¯Ÿå¿
* ãã©ãŠã¶ã®èšèªããµããŒããããŠããªãå Žåã¯ããã©ã«ããã±ãŒã«ã«ãã©ãŒã«ããã¯
*/
const pickLocale = (accept: string | null) => {
// æåã®èšèªåªå
床ãååŸïŒäŸïŒ"fr-CA,en-US;q=0.9" ãã "fr-CA"ïŒ
const raw = accept?.split(",")[0] ?? defaultLocale;
// ããŒã¹èšèªã³ãŒããæœåºïŒäŸïŒ"fr-CA" ãã "fr"ïŒ
const base = raw.toLowerCase().split("-")[0];
// ãã®ãã±ãŒã«ããµããŒãããŠããã確èªãããã§ãªããã°ããã©ã«ãã䜿çš
return (locales as readonly string[]).includes(base) ? base : defaultLocale;
};
/**
* Next.js ã®ãã±ãŒã«æ€åºããã³ã«ãŒãã£ã³ã°çšãããã·
* ããŒãžãã¬ã³ããªã³ã°ãããåã®ãã¹ãŠã®ãªã¯ãšã¹ãã§å®è¡ããã
* å¿
èŠã«å¿ããŠãã±ãŒã«æ¥é èŸä»ãURLãžèªåãªãã€ã¬ã¯ããè¡ã
*/
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// Next.jsã®å
éšåŠçãAPIã«ãŒããéçãã¡ã€ã«ã¯ãããã·ãã¹ããã
// ãããã¯ãã±ãŒã«æ¥é èŸãä»ããªã
if (
pathname.startsWith("/_next") ||
pathname.startsWith("/api") ||
pathname.startsWith("/static") ||
PUBLIC_FILE.test(pathname)
) {
return;
}
// URLã«ãã§ã«ãã±ãŒã«æ¥é èŸãããã確èª
// äŸ: "/fr/about" ã "/en" 㯠true ãè¿ã
const hasLocale = (locales as readonly string[]).some(
(locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)
);
// ãã±ãŒã«ã®ãã¬ãã£ãã¯ã¹ããªãå Žåããã±ãŒã«ãæ€åºããŠãªãã€ã¬ã¯ã
if (!hasLocale) {
// ãŸãã¯ãããŒãããã±ãŒã«ãååŸããããšããïŒãŠãŒã¶ãŒã®èšå®ïŒ
const cookieLocale = request.cookies.get("NEXT_LOCALE")?.value;
// ã¯ãããŒã®ãã±ãŒã«ãæå¹ãªãããã䜿ããããã§ãªããã°ãã©ãŠã¶ã®ããããŒããæ€åº
const locale =
cookieLocale && (locales as readonly string[]).includes(cookieLocale)
? cookieLocale
: pickLocale(request.headers.get("accept-language"));
// ãã¹åã倿Žããããã«URLãã¯ããŒã³
const url = request.nextUrl.clone();
// ãã¹åã«ãã±ãŒã«ã®ãã¬ãã£ãã¯ã¹ã远å
// ã«ãŒããã¹ã®å Žåã¯äºéã¹ã©ãã·ã¥ãé¿ããããç¹å¥ã«åŠç
url.pathname = `/${locale}${pathname === "/" ? "" : pathname}`;
// ãªãã€ã¬ã¯ãã¬ã¹ãã³ã¹ãäœæãããã±ãŒã«ã¯ãããŒãèšå®ãã
const res = NextResponse.redirect(url);
res.cookies.set("NEXT_LOCALE", locale, { path: "/" });
return res;
}
}
export const config = {
matcher: [
// 以äžãé€ããã¹ãŠã®ãã¹ã«ããã:
// - APIã«ãŒã (/api/*)
// - Next.jsã®å
éšåŠç (/_next/*)
// - éçãã¡ã€ã« (/static/*)
// - æ¡åŒµåã®ãããã¡ã€ã« (.*\\..*)
"/((?!api|_next|static|.*\\..*).*)",
],
};
```
### ïŒãªãã·ã§ã³ïŒã¹ããã19: Intlayerã䜿ã£ã翻蚳ã®èªåå
Intlayerã¯ãã¢ããªã±ãŒã·ã§ã³ã®ããŒã«ãªãŒãŒã·ã§ã³ããã»ã¹ãæ¯æŽããããã«èšèšããã**ç¡æ**ãã€**ãªãŒãã³ãœãŒã¹**ã®ã©ã€ãã©ãªã§ããi18nextã翻蚳ã®èªã¿èŸŒã¿ãšç®¡çãæ
åœããäžæ¹ã§ãIntlayerã¯ç¿»èš³ã¯ãŒã¯ãããŒã®èªååãæ¯æŽããŸãã
翻蚳ãæåã§ç®¡çããããšã¯æéããããããã¹ãçºçããããäœæ¥ã§ããIntlayerã¯ç¿»èš³ã®ãã¹ããçæã管çãèªååããæéãç¯çŽãããšãšãã«ãã¢ããªã±ãŒã·ã§ã³å
šäœã§ã®äžè²«æ§ã確ä¿ããŸãã
Intlayerãå¯èœã«ããããšïŒ
- **ã³ãŒãããŒã¹å
ã®å¥œããªå Žæã§ã³ã³ãã³ãã宣èšãã**
Intlayerã¯ã`.content.{ts|js|json}`ãã¡ã€ã«ã䜿çšããŠãã³ãŒãããŒã¹å
ã®å¥œããªå Žæã§ã³ã³ãã³ãã宣èšããããšãå¯èœã«ããŸããããã«ãããã³ã³ãã³ãã®æŽçãåäžããã³ãŒãããŒã¹ã®å¯èªæ§ãšä¿å®æ§ãé«ãŸããŸãã
- **ç¿»èš³ã®æ¬ èœããã¹ããã**
Intlayerã¯ãCI/CDãã€ãã©ã€ã³ããŠããããã¹ãã«çµ±åå¯èœãªãã¹ãæ©èœãæäŸããŸãã詳现ã¯[翻蚳ã®ãã¹ã](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/testing.md)ãã芧ãã ããã
- **翻蚳ã®èªåå**
Intlayerã¯ç¿»èš³ãèªååããããã®CLIãšVSCodeæ¡åŒµæ©èœãæäŸããŸãããããã¯CI/CDãã€ãã©ã€ã³ã«çµ±åå¯èœã§ãã詳现ã¯[翻蚳ã®èªååã«ã€ããŠ](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/intlayer_cli.md)ãã芧ãã ããã
ãèªèº«ã®**APIããŒãã奜ã¿ã®AIãããã€ããŒã䜿çš**ã§ããŸãããŸããã³ã³ããã¹ãã«å¿ãã翻蚳ãæäŸããŠããŸãã詳现ã¯[ã³ã³ãã³ãã®èªåè£å®](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/autoFill.md)ãã芧ãã ããã
- **å€éšã³ã³ãã³ãã®æ¥ç¶**
- **翻蚳ã®èªåå**
Intlayerã¯ç¿»èš³ãèªååããããã®CLIãšVSCodeæ¡åŒµæ©èœãæäŸããŠããŸãããããã¯CI/CDãã€ãã©ã€ã³ã«çµ±åå¯èœã§ãã詳现ã¯[翻蚳ã®èªååã«ã€ããŠ](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/intlayer_cli.md)ãã芧ãã ããã
ãèªèº«ã®**APIããŒãã奜ã¿ã®AIãããã€ããŒ**ã䜿çšããããšãã§ããŸãããŸããã³ã³ããã¹ãã«å¿ãã翻蚳ãæäŸããŠããŸãã詳ããã¯[ã³ã³ãã³ãã®èªåè£å®](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/autoFill.md)ãã芧ãã ããã
- **å€éšã³ã³ãã³ãã®æ¥ç¶**
Intlayerã¯ã³ã³ãã³ããå€éšã®ã³ã³ãã³ã管çã·ã¹ãã ïŒCMSïŒã«æ¥ç¶ããããšãå¯èœã«ããŸããæé©åãããæ¹æ³ã§ååŸããJSONãªãœãŒã¹ã«æ¿å
¥ããŸãã詳现ã¯[å€éšã³ã³ãã³ãã®ååŸ](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/dictionary/function_fetching.md)ãã芧ãã ããã
- **ããžã¥ã¢ã«ãšãã£ã¿ãŒ**
Intlayerã¯ç¡æã®ããžã¥ã¢ã«ãšãã£ã¿ãŒãæäŸããŠãããèŠèŠçã«ã³ã³ãã³ããç·šéã§ããŸãã詳现ã¯[翻蚳ã®ããžã¥ã¢ã«ç·šé](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/intlayer_visual_editor.md)ãã芧ãã ããã
ãã®ä»ã«ã倿°ã®æ©èœããããŸããIntlayerãæäŸãããã¹ãŠã®æ©èœã«ã€ããŠã¯ã[Intlayerã®å©ç¹ã«é¢ããããã¥ã¡ã³ã](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ja/interest_of_intlayer.md)ããåç
§ãã ããã