@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,360 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2025-09-28
|
|
3
|
+
updatedAt: 2025-09-28
|
|
4
|
+
title: SEO та i18n в Next.js
|
|
5
|
+
description: Дізнайтеся, як налаштувати багатомовне SEO у вашому додатку Next.js за допомогою next-intl, next-i18next та Intlayer.
|
|
6
|
+
keywords:
|
|
7
|
+
- Intlayer
|
|
8
|
+
- SEO
|
|
9
|
+
- Інтернаціоналізація
|
|
10
|
+
- Next.js
|
|
11
|
+
- i18n
|
|
12
|
+
- JavaScript
|
|
13
|
+
- React
|
|
14
|
+
- next-intl
|
|
15
|
+
- next-i18next
|
|
16
|
+
slugs:
|
|
17
|
+
- blog
|
|
18
|
+
- blog-seo-i18n-nextjs
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# SEO та i18n в Next.js: Перекладу недостатньо
|
|
22
|
+
|
|
23
|
+
Коли розробники думають про інтернаціоналізацію (i18n), першим рефлексом часто є: _перекласти контент_. Але зазвичай забувають, що головна мета інтернаціоналізації — зробити ваш вебсайт більш помітним для світу.
|
|
24
|
+
Якщо ваша багатомовна програма Next.js не повідомляє пошуковим системам, як сканувати й розуміти різні мовні версії, більшість ваших зусиль може залишитися непоміченою.
|
|
25
|
+
|
|
26
|
+
У цьому блозі ми розглянемо, чому **i18n — це суперсила для SEO**, і як правильно реалізувати її в Next.js за допомогою `next-intl`, `next-i18next` та `Intlayer`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Чому SEO та i18n
|
|
31
|
+
|
|
32
|
+
Додавання мов — це не лише про UX. Це також потужний важіль для **органічної видимості**. Ось чому:
|
|
33
|
+
|
|
34
|
+
1. **Краща виявлюваність:** Пошукові системи індексують локалізовані версії та ранжують їх для користувачів, які шукають рідною мовою.
|
|
35
|
+
2. **Уникнення дубльованого контенту:** Правильні канонічні та альтернативні теги повідомляють сканерам, яка сторінка належить до якої локалі.
|
|
36
|
+
3. **Кращий UX:** Відвідувачі потрапляють одразу на потрібну версію вашого сайту.
|
|
37
|
+
4. **Конкурентна перевага:** Лише небагато сайтів правильно реалізують багатомовне SEO, тож ви можете виділитися.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Найкращі практики для багатомовного SEO в Next.js
|
|
42
|
+
|
|
43
|
+
Ось контрольний список, який має реалізувати кожен багатомовний додаток:
|
|
44
|
+
|
|
45
|
+
- **Встановіть мета-теги `hreflang` у `<head>`**
|
|
46
|
+
Допомагає Google зрозуміти, які версії існують для кожної мови.
|
|
47
|
+
|
|
48
|
+
- **Перелічіть всі перекладені сторінки у `sitemap.xml`**
|
|
49
|
+
Використовуйте схему `xhtml`, щоб пошукові роботи могли легко знаходити альтернативні версії.
|
|
50
|
+
|
|
51
|
+
- **Виключіть приватні/локалізовані маршрути в `robots.txt`**
|
|
52
|
+
Наприклад, не дозволяйте індексацію `/dashboard`, `/fr/dashboard`, `/es/dashboard`.
|
|
53
|
+
|
|
54
|
+
- **Використовуйте локалізовані посилання**
|
|
55
|
+
Приклад: `<a href="/fr/about">Про нас</a>` замість посилання на сторінку за замовчуванням `/about`.
|
|
56
|
+
|
|
57
|
+
Це прості кроки — але їх пропуск може коштувати вам видимості.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Приклади реалізації
|
|
62
|
+
|
|
63
|
+
Розробники часто забувають правильно посилатися на свої сторінки для різних локалей, тож давайте подивимось, як це працює на практиці з різними бібліотеками.
|
|
64
|
+
|
|
65
|
+
### **next-intl**
|
|
66
|
+
|
|
67
|
+
<Tabs>
|
|
68
|
+
<TabItem label="next-intl">
|
|
69
|
+
|
|
70
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx
|
|
71
|
+
import type { Metadata } from "next";
|
|
72
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
73
|
+
import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
|
|
74
|
+
|
|
75
|
+
function localizedPath(locale: string, path: string) {
|
|
76
|
+
return locale === defaultLocale ? path : `/${locale}${path}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function generateMetadata({
|
|
80
|
+
params,
|
|
81
|
+
}: {
|
|
82
|
+
params: { locale: string };
|
|
83
|
+
}): Promise<Metadata> {
|
|
84
|
+
const { locale } = params;
|
|
85
|
+
const t = await getTranslations({ locale, namespace: "about" });
|
|
86
|
+
|
|
87
|
+
const url = "/about";
|
|
88
|
+
const languages = Object.fromEntries(
|
|
89
|
+
locales.map((l) => [l, localizedPath(l, url)])
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
title: t("title"),
|
|
94
|
+
description: t("description"),
|
|
95
|
+
alternates: {
|
|
96
|
+
canonical: localizedPath(locale, url),
|
|
97
|
+
languages: { ...languages, "x-default": url },
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ... решта коду сторінки
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```tsx fileName="src/app/sitemap.ts"
|
|
106
|
+
import type { MetadataRoute } from "next";
|
|
107
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
108
|
+
|
|
109
|
+
const origin = "https://example.com";
|
|
110
|
+
|
|
111
|
+
const formatterLocalizedPath = (locale: string, path: string) =>
|
|
112
|
+
locale === defaultLocale ? `${origin}${path}` : `${origin}/${locale}${path}`;
|
|
113
|
+
|
|
114
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
115
|
+
const aboutLanguages = Object.fromEntries(
|
|
116
|
+
locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
url: formatterLocalizedPath(defaultLocale, "/about"),
|
|
122
|
+
lastModified: new Date(),
|
|
123
|
+
changeFrequency: "monthly",
|
|
124
|
+
priority: 0.7,
|
|
125
|
+
alternates: { languages: aboutLanguages },
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```tsx fileName="src/app/robots.ts"
|
|
132
|
+
import type { MetadataRoute } from "next";
|
|
133
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
134
|
+
|
|
135
|
+
const origin = "https://example.com";
|
|
136
|
+
const withAllLocales = (path: string) => [
|
|
137
|
+
path,
|
|
138
|
+
...locales.filter((l) => l !== defaultLocale).map((l) => `/${l}${path}`),
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
export default function robots(): MetadataRoute.Robots {
|
|
142
|
+
const disallow = [
|
|
143
|
+
...withAllLocales("/dashboard"),
|
|
144
|
+
...withAllLocales("/admin"),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
149
|
+
host: origin,
|
|
150
|
+
sitemap: `${origin}/sitemap.xml`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### **next-i18next**
|
|
156
|
+
|
|
157
|
+
</TabItem>
|
|
158
|
+
<TabItem label="next-i18next">
|
|
159
|
+
|
|
160
|
+
```ts fileName="i18n.config.ts"
|
|
161
|
+
export const locales = ["en", "fr"] as const;
|
|
162
|
+
export type Locale = (typeof locales)[number];
|
|
163
|
+
export const defaultLocale: Locale = "en";
|
|
164
|
+
|
|
165
|
+
/** Додає префікс локалі до шляху, якщо це не локаль за замовчуванням */
|
|
166
|
+
export function localizedPath(locale: string, path: string) {
|
|
167
|
+
return locale === defaultLocale ? path : `/${locale}${path}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Допоміжна функція для абсолютних URL */
|
|
171
|
+
const ORIGIN = "https://example.com";
|
|
172
|
+
export function abs(locale: string, path: string) {
|
|
173
|
+
return `${ORIGIN}${localizedPath(locale, path)}`;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
178
|
+
import type { Metadata } from "next";
|
|
179
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
180
|
+
|
|
181
|
+
export async function generateMetadata({
|
|
182
|
+
params,
|
|
183
|
+
}: {
|
|
184
|
+
params: { locale: string };
|
|
185
|
+
}): Promise<Metadata> {
|
|
186
|
+
const { locale } = params;
|
|
187
|
+
|
|
188
|
+
// Динамічно імпортуємо відповідний JSON-файл
|
|
189
|
+
const messages = (await import(`@/../public/locales/${locale}/about.json`))
|
|
190
|
+
.default;
|
|
191
|
+
|
|
192
|
+
const languages = Object.fromEntries(
|
|
193
|
+
locales.map((l) => [l, localizedPath(l, "/about")])
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
title: messages.title,
|
|
198
|
+
description: messages.description,
|
|
199
|
+
alternates: {
|
|
200
|
+
canonical: localizedPath(locale, "/about"),
|
|
201
|
+
languages: { ...languages, "x-default": "/about" },
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default async function AboutPage() {
|
|
207
|
+
return <h1>Про нас</h1>;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```ts fileName="src/app/sitemap.ts"
|
|
212
|
+
import type { MetadataRoute } from "next";
|
|
213
|
+
import { locales, defaultLocale, abs } from "@/i18n.config";
|
|
214
|
+
|
|
215
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
216
|
+
const languages = Object.fromEntries(
|
|
217
|
+
locales.map((l) => [l, abs(l, "/about")])
|
|
218
|
+
);
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
url: abs(defaultLocale, "/about"),
|
|
222
|
+
lastModified: new Date(),
|
|
223
|
+
changeFrequency: "monthly",
|
|
224
|
+
priority: 0.7,
|
|
225
|
+
alternates: { languages },
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```ts fileName="src/app/robots.ts"
|
|
232
|
+
import type { MetadataRoute } from "next";
|
|
233
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
234
|
+
|
|
235
|
+
const ORIGIN = "https://example.com";
|
|
236
|
+
|
|
237
|
+
const expandAllLocales = (path: string) => [
|
|
238
|
+
localizedPath(defaultLocale, path),
|
|
239
|
+
...locales
|
|
240
|
+
.filter((l) => l !== defaultLocale)
|
|
241
|
+
.map((l) => localizedPath(l, path)),
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
export default function robots(): MetadataRoute.Robots {
|
|
245
|
+
const disallow = [
|
|
246
|
+
...expandAllLocales("/dashboard"),
|
|
247
|
+
...expandAllLocales("/admin"),
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
252
|
+
host: ORIGIN,
|
|
253
|
+
sitemap: `${ORIGIN}/sitemap.xml`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### **Intlayer**
|
|
259
|
+
|
|
260
|
+
</TabItem>
|
|
261
|
+
<TabItem label="intlayer">
|
|
262
|
+
|
|
263
|
+
````typescript fileName="src/app/[locale]/about/layout.tsx"
|
|
264
|
+
import { getIntlayer, getMultilingualUrls } from "intlayer";
|
|
265
|
+
import type { Metadata } from "next";
|
|
266
|
+
import type { LocalPromiseParams } from "next-intlayer";
|
|
267
|
+
|
|
268
|
+
export const generateMetadata = async ({
|
|
269
|
+
params,
|
|
270
|
+
}: LocalPromiseParams): Promise<Metadata> => {
|
|
271
|
+
const { locale } = await params;
|
|
272
|
+
|
|
273
|
+
const metadata = getIntlayer("page-metadata", locale);
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Генерує об'єкт, що містить усі URL-адреси для кожної локалі.
|
|
277
|
+
*
|
|
278
|
+
* Приклад:
|
|
279
|
+
* ```ts
|
|
280
|
+
* getMultilingualUrls('/about');
|
|
281
|
+
*
|
|
282
|
+
* // Повертає
|
|
283
|
+
* // {
|
|
284
|
+
* // en: '/about',
|
|
285
|
+
* // fr: '/fr/about',
|
|
286
|
+
* // es: '/es/about',
|
|
287
|
+
* // }
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
const multilingualUrls = getMultilingualUrls("/about");
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
...metadata,
|
|
294
|
+
alternates: {
|
|
295
|
+
canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
|
|
296
|
+
languages: { ...multilingualUrls, "x-default": "/about" },
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// ... Решта коду сторінки
|
|
302
|
+
````
|
|
303
|
+
|
|
304
|
+
```tsx fileName="src/app/sitemap.ts"
|
|
305
|
+
import { getMultilingualUrls } from "intlayer";
|
|
306
|
+
import type { MetadataRoute } from "next";
|
|
307
|
+
|
|
308
|
+
const sitemap = (): MetadataRoute.Sitemap => [
|
|
309
|
+
{
|
|
310
|
+
url: "https://example.com/about",
|
|
311
|
+
alternates: {
|
|
312
|
+
languages: { ...getMultilingualUrls("https://example.com/about") },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```tsx fileName="src/app/robots.ts"
|
|
319
|
+
import { getMultilingualUrls } from "intlayer";
|
|
320
|
+
import type { MetadataRoute } from "next";
|
|
321
|
+
|
|
322
|
+
const getAllMultilingualUrls = (urls: string[]) =>
|
|
323
|
+
urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
|
|
324
|
+
|
|
325
|
+
const robots = (): MetadataRoute.Robots => ({
|
|
326
|
+
rules: {
|
|
327
|
+
userAgent: "*",
|
|
328
|
+
allow: ["/"],
|
|
329
|
+
disallow: getAllMultilingualUrls(["/dashboard"]),
|
|
330
|
+
},
|
|
331
|
+
host: "https://example.com",
|
|
332
|
+
sitemap: `https://example.com/sitemap.xml`,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
export default robots;
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
> Intlayer надає функцію `getMultilingualUrls` для генерації багатомовних URL-адрес для вашого sitemap.
|
|
339
|
+
|
|
340
|
+
</TabItem>
|
|
341
|
+
</Tabs>
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Висновок
|
|
346
|
+
|
|
347
|
+
Правильна реалізація i18n у Next.js — це не просто переклад тексту, а забезпечення того, щоб пошукові системи та користувачі точно знали, яку версію вашого контенту показувати.
|
|
348
|
+
Налаштування hreflang, sitemap і правил для robots — те, що перетворює переклади на реальну SEO-цінність.
|
|
349
|
+
|
|
350
|
+
Хоча next-intl і next-i18next дають надійні способи це реалізувати, зазвичай вони вимагають багато ручного налаштування, щоб підтримувати консистентність між локалями.
|
|
351
|
+
|
|
352
|
+
Саме тут Intlayer дійсно вирізняється:
|
|
353
|
+
|
|
354
|
+
Воно постачається з вбудованими хелперами, такими як getMultilingualUrls, що робить інтеграцію hreflang, sitemap і robots майже беззусильною.
|
|
355
|
+
|
|
356
|
+
Метадані зберігаються централізовано замість того, щоб розкидуватися по JSON-файлах або власних утилітах.
|
|
357
|
+
|
|
358
|
+
Він спроєктований для Next.js з нуля, тож ви витрачаєте менше часу на налагодження конфігурації й більше — на реліз.
|
|
359
|
+
|
|
360
|
+
Якщо ваша мета — не просто перекладати, а масштабувати багатомовне SEO без зайвих зусиль, Intlayer дає вам найчистіше, найбільш стійке до майбутніх змін налаштування.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2025-09-10
|
|
3
|
+
updatedAt: 2025-09-10
|
|
4
|
+
title: Per-Component проти централізованого i18n: новий підхід з Intlayer
|
|
5
|
+
description: Детальний огляд стратегій інтернаціоналізації в React: порівняння централізованого, per-key і per-component підходів та презентація Intlayer.
|
|
6
|
+
keywords:
|
|
7
|
+
- i18n
|
|
8
|
+
- React
|
|
9
|
+
- Internationalization
|
|
10
|
+
- Intlayer
|
|
11
|
+
- Optimization
|
|
12
|
+
- Bundle Size
|
|
13
|
+
slugs:
|
|
14
|
+
- blog
|
|
15
|
+
- per-component-vs-centralized-i18n
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Підхід per-component проти централізованого i18n
|
|
19
|
+
|
|
20
|
+
Підхід per-component не є новим поняттям. Наприклад, в екосистемі Vue `vue-i18n` підтримує [i18n SFC (Single File Component)](https://vue-i18n.intlify.dev/guide/advanced/sfc.html). Nuxt також пропонує [переклади per-component](https://i18n.nuxtjs.org/docs/guide/per-component-translations), а Angular використовує подібний патерн через свої [Feature Modules](https://v17.angular.io/guide/feature-modules).
|
|
21
|
+
|
|
22
|
+
Навіть у Flutter-додатку ми часто можемо знайти цей шаблон:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lib/
|
|
26
|
+
└── features/
|
|
27
|
+
└── login/
|
|
28
|
+
├── login_screen.dart
|
|
29
|
+
└── login_screen.i18n.dart # <- Переклади знаходяться тут
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```dart fileName='lib/features/login/login_screen.i18n.dart'
|
|
33
|
+
import 'package:i18n_extension/i18n_extension.dart';
|
|
34
|
+
|
|
35
|
+
extension Localization on String {
|
|
36
|
+
static var _t = Translations.byText("en") +
|
|
37
|
+
{
|
|
38
|
+
"Hello": {
|
|
39
|
+
"en": "Hello",
|
|
40
|
+
"fr": "Bonjour",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
String get i18n => localize(this, _t);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Однак у світі React ми переважно бачимо різні підходи, які я згрупую в три категорії:
|
|
49
|
+
|
|
50
|
+
<Columns>
|
|
51
|
+
<Column>
|
|
52
|
+
|
|
53
|
+
**Централізований підхід** (i18next, next-intl, react-intl, lingui)
|
|
54
|
+
|
|
55
|
+
- (без неймспейсів) розглядає єдине джерело для отримання контенту. За замовчуванням ви завантажуєте контент зі всіх сторінок при завантаженні застосунку.
|
|
56
|
+
|
|
57
|
+
</Column>
|
|
58
|
+
<Column>
|
|
59
|
+
|
|
60
|
+
**Гранулярний підхід** (intlayer, inlang)
|
|
61
|
+
|
|
62
|
+
- деталізує отримання контенту за ключем або на рівні компонента.
|
|
63
|
+
|
|
64
|
+
</Column>
|
|
65
|
+
</Columns>
|
|
66
|
+
|
|
67
|
+
> У цьому блозі я не зосереджуватимусь на рішеннях на основі компілятора, які я вже розглянув тут: [Компілятор проти декларативного i18n](https://github.com/aymericzip/intlayer/blob/main/docs/blog/uk/compiler_vs_declarative_i18n.md).
|
|
68
|
+
> Зауважте, що компіляторні i18n-рішення (наприклад, Lingui) лише автоматизують витяг та завантаження контенту. Під капотом вони часто мають ті самі обмеження, що й інші підходи.
|
|
69
|
+
|
|
70
|
+
> Зауважте, що чим детальніше ви налаштовуєте спосіб отримання контенту, тим більший ризик додати додатковий стан і логіку в компоненти.
|
|
71
|
+
|
|
72
|
+
Гранулярні підходи є більш гнучкими, ніж централізовані, але часто це компроміс. Навіть якщо ці бібліотеки рекламують "tree shaking", на практиці ви часто в кінцевому підсумку завантажуєте сторінку на всіх мовах.
|
|
73
|
+
|
|
74
|
+
Отже, в загальних рисах рішення зводиться до наступного:
|
|
75
|
+
|
|
76
|
+
- Якщо в вашому застосунку більше сторінок, ніж мов, варто віддавати перевагу гранулярному підходу.
|
|
77
|
+
- Якщо мов більше, ніж сторінок, слід схилитися до централізованого підходу.
|
|
78
|
+
|
|
79
|
+
Звісно, автори бібліотек усвідомлюють ці обмеження і пропонують обхідні шляхи.
|
|
80
|
+
Серед них: розподіл на namespaces, динамічне завантаження JSON-файлів (`await import()`), або очищення контенту під час збірки.
|
|
81
|
+
|
|
82
|
+
Водночас потрібно знати, що коли ви динамічно завантажуєте свій вміст, ви вводите додаткові запити до сервера. Кожен додатковий `useState` або хук означає ще один запит до сервера.
|
|
83
|
+
|
|
84
|
+
> Щоб вирішити це питання, Intlayer пропонує групувати кілька визначень контенту під одним ключем — Intlayer потім об'єднає цей контент.
|
|
85
|
+
|
|
86
|
+
Але з-поміж усіх цих рішень очевидно, що найпопулярнішим є централізований підхід.
|
|
87
|
+
|
|
88
|
+
### Чому ж централізований підхід такий популярний?
|
|
89
|
+
|
|
90
|
+
- По-перше, i18next було першим рішенням, яке стало широко використовуваним, і воно слідувало філософії, запозиченій із PHP та Java-архітектур (MVC), які базуються на суворому розділенні обов'язків (триманні контенту окремо від коду). Воно з'явилося в 2011 році, встановивши свої стандарти ще до масового переходу до архітектур на основі компонентів (наприклад, React).
|
|
91
|
+
- Далі, коли бібліотека широко прийнята, змінити екосистему на інші патерни стає складно.
|
|
92
|
+
- Використання централізованого підходу також спрощує роботу в Translation Management Systems, таких як Crowdin, Phrase або Localized.
|
|
93
|
+
- Логіка підходу per-component складніша за централізований і вимагає додаткового часу на розробку, особливо коли потрібно вирішувати задачі на кшталт визначення місця розташування контенту.
|
|
94
|
+
|
|
95
|
+
### Добре, але чому б просто не дотримуватися централізованого підходу?
|
|
96
|
+
|
|
97
|
+
Дозвольте пояснити, чому це може бути проблематично для вашого додатка:
|
|
98
|
+
|
|
99
|
+
- **Невикористані дані:**
|
|
100
|
+
Коли завантажується сторінка, часто підвантажується контент з усіх інших сторінок. (У додатку з 10 сторінок це означає 90% невикористаного контенту.) Відкриваєте модальне вікно з відкладеним завантаженням? Бібліотека i18n байдуже — вона все одно спочатку підвантажує рядки.
|
|
101
|
+
- **Продуктивність:**
|
|
102
|
+
При кожному перерендері кожен ваш компонент отримує велике JSON-навантаження, що погіршує реактивність додатка в міру його зростання.
|
|
103
|
+
- **Підтримка:**
|
|
104
|
+
Підтримка великих JSON-файлів болюча. Потрібно переходити між файлами, щоб додати переклад, переконуючись, що не бракує перекладів і не залишилося **orphan keys**.
|
|
105
|
+
- **Дизайн-система:**
|
|
106
|
+
Воно створює несумісність із дизайн-системами (наприклад, компонентом `LoginForm`) і обмежує дублювання компонентів між різними додатками.
|
|
107
|
+
|
|
108
|
+
**"Але ми винайшли Namespaces!"**
|
|
109
|
+
|
|
110
|
+
Звісно, і це величезний крок уперед. Погляньмо на порівняння розміру основного бандла для налаштування Vite + React + React Router v7 + Intlayer. Ми змоделювали 20-сторінковий додаток.
|
|
111
|
+
|
|
112
|
+
Перший приклад не включає lazy-loaded переклади за локалями і не використовує розбиття на неймспейси. Другий включає content purging + динамічне завантаження перекладів.
|
|
113
|
+
|
|
114
|
+
| Оптимізований бандл | Бандл без оптимізації |
|
|
115
|
+
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
116
|
+
|  |  |
|
|
117
|
+
|
|
118
|
+
Отже, завдяки namespaces, ми перейшли від такої структури:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
locale/
|
|
122
|
+
├── en.json
|
|
123
|
+
├── fr.json
|
|
124
|
+
└── es.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
To this one:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
locale/
|
|
131
|
+
├── en/
|
|
132
|
+
│ ├── common.json
|
|
133
|
+
│ ├── navbar.json
|
|
134
|
+
│ ├── footer.json
|
|
135
|
+
│ ├── home.json
|
|
136
|
+
│ └── about.json
|
|
137
|
+
├── fr/
|
|
138
|
+
│ └── ...
|
|
139
|
+
└── es/
|
|
140
|
+
└── ...
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Тепер вам потрібно точно керувати тим, яка частина контенту вашого додатка має завантажуватися й де. У підсумку більшість проєктів просто пропускають цю частину через складність (див. наприклад [посібник next-i18next](https://github.com/aymericzip/intlayer/blob/main/docs/blog/uk/i18n_using_next-i18next.md), щоб побачити виклики, які становить (лише) дотримання найкращих практик).
|
|
145
|
+
Внаслідок цього ті проєкти опиняються з проблемою масивного завантаження JSON, описаною раніше.
|
|
146
|
+
|
|
147
|
+
> Зверніть увагу, що ця проблема не є специфічною для i18next, а характерна для всіх централізованих підходів, перелічених вище.
|
|
148
|
+
|
|
149
|
+
Проте хочу нагадати, що не всі гранулярні підходи вирішують цю проблему. Наприклад, підходи `vue-i18n SFC` або `inlang` за замовчуванням не виконують відкладене завантаження перекладів за локалями (lazy load), тому ви просто замінюєте проблему розміру бандла на іншу.
|
|
150
|
+
|
|
151
|
+
Більше того, без належного розділення обов'язків (separation of concerns) стає значно складніше витягувати та надавати ваші переклади перекладачам для перегляду.
|
|
152
|
+
|
|
153
|
+
### Як підхід Intlayer на рівні компонентів вирішує це
|
|
154
|
+
|
|
155
|
+
Intlayer виконує кілька кроків:
|
|
156
|
+
|
|
157
|
+
1. **Declaration:** Оголосіть ваш контент будь-де у кодовій базі, використовуючи файли `*.content.{ts|jsx|cjs|json|json5|...}`. Це забезпечує розділення обов'язків, зберігаючи контент поруч із компонентами. Файл контенту може бути на одну локаль або мультимовним.
|
|
158
|
+
2. **Опрацювання:** Intlayer запускає крок збірки для обробки JS-логіки, обробки fallback-значень для відсутніх перекладів, генерації типів TypeScript, керування дубльованим контентом, отримання контенту з вашого CMS та іншого.
|
|
159
|
+
3. **Очищення:** Коли ваша аплікація збирається, Intlayer очищає невикористовуваний контент (трохи так само, як Tailwind керує класами), замінюючи контент наступним чином:
|
|
160
|
+
|
|
161
|
+
**Декларація:**
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// src/MyComponent.tsx
|
|
165
|
+
export const MyComponent = () => {
|
|
166
|
+
const content = useIntlayer("my-key");
|
|
167
|
+
return <h1>{content.title}</h1>;
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// src/myComponent.content.ts
|
|
173
|
+
export const {
|
|
174
|
+
key: "my-key",
|
|
175
|
+
content: t({
|
|
176
|
+
uk: { title: "Мій заголовок" },
|
|
177
|
+
en: { title: "My title" },
|
|
178
|
+
fr: { title: "Mon titre" }
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Опрацювання:** Intlayer будує словник на основі файлу `.content` та генерує:
|
|
185
|
+
|
|
186
|
+
```json5
|
|
187
|
+
// .intlayer/dynamic_dictionary/en/my-key.json
|
|
188
|
+
{
|
|
189
|
+
"key": "my-key",
|
|
190
|
+
"content": { "title": "My title" },
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Заміна:** Intlayer перетворює ваш компонент під час збірки застосунку.
|
|
195
|
+
|
|
196
|
+
**- Режим статичного імпорту:**
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
// Представлення компонента у синтаксисі, подібному до JSX
|
|
200
|
+
export const MyComponent = () => {
|
|
201
|
+
const content = useDictionary({
|
|
202
|
+
key: "my-key",
|
|
203
|
+
content: {
|
|
204
|
+
nodeType: "translation",
|
|
205
|
+
translation: {
|
|
206
|
+
en: { title: "My title" },
|
|
207
|
+
fr: { title: "Mon titre" },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return <h1>{content.title}</h1>;
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**- Режим динамічного імпорту:**
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// Представлення компонента у синтаксисі, подібному до JSX
|
|
220
|
+
export const MyComponent = () => {
|
|
221
|
+
const content = useDictionaryAsync({
|
|
222
|
+
en: () =>
|
|
223
|
+
import(".intlayer/dynamic_dictionary/en/my-key.json", {
|
|
224
|
+
with: { type: "json" },
|
|
225
|
+
}).then((mod) => mod.default),
|
|
226
|
+
// Так само для інших мов
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return <h1>{content.title}</h1>;
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
> `useDictionaryAsync` використовує механізм, схожий на Suspense, щоб завантажувати локалізований JSON лише за потреби.
|
|
234
|
+
|
|
235
|
+
**Ключові переваги цього компонентного підходу:**
|
|
236
|
+
|
|
237
|
+
- Тримання оголошення контенту поруч із компонентами забезпечує кращу підтримуваність (наприклад, при переміщенні компонентів до іншого app або design system. Видалення папки компонента також видаляє пов'язаний контент, як ви, ймовірно, вже робите для ваших `.test`, `.stories`)
|
|
238
|
+
|
|
239
|
+
- Підхід на рівні компоненту запобігає необхідності AI-агентам переходити по всіх ваших різних файлах. Він обробляє всі переклади в одному місці, зменшуючи складність завдання та кількість використовуваних токенів.
|
|
240
|
+
|
|
241
|
+
### Обмеження
|
|
242
|
+
|
|
243
|
+
Звісно, цей підхід має свої компроміси:
|
|
244
|
+
|
|
245
|
+
- Важче підключатися до інших l10n-систем та додаткових інструментів.
|
|
246
|
+
- Ви потрапляєте у залежність (що, по суті, вже відбувається з будь-яким i18n-рішенням через їхній специфічний синтаксис).
|
|
247
|
+
|
|
248
|
+
Саме тому Intlayer намагається надати повний набір інструментів для i18n (100% безкоштовний і з відкритим вихідним кодом — OSS), включно з AI-перекладом за допомогою вашого власного AI-провайдера та API-ключів. Intlayer також надає інструменти для синхронізації ваших JSON-файлів, що працюють подібно до message formatters ICU / vue-i18n / i18next, щоб відобразити контент у їхніх специфічних форматах.
|