@intlayer/docs 7.5.13 → 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.
Files changed (35) hide show
  1. package/blog/ar/per-component_vs_centralized_i18n.md +248 -0
  2. package/blog/de/per-component_vs_centralized_i18n.md +248 -0
  3. package/blog/en/_per-component_vs_centralized_i18n.md +252 -0
  4. package/blog/en/per-component_vs_centralized_i18n.md +248 -0
  5. package/blog/en-GB/per-component_vs_centralized_i18n.md +247 -0
  6. package/blog/es/per-component_vs_centralized_i18n.md +245 -0
  7. package/blog/fr/per-component_vs_centralized_i18n.md +245 -0
  8. package/blog/hi/per-component_vs_centralized_i18n.md +249 -0
  9. package/blog/id/per-component_vs_centralized_i18n.md +248 -0
  10. package/blog/it/per-component_vs_centralized_i18n.md +247 -0
  11. package/blog/ja/per-component_vs_centralized_i18n.md +247 -0
  12. package/blog/ko/per-component_vs_centralized_i18n.md +246 -0
  13. package/blog/pl/per-component_vs_centralized_i18n.md +247 -0
  14. package/blog/pt/per-component_vs_centralized_i18n.md +246 -0
  15. package/blog/ru/per-component_vs_centralized_i18n.md +251 -0
  16. package/blog/tr/per-component_vs_centralized_i18n.md +244 -0
  17. package/blog/uk/per-component_vs_centralized_i18n.md +248 -0
  18. package/blog/vi/per-component_vs_centralized_i18n.md +246 -0
  19. package/blog/zh/per-component_vs_centralized_i18n.md +248 -0
  20. package/dist/cjs/common.cjs.map +1 -1
  21. package/dist/cjs/generated/blog.entry.cjs +20 -0
  22. package/dist/cjs/generated/blog.entry.cjs.map +1 -1
  23. package/dist/cjs/generated/docs.entry.cjs.map +1 -1
  24. package/dist/cjs/generated/frequentQuestions.entry.cjs.map +1 -1
  25. package/dist/cjs/generated/legal.entry.cjs.map +1 -1
  26. package/dist/esm/common.mjs.map +1 -1
  27. package/dist/esm/generated/blog.entry.mjs +20 -0
  28. package/dist/esm/generated/blog.entry.mjs.map +1 -1
  29. package/dist/esm/generated/docs.entry.mjs.map +1 -1
  30. package/dist/esm/generated/frequentQuestions.entry.mjs.map +1 -1
  31. package/dist/esm/generated/legal.entry.mjs.map +1 -1
  32. package/dist/types/generated/blog.entry.d.ts +1 -0
  33. package/dist/types/generated/blog.entry.d.ts.map +1 -1
  34. package/package.json +9 -9
  35. package/src/generated/blog.entry.ts +20 -0
@@ -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
+ | ![未优化的 bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png?raw=true) | ![已优化的 bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png?raw=true) |
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 的消息格式化器,用以将内容映射到它们的特定格式。
@@ -1 +1 @@
1
- {"version":3,"file":"common.cjs","names":["Locales","fileMetadataArray: FileMetadata[]"],"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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"}
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"),