@intlayer/docs 7.5.12 → 7.5.14
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/per-component_vs_centralized_i18n.md +248 -0
- package/blog/de/per-component_vs_centralized_i18n.md +248 -0
- package/blog/en/_per-component_vs_centralized_i18n.md +252 -0
- package/blog/en/per-component_vs_centralized_i18n.md +248 -0
- package/blog/en-GB/per-component_vs_centralized_i18n.md +247 -0
- package/blog/es/per-component_vs_centralized_i18n.md +245 -0
- package/blog/fr/per-component_vs_centralized_i18n.md +245 -0
- package/blog/hi/per-component_vs_centralized_i18n.md +249 -0
- package/blog/id/per-component_vs_centralized_i18n.md +248 -0
- package/blog/it/per-component_vs_centralized_i18n.md +247 -0
- package/blog/ja/per-component_vs_centralized_i18n.md +247 -0
- package/blog/ko/per-component_vs_centralized_i18n.md +246 -0
- package/blog/pl/per-component_vs_centralized_i18n.md +247 -0
- package/blog/pt/per-component_vs_centralized_i18n.md +246 -0
- package/blog/ru/per-component_vs_centralized_i18n.md +251 -0
- package/blog/tr/per-component_vs_centralized_i18n.md +244 -0
- package/blog/uk/compiler_vs_declarative_i18n.md +224 -0
- package/blog/uk/i18n_using_next-i18next.md +1086 -0
- package/blog/uk/i18n_using_next-intl.md +760 -0
- package/blog/uk/index.md +69 -0
- package/blog/uk/internationalization_and_SEO.md +273 -0
- package/blog/uk/intlayer_with_i18next.md +211 -0
- package/blog/uk/intlayer_with_next-i18next.md +202 -0
- package/blog/uk/intlayer_with_next-intl.md +203 -0
- package/blog/uk/intlayer_with_react-i18next.md +200 -0
- package/blog/uk/intlayer_with_react-intl.md +202 -0
- package/blog/uk/intlayer_with_vue-i18n.md +206 -0
- package/blog/uk/l10n_platform_alternative/Lokalise.md +80 -0
- package/blog/uk/l10n_platform_alternative/crowdin.md +80 -0
- package/blog/uk/l10n_platform_alternative/phrase.md +78 -0
- package/blog/uk/list_i18n_technologies/CMS/drupal.md +143 -0
- package/blog/uk/list_i18n_technologies/CMS/wix.md +167 -0
- package/blog/uk/list_i18n_technologies/CMS/wordpress.md +189 -0
- package/blog/uk/list_i18n_technologies/frameworks/angular.md +125 -0
- package/blog/uk/list_i18n_technologies/frameworks/flutter.md +128 -0
- package/blog/uk/list_i18n_technologies/frameworks/react-native.md +217 -0
- package/blog/uk/list_i18n_technologies/frameworks/react.md +155 -0
- package/blog/uk/list_i18n_technologies/frameworks/svelte.md +145 -0
- package/blog/uk/list_i18n_technologies/frameworks/vue.md +144 -0
- package/blog/uk/next-i18next_vs_next-intl_vs_intlayer.md +1499 -0
- package/blog/uk/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/uk/per-component_vs_centralized_i18n.md +248 -0
- package/blog/uk/rag_powered_documentation_assistant.md +288 -0
- package/blog/uk/react-i18next_vs_react-intl_vs_intlayer.md +164 -0
- package/blog/uk/vue-i18n_vs_intlayer.md +279 -0
- package/blog/uk/what_is_internationalization.md +167 -0
- package/blog/vi/per-component_vs_centralized_i18n.md +246 -0
- package/blog/zh/per-component_vs_centralized_i18n.md +248 -0
- package/dist/cjs/common.cjs.map +1 -1
- package/dist/cjs/generated/blog.entry.cjs +20 -0
- package/dist/cjs/generated/blog.entry.cjs.map +1 -1
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/cjs/generated/frequentQuestions.entry.cjs +20 -0
- package/dist/cjs/generated/frequentQuestions.entry.cjs.map +1 -1
- package/dist/cjs/generated/legal.entry.cjs.map +1 -1
- package/dist/esm/common.mjs.map +1 -1
- package/dist/esm/generated/blog.entry.mjs +20 -0
- package/dist/esm/generated/blog.entry.mjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/esm/generated/frequentQuestions.entry.mjs +20 -0
- package/dist/esm/generated/frequentQuestions.entry.mjs.map +1 -1
- package/dist/esm/generated/legal.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/dist/types/generated/frequentQuestions.entry.d.ts +1 -0
- package/dist/types/generated/frequentQuestions.entry.d.ts.map +1 -1
- package/docs/ar/configuration.md +6 -1
- package/docs/ar/dictionary/content_file.md +6 -1
- package/docs/de/configuration.md +6 -1
- package/docs/de/dictionary/content_file.md +6 -1
- package/docs/en/configuration.md +6 -1
- package/docs/en/dictionary/content_file.md +6 -1
- package/docs/en-GB/configuration.md +6 -1
- package/docs/en-GB/dictionary/content_file.md +3 -1
- package/docs/es/configuration.md +6 -1
- package/docs/es/dictionary/content_file.md +6 -1
- package/docs/fr/configuration.md +6 -1
- package/docs/fr/dictionary/content_file.md +3 -1
- package/docs/hi/configuration.md +6 -1
- package/docs/hi/dictionary/content_file.md +3 -1
- package/docs/id/configuration.md +6 -1
- package/docs/id/dictionary/content_file.md +3 -1
- package/docs/it/configuration.md +6 -1
- package/docs/it/dictionary/content_file.md +3 -1
- package/docs/ja/configuration.md +6 -1
- package/docs/ja/dictionary/content_file.md +3 -1
- package/docs/ko/configuration.md +6 -1
- package/docs/ko/dictionary/content_file.md +3 -1
- package/docs/pl/configuration.md +3 -1
- package/docs/pl/dictionary/content_file.md +3 -1
- package/docs/pt/configuration.md +6 -1
- package/docs/pt/dictionary/content_file.md +3 -1
- package/docs/ru/configuration.md +6 -1
- package/docs/ru/dictionary/content_file.md +6 -1
- package/docs/tr/configuration.md +6 -1
- package/docs/tr/dictionary/content_file.md +3 -1
- package/docs/uk/CI_CD.md +198 -0
- package/docs/uk/autoFill.md +307 -0
- package/docs/uk/bundle_optimization.md +185 -0
- package/docs/uk/cli/build.md +64 -0
- package/docs/uk/cli/ci.md +137 -0
- package/docs/uk/cli/configuration.md +63 -0
- package/docs/uk/cli/debug.md +46 -0
- package/docs/uk/cli/doc-review.md +43 -0
- package/docs/uk/cli/doc-translate.md +132 -0
- package/docs/uk/cli/editor.md +28 -0
- package/docs/uk/cli/fill.md +130 -0
- package/docs/uk/cli/index.md +190 -0
- package/docs/uk/cli/init.md +84 -0
- package/docs/uk/cli/list.md +90 -0
- package/docs/uk/cli/list_projects.md +128 -0
- package/docs/uk/cli/live.md +41 -0
- package/docs/uk/cli/login.md +157 -0
- package/docs/uk/cli/pull.md +78 -0
- package/docs/uk/cli/push.md +98 -0
- package/docs/uk/cli/sdk.md +71 -0
- package/docs/uk/cli/test.md +76 -0
- package/docs/uk/cli/transform.md +65 -0
- package/docs/uk/cli/version.md +24 -0
- package/docs/uk/cli/watch.md +37 -0
- package/docs/uk/configuration.md +742 -0
- package/docs/uk/dictionary/condition.md +237 -0
- package/docs/uk/dictionary/content_file.md +1134 -0
- package/docs/uk/dictionary/enumeration.md +245 -0
- package/docs/uk/dictionary/file.md +232 -0
- package/docs/uk/dictionary/function_fetching.md +212 -0
- package/docs/uk/dictionary/gender.md +273 -0
- package/docs/uk/dictionary/insertion.md +187 -0
- package/docs/uk/dictionary/markdown.md +383 -0
- package/docs/uk/dictionary/nesting.md +273 -0
- package/docs/uk/dictionary/translation.md +332 -0
- package/docs/uk/formatters.md +595 -0
- package/docs/uk/how_works_intlayer.md +256 -0
- package/docs/uk/index.md +175 -0
- package/docs/uk/interest_of_intlayer.md +297 -0
- package/docs/uk/intlayer_CMS.md +569 -0
- package/docs/uk/intlayer_visual_editor.md +292 -0
- package/docs/uk/intlayer_with_angular.md +710 -0
- package/docs/uk/intlayer_with_astro.md +256 -0
- package/docs/uk/intlayer_with_create_react_app.md +1258 -0
- package/docs/uk/intlayer_with_express.md +429 -0
- package/docs/uk/intlayer_with_fastify.md +446 -0
- package/docs/uk/intlayer_with_lynx+react.md +548 -0
- package/docs/uk/intlayer_with_nestjs.md +283 -0
- package/docs/uk/intlayer_with_next-i18next.md +640 -0
- package/docs/uk/intlayer_with_next-intl.md +456 -0
- package/docs/uk/intlayer_with_nextjs_page_router.md +1541 -0
- package/docs/uk/intlayer_with_nuxt.md +711 -0
- package/docs/uk/intlayer_with_react_router_v7.md +600 -0
- package/docs/uk/intlayer_with_react_router_v7_fs_routes.md +669 -0
- package/docs/uk/intlayer_with_svelte_kit.md +579 -0
- package/docs/uk/intlayer_with_tanstack.md +818 -0
- package/docs/uk/intlayer_with_vite+preact.md +1748 -0
- package/docs/uk/intlayer_with_vite+react.md +1449 -0
- package/docs/uk/intlayer_with_vite+solid.md +302 -0
- package/docs/uk/intlayer_with_vite+svelte.md +520 -0
- package/docs/uk/intlayer_with_vite+vue.md +1113 -0
- package/docs/uk/introduction.md +222 -0
- package/docs/uk/locale_mapper.md +242 -0
- package/docs/uk/mcp_server.md +211 -0
- package/docs/uk/packages/express-intlayer/t.md +465 -0
- package/docs/uk/packages/intlayer/getEnumeration.md +159 -0
- package/docs/uk/packages/intlayer/getHTMLTextDir.md +121 -0
- package/docs/uk/packages/intlayer/getLocaleLang.md +81 -0
- package/docs/uk/packages/intlayer/getLocaleName.md +135 -0
- package/docs/uk/packages/intlayer/getLocalizedUrl.md +338 -0
- package/docs/uk/packages/intlayer/getMultilingualUrls.md +359 -0
- package/docs/uk/packages/intlayer/getPathWithoutLocale.md +75 -0
- package/docs/uk/packages/intlayer/getPrefix.md +213 -0
- package/docs/uk/packages/intlayer/getTranslation.md +190 -0
- package/docs/uk/packages/intlayer/getTranslationContent.md +189 -0
- package/docs/uk/packages/next-intlayer/t.md +365 -0
- package/docs/uk/packages/next-intlayer/useDictionary.md +276 -0
- package/docs/uk/packages/next-intlayer/useIntlayer.md +263 -0
- package/docs/uk/packages/next-intlayer/useLocale.md +166 -0
- package/docs/uk/packages/react-intlayer/t.md +311 -0
- package/docs/uk/packages/react-intlayer/useDictionary.md +295 -0
- package/docs/uk/packages/react-intlayer/useI18n.md +250 -0
- package/docs/uk/packages/react-intlayer/useIntlayer.md +251 -0
- package/docs/uk/packages/react-intlayer/useLocale.md +210 -0
- package/docs/uk/per_locale_file.md +345 -0
- package/docs/uk/plugins/sync-json.md +398 -0
- package/docs/uk/readme.md +265 -0
- package/docs/uk/releases/v6.md +305 -0
- package/docs/uk/releases/v7.md +624 -0
- package/docs/uk/roadmap.md +346 -0
- package/docs/uk/testing.md +204 -0
- package/docs/vi/configuration.md +6 -1
- package/docs/vi/dictionary/content_file.md +6 -1
- package/docs/zh/configuration.md +6 -1
- package/docs/zh/dictionary/content_file.md +6 -1
- package/frequent_questions/ar/error-vite-env-only.md +77 -0
- package/frequent_questions/de/error-vite-env-only.md +77 -0
- package/frequent_questions/en/error-vite-env-only.md +77 -0
- package/frequent_questions/en-GB/error-vite-env-only.md +77 -0
- package/frequent_questions/es/error-vite-env-only.md +76 -0
- package/frequent_questions/fr/error-vite-env-only.md +77 -0
- package/frequent_questions/hi/error-vite-env-only.md +77 -0
- package/frequent_questions/id/error-vite-env-only.md +77 -0
- package/frequent_questions/it/error-vite-env-only.md +77 -0
- package/frequent_questions/ja/error-vite-env-only.md +77 -0
- package/frequent_questions/ko/error-vite-env-only.md +77 -0
- package/frequent_questions/pl/error-vite-env-only.md +77 -0
- package/frequent_questions/pt/error-vite-env-only.md +77 -0
- package/frequent_questions/ru/error-vite-env-only.md +77 -0
- package/frequent_questions/tr/error-vite-env-only.md +77 -0
- package/frequent_questions/uk/SSR_Next_no_[locale].md +104 -0
- package/frequent_questions/uk/array_as_content_declaration.md +72 -0
- package/frequent_questions/uk/build_dictionaries.md +58 -0
- package/frequent_questions/uk/build_error_CI_CD.md +74 -0
- package/frequent_questions/uk/bun_set_up.md +53 -0
- package/frequent_questions/uk/customized_locale_list.md +64 -0
- package/frequent_questions/uk/domain_routing.md +113 -0
- package/frequent_questions/uk/error-vite-env-only.md +77 -0
- package/frequent_questions/uk/esbuild_error.md +29 -0
- package/frequent_questions/uk/get_locale_cookie.md +142 -0
- package/frequent_questions/uk/intlayer_command_undefined.md +155 -0
- package/frequent_questions/uk/locale_incorect_in_url.md +73 -0
- package/frequent_questions/uk/package_version_error.md +181 -0
- package/frequent_questions/uk/static_rendering.md +44 -0
- package/frequent_questions/uk/translated_path_url.md +55 -0
- package/frequent_questions/uk/unknown_command.md +97 -0
- package/frequent_questions/vi/error-vite-env-only.md +77 -0
- package/frequent_questions/zh/error-vite-env-only.md +77 -0
- package/legal/uk/privacy_notice.md +83 -0
- package/legal/uk/terms_of_service.md +55 -0
- package/package.json +9 -9
- package/src/generated/blog.entry.ts +20 -0
- package/src/generated/frequentQuestions.entry.ts +20 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2025-10-05
|
|
3
|
+
updatedAt: 2025-10-05
|
|
4
|
+
title: Як інтернаціоналізувати ваш Next.js 15 за допомогою next-i18next – керівництво i18n 2026
|
|
5
|
+
description: Практичне керівництво, готове до продакшену, з інтернаціоналізації застосунку Next.js 15 (App Router) за допомогою i18next/next-i18next та покращення його за допомогою Intlayer.
|
|
6
|
+
keywords:
|
|
7
|
+
- Інтернаціоналізація
|
|
8
|
+
- Документація
|
|
9
|
+
- Intlayer
|
|
10
|
+
- Next.js 15
|
|
11
|
+
- next-i18next
|
|
12
|
+
- i18next
|
|
13
|
+
- JavaScript
|
|
14
|
+
- React
|
|
15
|
+
slugs:
|
|
16
|
+
- doc
|
|
17
|
+
- next-i18next
|
|
18
|
+
applicationTemplate: https://github.com/aymericzip/intlayer-next-i18next-template
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Як інтернаціоналізувати сайт на Next.js 15 з next-i18next та Intlayer | Internationalization (i18n)
|
|
22
|
+
|
|
23
|
+
### Для кого це керівництво
|
|
24
|
+
|
|
25
|
+
- **Junior**: Дотримуйтесь точних кроків та копіюйте блоки коду. Ви отримаєте робочий багатомовний додаток.
|
|
26
|
+
- **Mid-level**: Використовуйте чеклісти та позначки найкращих практик, щоб уникнути типових помилок.
|
|
27
|
+
- **Senior**: Перегляньте високорівневу структуру, розділи про SEO та автоматизацію; тут ви знайдете розумні налаштування за замовчуванням і точки розширення.
|
|
28
|
+
|
|
29
|
+
### Що ви створите
|
|
30
|
+
|
|
31
|
+
- Проєкт App Router з локалізованими маршрутами (наприклад, `/`, `/fr/...`)
|
|
32
|
+
- i18n-конфігурація з локалями, локаллю за замовчуванням та підтримкою RTL
|
|
33
|
+
- Ініціалізація i18n на сервері та клієнтський провайдер
|
|
34
|
+
- Переклади з неймспейсами, що завантажуються за потреби
|
|
35
|
+
- SEO з `hreflang`, локалізованим `sitemap` і `robots`
|
|
36
|
+
- Middleware для маршрутизації локалей
|
|
37
|
+
- Інтеграція Intlayer для автоматизації робочих процесів перекладу (тести, автозаповнення за допомогою AI, синхронізація JSON)
|
|
38
|
+
|
|
39
|
+
> Примітка: next-i18next побудований поверх i18next. Цей посібник використовує примітиви i18next, сумісні з next-i18next в App Router, одночасно зберігаючи архітектуру простою та готовою до продакшену.
|
|
40
|
+
> Для ширшого порівняння див. [next-i18next vs next-i18next vs Intlayer](https://github.com/aymericzip/intlayer/blob/main/docs/blog/uk/next-i18next_vs_next-i18next_vs_intlayer.md).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 1) Структура проєкту
|
|
45
|
+
|
|
46
|
+
Встановіть залежності next-i18next:
|
|
47
|
+
|
|
48
|
+
```bash packageManager="npm"
|
|
49
|
+
npm install next-i18next i18next react-i18next i18next-resources-to-backend
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```bash packageManager="pnpm"
|
|
53
|
+
pnpm add next-i18next i18next react-i18next i18next-resources-to-backend
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```bash packageManager="yarn"
|
|
57
|
+
yarn add next-i18next i18next react-i18next i18next-resources-to-backend
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash packageManager="bun"
|
|
61
|
+
bun add next-i18next i18next react-i18next i18next-resources-to-backend
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Почніть з чіткої структури. Тримайте повідомлення розділеними за locale та namespace.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
.
|
|
68
|
+
├── i18n.config.ts
|
|
69
|
+
└── src
|
|
70
|
+
├── locales
|
|
71
|
+
│ ├── en
|
|
72
|
+
│ │ ├── common.json
|
|
73
|
+
│ │ └── about.json
|
|
74
|
+
│ └── fr
|
|
75
|
+
│ ├── common.json
|
|
76
|
+
│ └── about.json
|
|
77
|
+
├── app
|
|
78
|
+
│ ├── i18n
|
|
79
|
+
│ │ └── server.ts
|
|
80
|
+
│ └── [locale]
|
|
81
|
+
│ ├── layout.tsx
|
|
82
|
+
│ └── about.tsx
|
|
83
|
+
└── components
|
|
84
|
+
├── I18nProvider.tsx
|
|
85
|
+
├── ClientComponent.tsx
|
|
86
|
+
└── ServerComponent.tsx
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Контрольний список (mid/senior):
|
|
90
|
+
|
|
91
|
+
- Майте по одному JSON-файлу на namespace для кожної locale
|
|
92
|
+
- Не централізуйте повідомлення надто; використовуйте невеликі простори імен (namespaces), орієнтовані на сторінку чи feature
|
|
93
|
+
- Уникайте імпорту всіх локалей одночасно; завантажуйте лише те, що потрібно
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 2) Встановлення залежностей
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pnpm add i18next react-i18next i18next-resources-to-backend
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Якщо ви плануєте використовувати API next-i18next або сумісність конфігурації, також додайте:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pnpm add next-i18next
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 3) Основна конфігурація i18n
|
|
112
|
+
|
|
113
|
+
Визначте локалі, локаль за замовчуванням, RTL та допоміжні функції для локалізованих шляхів/URL.
|
|
114
|
+
|
|
115
|
+
```ts fileName="i18n.config.ts"
|
|
116
|
+
export const locales = ["en", "fr"] as const;
|
|
117
|
+
export type Locale = (typeof locales)[number];
|
|
118
|
+
|
|
119
|
+
export const defaultLocale: Locale = "en";
|
|
120
|
+
|
|
121
|
+
export const rtlLocales = ["ar", "he", "fa", "ur"] as const;
|
|
122
|
+
export const isRtl = (locale: string) =>
|
|
123
|
+
(rtlLocales as readonly string[]).includes(locale);
|
|
124
|
+
|
|
125
|
+
export function localizedPath(locale: string, path: string) {
|
|
126
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ORIGIN = "https://example.com";
|
|
130
|
+
export function abs(locale: string, path: string) {
|
|
131
|
+
return ORIGIN + localizedPath(locale, path);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Примітка: Якщо ви використовуєте `next-i18next.config.js`, тримайте його узгодженим з `i18n.config.ts`, щоб уникнути розходжень.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 4) Ініціалізація i18n на сервері
|
|
140
|
+
|
|
141
|
+
Ініціалізуйте i18next на сервері з динамічним бекендом, який імпортує лише потрібні JSON для локалі та namespace.
|
|
142
|
+
|
|
143
|
+
```ts fileName="src/app/i18n/server.ts"
|
|
144
|
+
import { createInstance } from "i18next";
|
|
145
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
146
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
147
|
+
import { defaultLocale } from "@/i18n.config";
|
|
148
|
+
|
|
149
|
+
// Завантажити JSON-ресурси з src/locales/<locale>/<namespace>.json
|
|
150
|
+
const backend = resourcesToBackend(
|
|
151
|
+
(locale: string, namespace: string) =>
|
|
152
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
export async function initI18next(
|
|
156
|
+
locale: string,
|
|
157
|
+
namespaces: string[] = ["common"]
|
|
158
|
+
) {
|
|
159
|
+
const i18n = createInstance();
|
|
160
|
+
await i18n
|
|
161
|
+
.use(initReactI18next)
|
|
162
|
+
.use(backend)
|
|
163
|
+
.init({
|
|
164
|
+
lng: locale,
|
|
165
|
+
fallbackLng: defaultLocale,
|
|
166
|
+
ns: namespaces,
|
|
167
|
+
defaultNS: "common",
|
|
168
|
+
interpolation: { escapeValue: false },
|
|
169
|
+
react: { useSuspense: false },
|
|
170
|
+
});
|
|
171
|
+
return i18n;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Проміжна примітка: Тримайте список неймспейсів коротким для кожної сторінки, щоб обмежити обсяг передаваних даних. Уникайте глобальних «catch-all» бандлів.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 5) Клієнтський провайдер для React-компонентів
|
|
180
|
+
|
|
181
|
+
Оберніть клієнтські компоненти провайдером, який відповідає конфігурації сервера й завантажує лише запитані namespaces.
|
|
182
|
+
|
|
183
|
+
```tsx fileName="src/components/I18nProvider.tsx"
|
|
184
|
+
"use client";
|
|
185
|
+
|
|
186
|
+
import * as React from "react";
|
|
187
|
+
import { I18nextProvider } from "react-i18next";
|
|
188
|
+
import { createInstance } from "i18next";
|
|
189
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
190
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
191
|
+
import { defaultLocale } from "@/i18n.config";
|
|
192
|
+
|
|
193
|
+
const backend = resourcesToBackend(
|
|
194
|
+
(locale: string, namespace: string) =>
|
|
195
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
type Props = {
|
|
199
|
+
locale: string;
|
|
200
|
+
namespaces?: string[];
|
|
201
|
+
resources?: Record<string, any>; // { ns: bundle }
|
|
202
|
+
children: React.ReactNode;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export default function I18nProvider({
|
|
206
|
+
locale,
|
|
207
|
+
namespaces = ["common"],
|
|
208
|
+
resources,
|
|
209
|
+
children,
|
|
210
|
+
}: Props) {
|
|
211
|
+
const [i18n] = React.useState(() => {
|
|
212
|
+
const i = createInstance();
|
|
213
|
+
|
|
214
|
+
i.use(initReactI18next)
|
|
215
|
+
.use(backend)
|
|
216
|
+
.init({
|
|
217
|
+
lng: locale,
|
|
218
|
+
fallbackLng: defaultLocale,
|
|
219
|
+
ns: namespaces,
|
|
220
|
+
resources: resources ? { [locale]: resources } : undefined,
|
|
221
|
+
defaultNS: "common",
|
|
222
|
+
interpolation: { escapeValue: false },
|
|
223
|
+
react: { useSuspense: false },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return i;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Порада для початківців: Не потрібно передавати всі повідомлення на клієнт. Почніть лише з namespaces сторінки.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 6) Локалізований layout та routes
|
|
238
|
+
|
|
239
|
+
Встановіть мову й напрям тексту, та попередньо згенеруйте маршрути для кожної локалі, щоб віддати перевагу статичному рендерингу.
|
|
240
|
+
|
|
241
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
242
|
+
import type { ReactNode } from "react";
|
|
243
|
+
import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";
|
|
244
|
+
|
|
245
|
+
export const dynamicParams = false;
|
|
246
|
+
|
|
247
|
+
export function generateStaticParams() {
|
|
248
|
+
return locales.map((locale) => ({ locale }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default function LocaleLayout({
|
|
252
|
+
children,
|
|
253
|
+
params,
|
|
254
|
+
}: {
|
|
255
|
+
children: ReactNode;
|
|
256
|
+
params: { locale: string };
|
|
257
|
+
}) {
|
|
258
|
+
const locale: Locale = (locales as readonly string[]).includes(params.locale)
|
|
259
|
+
? (params.locale as any)
|
|
260
|
+
: defaultLocale;
|
|
261
|
+
|
|
262
|
+
const dir = isRtl(locale) ? "rtl" : "ltr";
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<html lang={locale} dir={dir}>
|
|
266
|
+
<body>{children}</body>
|
|
267
|
+
</html>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 7) Приклад сторінки з використанням сервера та клієнта
|
|
275
|
+
|
|
276
|
+
```tsx fileName="src/app/[locale]/about.tsx"
|
|
277
|
+
import I18nProvider from "@/components/I18nProvider";
|
|
278
|
+
import { initI18next } from "@/app/i18n/server";
|
|
279
|
+
import type { Locale } from "@/i18n.config";
|
|
280
|
+
import ClientComponent from "@/components/ClientComponent";
|
|
281
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
282
|
+
|
|
283
|
+
// Примусове статичне рендерення сторінки
|
|
284
|
+
export const dynamic = "force-static";
|
|
285
|
+
|
|
286
|
+
export default async function AboutPage({
|
|
287
|
+
params: { locale },
|
|
288
|
+
}: {
|
|
289
|
+
params: { locale: Locale };
|
|
290
|
+
}) {
|
|
291
|
+
const namespaces = ["common", "about"] as const;
|
|
292
|
+
|
|
293
|
+
const i18n = await initI18next(locale, [...namespaces]);
|
|
294
|
+
const tAbout = i18n.getFixedT(locale, "about");
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<I18nProvider locale={locale} namespaces={[...namespaces]}>
|
|
298
|
+
<main>
|
|
299
|
+
<h1>{tAbout("title")}</h1>
|
|
300
|
+
|
|
301
|
+
<ClientComponent />
|
|
302
|
+
<ServerComponent t={tAbout} locale={locale} count={0} />
|
|
303
|
+
</main>
|
|
304
|
+
</I18nProvider>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Переклади (один файл JSON на неймспейс під `src/locales/...`):
|
|
310
|
+
|
|
311
|
+
```json fileName="src/locales/en/about.json"
|
|
312
|
+
{
|
|
313
|
+
"title": "Про",
|
|
314
|
+
"description": "Опис сторінки \"Про\"",
|
|
315
|
+
"counter": {
|
|
316
|
+
"label": "Лічильник",
|
|
317
|
+
"increment": "Збільшити"
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```json fileName="src/locales/fr/about.json"
|
|
323
|
+
{
|
|
324
|
+
"title": "Про",
|
|
325
|
+
"description": "Опис сторінки \"Про\"",
|
|
326
|
+
"counter": {
|
|
327
|
+
"label": "Лічильник",
|
|
328
|
+
"increment": "Збільшити"
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Клієнтський компонент (завантажує лише потрібний неймспейс):
|
|
334
|
+
|
|
335
|
+
```tsx fileName="src/components/ClientComponent.tsx"
|
|
336
|
+
"use client";
|
|
337
|
+
|
|
338
|
+
import React, { useState } from "react";
|
|
339
|
+
tsx;
|
|
340
|
+
import { useTranslation } from "react-i18next";
|
|
341
|
+
|
|
342
|
+
const ClientComponent = () => {
|
|
343
|
+
const { t, i18n } = useTranslation("about");
|
|
344
|
+
const [count, setCount] = useState(0);
|
|
345
|
+
|
|
346
|
+
const numberFormat = new Intl.NumberFormat(i18n.language);
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div>
|
|
350
|
+
<p>{numberFormat.format(count)}</p>
|
|
351
|
+
<button
|
|
352
|
+
aria-label={t("counter.label")}
|
|
353
|
+
onClick={() => setCount((c) => c + 1)}
|
|
354
|
+
>
|
|
355
|
+
{t("counter.increment")}
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export default ClientComponent;
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
> Переконайтеся, що сторінка/провайдер включає лише ті простори імен, які вам потрібні (наприклад, `about`).
|
|
365
|
+
> Якщо ви використовуєте React < 19, мемоізуйте важкі форматери, такі як `Intl.NumberFormat`.
|
|
366
|
+
|
|
367
|
+
Синхронний серверний компонент, вбудований у клієнтську межу:
|
|
368
|
+
|
|
369
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
370
|
+
type ServerComponentProps = {
|
|
371
|
+
t: (key: string) => string;
|
|
372
|
+
locale: string;
|
|
373
|
+
count: number;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
|
|
377
|
+
const formatted = new Intl.NumberFormat(locale).format(count);
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div>
|
|
381
|
+
<p>{formatted}</p>
|
|
382
|
+
<button aria-label={t("counter.label")}>{t("counter.increment")}</button>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
export default ServerComponent;
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## 8) SEO: Метадані, Hreflang, Sitemap, Robots
|
|
393
|
+
|
|
394
|
+
Переклад контенту — це спосіб розширити охоплення. Ретельно налаштуйте багатомовне SEO.
|
|
395
|
+
|
|
396
|
+
Найкращі практики:
|
|
397
|
+
|
|
398
|
+
- Встановіть `lang` та `dir` у корені
|
|
399
|
+
- Додайте `alternates.languages` для кожної локалі (+ `x-default`)
|
|
400
|
+
- Перелічіть перекладені URL у `sitemap.xml` і використовуйте `hreflang`
|
|
401
|
+
- Виключайте локалізовані приватні розділи (наприклад, `/fr/admin`) у `robots.txt`
|
|
402
|
+
|
|
403
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
404
|
+
import type { Metadata } from "next";
|
|
405
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
406
|
+
|
|
407
|
+
export async function generateMetadata({
|
|
408
|
+
params,
|
|
409
|
+
}: {
|
|
410
|
+
params: { locale: string };
|
|
411
|
+
}): Promise<Metadata> {
|
|
412
|
+
const { locale } = params;
|
|
413
|
+
|
|
414
|
+
// Імпортуйте правильний JSON bundle з src/locales
|
|
415
|
+
const messages = (await import("@/locales/" + locale + "/about.json"))
|
|
416
|
+
.default;
|
|
417
|
+
|
|
418
|
+
const languages = Object.fromEntries(
|
|
419
|
+
locales.map((locale) => [locale, localizedPath(locale, "/about")])
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
title: messages.title,
|
|
424
|
+
description: messages.description,
|
|
425
|
+
alternates: {
|
|
426
|
+
canonical: localizedPath(locale, "/about"),
|
|
427
|
+
languages: { ...languages, "x-default": "/about" },
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export default async function AboutPage() {
|
|
433
|
+
return <h1>Про нас</h1>;
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
```ts fileName="src/app/sitemap.ts"
|
|
438
|
+
import type { MetadataRoute } from "next";
|
|
439
|
+
import { locales, defaultLocale, abs } from "@/i18n.config";
|
|
440
|
+
|
|
441
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
442
|
+
const languages = Object.fromEntries(
|
|
443
|
+
locales.map((locale) => [locale, abs(locale, "/about")])
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
url: abs(defaultLocale, "/about"),
|
|
449
|
+
lastModified: new Date(),
|
|
450
|
+
changeFrequency: "monthly",
|
|
451
|
+
priority: 0.7,
|
|
452
|
+
alternates: { languages },
|
|
453
|
+
},
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
```ts fileName="src/app/robots.ts"
|
|
459
|
+
import type { MetadataRoute } from "next";
|
|
460
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
461
|
+
|
|
462
|
+
const ORIGIN = "https://example.com";
|
|
463
|
+
|
|
464
|
+
const expandAllLocales = (path: string) => [
|
|
465
|
+
localizedPath(defaultLocale, path),
|
|
466
|
+
...locales
|
|
467
|
+
.filter((locale) => locale !== defaultLocale)
|
|
468
|
+
.map((locale) => localizedPath(locale, path)),
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
export default function robots(): MetadataRoute.Robots {
|
|
472
|
+
const disallow = [
|
|
473
|
+
...expandAllLocales("/dashboard"),
|
|
474
|
+
...expandAllLocales("/admin"),
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
479
|
+
host: ORIGIN,
|
|
480
|
+
sitemap: ORIGIN + "/sitemap.xml",
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## 9) Middleware для маршрутизації локалі
|
|
488
|
+
|
|
489
|
+
Виявляє локаль і перенаправляє на локалізований маршрут, якщо він відсутній.
|
|
490
|
+
|
|
491
|
+
```ts fileName="src/middleware.ts"
|
|
492
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
493
|
+
import { defaultLocale, locales } from "@/i18n.config";
|
|
494
|
+
|
|
495
|
+
const PUBLIC_FILE = /\.[^/]+$/; // виключити файли з розширеннями
|
|
496
|
+
|
|
497
|
+
export function middleware(request: NextRequest) {
|
|
498
|
+
const { pathname } = request.nextUrl;
|
|
499
|
+
|
|
500
|
+
if (
|
|
501
|
+
pathname.startsWith("/_next") ||
|
|
502
|
+
pathname.startsWith("/api") ||
|
|
503
|
+
pathname.startsWith("/static") ||
|
|
504
|
+
PUBLIC_FILE.test(pathname)
|
|
505
|
+
) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const hasLocale = locales.some(
|
|
510
|
+
(locale) =>
|
|
511
|
+
pathname === "/" + locale || pathname.startsWith("/" + locale + "/")
|
|
512
|
+
);
|
|
513
|
+
if (!hasLocale) {
|
|
514
|
+
const locale = defaultLocale;
|
|
515
|
+
const url = request.nextUrl.clone();
|
|
516
|
+
url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);
|
|
517
|
+
return NextResponse.redirect(url);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export const config = {
|
|
522
|
+
matcher: [
|
|
523
|
+
// Відповідає всім шляхам, окрім тих, що починаються з наведених і файлів з розширенням
|
|
524
|
+
"/((?!api|_next|static|.*\\..*).*)",
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## 10) Кращі практики продуктивності та DX
|
|
532
|
+
|
|
533
|
+
- **Встановіть атрибути html `lang` і `dir`**: Зроблено в `src/app/[locale]/layout.tsx`.
|
|
534
|
+
- **Розділяйте повідомлення за namespace**: Тримайте бандли маленькими (`common.json`, `about.json`, тощо).
|
|
535
|
+
- **Мінімізуйте payload клієнта**: На сторінках передавайте провайдеру лише потрібні namespace.
|
|
536
|
+
- **Надавайте перевагу статичним сторінкам**: Використовуйте `export const dynamic = 'force-static'` та `generateStaticParams` для кожної локалі.
|
|
537
|
+
- **Синхронізуйте server components**: Передавайте попередньо обчислені рядки/форматування замість асинхронних викликів під час рендерингу.
|
|
538
|
+
- **Застосовуйте memoization для важких операцій**: Особливо в клієнтському коді для старіших версій React.
|
|
539
|
+
- **Кеш і заголовки**: Віддавайте перевагу статичному рендерингу або `revalidate` замість динамічного, коли це можливо.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## 11) Тестування та CI
|
|
544
|
+
|
|
545
|
+
- Додайте юніт-тести для компонентів, що використовують `t`, щоб переконатися, що ключі існують.
|
|
546
|
+
- Перевіряйте, що в кожному namespace однакові ключі в усіх локалях.
|
|
547
|
+
- Забезпечуйте виявлення відсутніх ключів у CI перед деплоєм.
|
|
548
|
+
|
|
549
|
+
Intlayer автоматизує більшість цього (див. наступний розділ).
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## 12) Додайте Intlayer зверху (автоматизація)
|
|
554
|
+
|
|
555
|
+
Intlayer допомагає синхронізувати JSON-переклади, перевіряти відсутні ключі та за потреби заповнювати їх за допомогою AI.
|
|
556
|
+
|
|
557
|
+
Встановіть залежності intlayer:
|
|
558
|
+
|
|
559
|
+
```bash packageManager="npm"
|
|
560
|
+
npm install intlayer @intlayer/sync-json-plugin --save-dev
|
|
561
|
+
npx intlayer init
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
```bash packageManager="pnpm"
|
|
565
|
+
pnpm add intlayer @intlayer/sync-json-plugin --save-dev
|
|
566
|
+
pnpm intlayer init
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
```bash packageManager="yarn"
|
|
570
|
+
yarn add intlayer @intlayer/sync-json-plugin --dev
|
|
571
|
+
yarn intlayer init
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
```bash packageManager="bun"
|
|
575
|
+
bun add intlayer @intlayer/sync-json-plugin --dev
|
|
576
|
+
bunx intlayer init
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
```ts fileName="intlayer.config.ts"
|
|
580
|
+
import { type IntlayerConfig, Locales } from "intlayer";
|
|
581
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
582
|
+
import { syncJSON } from "@intlayer/sync-json";
|
|
583
|
+
|
|
584
|
+
export const locales = [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH];
|
|
585
|
+
|
|
586
|
+
const config: IntlayerConfig = {
|
|
587
|
+
internationalization: {
|
|
588
|
+
locales,
|
|
589
|
+
defaultLocale,
|
|
590
|
+
},
|
|
591
|
+
ai: {
|
|
592
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
593
|
+
},
|
|
594
|
+
plugins: [
|
|
595
|
+
syncJSON({
|
|
596
|
+
source: ({ locale }) => `./locales/${locale}.json`,
|
|
597
|
+
}),
|
|
598
|
+
],
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
export default config;
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Додайте скрипти у package.json:
|
|
605
|
+
|
|
606
|
+
```json fileName="package.json"
|
|
607
|
+
{
|
|
608
|
+
"scripts": {
|
|
609
|
+
"i18n:fill": "intlayer fill",
|
|
610
|
+
"i18n:test": "intlayer test"
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Типові сценарії:
|
|
616
|
+
|
|
617
|
+
- `pnpm i18n:test` у CI — щоб CI завершувався з помилкою при відсутніх ключах
|
|
618
|
+
- `pnpm i18n:fill` локально — пропонує AI-переклади для щойно доданих ключів
|
|
619
|
+
|
|
620
|
+
> Ви можете передавати аргументи CLI; див. [документацію Intlayer CLI](https://github.com/aymericzip/intlayer/blob/main/docs/docs/uk/cli/index.md).
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## 13) Усунення неполадок
|
|
625
|
+
|
|
626
|
+
- **Ключі не знайдені**: Переконайтесь, що сторінка/провайдер перераховують правильні простори імен (namespaces) і файл JSON існує за шляхом `src/locales/<locale>/<namespace>.json`.
|
|
627
|
+
- **Неправильна мова / мерехтіння англійською**: Перевірте визначення локалі в `middleware.ts` та значення `lng` у провайдері.
|
|
628
|
+
- **Проблеми з RTL-розкладкою**: Переконайтесь, що `dir` встановлюється через `isRtl(locale)` і що ваш CSS поважає `[dir="rtl"]`.
|
|
629
|
+
- **Відсутні SEO alternates**: Підтвердіть, що `alternates.languages` включає всі локалі та `x-default`.
|
|
630
|
+
- **Занадто великі бандли**: Розбивайте namespaces ще дрібніше та уникайте імпорту цілих дерев `locales` на клієнті.
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
## 14) Що далі
|
|
635
|
+
|
|
636
|
+
- Додайте більше локалей та namespaces у міру розвитку функціоналу (features)
|
|
637
|
+
- Локалізуйте сторінки помилок, електронні листи та контент, що подається через API
|
|
638
|
+
- Розширте робочі процеси Intlayer, щоб автоматично відкривати PR для оновлень перекладів
|
|
639
|
+
|
|
640
|
+
Якщо ви віддаєте перевагу стартовому шаблону, спробуйте: `https://github.com/aymericzip/intlayer-next-i18next-template`.
|