@intlayer/docs 9.2.0 → 9.3.0

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 (89) hide show
  1. package/blog/ar/index.md +1 -1
  2. package/blog/de/index.md +1 -1
  3. package/blog/en/index.md +1 -1
  4. package/blog/en-GB/index.md +1 -1
  5. package/blog/es/index.md +1 -1
  6. package/blog/fr/index.md +1 -1
  7. package/blog/hi/index.md +1 -1
  8. package/blog/id/index.md +1 -1
  9. package/blog/it/index.md +1 -1
  10. package/blog/ja/index.md +1 -1
  11. package/blog/ko/index.md +1 -1
  12. package/blog/pl/index.md +1 -1
  13. package/blog/pt/index.md +1 -1
  14. package/blog/ru/index.md +1 -1
  15. package/blog/uk/index.md +1 -1
  16. package/blog/vi/index.md +1 -1
  17. package/blog/zh/index.md +1 -1
  18. package/dist/cjs/common.cjs +24 -1
  19. package/dist/cjs/common.cjs.map +1 -1
  20. package/dist/cjs/generated/blog.entry.cjs +31 -4
  21. package/dist/cjs/generated/blog.entry.cjs.map +1 -1
  22. package/dist/cjs/generated/docs.entry.cjs +31 -4
  23. package/dist/cjs/generated/docs.entry.cjs.map +1 -1
  24. package/dist/cjs/generated/frequentQuestions.entry.cjs +31 -4
  25. package/dist/cjs/generated/frequentQuestions.entry.cjs.map +1 -1
  26. package/dist/cjs/generated/legal.entry.cjs +31 -4
  27. package/dist/cjs/generated/legal.entry.cjs.map +1 -1
  28. package/dist/esm/common.mjs +24 -1
  29. package/dist/esm/common.mjs.map +1 -1
  30. package/dist/esm/generated/blog.entry.mjs +31 -4
  31. package/dist/esm/generated/blog.entry.mjs.map +1 -1
  32. package/dist/esm/generated/docs.entry.mjs +31 -4
  33. package/dist/esm/generated/docs.entry.mjs.map +1 -1
  34. package/dist/esm/generated/frequentQuestions.entry.mjs +31 -4
  35. package/dist/esm/generated/frequentQuestions.entry.mjs.map +1 -1
  36. package/dist/esm/generated/legal.entry.mjs +31 -4
  37. package/dist/esm/generated/legal.entry.mjs.map +1 -1
  38. package/dist/types/common.d.ts.map +1 -1
  39. package/dist/types/generated/blog.entry.d.ts.map +1 -1
  40. package/dist/types/generated/docs.entry.d.ts.map +1 -1
  41. package/dist/types/generated/frequentQuestions.entry.d.ts.map +1 -1
  42. package/dist/types/generated/legal.entry.d.ts.map +1 -1
  43. package/docs/ar/bundle_optimization.md +58 -25
  44. package/docs/ar/live-sync.md +4 -0
  45. package/docs/bn/bundle_optimization.md +58 -25
  46. package/docs/cs/bundle_optimization.md +58 -25
  47. package/docs/de/bundle_optimization.md +58 -25
  48. package/docs/de/live-sync.md +4 -0
  49. package/docs/en/bundle_optimization.md +51 -23
  50. package/docs/en/live-sync.md +4 -0
  51. package/docs/en-GB/bundle_optimization.md +58 -25
  52. package/docs/en-GB/live-sync.md +4 -0
  53. package/docs/es/bundle_optimization.md +58 -25
  54. package/docs/es/live-sync.md +4 -0
  55. package/docs/fr/bundle_optimization.md +58 -25
  56. package/docs/fr/live-sync.md +4 -0
  57. package/docs/hi/bundle_optimization.md +58 -25
  58. package/docs/hi/live-sync.md +4 -0
  59. package/docs/id/bundle_optimization.md +58 -25
  60. package/docs/id/live-sync.md +4 -0
  61. package/docs/it/bundle_optimization.md +58 -25
  62. package/docs/it/live-sync.md +4 -0
  63. package/docs/ja/bundle_optimization.md +58 -25
  64. package/docs/ja/live-sync.md +4 -0
  65. package/docs/ko/bundle_optimization.md +58 -25
  66. package/docs/ko/live-sync.md +4 -0
  67. package/docs/nl/bundle_optimization.md +58 -25
  68. package/docs/pl/bundle_optimization.md +58 -25
  69. package/docs/pl/live-sync.md +4 -0
  70. package/docs/pt/bundle_optimization.md +58 -24
  71. package/docs/pt/live-sync.md +4 -0
  72. package/docs/ru/bundle_optimization.md +58 -25
  73. package/docs/ru/live-sync.md +4 -0
  74. package/docs/tr/bundle_optimization.md +58 -25
  75. package/docs/tr/live-sync.md +4 -0
  76. package/docs/uk/bundle_optimization.md +58 -25
  77. package/docs/uk/live-sync.md +4 -0
  78. package/docs/ur/bundle_optimization.md +58 -25
  79. package/docs/vi/bundle_optimization.md +58 -25
  80. package/docs/vi/live-sync.md +4 -0
  81. package/docs/zh/bundle_optimization.md +58 -25
  82. package/docs/zh/live-sync.md +4 -0
  83. package/docs/zh-TW/bundle_optimization.md +58 -25
  84. package/package.json +7 -7
  85. package/src/common.ts +39 -2
  86. package/src/generated/blog.entry.ts +39 -7
  87. package/src/generated/docs.entry.ts +39 -7
  88. package/src/generated/frequentQuestions.entry.ts +39 -7
  89. package/src/generated/legal.entry.ts +39 -7
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  createdAt: 2025-11-25
3
- updatedAt: 2026-06-07
3
+ updatedAt: 2026-08-09
4
4
  title: 最佳化 i18n 打包體積與效能
5
5
  description: 透過最佳化國際化(i18n)內容來縮減應用程式包的大小。了解如何利用 Intlayer 實現字典的 tree shaking 和延遲載入(lazy loading)。
6
6
  keywords:
@@ -16,6 +16,12 @@ slugs:
16
16
  - concept
17
17
  - bundle-optimization
18
18
  history:
19
+ - version: 9.2.1
20
+ date: 2026-08-09
21
+ changes: "`purge` 與 `minify` 現在可透過 `@intlayer/swc` 在 Next.js 上運作 — 不需要 `babel.config.js`"
22
+ - version: 8.12.0
23
+ date: 2026-06-24
24
+ changes: "在參考表中依所需的流水線順序列出 Babel 外掛(extract → purge → minify → optimize)"
19
25
  - version: 8.12.0
20
26
  date: 2026-06-07
21
27
  changes: "為 Babel/Webpack 引入了 `intlayerPurgeBabelPlugin` 和 `intlayerMinifyBabelPlugin`,明確了外掛程式管線"
@@ -191,12 +197,14 @@ Intlayer 的建置最佳化被劃分為若干個職責單一的外掛程式。
191
197
 
192
198
  這些被直接運用在基於 Webpack 設定的 `babel.config.js` 當中(比如使用了 Babel 的 Next.js、CRA,或是自訂的 Webpack 等)。
193
199
 
200
+ 下表依所需的流水線順序列出它們(與它們必須在 `babel.config.js` 中出現的順序相同):
201
+
194
202
  | 外掛程式 | 功能說明 |
195
203
  | :---------------------------- | :----------------------------------------------------------------------------------------------------- |
196
204
  | `intlayerExtractBabelPlugin` | 掃描 `.content.ts` 檔案並把編譯好的字典寫入 `.intlayer/` |
197
- | `intlayerOptimizeBabelPlugin` | 將 `useIntlayer('key')` 重寫為 `useDictionary(hash)` 並注入匹配對應字典的 `import` 語句 |
198
205
  | `intlayerPurgeBabelPlugin` | 掃描所有原始碼檔案,從已編譯的 `.intlayer/**/*.json` 字典檔案中刪除**未被使用的內容欄位** |
199
206
  | `intlayerMinifyBabelPlugin` | **重新命名內容欄位的鍵(keys)** 為簡短的字母別名(例如 `title` 變成 `a`),作用範圍包括 JSON 與原始碼 |
207
+ | `intlayerOptimizeBabelPlugin` | 將 `useIntlayer('key')` 重寫為 `useDictionary(hash)` 並注入匹配對應字典的 `import` 語句 |
200
208
 
201
209
  > **外掛程式的執行順序很重要。** 在您的 `babel.config.js` 裡,purge 和 minify 的外掛程式必須放置在 optimize 外掛程式**之前**。最佳化步驟(optimize)會把 `useIntlayer('key')` 替換為模糊的 `useDictionary(hash)`,此舉抹除了能夠讓 purge 和 minify 識別哪些欄位被使用過的字典 key 資訊。
202
210
 
@@ -205,9 +213,9 @@ Intlayer 的建置最佳化被劃分為若干個職責單一的外掛程式。
205
213
  | 選項助手 | 配套外掛程式 |
206
214
  | :--------------------------- | :---------------------------- |
207
215
  | `getExtractPluginOptions()` | `intlayerExtractBabelPlugin` |
208
- | `getOptimizePluginOptions()` | `intlayerOptimizeBabelPlugin` |
209
216
  | `getPurgePluginOptions()` | `intlayerPurgeBabelPlugin` |
210
217
  | `getMinifyPluginOptions()` | `intlayerMinifyBabelPlugin` |
218
+ | `getOptimizePluginOptions()` | `intlayerOptimizeBabelPlugin` |
211
219
 
212
220
  ### Vite 外掛程式 (`vite-intlayer`)
213
221
 
@@ -220,6 +228,20 @@ Vite 使用者**不需要直接對它們進行設定**。當您在 `vite.config.
220
228
  | Dictionary minify | 等同於 `intlayerMinifyBabelPlugin` 的 JSON 寫入步驟 |
221
229
  | Babel transform | 等同於 `intlayerMinifyBabelPlugin` 的程式碼重新命名步驟 + `intlayerOptimizeBabelPlugin` |
222
230
 
231
+ ### SWC 外掛(`@intlayer/swc`)
232
+
233
+ Next.js 使用者同樣**從不直接設定這些**。自 **v9.2.1** 起,`next.config.ts` 中的 `withIntlayer()` 僅憑 `build.purge` 與 `build.minify` 兩個旗標就會執行完整流水線 —— 清除、壓縮與匯入重寫。
234
+
235
+ 工作被分成兩部分,因為 SWC Wasm 外掛一次只轉換一個檔案,且無法存取檔案系統:
236
+
237
+ | 階段 | 執行位置 | 作用 |
238
+ | :----------------------------------- | :----------------------------- | :------------------------------------------------------------- |
239
+ | 使用分析 + JSON 清除/壓縮 | Node,位於 `withIntlayer()` 內 | 讀取每個元件原始檔,重寫 `.intlayer/**/*.json`,產生重新命名表 |
240
+ | 原始碼重寫(`content.title` → `.a`) | `@intlayer/swc`(Wasm) | 將重新命名表套用到你程式碼中對應的屬性存取 |
241
+ | 匯入重寫(`useIntlayer` → dict) | `@intlayer/swc`(Wasm) | 與 `intlayerOptimizeBabelPlugin` 相同 |
242
+
243
+ 判斷_哪些_欄位未被使用以及每個欄位取得_什麼_別名,需要跨檔案狀態與檔案 I/O,因此這一半在 Node 中執行;SWC 外掛只接收產生的表。
244
+
223
245
  ## 各平台設定指南
224
246
 
225
247
  <Tabs>
@@ -227,10 +249,12 @@ Vite 使用者**不需要直接對它們進行設定**。當您在 `vite.config.
227
249
 
228
250
  ### Next.js
229
251
 
230
- Next.js 需要依靠 `@intlayer/swc` 外掛程式來進行最佳化步驟(匯入重寫),因為 Next.js 採用 SWC 作為編譯器。
252
+ Next.js 需要 `@intlayer/swc` 外掛,因為 Next.js 使用 SWC 進行建置。自 **v9.2.1** 起,這一個套件即可涵蓋整條流水線 —— 最佳化(匯入重寫)、清除與壓縮。
231
253
 
232
254
  > 該外掛程式並未預設安裝,因為 SWC 外掛程式在 Next.js 當中目前仍處於實驗階段。未來這部分有可能會發生改變。
233
255
 
256
+ > **Next.js 16.1.0 是最低版本。** 它是首個基於 SWC 向前相容 Wasm 外掛 ABI 建置的版本;更早的版本會拒絕該外掛。`withIntlayer` 會讀取你專案的 Next.js 版本,低於 16.1.0 時乾脆不註冊該外掛 —— 這些建置仍會成功,只是在沒有打包最佳化的情況下執行。
257
+
234
258
  <Tabs>
235
259
  <Tab value="npm">
236
260
 
@@ -265,33 +289,40 @@ intlayer-swc-plugin = "*"
265
289
 
266
290
  安裝完畢後,Intlayer 將會自動偵測並使用該外掛程式。
267
291
 
268
- 至於**清除(purge)和最小化(minify)**步驟(即欄位移除和欄位重新命名),請連同 `@intlayer/babel` 一併安裝並加入 Babel 外掛程式。由於 Next.js 依靠 SWC 處理程式碼轉化,但仍會評估 `babel.config.js` 以決定外掛程式設定,因此上述 Babel 外掛程式能夠在進入 SWC 前作為預處理步驟得以執行。
292
+ **清除與壓縮**階段(欄位移除與欄位重新命名)不需要額外套件,也不需要 `babel.config.js`。用 `withIntlayer` 包住你的設定,並在 `intlayer.config.ts` 中開啟相應旗標:
269
293
 
270
- ```bash packageManager="npm"
271
- npm install -D @intlayer/babel
294
+ ```typescript fileName="next.config.ts"
295
+ import { withIntlayer } from "next-intlayer/server";
296
+ import type { NextConfig } from "next";
297
+
298
+ const nextConfig: NextConfig = {/* 你的設定 */};
299
+
300
+ export default withIntlayer(nextConfig);
272
301
  ```
273
302
 
274
- ```javascript fileName="babel.config.js"
275
- const {
276
- intlayerPurgeBabelPlugin,
277
- intlayerMinifyBabelPlugin,
278
- getPurgePluginOptions,
279
- getMinifyPluginOptions,
280
- } = require("@intlayer/babel");
303
+ ```typescript fileName="intlayer.config.ts"
304
+ import type { IntlayerConfig } from "intlayer";
281
305
 
282
- module.exports = {
283
- presets: ["next/babel"],
284
- plugins: [
285
- // Purge: 移除 .intlayer/**/*.json 裡未被使用的內容欄位
286
- [intlayerPurgeBabelPlugin, getPurgePluginOptions()],
287
- // Minify: 對 JSON 以及原始碼裡的內容欄位的鍵(keys)進行重新命名
288
- [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],
289
- // 注意: 在這裡不需要使用 intlayerOptimizeBabelPlugin,因為
290
- // @intlayer/swc 已經處理了 useIntlayer → useDictionary 的重寫過程。
291
- ],
306
+ const config: IntlayerConfig = {
307
+ build: {
308
+ purge: true, // 從打包的 JSON 中移除未使用的內容欄位
309
+ minify: true, // 將內容欄位鍵重新命名為簡短別名
310
+ },
292
311
  };
312
+
313
+ export default config;
293
314
  ```
294
315
 
316
+ 在 `next build` 期間,`withIntlayer` 會分析你的原始碼、重寫已編譯的字典,並將產生的欄位重新命名表交給 `@intlayer/swc`,由它更新你程式碼中對應的屬性存取。
317
+
318
+ > 請使用非同步的 `withIntlayer`,而不是 `withIntlayerSync`。同步版本不會執行分析流水線,因此清除與壓縮對它沒有效果。
319
+
320
+ > 清除與壓縮僅在 `next build` 時執行 —— 最佳化流水線在 `next dev` 期間是關閉的。
321
+
322
+ > 當設定了相容轉接器呼叫方時它們也會被停用(`swcExtraCallers`,由 `@intlayer/next-intl`、`@intlayer/react-i18next` 等相容套件設定):這些呼叫點對使用分析器不可見,因此清除會移除程式碼仍在讀取的欄位。匯入重寫仍保持啟用。
323
+
324
+ **更早的版本(9.2.1 之前)** 需要 `@intlayer/babel` 以及一個宣告 `intlayerPurgeBabelPlugin` 與 `intlayerMinifyBabelPlugin` 的 `babel.config.js`。該檔案不再需要,可以刪除。
325
+
295
326
  </Tab>
296
327
  <Tab value="vite">
297
328
 
@@ -447,6 +478,8 @@ export default config;
447
478
 
448
479
  > 如果在 `optimize` 被設為 `false`、又或是啟用了視覺化編輯器也就是當 `editor.enabled` 設定為了 `true` 時(由於編輯器需要保留欄位名稱以便做後續處理),重新命名這個操作都將會被跳過。
449
480
 
481
+ > 在 Next.js 上,當 `@intlayer/swc` 未安裝或無法載入時(Next.js 低於 16.1.0),壓縮同樣會被略過。重寫原始碼存取的正是這個外掛,因此在沒有它的情況下重新命名字典,會讓你的程式碼讀取已不存在的欄位名稱。
482
+
450
483
  > 同理,如果是利用了 `importMode: 'fetch'` 來載入欄位時此過程同樣不適用。因為它們的內容會以原始命名由後端 API 所提供,對客戶端內容隨意重新命名會破壞客戶端與伺服器端的匹配契約。
451
484
 
452
485
  ### 欄位清除 / Purging (去掉未被引用的欄位內容)
@@ -475,7 +508,7 @@ export default config;
475
508
  { "title": "…", "subtitle": "…" }
476
509
  ```
477
510
 
478
- > 與前邊提到的類似,當 `optimize` 是 `false` 或是開啟了視覺化編輯器(`editor.enabled` 取值 `true`) 時,它會選擇跳過對資料的清除操作。
511
+ > 與前邊提到的類似,當 `optimize` 是 `false` 或是開啟了視覺化編輯器(`editor.enabled` 取值 `true`) 時,它會選擇跳過對資料的清除操作。 在 Next.js 上,當 `@intlayer/swc` 不可用以及設定了相容轉接器呼叫方時,還會被額外略過。
479
512
 
480
513
  > 當檢測到某份程式碼因為異常無法順利解析、又或者當把由 `useIntlayer` 輸出的值以靜態解析器難以預測分析的模式在不同元件中來回丟(比如被打包成物件傳入等而未被進行解構)的時候,它同樣會跳過,以此保守地保留整部字典的全部資訊,避免意外發生。
481
514
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intlayer/docs",
3
- "version": "9.2.0",
3
+ "version": "9.3.0",
4
4
  "private": false,
5
5
  "description": "Intlayer documentation",
6
6
  "keywords": [
@@ -73,14 +73,14 @@
73
73
  "watch": "webpack --config ./webpack.config.ts --watch"
74
74
  },
75
75
  "dependencies": {
76
- "@intlayer/config": "9.2.0",
77
- "@intlayer/core": "9.2.0",
78
- "@intlayer/types": "9.2.0"
76
+ "@intlayer/config": "9.3.0",
77
+ "@intlayer/core": "9.3.0",
78
+ "@intlayer/types": "9.3.0"
79
79
  },
80
80
  "devDependencies": {
81
- "@intlayer/api": "9.2.0",
82
- "@intlayer/cli": "9.2.0",
83
- "@types/node": "26.1.2",
81
+ "@intlayer/api": "9.3.0",
82
+ "@intlayer/cli": "9.3.0",
83
+ "@types/node": "26.2.0",
84
84
  "@utils/ts-config": "1.0.4",
85
85
  "@utils/ts-config-types": "1.0.4",
86
86
  "@utils/tsdown-config": "1.0.4",
package/src/common.ts CHANGED
@@ -131,11 +131,25 @@ export const getFileMetadata = async <
131
131
  return formatMetadata(docKey as string, file, locale) as R;
132
132
  };
133
133
 
134
- export const getFileMetadataRecord = async <
134
+ /**
135
+ * Per-locale metadata records, keyed by the entry map they were built from.
136
+ *
137
+ * Building a record parses the frontmatter of every document of a category, and
138
+ * callers such as {@link getFileMetadataBySlug} need the whole record just to
139
+ * resolve a single slug — which a static site generator repeats for every page
140
+ * it renders. Documents are read once per process anyway (each entry caches its
141
+ * own read), so the derived metadata is equally safe to keep.
142
+ */
143
+ const fileMetadataRecordCache = new WeakMap<
144
+ Record<string, unknown>,
145
+ Map<LocalesValues, Promise<Record<string, FileMetadata>>>
146
+ >();
147
+
148
+ const buildFileMetadataRecord = async <
135
149
  F extends Record<string, Record<LocalesValues, Promise<string>>>,
136
150
  >(
137
151
  files: F,
138
- locale: LocalesValues = defaultLocale as LocalesValues
152
+ locale: LocalesValues
139
153
  ): Promise<Record<keyof F, FileMetadata>> => {
140
154
  const results = await Promise.allSettled(
141
155
  Object.entries(files).map(async ([key]) => {
@@ -153,6 +167,29 @@ export const getFileMetadataRecord = async <
153
167
  return filesResult as Record<keyof F, FileMetadata>;
154
168
  };
155
169
 
170
+ export const getFileMetadataRecord = async <
171
+ F extends Record<string, Record<LocalesValues, Promise<string>>>,
172
+ >(
173
+ files: F,
174
+ locale: LocalesValues = defaultLocale as LocalesValues
175
+ ): Promise<Record<keyof F, FileMetadata>> => {
176
+ let localeCache = fileMetadataRecordCache.get(files);
177
+
178
+ if (!localeCache) {
179
+ localeCache = new Map();
180
+ fileMetadataRecordCache.set(files, localeCache);
181
+ }
182
+
183
+ let record = localeCache.get(locale);
184
+
185
+ if (!record) {
186
+ record = buildFileMetadataRecord(files, locale);
187
+ localeCache.set(locale, record);
188
+ }
189
+
190
+ return (await record) as Record<keyof F, FileMetadata>;
191
+ };
192
+
156
193
  export const getFileMetadataBySlug = async <
157
194
  F extends Record<string, Record<LocalesValues, Promise<string>>>,
158
195
  >(
@@ -32,26 +32,58 @@ try {
32
32
  }
33
33
  }
34
34
 
35
- const readLocale = (
35
+ /**
36
+ * Reads a document, preferring the requested locale and falling back to English.
37
+ */
38
+ const readLocaleFile = async (
36
39
  relativeAfterLocale: string,
37
40
  locale: LocalesValues
38
41
  ): Promise<string> => {
39
42
  const target1 = join(baseDir, `./blog/${locale}/${relativeAfterLocale}`);
40
43
  if (existsSync(target1)) {
41
- return readFile(target1, 'utf8');
44
+ return await readFile(target1, 'utf8');
42
45
  }
43
46
  const target2 = join(baseDir, `./blog/en/${relativeAfterLocale}`);
44
47
  if (existsSync(target2)) {
45
- return readFile(target2, 'utf8');
48
+ return await readFile(target2, 'utf8');
46
49
  }
47
50
 
48
- return Promise.reject(
49
- new Error(
50
- `[docs] File not found: ${relativeAfterLocale} - locale: ${locale} - path: ${target1} - path: ${target2}`
51
- )
51
+ throw new Error(
52
+ `[docs] File not found: ${relativeAfterLocale} - locale: ${locale} - path: ${target1} - path: ${target2}`
52
53
  );
53
54
  };
54
55
 
56
+ /**
57
+ * Builds a lazy, awaitable handle over a document.
58
+ *
59
+ * The entry map below holds one handle per document *per locale*, so reading
60
+ * eagerly would pull every markdown file of every locale into memory as soon as
61
+ * this module is imported — hundreds of megabytes, duplicated in every
62
+ * prerender worker, for the handful of documents a page actually renders.
63
+ *
64
+ * The returned value is a thenable rather than a promise: consumers only ever
65
+ * `await` it, and `await` triggers `then`, so the file is read on first use and
66
+ * the resulting promise is cached for subsequent reads.
67
+ */
68
+ const readLocale = (
69
+ relativeAfterLocale: string,
70
+ locale: LocalesValues
71
+ ): Promise<string> => {
72
+ let pendingRead: Promise<string> | undefined;
73
+
74
+ const read = (): Promise<string> => {
75
+ pendingRead ??= readLocaleFile(relativeAfterLocale, locale);
76
+ return pendingRead;
77
+ };
78
+
79
+ return {
80
+ // biome-ignore lint/suspicious/noThenProperty: the thenable is intentional — `await` is what triggers the lazy read.
81
+ then: (onFulfilled, onRejected) => read().then(onFulfilled, onRejected),
82
+ catch: (onRejected) => read().catch(onRejected),
83
+ finally: (onFinally) => read().finally(onFinally),
84
+ } as Promise<string>;
85
+ };
86
+
55
87
  export const blogEntry = {
56
88
  './blog/en/compiler_vs_declarative_i18n.md': {
57
89
  en: readLocale('compiler_vs_declarative_i18n.md', 'en'),
@@ -32,26 +32,58 @@ try {
32
32
  }
33
33
  }
34
34
 
35
- const readLocale = (
35
+ /**
36
+ * Reads a document, preferring the requested locale and falling back to English.
37
+ */
38
+ const readLocaleFile = async (
36
39
  relativeAfterLocale: string,
37
40
  locale: LocalesValues
38
41
  ): Promise<string> => {
39
42
  const target1 = join(baseDir, `./docs/${locale}/${relativeAfterLocale}`);
40
43
  if (existsSync(target1)) {
41
- return readFile(target1, 'utf8');
44
+ return await readFile(target1, 'utf8');
42
45
  }
43
46
  const target2 = join(baseDir, `./docs/en/${relativeAfterLocale}`);
44
47
  if (existsSync(target2)) {
45
- return readFile(target2, 'utf8');
48
+ return await readFile(target2, 'utf8');
46
49
  }
47
50
 
48
- return Promise.reject(
49
- new Error(
50
- `[docs] File not found: ${relativeAfterLocale} - locale: ${locale} - path: ${target1} - path: ${target2}`
51
- )
51
+ throw new Error(
52
+ `[docs] File not found: ${relativeAfterLocale} - locale: ${locale} - path: ${target1} - path: ${target2}`
52
53
  );
53
54
  };
54
55
 
56
+ /**
57
+ * Builds a lazy, awaitable handle over a document.
58
+ *
59
+ * The entry map below holds one handle per document *per locale*, so reading
60
+ * eagerly would pull every markdown file of every locale into memory as soon as
61
+ * this module is imported — hundreds of megabytes, duplicated in every
62
+ * prerender worker, for the handful of documents a page actually renders.
63
+ *
64
+ * The returned value is a thenable rather than a promise: consumers only ever
65
+ * `await` it, and `await` triggers `then`, so the file is read on first use and
66
+ * the resulting promise is cached for subsequent reads.
67
+ */
68
+ const readLocale = (
69
+ relativeAfterLocale: string,
70
+ locale: LocalesValues
71
+ ): Promise<string> => {
72
+ let pendingRead: Promise<string> | undefined;
73
+
74
+ const read = (): Promise<string> => {
75
+ pendingRead ??= readLocaleFile(relativeAfterLocale, locale);
76
+ return pendingRead;
77
+ };
78
+
79
+ return {
80
+ // biome-ignore lint/suspicious/noThenProperty: the thenable is intentional — `await` is what triggers the lazy read.
81
+ then: (onFulfilled, onRejected) => read().then(onFulfilled, onRejected),
82
+ catch: (onRejected) => read().catch(onRejected),
83
+ finally: (onFinally) => read().finally(onFinally),
84
+ } as Promise<string>;
85
+ };
86
+
55
87
  export const docsEntry = {
56
88
  './docs/en/CI_CD.md': {
57
89
  en: readLocale('CI_CD.md', 'en'),
@@ -32,7 +32,10 @@ try {
32
32
  }
33
33
  }
34
34
 
35
- const readLocale = (
35
+ /**
36
+ * Reads a document, preferring the requested locale and falling back to English.
37
+ */
38
+ const readLocaleFile = async (
36
39
  relativeAfterLocale: string,
37
40
  locale: LocalesValues
38
41
  ): Promise<string> => {
@@ -41,23 +44,52 @@ const readLocale = (
41
44
  `./frequent_questions/${locale}/${relativeAfterLocale}`
42
45
  );
43
46
  if (existsSync(target1)) {
44
- return readFile(target1, 'utf8');
47
+ return await readFile(target1, 'utf8');
45
48
  }
46
49
  const target2 = join(
47
50
  baseDir,
48
51
  `./frequent_questions/en/${relativeAfterLocale}`
49
52
  );
50
53
  if (existsSync(target2)) {
51
- return readFile(target2, 'utf8');
54
+ return await readFile(target2, 'utf8');
52
55
  }
53
56
 
54
- return Promise.reject(
55
- new Error(
56
- `[docs] File not found: ${relativeAfterLocale} - locale: ${locale} - path: ${target1} - path: ${target2}`
57
- )
57
+ throw new Error(
58
+ `[docs] File not found: ${relativeAfterLocale} - locale: ${locale} - path: ${target1} - path: ${target2}`
58
59
  );
59
60
  };
60
61
 
62
+ /**
63
+ * Builds a lazy, awaitable handle over a document.
64
+ *
65
+ * The entry map below holds one handle per document *per locale*, so reading
66
+ * eagerly would pull every markdown file of every locale into memory as soon as
67
+ * this module is imported — hundreds of megabytes, duplicated in every
68
+ * prerender worker, for the handful of documents a page actually renders.
69
+ *
70
+ * The returned value is a thenable rather than a promise: consumers only ever
71
+ * `await` it, and `await` triggers `then`, so the file is read on first use and
72
+ * the resulting promise is cached for subsequent reads.
73
+ */
74
+ const readLocale = (
75
+ relativeAfterLocale: string,
76
+ locale: LocalesValues
77
+ ): Promise<string> => {
78
+ let pendingRead: Promise<string> | undefined;
79
+
80
+ const read = (): Promise<string> => {
81
+ pendingRead ??= readLocaleFile(relativeAfterLocale, locale);
82
+ return pendingRead;
83
+ };
84
+
85
+ return {
86
+ // biome-ignore lint/suspicious/noThenProperty: the thenable is intentional — `await` is what triggers the lazy read.
87
+ then: (onFulfilled, onRejected) => read().then(onFulfilled, onRejected),
88
+ catch: (onRejected) => read().catch(onRejected),
89
+ finally: (onFinally) => read().finally(onFinally),
90
+ } as Promise<string>;
91
+ };
92
+
61
93
  export const frequentQuestionsEntry = {
62
94
  './frequent_questions/en/SSR_Next_no_[locale].md': {
63
95
  en: readLocale('SSR_Next_no_[locale].md', 'en'),
@@ -32,26 +32,58 @@ try {
32
32
  }
33
33
  }
34
34
 
35
- const readLocale = (
35
+ /**
36
+ * Reads a document, preferring the requested locale and falling back to English.
37
+ */
38
+ const readLocaleFile = async (
36
39
  relativeAfterLocale: string,
37
40
  locale: LocalesValues
38
41
  ): Promise<string> => {
39
42
  const target1 = join(baseDir, `./legal/${locale}/${relativeAfterLocale}`);
40
43
  if (existsSync(target1)) {
41
- return readFile(target1, 'utf8');
44
+ return await readFile(target1, 'utf8');
42
45
  }
43
46
  const target2 = join(baseDir, `./legal/en/${relativeAfterLocale}`);
44
47
  if (existsSync(target2)) {
45
- return readFile(target2, 'utf8');
48
+ return await readFile(target2, 'utf8');
46
49
  }
47
50
 
48
- return Promise.reject(
49
- new Error(
50
- `[docs] File not found: ${relativeAfterLocale} - locale: ${locale} - path: ${target1} - path: ${target2}`
51
- )
51
+ throw new Error(
52
+ `[docs] File not found: ${relativeAfterLocale} - locale: ${locale} - path: ${target1} - path: ${target2}`
52
53
  );
53
54
  };
54
55
 
56
+ /**
57
+ * Builds a lazy, awaitable handle over a document.
58
+ *
59
+ * The entry map below holds one handle per document *per locale*, so reading
60
+ * eagerly would pull every markdown file of every locale into memory as soon as
61
+ * this module is imported — hundreds of megabytes, duplicated in every
62
+ * prerender worker, for the handful of documents a page actually renders.
63
+ *
64
+ * The returned value is a thenable rather than a promise: consumers only ever
65
+ * `await` it, and `await` triggers `then`, so the file is read on first use and
66
+ * the resulting promise is cached for subsequent reads.
67
+ */
68
+ const readLocale = (
69
+ relativeAfterLocale: string,
70
+ locale: LocalesValues
71
+ ): Promise<string> => {
72
+ let pendingRead: Promise<string> | undefined;
73
+
74
+ const read = (): Promise<string> => {
75
+ pendingRead ??= readLocaleFile(relativeAfterLocale, locale);
76
+ return pendingRead;
77
+ };
78
+
79
+ return {
80
+ // biome-ignore lint/suspicious/noThenProperty: the thenable is intentional — `await` is what triggers the lazy read.
81
+ then: (onFulfilled, onRejected) => read().then(onFulfilled, onRejected),
82
+ catch: (onRejected) => read().catch(onRejected),
83
+ finally: (onFinally) => read().finally(onFinally),
84
+ } as Promise<string>;
85
+ };
86
+
55
87
  export const legalEntry = {
56
88
  './legal/en/privacy_notice.md': {
57
89
  en: readLocale('privacy_notice.md', 'en'),