@intlayer/docs 6.1.4 → 6.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blog/ar/next-i18next_vs_next-intl_vs_intlayer.md +1135 -75
- package/blog/ar/nextjs-multilingual-seo-comparison.md +364 -0
- package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +1139 -72
- package/blog/de/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/en/next-i18next_vs_next-intl_vs_intlayer.md +224 -240
- package/blog/en/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +1134 -37
- package/blog/en-GB/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +1122 -64
- package/blog/es/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +1132 -75
- package/blog/fr/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/hi/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +1120 -55
- package/blog/it/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +1140 -76
- package/blog/ja/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +1129 -73
- package/blog/ko/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +1133 -76
- package/blog/pt/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +1142 -74
- package/blog/ru/nextjs-multilingual-seo-comparison.md +370 -0
- package/blog/tr/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +1142 -75
- package/blog/zh/nextjs-multilingual-seo-comparison.md +394 -0
- package/dist/cjs/generated/blog.entry.cjs +16 -0
- package/dist/cjs/generated/blog.entry.cjs.map +1 -1
- package/dist/esm/generated/blog.entry.mjs +16 -0
- package/dist/esm/generated/blog.entry.mjs.map +1 -1
- package/dist/types/generated/blog.entry.d.ts +1 -0
- package/dist/types/generated/blog.entry.d.ts.map +1 -1
- package/docs/en/interest_of_intlayer.md +2 -2
- package/package.json +10 -10
- package/src/generated/blog.entry.ts +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
createdAt: 2025-08-23
|
|
3
|
-
updatedAt: 2025-
|
|
3
|
+
updatedAt: 2025-09-29
|
|
4
4
|
title: next-i18next vs next-intl vs Intlayer
|
|
5
5
|
description: Next.jsアプリの国際化(i18n)におけるnext-i18next、next-intl、Intlayerの比較
|
|
6
6
|
keywords:
|
|
@@ -19,144 +19,1208 @@ slugs:
|
|
|
19
19
|
|
|
20
20
|
# next-i18next VS next-intl VS intlayer | Next.jsの国際化(i18n)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
Next.js向けの3つのi18nオプション、next-i18next、next-intl、Intlayerの類似点と相違点を見ていきましょう。
|
|
23
|
+
|
|
24
|
+
これは完全なチュートリアルではなく、選択の参考となる比較です。
|
|
25
|
+
|
|
26
|
+
私たちは**Next.js 13+のApp Router**(**React Server Components**対応)に焦点を当て、以下を評価します:
|
|
24
27
|
|
|
25
28
|
1. **アーキテクチャとコンテンツの構成**
|
|
26
29
|
2. **TypeScriptと安全性**
|
|
27
|
-
3.
|
|
30
|
+
3. **翻訳欠落の処理**
|
|
28
31
|
4. **ルーティングとミドルウェア**
|
|
29
32
|
5. **パフォーマンスと読み込み挙動**
|
|
30
33
|
6. **開発者体験(DX)、ツールとメンテナンス**
|
|
31
|
-
7. **SEO
|
|
34
|
+
7. **SEOと大規模プロジェクトの拡張性**
|
|
35
|
+
|
|
36
|
+
> **要約**: 3つのいずれもNext.jsアプリのローカライズが可能です。もし**コンポーネント単位のコンテンツ管理**、**厳格なTypeScript型**、**ビルド時の欠落キー検出**、**ツリーシェイク可能な辞書**、そして**一流のApp Router + SEOヘルパー**を求めるなら、**Intlayer**が最も完全でモダンな選択肢です。
|
|
32
37
|
|
|
33
|
-
>
|
|
38
|
+
> 開発者がよく混同しがちなのは、`next-intl`が`react-intl`のNext.js版だと思うことです。そうではありません。`next-intl`は[Amann](https://github.com/amannn)によってメンテナンスされており、`react-intl`は[FormatJS](https://github.com/formatjs/formatjs)によってメンテナンスされています。
|
|
34
39
|
|
|
35
40
|
---
|
|
36
41
|
|
|
37
|
-
##
|
|
42
|
+
## 簡単に言うと
|
|
38
43
|
|
|
39
|
-
- **next-intl** - 軽量でシンプルなメッセージフォーマットを提供し、Next.js
|
|
40
|
-
- **next-i18next** - Next.js向けにラップされたi18next
|
|
41
|
-
- **Intlayer** - Next.js向けのコンポーネント中心のコンテンツモデル、**厳格なTS
|
|
44
|
+
- **next-intl** - 軽量でシンプルなメッセージフォーマットを提供し、Next.jsのサポートがしっかりしています。カタログは中央集権的であることが多く、開発者体験(DX)はシンプルですが、安全性や大規模なメンテナンスは主にあなたの責任となります。
|
|
45
|
+
- **next-i18next** - Next.js向けにラップされたi18nextです。成熟したエコシステムとプラグイン(例:ICU)による機能を持ちますが、設定が冗長になりがちで、プロジェクトが大きくなるにつれてカタログは中央集権化しやすいです。
|
|
46
|
+
- **Intlayer** - Next.js向けのコンポーネント中心のコンテンツモデル、**厳格なTS型付け**、**ビルド時チェック**、**ツリーシェイキング**、**組み込みのミドルウェア&SEOヘルパー**、オプションの**ビジュアルエディター/CMS**、および**AI支援翻訳**。
|
|
42
47
|
|
|
43
48
|
---
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
| Library | GitHub Stars | Total Commits | Last Commit | First Version | NPM Version | NPM Downloads |
|
|
51
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
52
|
+
| `aymericzip/intlayer` | [](https://github.com/aymericzip/intlayer/stargazers) | [](https://github.com/aymericzip/intlayer/commits) | [](https://github.com/aymericzip/intlayer/commits) | April 2024 | [](https://www.npmjs.com/package/intlayer) | [](https://www.npmjs.com/package/intlayer) |
|
|
53
|
+
| `amannn/next-intl` | [](https://github.com/amannn/next-intl/stargazers) | [](https://github.com/amannn/next-intl/commits) | [](https://github.com/amannn/next-intl/commits) | Nov 2020 | [](https://www.npmjs.com/package/next-intl) | [](https://www.npmjs.com/package/next-intl) |
|
|
54
|
+
| `i18next/i18next` | [](https://github.com/i18next/i18next/stargazers) | [](https://github.com/i18next/i18next/commits) | [](https://github.com/i18next/i18next/commits) | Jan 2012 | [](https://www.npmjs.com/package/i18next) | [](https://www.npmjs.com/package/i18next) |
|
|
55
|
+
| `i18next/next-i18next` | [](https://github.com/i18next/next-i18next/stargazers) | [](https://github.com/i18next/next-i18next/commits) | [](https://github.com/i18next/next-i18next/commits) | Nov 2018 | [](https://www.npmjs.com/package/next-i18next) | [](https://www.npmjs.com/package/next-i18next) |
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
-
| **コンポーネント近くの翻訳** | ✅ はい、各コンポーネントにコンテンツが配置されています | ❌ いいえ | ❌ いいえ |
|
|
50
|
-
| **TypeScript統合** | ✅ 高度、厳密な型が自動生成されます | ✅ 良好 | ⚠️ 基本的 |
|
|
51
|
-
| **翻訳漏れ検出** | ✅ TypeScriptのエラー強調表示およびビルド時のエラー/警告 | ⚠️ 実行時フォールバック | ⚠️ 実行時フォールバック |
|
|
52
|
-
| **リッチコンテンツ(JSX/Markdown/コンポーネント)** | ✅ 直接サポート | ❌ リッチノード向けに設計されていません | ⚠️ 制限あり |
|
|
53
|
-
| **AI搭載翻訳** | ✅ はい、複数のAIプロバイダーをサポート。独自のAPIキーを使用可能。アプリケーションとコンテンツの範囲のコンテキストを考慮します。 | ❌ いいえ | ❌ いいえ |
|
|
54
|
-
| **ビジュアルエディター** | ✅ はい、ローカルのビジュアルエディター+オプションのCMS;コードベースのコンテンツを外部化可能;埋め込み可能 | ❌ いいえ / 外部のローカリゼーションプラットフォーム経由で利用可能 | ❌ いいえ / 外部のローカリゼーションプラットフォーム経由で利用可能 |
|
|
55
|
-
| **ローカライズされたルーティング** | ✅ はい、ローカライズされたパスを標準でサポート(Next.js & Viteで動作) | ✅ 組み込み、App Routerは`[locale]`セグメントをサポート | ✅ 組み込み |
|
|
56
|
-
| **動的ルート生成** | ✅ はい | ✅ はい | ✅ はい |
|
|
57
|
-
| **複数形処理** | ✅ 列挙ベースのパターン | ✅ 良好 | ✅ 良好 |
|
|
58
|
-
| **フォーマット(日時、数値、通貨)** | ✅ 最適化されたフォーマッター(内部でIntlを使用) | ✅ 良好(Intlヘルパー) | ✅ 良好(Intlヘルパー) |
|
|
59
|
-
| **コンテンツフォーマット** | ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml 作業中) | ✅ .json, .js, .ts | ⚠️ .json |
|
|
60
|
-
| **ICUサポート** | ⚠️ 作業中 | ✅ あり | ⚠️ プラグイン経由(`i18next-icu`) |
|
|
61
|
-
| **SEOヘルパー(hreflang、サイトマップ)** | ✅ 組み込みツール:サイトマップ、robots.txt、メタデータのヘルパー | ✅ 良好 | ✅ 良好 |
|
|
62
|
-
| **エコシステム / コミュニティ** | ⚠️ 小規模だが急速に成長し、反応が良い | ✅ 中規模、Next.jsに特化 | ✅ 中規模、Next.jsに特化 |
|
|
63
|
-
| **サーバーサイドレンダリング&サーバーコンポーネント** | ✅ はい、SSR / Reactサーバーコンポーネント向けに最適化されています | ⚠️ ページレベルでサポートされていますが、子のサーバーコンポーネントに対してt関数をコンポーネントツリーに渡す必要があります | ⚠️ ページレベルでサポートされていますが、子のサーバーコンポーネントに対してt関数をコンポーネントツリーに渡す必要があります |
|
|
64
|
-
| **ツリーシェイキング(使用されるコンテンツのみ読み込み)** | ✅ はい、Babel/SWCプラグインを使用してビルド時にコンポーネント単位で実施されます | ⚠️ 部分的に対応 | ⚠️ 部分的に対応 |
|
|
65
|
-
| **遅延読み込み** | ✅ はい、ロケールごと / 辞書ごと | ✅ はい(ルートごと / ロケールごと)、名前空間管理が必要 | ✅ はい(ルートごと / ロケールごと)、名前空間管理が必要 |
|
|
66
|
-
| **未使用コンテンツの削除** | ✅ はい、ビルド時に辞書ごと | ❌ いいえ、名前空間管理で手動管理可能 | ❌ いいえ、名前空間管理で手動管理可能 |
|
|
67
|
-
| **大規模プロジェクトの管理** | ✅ モジュール化を推奨し、デザインシステムに適している | ✅ セットアップによるモジュール化 | ✅ セットアップによるモジュール化 |
|
|
57
|
+
> バッジは自動的に更新されます。スナップショットは時間とともに変動します。
|
|
68
58
|
|
|
69
59
|
---
|
|
70
60
|
|
|
71
|
-
##
|
|
61
|
+
## 並列機能比較(Next.jsに特化)
|
|
72
62
|
|
|
73
|
-
|
|
63
|
+
| 機能 | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
|
|
74
64
|
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
> バッジは自動的に更新されます。スナップショットは時間とともに変化します。
|
|
66
|
+
|
|
67
|
+
---
|
|
77
68
|
|
|
78
|
-
|
|
69
|
+
## 並列機能比較(Next.jsに特化)
|
|
70
|
+
|
|
71
|
+
| 機能 | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
|
|
72
|
+
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
73
|
+
| **コンポーネント近くの翻訳** | ✅ はい、各コンポーネントにコンテンツが配置されています | ❌ いいえ | ❌ いいえ |
|
|
74
|
+
| **TypeScript 統合** | ✅ 高度、自動生成された厳密な型 | ✅ 良好 | ⚠️ 基本 |
|
|
75
|
+
| **翻訳漏れ検出** | ✅ TypeScript エラーのハイライトおよびビルド時のエラー/警告 | ⚠️ ランタイムフォールバック | ⚠️ ランタイムフォールバック |
|
|
76
|
+
| **リッチコンテンツ(JSX/Markdown/コンポーネント)** | ✅ 直接サポート | ❌ リッチノード向けに設計されていません | ⚠️ 制限あり |
|
|
77
|
+
| **AI搭載翻訳** | ✅ はい、複数のAIプロバイダーをサポート。独自のAPIキーを使用可能。アプリケーションのコンテキストとコンテンツの範囲を考慮します | ❌ いいえ | ❌ いいえ |
|
|
78
|
+
| **ビジュアルエディター** | ✅ はい、ローカルのビジュアルエディター+オプションのCMS;コードベースのコンテンツを外部化可能;埋め込み可能 | ❌ いいえ/外部のローカリゼーションプラットフォーム経由で利用可能 | ❌ いいえ/外部のローカリゼーションプラットフォーム経由で利用可能 |
|
|
79
|
+
| **ローカライズされたルーティング** | ✅ はい、標準でローカライズされたパスをサポート(Next.js & Viteで動作) | ✅ 組み込み、App Routerは`[locale]`セグメントをサポート | ✅ 組み込み |
|
|
80
|
+
| **動的ルート生成** | ✅ はい | ✅ はい | ✅ はい |
|
|
81
|
+
| **複数形対応** | ✅ 列挙ベースのパターン | ✅ 良好 | ✅ 良好 |
|
|
82
|
+
| **フォーマット(日時、数値、通貨)** | ✅ 最適化されたフォーマッター(内部でIntlを使用) | ✅ 良好(Intlヘルパー) | ✅ 良好(Intlヘルパー) |
|
|
83
|
+
| **コンテンツフォーマット** | ✅ .tsx、.ts、.js、.json、.md、.txt、(.yaml 開発中) | ✅ .json、.js、.ts | ⚠️ .json |
|
|
84
|
+
| **ICUサポート** | ⚠️ 作業中 | ✅ あり | ⚠️ プラグイン経由(`i18next-icu`) |
|
|
85
|
+
| **SEOヘルパー(hreflang、サイトマップ)** | ✅ 組み込みツール:サイトマップ、robots.txt、メタデータのヘルパー | ✅ 良好 | ✅ 良好 |
|
|
86
|
+
| **エコシステム / コミュニティ** | ⚠️ 小規模だが急速に成長し、反応が良い | ✅ 良好 | ✅ 良好 |
|
|
87
|
+
| **サーバーサイドレンダリング & サーバーコンポーネント** | ✅ はい、SSR / Reactサーバーコンポーネント向けに最適化 | ⚠️ ページレベルでサポートされているが、子のサーバーコンポーネントに対してt関数をコンポーネントツリーに渡す必要がある | ⚠️ ページレベルでサポートされているが、子のサーバーコンポーネントに対してt関数をコンポーネントツリーに渡す必要がある |
|
|
88
|
+
| **ツリーシェイキング(使用されたコンテンツのみを読み込む)** | ✅ はい、Babel/SWCプラグインを使用したビルド時のコンポーネント単位で対応 | ⚠️ 部分的に対応 | ⚠️ 部分的に対応 |
|
|
89
|
+
| **遅延読み込み** | ✅ はい、ロケール単位 / 辞書単位で対応 | ✅ はい(ルート単位/ロケール単位)、名前空間管理が必要 | ✅ はい(ルート単位/ロケール単位)、名前空間管理が必要 |
|
|
90
|
+
| **未使用コンテンツの削除** | ✅ はい、ビルド時に辞書単位で対応 | ❌ いいえ、名前空間管理で手動対応可能 | ❌ いいえ、名前空間管理で手動対応可能 |
|
|
91
|
+
| **大規模プロジェクトの管理** | ✅ モジュール化を推奨し、デザインシステムに適している | ✅ セットアップによるモジュール化対応 | ✅ セットアップによるモジュール化対応 |
|
|
92
|
+
| **翻訳漏れのテスト(CLI/CI)** | ✅ CLI: `npx intlayer content test`(CIに適した監査) | ⚠️ 組み込みではない;ドキュメントでは `npx @lingual/i18n-check` を推奨 | ⚠️ 組み込みではない;i18nextツールやランタイムの `saveMissing` に依存 |
|
|
79
93
|
|
|
80
94
|
---
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
## はじめに
|
|
83
97
|
|
|
84
|
-
|
|
85
|
-
- **next-i18next**: フックの基本的な型定義がありますが、**厳密なキーの型付けには追加のツールや設定が必要です**。
|
|
86
|
-
- **Intlayer**: コンテンツから**厳密な型を生成**します。**IDEの自動補完**や**コンパイル時のエラー**により、デプロイ前にタイプミスやキーの欠落を検出します。
|
|
98
|
+
Next.jsは国際化されたルーティング(例:ロケールセグメント)を組み込みでサポートしています。しかし、その機能だけでは翻訳は行われません。ユーザーにローカライズされたコンテンツを表示するには、別途ライブラリが必要です。
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
多くのi18nライブラリが存在しますが、Next.jsの世界では現在、next-i18next、next-intl、そしてIntlayerの3つが注目されています。
|
|
89
101
|
|
|
90
102
|
---
|
|
91
103
|
|
|
92
|
-
|
|
104
|
+
## アーキテクチャとスケーラビリティ
|
|
93
105
|
|
|
94
|
-
- **next-intl / next-i18next**:
|
|
95
|
-
- **Intlayer**:
|
|
106
|
+
- **next-intl / next-i18next**: ロケールごとに **集中管理されたカタログ**(および i18next の場合は **ネームスペース**)をデフォルトとします。初期段階では問題なく機能しますが、結合度が高まりキーの変更が頻繁になると、大きな共有領域となってしまいます。
|
|
107
|
+
- **Intlayer**: サービスするコードと **共置** された **コンポーネント単位**(または機能単位)の辞書を推奨します。これにより認知負荷が軽減され、UIパーツの重複や移行が容易になり、チーム間の競合も減少します。未使用のコンテンツも自然に見つけやすく削除しやすくなります。
|
|
96
108
|
|
|
97
|
-
**なぜ重要か:**
|
|
109
|
+
**なぜ重要か:** 大規模なコードベースやデザインシステムのセットアップでは、**モジュール化されたコンテンツ**の方がモノリシックなカタログよりもスケールしやすいです。
|
|
98
110
|
|
|
99
111
|
---
|
|
100
112
|
|
|
101
|
-
|
|
113
|
+
## バンドルサイズと依存関係
|
|
114
|
+
|
|
115
|
+
アプリケーションをビルドした後、バンドルとはブラウザがページをレンダリングするために読み込むJavaScriptのことです。したがって、バンドルサイズはアプリケーションのパフォーマンスにとって重要です。
|
|
116
|
+
|
|
117
|
+
多言語アプリケーションのバンドルにおいて重要な2つの要素は以下の通りです:
|
|
118
|
+
|
|
119
|
+
- アプリケーションコード
|
|
120
|
+
- ブラウザによって読み込まれるコンテンツ
|
|
121
|
+
|
|
122
|
+
## アプリケーションコード
|
|
123
|
+
|
|
124
|
+
この場合、アプリケーションコードの重要性は最小限です。3つのソリューションすべてがツリーシェイカブルであり、未使用のコード部分はバンドルに含まれません。
|
|
125
|
+
|
|
126
|
+
以下は、3つのソリューションを用いた多言語アプリケーションでブラウザが読み込むJavaScriptバンドルサイズの比較です。
|
|
127
|
+
|
|
128
|
+
アプリケーション内でフォーマッターを必要としない場合、ツリーシェイキング後にエクスポートされる関数のリストは以下のようになります:
|
|
129
|
+
|
|
130
|
+
- **next-intlayer**: `useIntlayer`, `useLocale`, `NextIntlClientProvider`、(バンドルサイズは180.6 kB -> 78.6 kB(gzip))
|
|
131
|
+
- **next-intl**: `useTranslations`, `useLocale`, `NextIntlClientProvider`、(バンドルサイズは101.3 kB -> 31.4 kB(gzip))
|
|
132
|
+
- **next-i18next**: `useTranslation`, `useI18n`, `I18nextProvider`、(バンドルサイズは80.7 kB -> 25.5 kB(gzip))
|
|
133
|
+
|
|
134
|
+
これらの関数はReactのコンテキスト/ステートのラッパーに過ぎないため、i18nライブラリがバンドルサイズに与える影響は最小限です。
|
|
135
|
+
|
|
136
|
+
> Intlayerは、`useIntlayer`関数により多くのロジックを含んでいるため、`next-intl`や`next-i18next`よりわずかに大きくなっています。これはマークダウンや`intlayer-editor`の統合に関連しています。
|
|
137
|
+
|
|
138
|
+
## コンテンツと翻訳
|
|
139
|
+
|
|
140
|
+
この部分は開発者によってしばしば無視されますが、10ページで構成され、10言語に対応したアプリケーションの場合を考えてみましょう。計算を簡単にするために、各ページが100%ユニークなコンテンツを統合していると仮定します(実際には、ページタイトル、ヘッダー、フッターなど、ページ間で重複するコンテンツが多くあります)。
|
|
102
141
|
|
|
103
|
-
-
|
|
104
|
-
- **Intlayer** はさらに進んで、**i18n ミドルウェア**(ヘッダーやクッキーによるロケール検出)や、ローカライズされた URL や `<link rel="alternate" hreflang="…">` タグを生成するための **ヘルパー** を提供します。
|
|
142
|
+
`/fr/about` ページを訪れたいユーザーは、特定の言語で1ページ分のコンテンツを読み込みます。コンテンツの最適化を無視すると、アプリケーションのコンテンツの8,200% `((1 + (((10ページ - 1) × (10言語 - 1)))) × 100)` を不必要に読み込むことになります。この問題がわかりますか?このコンテンツがテキストのままであっても、おそらくサイトの画像の最適化を考える方が多いでしょうが、無駄なコンテンツを世界中に送信し、ユーザーのコンピューターに無意味な処理をさせているのです。
|
|
105
143
|
|
|
106
|
-
|
|
144
|
+
2つの重要な問題:
|
|
145
|
+
|
|
146
|
+
- **ルートによる分割:**
|
|
147
|
+
|
|
148
|
+
> `/about` ページにいる場合、`/home` ページのコンテンツを読み込みたくない
|
|
149
|
+
|
|
150
|
+
- **ロケールによる分割:**
|
|
151
|
+
|
|
152
|
+
> `/fr/about` ページにいる場合、`/en/about` ページのコンテンツを読み込みたくない
|
|
153
|
+
|
|
154
|
+
改めて、これら3つのソリューションはこれらの問題を認識しており、これらの最適化を管理することができます。3つのソリューションの違いはDX(開発者体験)にあります。
|
|
155
|
+
|
|
156
|
+
`next-intl` と `next-i18next` は翻訳を管理するために集中管理型のアプローチを使用しており、ロケールやサブファイルごとにJSONを分割することが可能です。`next-i18next` ではJSONファイルを「ネームスペース」と呼び、`next-intl` ではメッセージを宣言することができます。`intlayer` ではJSONファイルを「辞書」と呼びます。
|
|
157
|
+
|
|
158
|
+
- `next-intl`の場合は、`next-i18next`と同様に、コンテンツはページやレイアウトレベルで読み込まれ、その後このコンテンツがコンテキストプロバイダーに読み込まれます。つまり、開発者は各ページで読み込まれるJSONファイルを手動で管理する必要があります。
|
|
159
|
+
|
|
160
|
+
> 実際には、開発者はこの最適化を省略し、単純さのためにページのコンテキストプロバイダーにすべてのコンテンツを読み込むことを好むことが多いです。
|
|
161
|
+
|
|
162
|
+
- `intlayer`の場合は、すべてのコンテンツがアプリケーション内で読み込まれます。その後、プラグイン(`@intlayer/babel` / `@intlayer/swc`)がバンドルを最適化し、ページで使用されるコンテンツのみを読み込みます。したがって、開発者は読み込まれる辞書を手動で管理する必要がありません。これにより、より良い最適化、より良い保守性、そして開発時間の短縮が可能になります。
|
|
163
|
+
|
|
164
|
+
アプリケーションが成長するにつれて(特に複数の開発者がアプリケーションに関わっている場合)、JSONファイルからもはや使用されていないコンテンツを削除し忘れることがよくあります。
|
|
165
|
+
|
|
166
|
+
> すべてのJSONはすべての場合に読み込まれることに注意してください(next-intl、next-i18next、intlayer)。
|
|
167
|
+
|
|
168
|
+
これがIntlayerのアプローチがよりパフォーマンスに優れている理由です。コンポーネントがもはや使用されていない場合、その辞書はバンドルに読み込まれません。
|
|
169
|
+
|
|
170
|
+
ライブラリがフォールバックをどのように処理するかも重要です。アプリケーションがデフォルトで英語であり、ユーザーが`/fr/about`ページを訪れたとします。フランス語の翻訳が欠けている場合、英語のフォールバックが考慮されます。
|
|
171
|
+
|
|
172
|
+
`next-intl` と `next-i18next` の場合、ライブラリは現在のロケールに関連する JSON に加えて、フォールバックロケールの JSON も読み込む必要があります。したがって、すべてのコンテンツが翻訳されていると仮定すると、各ページは 100% 不要なコンテンツを読み込むことになります。**これに対して、`intlayer` は辞書のビルド時にフォールバックを処理します。したがって、各ページは使用されるコンテンツのみを読み込みます。**
|
|
173
|
+
|
|
174
|
+
以下は、vite + react アプリケーションで `intlayer` を使用したバンドルサイズ最適化の影響の例です:
|
|
175
|
+
|
|
176
|
+
| 最適化されたバンドル | 最適化されていないバンドル |
|
|
177
|
+
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
178
|
+
|  |  |
|
|
107
179
|
|
|
108
180
|
---
|
|
109
181
|
|
|
110
|
-
|
|
182
|
+
## TypeScript と安全性
|
|
183
|
+
|
|
184
|
+
<Columns>
|
|
185
|
+
<Column>
|
|
111
186
|
|
|
112
|
-
-
|
|
113
|
-
- **Intlayer** は一貫した API と RSC 向けに設計されたプロバイダーで、**サーバー/クライアントの境界** をスムーズにし、フォーマッターや t 関数をコンポーネントツリー間で渡す必要をなくします。
|
|
187
|
+
**next-intl**
|
|
114
188
|
|
|
115
|
-
|
|
189
|
+
- 安定した TypeScript サポートを提供しますが、**キーはデフォルトで厳密に型付けされていません**。安全性のパターンは手動で維持する必要があります。
|
|
190
|
+
|
|
191
|
+
</Column>
|
|
192
|
+
<Column>
|
|
193
|
+
|
|
194
|
+
**next-i18next**
|
|
195
|
+
|
|
196
|
+
- フックの基本的な型定義がありますが、**厳密なキーの型付けには追加のツールや設定が必要です**。
|
|
197
|
+
|
|
198
|
+
</Column>
|
|
199
|
+
<Column>
|
|
200
|
+
|
|
201
|
+
**intlayer**
|
|
202
|
+
|
|
203
|
+
- **コンテンツから厳密な型を生成**します。**IDEのオートコンプリート**や**コンパイル時エラー**により、デプロイ前にタイプミスやキーの欠落を検出します。
|
|
204
|
+
|
|
205
|
+
</Column>
|
|
206
|
+
</Columns>
|
|
207
|
+
|
|
208
|
+
**なぜ重要か:** 強い型付けにより、失敗を**右(実行時)**ではなく**左(CI/ビルド時)**にシフトさせます。
|
|
116
209
|
|
|
117
210
|
---
|
|
118
211
|
|
|
119
|
-
|
|
212
|
+
## 翻訳欠落の取り扱い
|
|
213
|
+
|
|
214
|
+
**next-intl**
|
|
120
215
|
|
|
121
|
-
-
|
|
122
|
-
- **Intlayer**: ビルド時に**ツリーシェイク**を行い、辞書やロケールごとに**遅延ロード**します。未使用のコンテンツは配信されません。
|
|
216
|
+
- **実行時のフォールバック**に依存(例:キーやデフォルトロケールを表示)。ビルドは失敗しません。
|
|
123
217
|
|
|
124
|
-
|
|
218
|
+
**next-i18next**
|
|
219
|
+
|
|
220
|
+
- **実行時のフォールバック**に依存(例:キーやデフォルトロケールを表示)。ビルドは失敗しません。
|
|
221
|
+
|
|
222
|
+
**intlayer**
|
|
223
|
+
|
|
224
|
+
- **ビルド時検出**により、ロケールやキーの欠落に対して**警告/エラー**を出します。
|
|
225
|
+
|
|
226
|
+
**なぜ重要か:** ビルド時に欠落を検出することで、本番環境での「謎の文字列」発生を防ぎ、厳格なリリースゲートに適合します。
|
|
125
227
|
|
|
126
228
|
---
|
|
127
229
|
|
|
128
|
-
|
|
230
|
+
## ルーティング、ミドルウェア & URL戦略
|
|
231
|
+
|
|
232
|
+
<Columns>
|
|
233
|
+
<Column>
|
|
234
|
+
|
|
235
|
+
**next-intl**
|
|
236
|
+
|
|
237
|
+
- App Router上の**Next.jsのローカライズされたルーティング**に対応。
|
|
238
|
+
|
|
239
|
+
</Column>
|
|
240
|
+
<Column>
|
|
129
241
|
|
|
130
|
-
|
|
131
|
-
- **Intlayer**: **無料のビジュアルエディター**と**オプションのCMS**(Git対応または外部化)を提供します。さらに、コンテンツ作成用の**VSCode拡張機能**や、独自のプロバイダーキーを使った**AI支援翻訳**も備えています。
|
|
242
|
+
**next-i18next**
|
|
132
243
|
|
|
133
|
-
|
|
244
|
+
- App Router上の**Next.jsのローカライズされたルーティング**に対応。
|
|
245
|
+
|
|
246
|
+
</Column>
|
|
247
|
+
<Column>
|
|
248
|
+
|
|
249
|
+
**intlayer**
|
|
250
|
+
|
|
251
|
+
- 上記すべてに加え、**i18nミドルウェア**(ヘッダーやクッキーによるロケール検出)およびローカライズされたURLや`<link rel="alternate" hreflang="…">`タグを生成する**ヘルパー**を提供。
|
|
252
|
+
|
|
253
|
+
</Column>
|
|
254
|
+
</Columns>
|
|
255
|
+
|
|
256
|
+
**重要な理由:** カスタムの接着層が減り、**一貫したUX**と**クリーンなSEO**をロケール間で実現。
|
|
134
257
|
|
|
135
258
|
---
|
|
136
259
|
|
|
137
|
-
##
|
|
260
|
+
## サーバーコンポーネント(RSC)対応
|
|
261
|
+
|
|
262
|
+
<Columns>
|
|
263
|
+
<Column>
|
|
264
|
+
|
|
265
|
+
**next-intl**
|
|
266
|
+
|
|
267
|
+
- Next.js 13+をサポート。ハイブリッド構成では、t関数やフォーマッターをコンポーネントツリーに渡すことが多い。
|
|
268
|
+
|
|
269
|
+
</Column>
|
|
270
|
+
<Column>
|
|
138
271
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
272
|
+
**next-i18next**
|
|
273
|
+
|
|
274
|
+
- Next.js 13+ をサポート。翻訳ユーティリティを境界を越えて渡す際に類似の制約があります。
|
|
275
|
+
|
|
276
|
+
</Column>
|
|
277
|
+
<Column>
|
|
278
|
+
|
|
279
|
+
**intlayer**
|
|
280
|
+
|
|
281
|
+
- Next.js 13+ をサポートし、一貫した API と RSC 指向のプロバイダーで **サーバー/クライアントの境界** をスムーズにし、フォーマッターや t-関数のやり取りを回避します。
|
|
282
|
+
|
|
283
|
+
</Column>
|
|
284
|
+
</Columns>
|
|
285
|
+
|
|
286
|
+
**重要な理由:** ハイブリッドツリーにおけるメンタルモデルがより明確になり、エッジケースが減少します。
|
|
142
287
|
|
|
143
288
|
---
|
|
144
289
|
|
|
145
|
-
##
|
|
290
|
+
## DX、ツール&メンテナンス
|
|
291
|
+
|
|
292
|
+
<Columns>
|
|
293
|
+
<Column>
|
|
294
|
+
|
|
295
|
+
**next-intl**
|
|
296
|
+
|
|
297
|
+
- 外部のローカリゼーションプラットフォームや編集ワークフローと組み合わせて使われることが多いです。
|
|
298
|
+
|
|
299
|
+
</Column>
|
|
300
|
+
<Column>
|
|
301
|
+
|
|
302
|
+
**next-i18next**
|
|
303
|
+
|
|
304
|
+
- 外部のローカリゼーションプラットフォームや編集ワークフローと組み合わせて使われることが多いです。
|
|
305
|
+
|
|
306
|
+
</Column>
|
|
307
|
+
<Column>
|
|
308
|
+
|
|
309
|
+
**intlayer**
|
|
310
|
+
|
|
311
|
+
- 無料の**ビジュアルエディター**と**オプションのCMS**(Git対応または外部化可能)を提供し、さらに**VSCode拡張機能**と、独自のプロバイダーキーを使用した**AI支援翻訳**も備えています。
|
|
312
|
+
|
|
313
|
+
</Column>
|
|
314
|
+
</Columns>
|
|
315
|
+
|
|
316
|
+
**重要な理由:** 運用コストを削減し、開発者とコンテンツ作成者間のフィードバックループを短縮します。
|
|
317
|
+
|
|
318
|
+
## ローカリゼーションプラットフォーム(TMS)との統合
|
|
319
|
+
|
|
320
|
+
大規模な組織では、**Crowdin**、**Phrase**、**Lokalise**、**Localizely**、**Localazy**などの翻訳管理システム(TMS)に依存することが多いです。
|
|
321
|
+
|
|
322
|
+
- **企業が重視する理由**
|
|
323
|
+
- **協力と役割分担**:複数の関係者が関与します。開発者、プロダクトマネージャー、翻訳者、レビュアー、マーケティングチームなど。
|
|
324
|
+
- **規模と効率性**:継続的なローカリゼーション、コンテキスト内レビュー。
|
|
325
|
+
|
|
326
|
+
- **next-intl / next-i18next**
|
|
327
|
+
- 通常は**集中管理されたJSONカタログ**を使用するため、TMSとのエクスポート/インポートが簡単です。
|
|
328
|
+
- 上記プラットフォーム向けの成熟したエコシステムや例/統合があります。
|
|
329
|
+
|
|
330
|
+
- **Intlayer**
|
|
331
|
+
- **分散型のコンポーネントごとの辞書**を推奨し、**TypeScript/TSX/JS/JSON/MD**コンテンツをサポートします。
|
|
332
|
+
- これによりコードのモジュール性が向上しますが、ツールが集中管理されたフラットなJSONファイルを期待する場合、プラグアンドプレイのTMS統合が難しくなることがあります。
|
|
333
|
+
- Intlayerは代替手段を提供します:**AI支援翻訳**(ご自身のプロバイダーキーを使用)、**ビジュアルエディター/CMS**、およびギャップを検出して事前入力するための**CLI/CI**ワークフロー。
|
|
334
|
+
|
|
335
|
+
> 注意: `next-intl` と `i18next` は TypeScript カタログも受け入れます。もしチームがメッセージを `.ts` ファイルに保存したり、機能ごとに分散管理している場合、同様の TMS の摩擦に直面することがあります。しかし、多くの `next-intl` のセットアップは `locales/` フォルダに集中しており、TMS 用に JSON にリファクタリングするのが少し容易です。
|
|
336
|
+
|
|
337
|
+
## 開発者体験
|
|
338
|
+
|
|
339
|
+
この部分では、3つのソリューションを深く比較します。各ソリューションの「はじめに」ドキュメントに記載されている単純なケースを考慮するのではなく、より実際のプロジェクトに近い実用的なユースケースを考えます。
|
|
340
|
+
|
|
341
|
+
### アプリ構造
|
|
342
|
+
|
|
343
|
+
アプリの構造は、コードベースの良好な保守性を確保するために重要です。
|
|
344
|
+
|
|
345
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
346
|
+
|
|
347
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
.
|
|
351
|
+
├── public
|
|
352
|
+
│ └── locales
|
|
353
|
+
│ ├── en
|
|
354
|
+
│ │ ├── home.json
|
|
355
|
+
│ │ └── navbar.json
|
|
356
|
+
│ ├── fr
|
|
357
|
+
│ │ ├── home.json
|
|
358
|
+
│ │ └── navbar.json
|
|
359
|
+
│ └── es
|
|
360
|
+
│ ├── home.json
|
|
361
|
+
│ └── navbar.json
|
|
362
|
+
├── next-i18next.config.js
|
|
363
|
+
└── src
|
|
364
|
+
├── middleware.ts
|
|
365
|
+
├── app
|
|
366
|
+
│ └── home.tsx
|
|
367
|
+
└── components
|
|
368
|
+
└── Navbar
|
|
369
|
+
└── index.tsx
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
</TabItem>
|
|
373
|
+
<TabItem label="next-intl" value="next-intl">
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
.
|
|
377
|
+
├── locales
|
|
378
|
+
│ ├── en
|
|
379
|
+
│ │ ├── home.json
|
|
380
|
+
│ │ └── navbar.json
|
|
381
|
+
│ ├── fr
|
|
382
|
+
│ │ ├── home.json
|
|
383
|
+
│ │ └── navbar.json
|
|
384
|
+
│ └── es
|
|
385
|
+
│ ├── home.json
|
|
386
|
+
│ └── navbar.json
|
|
387
|
+
├── i18n.ts
|
|
388
|
+
└── src
|
|
389
|
+
├── middleware.ts
|
|
390
|
+
├── app
|
|
391
|
+
│ └── home.tsx
|
|
392
|
+
└── components
|
|
393
|
+
└── Navbar
|
|
394
|
+
└── index.tsx
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
</TabItem>
|
|
398
|
+
<TabItem label="intlayer" value="intlayer">
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
.
|
|
402
|
+
├── intlayer.config.ts
|
|
403
|
+
└── src
|
|
404
|
+
├── middleware.ts
|
|
405
|
+
├── app
|
|
406
|
+
│ └── home
|
|
407
|
+
│ └── index.tsx
|
|
408
|
+
│ └── index.content.ts
|
|
409
|
+
└── components
|
|
410
|
+
└── Navbar
|
|
411
|
+
├── index.tsx
|
|
412
|
+
└── index.content.ts
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
</TabItem>
|
|
416
|
+
</Tab>
|
|
417
|
+
|
|
418
|
+
#### 比較
|
|
419
|
+
|
|
420
|
+
- **next-intl / next-i18next**: 集中管理されたカタログ(JSON; 名前空間/メッセージ)。構造が明確で翻訳プラットフォームとよく統合されるが、アプリが大きくなるとファイル間の編集が増える可能性がある。
|
|
421
|
+
- **Intlayer**: コンポーネントごとに `.content.{ts|js|json}` 辞書がコンポーネントと同じ場所に配置されている。コンポーネントの再利用や局所的な理解が容易になるが、ファイルが増え、ビルド時のツールに依存する。
|
|
422
|
+
|
|
423
|
+
#### セットアップとコンテンツの読み込み
|
|
424
|
+
|
|
425
|
+
前述のように、各JSONファイルのインポート方法を最適化する必要があります。
|
|
426
|
+
ライブラリがコンテンツの読み込みをどのように処理するかが重要です。
|
|
427
|
+
|
|
428
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
429
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
430
|
+
|
|
431
|
+
```tsx fileName="next-i18next.config.js"
|
|
432
|
+
module.exports = {
|
|
433
|
+
i18n: {
|
|
434
|
+
locales: ["en", "fr", "es"],
|
|
435
|
+
defaultLocale: "en",
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
```tsx fileName="src/app/_app.tsx"
|
|
441
|
+
import { appWithTranslation } from "next-i18next";
|
|
442
|
+
|
|
443
|
+
const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
|
|
444
|
+
|
|
445
|
+
export default appWithTranslation(MyApp);
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
449
|
+
import type { GetStaticProps } from "next";
|
|
450
|
+
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
|
451
|
+
import { useTranslation } from "next-i18next";
|
|
452
|
+
import { I18nextProvider, initReactI18next } from "react-i18next";
|
|
453
|
+
import { createInstance } from "i18next";
|
|
454
|
+
import { ClientComponent, ServerComponent } from "@components";
|
|
455
|
+
|
|
456
|
+
export default function HomePage({ locale }: { locale: string }) {
|
|
457
|
+
// このコンポーネントで使用する名前空間を明示的に宣言します
|
|
458
|
+
const resources = await loadMessagesFor(locale); // あなたのローダー(JSONなど)
|
|
459
|
+
|
|
460
|
+
const i18n = createInstance();
|
|
461
|
+
i18n.use(initReactI18next).init({
|
|
462
|
+
lng: locale,
|
|
463
|
+
fallbackLng: "en",
|
|
464
|
+
resources,
|
|
465
|
+
ns: ["common", "about"],
|
|
466
|
+
defaultNS: "common",
|
|
467
|
+
interpolation: { escapeValue: false },
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const { t } = useTranslation("about");
|
|
471
|
+
|
|
472
|
+
return (
|
|
473
|
+
<I18nextProvider i18n={i18n}>
|
|
474
|
+
<main>
|
|
475
|
+
<h1>{t("title")}</h1>
|
|
476
|
+
<ClientComponent />
|
|
477
|
+
<ServerComponent />
|
|
478
|
+
</main>
|
|
479
|
+
</I18nextProvider>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export const getStaticProps: GetStaticProps = async ({ locale }) => {
|
|
484
|
+
// このページに必要な名前空間のみをプリロードします
|
|
485
|
+
return {
|
|
486
|
+
props: {
|
|
487
|
+
...(await serverSideTranslations(locale ?? "en", ["common", "about"])),
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
</TabItem>
|
|
494
|
+
<TabItem label="next-intl" value="next-intl">
|
|
495
|
+
|
|
496
|
+
```tsx fileName="i18n.ts"
|
|
497
|
+
import { getRequestConfig } from "next-intl/server";
|
|
498
|
+
import { notFound } from "next/navigation";
|
|
499
|
+
|
|
500
|
+
// 共有設定からインポート可能
|
|
501
|
+
const locales = ["en", "fr", "es"];
|
|
502
|
+
|
|
503
|
+
export default getRequestConfig(async ({ locale }) => {
|
|
504
|
+
// 受け取った `locale` パラメータが有効か検証します
|
|
505
|
+
if (!locales.includes(locale as any)) notFound();
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
messages: (await import(`../messages/${locale}.json`)).default,
|
|
509
|
+
};
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
514
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
515
|
+
import { getMessages, unstable_setRequestLocale } from "next-intl/server";
|
|
516
|
+
import pick from "lodash/pick";
|
|
517
|
+
|
|
518
|
+
export default async function LocaleLayout({
|
|
519
|
+
children,
|
|
520
|
+
params,
|
|
521
|
+
}: {
|
|
522
|
+
children: React.ReactNode;
|
|
523
|
+
params: { locale: string };
|
|
524
|
+
}) {
|
|
525
|
+
const { locale } = params;
|
|
526
|
+
|
|
527
|
+
// このサーバーレンダリング(RSC)用にアクティブなリクエストロケールを設定します
|
|
528
|
+
unstable_setRequestLocale(locale);
|
|
529
|
+
|
|
530
|
+
// メッセージは src/i18n/request.ts 経由でサーバー側で読み込まれます
|
|
531
|
+
// (next-intl のドキュメント参照)。ここではクライアントコンポーネントに必要な
|
|
532
|
+
// サブセットのみをクライアントに渡します(ペイロード最適化)。
|
|
533
|
+
const messages = await getMessages();
|
|
534
|
+
const clientMessages = pick(messages, ["common", "about"]);
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<html lang={locale}>
|
|
538
|
+
<body>
|
|
539
|
+
<NextIntlClientProvider locale={locale} messages={clientMessages}>
|
|
540
|
+
{children}
|
|
541
|
+
</NextIntlClientProvider>
|
|
542
|
+
</body>
|
|
543
|
+
</html>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
549
|
+
import { getTranslations } from "next-intl/server";
|
|
550
|
+
import { ClientComponent, ServerComponent } from "@components";
|
|
551
|
+
|
|
552
|
+
export default async function LandingPage({
|
|
553
|
+
params,
|
|
554
|
+
}: {
|
|
555
|
+
params: { locale: string };
|
|
556
|
+
}) {
|
|
557
|
+
// 完全にサーバー側での読み込み(クライアント側でのハイドレーションなし)
|
|
558
|
+
const t = await getTranslations("about");
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
<main>
|
|
562
|
+
<h1>{t("title")}</h1>
|
|
563
|
+
<ClientComponent />
|
|
564
|
+
<ServerComponent />
|
|
565
|
+
</main>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
</TabItem>
|
|
571
|
+
<TabItem label="intlayer" value="intlayer">
|
|
572
|
+
|
|
573
|
+
```tsx fileName="intlayer.config.ts"
|
|
574
|
+
export default {
|
|
575
|
+
internationalization: {
|
|
576
|
+
locales: ["en", "fr", "es"],
|
|
577
|
+
defaultLocale: "en",
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
583
|
+
import { getHTMLTextDir } from "intlayer";
|
|
584
|
+
import {
|
|
585
|
+
IntlayerClientProvider,
|
|
586
|
+
generateStaticParams,
|
|
587
|
+
type NextLayoutIntlayer,
|
|
588
|
+
} from "next-intlayer";
|
|
589
|
+
|
|
590
|
+
export const dynamic = "force-static";
|
|
591
|
+
|
|
592
|
+
const LandingLayout: NextLayoutIntlayer = async ({ children, params }) => {
|
|
593
|
+
const { locale } = await params;
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<html lang={locale} dir={getHTMLTextDir(locale)}>
|
|
597
|
+
<IntlayerClientProvider locale={locale}>
|
|
598
|
+
{children}
|
|
599
|
+
</IntlayerClientProvider>
|
|
600
|
+
</html>
|
|
601
|
+
);
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
export default LandingLayout;
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
608
|
+
import { PageContent } from "@components/PageContent";
|
|
609
|
+
import type { NextPageIntlayer } from "next-intlayer";
|
|
610
|
+
import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";
|
|
611
|
+
import { ClientComponent, ServerComponent } from "@components";
|
|
612
|
+
|
|
613
|
+
const LandingPage: NextPageIntlayer = async ({ params }) => {
|
|
614
|
+
const { locale } = await params;
|
|
615
|
+
const { title } = useIntlayer("about", locale);
|
|
616
|
+
|
|
617
|
+
return (
|
|
618
|
+
<IntlayerServerProvider locale={locale}>
|
|
619
|
+
<main>
|
|
620
|
+
<h1>{title}</h1>
|
|
621
|
+
<ClientComponent />
|
|
622
|
+
<ServerComponent />
|
|
623
|
+
</main>
|
|
624
|
+
</IntlayerServerProvider>
|
|
625
|
+
);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
export default LandingPage;
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
</TabItem>
|
|
632
|
+
</Tab>
|
|
633
|
+
|
|
634
|
+
#### 比較
|
|
635
|
+
|
|
636
|
+
3つすべてがロケールごとのコンテンツ読み込みとプロバイダーをサポートしています。
|
|
637
|
+
|
|
638
|
+
- **next-intl/next-i18next** では、通常、ルートごとに選択されたメッセージや名前空間を読み込み、必要な場所にプロバイダーを配置します。
|
|
639
|
+
|
|
640
|
+
- **Intlayer** では、ビルド時の解析を追加して使用状況を推測し、手動の配線を減らし、単一のルートプロバイダーを許可する場合があります。
|
|
641
|
+
|
|
642
|
+
チームの好みに応じて、明示的な制御と自動化のどちらかを選択してください。
|
|
643
|
+
|
|
644
|
+
### クライアントコンポーネントでの使用例
|
|
645
|
+
|
|
646
|
+
カウンターをレンダリングするクライアントコンポーネントの例を見てみましょう。
|
|
647
|
+
|
|
648
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
649
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
650
|
+
|
|
651
|
+
**翻訳(`public/locales/...` に実際のJSONファイルが必要です)**
|
|
652
|
+
|
|
653
|
+
```json fileName="public/locales/en/about.json"
|
|
654
|
+
{
|
|
655
|
+
"counter": {
|
|
656
|
+
"label": "Counter",
|
|
657
|
+
"increment": "Increment"
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
```json fileName="public/locales/fr/about.json"
|
|
663
|
+
{
|
|
664
|
+
"counter": {
|
|
665
|
+
"label": "Compteur",
|
|
666
|
+
"increment": "Incrémenter"
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**クライアントコンポーネント**
|
|
672
|
+
|
|
673
|
+
```tsx fileName="src/components/ClientComponentExample.tsx"
|
|
674
|
+
"use client";
|
|
675
|
+
|
|
676
|
+
import React, { useMemo, useState } from "react";
|
|
677
|
+
import { useTranslation } from "next-i18next";
|
|
678
|
+
|
|
679
|
+
const ClientComponentExample = () => {
|
|
680
|
+
const { t, i18n } = useTranslation("about");
|
|
681
|
+
const [count, setCount] = useState(0);
|
|
682
|
+
|
|
683
|
+
// next-i18nextはuseNumberを公開していないため、Intl.NumberFormatを使用
|
|
684
|
+
const numberFormat = new Intl.NumberFormat(i18n.language);
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
<div>
|
|
688
|
+
<p>{numberFormat.format(count)}</p>
|
|
689
|
+
<button
|
|
690
|
+
aria-label={t("counter.label")}
|
|
691
|
+
onClick={() => setCount((count) => count + 1)}
|
|
692
|
+
>
|
|
693
|
+
{t("counter.increment")}
|
|
694
|
+
</button>
|
|
695
|
+
</div>
|
|
696
|
+
);
|
|
697
|
+
};
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
> ページの serverSideTranslations に "about" ネームスペースを追加するのを忘れないでください
|
|
701
|
+
> ここでは React 19.x.x のバージョンを使用していますが、より低いバージョンの場合は、重い関数であるためフォーマッターのインスタンスを保持するために useMemo を使用する必要があります
|
|
702
|
+
|
|
703
|
+
</TabItem>
|
|
704
|
+
<TabItem label="next-intl" value="next-intl">
|
|
705
|
+
|
|
706
|
+
**翻訳(形状は再利用可能;お好みで next-intl のメッセージにロードしてください)**
|
|
707
|
+
|
|
708
|
+
```json fileName="locales/en/about.json"
|
|
709
|
+
{
|
|
710
|
+
"counter": {
|
|
711
|
+
"label": "Counter",
|
|
712
|
+
"increment": "Increment"
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
```json fileName="locales/fr/about.json"
|
|
718
|
+
{
|
|
719
|
+
"counter": {
|
|
720
|
+
"label": "Compteur",
|
|
721
|
+
"increment": "Incrémenter"
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
**クライアントコンポーネント**
|
|
727
|
+
|
|
728
|
+
```tsx fileName="src/components/ClientComponentExample.tsx"
|
|
729
|
+
"use client";
|
|
730
|
+
|
|
731
|
+
import React, { useState } from "react";
|
|
732
|
+
import { useTranslations, useFormatter } from "next-intl";
|
|
733
|
+
|
|
734
|
+
const ClientComponentExample = () => {
|
|
735
|
+
// ネストされたオブジェクトに直接スコープを設定
|
|
736
|
+
const t = useTranslations("about.counter");
|
|
737
|
+
const format = useFormatter();
|
|
738
|
+
const [count, setCount] = useState(0);
|
|
739
|
+
|
|
740
|
+
return (
|
|
741
|
+
<div>
|
|
742
|
+
<p>{format.number(count)}</p>
|
|
743
|
+
<button
|
|
744
|
+
aria-label={t("label")}
|
|
745
|
+
onClick={() => setCount((count) => count + 1)}
|
|
746
|
+
>
|
|
747
|
+
{t("increment")}
|
|
748
|
+
</button>
|
|
749
|
+
</div>
|
|
750
|
+
);
|
|
751
|
+
};
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
> ページのクライアントメッセージに "about" メッセージを追加するのを忘れないでください
|
|
755
|
+
|
|
756
|
+
</TabItem>
|
|
757
|
+
<TabItem label="intlayer" value="intlayer">
|
|
758
|
+
|
|
759
|
+
**コンテンツ**
|
|
760
|
+
|
|
761
|
+
```ts fileName="src/components/ClientComponentExample/index.content.ts"
|
|
762
|
+
import { t, type Dictionary } from "intlayer";
|
|
763
|
+
|
|
764
|
+
const counterContent = {
|
|
765
|
+
key: "counter",
|
|
766
|
+
content: {
|
|
767
|
+
label: t({ ja: "カウンター", en: "Counter", fr: "Compteur" }),
|
|
768
|
+
increment: t({ ja: "インクリメント", en: "Increment", fr: "Incrémenter" }),
|
|
769
|
+
},
|
|
770
|
+
} satisfies Dictionary;
|
|
771
|
+
|
|
772
|
+
export default counterContent;
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**クライアントコンポーネント**
|
|
776
|
+
|
|
777
|
+
```tsx fileName="src/components/ClientComponentExample/index.tsx"
|
|
778
|
+
"use client";
|
|
779
|
+
|
|
780
|
+
import React, { useState } from "react";
|
|
781
|
+
import { useNumber, useIntlayer } from "next-intlayer";
|
|
782
|
+
|
|
783
|
+
const ClientComponentExample = () => {
|
|
784
|
+
const [count, setCount] = useState(0);
|
|
785
|
+
const { label, increment } = useIntlayer("counter"); // 文字列を返す
|
|
786
|
+
const { number } = useNumber();
|
|
787
|
+
|
|
788
|
+
return (
|
|
789
|
+
<div>
|
|
790
|
+
<p>{number(count)}</p>
|
|
791
|
+
<button aria-label={label} onClick={() => setCount((count) => count + 1)}>
|
|
792
|
+
{increment}
|
|
793
|
+
</button>
|
|
794
|
+
</div>
|
|
795
|
+
);
|
|
796
|
+
};
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
</TabItem>
|
|
800
|
+
</Tab>
|
|
801
|
+
|
|
802
|
+
#### 比較
|
|
803
|
+
|
|
804
|
+
- **数値フォーマット**
|
|
805
|
+
- **next-i18next**: `useNumber` はなく、`Intl.NumberFormat`(または i18next-icu)を使用。
|
|
806
|
+
- **next-intl**: `useFormatter().number(value)` を使用。
|
|
807
|
+
- **Intlayer**: 組み込みの `useNumber()` を使用。
|
|
808
|
+
|
|
809
|
+
- **キー**
|
|
810
|
+
- ネストされた構造(`about.counter.label`)を維持し、フックのスコープを適切に設定する(`useTranslation("about")` + `t("counter.label")` または `useTranslations("about.counter")` + `t("label")`)。
|
|
811
|
+
|
|
812
|
+
- **ファイルの場所**
|
|
813
|
+
- **next-i18next** は `public/locales/{lng}/{ns}.json` に JSON を期待。
|
|
814
|
+
- **next-intl** は柔軟で、設定に応じてメッセージをロード可能。
|
|
815
|
+
- **Intlayer** は TS/JS の辞書にコンテンツを格納し、キーで解決。
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
### サーバーコンポーネントでの使用
|
|
820
|
+
|
|
821
|
+
UIコンポーネントの場合を考えます。このコンポーネントはサーバーコンポーネントであり、クライアントコンポーネントの子として挿入できる必要があります。(ページ(サーバーコンポーネント) -> クライアントコンポーネント -> サーバーコンポーネント)。このコンポーネントはクライアントコンポーネントの子として挿入できるため、非同期にはできません。
|
|
822
|
+
|
|
823
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
824
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
825
|
+
|
|
826
|
+
```tsx fileName="src/pages/about.tsx"
|
|
827
|
+
import type { GetStaticProps } from "next";
|
|
828
|
+
import { useTranslation } from "next-i18next";
|
|
829
|
+
|
|
830
|
+
type ServerComponentProps = {
|
|
831
|
+
count: number;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
835
|
+
const { t, i18n } = useTranslation("about");
|
|
836
|
+
const formatted = new Intl.NumberFormat(i18n.language).format(count);
|
|
837
|
+
|
|
838
|
+
return (
|
|
839
|
+
<div>
|
|
840
|
+
<p>{formatted}</p>
|
|
841
|
+
<button aria-label={t("counter.label")}>{t("counter.increment")}</button>
|
|
842
|
+
</div>
|
|
843
|
+
);
|
|
844
|
+
};
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
</TabItem>
|
|
848
|
+
<TabItem label="next-intl" value="next-intl">
|
|
849
|
+
|
|
850
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
851
|
+
type ServerComponentProps = {
|
|
852
|
+
count: number;
|
|
853
|
+
t: (key: string) => string;
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const ServerComponent = ({ t, count }: ServerComponentProps) => {
|
|
857
|
+
const formatted = new Intl.NumberFormat(i18n.language).format(count);
|
|
858
|
+
|
|
859
|
+
return (
|
|
860
|
+
<div>
|
|
861
|
+
<p>{formatted}</p>
|
|
862
|
+
<button aria-label={t("label")}>{t("increment")}</button>
|
|
863
|
+
</div>
|
|
864
|
+
);
|
|
865
|
+
};
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
> サーバーコンポーネントは非同期にできないため、翻訳関数とフォーマッター関数をプロパティとして渡す必要があります。
|
|
869
|
+
>
|
|
870
|
+
> - `const t = await getTranslations("about.counter");`
|
|
871
|
+
> - `const format = await getFormatter();`
|
|
872
|
+
|
|
873
|
+
</TabItem>
|
|
874
|
+
<TabItem label="intlayer" value="intlayer">
|
|
875
|
+
|
|
876
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
877
|
+
import { useIntlayer, useNumber } from "next-intlayer/server";
|
|
878
|
+
|
|
879
|
+
const ServerComponent = ({ count }: { count: number }) => {
|
|
880
|
+
const { label, increment } = useIntlayer("counter");
|
|
881
|
+
const { number } = useNumber();
|
|
882
|
+
|
|
883
|
+
return (
|
|
884
|
+
<div>
|
|
885
|
+
<p>{number(count)}</p>
|
|
886
|
+
<button aria-label={label}>{increment}</button>
|
|
887
|
+
</div>
|
|
888
|
+
);
|
|
889
|
+
};
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
</TabItem>
|
|
893
|
+
</Tab>
|
|
894
|
+
|
|
895
|
+
> Intlayerは`next-intlayer/server`を通じて**サーバーセーフ**なフックを提供します。動作するために、`useIntlayer`と`useNumber`はクライアントフックに似たフックのような構文を使用しますが、内部的にはサーバーコンテキスト(`IntlayerServerProvider`)に依存しています。
|
|
896
|
+
|
|
897
|
+
### メタデータ / サイトマップ / ロボット
|
|
898
|
+
|
|
899
|
+
コンテンツの翻訳は素晴らしいことです。しかし、多くの人は国際化の主な目的があなたのウェブサイトを世界により見えるようにすることだということを忘れがちです。I18nはあなたのウェブサイトの可視性を向上させるための非常に強力な手段です。
|
|
900
|
+
|
|
901
|
+
以下は多言語SEOに関するベストプラクティスのリストです。
|
|
902
|
+
|
|
903
|
+
- `<head>`タグ内にhreflangメタタグを設定する
|
|
904
|
+
> これは検索エンジンがページで利用可能な言語を理解するのに役立ちます
|
|
905
|
+
- sitemap.xml にすべてのページの翻訳を `http://www.w3.org/1999/xhtml` XML スキーマを使ってリストアップする
|
|
906
|
+
>
|
|
907
|
+
- robots.txt からプレフィックス付きページを除外するのを忘れない(例:`/dashboard`、および `/fr/dashboard`、`/es/dashboard`)
|
|
908
|
+
>
|
|
909
|
+
- カスタム Link コンポーネントを使って最もローカライズされたページへリダイレクトする(例:フランス語では `<a href="/fr/about">A propos</a>`)
|
|
910
|
+
>
|
|
911
|
+
|
|
912
|
+
開発者はしばしばロケール間でページを適切に参照することを忘れがちです。
|
|
913
|
+
|
|
914
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
915
|
+
|
|
916
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
917
|
+
|
|
918
|
+
```ts fileName="i18n.config.ts"
|
|
919
|
+
export const locales = ["en", "fr"] as const;
|
|
920
|
+
export type Locale = (typeof locales)[number];
|
|
921
|
+
export const defaultLocale: Locale = "en";
|
|
922
|
+
|
|
923
|
+
export function localizedPath(locale: string, path: string) {
|
|
924
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const ORIGIN = "https://example.com";
|
|
928
|
+
export function abs(locale: string, path: string) {
|
|
929
|
+
return ORIGIN + localizedPath(locale, path);
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
934
|
+
import type { Metadata } from "next";
|
|
935
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
936
|
+
|
|
937
|
+
export async function generateMetadata({
|
|
938
|
+
params,
|
|
939
|
+
}: {
|
|
940
|
+
params: { locale: string };
|
|
941
|
+
}): Promise<Metadata> {
|
|
942
|
+
const { locale } = params;
|
|
943
|
+
|
|
944
|
+
// 正しいJSONファイルを動的にインポートする
|
|
945
|
+
const messages = (
|
|
946
|
+
await import("@/../public/locales/" + locale + "/about.json")
|
|
947
|
+
).default;
|
|
948
|
+
|
|
949
|
+
const languages = Object.fromEntries(
|
|
950
|
+
locales.map((locale) => [locale, localizedPath(locale, "/about")])
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
return {
|
|
954
|
+
title: messages.title,
|
|
955
|
+
description: messages.description,
|
|
956
|
+
alternates: {
|
|
957
|
+
canonical: localizedPath(locale, "/about"),
|
|
958
|
+
languages: { ...languages, "x-default": "/about" },
|
|
959
|
+
},
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export default async function AboutPage() {
|
|
964
|
+
return <h1>概要</h1>; // 「About」の日本語訳
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
```ts fileName="src/app/sitemap.ts"
|
|
969
|
+
import type { MetadataRoute } from "next";
|
|
970
|
+
import { locales, defaultLocale, abs } from "@/i18n.config";
|
|
971
|
+
|
|
972
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
973
|
+
const languages = Object.fromEntries(
|
|
974
|
+
locales.map((locale) => [locale, abs(locale, "/about")])
|
|
975
|
+
);
|
|
976
|
+
return [
|
|
977
|
+
{
|
|
978
|
+
url: abs(defaultLocale, "/about"),
|
|
979
|
+
lastModified: new Date(),
|
|
980
|
+
changeFrequency: "monthly", // 更新頻度:毎月
|
|
981
|
+
priority: 0.7, // 優先度
|
|
982
|
+
alternates: { languages },
|
|
983
|
+
},
|
|
984
|
+
];
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
```ts fileName="src/app/robots.ts"
|
|
989
|
+
import type { MetadataRoute } from "next";
|
|
990
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
991
|
+
|
|
992
|
+
const ORIGIN = "https://example.com";
|
|
993
|
+
|
|
994
|
+
const expandAllLocales = (path: string) => [
|
|
995
|
+
localizedPath(defaultLocale, path),
|
|
996
|
+
...locales
|
|
997
|
+
.filter((locale) => locale !== defaultLocale)
|
|
998
|
+
.map((locale) => localizedPath(locale, path)),
|
|
999
|
+
];
|
|
1000
|
+
|
|
1001
|
+
export default function robots(): MetadataRoute.Robots {
|
|
1002
|
+
const disallow = [
|
|
1003
|
+
...expandAllLocales("/dashboard"),
|
|
1004
|
+
...expandAllLocales("/admin"),
|
|
1005
|
+
];
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
1009
|
+
host: ORIGIN,
|
|
1010
|
+
sitemap: ORIGIN + "/sitemap.xml",
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
</TabItem>
|
|
1016
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1017
|
+
|
|
1018
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
1019
|
+
import type { Metadata } from "next";
|
|
1020
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1021
|
+
import { getTranslations } from "next-intl/server";
|
|
1022
|
+
|
|
1023
|
+
function localizedPath(locale: string, path: string) {
|
|
1024
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export async function generateMetadata({
|
|
1028
|
+
params,
|
|
1029
|
+
}: {
|
|
1030
|
+
params: { locale: string };
|
|
1031
|
+
}): Promise<Metadata> {
|
|
1032
|
+
const { locale } = params;
|
|
1033
|
+
const t = await getTranslations({ locale, namespace: "about" });
|
|
1034
|
+
|
|
1035
|
+
const url = "/about";
|
|
1036
|
+
const languages = Object.fromEntries(
|
|
1037
|
+
locales.map((locale) => [locale, localizedPath(locale, url)])
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
return {
|
|
1041
|
+
title: t("title"),
|
|
1042
|
+
description: t("description"),
|
|
1043
|
+
alternates: {
|
|
1044
|
+
canonical: localizedPath(locale, url),
|
|
1045
|
+
languages: { ...languages, "x-default": url },
|
|
1046
|
+
},
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ... ページの残りのコード
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
```tsx fileName="src/app/sitemap.ts"
|
|
1054
|
+
import type { MetadataRoute } from "next";
|
|
1055
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1056
|
+
|
|
1057
|
+
const origin = "https://example.com";
|
|
1058
|
+
|
|
1059
|
+
const formatterLocalizedPath = (locale: string, path: string) =>
|
|
1060
|
+
locale === defaultLocale ? origin + path : origin + "/" + locale + path;
|
|
1061
|
+
|
|
1062
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
1063
|
+
const aboutLanguages = Object.fromEntries(
|
|
1064
|
+
locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
|
|
1065
|
+
);
|
|
1066
|
+
|
|
1067
|
+
return [
|
|
1068
|
+
{
|
|
1069
|
+
url: formatterLocalizedPath(defaultLocale, "/about"),
|
|
1070
|
+
lastModified: new Date(),
|
|
1071
|
+
changeFrequency: "monthly",
|
|
1072
|
+
priority: 0.7,
|
|
1073
|
+
alternates: { languages: aboutLanguages },
|
|
1074
|
+
},
|
|
1075
|
+
];
|
|
1076
|
+
}
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
```tsx fileName="src/app/robots.ts"
|
|
1080
|
+
import type { MetadataRoute } from "next";
|
|
1081
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1082
|
+
|
|
1083
|
+
const origin = "https://example.com";
|
|
1084
|
+
const withAllLocales = (path: string) => [
|
|
1085
|
+
path,
|
|
1086
|
+
...locales
|
|
1087
|
+
.filter((locale) => locale !== defaultLocale)
|
|
1088
|
+
.map((locale) => "/" + locale + path),
|
|
1089
|
+
];
|
|
1090
|
+
|
|
1091
|
+
export default function robots(): MetadataRoute.Robots {
|
|
1092
|
+
const disallow = [
|
|
1093
|
+
...withAllLocales("/dashboard"),
|
|
1094
|
+
...withAllLocales("/admin"),
|
|
1095
|
+
];
|
|
1096
|
+
|
|
1097
|
+
return {
|
|
1098
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
1099
|
+
host: origin,
|
|
1100
|
+
sitemap: origin + "/sitemap.xml",
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
</TabItem>
|
|
1106
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1107
|
+
|
|
1108
|
+
```typescript fileName="src/app/[locale]/about/layout.tsx"
|
|
1109
|
+
import { getIntlayer, getMultilingualUrls } from "intlayer";
|
|
1110
|
+
import type { Metadata } from "next";
|
|
1111
|
+
import type { LocalPromiseParams } from "next-intlayer";
|
|
1112
|
+
|
|
1113
|
+
export const generateMetadata = async ({
|
|
1114
|
+
params,
|
|
1115
|
+
}: LocalPromiseParams): Promise<Metadata> => {
|
|
1116
|
+
const { locale } = await params;
|
|
1117
|
+
|
|
1118
|
+
const metadata = getIntlayer("page-metadata", locale);
|
|
1119
|
+
|
|
1120
|
+
const multilingualUrls = getMultilingualUrls("/about");
|
|
1121
|
+
|
|
1122
|
+
return {
|
|
1123
|
+
...metadata,
|
|
1124
|
+
alternates: {
|
|
1125
|
+
canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
|
|
1126
|
+
languages: { ...multilingualUrls, "x-default": "/about" },
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// ... ページの残りのコード
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
```tsx fileName="src/app/sitemap.ts"
|
|
1135
|
+
import { getMultilingualUrls } from "intlayer";
|
|
1136
|
+
import type { MetadataRoute } from "next";
|
|
1137
|
+
|
|
1138
|
+
const sitemap = (): MetadataRoute.Sitemap => [
|
|
1139
|
+
{
|
|
1140
|
+
url: "https://example.com/about",
|
|
1141
|
+
alternates: {
|
|
1142
|
+
languages: { ...getMultilingualUrls("https://example.com/about") },
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
];
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
```tsx fileName="src/app/robots.ts"
|
|
1149
|
+
import { getMultilingualUrls } from "intlayer";
|
|
1150
|
+
import type { MetadataRoute } from "next";
|
|
1151
|
+
|
|
1152
|
+
const getAllMultilingualUrls = (urls: string[]) =>
|
|
1153
|
+
urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
|
|
1154
|
+
|
|
1155
|
+
const robots = (): MetadataRoute.Robots => ({
|
|
1156
|
+
rules: {
|
|
1157
|
+
userAgent: "*",
|
|
1158
|
+
allow: ["/"],
|
|
1159
|
+
disallow: getAllMultilingualUrls(["/dashboard"]),
|
|
1160
|
+
},
|
|
1161
|
+
host: "https://example.com",
|
|
1162
|
+
sitemap: "https://example.com/sitemap.xml",
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
export default robots;
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
</TabItem>
|
|
1169
|
+
</Tab>
|
|
1170
|
+
|
|
1171
|
+
> Intlayerは、サイトマップ用の多言語URLを生成するための`getMultilingualUrls`関数を提供しています。
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1175
|
+
---
|
|
1176
|
+
|
|
1177
|
+
## そして勝者は…
|
|
1178
|
+
|
|
1179
|
+
簡単ではありません。各オプションにはトレードオフがあります。私の見解は以下の通りです:
|
|
1180
|
+
|
|
1181
|
+
<Columns>
|
|
1182
|
+
<Column>
|
|
1183
|
+
|
|
1184
|
+
**next-intl**
|
|
1185
|
+
|
|
1186
|
+
- 最もシンプルで軽量、強制される決定が少ないです。**最小限**のソリューションを求めていて、集中管理されたカタログに慣れており、アプリが**小規模から中規模**の場合に適しています。
|
|
1187
|
+
|
|
1188
|
+
</Column>
|
|
1189
|
+
<Column>
|
|
1190
|
+
|
|
1191
|
+
**next-i18next**
|
|
1192
|
+
|
|
1193
|
+
- 成熟しており、機能が豊富でコミュニティプラグインも多いですが、セットアップコストは高めです。**i18nextのプラグインエコシステム**(例:プラグイン経由の高度なICUルール)が必要で、チームがすでにi18nextを知っていて、柔軟性のために**より多くの設定**を受け入れられる場合に適しています。
|
|
1194
|
+
|
|
1195
|
+
</Column>
|
|
1196
|
+
<Column>
|
|
1197
|
+
|
|
1198
|
+
**Intlayer**
|
|
1199
|
+
|
|
1200
|
+
- モダンな Next.js 向けに構築されており、モジュラーコンテンツ、型安全性、ツール群、そしてボイラープレートの削減を実現しています。**コンポーネント単位のコンテンツ管理**、**厳格な TypeScript**、**ビルド時の保証**、**ツリーシェイキング**、そして**ルーティング/SEO/エディターツールがバッテリー込み**で提供されることを重視する場合、特に **Next.js App Router**、デザインシステム、そして**大規模でモジュラーなコードベース**に最適です。
|
|
1201
|
+
|
|
1202
|
+
</Column>
|
|
1203
|
+
</Columns>
|
|
1204
|
+
|
|
1205
|
+
セットアップを最小限に抑え、多少の手動設定を許容できるなら next-intl が良い選択です。すべての機能が必要で複雑さを気にしないなら next-i18next が適しています。しかし、モダンでスケーラブル、モジュラーなソリューションをビルトインツールと共に求めるなら、Intlayer はそれをすぐに提供することを目指しています。
|
|
1206
|
+
|
|
1207
|
+
> **エンタープライズチーム向けの代替案**: **Crowdin**、**Phrase**、またはその他のプロフェッショナルな翻訳管理システムのような確立されたローカリゼーションプラットフォームと完全に連携する、実績のあるソリューションが必要な場合は、成熟したエコシステムと実証済みの統合を持つ **next-intl** または **next-i18next** を検討してください。
|
|
1208
|
+
|
|
1209
|
+
> **今後のロードマップ**: Intlayer は、**i18next** および **next-intl** ソリューションの上に動作するプラグインの開発も計画しています。これにより、Intlayer の自動化、構文、およびコンテンツ管理の利点を享受しつつ、これらの確立されたソリューションがアプリケーションコードに提供するセキュリティと安定性を維持できます。
|
|
1210
|
+
|
|
1211
|
+
## GitHub STARs
|
|
1212
|
+
|
|
1213
|
+
GitHubのスターは、プロジェクトの人気、コミュニティの信頼、そして長期的な関連性を示す強力な指標です。技術的な品質の直接的な尺度ではありませんが、どれだけ多くの開発者がそのプロジェクトを有用と感じ、進捗を追い、採用する可能性が高いかを反映しています。プロジェクトの価値を評価する際、スターは代替案間のトラクションを比較し、エコシステムの成長に関する洞察を提供するのに役立ちます。
|
|
146
1214
|
|
|
147
|
-
-
|
|
148
|
-
- **旧カタログを並行して維持**:移行中の橋渡しとして使用し、一斉移行は避けます。
|
|
149
|
-
- **厳密なチェックを有効化**:ビルド時の検出で早期にギャップを明らかにします。
|
|
150
|
-
- **ミドルウェアとヘルパーを採用**:サイト全体でロケール検出とSEOタグを標準化します。
|
|
151
|
-
- **バンドルサイズを測定**:未使用のコンテンツが削除されるため、**バンドルサイズの削減**が期待できます。
|
|
1215
|
+
[](https://www.star-history.com/#i18next/next-i18next&amannn/next-intl&aymericzip/intlayer)
|
|
152
1216
|
|
|
153
1217
|
---
|
|
154
1218
|
|
|
155
1219
|
## 結論
|
|
156
1220
|
|
|
157
|
-
3つのライブラリはすべてコアなローカリゼーションに成功しています。違いは、**モダンな Next.js
|
|
1221
|
+
3つのライブラリはすべてコアなローカリゼーションに成功しています。違いは、**モダンな Next.js** で堅牢でスケーラブルなセットアップを実現するために、**どれだけの作業が必要か**という点です。
|
|
158
1222
|
|
|
159
|
-
- **Intlayer
|
|
160
|
-
-
|
|
1223
|
+
- **Intlayer** では、**モジュラーコンテンツ**、**厳格なTS**、**ビルド時の安全性**、**ツリーシェイクされたバンドル**、および **一流のApp Router + SEOツール** が **デフォルト** であり、手間ではありません。
|
|
1224
|
+
- チームが多言語対応のコンポーネント駆動型アプリにおいて、**保守性と速度**を重視するなら、Intlayerは今日最も**完全な**体験を提供します。
|
|
161
1225
|
|
|
162
|
-
詳細は[
|
|
1226
|
+
詳細は ['Why Intlayer?' ドキュメント](https://intlayer.org/doc/why) を参照してください。
|