@intlayer/docs 7.5.11 → 7.5.13
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/intlayer_with_i18next.md +0 -2
- package/blog/ar/intlayer_with_next-i18next.md +0 -2
- package/blog/ar/intlayer_with_react-i18next.md +0 -2
- package/blog/de/intlayer_with_i18next.md +0 -45
- package/blog/de/intlayer_with_next-i18next.md +0 -46
- package/blog/de/intlayer_with_react-i18next.md +0 -2
- package/blog/en/intlayer_with_i18next.md +0 -46
- package/blog/en/intlayer_with_next-i18next.md +0 -48
- package/blog/en/intlayer_with_next-intl.md +0 -44
- package/blog/en/intlayer_with_react-i18next.md +0 -44
- package/blog/en/intlayer_with_react-intl.md +0 -42
- package/blog/en/intlayer_with_vue-i18n.md +0 -44
- package/blog/en-GB/intlayer_with_i18next.md +0 -45
- package/blog/en-GB/intlayer_with_next-i18next.md +0 -47
- package/blog/en-GB/intlayer_with_next-intl.md +0 -42
- package/blog/en-GB/intlayer_with_react-i18next.md +0 -43
- package/blog/en-GB/intlayer_with_react-intl.md +0 -42
- package/blog/en-GB/intlayer_with_vue-i18n.md +0 -46
- package/blog/es/intlayer_with_i18next.md +0 -45
- package/blog/es/intlayer_with_next-i18next.md +0 -47
- package/blog/es/intlayer_with_next-intl.md +0 -42
- package/blog/es/intlayer_with_react-i18next.md +0 -43
- package/blog/es/intlayer_with_react-intl.md +0 -42
- package/blog/es/intlayer_with_vue-i18n.md +0 -46
- package/blog/fr/intlayer_with_i18next.md +0 -45
- package/blog/fr/intlayer_with_next-i18next.md +0 -47
- package/blog/fr/intlayer_with_next-intl.md +0 -42
- package/blog/fr/intlayer_with_react-i18next.md +0 -43
- package/blog/fr/intlayer_with_react-intl.md +0 -42
- package/blog/fr/intlayer_with_vue-i18n.md +0 -46
- package/blog/hi/intlayer_with_i18next.md +0 -2
- package/blog/hi/intlayer_with_next-i18next.md +0 -2
- package/blog/hi/intlayer_with_react-i18next.md +0 -2
- package/blog/id/intlayer_with_i18next.md +0 -2
- package/blog/id/intlayer_with_next-i18next.md +0 -2
- package/blog/id/intlayer_with_react-i18next.md +0 -2
- package/blog/it/intlayer_with_i18next.md +0 -2
- package/blog/it/intlayer_with_next-i18next.md +0 -2
- package/blog/it/intlayer_with_react-i18next.md +0 -2
- package/blog/ja/intlayer_with_i18next.md +0 -45
- package/blog/ja/intlayer_with_next-i18next.md +0 -46
- package/blog/ja/intlayer_with_next-intl.md +0 -42
- package/blog/ja/intlayer_with_react-i18next.md +0 -42
- package/blog/ja/intlayer_with_react-intl.md +0 -42
- package/blog/ja/intlayer_with_vue-i18n.md +0 -46
- package/blog/ko/intlayer_with_i18next.md +0 -2
- package/blog/ko/intlayer_with_next-i18next.md +0 -2
- package/blog/ko/intlayer_with_react-i18next.md +0 -1
- package/blog/pl/intlayer_with_i18next.md +0 -45
- package/blog/pl/intlayer_with_next-i18next.md +0 -46
- package/blog/pl/intlayer_with_next-intl.md +0 -42
- package/blog/pl/intlayer_with_react-i18next.md +0 -43
- package/blog/pl/intlayer_with_react-intl.md +0 -42
- package/blog/pl/intlayer_with_vue-i18n.md +0 -46
- package/blog/pt/intlayer_with_i18next.md +0 -2
- package/blog/pt/intlayer_with_next-i18next.md +0 -2
- package/blog/pt/intlayer_with_react-i18next.md +0 -2
- package/blog/ru/intlayer_with_i18next.md +0 -45
- package/blog/ru/intlayer_with_next-i18next.md +0 -47
- package/blog/ru/intlayer_with_next-intl.md +0 -42
- package/blog/ru/intlayer_with_react-i18next.md +0 -43
- package/blog/ru/intlayer_with_react-intl.md +0 -42
- package/blog/ru/intlayer_with_vue-i18n.md +0 -46
- package/blog/tr/intlayer_with_i18next.md +0 -2
- package/blog/tr/intlayer_with_next-i18next.md +0 -1
- package/blog/tr/intlayer_with_react-i18next.md +0 -2
- 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/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/intlayer_with_i18next.md +0 -2
- package/blog/vi/intlayer_with_next-i18next.md +0 -2
- package/blog/vi/intlayer_with_react-i18next.md +0 -2
- package/blog/zh/intlayer_with_i18next.md +0 -2
- package/blog/zh/intlayer_with_next-i18next.md +0 -2
- package/blog/zh/intlayer_with_react-i18next.md +0 -2
- package/blog/zh/intlayer_with_vue-i18n.md +0 -46
- package/dist/cjs/generated/blog.entry.cjs +58 -29
- package/dist/cjs/generated/blog.entry.cjs.map +1 -1
- package/dist/cjs/generated/docs.entry.cjs +218 -99
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/cjs/generated/frequentQuestions.entry.cjs +50 -15
- package/dist/cjs/generated/frequentQuestions.entry.cjs.map +1 -1
- package/dist/cjs/generated/legal.entry.cjs +4 -2
- package/dist/cjs/generated/legal.entry.cjs.map +1 -1
- package/dist/esm/generated/blog.entry.mjs +58 -29
- package/dist/esm/generated/blog.entry.mjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs +218 -99
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/esm/generated/frequentQuestions.entry.mjs +50 -15
- package/dist/esm/generated/frequentQuestions.entry.mjs.map +1 -1
- package/dist/esm/generated/legal.entry.mjs +4 -2
- package/dist/esm/generated/legal.entry.mjs.map +1 -1
- package/dist/types/generated/blog.entry.d.ts.map +1 -1
- package/dist/types/generated/docs.entry.d.ts +1 -0
- package/dist/types/generated/docs.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/dist/types/generated/legal.entry.d.ts.map +1 -1
- package/docs/ar/configuration.md +6 -1
- package/docs/ar/dictionary/content_file.md +6 -1
- package/docs/ar/intlayer_with_next-i18next.md +0 -1
- package/docs/ar/intlayer_with_nextjs_14.md +28 -0
- package/docs/ar/intlayer_with_nextjs_15.md +28 -0
- package/docs/ar/intlayer_with_nextjs_16.md +28 -0
- package/docs/ar/intlayer_with_nextjs_no_locale_path.md +1159 -0
- package/docs/ar/plugins/sync-json.md +6 -2
- package/docs/de/configuration.md +6 -1
- package/docs/de/dictionary/content_file.md +6 -1
- package/docs/de/intlayer_with_next-i18next.md +0 -1
- package/docs/de/intlayer_with_nextjs_14.md +28 -0
- package/docs/de/intlayer_with_nextjs_15.md +28 -0
- package/docs/de/intlayer_with_nextjs_16.md +28 -0
- package/docs/de/intlayer_with_nextjs_no_locale_path.md +1152 -0
- package/docs/de/plugins/sync-json.md +6 -2
- package/docs/en/configuration.md +6 -1
- package/docs/en/dictionary/content_file.md +6 -1
- package/docs/en/intlayer_with_next-i18next.md +0 -1
- package/docs/en/intlayer_with_nextjs_14.md +28 -0
- package/docs/en/intlayer_with_nextjs_15.md +28 -0
- package/docs/en/intlayer_with_nextjs_16.md +31 -1
- package/docs/en/intlayer_with_nextjs_no_locale_path.md +1132 -0
- package/docs/en/plugins/sync-json.md +6 -2
- package/docs/en-GB/configuration.md +6 -1
- package/docs/en-GB/dictionary/content_file.md +3 -1
- package/docs/en-GB/intlayer_with_next-i18next.md +0 -1
- package/docs/en-GB/intlayer_with_nextjs_14.md +28 -0
- package/docs/en-GB/intlayer_with_nextjs_15.md +28 -0
- package/docs/en-GB/intlayer_with_nextjs_16.md +28 -0
- package/docs/en-GB/intlayer_with_nextjs_no_locale_path.md +1154 -0
- package/docs/en-GB/plugins/sync-json.md +6 -2
- package/docs/es/configuration.md +6 -1
- package/docs/es/dictionary/content_file.md +6 -1
- package/docs/es/intlayer_with_next-i18next.md +0 -1
- package/docs/es/intlayer_with_nextjs_14.md +28 -0
- package/docs/es/intlayer_with_nextjs_15.md +28 -0
- package/docs/es/intlayer_with_nextjs_16.md +28 -0
- package/docs/es/intlayer_with_nextjs_no_locale_path.md +1143 -0
- package/docs/es/plugins/sync-json.md +6 -2
- package/docs/fr/configuration.md +6 -1
- package/docs/fr/dictionary/content_file.md +3 -1
- package/docs/fr/intlayer_with_next-i18next.md +0 -1
- package/docs/fr/intlayer_with_nextjs_14.md +28 -0
- package/docs/fr/intlayer_with_nextjs_15.md +28 -0
- package/docs/fr/intlayer_with_nextjs_16.md +28 -0
- package/docs/fr/intlayer_with_nextjs_no_locale_path.md +1174 -0
- package/docs/fr/plugins/sync-json.md +9 -5
- package/docs/hi/configuration.md +6 -1
- package/docs/hi/dictionary/content_file.md +3 -1
- package/docs/hi/intlayer_with_next-i18next.md +0 -1
- package/docs/hi/intlayer_with_nextjs_14.md +28 -0
- package/docs/hi/intlayer_with_nextjs_15.md +28 -0
- package/docs/hi/intlayer_with_nextjs_16.md +28 -0
- package/docs/hi/intlayer_with_nextjs_no_locale_path.md +1151 -0
- package/docs/hi/plugins/sync-json.md +6 -2
- package/docs/id/configuration.md +6 -1
- package/docs/id/dictionary/content_file.md +3 -1
- package/docs/id/intlayer_with_next-i18next.md +0 -1
- package/docs/id/intlayer_with_nextjs_14.md +28 -0
- package/docs/id/intlayer_with_nextjs_15.md +28 -0
- package/docs/id/intlayer_with_nextjs_16.md +28 -0
- package/docs/id/intlayer_with_nextjs_no_locale_path.md +1154 -0
- package/docs/id/plugins/sync-json.md +6 -2
- package/docs/it/configuration.md +6 -1
- package/docs/it/dictionary/content_file.md +3 -1
- package/docs/it/intlayer_with_next-i18next.md +0 -1
- package/docs/it/intlayer_with_nextjs_14.md +28 -0
- package/docs/it/intlayer_with_nextjs_15.md +28 -0
- package/docs/it/intlayer_with_nextjs_16.md +28 -0
- package/docs/it/intlayer_with_nextjs_no_locale_path.md +1148 -0
- package/docs/it/plugins/sync-json.md +6 -2
- package/docs/ja/configuration.md +6 -1
- package/docs/ja/dictionary/content_file.md +3 -1
- package/docs/ja/intlayer_with_next-i18next.md +0 -1
- package/docs/ja/intlayer_with_nextjs_14.md +28 -0
- package/docs/ja/intlayer_with_nextjs_15.md +28 -0
- package/docs/ja/intlayer_with_nextjs_16.md +28 -0
- package/docs/ja/intlayer_with_nextjs_no_locale_path.md +1222 -0
- package/docs/ja/plugins/sync-json.md +6 -2
- package/docs/ko/configuration.md +6 -1
- package/docs/ko/dictionary/content_file.md +3 -1
- package/docs/ko/intlayer_with_next-i18next.md +0 -1
- package/docs/ko/intlayer_with_nextjs_14.md +28 -0
- package/docs/ko/intlayer_with_nextjs_15.md +28 -0
- package/docs/ko/intlayer_with_nextjs_16.md +28 -0
- package/docs/ko/intlayer_with_nextjs_no_locale_path.md +1205 -0
- package/docs/ko/plugins/sync-json.md +6 -2
- package/docs/pl/configuration.md +3 -1
- package/docs/pl/dictionary/content_file.md +3 -1
- package/docs/pl/intlayer_with_next-i18next.md +0 -1
- package/docs/pl/intlayer_with_nextjs_14.md +28 -0
- package/docs/pl/intlayer_with_nextjs_15.md +28 -0
- package/docs/pl/intlayer_with_nextjs_16.md +28 -0
- package/docs/pl/intlayer_with_nextjs_no_locale_path.md +1149 -0
- package/docs/pl/plugins/sync-json.md +6 -2
- package/docs/pt/configuration.md +6 -1
- package/docs/pt/dictionary/content_file.md +3 -1
- package/docs/pt/intlayer_with_next-i18next.md +0 -1
- package/docs/pt/intlayer_with_nextjs_14.md +28 -0
- package/docs/pt/intlayer_with_nextjs_15.md +28 -0
- package/docs/pt/intlayer_with_nextjs_16.md +28 -0
- package/docs/pt/intlayer_with_nextjs_no_locale_path.md +1152 -0
- package/docs/pt/plugins/sync-json.md +6 -2
- package/docs/ru/configuration.md +6 -1
- package/docs/ru/dictionary/content_file.md +6 -1
- package/docs/ru/intlayer_with_next-i18next.md +0 -1
- package/docs/ru/intlayer_with_nextjs_14.md +28 -0
- package/docs/ru/intlayer_with_nextjs_15.md +28 -0
- package/docs/ru/intlayer_with_nextjs_16.md +28 -0
- package/docs/ru/intlayer_with_nextjs_no_locale_path.md +1204 -0
- package/docs/ru/plugins/sync-json.md +6 -2
- package/docs/tr/configuration.md +6 -1
- package/docs/tr/dictionary/content_file.md +3 -1
- package/docs/tr/intlayer_with_next-i18next.md +0 -1
- package/docs/tr/intlayer_with_nextjs_14.md +28 -0
- package/docs/tr/intlayer_with_nextjs_15.md +28 -0
- package/docs/tr/intlayer_with_nextjs_16.md +28 -0
- package/docs/tr/intlayer_with_nextjs_no_locale_path.md +1159 -0
- package/docs/tr/plugins/sync-json.md +6 -2
- 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/compiler.md +133 -0
- package/docs/uk/component_i18n.md +194 -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_14.md +1646 -0
- package/docs/uk/intlayer_with_nextjs_15.md +1910 -0
- package/docs/uk/intlayer_with_nextjs_16.md +1763 -0
- package/docs/uk/intlayer_with_nextjs_no_locale_path.md +1159 -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_native+expo.md +715 -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/getConfiguration.md +145 -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/uk/vs_code_extension.md +133 -0
- package/docs/vi/configuration.md +6 -1
- package/docs/vi/dictionary/content_file.md +6 -1
- package/docs/vi/intlayer_with_next-i18next.md +0 -1
- package/docs/vi/intlayer_with_nextjs_14.md +28 -0
- package/docs/vi/intlayer_with_nextjs_15.md +28 -0
- package/docs/vi/intlayer_with_nextjs_16.md +28 -0
- package/docs/vi/intlayer_with_nextjs_no_locale_path.md +1151 -0
- package/docs/vi/plugins/sync-json.md +6 -2
- package/docs/zh/configuration.md +6 -1
- package/docs/zh/dictionary/content_file.md +6 -1
- package/docs/zh/intlayer_with_next-i18next.md +0 -1
- package/docs/zh/intlayer_with_nextjs_14.md +28 -0
- package/docs/zh/intlayer_with_nextjs_15.md +28 -0
- package/docs/zh/intlayer_with_nextjs_16.md +28 -0
- package/docs/zh/intlayer_with_nextjs_no_locale_path.md +1206 -0
- package/docs/zh/plugins/sync-json.md +9 -5
- package/frequent_questions/ar/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/ar/error-vite-env-only.md +77 -0
- package/frequent_questions/de/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/de/error-vite-env-only.md +77 -0
- package/frequent_questions/en/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/en/error-vite-env-only.md +77 -0
- package/frequent_questions/en-GB/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/en-GB/error-vite-env-only.md +77 -0
- package/frequent_questions/es/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/es/error-vite-env-only.md +76 -0
- package/frequent_questions/fr/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/fr/error-vite-env-only.md +77 -0
- package/frequent_questions/hi/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/hi/error-vite-env-only.md +77 -0
- package/frequent_questions/id/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/id/error-vite-env-only.md +77 -0
- package/frequent_questions/it/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/it/error-vite-env-only.md +77 -0
- package/frequent_questions/ja/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/ja/error-vite-env-only.md +77 -0
- package/frequent_questions/ko/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/ko/error-vite-env-only.md +77 -0
- package/frequent_questions/pl/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/pl/error-vite-env-only.md +77 -0
- package/frequent_questions/pt/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/pt/error-vite-env-only.md +77 -0
- package/frequent_questions/ru/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/ru/error-vite-env-only.md +77 -0
- package/frequent_questions/tr/SSR_Next_no_[locale].md +1 -1
- 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/SSR_Next_no_[locale].md +1 -1
- package/frequent_questions/vi/error-vite-env-only.md +77 -0
- package/frequent_questions/zh/SSR_Next_no_[locale].md +1 -1
- 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 +6 -6
- package/src/generated/blog.entry.ts +29 -0
- package/src/generated/docs.entry.ts +119 -0
- package/src/generated/frequentQuestions.entry.ts +35 -0
- package/src/generated/legal.entry.ts +2 -0
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2025-08-23
|
|
3
|
+
updatedAt: 2025-09-29
|
|
4
|
+
title: next-i18next проти next-intl проти Intlayer
|
|
5
|
+
description: Порівняйте next-i18next з next-intl та Intlayer для інтернаціоналізації (i18n) додатку Next.js
|
|
6
|
+
keywords:
|
|
7
|
+
- next-intl
|
|
8
|
+
- next-i18next
|
|
9
|
+
- Intlayer
|
|
10
|
+
- Інтернаціоналізація
|
|
11
|
+
- Блог
|
|
12
|
+
- Next.js
|
|
13
|
+
- JavaScript
|
|
14
|
+
- React
|
|
15
|
+
slugs:
|
|
16
|
+
- blog
|
|
17
|
+
- next-i18next-vs-next-intl-vs-intlayer
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# next-i18next VS next-intl VS intlayer | Інтернаціоналізація Next.js (i18n)
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
Розглянемо схожості та відмінності між трьома варіантами i18n для Next.js: next-i18next, next-intl та Intlayer.
|
|
25
|
+
|
|
26
|
+
Це не повний посібник. Це порівняння, яке допоможе вам обрати.
|
|
27
|
+
|
|
28
|
+
Ми зосереджуємось на **Next.js 13+ App Router** (з **React Server Components**) і оцінюємо:
|
|
29
|
+
|
|
30
|
+
<TOC/>
|
|
31
|
+
|
|
32
|
+
> **tl;dr**: Усі три можуть локалізувати Next.js додаток. Якщо вам потрібні **контент, прив'язаний до компонентів**, **строгі TypeScript-типи**, **перевірка відсутніх ключів на етапі збірки**, **tree-shaken словники** та **першокласна підтримка App Router + SEO-хелперів**, **Intlayer** — найповніший, сучасний вибір.
|
|
33
|
+
>
|
|
34
|
+
> Частою помилкою розробників є думка, що `next-intl` — це версія `react-intl` для Next.js. Це не так: `next-intl` підтримується [Amann](https://github.com/amannn), тоді як `react-intl` підтримується [FormatJS](https://github.com/formatjs/formatjs).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Коротко
|
|
39
|
+
|
|
40
|
+
- **next-intl** - Легкий, простий механізм форматування повідомлень із надійною підтримкою Next.js. Центральні каталоги є поширеними; DX простий, але безпека та масштабне обслуговування переважно залишаються вашою відповідальністю.
|
|
41
|
+
- **next-i18next** - i18next у варіанті для Next.js. Зріла екосистема та функціональність через плагіни (наприклад, ICU), але конфігурація може бути громіздкою, а каталоги мають тенденцію централізуватися в міру зростання проєкту.
|
|
42
|
+
- **Intlayer** - Компонентно-орієнтована модель контенту для Next.js, **строга типізація TS**, **перевірки на етапі збірки**, **tree-shaking**, **вбудовані middleware і SEO-помічники**, опційний **візуальний редактор/CMS** та **AI-підтримані переклади**.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
| Бібліотека | Зірки GitHub | Загальна кількість комітів | Останній коміт | Перша версія | Версія NPM | Завантаження з NPM |
|
|
47
|
+
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| `aymericzip/intlayer` | [](https://github.com/aymericzip/intlayer/stargazers) | [](https://github.com/aymericzip/intlayer/commits) | [](https://github.com/aymericzip/intlayer/commits) | квітень 2024 | [](https://www.npmjs.com/package/intlayer) | [](https://www.npmjs.com/package/intlayer) |
|
|
49
|
+
| `amannn/next-intl` | [](https://github.com/amannn/next-intl/stargazers) | [](https://github.com/amannn/next-intl/commits) | [](https://github.com/amannn/next-intl/commits) | Листопад 2020 | [](https://www.npmjs.com/package/next-intl) | [](https://www.npmjs.com/package/next-intl) |
|
|
50
|
+
| `i18next/i18next` | [](https://github.com/i18next/i18next/stargazers) | [](https://github.com/i18next/i18next/commits) | [](https://github.com/i18next/i18next/commits) | січ 2012 | [](https://www.npmjs.com/package/i18next) | [](https://www.npmjs.com/package/i18next) |
|
|
51
|
+
| `i18next/next-i18next` | [](https://github.com/i18next/next-i18next/stargazers) | [](https://github.com/i18next/next-i18next/commits) | [](https://github.com/i18next/next-i18next/commits) | Листопад 2018 | [](https://www.npmjs.com/package/next-i18next) | [](https://www.npmjs.com/package/next-i18next) |
|
|
52
|
+
|
|
53
|
+
> Значки оновлюються автоматично. Знімки можуть змінюватися з часом.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Порівняння функцій бок-о-бок (орієнтовано на Next.js)
|
|
58
|
+
|
|
59
|
+
| Функція | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
|
|
60
|
+
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| **Переклади поруч з компонентами** | ✅ Так, вміст розміщено поряд із кожним компонентом | ❌ Ні | ❌ Ні |
|
|
62
|
+
| **Інтеграція з TypeScript** | ✅ Просунута, автогенеровані суворі типи | ✅ Добре | ⚠️ Базовий |
|
|
63
|
+
| **Виявлення відсутніх перекладів** | ✅ Підсвітка помилок TypeScript та помилки/попередження на етапі збірки | ⚠️ fallback під час виконання | ⚠️ fallback під час виконання |
|
|
64
|
+
| **Багатий контент (JSX/Markdown/components)** | ✅ Пряма підтримка | ❌ Не призначено для багатих вузлів | ⚠️ Обмежено |
|
|
65
|
+
| **Переклад із використанням ШІ** | ✅ Так, підтримує кількох постачальників ШІ. Можна використовувати з власними API-ключами. Бере до уваги контекст вашого додатка та обсяг контенту | ❌ Ні | ❌ Ні |
|
|
66
|
+
| **Візуальний редактор** | ✅ Так, локальний Visual Editor + опційний CMS; можливість винести контент codebase; може вбудовуватись | ❌ Ні / доступно через зовнішні платформи локалізації | ❌ Ні / доступно через зовнішні платформи локалізації |
|
|
67
|
+
| **Локалізована маршрутизація** | ✅ Так, підтримує локалізовані шляхи з коробки (працює з Next.js та Vite) | ✅ Вбудовано, App Router підтримує сегмент `[locale]` | ✅ Вбудовано |
|
|
68
|
+
| **Динамічна генерація маршрутів** | ✅ Так | ✅ Так | ✅ Так |
|
|
69
|
+
| **Плюралізація** | ✅ Шаблони на основі перелічення | ✅ Добре | ✅ Добре |
|
|
70
|
+
| **Форматування (дати, числа, валюти)** | ✅ Оптимізовані форматери (Intl під капотом) | ✅ Добре (Intl helpers) | ✅ Добре (Intl helpers) |
|
|
71
|
+
| **Формат вмісту** | ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml WIP) | ✅ .json, .js, .ts | ⚠️ .json |
|
|
72
|
+
| **Підтримка ICU** | ⚠️ У розробці (WIP) | ✅ Так | ⚠️ Через плагін (`i18next-icu`) |
|
|
73
|
+
| **SEO-інструменти (hreflang, sitemap)** | ✅ Вбудовані інструменти: помічники для sitemap, robots.txt та метаданих | ✅ Добре | ✅ Добре |
|
|
74
|
+
| **Екосистема / Спільнота** | ⚠️ Менша, але швидко зростає та реактивна | ✅ Добре | ✅ Добре |
|
|
75
|
+
| **Server-side Rendering & Server Components** | ✅ Так, оптимізовано для SSR / React Server Components | ⚠️ Підтримується на рівні сторінки, але потрібно передавати t-функції через дерево компонентів для дочірніх Server Components | ⚠️ Підтримується на рівні сторінки, але потрібно передавати t-функції через дерево компонентів для дочірніх Server Components |
|
|
76
|
+
| **Tree-shaking (завантаження лише використовуваного контенту)** | ✅ Так, по компоненту під час збірки через плагіни Babel/SWC | ⚠️ Частково | ⚠️ Частково |
|
|
77
|
+
| **Ліниве завантаження** | ✅ Так, за локаллю / за словником | ✅ Так (за маршрутом/за локаллю), потребує управління просторами імен | ✅ Так (за маршрутом/за локаллю), потребує управління просторами імен |
|
|
78
|
+
| **Очищення невикористовуваного вмісту** | ✅ Так, для кожного словника під час збірки | ❌ Ні, може керуватися вручну за допомогою управління просторами імен | ❌ Ні, може керуватися вручну за допомогою управління просторами імен |
|
|
79
|
+
| **Управління великими проєктами** | ✅ Заохочує модульність, підходить для дизайн-систем | ✅ Модульний за умови налаштування | ✅ Модульний за умови налаштування |
|
|
80
|
+
| **Тестування відсутніх перекладів (CLI/CI)** | ✅ CLI: `npx intlayer content test` (аудит, придатний для CI) | ⚠️ Не вбудовано; у документації радять `npx @lingual/i18n-check` | ⚠️ Не вбудовано; покладається на інструменти i18next або runtime `saveMissing` |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Вступ
|
|
85
|
+
|
|
86
|
+
Next.js надає вбудовану підтримку інтернаціоналізованої маршрутизації (наприклад, locale segments). Але ця функція сама по собі не виконує переклади. Вам все одно потрібна бібліотека, щоб відображати локалізований контент користувачам.
|
|
87
|
+
|
|
88
|
+
Існує багато i18n-бібліотек, але в екосистемі Next.js сьогодні три з них набувають популярності: next-i18next, next-intl та Intlayer.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Архітектура та масштабованість
|
|
93
|
+
|
|
94
|
+
/// **next-intl / next-i18next**: За замовчуванням використовують **централізовані каталоги** для кожної локалі (плюс **namespaces** у i18next). Добре працює на початкових етапах, але часто перетворюється на велику спільну поверхню зі зростаючою зв’язністю та хаотичністю ключів.
|
|
95
|
+
/// **Intlayer**: Заохочує **per-component** (або **per-feature**) словники, **розміщені поруч** із кодом, який вони обслуговують. Це зменшує когнітивне навантаження, полегшує дублювання/міграцію UI-блоків і знижує конфлікти між командами. Невикористаний контент природно легше помітити й видалити.
|
|
96
|
+
///
|
|
97
|
+
/// **Чому це важливо:** У великих кодових базах або при роботі з design-system **модульний контент** масштабується краще, ніж монолітні каталоги.
|
|
98
|
+
///
|
|
99
|
+
/// ---
|
|
100
|
+
///
|
|
101
|
+
/// ## Розміри бандлів та залежності
|
|
102
|
+
|
|
103
|
+
Після збірки застосунку, bundle — це JavaScript, який браузер завантажує для відображення сторінки. Тому розмір bundle важливий для продуктивності застосунку.
|
|
104
|
+
|
|
105
|
+
У контексті багатомовного застосунку важливі два складники bundle:
|
|
106
|
+
|
|
107
|
+
- Код застосунку
|
|
108
|
+
- Контент, який завантажує браузер
|
|
109
|
+
|
|
110
|
+
## Код застосунку
|
|
111
|
+
|
|
112
|
+
Важливість коду застосунку в цьому випадку мінімальна. Усі три рішення є tree-shakable, тобто невикористані частини коду не включаються до bundle.
|
|
113
|
+
|
|
114
|
+
Нижче — порівняння розміру JavaScript-бандла, який завантажує браузер для багатомовного застосунку з трьома рішеннями.
|
|
115
|
+
|
|
116
|
+
Якщо в застосунку нам не потрібен жоден formatter, список експортованих функцій після tree-shaking буде:
|
|
117
|
+
|
|
118
|
+
- **next-intlayer**: `useIntlayer`, `useLocale`, `NextIntlClientProvider`, (Розмір бандла 180.6 kB -> 78.6 kB (gzip))
|
|
119
|
+
- **next-intl**: `useTranslations`, `useLocale`, `NextIntlClientProvider`, (Розмір бандла 101.3 kB -> 31.4 kB (gzip))
|
|
120
|
+
- **next-i18next**: `useTranslation`, `useI18n`, `I18nextProvider`, (Розмір бандла 80.7 kB -> 25.5 kB (gzip))
|
|
121
|
+
|
|
122
|
+
Ці функції — лише обгортки над React context/state, тому загальний вплив бібліотеки i18n на розмір бандла мінімальний.
|
|
123
|
+
|
|
124
|
+
> Intlayer трохи більший, ніж `next-intl` та `next-i18next`, оскільки він містить більше логіки в функції `useIntlayer`. Це пов'язано з інтеграцією markdown та `intlayer-editor`.
|
|
125
|
+
|
|
126
|
+
## Контент та переклади
|
|
127
|
+
|
|
128
|
+
Цей аспект часто ігнорують розробники, але розгляньмо випадок застосунку, що складається з 10 сторінок у 10 мовах. Припустимо для спрощення розрахунків, що кожна сторінка містить 100% унікального контенту (на практиці багато контенту дублюється між сторінками, наприклад заголовок сторінки, header, footer тощо).
|
|
129
|
+
|
|
130
|
+
Користувач, який хоче відвідати сторінку `/fr/about`, завантажить контент однієї сторінки певною мовою. Ігнорування оптимізації контенту призведе до зайвого завантаження 8,200% `((1 + (((10 pages - 1) × (10 languages - 1)))) × 100)` контенту застосунку. Чи бачите проблему? Навіть якщо цей контент — лише текст, і хоча ви, ймовірно, більше зосереджені на оптимізації зображень сайту, ви передаєте непотрібний контент по всьому світу й змушуєте пристрої користувачів обробляти його даремно.
|
|
131
|
+
|
|
132
|
+
Дві важливі проблеми:
|
|
133
|
+
|
|
134
|
+
- **Розбиття за маршрутом:**
|
|
135
|
+
|
|
136
|
+
> Якщо я на сторінці `/about`, я не хочу завантажувати вміст сторінки `/home`
|
|
137
|
+
|
|
138
|
+
- **Розбиття за локаллю:**
|
|
139
|
+
|
|
140
|
+
> Якщо я на сторінці `/fr/about`, я не хочу завантажувати вміст сторінки `/en/about`
|
|
141
|
+
|
|
142
|
+
Знову ж таки, усі три рішення усвідомлюють ці проблеми та дозволяють керувати цими оптимізаціями. Різниця між ними — це DX (Developer Experience).
|
|
143
|
+
|
|
144
|
+
`next-intl` та `next-i18next` використовують централізований підхід для керування перекладами, дозволяючи розбивати JSON за локаллю й на підфайли. У `next-i18next` ми називаємо JSON-файли 'namespaces'; `next-intl` дозволяє оголошувати messages. В `intlayer` ми називаємо JSON-файли 'dictionaries'.
|
|
145
|
+
|
|
146
|
+
- У випадку `next-intl`, як і у `next-i18next`, контент завантажується на рівні сторінки/макета, а потім цей контент передається в context provider. Це означає, що розробник має вручну керувати JSON-файлами, які будуть завантажені для кожної сторінки.
|
|
147
|
+
|
|
148
|
+
> На практиці це означає, що розробники часто пропускають цю оптимізацію, віддаючи перевагу завантаженню всього контенту в context provider сторінки заради простоти.
|
|
149
|
+
|
|
150
|
+
- У випадку `intlayer`, увесь контент завантажується в застосунок. Далі плагін (`@intlayer/babel` / `@intlayer/swc`) піклується про оптимізацію bundle, завантажуючи лише той контент, що використовується на сторінці. Отже, розробнику не потрібно вручну керувати словниками, які будуть завантажені. Це дозволяє кращу оптимізацію, кращу maintainability та зменшує час розробки.
|
|
151
|
+
|
|
152
|
+
У міру зростання додатка (особливо коли над ним працюють кілька розробників) часто забувають видаляти з JSON-файлів контент, який більше не використовується.
|
|
153
|
+
|
|
154
|
+
> Примітка: у всіх випадках завантажуються всі JSON-файли (next-intl, next-i18next, intlayer).
|
|
155
|
+
|
|
156
|
+
Ось чому підхід Intlayer є більш продуктивним: якщо компонент більше не використовується, його dictionary не завантажується в bundle.
|
|
157
|
+
|
|
158
|
+
Також важливо, як бібліотека обробляє fallback. Припустімо, що додаток за замовчуванням англійською, і користувач відкриває сторінку /fr/about. Якщо переклади французькою відсутні, буде використано англійський fallback.
|
|
159
|
+
|
|
160
|
+
У випадку `next-intl` та `next-i18next` бібліотека вимагає завантаження JSON, пов'язаних не лише з поточною локаллю, але й з fallback-локаллю. Тому, якщо весь контент перекладено, кожна сторінка завантажуватиме 100% непотрібного вмісту. **На відміну від цього, `intlayer` обробляє fallback під час побудови словників. Отже, кожна сторінка завантажує лише використовуваний вміст.**
|
|
161
|
+
|
|
162
|
+
> Примітка: Щоб оптимізувати бандл за допомогою `intlayer`, потрібно встановити опцію `importMode: 'dynamic'` у файлі `intlayer.config.ts`. Також переконайтесь, що плагін `@intlayer/babel` / `@intlayer/swc` встановлено (встановлюється за замовчуванням при використанні `vite-intlayer`).
|
|
163
|
+
|
|
164
|
+
Нижче приклад впливу оптимізації розміру бандла за допомогою `intlayer` у застосунку на vite + react:
|
|
165
|
+
|
|
166
|
+
| Оптимізований бандл | Неоптимізований бандл |
|
|
167
|
+
| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
168
|
+
|  |  |
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## TypeScript і безпека
|
|
173
|
+
|
|
174
|
+
<Columns>
|
|
175
|
+
<Column>
|
|
176
|
+
|
|
177
|
+
**next-i18next**
|
|
178
|
+
|
|
179
|
+
- Базові типи для хуків. **строга типізація ключів вимагає додаткових інструментів/конфігурації**.
|
|
180
|
+
|
|
181
|
+
</Column>
|
|
182
|
+
<Column>
|
|
183
|
+
|
|
184
|
+
**next-intl**
|
|
185
|
+
|
|
186
|
+
- Надійна підтримка TypeScript, але **ключі за замовчуванням не є строго типізованими**. вам доведеться підтримувати шаблони безпеки вручну.
|
|
187
|
+
|
|
188
|
+
</Column>
|
|
189
|
+
<Column>
|
|
190
|
+
|
|
191
|
+
**intlayer**
|
|
192
|
+
|
|
193
|
+
- **Генерує строгі типи** з вашого контенту. **Автодоповнення IDE** та **помилки на етапі компіляції** виявляють опечатки та відсутні ключі перед деплоєм.
|
|
194
|
+
|
|
195
|
+
</Column>
|
|
196
|
+
</Columns>
|
|
197
|
+
|
|
198
|
+
**Чому це важливо:** Сильна типізація зміщує помилки вліво (CI/build) замість вправо (runtime).
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Обробка відсутніх перекладів
|
|
203
|
+
|
|
204
|
+
<Columns>
|
|
205
|
+
<Column>
|
|
206
|
+
|
|
207
|
+
**next-i18next**
|
|
208
|
+
|
|
209
|
+
- Залежить від **runtime fallbacks**. Збірка не завершується з помилкою.
|
|
210
|
+
|
|
211
|
+
</Column>
|
|
212
|
+
<Column>
|
|
213
|
+
|
|
214
|
+
**next-intl**
|
|
215
|
+
|
|
216
|
+
- Залежить від **runtime fallbacks**. Збірка не завершується з помилкою.
|
|
217
|
+
|
|
218
|
+
</Column>
|
|
219
|
+
<Column>
|
|
220
|
+
|
|
221
|
+
**intlayer**
|
|
222
|
+
|
|
223
|
+
- **Виявлення під час збірки** з **попередженнями/помилками** для відсутніх локалей або ключів.
|
|
224
|
+
|
|
225
|
+
</Column>
|
|
226
|
+
</Columns>
|
|
227
|
+
|
|
228
|
+
**Чому це важливо:** Виявлення прогалин під час збірки запобігає появі 'undefined' рядків у продакшені.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Маршрутизація, middleware та стратегія URL
|
|
233
|
+
|
|
234
|
+
<Columns>
|
|
235
|
+
<Column>
|
|
236
|
+
|
|
237
|
+
**next-i18next**
|
|
238
|
+
|
|
239
|
+
- Дозволяє локалізовану маршрутизацію. Але middleware не вбудовано.
|
|
240
|
+
|
|
241
|
+
</Column>
|
|
242
|
+
<Column>
|
|
243
|
+
|
|
244
|
+
**next-intl**
|
|
245
|
+
|
|
246
|
+
- Дозволяє локалізовану маршрутизацію.
|
|
247
|
+
- Надає middleware.
|
|
248
|
+
|
|
249
|
+
</Column>
|
|
250
|
+
<Column>
|
|
251
|
+
|
|
252
|
+
**intlayer**
|
|
253
|
+
|
|
254
|
+
- Дозволяє локалізовану маршрутизацію.
|
|
255
|
+
- Надає middleware.
|
|
256
|
+
|
|
257
|
+
</Column>
|
|
258
|
+
</Columns>
|
|
259
|
+
|
|
260
|
+
**Чому це важливо:** Допомагає для SEO, покращує видимість і досвід користувача.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Узгодження із Server Components (RSC)
|
|
265
|
+
|
|
266
|
+
<Columns>
|
|
267
|
+
<Column>
|
|
268
|
+
|
|
269
|
+
**next-i18next**
|
|
270
|
+
|
|
271
|
+
- Підтримує server components для сторінок і layout.
|
|
272
|
+
- Не надавати синхронний API для дочірніх серверних компонент.
|
|
273
|
+
|
|
274
|
+
</Column>
|
|
275
|
+
<Column>
|
|
276
|
+
|
|
277
|
+
**next-intl**
|
|
278
|
+
|
|
279
|
+
- Підтримує сторінкові та layout серверні компоненти.
|
|
280
|
+
- Не надавати синхронний API для дочірніх серверних компонент.
|
|
281
|
+
|
|
282
|
+
</Column>
|
|
283
|
+
<Column>
|
|
284
|
+
|
|
285
|
+
**intlayer**
|
|
286
|
+
|
|
287
|
+
- Підтримує сторінкові та layout серверні компоненти.
|
|
288
|
+
- Надає синхронний API для дочірніх серверних компонент.
|
|
289
|
+
|
|
290
|
+
</Column>
|
|
291
|
+
</Columns>
|
|
292
|
+
|
|
293
|
+
**Чому це важливо:** Підтримка серверних компонент — ключова особливість Next.js 13+, що покращує продуктивність. Передача через props `locale` або функції `t` від батьківської до дочірньої серверної компоненти робить ваші компоненти менш повторно використовуваними.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Інтеграція з платформами локалізації (TMS)
|
|
298
|
+
|
|
299
|
+
Великі організації часто покладаються на системи керування перекладами (TMS), такі як **Crowdin**, **Phrase**, **Lokalise**, **Localizely** або **Localazy**.
|
|
300
|
+
|
|
301
|
+
- **Чому компаніям це важливо**
|
|
302
|
+
- **Співпраця та ролі**: у процесі задіяні різні учасники: розробники, продакт-менеджери, перекладачі, рецензенти, маркетингові команди.
|
|
303
|
+
- **Масштаб і ефективність**: безперервна локалізація, перегляд у контексті.
|
|
304
|
+
|
|
305
|
+
- **next-intl / next-i18next**
|
|
306
|
+
- Зазвичай використовують **централізовані JSON-каталоги**, тож експорт/імпорт у TMS є простим.
|
|
307
|
+
- Високорозвинені екосистеми та приклади/інтеграції для вказаних платформ.
|
|
308
|
+
|
|
309
|
+
- **Intlayer**
|
|
310
|
+
- Заохочує **децентралізовані словники на рівні компонентів** та підтримує **TypeScript/TSX/JS/JSON/MD** контент.
|
|
311
|
+
- Це покращує модульність у коді, але може ускладнити plug‑and‑play інтеграцію з TMS, коли інструмент очікує централізовані плоскі JSON-файли.
|
|
312
|
+
- Intlayer пропонує альтернативи: **AI‑асистовані переклади** (використовуючи ваші власні ключі провайдера), **Visual Editor/CMS**, та робочі процеси **CLI/CI** для виявлення й попереднього заповнення прогалин.
|
|
313
|
+
|
|
314
|
+
> Примітка: `next-intl` та `i18next` також підтримують TypeScript-каталоги. Якщо ваша команда зберігає повідомлення в файлах `.ts` або децентралізує їх за функціоналом, ви можете зіштовхнутися з подібними проблемами інтеграції з TMS. Однак багато налаштувань `next-intl` залишаються централізованими в папці `locales/`, що трохи простіше перетворити в JSON для TMS.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Досвід розробника
|
|
319
|
+
|
|
320
|
+
У цій частині виконується детальне порівняння трьох рішень. Замість розгляду простих випадків, як описано в розділі 'Початок роботи' для кожного рішення, ми розглянемо реальний сценарій використання, ближчий до справжнього проєкту.
|
|
321
|
+
|
|
322
|
+
### Структура додатку
|
|
323
|
+
|
|
324
|
+
Структура додатку важлива для забезпечення доброї підтримуваності вашого codebase.
|
|
325
|
+
|
|
326
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
327
|
+
|
|
328
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
.
|
|
332
|
+
├── i18n.config.ts
|
|
333
|
+
└── src
|
|
334
|
+
├── locales
|
|
335
|
+
│ ├── en
|
|
336
|
+
│ │ ├── common.json
|
|
337
|
+
│ │ └── about.json
|
|
338
|
+
│ └── fr
|
|
339
|
+
│ ├── common.json
|
|
340
|
+
│ └── about.json
|
|
341
|
+
├── app
|
|
342
|
+
│ ├── i18n
|
|
343
|
+
│ │ └── server.ts
|
|
344
|
+
│ └── [locale]
|
|
345
|
+
│ ├── layout.tsx
|
|
346
|
+
│ └── about.tsx
|
|
347
|
+
└── components
|
|
348
|
+
├── I18nProvider.tsx
|
|
349
|
+
├── ClientComponent.tsx
|
|
350
|
+
└── ServerComponent.tsx
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
</TabItem>
|
|
354
|
+
<TabItem label="next-intl" value="next-intl">
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
.
|
|
358
|
+
├── i18n.ts
|
|
359
|
+
├── locales
|
|
360
|
+
│ ├── en
|
|
361
|
+
│ │ ├── home.json
|
|
362
|
+
│ │ └── navbar.json
|
|
363
|
+
│ ├── fr
|
|
364
|
+
│ │ ├── home.json
|
|
365
|
+
│ │ └── navbar.json
|
|
366
|
+
│ └── es
|
|
367
|
+
│ ├── home.json
|
|
368
|
+
│ └── navbar.json
|
|
369
|
+
└── src
|
|
370
|
+
├── middleware.ts
|
|
371
|
+
├── app
|
|
372
|
+
│ ├── i18n
|
|
373
|
+
│ │ └── server.ts
|
|
374
|
+
│ └── [locale]
|
|
375
|
+
│ └── home.tsx
|
|
376
|
+
└── components
|
|
377
|
+
└── Navbar
|
|
378
|
+
└── index.tsx
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
</TabItem>
|
|
382
|
+
<TabItem label="intlayer" value="intlayer">
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
.
|
|
386
|
+
├── intlayer.config.ts
|
|
387
|
+
└── src
|
|
388
|
+
├── middleware.ts
|
|
389
|
+
├── app
|
|
390
|
+
│ └── [locale]
|
|
391
|
+
│ ├── layout.tsx
|
|
392
|
+
│ └── home
|
|
393
|
+
│ ├── index.tsx
|
|
394
|
+
│ └── index.content.ts
|
|
395
|
+
└── components
|
|
396
|
+
└── Navbar
|
|
397
|
+
├── index.tsx
|
|
398
|
+
└── index.content.ts
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
</TabItem>
|
|
402
|
+
</Tab>
|
|
403
|
+
|
|
404
|
+
#### Порівняння
|
|
405
|
+
|
|
406
|
+
- **next-intl / next-i18next**: Централізовані каталоги (JSON; namespaces/messages). Чітка структура, добре інтегрується з платформами перекладів, але може призводити до більшої кількості змін у різних файлах у міру зростання застосунку.
|
|
407
|
+
- **Intlayer**: Словники `.content.{ts|js|json}` для кожного компонента, розміщені поруч із компонентами. Полегшує повторне використання компонентів і локальне розуміння; додає файли та покладається на інструменти на етапі збірки.
|
|
408
|
+
|
|
409
|
+
#### Налаштування та завантаження контенту
|
|
410
|
+
|
|
411
|
+
Як зазначалося раніше, потрібно оптимізувати спосіб імпортування кожного JSON-файлу у ваш код.
|
|
412
|
+
Важливо, як бібліотека обробляє завантаження контенту.
|
|
413
|
+
|
|
414
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
415
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
416
|
+
|
|
417
|
+
```ts fileName="i18n.config.ts"
|
|
418
|
+
export const locales = ["en", "fr"] as const;
|
|
419
|
+
export type Locale = (typeof locales)[number];
|
|
420
|
+
|
|
421
|
+
export const defaultLocale: Locale = "en";
|
|
422
|
+
|
|
423
|
+
export const rtlLocales = ["ar", "he", "fa", "ur"] as const;
|
|
424
|
+
export const isRtl = (locale: string) =>
|
|
425
|
+
(rtlLocales as readonly string[]).includes(locale);
|
|
426
|
+
|
|
427
|
+
export function localizedPath(locale: string, path: string) {
|
|
428
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const ORIGIN = "https://example.com";
|
|
432
|
+
export function abs(locale: string, path: string) {
|
|
433
|
+
return ORIGIN + localizedPath(locale, path);
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
```ts fileName="src/app/i18n/server.ts"
|
|
438
|
+
import { createInstance } from "i18next";
|
|
439
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
440
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
441
|
+
import { defaultLocale } from "@/i18n.config";
|
|
442
|
+
|
|
443
|
+
// Завантаження JSON-ресурсів із src/locales/<locale>/<namespace>.json
|
|
444
|
+
const backend = resourcesToBackend(
|
|
445
|
+
(locale: string, namespace: string) =>
|
|
446
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
export async function initI18next(
|
|
450
|
+
locale: string,
|
|
451
|
+
namespaces: string[] = ["common"]
|
|
452
|
+
) {
|
|
453
|
+
const i18n = createInstance();
|
|
454
|
+
await i18n
|
|
455
|
+
.use(initReactI18next)
|
|
456
|
+
.use(backend)
|
|
457
|
+
.init({
|
|
458
|
+
lng: locale,
|
|
459
|
+
fallbackLng: defaultLocale,
|
|
460
|
+
ns: namespaces,
|
|
461
|
+
defaultNS: "common",
|
|
462
|
+
interpolation: { escapeValue: false },
|
|
463
|
+
react: { useSuspense: false },
|
|
464
|
+
});
|
|
465
|
+
return i18n;
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```tsx fileName="src/components/I18nProvider.tsx"
|
|
470
|
+
"use client";
|
|
471
|
+
|
|
472
|
+
import * as React from "react";
|
|
473
|
+
import { I18nextProvider } from "react-i18next";
|
|
474
|
+
import { createInstance } from "i18next";
|
|
475
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
476
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
477
|
+
import { defaultLocale } from "@/i18n.config";
|
|
478
|
+
|
|
479
|
+
const backend = resourcesToBackend(
|
|
480
|
+
(locale: string, namespace: string) =>
|
|
481
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
type Props = {
|
|
485
|
+
locale: string;
|
|
486
|
+
namespaces?: string[];
|
|
487
|
+
resources?: Record<string, any>; // { ns: пакет }
|
|
488
|
+
children: React.ReactNode;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
export default function I18nProvider({
|
|
492
|
+
locale,
|
|
493
|
+
namespaces = ["common"],
|
|
494
|
+
resources,
|
|
495
|
+
children,
|
|
496
|
+
}: Props) {
|
|
497
|
+
const [i18n] = React.useState(() => {
|
|
498
|
+
const i = createInstance();
|
|
499
|
+
|
|
500
|
+
i.use(initReactI18next)
|
|
501
|
+
.use(backend)
|
|
502
|
+
.init({
|
|
503
|
+
lng: locale,
|
|
504
|
+
fallbackLng: defaultLocale,
|
|
505
|
+
ns: namespaces,
|
|
506
|
+
resources: resources ? { [locale]: resources } : undefined,
|
|
507
|
+
defaultNS: "common",
|
|
508
|
+
interpolation: { escapeValue: false },
|
|
509
|
+
react: { useSuspense: false },
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return i;
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
520
|
+
import type { ReactNode } from "react";
|
|
521
|
+
import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";
|
|
522
|
+
|
|
523
|
+
export const dynamicParams = false;
|
|
524
|
+
|
|
525
|
+
export function generateStaticParams() {
|
|
526
|
+
return locales.map((locale) => ({ locale }));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export default function LocaleLayout({
|
|
530
|
+
children,
|
|
531
|
+
params,
|
|
532
|
+
}: {
|
|
533
|
+
children: ReactNode;
|
|
534
|
+
params: { locale: string };
|
|
535
|
+
}) {
|
|
536
|
+
const locale: Locale = (locales as readonly string[]).includes(params.locale)
|
|
537
|
+
? (params.locale as any)
|
|
538
|
+
: defaultLocale;
|
|
539
|
+
|
|
540
|
+
const dir = isRtl(locale) ? "rtl" : "ltr";
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<html lang={locale} dir={dir}>
|
|
544
|
+
<body>{children}</body>
|
|
545
|
+
</html>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
```tsx fileName="src/app/[locale]/about.tsx"
|
|
551
|
+
import I18nProvider from "@/components/I18nProvider";
|
|
552
|
+
import { initI18next } from "@/app/i18n/server";
|
|
553
|
+
import type { Locale } from "@/i18n.config";
|
|
554
|
+
import ClientComponent from "@/components/ClientComponent";
|
|
555
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
556
|
+
|
|
557
|
+
// Примусове статичне рендерення сторінки
|
|
558
|
+
export const dynamic = "force-static";
|
|
559
|
+
|
|
560
|
+
export default async function AboutPage({
|
|
561
|
+
params: { locale },
|
|
562
|
+
}: {
|
|
563
|
+
params: { locale: Locale };
|
|
564
|
+
}) {
|
|
565
|
+
const namespaces = ["common", "about"] as const;
|
|
566
|
+
|
|
567
|
+
const i18n = await initI18next(locale, [...namespaces]);
|
|
568
|
+
const tAbout = i18n.getFixedT(locale, "about");
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<I18nProvider locale={locale} namespaces={[...namespaces]}>
|
|
572
|
+
<main>
|
|
573
|
+
<h1>{tAbout("title")}</h1>
|
|
574
|
+
|
|
575
|
+
<ClientComponent />
|
|
576
|
+
<ServerComponent t={tAbout} locale={locale} count={0} />
|
|
577
|
+
</main>
|
|
578
|
+
</I18nProvider>
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
</TabItem>
|
|
584
|
+
<TabItem label="next-intl" value="next-intl">
|
|
585
|
+
|
|
586
|
+
```tsx fileName="src/i18n.ts"
|
|
587
|
+
import { getRequestConfig } from "next-intl/server";
|
|
588
|
+
import { notFound } from "next/navigation";
|
|
589
|
+
|
|
590
|
+
export const locales = ["en", "fr", "es"] as const;
|
|
591
|
+
export const defaultLocale = "en" as const;
|
|
592
|
+
|
|
593
|
+
async function loadMessages(locale: string) {
|
|
594
|
+
// Завантажуйте лише ті неймспейси, які потрібні вашим layout/pages
|
|
595
|
+
const [common, about] = await Promise.all([
|
|
596
|
+
import(`../locales/${locale}/common.json`).then((m) => m.default),
|
|
597
|
+
import(`../locales/${locale}/about.json`).then((m) => m.default),
|
|
598
|
+
]);
|
|
599
|
+
|
|
600
|
+
return { common, about } as const;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export default getRequestConfig(async ({ locale }) => {
|
|
604
|
+
if (!locales.includes(locale as any)) notFound();
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
messages: await loadMessages(locale),
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
613
|
+
import type { ReactNode } from "react";
|
|
614
|
+
import { locales } from "@/i18n";
|
|
615
|
+
import {
|
|
616
|
+
getLocaleDirection,
|
|
617
|
+
unstable_setRequestLocale,
|
|
618
|
+
} from "next-intl/server";
|
|
619
|
+
|
|
620
|
+
export const dynamic = "force-static";
|
|
621
|
+
|
|
622
|
+
export function generateStaticParams() {
|
|
623
|
+
return locales.map((locale) => ({ locale }));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export default async function LocaleLayout({
|
|
627
|
+
children,
|
|
628
|
+
params,
|
|
629
|
+
}: {
|
|
630
|
+
children: ReactNode;
|
|
631
|
+
params: Promise<{ locale: string }>;
|
|
632
|
+
}) {
|
|
633
|
+
const { locale } = await params;
|
|
634
|
+
|
|
635
|
+
// Встановлює активну локаль запиту для цього серверного рендерингу (RSC)
|
|
636
|
+
unstable_setRequestLocale(locale);
|
|
637
|
+
|
|
638
|
+
const dir = getLocaleDirection(locale);
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<html lang={locale} dir={dir}>
|
|
642
|
+
<body>{children}</body>
|
|
643
|
+
</html>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
649
|
+
import { getTranslations, getMessages, getFormatter } from "next-intl/server";
|
|
650
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
651
|
+
import pick from "lodash/pick";
|
|
652
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
653
|
+
import ClientComponentExample from "@/components/ClientComponentExample";
|
|
654
|
+
|
|
655
|
+
export const dynamic = "force-static";
|
|
656
|
+
|
|
657
|
+
export default async function AboutPage({
|
|
658
|
+
params,
|
|
659
|
+
}: {
|
|
660
|
+
params: Promise<{ locale: string }>;
|
|
661
|
+
}) {
|
|
662
|
+
const { locale } = await params;
|
|
663
|
+
|
|
664
|
+
// Повідомлення завантажуються на сервері. Надсилайте на клієнт лише те, що потрібно.
|
|
665
|
+
const messages = await getMessages();
|
|
666
|
+
const clientMessages = pick(messages, ["common", "about"]);
|
|
667
|
+
|
|
668
|
+
// Переклади/форматування, що виконуються виключно на сервері
|
|
669
|
+
const tAbout = await getTranslations("about");
|
|
670
|
+
const tCounter = await getTranslations("about.counter");
|
|
671
|
+
const format = await getFormatter();
|
|
672
|
+
|
|
673
|
+
const initialFormattedCount = format.number(0);
|
|
674
|
+
|
|
675
|
+
return (
|
|
676
|
+
<NextIntlClientProvider locale={locale} messages={clientMessages}>
|
|
677
|
+
<main>
|
|
678
|
+
<h1>{tAbout("title")}</h1>
|
|
679
|
+
<ClientComponentExample />
|
|
680
|
+
<ServerComponent
|
|
681
|
+
formattedCount={initialFormattedCount}
|
|
682
|
+
label={tCounter("label")}
|
|
683
|
+
increment={tCounter("increment")}
|
|
684
|
+
/>
|
|
685
|
+
</main>
|
|
686
|
+
</NextIntlClientProvider>
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
</TabItem>
|
|
692
|
+
<TabItem label="intlayer" value="intlayer">
|
|
693
|
+
|
|
694
|
+
```tsx fileName="intlayer.config.ts"
|
|
695
|
+
import { type IntlayerConfig, Locales } from "intlayer";
|
|
696
|
+
|
|
697
|
+
const config: IntlayerConfig = {
|
|
698
|
+
internationalization: {
|
|
699
|
+
locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
|
|
700
|
+
defaultLocale: Locales.ENGLISH,
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
export default config;
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
708
|
+
import { getHTMLTextDir } from "intlayer";
|
|
709
|
+
import {
|
|
710
|
+
IntlayerClientProvider,
|
|
711
|
+
generateStaticParams,
|
|
712
|
+
type NextLayoutIntlayer,
|
|
713
|
+
} from "next-intlayer";
|
|
714
|
+
|
|
715
|
+
export const dynamic = "force-static";
|
|
716
|
+
|
|
717
|
+
const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {
|
|
718
|
+
const { locale } = await params;
|
|
719
|
+
|
|
720
|
+
return (
|
|
721
|
+
<html lang={locale} dir={getHTMLTextDir(locale)}>
|
|
722
|
+
<body>
|
|
723
|
+
<IntlayerClientProvider locale={locale}>
|
|
724
|
+
{children}
|
|
725
|
+
</IntlayerClientProvider>
|
|
726
|
+
</body>
|
|
727
|
+
</html>
|
|
728
|
+
);
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
export default LandingLayout;
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
735
|
+
import { PageContent } from "@components/PageContent";
|
|
736
|
+
import type { NextPageIntlayer } from "next-intlayer";
|
|
737
|
+
import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";
|
|
738
|
+
import { ClientComponent, ServerComponent } from "@components";
|
|
739
|
+
|
|
740
|
+
const LandingPage: NextPageIntlayer = async ({ params }) => {
|
|
741
|
+
const { locale } = await params;
|
|
742
|
+
const { title } = useIntlayer("about", locale);
|
|
743
|
+
|
|
744
|
+
return (
|
|
745
|
+
<IntlayerServerProvider locale={locale}>
|
|
746
|
+
<main>
|
|
747
|
+
<h1>{title}</h1>
|
|
748
|
+
<ClientComponent />
|
|
749
|
+
<ServerComponent />
|
|
750
|
+
</main>
|
|
751
|
+
</IntlayerServerProvider>
|
|
752
|
+
);
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
export default LandingPage;
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
</TabItem>
|
|
759
|
+
</Tab>
|
|
760
|
+
|
|
761
|
+
#### Порівняння
|
|
762
|
+
|
|
763
|
+
Усі три рішення підтримують завантаження контенту для кожної локалі та використання провайдерів.
|
|
764
|
+
|
|
765
|
+
- У **next-intl/next-i18next** зазвичай завантажують вибрані повідомлення/простори імен (namespaces) для кожного маршруту та розміщують провайдери там, де це потрібно.
|
|
766
|
+
|
|
767
|
+
- **Intlayer** додає аналіз під час збірки для виявлення використання, що може зменшити ручне підключення провайдерів і дозволити використання одного кореневого провайдера.
|
|
768
|
+
|
|
769
|
+
Вибирайте між явним контролем та автоматизацією залежно від уподобань команди.
|
|
770
|
+
|
|
771
|
+
### Використання в клієнтському компоненті
|
|
772
|
+
|
|
773
|
+
Розглянемо приклад клієнтського компонента, що рендерить лічильник.
|
|
774
|
+
|
|
775
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
776
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
777
|
+
|
|
778
|
+
**Переклади (по одному JSON на простір імен у `src/locales/...`)**
|
|
779
|
+
|
|
780
|
+
```json fileName="src/locales/en/about.json"
|
|
781
|
+
{
|
|
782
|
+
"title": "About",
|
|
783
|
+
"description": "About page description",
|
|
784
|
+
"counter": {
|
|
785
|
+
"label": "Counter",
|
|
786
|
+
"increment": "Increment"
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
```json fileName="src/locales/fr/about.json"
|
|
792
|
+
{
|
|
793
|
+
"title": "À propos",
|
|
794
|
+
"description": "Description de la page À propos",
|
|
795
|
+
"counter": {
|
|
796
|
+
"label": "Compteur",
|
|
797
|
+
"increment": "Incrémenter"
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**Клієнтський компонент (завантажує лише необхідний простір імен)**
|
|
803
|
+
|
|
804
|
+
```tsx fileName="src/components/ClientComponent.tsx"
|
|
805
|
+
"use client";
|
|
806
|
+
|
|
807
|
+
import React, { useState } from "react";
|
|
808
|
+
import { useTranslation } from "react-i18next";
|
|
809
|
+
|
|
810
|
+
const ClientComponent = () => {
|
|
811
|
+
const { t, i18n } = useTranslation("about");
|
|
812
|
+
const [count, setCount] = useState(0);
|
|
813
|
+
|
|
814
|
+
const numberFormat = new Intl.NumberFormat(i18n.language);
|
|
815
|
+
|
|
816
|
+
return (
|
|
817
|
+
<div>
|
|
818
|
+
<p>{numberFormat.format(count)}</p>
|
|
819
|
+
<button
|
|
820
|
+
aria-label={t("counter.label")}
|
|
821
|
+
onClick={() => setCount((c) => c + 1)}
|
|
822
|
+
>
|
|
823
|
+
{t("counter.increment")}
|
|
824
|
+
</button>
|
|
825
|
+
</div>
|
|
826
|
+
);
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
export default ClientComponent;
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
> Переконайтеся, що сторінка/провайдер включає лише ті namespaces, які вам потрібні (наприклад, `about`).
|
|
833
|
+
> Якщо ви використовуєте React < 19, memoize важкі форматери, такі як `Intl.NumberFormat`.
|
|
834
|
+
|
|
835
|
+
</TabItem>
|
|
836
|
+
<TabItem label="next-intl" value="next-intl">
|
|
837
|
+
|
|
838
|
+
**Переклади (структура збережена; завантажуйте їх у messages next-intl як вам зручніше)**
|
|
839
|
+
|
|
840
|
+
```json fileName="locales/en/about.json"
|
|
841
|
+
{
|
|
842
|
+
"counter": {
|
|
843
|
+
"label": "Counter",
|
|
844
|
+
"increment": "Increment"
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
```json fileName="locales/fr/about.json"
|
|
850
|
+
{
|
|
851
|
+
"counter": {
|
|
852
|
+
"label": "Compteur",
|
|
853
|
+
"increment": "Incrémenter"
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**Клієнтський компонент**
|
|
859
|
+
|
|
860
|
+
```tsx fileName="src/components/ClientComponentExample.tsx"
|
|
861
|
+
"use client";
|
|
862
|
+
|
|
863
|
+
import React, { useState } from "react";
|
|
864
|
+
import { useTranslations, useFormatter } from "next-intl";
|
|
865
|
+
|
|
866
|
+
const ClientComponentExample = () => {
|
|
867
|
+
// Обмежити область безпосередньо на вкладений об'єкт
|
|
868
|
+
const t = useTranslations("about.counter");
|
|
869
|
+
const format = useFormatter();
|
|
870
|
+
const [count, setCount] = useState(0);
|
|
871
|
+
|
|
872
|
+
return (
|
|
873
|
+
<div>
|
|
874
|
+
<p>{format.number(count)}</p>
|
|
875
|
+
<button
|
|
876
|
+
aria-label={t("label")}
|
|
877
|
+
onClick={() => setCount((count) => count + 1)}
|
|
878
|
+
>
|
|
879
|
+
{t("increment")}
|
|
880
|
+
</button>
|
|
881
|
+
</div>
|
|
882
|
+
);
|
|
883
|
+
};
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
> Не забудьте додати повідомлення "about" у клієнтські повідомлення сторінки
|
|
887
|
+
|
|
888
|
+
</TabItem>
|
|
889
|
+
<TabItem label="intlayer" value="intlayer">
|
|
890
|
+
|
|
891
|
+
**Контент**
|
|
892
|
+
|
|
893
|
+
```ts fileName="src/components/ClientComponentExample/index.content.ts"
|
|
894
|
+
import { t, type Dictionary } from "intlayer";
|
|
895
|
+
|
|
896
|
+
const counterContent = {
|
|
897
|
+
key: "counter",
|
|
898
|
+
content: {
|
|
899
|
+
label: t({ uk: "Лічильник", en: "Counter", fr: "Compteur" }),
|
|
900
|
+
increment: t({ uk: "Збільшити", en: "Increment", fr: "Incrémenter" }),
|
|
901
|
+
},
|
|
902
|
+
} satisfies Dictionary;
|
|
903
|
+
|
|
904
|
+
export default counterContent;
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
**Клієнтський компонент**
|
|
908
|
+
|
|
909
|
+
```tsx fileName="src/components/ClientComponentExample/index.tsx"
|
|
910
|
+
"use client";
|
|
911
|
+
|
|
912
|
+
import React, { useState } from "react";
|
|
913
|
+
import { useNumber, useIntlayer } from "next-intlayer";
|
|
914
|
+
|
|
915
|
+
const ClientComponentExample = () => {
|
|
916
|
+
const [count, setCount] = useState(0);
|
|
917
|
+
const { label, increment } = useIntlayer("counter"); // returns strings
|
|
918
|
+
const { number } = useNumber();
|
|
919
|
+
|
|
920
|
+
return (
|
|
921
|
+
<div>
|
|
922
|
+
<p>{number(count)}</p>
|
|
923
|
+
<button aria-label={label} onClick={() => setCount((count) => count + 1)}>
|
|
924
|
+
{increment}
|
|
925
|
+
</button>
|
|
926
|
+
</div>
|
|
927
|
+
);
|
|
928
|
+
};
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
</TabItem>
|
|
932
|
+
</Tab>
|
|
933
|
+
|
|
934
|
+
#### Порівняння
|
|
935
|
+
|
|
936
|
+
- **Форматування чисел**
|
|
937
|
+
- **next-i18next**: немає `useNumber`; використовуйте `Intl.NumberFormat` (або i18next-icu).
|
|
938
|
+
- **next-intl**: `useFormatter().number(value)`.
|
|
939
|
+
- **Intlayer**: вбудований `useNumber()`.
|
|
940
|
+
|
|
941
|
+
- **Ключі**
|
|
942
|
+
- Зберігайте вкладену структуру (`about.counter.label`) і налаштовуйте область дії вашого hook відповідно (`useTranslation("about")` + `t("counter.label")` або `useTranslations("about.counter")` + `t("label")`).
|
|
943
|
+
|
|
944
|
+
- **File locations**
|
|
945
|
+
- **next-i18next** очікує JSON у `public/locales/{lng}/{ns}.json`.
|
|
946
|
+
- **next-intl** гнучкий; завантажуйте повідомлення так, як ви налаштуєте.
|
|
947
|
+
- **Intlayer** зберігає контент у словниках TS/JS та звертається по ключу.
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
### Використання в server component
|
|
952
|
+
|
|
953
|
+
Розглянемо випадок UI-компонента. Цей компонент — server component, і він повинен мати можливість бути вставленим як дочірній елемент client component. (page (server component) -> client component -> server component). Оскільки цей компонент може бути вставлений як дочірній елемент client component, він не може бути async.
|
|
954
|
+
|
|
955
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
956
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
957
|
+
|
|
958
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
959
|
+
type ServerComponentProps = {
|
|
960
|
+
t: (key: string) => string;
|
|
961
|
+
locale: string;
|
|
962
|
+
count: number;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
|
|
966
|
+
const formatted = new Intl.NumberFormat(locale).format(count);
|
|
967
|
+
|
|
968
|
+
return (
|
|
969
|
+
<div>
|
|
970
|
+
<p>{formatted}</p>
|
|
971
|
+
<button aria-label={t("counter.label")}>{t("counter.increment")}</button>
|
|
972
|
+
</div>
|
|
973
|
+
);
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
export default ServerComponent;
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
</TabItem>
|
|
980
|
+
<TabItem label="next-intl" value="next-intl">
|
|
981
|
+
|
|
982
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
983
|
+
type ServerComponentProps = {
|
|
984
|
+
t: (key: string) => string;
|
|
985
|
+
locale: string;
|
|
986
|
+
count: number;
|
|
987
|
+
formatter: Intl.NumberFormat;
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
const ServerComponent = ({
|
|
991
|
+
t,
|
|
992
|
+
locale,
|
|
993
|
+
count,
|
|
994
|
+
formatter,
|
|
995
|
+
}: ServerComponentProps) => {
|
|
996
|
+
const formatted = formatter.format(count);
|
|
997
|
+
|
|
998
|
+
return (
|
|
999
|
+
<div>
|
|
1000
|
+
<p>{formatted}</p>
|
|
1001
|
+
<button aria-label={t("counter.label")}>{t("counter.increment")}</button>
|
|
1002
|
+
</div>
|
|
1003
|
+
);
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
export default ServerComponent;
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
> Оскільки серверний компонент не може бути async, вам потрібно передати переклади та функцію форматування як пропси.
|
|
1010
|
+
>
|
|
1011
|
+
> На вашій сторінці / layout:
|
|
1012
|
+
>
|
|
1013
|
+
> - `import { getTranslations, getFormatter } from "next-intl/server";`
|
|
1014
|
+
> - `const t = await getTranslations("about.counter");`
|
|
1015
|
+
> - `const formatter = await getFormatter().then((formatter) => formatter.number());`
|
|
1016
|
+
|
|
1017
|
+
</TabItem>
|
|
1018
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1019
|
+
|
|
1020
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
1021
|
+
import { useIntlayer, useNumber } from "next-intlayer/server";
|
|
1022
|
+
|
|
1023
|
+
type ServerComponentProps = {
|
|
1024
|
+
count: number;
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
1028
|
+
const { label, increment } = useIntlayer("counter");
|
|
1029
|
+
const { number } = useNumber();
|
|
1030
|
+
|
|
1031
|
+
return (
|
|
1032
|
+
<div>
|
|
1033
|
+
<p>{number(count)}</p>
|
|
1034
|
+
<button aria-label={label}>{increment}</button>
|
|
1035
|
+
</div>
|
|
1036
|
+
);
|
|
1037
|
+
};
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
</TabItem>
|
|
1041
|
+
</Tab>
|
|
1042
|
+
|
|
1043
|
+
> Intlayer надає **безпечні для сервера** хуки через `next-intlayer/server`. Для роботи `useIntlayer` і `useNumber` використовують синтаксис, схожий на клієнтські хуки, але під капотом залежать від серверного контексту (`IntlayerServerProvider`).
|
|
1044
|
+
|
|
1045
|
+
### Метадані / Sitemap / Robots
|
|
1046
|
+
|
|
1047
|
+
Перекладати контент — це чудово. Але люди зазвичай забувають, що головна мета інтернаціоналізації — зробити ваш вебсайт більш помітним для світу. I18n — надзвичайно ефективний важіль для покращення видимості вашого сайту.
|
|
1048
|
+
|
|
1049
|
+
Ось перелік кращих практик щодо багатомовного SEO.
|
|
1050
|
+
|
|
1051
|
+
- встановлюйте мета-теги hreflang у тегу `<head>`
|
|
1052
|
+
> Це допомагає пошуковим системам зрозуміти, які мови доступні на сторінці
|
|
1053
|
+
- перераховуйте всі переклади сторінок у sitemap.xml, використовуючи XML-схему `http://www.w3.org/1999/xhtml`
|
|
1054
|
+
>
|
|
1055
|
+
- не забудьте виключити префіксовані сторінки з robots.txt (наприклад `/dashboard`, `/fr/dashboard`, `/es/dashboard`)
|
|
1056
|
+
>
|
|
1057
|
+
- використовуйте кастомний компонент Link, щоб перенаправляти на максимально локалізовану версію сторінки (наприклад французькою `<a href="/fr/about">A propos</a>` )
|
|
1058
|
+
>
|
|
1059
|
+
|
|
1060
|
+
Розробники часто забувають правильно посилатися на свої сторінки в різних локалях.
|
|
1061
|
+
|
|
1062
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1063
|
+
|
|
1064
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1065
|
+
|
|
1066
|
+
```ts fileName="i18n.config.ts"
|
|
1067
|
+
export const locales = ["en", "fr"] as const;
|
|
1068
|
+
export type Locale = (typeof locales)[number];
|
|
1069
|
+
export const defaultLocale: Locale = "en";
|
|
1070
|
+
|
|
1071
|
+
export function localizedPath(locale: string, path: string) {
|
|
1072
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const ORIGIN = "https://example.com";
|
|
1076
|
+
export function abs(locale: string, path: string) {
|
|
1077
|
+
return ORIGIN + localizedPath(locale, path);
|
|
1078
|
+
}
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
1082
|
+
import type { Metadata } from "next";
|
|
1083
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
1084
|
+
|
|
1085
|
+
type GenerateMetadataParams = {
|
|
1086
|
+
params: Promise<{
|
|
1087
|
+
locale: string;
|
|
1088
|
+
}>;
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
export const generateMetadata = async ({
|
|
1092
|
+
params,
|
|
1093
|
+
}: GenerateMetadataParams): Promise<Metadata> => {
|
|
1094
|
+
const { locale } = await params;
|
|
1095
|
+
|
|
1096
|
+
// Імпортувати правильний JSON-бандл з src/locales
|
|
1097
|
+
const messages = (await import("@/locales/" + locale + "/about.json"))
|
|
1098
|
+
.default;
|
|
1099
|
+
|
|
1100
|
+
const languages = Object.fromEntries(
|
|
1101
|
+
locales.map((locale) => [locale, localizedPath(locale, "/about")])
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
return {
|
|
1105
|
+
title: messages.title,
|
|
1106
|
+
description: messages.description,
|
|
1107
|
+
alternates: {
|
|
1108
|
+
canonical: localizedPath(locale, "/about"),
|
|
1109
|
+
languages: { ...languages, "x-default": "/about" },
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
export default async function AboutPage() {
|
|
1115
|
+
return <h1>Про нас</h1>;
|
|
1116
|
+
}
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
```ts fileName="src/app/sitemap.ts"
|
|
1120
|
+
import type { MetadataRoute } from "next";
|
|
1121
|
+
import { locales, defaultLocale, abs } from "@/i18n.config";
|
|
1122
|
+
|
|
1123
|
+
export const sitemap = (): MetadataRoute.Sitemap => {
|
|
1124
|
+
const languages = Object.fromEntries(
|
|
1125
|
+
locales.map((locale) => [locale, abs(locale, "/about")])
|
|
1126
|
+
);
|
|
1127
|
+
return [
|
|
1128
|
+
{
|
|
1129
|
+
url: abs(defaultLocale, "/about"),
|
|
1130
|
+
lastModified: new Date(),
|
|
1131
|
+
changeFrequency: "monthly",
|
|
1132
|
+
priority: 0.7,
|
|
1133
|
+
alternates: { languages },
|
|
1134
|
+
},
|
|
1135
|
+
];
|
|
1136
|
+
};
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
```ts fileName="src/app/robots.ts"
|
|
1140
|
+
import type { MetadataRoute } from "next";
|
|
1141
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
1142
|
+
|
|
1143
|
+
const ORIGIN = "https://example.com";
|
|
1144
|
+
|
|
1145
|
+
const expandAllLocales = (path: string) => [
|
|
1146
|
+
localizedPath(defaultLocale, path),
|
|
1147
|
+
...locales
|
|
1148
|
+
.filter((locale) => locale !== defaultLocale)
|
|
1149
|
+
.map((locale) => localizedPath(locale, path)),
|
|
1150
|
+
];
|
|
1151
|
+
|
|
1152
|
+
export const robots = (): MetadataRoute.Robots => {
|
|
1153
|
+
const disallow = [
|
|
1154
|
+
...expandAllLocales("/dashboard"),
|
|
1155
|
+
...expandAllLocales("/admin"),
|
|
1156
|
+
];
|
|
1157
|
+
|
|
1158
|
+
return {
|
|
1159
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
1160
|
+
host: ORIGIN,
|
|
1161
|
+
sitemap: ORIGIN + "/sitemap.xml",
|
|
1162
|
+
};
|
|
1163
|
+
};
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
</TabItem>
|
|
1167
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1168
|
+
|
|
1169
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
1170
|
+
import type { Metadata } from "next";
|
|
1171
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1172
|
+
import { getTranslations } from "next-intl/server";
|
|
1173
|
+
|
|
1174
|
+
const localizedPath = (locale: string, path: string) => {
|
|
1175
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
type GenerateMetadataParams = {
|
|
1179
|
+
params: Promise<{
|
|
1180
|
+
locale: string;
|
|
1181
|
+
}>;
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
export const generateMetadata = async ({
|
|
1185
|
+
params,
|
|
1186
|
+
}: GenerateMetadataParams): Promise<Metadata> => {
|
|
1187
|
+
const { locale } = await params;
|
|
1188
|
+
const t = await getTranslations({ locale, namespace: "about" });
|
|
1189
|
+
|
|
1190
|
+
const url = "/about";
|
|
1191
|
+
const languages = Object.fromEntries(
|
|
1192
|
+
locales.map((locale) => [locale, localizedPath(locale, url)])
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
return {
|
|
1196
|
+
title: t("title"),
|
|
1197
|
+
description: t("description"),
|
|
1198
|
+
alternates: {
|
|
1199
|
+
canonical: localizedPath(locale, url),
|
|
1200
|
+
languages: { ...languages, "x-default": url },
|
|
1201
|
+
},
|
|
1202
|
+
};
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// ... Решта коду сторінки
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
```tsx fileName="src/app/sitemap.ts"
|
|
1209
|
+
import type { MetadataRoute } from "next";
|
|
1210
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1211
|
+
|
|
1212
|
+
const origin = "https://example.com";
|
|
1213
|
+
|
|
1214
|
+
const formatterLocalizedPath = (locale: string, path: string) =>
|
|
1215
|
+
locale === defaultLocale ? origin + path : origin + "/" + locale + path;
|
|
1216
|
+
|
|
1217
|
+
export const sitemap = (): MetadataRoute.Sitemap => {
|
|
1218
|
+
const aboutLanguages = Object.fromEntries(
|
|
1219
|
+
locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
return [
|
|
1223
|
+
{
|
|
1224
|
+
url: formatterLocalizedPath(defaultLocale, "/about"),
|
|
1225
|
+
lastModified: new Date(),
|
|
1226
|
+
changeFrequency: "monthly",
|
|
1227
|
+
priority: 0.7,
|
|
1228
|
+
alternates: { languages: aboutLanguages },
|
|
1229
|
+
},
|
|
1230
|
+
];
|
|
1231
|
+
};
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
```tsx fileName="src/app/robots.ts"
|
|
1235
|
+
import type { MetadataRoute } from "next";
|
|
1236
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1237
|
+
|
|
1238
|
+
const origin = "https://example.com";
|
|
1239
|
+
const withAllLocales = (path: string) => [
|
|
1240
|
+
path,
|
|
1241
|
+
...locales
|
|
1242
|
+
.filter((locale) => locale !== defaultLocale)
|
|
1243
|
+
.map((locale) => "/" + locale + path),
|
|
1244
|
+
];
|
|
1245
|
+
|
|
1246
|
+
export const robots = (): MetadataRoute.Robots => {
|
|
1247
|
+
const disallow = [
|
|
1248
|
+
...withAllLocales("/dashboard"),
|
|
1249
|
+
...withAllLocales("/admin"),
|
|
1250
|
+
];
|
|
1251
|
+
|
|
1252
|
+
return {
|
|
1253
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
1254
|
+
host: origin,
|
|
1255
|
+
sitemap: origin + "/sitemap.xml",
|
|
1256
|
+
};
|
|
1257
|
+
};
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
</TabItem>
|
|
1261
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1262
|
+
|
|
1263
|
+
```typescript fileName="src/app/[locale]/about/layout.tsx"
|
|
1264
|
+
import { getIntlayer, getMultilingualUrls } from "intlayer";
|
|
1265
|
+
import type { Metadata } from "next";
|
|
1266
|
+
import type { LocalPromiseParams } from "next-intlayer";
|
|
1267
|
+
|
|
1268
|
+
export const generateMetadata = async ({
|
|
1269
|
+
params,
|
|
1270
|
+
}: LocalPromiseParams): Promise<Metadata> => {
|
|
1271
|
+
const { locale } = await params;
|
|
1272
|
+
|
|
1273
|
+
const metadata = getIntlayer("page-metadata", locale);
|
|
1274
|
+
|
|
1275
|
+
const multilingualUrls = getMultilingualUrls("/about");
|
|
1276
|
+
|
|
1277
|
+
return {
|
|
1278
|
+
...metadata,
|
|
1279
|
+
alternates: {
|
|
1280
|
+
canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
|
|
1281
|
+
languages: { ...multilingualUrls, "x-default": "/about" },
|
|
1282
|
+
},
|
|
1283
|
+
};
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
// ... Решта коду сторінки
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
```tsx fileName="src/app/sitemap.ts"
|
|
1290
|
+
import { getMultilingualUrls } from "intlayer";
|
|
1291
|
+
import type { MetadataRoute } from "next";
|
|
1292
|
+
|
|
1293
|
+
const sitemap = (): MetadataRoute.Sitemap => [
|
|
1294
|
+
{
|
|
1295
|
+
url: "https://example.com/about",
|
|
1296
|
+
alternates: {
|
|
1297
|
+
languages: { ...getMultilingualUrls("https://example.com/about") },
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
];
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
```tsx fileName="src/app/robots.ts"
|
|
1304
|
+
ts;
|
|
1305
|
+
import { getMultilingualUrls } from "intlayer";
|
|
1306
|
+
import type { MetadataRoute } from "next";
|
|
1307
|
+
|
|
1308
|
+
const getAllMultilingualUrls = (urls: string[]) =>
|
|
1309
|
+
urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
|
|
1310
|
+
|
|
1311
|
+
const robots = (): MetadataRoute.Robots => ({
|
|
1312
|
+
rules: {
|
|
1313
|
+
userAgent: "*",
|
|
1314
|
+
allow: ["/"],
|
|
1315
|
+
disallow: getAllMultilingualUrls(["/dashboard"]),
|
|
1316
|
+
},
|
|
1317
|
+
host: "https://example.com",
|
|
1318
|
+
sitemap: "https://example.com/sitemap.xml",
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
export default robots;
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
</TabItem>
|
|
1325
|
+
</Tab>
|
|
1326
|
+
|
|
1327
|
+
> Intlayer надає функцію `getMultilingualUrls` для генерації багатомовних URL-адрес для вашого sitemap.
|
|
1328
|
+
|
|
1329
|
+
### Middleware для маршрутизації локалі
|
|
1330
|
+
|
|
1331
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1332
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1333
|
+
|
|
1334
|
+
Додайте middleware для обробки визначення локалі та маршрутизації:
|
|
1335
|
+
|
|
1336
|
+
```ts fileName="src/middleware.ts"
|
|
1337
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
1338
|
+
import { defaultLocale, locales } from "@/i18n.config";
|
|
1339
|
+
|
|
1340
|
+
const PUBLIC_FILE = /\.[^/]+$/; // виключити файли з розширеннями
|
|
1341
|
+
|
|
1342
|
+
export function middleware(request: NextRequest) {
|
|
1343
|
+
const { pathname } = request.nextUrl;
|
|
1344
|
+
|
|
1345
|
+
if (
|
|
1346
|
+
pathname.startsWith("/_next") ||
|
|
1347
|
+
pathname.startsWith("/api") ||
|
|
1348
|
+
pathname.startsWith("/static") ||
|
|
1349
|
+
PUBLIC_FILE.test(pathname)
|
|
1350
|
+
) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const hasLocale = locales.some(
|
|
1355
|
+
(l) => pathname === "/" + l || pathname.startsWith("/" + l + "/")
|
|
1356
|
+
);
|
|
1357
|
+
if (!hasLocale) {
|
|
1358
|
+
const locale = defaultLocale;
|
|
1359
|
+
const url = request.nextUrl.clone();
|
|
1360
|
+
url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);
|
|
1361
|
+
return NextResponse.redirect(url);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
export const config = {
|
|
1366
|
+
matcher: [
|
|
1367
|
+
// Відповідає всім шляхам, крім тих, що починаються з наведених, та файлів з розширенням
|
|
1368
|
+
"/((?!api|_next|static|.*\\..*).*)",
|
|
1369
|
+
],
|
|
1370
|
+
};
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
</TabItem>
|
|
1374
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1375
|
+
|
|
1376
|
+
Додайте middleware для визначення локалі та маршрутизації:
|
|
1377
|
+
|
|
1378
|
+
```ts fileName="src/middleware.ts"
|
|
1379
|
+
import createMiddleware from "next-intl/middleware";
|
|
1380
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1381
|
+
|
|
1382
|
+
export default createMiddleware({
|
|
1383
|
+
locales: [...locales],
|
|
1384
|
+
defaultLocale,
|
|
1385
|
+
localeDetection: true,
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
export const config = {
|
|
1389
|
+
// Пропустити API, внутрішні ресурси Next та статичні ресурси
|
|
1390
|
+
matcher: ["/((?!api|_next|.*\\..*).*)"],
|
|
1391
|
+
};
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
</TabItem>
|
|
1395
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1396
|
+
|
|
1397
|
+
Intlayer забезпечує вбудовану підтримку middleware через конфігурацію пакета `next-intlayer`.
|
|
1398
|
+
|
|
1399
|
+
```ts fileName="src/middleware.ts"
|
|
1400
|
+
import { intlayerProxy } from "next-intlayer/proxy";
|
|
1401
|
+
|
|
1402
|
+
export const middleware = intlayerProxy();
|
|
1403
|
+
|
|
1404
|
+
// застосовує цей middleware лише до файлів у директорії app
|
|
1405
|
+
export const config = {
|
|
1406
|
+
matcher: "/((?!api|_next|static|.*\\..*).*)",
|
|
1407
|
+
};
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
Налаштування middleware централізовано в файлі `intlayer.config.ts`.
|
|
1411
|
+
|
|
1412
|
+
</TabItem>
|
|
1413
|
+
</Tab>
|
|
1414
|
+
|
|
1415
|
+
### Контрольний список налаштувань та найкращі практики
|
|
1416
|
+
|
|
1417
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1418
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1419
|
+
|
|
1420
|
+
- Переконайтеся, що `lang` і `dir` встановлені на кореневому `<html>` у `src/app/[locale]/layout.tsx`.
|
|
1421
|
+
- Розділяйте переклади за просторами імен (наприклад `common.json`, `about.json`) у `src/locales/<locale>/`.
|
|
1422
|
+
- Завантажуйте у клієнтських компонентах лише необхідні простори імен, використовуючи `useTranslation('<ns>')` та обмежуючи `I18nProvider` тими ж просторами імен.
|
|
1423
|
+
- Зберігайте сторінки статичними, коли це можливо: експортуйте `export const dynamic = 'force-static'` у сторінках; встановіть `dynamicParams = false` та реалізуйте `generateStaticParams`.
|
|
1424
|
+
- Використовуйте синхронні серверні компоненти, вкладені під client boundaries, передаючи вже обчислені рядки або функцію `t` і `locale`.
|
|
1425
|
+
- Для SEO встановіть `alternates.languages` у metadata, перелічіть локалізовані URL у `sitemap.ts` та забороніть дублювання локалізованих маршрутів у `robots.ts`.
|
|
1426
|
+
- Віддавайте перевагу форматувальникам, що враховують локаль (наприклад, `Intl.NumberFormat(locale)`), і мемоізуйте їх на клієнті, якщо використовуєте React < 19.
|
|
1427
|
+
|
|
1428
|
+
</TabItem>
|
|
1429
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1430
|
+
|
|
1431
|
+
- **Встановіть html `lang` і `dir`**: У `src/app/[locale]/layout.tsx` обчисліть `dir` за допомогою `getLocaleDirection(locale)` і встановіть `<html lang={locale} dir={dir}>`.
|
|
1432
|
+
- **Розділяйте повідомлення за namespace**: Організуйте JSON за локаллю та namespace (наприклад, `common.json`, `about.json`).
|
|
1433
|
+
- **Мінімізуйте навантаження на клієнт**: На сторінках надсилайте в `NextIntlClientProvider` лише потрібні namespace (наприклад, `pick(messages, ['common', 'about'])`).
|
|
1434
|
+
- **Віддавайте перевагу статичним сторінкам**: Експортуйте `export const dynamic = 'force-static'` і згенеруйте статичні params для всіх `locales`.
|
|
1435
|
+
- **Синхронні серверні компоненти**: Тримайте серверні компоненти синхронними, передаючи попередньо обчислені рядки (перекладені підписи, відформатовані числа) замість асинхронних викликів або несеріалізованих функцій.
|
|
1436
|
+
|
|
1437
|
+
</TabItem>
|
|
1438
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1439
|
+
|
|
1440
|
+
- **Модульний контент**: Розміщуйте словники контенту разом з компонентами, використовуючи файли `.content.{ts|js|json}`.
|
|
1441
|
+
- **Безпека типів**: Використовуйте інтеграцію з TypeScript для перевірки контенту під час компіляції.
|
|
1442
|
+
- **Оптимізація під час збірки**: Використовуйте інструменти збірки Intlayer для автоматичного tree-shaking та оптимізації бандлів.
|
|
1443
|
+
- **Вбудовані інструменти**: Скористайтеся вбудованим роутингом, допоміжними інструментами для SEO та підтримкою візуального редактора.
|
|
1444
|
+
|
|
1445
|
+
</TabItem>
|
|
1446
|
+
</Tab>
|
|
1447
|
+
|
|
1448
|
+
---
|
|
1449
|
+
|
|
1450
|
+
## А переможець…
|
|
1451
|
+
|
|
1452
|
+
Це не просто. Кожен варіант має свої компроміси. Ось як я це бачу:
|
|
1453
|
+
|
|
1454
|
+
<Columns>
|
|
1455
|
+
<Column>
|
|
1456
|
+
|
|
1457
|
+
**next-i18next**
|
|
1458
|
+
|
|
1459
|
+
- зріла, багатофункціональна, має безліч плагінів від спільноти, але з вищими витратами на налаштування. Якщо вам потрібна **екосистема плагінів i18next** (наприклад, розширені правила ICU через плагіни) і ваша команда вже знає i18next, готові прийняти **більше налаштувань** заради гнучкості.
|
|
1460
|
+
|
|
1461
|
+
</Column>
|
|
1462
|
+
<Column>
|
|
1463
|
+
|
|
1464
|
+
**next-intl**
|
|
1465
|
+
|
|
1466
|
+
- найпростіше, легке, менше нав'язаних рішень. Якщо ви хочете **мінімальне** рішення, вам зручно працювати з централізованими каталогами, і ваш додаток **малий або середнього розміру**.
|
|
1467
|
+
|
|
1468
|
+
</Column>
|
|
1469
|
+
<Column>
|
|
1470
|
+
|
|
1471
|
+
**Intlayer**
|
|
1472
|
+
|
|
1473
|
+
- створений для сучасного Next.js, з модульним контентом, type safety, інструментами та меншим boilerplate. Якщо ви цінуєте **component-scoped content**, **strict TypeScript**, **build-time guarantees**, **tree-shaking**, і **batteries-included** інструменти для маршрутизації/SEO/редакторів — особливо для **Next.js App Router**, design-systems та **великих, модульних codebases**.
|
|
1474
|
+
|
|
1475
|
+
</Column>
|
|
1476
|
+
</Columns>
|
|
1477
|
+
|
|
1478
|
+
Якщо ви віддаєте перевагу мінімальній конфігурації і готові до деякої ручної інтеграції, next-intl — хороший вибір. Якщо вам потрібні всі функції і вас не лякає складність, next-i18next теж підходить. Але якщо ви хочете сучасне, масштабоване, модульне рішення з готовими інструментами, Intlayer прагне надати це "з коробки".
|
|
1479
|
+
|
|
1480
|
+
> **Альтернатива для корпоративних команд**: Якщо вам потрібне перевірене рішення, яке відмінно працює з усталеними платформами локалізації, такими як **Crowdin**, **Phrase** або іншими професійними системами управління перекладами, розгляньте **next-intl** або **next-i18next** через їхню зрілу екосистему та перевірені інтеграції.
|
|
1481
|
+
|
|
1482
|
+
> **Майбутній роадмап**: Intlayer також планує розробляти плагіни, які працюватимуть поверх рішень **i18next** та **next-intl**. Це дасть вам переваги Intlayer у сфері автоматизації, синтаксису та управління контентом, зберігаючи при цьому безпеку й стабільність, які надають ці усталені рішення у вашому коді застосунку.
|
|
1483
|
+
|
|
1484
|
+
## GitHub зірки
|
|
1485
|
+
|
|
1486
|
+
Зірки на GitHub є потужним індикатором популярності проєкту, довіри спільноти та його довгострокової значущості. Хоча це не прямий показник технічної якості, вони відображають, скільки розробників вважають проєкт корисним, стежать за його розвитком і, ймовірно, приймуть його у використання. Для оцінки цінності проєкту зірки допомагають порівнювати залученість між альтернативами та дають уявлення про зростання екосистеми.
|
|
1487
|
+
|
|
1488
|
+
[](https://www.star-history.com/#i18next/next-i18next&amannn/next-intl&aymericzip/intlayer)
|
|
1489
|
+
|
|
1490
|
+
---
|
|
1491
|
+
|
|
1492
|
+
## Висновок
|
|
1493
|
+
|
|
1494
|
+
Усі три бібліотеки успішно справляються з базовою локалізацією. Різниця полягає в тому, **скільки роботи вам доведеться виконати**, щоб досягти надійної, масштабованої конфігурації в **сучасному Next.js**:
|
|
1495
|
+
|
|
1496
|
+
- З **Intlayer**, **модульний контент**, **строгий TS**, **безпека на етапі збірки**, **tree-shaken bundles**, і **first-class App Router + SEO tooling** — це **за замовчуванням**, а не обов'язок.
|
|
1497
|
+
- Якщо ваша команда цінує **підтримуваність і швидкість** у багатомовному, орієнтованому на компоненти додатку, Intlayer сьогодні пропонує **найповніший** досвід.
|
|
1498
|
+
|
|
1499
|
+
Зверніться до документа ['Чому Intlayer?'](https://intlayer.org/doc/why) для детальнішої інформації.
|