@intlayer/docs 7.5.12 → 7.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blog/ar/per-component_vs_centralized_i18n.md +248 -0
- package/blog/de/per-component_vs_centralized_i18n.md +248 -0
- package/blog/en/_per-component_vs_centralized_i18n.md +252 -0
- package/blog/en/per-component_vs_centralized_i18n.md +248 -0
- package/blog/en-GB/per-component_vs_centralized_i18n.md +247 -0
- package/blog/es/per-component_vs_centralized_i18n.md +245 -0
- package/blog/fr/per-component_vs_centralized_i18n.md +245 -0
- package/blog/hi/per-component_vs_centralized_i18n.md +249 -0
- package/blog/id/per-component_vs_centralized_i18n.md +248 -0
- package/blog/it/per-component_vs_centralized_i18n.md +247 -0
- package/blog/ja/per-component_vs_centralized_i18n.md +247 -0
- package/blog/ko/per-component_vs_centralized_i18n.md +246 -0
- package/blog/pl/per-component_vs_centralized_i18n.md +247 -0
- package/blog/pt/per-component_vs_centralized_i18n.md +246 -0
- package/blog/ru/per-component_vs_centralized_i18n.md +251 -0
- package/blog/tr/per-component_vs_centralized_i18n.md +244 -0
- package/blog/uk/compiler_vs_declarative_i18n.md +224 -0
- package/blog/uk/i18n_using_next-i18next.md +1086 -0
- package/blog/uk/i18n_using_next-intl.md +760 -0
- package/blog/uk/index.md +69 -0
- package/blog/uk/internationalization_and_SEO.md +273 -0
- package/blog/uk/intlayer_with_i18next.md +211 -0
- package/blog/uk/intlayer_with_next-i18next.md +202 -0
- package/blog/uk/intlayer_with_next-intl.md +203 -0
- package/blog/uk/intlayer_with_react-i18next.md +200 -0
- package/blog/uk/intlayer_with_react-intl.md +202 -0
- package/blog/uk/intlayer_with_vue-i18n.md +206 -0
- package/blog/uk/l10n_platform_alternative/Lokalise.md +80 -0
- package/blog/uk/l10n_platform_alternative/crowdin.md +80 -0
- package/blog/uk/l10n_platform_alternative/phrase.md +78 -0
- package/blog/uk/list_i18n_technologies/CMS/drupal.md +143 -0
- package/blog/uk/list_i18n_technologies/CMS/wix.md +167 -0
- package/blog/uk/list_i18n_technologies/CMS/wordpress.md +189 -0
- package/blog/uk/list_i18n_technologies/frameworks/angular.md +125 -0
- package/blog/uk/list_i18n_technologies/frameworks/flutter.md +128 -0
- package/blog/uk/list_i18n_technologies/frameworks/react-native.md +217 -0
- package/blog/uk/list_i18n_technologies/frameworks/react.md +155 -0
- package/blog/uk/list_i18n_technologies/frameworks/svelte.md +145 -0
- package/blog/uk/list_i18n_technologies/frameworks/vue.md +144 -0
- package/blog/uk/next-i18next_vs_next-intl_vs_intlayer.md +1499 -0
- package/blog/uk/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/uk/per-component_vs_centralized_i18n.md +248 -0
- package/blog/uk/rag_powered_documentation_assistant.md +288 -0
- package/blog/uk/react-i18next_vs_react-intl_vs_intlayer.md +164 -0
- package/blog/uk/vue-i18n_vs_intlayer.md +279 -0
- package/blog/uk/what_is_internationalization.md +167 -0
- package/blog/vi/per-component_vs_centralized_i18n.md +246 -0
- package/blog/zh/per-component_vs_centralized_i18n.md +248 -0
- package/dist/cjs/common.cjs.map +1 -1
- package/dist/cjs/generated/blog.entry.cjs +20 -0
- package/dist/cjs/generated/blog.entry.cjs.map +1 -1
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/cjs/generated/frequentQuestions.entry.cjs +20 -0
- package/dist/cjs/generated/frequentQuestions.entry.cjs.map +1 -1
- package/dist/cjs/generated/legal.entry.cjs.map +1 -1
- package/dist/esm/common.mjs.map +1 -1
- package/dist/esm/generated/blog.entry.mjs +20 -0
- package/dist/esm/generated/blog.entry.mjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/esm/generated/frequentQuestions.entry.mjs +20 -0
- package/dist/esm/generated/frequentQuestions.entry.mjs.map +1 -1
- package/dist/esm/generated/legal.entry.mjs.map +1 -1
- package/dist/types/generated/blog.entry.d.ts +1 -0
- package/dist/types/generated/blog.entry.d.ts.map +1 -1
- package/dist/types/generated/frequentQuestions.entry.d.ts +1 -0
- package/dist/types/generated/frequentQuestions.entry.d.ts.map +1 -1
- package/docs/ar/configuration.md +6 -1
- package/docs/ar/dictionary/content_file.md +6 -1
- package/docs/de/configuration.md +6 -1
- package/docs/de/dictionary/content_file.md +6 -1
- package/docs/en/configuration.md +6 -1
- package/docs/en/dictionary/content_file.md +6 -1
- package/docs/en-GB/configuration.md +6 -1
- package/docs/en-GB/dictionary/content_file.md +3 -1
- package/docs/es/configuration.md +6 -1
- package/docs/es/dictionary/content_file.md +6 -1
- package/docs/fr/configuration.md +6 -1
- package/docs/fr/dictionary/content_file.md +3 -1
- package/docs/hi/configuration.md +6 -1
- package/docs/hi/dictionary/content_file.md +3 -1
- package/docs/id/configuration.md +6 -1
- package/docs/id/dictionary/content_file.md +3 -1
- package/docs/it/configuration.md +6 -1
- package/docs/it/dictionary/content_file.md +3 -1
- package/docs/ja/configuration.md +6 -1
- package/docs/ja/dictionary/content_file.md +3 -1
- package/docs/ko/configuration.md +6 -1
- package/docs/ko/dictionary/content_file.md +3 -1
- package/docs/pl/configuration.md +3 -1
- package/docs/pl/dictionary/content_file.md +3 -1
- package/docs/pt/configuration.md +6 -1
- package/docs/pt/dictionary/content_file.md +3 -1
- package/docs/ru/configuration.md +6 -1
- package/docs/ru/dictionary/content_file.md +6 -1
- package/docs/tr/configuration.md +6 -1
- package/docs/tr/dictionary/content_file.md +3 -1
- package/docs/uk/CI_CD.md +198 -0
- package/docs/uk/autoFill.md +307 -0
- package/docs/uk/bundle_optimization.md +185 -0
- package/docs/uk/cli/build.md +64 -0
- package/docs/uk/cli/ci.md +137 -0
- package/docs/uk/cli/configuration.md +63 -0
- package/docs/uk/cli/debug.md +46 -0
- package/docs/uk/cli/doc-review.md +43 -0
- package/docs/uk/cli/doc-translate.md +132 -0
- package/docs/uk/cli/editor.md +28 -0
- package/docs/uk/cli/fill.md +130 -0
- package/docs/uk/cli/index.md +190 -0
- package/docs/uk/cli/init.md +84 -0
- package/docs/uk/cli/list.md +90 -0
- package/docs/uk/cli/list_projects.md +128 -0
- package/docs/uk/cli/live.md +41 -0
- package/docs/uk/cli/login.md +157 -0
- package/docs/uk/cli/pull.md +78 -0
- package/docs/uk/cli/push.md +98 -0
- package/docs/uk/cli/sdk.md +71 -0
- package/docs/uk/cli/test.md +76 -0
- package/docs/uk/cli/transform.md +65 -0
- package/docs/uk/cli/version.md +24 -0
- package/docs/uk/cli/watch.md +37 -0
- package/docs/uk/configuration.md +742 -0
- package/docs/uk/dictionary/condition.md +237 -0
- package/docs/uk/dictionary/content_file.md +1134 -0
- package/docs/uk/dictionary/enumeration.md +245 -0
- package/docs/uk/dictionary/file.md +232 -0
- package/docs/uk/dictionary/function_fetching.md +212 -0
- package/docs/uk/dictionary/gender.md +273 -0
- package/docs/uk/dictionary/insertion.md +187 -0
- package/docs/uk/dictionary/markdown.md +383 -0
- package/docs/uk/dictionary/nesting.md +273 -0
- package/docs/uk/dictionary/translation.md +332 -0
- package/docs/uk/formatters.md +595 -0
- package/docs/uk/how_works_intlayer.md +256 -0
- package/docs/uk/index.md +175 -0
- package/docs/uk/interest_of_intlayer.md +297 -0
- package/docs/uk/intlayer_CMS.md +569 -0
- package/docs/uk/intlayer_visual_editor.md +292 -0
- package/docs/uk/intlayer_with_angular.md +710 -0
- package/docs/uk/intlayer_with_astro.md +256 -0
- package/docs/uk/intlayer_with_create_react_app.md +1258 -0
- package/docs/uk/intlayer_with_express.md +429 -0
- package/docs/uk/intlayer_with_fastify.md +446 -0
- package/docs/uk/intlayer_with_lynx+react.md +548 -0
- package/docs/uk/intlayer_with_nestjs.md +283 -0
- package/docs/uk/intlayer_with_next-i18next.md +640 -0
- package/docs/uk/intlayer_with_next-intl.md +456 -0
- package/docs/uk/intlayer_with_nextjs_page_router.md +1541 -0
- package/docs/uk/intlayer_with_nuxt.md +711 -0
- package/docs/uk/intlayer_with_react_router_v7.md +600 -0
- package/docs/uk/intlayer_with_react_router_v7_fs_routes.md +669 -0
- package/docs/uk/intlayer_with_svelte_kit.md +579 -0
- package/docs/uk/intlayer_with_tanstack.md +818 -0
- package/docs/uk/intlayer_with_vite+preact.md +1748 -0
- package/docs/uk/intlayer_with_vite+react.md +1449 -0
- package/docs/uk/intlayer_with_vite+solid.md +302 -0
- package/docs/uk/intlayer_with_vite+svelte.md +520 -0
- package/docs/uk/intlayer_with_vite+vue.md +1113 -0
- package/docs/uk/introduction.md +222 -0
- package/docs/uk/locale_mapper.md +242 -0
- package/docs/uk/mcp_server.md +211 -0
- package/docs/uk/packages/express-intlayer/t.md +465 -0
- package/docs/uk/packages/intlayer/getEnumeration.md +159 -0
- package/docs/uk/packages/intlayer/getHTMLTextDir.md +121 -0
- package/docs/uk/packages/intlayer/getLocaleLang.md +81 -0
- package/docs/uk/packages/intlayer/getLocaleName.md +135 -0
- package/docs/uk/packages/intlayer/getLocalizedUrl.md +338 -0
- package/docs/uk/packages/intlayer/getMultilingualUrls.md +359 -0
- package/docs/uk/packages/intlayer/getPathWithoutLocale.md +75 -0
- package/docs/uk/packages/intlayer/getPrefix.md +213 -0
- package/docs/uk/packages/intlayer/getTranslation.md +190 -0
- package/docs/uk/packages/intlayer/getTranslationContent.md +189 -0
- package/docs/uk/packages/next-intlayer/t.md +365 -0
- package/docs/uk/packages/next-intlayer/useDictionary.md +276 -0
- package/docs/uk/packages/next-intlayer/useIntlayer.md +263 -0
- package/docs/uk/packages/next-intlayer/useLocale.md +166 -0
- package/docs/uk/packages/react-intlayer/t.md +311 -0
- package/docs/uk/packages/react-intlayer/useDictionary.md +295 -0
- package/docs/uk/packages/react-intlayer/useI18n.md +250 -0
- package/docs/uk/packages/react-intlayer/useIntlayer.md +251 -0
- package/docs/uk/packages/react-intlayer/useLocale.md +210 -0
- package/docs/uk/per_locale_file.md +345 -0
- package/docs/uk/plugins/sync-json.md +398 -0
- package/docs/uk/readme.md +265 -0
- package/docs/uk/releases/v6.md +305 -0
- package/docs/uk/releases/v7.md +624 -0
- package/docs/uk/roadmap.md +346 -0
- package/docs/uk/testing.md +204 -0
- package/docs/vi/configuration.md +6 -1
- package/docs/vi/dictionary/content_file.md +6 -1
- package/docs/zh/configuration.md +6 -1
- package/docs/zh/dictionary/content_file.md +6 -1
- package/frequent_questions/ar/error-vite-env-only.md +77 -0
- package/frequent_questions/de/error-vite-env-only.md +77 -0
- package/frequent_questions/en/error-vite-env-only.md +77 -0
- package/frequent_questions/en-GB/error-vite-env-only.md +77 -0
- package/frequent_questions/es/error-vite-env-only.md +76 -0
- package/frequent_questions/fr/error-vite-env-only.md +77 -0
- package/frequent_questions/hi/error-vite-env-only.md +77 -0
- package/frequent_questions/id/error-vite-env-only.md +77 -0
- package/frequent_questions/it/error-vite-env-only.md +77 -0
- package/frequent_questions/ja/error-vite-env-only.md +77 -0
- package/frequent_questions/ko/error-vite-env-only.md +77 -0
- package/frequent_questions/pl/error-vite-env-only.md +77 -0
- package/frequent_questions/pt/error-vite-env-only.md +77 -0
- package/frequent_questions/ru/error-vite-env-only.md +77 -0
- package/frequent_questions/tr/error-vite-env-only.md +77 -0
- package/frequent_questions/uk/SSR_Next_no_[locale].md +104 -0
- package/frequent_questions/uk/array_as_content_declaration.md +72 -0
- package/frequent_questions/uk/build_dictionaries.md +58 -0
- package/frequent_questions/uk/build_error_CI_CD.md +74 -0
- package/frequent_questions/uk/bun_set_up.md +53 -0
- package/frequent_questions/uk/customized_locale_list.md +64 -0
- package/frequent_questions/uk/domain_routing.md +113 -0
- package/frequent_questions/uk/error-vite-env-only.md +77 -0
- package/frequent_questions/uk/esbuild_error.md +29 -0
- package/frequent_questions/uk/get_locale_cookie.md +142 -0
- package/frequent_questions/uk/intlayer_command_undefined.md +155 -0
- package/frequent_questions/uk/locale_incorect_in_url.md +73 -0
- package/frequent_questions/uk/package_version_error.md +181 -0
- package/frequent_questions/uk/static_rendering.md +44 -0
- package/frequent_questions/uk/translated_path_url.md +55 -0
- package/frequent_questions/uk/unknown_command.md +97 -0
- package/frequent_questions/vi/error-vite-env-only.md +77 -0
- package/frequent_questions/zh/error-vite-env-only.md +77 -0
- package/legal/uk/privacy_notice.md +83 -0
- package/legal/uk/terms_of_service.md +55 -0
- package/package.json +9 -9
- package/src/generated/blog.entry.ts +20 -0
- package/src/generated/frequentQuestions.entry.ts +20 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2025-09-10
|
|
3
|
+
updatedAt: 2025-09-10
|
|
4
|
+
title: i18n theo thành phần so với i18n tập trung: Một cách tiếp cận mới với Intlayer
|
|
5
|
+
description: Phân tích sâu các chiến lược quốc tế hóa trong React, so sánh các phương pháp tập trung, theo khóa và theo thành phần, và giới thiệu Intlayer.
|
|
6
|
+
keywords:
|
|
7
|
+
- i18n
|
|
8
|
+
- React
|
|
9
|
+
- Quốc tế hóa
|
|
10
|
+
- Intlayer
|
|
11
|
+
- Tối ưu hóa
|
|
12
|
+
- Kích thước bundle
|
|
13
|
+
slugs:
|
|
14
|
+
- blog
|
|
15
|
+
- per-component-vs-centralized-i18n
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# i18n theo thành phần so với i18n tập trung
|
|
19
|
+
|
|
20
|
+
Cách tiếp cận theo thành phần không phải là một khái niệm mới. Ví dụ, trong hệ sinh thái Vue, `vue-i18n` hỗ trợ [i18n SFC (Single File Component)](https://vue-i18n.intlify.dev/guide/advanced/sfc.html). Nuxt cũng cung cấp [bản dịch theo thành phần](https://i18n.nuxtjs.org/docs/guide/per-component-translations), và Angular sử dụng một mẫu tương tự thông qua [Feature Modules](https://v17.angular.io/guide/feature-modules) của nó.
|
|
21
|
+
|
|
22
|
+
Ngay cả trong một ứng dụng Flutter, chúng ta thường thấy mẫu sau:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lib/
|
|
26
|
+
└── features/
|
|
27
|
+
└── login/
|
|
28
|
+
├── login_screen.dart
|
|
29
|
+
└── login_screen.i18n.dart # <- Các bản dịch nằm ở đây
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```dart fileName='lib/features/login/login_screen.i18n.dart'
|
|
33
|
+
import 'package:i18n_extension/i18n_extension.dart';
|
|
34
|
+
|
|
35
|
+
extension Localization on String {
|
|
36
|
+
static var _t = Translations.byText("en") +
|
|
37
|
+
{
|
|
38
|
+
"Hello": {
|
|
39
|
+
"en": "Hello",
|
|
40
|
+
"fr": "Bonjour",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
String get i18n => localize(this, _t);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Tuy nhiên, trong thế giới React, chúng ta chủ yếu thấy các cách tiếp cận khác nhau, mà tôi sẽ gom thành ba loại:
|
|
49
|
+
|
|
50
|
+
<Columns>
|
|
51
|
+
<Column>
|
|
52
|
+
|
|
53
|
+
**Cách tiếp cận tập trung** (i18next, next-intl, react-intl, lingui)
|
|
54
|
+
|
|
55
|
+
- (không có namespaces) coi một nguồn duy nhất để truy xuất nội dung. Theo mặc định, bạn tải nội dung từ tất cả các trang khi ứng dụng của bạn tải.
|
|
56
|
+
|
|
57
|
+
</Column>
|
|
58
|
+
<Column>
|
|
59
|
+
|
|
60
|
+
**Tiếp cận chi tiết** (intlayer, inlang)
|
|
61
|
+
|
|
62
|
+
- phân nhỏ việc truy xuất nội dung theo key, hoặc theo component.
|
|
63
|
+
|
|
64
|
+
</Column>
|
|
65
|
+
</Columns>
|
|
66
|
+
|
|
67
|
+
> Trong blog này, tôi sẽ không tập trung vào các giải pháp dựa trên compiler, những cái tôi đã đề cập ở đây: [Compiler vs Declarative i18n](https://github.com/aymericzip/intlayer/blob/main/docs/blog/vi/compiler_vs_declarative_i18n.md).
|
|
68
|
+
> Lưu ý rằng i18n dựa trên compiler (ví dụ: Lingui) chỉ đơn giản tự động hóa việc trích xuất và tải nội dung. Về bản chất, chúng thường chia sẻ những hạn chế tương tự với các phương pháp khác.
|
|
69
|
+
|
|
70
|
+
> Lưu ý rằng càng phân nhỏ cách bạn truy xuất nội dung, bạn càng có nguy cơ đưa thêm trạng thái và logic vào các component.
|
|
71
|
+
|
|
72
|
+
Granular approaches are more flexible than centralized ones, but it's often a tradeoff. Even if "tree shaking" is advertised by that libraries, in practice, you'll often end up loading a page in every language.
|
|
73
|
+
|
|
74
|
+
So, broadly speaking, the decision breaks down like this:
|
|
75
|
+
|
|
76
|
+
- Nếu ứng dụng của bạn có nhiều trang hơn số ngôn ngữ, bạn nên ưu tiên phương pháp granular.
|
|
77
|
+
- Nếu bạn có nhiều ngôn ngữ hơn trang, bạn nên nghiêng về phương pháp tập trung.
|
|
78
|
+
|
|
79
|
+
Tất nhiên, các tác giả thư viện nhận thức được những giới hạn này và cung cấp các cách khắc phục. Trong số đó: tách thành namespaces, tải động các file JSON (`await import()`), hoặc loại bỏ (purge) nội dung trong quá trình build.
|
|
80
|
+
|
|
81
|
+
Đồng thời, bạn cần biết rằng khi bạn tải nội dung một cách động, bạn sẽ tạo thêm các yêu cầu tới server. Mỗi `useState` bổ sung hoặc hook đồng nghĩa với một yêu cầu server thêm.
|
|
82
|
+
|
|
83
|
+
> Để khắc phục vấn đề này, Intlayer đề xuất gom nhiều định nghĩa nội dung dưới cùng một khóa; Intlayer sau đó sẽ hợp nhất những nội dung đó.
|
|
84
|
+
|
|
85
|
+
Nhưng từ tất cả các giải pháp đó, rõ ràng rằng cách tiếp cận phổ biến nhất là cách tập trung.
|
|
86
|
+
|
|
87
|
+
### Vậy tại sao phương pháp tập trung lại được ưa chuộng đến vậy?
|
|
88
|
+
|
|
89
|
+
- Trước hết, i18next là giải pháp đầu tiên được sử dụng rộng rãi, theo triết lý lấy cảm hứng từ các kiến trúc PHP và Java (MVC), vốn dựa trên nguyên tắc tách biệt trách nhiệm nghiêm ngặt (giữ nội dung tách khỏi mã). Nó xuất hiện vào năm 2011, thiết lập các tiêu chuẩn của mình thậm chí trước cả khi có sự dịch chuyển mạnh mẽ sang kiến trúc dựa trên component (Component-Based Architectures) như React.
|
|
90
|
+
- Sau đó, một khi một thư viện được chấp nhận rộng rãi, sẽ rất khó để chuyển cả hệ sinh thái sang các mô hình khác.
|
|
91
|
+
- Việc sử dụng cách tiếp cận tập trung cũng khiến các công việc trong các hệ thống quản lý bản dịch như Crowdin, Phrase hoặc Localized trở nên dễ dàng hơn.
|
|
92
|
+
- Logic đằng sau cách tiếp cận theo từng component phức tạp hơn so với cách tiếp cận tập trung và tốn thêm thời gian để phát triển, đặc biệt khi phải giải quyết các vấn đề như xác định nội dung nằm ở đâu.
|
|
93
|
+
|
|
94
|
+
### Được, nhưng tại sao không chỉ gắn bó với cách tiếp cận tập trung?
|
|
95
|
+
|
|
96
|
+
Hãy để tôi nói lý do điều đó có thể gây vấn đề cho ứng dụng của bạn:
|
|
97
|
+
|
|
98
|
+
- **Dữ liệu không sử dụng:**
|
|
99
|
+
Khi một trang được tải, bạn thường tải luôn nội dung từ tất cả các trang khác. (Trong một ứng dụng 10 trang, đó là 90% nội dung bị tải nhưng không dùng). Bạn lazy-load một modal? Thư viện i18n cũng mặc kệ — nó vẫn tải các chuỗi lên trước.
|
|
100
|
+
- **Hiệu năng:**
|
|
101
|
+
Mỗi lần re-render, từng component của bạn đều được hydrated với một payload JSON lớn, điều này ảnh hưởng đến tính phản ứng (reactivity) của app khi nó phát triển.
|
|
102
|
+
- **Bảo trì:**
|
|
103
|
+
Quản lý các file JSON lớn rất đau đầu. Bạn phải nhảy giữa các file để chèn bản dịch, đảm bảo không thiếu bản dịch nào và không để lại các **khóa mồ côi (orphan keys)**.
|
|
104
|
+
- **Hệ thống thiết kế:**
|
|
105
|
+
Điều đó tạo ra sự không tương thích với design systems (ví dụ: một component `LoginForm`) và hạn chế việc sao chép component giữa các ứng dụng khác nhau.
|
|
106
|
+
|
|
107
|
+
**"Nhưng chúng ta đã phát minh ra Namespaces!"**
|
|
108
|
+
|
|
109
|
+
Chắc chắn, và đó là một bước tiến lớn. Hãy xem so sánh kích thước main bundle của một cấu hình Vite + React + React Router v7 + Intlayer. Chúng tôi đã mô phỏng một ứng dụng 20 trang.
|
|
110
|
+
|
|
111
|
+
Ví dụ đầu tiên không bao gồm việc lazy-load các bản dịch theo locale và không tách namespace. Ví dụ thứ hai bao gồm content purging + tải động các bản dịch.
|
|
112
|
+
|
|
113
|
+
| Bundle tối ưu hóa | Bundle không tối ưu hóa |
|
|
114
|
+
| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
115
|
+
|  |  |
|
|
116
|
+
|
|
117
|
+
Nhờ có namespaces, chúng ta đã chuyển từ cấu trúc này:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
locale/
|
|
121
|
+
├── en.json
|
|
122
|
+
├── fr.json
|
|
123
|
+
└── es.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
sang cấu trúc này:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
locale/
|
|
130
|
+
├── en/
|
|
131
|
+
│ ├── common.json
|
|
132
|
+
│ ├── navbar.json
|
|
133
|
+
│ ├── footer.json
|
|
134
|
+
│ ├── home.json
|
|
135
|
+
│ └── about.json
|
|
136
|
+
├── fr/
|
|
137
|
+
│ └── ...
|
|
138
|
+
└── es/
|
|
139
|
+
└── ...
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Bây giờ bạn phải quản lý một cách chi tiết phần nội dung nào của ứng dụng nên được tải và ở đâu. Kết luận là đại đa số các dự án chỉ bỏ qua phần này vì tính phức tạp (xem hướng dẫn [next-i18next](https://github.com/aymericzip/intlayer/blob/main/docs/blog/vi/i18n_using_next-i18next.md) để thấy các thách thức mà việc (chỉ) tuân theo các best practices mang lại).
|
|
144
|
+
Do đó, các dự án đó cuối cùng gặp phải vấn đề tải JSON khổng lồ đã được giải thích ở phần trước.
|
|
145
|
+
|
|
146
|
+
> Lưu ý rằng vấn đề này không đặc thù cho i18next, mà áp dụng cho tất cả các phương pháp tập trung được liệt kê ở trên.
|
|
147
|
+
|
|
148
|
+
Tuy nhiên, tôi muốn nhắc bạn rằng không phải mọi cách tiếp cận theo hướng phân mảnh đều giải quyết được vấn đề này. Ví dụ, các cách tiếp cận như `vue-i18n SFC` hay `inlang` không tự động lazy load bản dịch theo từng locale, nên bạn chỉ đang đánh đổi vấn đề kích thước bundle này sang một vấn đề khác.
|
|
149
|
+
|
|
150
|
+
Hơn nữa, nếu không tách biệt rõ ràng các mối quan tâm (separation of concerns), việc trích xuất và cung cấp bản dịch cho người dịch để họ xem xét sẽ trở nên khó khăn hơn nhiều.
|
|
151
|
+
|
|
152
|
+
### Cách tiếp cận per-component của Intlayer giải quyết vấn đề này
|
|
153
|
+
|
|
154
|
+
Intlayer thực hiện theo một số bước:
|
|
155
|
+
|
|
156
|
+
1. **Khai báo:** Khai báo nội dung ở bất kỳ đâu trong codebase của bạn bằng các file `*.content.{ts|jsx|cjs|json|json5|...}`. Điều này đảm bảo tách biệt các mối quan tâm trong khi vẫn giữ nội dung được colocated. Một file nội dung có thể là theo từng locale hoặc đa ngôn ngữ.
|
|
157
|
+
2. **Xử lý:** Intlayer thực hiện một bước build để xử lý logic JS, xử lý các fallback cho bản dịch bị thiếu, sinh các kiểu TypeScript, quản lý nội dung trùng lặp, lấy nội dung từ CMS của bạn, và nhiều thứ khác.
|
|
158
|
+
3. **Thanh lọc:** Khi ứng dụng của bạn được build, Intlayer sẽ loại bỏ nội dung không dùng (tương tự cách Tailwind quản lý các class) bằng cách thay thế nội dung như sau:
|
|
159
|
+
|
|
160
|
+
**Khai báo:**
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
// src/MyComponent.tsx
|
|
164
|
+
export const MyComponent = () => {
|
|
165
|
+
const content = useIntlayer("my-key");
|
|
166
|
+
return <h1>{content.title}</h1>;
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
// src/myComponent.content.ts
|
|
172
|
+
export const {
|
|
173
|
+
key: "my-key",
|
|
174
|
+
content: t({
|
|
175
|
+
en: { title: "My title" },
|
|
176
|
+
fr: { title: "Mon titre" }
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Xử lý:** Intlayer xây dựng dictionary dựa trên file `.content` và sinh ra:
|
|
183
|
+
|
|
184
|
+
```json5
|
|
185
|
+
// .intlayer/dynamic_dictionary/en/my-key.json
|
|
186
|
+
{
|
|
187
|
+
"key": "my-key",
|
|
188
|
+
"content": { "title": "My title" },
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Thay thế:** Intlayer biến đổi component của bạn trong quá trình build ứng dụng.
|
|
193
|
+
|
|
194
|
+
**- Chế độ Import Tĩnh:**
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// Biểu diễn component theo cú pháp tương tự JSX
|
|
198
|
+
export const MyComponent = () => {
|
|
199
|
+
const content = useDictionary({
|
|
200
|
+
key: "my-key",
|
|
201
|
+
content: {
|
|
202
|
+
nodeType: "translation",
|
|
203
|
+
translation: {
|
|
204
|
+
en: { title: "My title" },
|
|
205
|
+
fr: { title: "Mon titre" },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return <h1>{content.title}</h1>;
|
|
211
|
+
};
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**- Chế độ Import Động:**
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
// Biểu diễn component theo cú pháp tương tự JSX
|
|
218
|
+
export const MyComponent = () => {
|
|
219
|
+
const content = useDictionaryAsync({
|
|
220
|
+
en: () =>
|
|
221
|
+
import(".intlayer/dynamic_dictionary/en/my-key.json", {
|
|
222
|
+
with: { type: "json" },
|
|
223
|
+
}).then((mod) => mod.default),
|
|
224
|
+
// Tương tự cho các ngôn ngữ khác
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return <h1>{content.title}</h1>;
|
|
228
|
+
};
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
> `useDictionaryAsync` sử dụng cơ chế giống Suspense để chỉ tải JSON đã được địa phương hóa khi cần.
|
|
232
|
+
|
|
233
|
+
**Lợi ích chính của cách tiếp cận theo thành phần này:**
|
|
234
|
+
|
|
235
|
+
- Giữ khai báo nội dung gần với các components của bạn giúp việc bảo trì tốt hơn (ví dụ: di chuyển một component sang một app hoặc design system khác. Xóa thư mục component sẽ xóa luôn nội dung liên quan, giống như bạn có lẽ vẫn làm với các file `.test`, `.stories`)
|
|
236
|
+
|
|
237
|
+
- Cách tiếp cận theo từng component ngăn các agent AI phải lục qua tất cả các file khác nhau của bạn. Nó xử lý tất cả các bản dịch tại một chỗ, giảm độ phức tạp của nhiệm vụ và lượng token sử dụng.
|
|
238
|
+
|
|
239
|
+
### Hạn chế
|
|
240
|
+
|
|
241
|
+
Tất nhiên, cách tiếp cận này đi kèm với những đánh đổi:
|
|
242
|
+
|
|
243
|
+
- Khó kết nối với các hệ thống l10n khác và các tooling bổ sung.
|
|
244
|
+
- Bạn có nguy cơ bị lock-in (điều này cơ bản đã xảy ra với bất kỳ giải pháp i18n nào do cú pháp đặc thù của chúng).
|
|
245
|
+
|
|
246
|
+
Đó là lý do Intlayer cố gắng cung cấp một bộ công cụ hoàn chỉnh cho i18n (100% miễn phí và OSS), bao gồm dịch AI sử dụng AI Provider và API keys của riêng bạn. Intlayer cũng cung cấp công cụ để đồng bộ hóa JSON của bạn, hoạt động giống như các message formatter của ICU / vue-i18n / i18next để ánh xạ nội dung sang định dạng tương ứng.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2025-09-10
|
|
3
|
+
updatedAt: 2025-09-10
|
|
4
|
+
title: 每组件(Per-Component)与集中式(Centralized)i18n:Intlayer 的新方法
|
|
5
|
+
description: 深入探讨 React 国际化策略,对比集中式、按键(per-key)和按组件(per-component)方法,并介绍 Intlayer。
|
|
6
|
+
keywords:
|
|
7
|
+
- i18n
|
|
8
|
+
- React
|
|
9
|
+
- Internationalization
|
|
10
|
+
- Intlayer
|
|
11
|
+
- Optimization
|
|
12
|
+
- Bundle Size
|
|
13
|
+
slugs:
|
|
14
|
+
- blog
|
|
15
|
+
- per-component-vs-centralized-i18n
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# 每组件(Per-Component)与集中式(Centralized)i18n
|
|
19
|
+
|
|
20
|
+
每组件(per-component)方法并非新概念。例如,在 Vue 生态系统中,`vue-i18n` 支持 [SFC i18n(单文件组件)](https://vue-i18n.intlify.dev/guide/advanced/sfc.html)。Nuxt 也提供 [按组件翻译](https://i18n.nuxtjs.org/docs/guide/per-component-translations),Angular 通过其 [Feature Modules](https://v17.angular.io/guide/feature-modules) 采用类似的模式。
|
|
21
|
+
|
|
22
|
+
即使在 Flutter 应用中,我们也常能发现这样的模式:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lib/
|
|
26
|
+
└── features/
|
|
27
|
+
└── login/
|
|
28
|
+
├── login_screen.dart
|
|
29
|
+
└── login_screen.i18n.dart # <- 翻译存放在这里
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```dart fileName='lib/features/login/login_screen.i18n.dart'
|
|
33
|
+
import 'package:i18n_extension/i18n_extension.dart';
|
|
34
|
+
|
|
35
|
+
extension Localization on String {
|
|
36
|
+
static var _t = Translations.byText("en") +
|
|
37
|
+
{
|
|
38
|
+
"Hello": {
|
|
39
|
+
"en": "Hello",
|
|
40
|
+
"fr": "Bonjour",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
String get i18n => localize(this, _t);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
然而在 React 领域,我们主要看到不同的做法,我会将它们分为三类:
|
|
49
|
+
|
|
50
|
+
<Columns>
|
|
51
|
+
<Column>
|
|
52
|
+
|
|
53
|
+
**集中式方法**(i18next、next-intl、react-intl、lingui)
|
|
54
|
+
|
|
55
|
+
- (无命名空间)将内容视为单一来源进行检索。默认情况下,当应用加载时,会从所有页面加载内容。
|
|
56
|
+
|
|
57
|
+
</Column>
|
|
58
|
+
<Column>
|
|
59
|
+
|
|
60
|
+
**细粒度方法** (intlayer, inlang)
|
|
61
|
+
|
|
62
|
+
- 按键或按组件对内容检索进行细化。
|
|
63
|
+
|
|
64
|
+
</Column>
|
|
65
|
+
</Columns>
|
|
66
|
+
|
|
67
|
+
> 在本博文中,我不会专注于基于编译器的解决方案,我已经在这里覆盖过:[Compiler vs Declarative i18n](https://github.com/aymericzip/intlayer/blob/main/docs/blog/zh/compiler_vs_declarative_i18n.md).
|
|
68
|
+
> 注意,基于编译器的 i18n(例如 Lingui)只是自动化了内容的提取和加载。在底层,它们通常与其他方法共享相同的限制。
|
|
69
|
+
|
|
70
|
+
> 注意,你越细化内容的检索方式,就越有可能将额外的 state 和逻辑插入到组件中。
|
|
71
|
+
|
|
72
|
+
细粒度方法比集中式方法更灵活,但这通常是一种权衡。即使这些 libraries 宣称支持 "tree shaking",在实际中,你通常仍会以每种语言加载整个页面。
|
|
73
|
+
|
|
74
|
+
所以,概括来说,决策大致可以分为:
|
|
75
|
+
|
|
76
|
+
- 如果你的应用页面数量多于语言数量,应优先采用细粒度方法。
|
|
77
|
+
- 如果语言数量多于页面数量,则应倾向于集中式方法。
|
|
78
|
+
|
|
79
|
+
当然,library 的作者们也意识到这些限制并提供了解决方案。
|
|
80
|
+
其中包括:将内容拆分为命名空间、动态加载 JSON 文件(`await import()`),或在构建时剔除内容。
|
|
81
|
+
|
|
82
|
+
与此同时,你应该知道,当你动态加载内容时,会向服务器引入额外的请求。每多一个 `useState` 或 hook,就意味着一次额外的服务器请求。
|
|
83
|
+
|
|
84
|
+
> 为了解决这一点,Intlayer 建议将多个内容定义分组到同一个键下,Intlayer 会合并这些内容。
|
|
85
|
+
|
|
86
|
+
但综观这些解决方案,最流行的方法显然是集中式的。
|
|
87
|
+
|
|
88
|
+
### 那么为什么集中式方法如此受欢迎?
|
|
89
|
+
|
|
90
|
+
- 首先,i18next 是第一个被广泛采用的解决方案,其理念受 PHP 和 Java 架构(MVC)的启发,依赖于严格的关注点分离(将内容与代码分离)。它于 2011 年出现,在向基于组件的架构(如 React)大规模转变之前就确立了其标准。
|
|
91
|
+
- 此外,一旦某个库被广泛采用,就很难将生态系统转向其他模式。
|
|
92
|
+
- 在 Crowdin、Phrase 或 Localized 等翻译管理系统中,使用集中式方法也更为方便。
|
|
93
|
+
- 按组件(per-component)方法背后的逻辑比集中式更复杂,开发需要更多时间,尤其是在需要解决诸如识别内容位置等问题时。
|
|
94
|
+
|
|
95
|
+
### 好的,但为什么不直接坚持集中式方法?
|
|
96
|
+
|
|
97
|
+
让我告诉你这对你的应用可能会带来哪些问题:
|
|
98
|
+
|
|
99
|
+
- **未使用的数据:**
|
|
100
|
+
当一个页面加载时,你通常会加载来自所有其他页面的内容。(在一个 10 页的应用中,那就是 90% 未使用的内容被加载)。你懒加载一个 modal?i18n 库并不在意,它反正会先加载这些字符串。
|
|
101
|
+
- **性能:**
|
|
102
|
+
每次重新渲染时,你的每个组件都会被一个巨大的 JSON payload 进行 hydrate,这会随着应用增长影响其响应性。
|
|
103
|
+
- **维护:**
|
|
104
|
+
维护大型 JSON 文件很痛苦。你必须在文件之间来回跳转以插入翻译,确保没有翻译缺失且没有孤立的 key 留下。
|
|
105
|
+
- **设计系统:**
|
|
106
|
+
这会导致与设计系统不兼容(例如,`LoginForm` 组件),并限制在不同应用之间复制组件的能力。
|
|
107
|
+
|
|
108
|
+
**“但我们发明了 Namespaces!”**
|
|
109
|
+
|
|
110
|
+
当然,这确实是一个巨大的进步。下面对比一下在 Vite + React + React Router v7 + Intlayer 配置下主 bundle 大小的差异。我们模拟了一个 20 页的应用。
|
|
111
|
+
|
|
112
|
+
第一个示例没有为每个 locale 进行懒加载翻译,也没有进行命名空间拆分。第二个示例则包含内容清理(purging)和翻译的动态加载。
|
|
113
|
+
|
|
114
|
+
| 已优化的 bundle | 未优化的 bundle |
|
|
115
|
+
| -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
116
|
+
|  |  |
|
|
117
|
+
|
|
118
|
+
因此,多亏了 namespaces,我们将结构从以下形式迁移:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
locale/
|
|
122
|
+
├── en.json
|
|
123
|
+
├── fr.json
|
|
124
|
+
└── es.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
到这个结构:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
locale/
|
|
131
|
+
├── en/
|
|
132
|
+
│ ├── common.json
|
|
133
|
+
│ ├── navbar.json
|
|
134
|
+
│ ├── footer.json
|
|
135
|
+
│ ├── home.json
|
|
136
|
+
│ └── about.json
|
|
137
|
+
├── fr/
|
|
138
|
+
│ └── ...
|
|
139
|
+
└── es/
|
|
140
|
+
└── ...
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
现在你必须精细地管理应用的哪些内容应该被加载,以及在何处加载它们。总之,由于复杂性,绝大多数项目都会跳过这一步(例如参见 [next-i18next 指南](https://github.com/aymericzip/intlayer/blob/main/docs/blog/zh/i18n_using_next-i18next.md) 来了解仅仅遵循良好实践也会带来哪些挑战)。因此,这些项目最终会遇到前面解释的庞大 JSON 加载问题。
|
|
145
|
+
|
|
146
|
+
> 注意,这个问题并非 i18next 所特有,而是上述所有集中式方法共有的问题。
|
|
147
|
+
|
|
148
|
+
然而,我想提醒你,并非所有细粒度方法都能解决这个问题。例如,`vue-i18n SFC` 或 `inlang` 的做法并不会本质上为每个语言环境按需懒加载翻译,因此你只是将捆绑包大小的问题换成了另一个问题。
|
|
149
|
+
|
|
150
|
+
此外,如果没有适当的关注点分离,就更难将翻译内容提取并提供给译者进行审核。
|
|
151
|
+
|
|
152
|
+
### Intlayer 的按组件方法如何解决这个问题
|
|
153
|
+
|
|
154
|
+
Intlayer 通过以下几个步骤来处理:
|
|
155
|
+
|
|
156
|
+
1. **声明:** 在代码库的任何位置使用 `*.content.{ts|jsx|cjs|json|json5|...}` 文件声明你的内容。这既确保了关注点分离,又保持内容与组件同处一处。内容文件可以是针对单一语言的,也可以是多语言的。
|
|
157
|
+
2. **Processing:** Intlayer 在构建步骤中运行,用于处理 JS 逻辑、处理缺失的翻译回退、生成 TypeScript 类型、管理重复内容、从你的 CMS 获取内容,等等。
|
|
158
|
+
3. **Purging:** 当你的应用构建时,Intlayer 会清除未使用的内容(有点像 Tailwind 管理你的类的方式),通过如下方式替换内容:
|
|
159
|
+
|
|
160
|
+
**Declaration:**
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
// src/MyComponent.tsx
|
|
164
|
+
export const MyComponent = () => {
|
|
165
|
+
const content = useIntlayer("my-key");
|
|
166
|
+
return <h1>{content.title}</h1>;
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
// src/myComponent.content.ts
|
|
172
|
+
export const {
|
|
173
|
+
key: "my-key",
|
|
174
|
+
content: t({
|
|
175
|
+
zh: { title: "我的标题" },
|
|
176
|
+
en: { title: "My title" },
|
|
177
|
+
fr: { title: "Mon titre" }
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Processing:** Intlayer builds the dictionary based on the `.content` file and generates:
|
|
184
|
+
|
|
185
|
+
```json5
|
|
186
|
+
// .intlayer/dynamic_dictionary/zh/my-key.json(翻译后的 JSON 文件示例)
|
|
187
|
+
{
|
|
188
|
+
"key": "my-key",
|
|
189
|
+
"content": { "title": "我的标题" },
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**替换:** Intlayer 在应用构建期间转换你的组件。
|
|
194
|
+
|
|
195
|
+
**- 静态导入模式:**
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// 在类 JSX 语法中的组件表示
|
|
199
|
+
export const MyComponent = () => {
|
|
200
|
+
const content = useDictionary({
|
|
201
|
+
key: "my-key",
|
|
202
|
+
content: {
|
|
203
|
+
nodeType: "translation",
|
|
204
|
+
translation: {
|
|
205
|
+
zh: { title: "我的标题" },
|
|
206
|
+
en: { title: "My title" },
|
|
207
|
+
fr: { title: "Mon titre" },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return <h1>{content.title}</h1>;
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**- 动态导入模式:**
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// 在类 JSX 语法中的组件表示
|
|
220
|
+
export const MyComponent = () => {
|
|
221
|
+
const content = useDictionaryAsync({
|
|
222
|
+
en: () =>
|
|
223
|
+
import(".intlayer/dynamic_dictionary/en/my-key.json", {
|
|
224
|
+
with: { type: "json" },
|
|
225
|
+
}).then((mod) => mod.default),
|
|
226
|
+
// Same for other languages
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return <h1>{content.title}</h1>;
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
> `useDictionaryAsync` 使用类似 Suspense 的机制,仅在需要时加载本地化的 JSON。
|
|
234
|
+
|
|
235
|
+
**此按组件方法的主要优点:**
|
|
236
|
+
|
|
237
|
+
- 将内容声明与组件放在一起可以提高可维护性(例如:将组件移动到另一个应用或 design system。删除组件文件夹时也会同时移除相关内容,就像你通常对 `.test`、`.stories` 所做的那样)
|
|
238
|
+
|
|
239
|
+
- 以组件为单位的方法可以防止 AI 代理需要在你所有不同的文件之间来回跳转。它将所有翻译集中在一个地方,限制了任务的复杂性以及使用的 tokens 数量。
|
|
240
|
+
|
|
241
|
+
### 限制
|
|
242
|
+
|
|
243
|
+
当然,这种方法有其权衡:
|
|
244
|
+
|
|
245
|
+
- 更难与其他 l10n 系统和额外的工具链对接。
|
|
246
|
+
- 会产生锁定(lock-in)问题(这在任何 i18n 解决方案中基本都存在,因为它们有特定的语法)。
|
|
247
|
+
|
|
248
|
+
这就是 Intlayer 试图为 i18n 提供完整工具集(100% 免费且 OSS)的原因,包括使用你自己的 AI Provider 和 API 密钥进行 AI 翻译的功能。Intlayer 还提供用于同步你的 JSON 的工具,类似于 ICU / vue-i18n / i18next 的消息格式化器,用以将内容映射到它们的特定格式。
|
package/dist/cjs/common.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.cjs","names":["Locales"
|
|
1
|
+
{"version":3,"file":"common.cjs","names":["Locales"],"sources":["../../src/common.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { getLocalizedUrl, getMarkdownMetadata } from '@intlayer/core';\nimport { Locales, type LocalesValues } from '@intlayer/types';\n\nexport const defaultLocale = Locales.ENGLISH;\n\nexport const GITHUB_URL_PREFIX =\n 'https://github.com/aymericzip/intlayer/blob/main/docs';\nexport const URL_PREFIX = 'https://intlayer.org/';\n\nexport const getKeys = <T extends Record<string, any>>(obj: T): (keyof T)[] =>\n Object.keys(obj) as (keyof T)[];\n\nexport const getFiles = async <\n F extends Record<`./${string}`, Record<LocalesValues, Promise<string>>>,\n>(\n files: F,\n lang: LocalesValues = defaultLocale as LocalesValues\n): Promise<Record<string, string>> => {\n const filesEntries = await Promise.all(\n Object.entries(files)\n .map(([key, value]) => [key, value[lang as LocalesValues]])\n .map(async ([key, value]) => [key, await value])\n );\n const filesResult = Object.fromEntries(filesEntries);\n return filesResult;\n};\n\nexport const getFile = async <\n F extends Record<string, Record<LocalesValues, Promise<string>>>,\n>(\n files: F,\n docKey: keyof F,\n locale: LocalesValues = defaultLocale as LocalesValues\n): Promise<string> => {\n const fileRecord = files[docKey];\n\n if (!fileRecord) {\n throw new Error(`File ${docKey as string} not found`);\n }\n\n const file = await files[docKey]?.[locale];\n\n if (!file) {\n const englishFile = await files[docKey][defaultLocale as LocalesValues];\n\n if (!englishFile) {\n throw new Error(`File ${docKey as string} not found`);\n }\n\n return englishFile;\n }\n\n return file;\n};\n\nexport type FileMetadata = {\n docKey: string;\n url: string;\n relativeUrl: string;\n githubUrl: string;\n title: string;\n slugs: string[];\n description: string;\n keywords: string[];\n updatedAt: string;\n createdAt: string;\n author?: string;\n youtubeVideo?: string;\n applicationTemplate?: string;\n history?: {\n version: string;\n date: string;\n changes: string;\n }[];\n};\n\nexport const formatMetadata = (\n docKey: string,\n file: string,\n locale: LocalesValues = defaultLocale as LocalesValues\n): FileMetadata => {\n const metadata = getMarkdownMetadata(file);\n\n const slugs = (metadata.slugs ?? []).map(String);\n const keywords = (metadata.keywords ?? []).map(String);\n\n const relativeUrl = join('/', ...slugs);\n\n const slicedDocKey = docKey.slice(1);\n\n return {\n ...metadata,\n docKey,\n slugs,\n keywords,\n githubUrl: `${GITHUB_URL_PREFIX}${slicedDocKey}`.replace(\n '/en/',\n `/${locale}/`\n ),\n relativeUrl: getLocalizedUrl(relativeUrl, locale),\n url: getLocalizedUrl(join(URL_PREFIX, relativeUrl), locale),\n } as FileMetadata;\n};\n\nexport const getFileMetadata = async <\n F extends Record<string, Record<LocalesValues, Promise<string>>>,\n R extends FileMetadata,\n>(\n files: F,\n docKey: keyof F,\n locale: LocalesValues = defaultLocale as LocalesValues\n): Promise<R> => {\n const file = await getFile(files, docKey, locale);\n\n return formatMetadata(docKey as string, file, locale) as R;\n};\n\nexport const getFileMetadataRecord = async <\n F extends Record<string, Record<LocalesValues, Promise<string>>>,\n>(\n files: F,\n locale: LocalesValues = defaultLocale as LocalesValues\n): Promise<Record<keyof F, FileMetadata>> => {\n const filesEntries = await Promise.all(\n Object.entries(files).map(async ([key]) => [\n key,\n await getFileMetadata(files, key as keyof F, locale),\n ])\n );\n const filesResult = Object.fromEntries(filesEntries);\n return filesResult;\n};\n\nexport const getFileMetadataBySlug = async <\n F extends Record<string, Record<LocalesValues, Promise<string>>>,\n>(\n files: F,\n slugs: string | string[],\n locale: LocalesValues = defaultLocale as LocalesValues,\n strict = false\n) => {\n const slugsArray = Array.isArray(slugs) ? slugs : [slugs];\n const filesMetadata = await getFileMetadataRecord(\n files,\n defaultLocale as LocalesValues\n );\n\n let fileMetadataArray: FileMetadata[] = Object.values(filesMetadata).filter(\n (fileMetadata) =>\n slugsArray.every((slug) => fileMetadata.slugs?.includes(slug))\n );\n\n if (strict) {\n fileMetadataArray = fileMetadataArray.filter(\n (fileMetadata) => fileMetadata.slugs.length === slugsArray.length\n );\n }\n\n if (locale !== defaultLocale) {\n const localizedFileMetadata = await Promise.all(\n fileMetadataArray.map(\n async (fileMetadata) =>\n await getFileMetadata(files, fileMetadata.docKey, locale)\n )\n );\n\n return localizedFileMetadata;\n }\n\n return fileMetadataArray;\n};\n\nexport const getFileBySlug = async <\n F extends Record<string, Record<LocalesValues, Promise<string>>>,\n>(\n files: F,\n slugs: string | string[],\n locale: LocalesValues = defaultLocale as LocalesValues,\n strict = false\n) => {\n const slugsArray = Array.isArray(slugs) ? slugs : [slugs];\n const filesMetadata = await getFileMetadataRecord(\n files,\n defaultLocale as LocalesValues\n );\n\n let fileMetadataArray = Object.values(filesMetadata).filter((fileMetadata) =>\n slugsArray.every((slug) => fileMetadata.slugs?.includes(slug))\n );\n\n if (strict) {\n fileMetadataArray = fileMetadataArray.filter(\n (fileMetadata) => fileMetadata.slugs.length === slugsArray.length\n );\n }\n\n const fileList = await Promise.all(\n fileMetadataArray.map(async (fileMetadata) => {\n const file = await getFile(files, fileMetadata.docKey, locale);\n return file;\n })\n );\n\n return fileList;\n};\n"],"mappings":"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"}
|
|
@@ -524,6 +524,26 @@ const blogEntry = {
|
|
|
524
524
|
vi: readLocale("nextjs-multilingual-seo-comparison.md", "vi"),
|
|
525
525
|
uk: readLocale("nextjs-multilingual-seo-comparison.md", "uk")
|
|
526
526
|
},
|
|
527
|
+
"./blog/en/per-component_vs_centralized_i18n.md": {
|
|
528
|
+
en: readLocale("per-component_vs_centralized_i18n.md", "en"),
|
|
529
|
+
ru: readLocale("per-component_vs_centralized_i18n.md", "ru"),
|
|
530
|
+
ja: readLocale("per-component_vs_centralized_i18n.md", "ja"),
|
|
531
|
+
fr: readLocale("per-component_vs_centralized_i18n.md", "fr"),
|
|
532
|
+
ko: readLocale("per-component_vs_centralized_i18n.md", "ko"),
|
|
533
|
+
zh: readLocale("per-component_vs_centralized_i18n.md", "zh"),
|
|
534
|
+
es: readLocale("per-component_vs_centralized_i18n.md", "es"),
|
|
535
|
+
de: readLocale("per-component_vs_centralized_i18n.md", "de"),
|
|
536
|
+
ar: readLocale("per-component_vs_centralized_i18n.md", "ar"),
|
|
537
|
+
it: readLocale("per-component_vs_centralized_i18n.md", "it"),
|
|
538
|
+
"en-GB": readLocale("per-component_vs_centralized_i18n.md", "en-GB"),
|
|
539
|
+
pt: readLocale("per-component_vs_centralized_i18n.md", "pt"),
|
|
540
|
+
hi: readLocale("per-component_vs_centralized_i18n.md", "hi"),
|
|
541
|
+
tr: readLocale("per-component_vs_centralized_i18n.md", "tr"),
|
|
542
|
+
pl: readLocale("per-component_vs_centralized_i18n.md", "pl"),
|
|
543
|
+
id: readLocale("per-component_vs_centralized_i18n.md", "id"),
|
|
544
|
+
vi: readLocale("per-component_vs_centralized_i18n.md", "vi"),
|
|
545
|
+
uk: readLocale("per-component_vs_centralized_i18n.md", "uk")
|
|
546
|
+
},
|
|
527
547
|
"./blog/en/rag_powered_documentation_assistant.md": {
|
|
528
548
|
en: readLocale("rag_powered_documentation_assistant.md", "en"),
|
|
529
549
|
ru: readLocale("rag_powered_documentation_assistant.md", "ru"),
|