@inlang/paraglide-js 2.26.0 → 2.28.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 (225) hide show
  1. package/README.md +11 -1
  2. package/dist/bundler-plugins/per-locale-build/index.js +14 -7
  3. package/dist/bundler-plugins/per-locale-build/index.js.map +1 -1
  4. package/dist/bundler-plugins/per-locale-build/vite-environments.test.js +237 -4
  5. package/dist/bundler-plugins/per-locale-build/vite-environments.test.js.map +1 -1
  6. package/dist/bundler-plugins/unplugin.d.ts +1 -1
  7. package/dist/bundler-plugins/unplugin.js +79 -10
  8. package/dist/bundler-plugins/unplugin.js.map +1 -1
  9. package/dist/bundler-plugins/unplugin.test.js +334 -1
  10. package/dist/bundler-plugins/unplugin.test.js.map +1 -1
  11. package/dist/bundler-plugins/vite.js +2 -1
  12. package/dist/bundler-plugins/vite.js.map +1 -1
  13. package/dist/bundler-plugins/vite.test.js +1 -1
  14. package/dist/bundler-plugins/vite.test.js.map +1 -1
  15. package/dist/cli/commands/compile/command.js +13 -0
  16. package/dist/cli/commands/compile/command.js.map +1 -1
  17. package/dist/cli/commands/compile/command.test.js +87 -3
  18. package/dist/cli/commands/compile/command.test.js.map +1 -1
  19. package/dist/cli/commands/init/command.d.ts +5 -0
  20. package/dist/cli/commands/init/command.js +216 -44
  21. package/dist/cli/commands/init/command.js.map +1 -1
  22. package/dist/cli/commands/init/command.test.d.ts +1 -0
  23. package/dist/cli/commands/init/command.test.js +123 -0
  24. package/dist/cli/commands/init/command.test.js.map +1 -0
  25. package/dist/cli/defaults.js +2 -2
  26. package/dist/cli/defaults.js.map +1 -1
  27. package/dist/cli/init-options.d.ts +11 -0
  28. package/dist/cli/init-options.js +2 -0
  29. package/dist/cli/init-options.js.map +1 -0
  30. package/dist/cli/steps/add-inlang-check.d.ts +19 -0
  31. package/dist/cli/steps/add-inlang-check.js +60 -0
  32. package/dist/cli/steps/add-inlang-check.js.map +1 -0
  33. package/dist/cli/steps/add-inlang-check.test.d.ts +1 -0
  34. package/dist/cli/steps/add-inlang-check.test.js +88 -0
  35. package/dist/cli/steps/add-inlang-check.test.js.map +1 -0
  36. package/dist/cli/steps/add-message-lint.d.ts +34 -0
  37. package/dist/cli/steps/add-message-lint.js +105 -0
  38. package/dist/cli/steps/add-message-lint.js.map +1 -0
  39. package/dist/cli/steps/add-message-lint.test.d.ts +1 -0
  40. package/dist/cli/steps/add-message-lint.test.js +74 -0
  41. package/dist/cli/steps/add-message-lint.test.js.map +1 -0
  42. package/dist/cli/steps/add-vite-plugin.d.ts +18 -0
  43. package/dist/cli/steps/add-vite-plugin.js +34 -6
  44. package/dist/cli/steps/add-vite-plugin.js.map +1 -1
  45. package/dist/cli/steps/add-vite-plugin.test.js +25 -9
  46. package/dist/cli/steps/add-vite-plugin.test.js.map +1 -1
  47. package/dist/cli/steps/agents-md-suggestion.d.ts +11 -0
  48. package/dist/cli/steps/agents-md-suggestion.js +19 -0
  49. package/dist/cli/steps/agents-md-suggestion.js.map +1 -0
  50. package/dist/cli/steps/agents-md-suggestion.test.d.ts +1 -0
  51. package/dist/cli/steps/agents-md-suggestion.test.js +36 -0
  52. package/dist/cli/steps/agents-md-suggestion.test.js.map +1 -0
  53. package/dist/cli/steps/check-for-uncomitted-changes.d.ts +3 -0
  54. package/dist/cli/steps/check-for-uncomitted-changes.js +15 -3
  55. package/dist/cli/steps/check-for-uncomitted-changes.js.map +1 -1
  56. package/dist/cli/steps/detect-bundler.d.ts +1 -0
  57. package/dist/cli/steps/detect-bundler.js +7 -1
  58. package/dist/cli/steps/detect-bundler.js.map +1 -1
  59. package/dist/cli/steps/initialize-inlang-project.d.ts +4 -3
  60. package/dist/cli/steps/initialize-inlang-project.js +60 -157
  61. package/dist/cli/steps/initialize-inlang-project.js.map +1 -1
  62. package/dist/cli/steps/maybe-add-machine-translation.d.ts +4 -0
  63. package/dist/cli/steps/maybe-add-machine-translation.js +14 -8
  64. package/dist/cli/steps/maybe-add-machine-translation.js.map +1 -1
  65. package/dist/cli/steps/maybe-add-sherlock.d.ts +2 -0
  66. package/dist/cli/steps/maybe-add-sherlock.js +13 -7
  67. package/dist/cli/steps/maybe-add-sherlock.js.map +1 -1
  68. package/dist/cli/steps/maybe-add-sherlock.test.d.ts +1 -0
  69. package/dist/cli/steps/maybe-add-sherlock.test.js +44 -0
  70. package/dist/cli/steps/maybe-add-sherlock.test.js.map +1 -0
  71. package/dist/cli/steps/prompt-for-outdir.d.ts +2 -0
  72. package/dist/cli/steps/prompt-for-outdir.js +4 -0
  73. package/dist/cli/steps/prompt-for-outdir.js.map +1 -1
  74. package/dist/cli/steps/update-package-json.js +8 -13
  75. package/dist/cli/steps/update-package-json.js.map +1 -1
  76. package/dist/cli/steps/update-ts-config.d.ts +6 -0
  77. package/dist/cli/steps/update-ts-config.js +27 -4
  78. package/dist/cli/steps/update-ts-config.js.map +1 -1
  79. package/dist/cli/utils.d.ts +11 -0
  80. package/dist/cli/utils.js +21 -0
  81. package/dist/cli/utils.js.map +1 -1
  82. package/dist/cli/utils.test.d.ts +1 -0
  83. package/dist/cli/utils.test.js +16 -0
  84. package/dist/cli/utils.test.js.map +1 -0
  85. package/dist/compiler/compile-annotation.d.ts +48 -1
  86. package/dist/compiler/compile-annotation.js +243 -10
  87. package/dist/compiler/compile-annotation.js.map +1 -1
  88. package/dist/compiler/compile-bundle.d.ts +7 -2
  89. package/dist/compiler/compile-bundle.js +97 -3
  90. package/dist/compiler/compile-bundle.js.map +1 -1
  91. package/dist/compiler/compile-bundle.test.js +232 -19
  92. package/dist/compiler/compile-bundle.test.js.map +1 -1
  93. package/dist/compiler/compile-lifecycle.test.d.ts +1 -0
  94. package/dist/compiler/compile-lifecycle.test.js +69 -0
  95. package/dist/compiler/compile-lifecycle.test.js.map +1 -0
  96. package/dist/compiler/compile-local-variable.d.ts +3 -0
  97. package/dist/compiler/compile-local-variable.js +5 -2
  98. package/dist/compiler/compile-local-variable.js.map +1 -1
  99. package/dist/compiler/compile-local-variable.test.js +55 -15
  100. package/dist/compiler/compile-local-variable.test.js.map +1 -1
  101. package/dist/compiler/compile-message.d.ts +29 -1
  102. package/dist/compiler/compile-message.exact-number.test.d.ts +1 -0
  103. package/dist/compiler/compile-message.exact-number.test.js +786 -0
  104. package/dist/compiler/compile-message.exact-number.test.js.map +1 -0
  105. package/dist/compiler/compile-message.js +162 -66
  106. package/dist/compiler/compile-message.js.map +1 -1
  107. package/dist/compiler/compile-message.locals.test.d.ts +1 -0
  108. package/dist/compiler/compile-message.locals.test.js +498 -0
  109. package/dist/compiler/compile-message.locals.test.js.map +1 -0
  110. package/dist/compiler/compile-message.test.js +113 -22
  111. package/dist/compiler/compile-message.test.js.map +1 -1
  112. package/dist/compiler/compile-pattern.d.ts +5 -0
  113. package/dist/compiler/compile-pattern.js +6 -18
  114. package/dist/compiler/compile-pattern.js.map +1 -1
  115. package/dist/compiler/compile-pattern.test.js +169 -5
  116. package/dist/compiler/compile-pattern.test.js.map +1 -1
  117. package/dist/compiler/compile-project.js +5 -1
  118. package/dist/compiler/compile-project.js.map +1 -1
  119. package/dist/compiler/compile-project.test.js +1260 -185
  120. package/dist/compiler/compile-project.test.js.map +1 -1
  121. package/dist/compiler/compile.js +36 -17
  122. package/dist/compiler/compile.js.map +1 -1
  123. package/dist/compiler/compile.test.js +71 -1
  124. package/dist/compiler/compile.test.js.map +1 -1
  125. package/dist/compiler/compiler-options.d.ts +41 -0
  126. package/dist/compiler/compiler-options.js.map +1 -1
  127. package/dist/compiler/create-readme.d.ts +4 -1
  128. package/dist/compiler/create-readme.js +33 -125
  129. package/dist/compiler/create-readme.js.map +1 -1
  130. package/dist/compiler/emit-ts-declarations.interop.test.js +2 -2
  131. package/dist/compiler/emit-ts-declarations.interop.test.js.map +1 -1
  132. package/dist/compiler/emit-ts-declarations.test.js +4 -4
  133. package/dist/compiler/emit-ts-declarations.test.js.map +1 -1
  134. package/dist/compiler/index.d.ts +1 -1
  135. package/dist/compiler/index.js +1 -1
  136. package/dist/compiler/index.js.map +1 -1
  137. package/dist/compiler/input-alias.d.ts +21 -0
  138. package/dist/compiler/input-alias.js +42 -0
  139. package/dist/compiler/input-alias.js.map +1 -0
  140. package/dist/compiler/input-alias.test.d.ts +1 -0
  141. package/dist/compiler/input-alias.test.js +58 -0
  142. package/dist/compiler/input-alias.test.js.map +1 -0
  143. package/dist/compiler/message-locals.d.ts +46 -0
  144. package/dist/compiler/message-locals.js +129 -0
  145. package/dist/compiler/message-locals.js.map +1 -0
  146. package/dist/compiler/output-structure/locale-modules.js +7 -1
  147. package/dist/compiler/output-structure/locale-modules.js.map +1 -1
  148. package/dist/compiler/output-structure/locale-modules.test.js +35 -0
  149. package/dist/compiler/output-structure/locale-modules.test.js.map +1 -1
  150. package/dist/compiler/output-structure/message-modules.js +6 -4
  151. package/dist/compiler/output-structure/message-modules.js.map +1 -1
  152. package/dist/compiler/output-structure/message-modules.test.js +35 -0
  153. package/dist/compiler/output-structure/message-modules.test.js.map +1 -1
  154. package/dist/compiler/registry.d.ts +12 -0
  155. package/dist/compiler/registry.js +158 -31
  156. package/dist/compiler/registry.js.map +1 -1
  157. package/dist/compiler/registry.test.d.ts +1 -0
  158. package/dist/compiler/registry.test.js +291 -0
  159. package/dist/compiler/registry.test.js.map +1 -0
  160. package/dist/compiler/runtime/create-runtime.d.ts +2 -0
  161. package/dist/compiler/runtime/create-runtime.js +9 -0
  162. package/dist/compiler/runtime/create-runtime.js.map +1 -1
  163. package/dist/compiler/runtime/extract-locale-from-cookie.js +8 -1
  164. package/dist/compiler/runtime/extract-locale-from-cookie.js.map +1 -1
  165. package/dist/compiler/runtime/extract-locale-from-cookie.test.js +26 -0
  166. package/dist/compiler/runtime/extract-locale-from-cookie.test.js.map +1 -1
  167. package/dist/compiler/runtime/extract-locale-from-header.js +47 -5
  168. package/dist/compiler/runtime/extract-locale-from-header.js.map +1 -1
  169. package/dist/compiler/runtime/extract-locale-from-header.test.js +58 -0
  170. package/dist/compiler/runtime/extract-locale-from-header.test.js.map +1 -1
  171. package/dist/compiler/runtime/get-locale.js +7 -1
  172. package/dist/compiler/runtime/get-locale.js.map +1 -1
  173. package/dist/compiler/runtime/get-locale.test.js +95 -0
  174. package/dist/compiler/runtime/get-locale.test.js.map +1 -1
  175. package/dist/compiler/runtime/localize-href.d.ts +8 -0
  176. package/dist/compiler/runtime/localize-href.js +60 -0
  177. package/dist/compiler/runtime/localize-href.js.map +1 -1
  178. package/dist/compiler/runtime/localize-href.test.js +227 -1
  179. package/dist/compiler/runtime/localize-href.test.js.map +1 -1
  180. package/dist/compiler/runtime/set-locale.js +24 -6
  181. package/dist/compiler/runtime/set-locale.js.map +1 -1
  182. package/dist/compiler/runtime/set-locale.test.js +182 -0
  183. package/dist/compiler/runtime/set-locale.test.js.map +1 -1
  184. package/dist/compiler/runtime/type.d.ts +2 -0
  185. package/dist/compiler/runtime/type.js.map +1 -1
  186. package/dist/compiler/runtime/type.test.js +34 -0
  187. package/dist/compiler/runtime/type.test.js.map +1 -1
  188. package/dist/compiler/runtime/variables.d.ts +2 -0
  189. package/dist/compiler/runtime/variables.js +7 -0
  190. package/dist/compiler/runtime/variables.js.map +1 -1
  191. package/dist/compiler/server/create-server-file.js +24 -0
  192. package/dist/compiler/server/create-server-file.js.map +1 -1
  193. package/dist/compiler/server/create-server-file.test.js +8 -2
  194. package/dist/compiler/server/create-server-file.test.js.map +1 -1
  195. package/dist/compiler/server/middleware.d.ts +8 -2
  196. package/dist/compiler/server/middleware.js +204 -11
  197. package/dist/compiler/server/middleware.js.map +1 -1
  198. package/dist/compiler/server/middleware.test.js +750 -3
  199. package/dist/compiler/server/middleware.test.js.map +1 -1
  200. package/dist/eslint/index.d.ts +4 -0
  201. package/dist/eslint/index.js +8 -0
  202. package/dist/eslint/index.js.map +1 -0
  203. package/dist/eslint/no-dynamic-message-access.d.ts +3 -0
  204. package/dist/eslint/no-dynamic-message-access.js +150 -0
  205. package/dist/eslint/no-dynamic-message-access.js.map +1 -0
  206. package/dist/eslint/no-dynamic-message-access.test.d.ts +1 -0
  207. package/dist/eslint/no-dynamic-message-access.test.js +126 -0
  208. package/dist/eslint/no-dynamic-message-access.test.js.map +1 -0
  209. package/dist/eslint/oxlint.test.d.ts +1 -0
  210. package/dist/eslint/oxlint.test.js +64 -0
  211. package/dist/eslint/oxlint.test.js.map +1 -0
  212. package/dist/services/config/config-schema.d.ts +2 -0
  213. package/dist/services/config/config-schema.js +2 -0
  214. package/dist/services/config/config-schema.js.map +1 -1
  215. package/dist/services/config/load-paraglide-config.test.js +12 -0
  216. package/dist/services/config/load-paraglide-config.test.js.map +1 -1
  217. package/dist/services/env-variables/index.js +1 -1
  218. package/dist/services/env-variables/index.js.map +1 -1
  219. package/dist/services/file-handling/write-output.bench.js +51 -11
  220. package/dist/services/file-handling/write-output.bench.js.map +1 -1
  221. package/dist/services/file-handling/write-output.js +60 -20
  222. package/dist/services/file-handling/write-output.js.map +1 -1
  223. package/dist/services/file-handling/write-output.test.js +175 -0
  224. package/dist/services/file-handling/write-output.test.js.map +1 -1
  225. package/package.json +15 -5
@@ -1,4 +1,5 @@
1
1
  import { expect, test, describe, vi, beforeEach } from "vitest";
2
+ import vm from "node:vm";
2
3
  import { createProject as typescriptProject, ts, } from "@ts-morph/bootstrap";
3
4
  import { Declaration, insertBundleNested, loadProjectInMemory, newProject, Pattern, VariableReference, } from "@inlang/sdk";
4
5
  import { compileProject, getFallbackMap } from "./compile-project.js";
@@ -24,6 +25,35 @@ function hasFallbackCycle(fallbackMap, start) {
24
25
  }
25
26
  return false;
26
27
  }
28
+ test("locale-modules retain language-grouped static text helpers", async () => {
29
+ const project = await loadProjectInMemory({
30
+ blob: await newProject({
31
+ settings: { baseLocale: "en", locales: ["en", "de"] },
32
+ }),
33
+ });
34
+ await insertBundleNested(project.db, createBundleNested({
35
+ id: "literal_layout",
36
+ declarations: [],
37
+ messages: ["en", "de"].map((locale) => ({
38
+ locale,
39
+ variants: [
40
+ {
41
+ pattern: [
42
+ { type: "text", value: `layout text ${locale}` },
43
+ ],
44
+ },
45
+ ],
46
+ })),
47
+ }));
48
+ const output = await compileProject({
49
+ project,
50
+ compilerOptions: { outputStructure: "locale-modules" },
51
+ });
52
+ expect(output["messages/_index.js"]).toContain("return __de.literal_layout(inputs)");
53
+ expect(output["messages/_index.js"]).not.toContain('return /** @type {LocalizedString} */ ("layout text de")');
54
+ expect(output["messages/de.js"]).toContain("export const literal_layout =");
55
+ await project.close();
56
+ });
27
57
  test("emitGitignore", async () => {
28
58
  const project = await loadProjectInMemory({
29
59
  blob: await newProject({
@@ -87,6 +117,70 @@ test("marks tree-shakeable generated message layouts as side-effect-free", async
87
117
  sideEffects: false,
88
118
  });
89
119
  });
120
+ test("inlines static text and retains only fallback helpers under strict checkJs", async () => {
121
+ const project = await loadProjectInMemory({
122
+ blob: await newProject({
123
+ settings: {
124
+ baseLocale: "en",
125
+ locales: ["en", "de", "de-AT"],
126
+ },
127
+ }),
128
+ });
129
+ await insertBundleNested(project.db, createBundleNested({
130
+ id: "plain_greeting",
131
+ messages: [
132
+ {
133
+ locale: "en",
134
+ variants: [{ pattern: [{ type: "text", value: "Hello" }] }],
135
+ },
136
+ {
137
+ locale: "de",
138
+ variants: [{ pattern: [{ type: "text", value: "Hallo" }] }],
139
+ },
140
+ ],
141
+ }));
142
+ const output = await compileProject({
143
+ project,
144
+ compilerOptions: { outputStructure: "message-modules" },
145
+ });
146
+ const messageModule = output["messages/plain_greeting.js"];
147
+ // The base locale is returned inline, while de-AT's missing-locale alias
148
+ // still needs the de message function.
149
+ expect(messageModule).not.toContain("const en_plain_greeting =");
150
+ expect(messageModule).toContain("const de_plain_greeting =");
151
+ expect(messageModule).toMatch(/const de_at\d*_plain_greeting = de_plain_greeting;/);
152
+ const tsProject = await typescriptProject({
153
+ useInMemoryFileSystem: true,
154
+ compilerOptions: {
155
+ outDir: "dist",
156
+ declaration: true,
157
+ allowJs: true,
158
+ checkJs: true,
159
+ noImplicitAny: true,
160
+ noUnusedLocals: true,
161
+ noUnusedParameters: true,
162
+ noImplicitReturns: true,
163
+ noImplicitThis: true,
164
+ noUncheckedIndexedAccess: true,
165
+ noPropertyAccessFromIndexSignature: true,
166
+ module: ts.ModuleKind.Node16,
167
+ strict: true,
168
+ },
169
+ });
170
+ for (const [fileName, code] of Object.entries(output)) {
171
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
172
+ tsProject.createSourceFile(fileName, code);
173
+ }
174
+ }
175
+ const diagnostics = ts
176
+ .getPreEmitDiagnostics(tsProject.createProgram())
177
+ .filter((d) => !d.messageText.toString().includes("Cannot find module 'async_hooks'"));
178
+ expect(diagnostics).toEqual([]);
179
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
180
+ expect(m.plain_greeting({}, { locale: "en" })).toBe("Hello");
181
+ expect(m.plain_greeting({}, { locale: "de" })).toBe("Hallo");
182
+ expect(m.plain_greeting({}, { locale: "de-AT" })).toBe("Hallo");
183
+ });
90
184
  test("emitPrettierIgnore", async () => {
91
185
  const project = await loadProjectInMemory({
92
186
  blob: await newProject({
@@ -163,6 +257,8 @@ test("emitReadme includes project path", async () => {
163
257
  expect(output["README.md"]).toContain(projectPath);
164
258
  expect(output["README.md"]).toContain("Compiled from:");
165
259
  expect(output["README.md"]).toContain("Paraglide JS");
260
+ expect(output["README.md"]).toContain(`npx @inlang/paraglide-js compile --project ${projectPath}`);
261
+ expect(output["README.md"]).toContain("npx @inlang/cli check");
166
262
  });
167
263
  test("throws during compile for invalid routeStrategies match patterns", async () => {
168
264
  const project = await loadProjectInMemory({
@@ -1143,7 +1239,7 @@ describe.each([
1143
1239
  project: projectWithPatternAnnotation,
1144
1240
  compilerOptions,
1145
1241
  });
1146
- expect(Object.values(output).join("\n")).toContain('registry.number("en", i?.count, { maximumFractionDigits: 1 })');
1242
+ expect(Object.values(output).join("\n")).toContain('registry.number("en", i?.count, { maximumFractionDigits: 1 }, true)');
1147
1243
  const tsProject = await typescriptProject({
1148
1244
  useInMemoryFileSystem: true,
1149
1245
  compilerOptions: superStrictRuleOutAnyErrorTsSettings,
@@ -1250,179 +1346,1143 @@ describe.each([
1250
1346
  }
1251
1347
  expect(diagnostics.length).toEqual(0);
1252
1348
  });
1253
- });
1254
- async function bundleCode(output, file) {
1255
- output["runtime.js"] = output["runtime.js"].replace('import "@inlang/paraglide-js/urlpattern-polyfill";', "/** @type {any} */const URLPattern = {};").replace('const { AsyncLocalStorage } = await import("async_hooks");', "const AsyncLocalStorage = class {};");
1256
- const bundle = await rolldown({
1257
- input: ["main.js"],
1258
- plugins: [
1259
- // @ts-expect-error - rollup types are not up to date
1260
- virtual({
1261
- ...Object.fromEntries(Object.entries(output).map(([fileName, code]) => [
1262
- "paraglide/" + fileName,
1263
- code,
1264
- ])),
1265
- "main.js": file,
1349
+ test("ICU exact number aliases select numerically and pass checkJs", async () => {
1350
+ const icuProject = await loadProjectInMemory({
1351
+ blob: await newProject({
1352
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1266
1353
  }),
1267
- ],
1268
- });
1269
- const compiled = await bundle.generate({ format: "esm" });
1270
- const code = compiled.output[0].code;
1271
- return code;
1272
- }
1273
- async function importCode(code) {
1274
- // The random comment ensures that each code is a unique module
1275
- // and state is not shared between imports
1276
- const randomComment = `// ${Math.random()}`;
1277
- const codeWithComment = `${randomComment}\n${code}`;
1278
- return await import(`data:application/javascript;base64,${Buffer.from(codeWithComment, "utf8").toString("base64")}`);
1279
- }
1280
- const project = await loadProjectInMemory({
1281
- blob: await newProject({
1282
- settings: {
1283
- baseLocale: "en",
1284
- locales: ["en", "de", "en-US"],
1285
- },
1286
- }),
1287
- });
1288
- const mockBundles = [
1289
- createBundleNested({
1290
- id: "sad_penguin_bundle",
1291
- messages: [
1292
- {
1293
- locale: "en",
1294
- variants: [{ pattern: [{ type: "text", value: "A simple message." }] }],
1295
- },
1296
- {
1297
- locale: "de",
1298
- variants: [
1299
- { pattern: [{ type: "text", value: "Eine einfache Nachricht." }] },
1300
- ],
1301
- },
1302
- ],
1303
- }),
1304
- createBundleNested({
1305
- id: "Sad_penguin_bundle",
1306
- messages: [
1307
- {
1308
- locale: "en",
1309
- variants: [
1310
- { pattern: [{ type: "text", value: "Capital Sad penguin" }] },
1311
- ],
1312
- },
1313
- {
1314
- locale: "de",
1315
- variants: [
1316
- {
1317
- pattern: [
1318
- { type: "text", value: "Grossgeschriebenes Sad penguin" },
1319
- ],
1320
- },
1321
- ],
1322
- },
1323
- ],
1324
- }),
1325
- {
1326
- id: "depressed_dog",
1327
- declarations: [
1354
+ });
1355
+ const countPluralExact = (value) => value === undefined
1356
+ ? { type: "catchall-match", key: "countPluralExact" }
1357
+ : {
1358
+ type: "literal-match",
1359
+ key: "countPluralExact",
1360
+ value,
1361
+ };
1362
+ const countPlural = (value) => value === undefined
1363
+ ? { type: "catchall-match", key: "countPlural" }
1364
+ : { type: "literal-match", key: "countPlural", value };
1365
+ const withCount = (suffix) => [
1328
1366
  {
1329
- type: "input-variable",
1330
- name: "name",
1367
+ type: "expression",
1368
+ arg: { type: "variable-reference", name: "count" },
1331
1369
  },
1332
- ],
1333
- messages: [
1334
- {
1335
- id: "depressed_dog_en",
1336
- bundleId: "depressed_dog",
1337
- locale: "en",
1338
- selectors: [],
1339
- variants: [
1340
- {
1341
- id: "depressed_dog_en_variant_one",
1342
- messageId: "depressed_dog_en",
1343
- matches: [],
1344
- pattern: [
1345
- { type: "text", value: "Good morning " },
1346
- {
1347
- type: "expression",
1348
- arg: { type: "variable-reference", name: "name" },
1349
- },
1350
- { type: "text", value: "!" },
1351
- ],
1370
+ { type: "text", value: suffix },
1371
+ ];
1372
+ // the shape the ICU1 plugin imports {count, plural, =0 {…} one {…} other {…}} as
1373
+ await insertBundleNested(icuProject.db, createBundleNested({
1374
+ id: "item_count",
1375
+ declarations: [
1376
+ { type: "input-variable", name: "count" },
1377
+ {
1378
+ type: "local-variable",
1379
+ name: "countPluralExact",
1380
+ value: {
1381
+ type: "expression",
1382
+ arg: { type: "variable-reference", name: "count" },
1352
1383
  },
1353
- ],
1354
- },
1355
- {
1356
- id: "depressed_dog_de",
1357
- bundleId: "depressed_dog",
1358
- locale: "de",
1359
- selectors: [],
1360
- variants: [
1361
- {
1362
- id: "depressed_dog_de_variant_one",
1363
- messageId: "depressed_dog_de",
1364
- matches: [],
1365
- pattern: [
1366
- { type: "text", value: "Guten Morgen " },
1367
- {
1368
- type: "expression",
1369
- arg: { type: "variable-reference", name: "name" },
1370
- },
1371
- { type: "text", value: "!" },
1372
- ],
1384
+ },
1385
+ {
1386
+ type: "local-variable",
1387
+ name: "countPlural",
1388
+ value: {
1389
+ type: "expression",
1390
+ arg: { type: "variable-reference", name: "count" },
1391
+ annotation: {
1392
+ type: "function-reference",
1393
+ name: "plural",
1394
+ options: [],
1395
+ },
1373
1396
  },
1374
- ],
1375
- },
1376
- ],
1377
- },
1378
- {
1379
- id: "insane_cats",
1380
- declarations: [
1381
- {
1382
- type: "input-variable",
1383
- name: "name",
1384
- },
1385
- {
1386
- type: "input-variable",
1387
- name: "count",
1388
- },
1389
- ],
1390
- messages: [
1391
- {
1392
- id: "insane_cats_en",
1393
- bundleId: "insane_cats",
1394
- locale: "en",
1395
- selectors: [],
1396
- variants: [
1397
- {
1398
- id: "insane_cats_en_variant_one",
1399
- messageId: "insane_cats_en",
1400
- matches: [],
1401
- pattern: [
1402
- { type: "text", value: "Hello " },
1403
- {
1404
- type: "expression",
1405
- arg: { type: "variable-reference", name: "name" },
1406
- },
1407
- { type: "text", value: "! You have " },
1408
- {
1409
- type: "expression",
1410
- arg: { type: "variable-reference", name: "count" },
1411
- },
1412
- { type: "text", value: " messages." },
1413
- ],
1397
+ },
1398
+ ],
1399
+ messages: [
1400
+ {
1401
+ locale: "en",
1402
+ selectors: [
1403
+ { type: "variable-reference", name: "countPluralExact" },
1404
+ { type: "variable-reference", name: "countPlural" },
1405
+ ],
1406
+ variants: [
1407
+ {
1408
+ matches: [countPluralExact("0"), countPlural()],
1409
+ pattern: [{ type: "text", value: "No items" }],
1410
+ },
1411
+ {
1412
+ matches: [countPluralExact(), countPlural("one")],
1413
+ pattern: withCount(" item"),
1414
+ },
1415
+ {
1416
+ matches: [countPluralExact(), countPlural()],
1417
+ pattern: withCount(" items"),
1418
+ },
1419
+ ],
1420
+ },
1421
+ {
1422
+ locale: "fr",
1423
+ selectors: [
1424
+ { type: "variable-reference", name: "countPluralExact" },
1425
+ { type: "variable-reference", name: "countPlural" },
1426
+ ],
1427
+ // an editor appends the exact variant last
1428
+ variants: [
1429
+ {
1430
+ matches: [countPluralExact(), countPlural("one")],
1431
+ pattern: withCount(" élément"),
1432
+ },
1433
+ {
1434
+ matches: [countPluralExact(), countPlural()],
1435
+ pattern: withCount(" éléments"),
1436
+ },
1437
+ {
1438
+ matches: [countPluralExact("0"), countPlural()],
1439
+ pattern: [{ type: "text", value: "Aucun élément" }],
1440
+ },
1441
+ ],
1442
+ },
1443
+ ],
1444
+ }));
1445
+ // en narrows `status` by a direct match, fr matches through an alias
1446
+ // with a catchall, so any status is accepted
1447
+ await insertBundleNested(icuProject.db, createBundleNested({
1448
+ id: "status_message",
1449
+ declarations: [
1450
+ { type: "input-variable", name: "status" },
1451
+ {
1452
+ type: "local-variable",
1453
+ name: "statusExact",
1454
+ value: {
1455
+ type: "expression",
1456
+ arg: { type: "variable-reference", name: "status" },
1414
1457
  },
1415
- ],
1416
- },
1417
- {
1458
+ },
1459
+ ],
1460
+ messages: [
1461
+ {
1462
+ locale: "en",
1463
+ selectors: [{ type: "variable-reference", name: "status" }],
1464
+ variants: [
1465
+ {
1466
+ matches: [
1467
+ { type: "literal-match", key: "status", value: "active" },
1468
+ ],
1469
+ // reads statusExact: unused bundle locals fail noUnusedLocals
1470
+ pattern: [
1471
+ { type: "text", value: "Active: " },
1472
+ {
1473
+ type: "expression",
1474
+ arg: { type: "variable-reference", name: "statusExact" },
1475
+ },
1476
+ ],
1477
+ },
1478
+ ],
1479
+ },
1480
+ {
1481
+ locale: "fr",
1482
+ selectors: [{ type: "variable-reference", name: "statusExact" }],
1483
+ variants: [
1484
+ {
1485
+ matches: [
1486
+ { type: "literal-match", key: "statusExact", value: "0" },
1487
+ ],
1488
+ pattern: [{ type: "text", value: "Zéro" }],
1489
+ },
1490
+ {
1491
+ matches: [{ type: "catchall-match", key: "statusExact" }],
1492
+ pattern: [{ type: "text", value: "Autre" }],
1493
+ },
1494
+ ],
1495
+ },
1496
+ ],
1497
+ }));
1498
+ const output = await compileProject({
1499
+ project: icuProject,
1500
+ compilerOptions,
1501
+ });
1502
+ const tsProject = await typescriptProject({
1503
+ useInMemoryFileSystem: true,
1504
+ compilerOptions: superStrictRuleOutAnyErrorTsSettings,
1505
+ });
1506
+ for (const [fileName, code] of Object.entries(output)) {
1507
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
1508
+ tsProject.createSourceFile(fileName, code);
1509
+ }
1510
+ }
1511
+ tsProject.createSourceFile("test.ts", `
1512
+ import * as m from "./messages.js"
1513
+
1514
+ m.item_count({ count: 0 }) satisfies string
1515
+ m.item_count({ count: "0" }) satisfies string
1516
+ m.status_message({ status: "active" }) satisfies string
1517
+ m.status_message({ status: "inactive" }) satisfies string
1518
+ `);
1519
+ const program = tsProject.createProgram();
1520
+ const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => {
1521
+ return !d.messageText
1522
+ .toString()
1523
+ .includes("Cannot find module 'async_hooks'");
1524
+ });
1525
+ for (const diagnostic of diagnostics) {
1526
+ console.error(diagnostic.messageText, diagnostic.file?.fileName);
1527
+ }
1528
+ expect(diagnostics.length).toEqual(0);
1529
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
1530
+ const render = (locale) => [0, 1, 2, 5, 21, "0"].map((count) => m.item_count({ count }, { locale }));
1531
+ expect(render("en")).toEqual([
1532
+ "No items",
1533
+ "1 item",
1534
+ "2 items",
1535
+ "5 items",
1536
+ "21 items",
1537
+ "No items",
1538
+ ]);
1539
+ expect(render("fr")).toEqual([
1540
+ "Aucun élément",
1541
+ "1 élément",
1542
+ "2 éléments",
1543
+ "5 éléments",
1544
+ "21 éléments",
1545
+ "Aucun élément",
1546
+ ]);
1547
+ expect(m.status_message({ status: "inactive" }, { locale: "fr" })).toBe("Autre");
1548
+ });
1549
+ test("emits only the locals a locale reads, in dependency order, and supports plural offsets under checkJs", async () => {
1550
+ const localsProject = await loadProjectInMemory({
1551
+ blob: await newProject({
1552
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1553
+ }),
1554
+ });
1555
+ const ref = (name) => ({ type: "variable-reference", name });
1556
+ const literal = (key, value) => ({ type: "literal-match", key, value });
1557
+ const catchall = (key) => ({ type: "catchall-match", key });
1558
+ const text = (value) => [{ type: "text", value }];
1559
+ // en never reads statusExact, fr only reads it
1560
+ await insertBundleNested(localsProject.db, createBundleNested({
1561
+ id: "status_label",
1562
+ declarations: [
1563
+ { type: "input-variable", name: "status" },
1564
+ {
1565
+ type: "local-variable",
1566
+ name: "statusExact",
1567
+ value: { type: "expression", arg: ref("status") },
1568
+ },
1569
+ ],
1570
+ messages: [
1571
+ {
1572
+ locale: "en",
1573
+ selectors: [ref("status")],
1574
+ variants: [
1575
+ {
1576
+ matches: [literal("status", "active")],
1577
+ pattern: text("Active"),
1578
+ },
1579
+ { matches: [catchall("status")], pattern: text("Other") },
1580
+ ],
1581
+ },
1582
+ {
1583
+ locale: "fr",
1584
+ selectors: [ref("statusExact")],
1585
+ variants: [
1586
+ {
1587
+ matches: [literal("statusExact", "0")],
1588
+ pattern: text("Zéro"),
1589
+ },
1590
+ { matches: [catchall("statusExact")], pattern: text("Autre") },
1591
+ ],
1592
+ },
1593
+ ],
1594
+ }));
1595
+ // the shape the ICU1 plugin imports
1596
+ // {count, plural, offset:1 =0 {…} =1 {…} one {…} other {…}} as
1597
+ await insertBundleNested(localsProject.db, createBundleNested({
1598
+ id: "guest_list",
1599
+ declarations: [
1600
+ { type: "input-variable", name: "count" },
1601
+ {
1602
+ type: "local-variable",
1603
+ name: "countPluralOffset1",
1604
+ value: {
1605
+ type: "expression",
1606
+ arg: ref("count"),
1607
+ annotation: {
1608
+ type: "function-reference",
1609
+ name: "plural",
1610
+ options: [
1611
+ { name: "offset", value: { type: "literal", value: "1" } },
1612
+ ],
1613
+ },
1614
+ },
1615
+ },
1616
+ {
1617
+ type: "local-variable",
1618
+ name: "countPluralOffset1Exact",
1619
+ value: { type: "expression", arg: ref("count") },
1620
+ },
1621
+ ],
1622
+ messages: [
1623
+ {
1624
+ locale: "en",
1625
+ selectors: [
1626
+ ref("countPluralOffset1Exact"),
1627
+ ref("countPluralOffset1"),
1628
+ ],
1629
+ variants: [
1630
+ {
1631
+ matches: [
1632
+ literal("countPluralOffset1Exact", "0"),
1633
+ catchall("countPluralOffset1"),
1634
+ ],
1635
+ pattern: text("Nobody"),
1636
+ },
1637
+ {
1638
+ matches: [
1639
+ literal("countPluralOffset1Exact", "1"),
1640
+ catchall("countPluralOffset1"),
1641
+ ],
1642
+ pattern: text("Only you"),
1643
+ },
1644
+ {
1645
+ matches: [
1646
+ catchall("countPluralOffset1Exact"),
1647
+ literal("countPluralOffset1", "one"),
1648
+ ],
1649
+ pattern: text("You and one other"),
1650
+ },
1651
+ {
1652
+ matches: [
1653
+ catchall("countPluralOffset1Exact"),
1654
+ catchall("countPluralOffset1"),
1655
+ ],
1656
+ pattern: text("You and others"),
1657
+ },
1658
+ ],
1659
+ },
1660
+ {
1661
+ // fr does not translate the exact cases: countPluralOffset1Exact
1662
+ // is not read
1663
+ locale: "fr",
1664
+ selectors: [ref("countPluralOffset1")],
1665
+ variants: [
1666
+ {
1667
+ matches: [literal("countPluralOffset1", "one")],
1668
+ pattern: text("Vous et un autre"),
1669
+ },
1670
+ {
1671
+ matches: [catchall("countPluralOffset1")],
1672
+ pattern: text("Vous et d'autres"),
1673
+ },
1674
+ ],
1675
+ },
1676
+ ],
1677
+ }));
1678
+ // fr reads no variable at all
1679
+ await insertBundleNested(localsProject.db, createBundleNested({
1680
+ id: "greeting_label",
1681
+ declarations: [
1682
+ { type: "input-variable", name: "name" },
1683
+ {
1684
+ type: "local-variable",
1685
+ name: "nameValue",
1686
+ value: { type: "expression", arg: ref("name") },
1687
+ },
1688
+ ],
1689
+ messages: [
1690
+ {
1691
+ locale: "en",
1692
+ selectors: [],
1693
+ variants: [
1694
+ {
1695
+ matches: [],
1696
+ pattern: [
1697
+ { type: "text", value: "Hello " },
1698
+ { type: "expression", arg: ref("nameValue") },
1699
+ ],
1700
+ },
1701
+ ],
1702
+ },
1703
+ {
1704
+ locale: "fr",
1705
+ selectors: [],
1706
+ variants: [{ matches: [], pattern: text("Bonjour") }],
1707
+ },
1708
+ ],
1709
+ }));
1710
+ // a local declared before the local it reads
1711
+ await insertBundleNested(localsProject.db, createBundleNested({
1712
+ id: "reordered_locals",
1713
+ declarations: [
1714
+ { type: "input-variable", name: "count" },
1715
+ {
1716
+ type: "local-variable",
1717
+ name: "countPlural",
1718
+ value: {
1719
+ type: "expression",
1720
+ arg: ref("countValue"),
1721
+ annotation: {
1722
+ type: "function-reference",
1723
+ name: "plural",
1724
+ options: [],
1725
+ },
1726
+ },
1727
+ },
1728
+ {
1729
+ type: "local-variable",
1730
+ name: "countValue",
1731
+ value: { type: "expression", arg: ref("count") },
1732
+ },
1733
+ ],
1734
+ messages: [
1735
+ {
1736
+ locale: "en",
1737
+ selectors: [ref("countPlural")],
1738
+ variants: [
1739
+ {
1740
+ matches: [literal("countPlural", "one")],
1741
+ pattern: text("one"),
1742
+ },
1743
+ { matches: [catchall("countPlural")], pattern: text("other") },
1744
+ ],
1745
+ },
1746
+ ],
1747
+ }));
1748
+ const output = await compileProject({
1749
+ project: localsProject,
1750
+ compilerOptions,
1751
+ });
1752
+ const tsProject = await typescriptProject({
1753
+ useInMemoryFileSystem: true,
1754
+ compilerOptions: superStrictRuleOutAnyErrorTsSettings,
1755
+ });
1756
+ for (const [fileName, code] of Object.entries(output)) {
1757
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
1758
+ tsProject.createSourceFile(fileName, code);
1759
+ }
1760
+ }
1761
+ tsProject.createSourceFile("test.ts", `
1762
+ import * as m from "./messages.js"
1763
+
1764
+ m.status_label({ status: "active" }) satisfies string
1765
+ m.guest_list({ count: 0 }) satisfies string
1766
+ m.reordered_locals({ count: 1 }) satisfies string
1767
+ m.greeting_label({ name: "Ada" }) satisfies string
1768
+ `);
1769
+ const program = tsProject.createProgram();
1770
+ const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => {
1771
+ return !d.messageText
1772
+ .toString()
1773
+ .includes("Cannot find module 'async_hooks'");
1774
+ });
1775
+ for (const diagnostic of diagnostics) {
1776
+ console.error(diagnostic.messageText, diagnostic.file?.fileName);
1777
+ }
1778
+ expect(diagnostics.length).toEqual(0);
1779
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
1780
+ expect(["active", "inactive"].map((status) => m.status_label({ status }, { locale: "en" }))).toEqual(["Active", "Other"]);
1781
+ expect([0, "0", 1].map((status) => m.status_label({ status }, { locale: "fr" }))).toEqual(["Zéro", "Zéro", "Autre"]);
1782
+ const guests = (locale) => [0, 1, 2, 3, 22, "1", "2"].map((count) => m.guest_list({ count }, { locale }));
1783
+ expect(guests("en")).toEqual([
1784
+ "Nobody",
1785
+ "Only you",
1786
+ "You and one other",
1787
+ "You and others",
1788
+ "You and others",
1789
+ "Only you",
1790
+ "You and one other",
1791
+ ]);
1792
+ // French "one" covers 0 and 1 (and -1 for 0 guests), so 1 and 2 guests
1793
+ // (0 and 1 others) select "one"
1794
+ expect(guests("fr")).toEqual([
1795
+ "Vous et un autre",
1796
+ "Vous et un autre",
1797
+ "Vous et un autre",
1798
+ "Vous et d'autres",
1799
+ "Vous et d'autres",
1800
+ "Vous et un autre",
1801
+ "Vous et un autre",
1802
+ ]);
1803
+ expect([1, 2].map((count) => m.reordered_locals({ count }, { locale: "en" }))).toEqual(["one", "other"]);
1804
+ expect(m.greeting_label({ name: "Ada" }, { locale: "en" })).toBe("Hello Ada");
1805
+ expect(m.greeting_label({ name: "Ada" }, { locale: "fr" })).toBe("Bonjour");
1806
+ });
1807
+ test("imports the registry only where a message calls it, and formats ICU # with offset under checkJs", async () => {
1808
+ const compileChecked = async (bundles) => {
1809
+ const icuProject = await loadProjectInMemory({
1810
+ blob: await newProject({
1811
+ settings: { baseLocale: "en", locales: ["en", "de"] },
1812
+ }),
1813
+ });
1814
+ for (const bundle of bundles) {
1815
+ await insertBundleNested(icuProject.db, createBundleNested(bundle));
1816
+ }
1817
+ const output = await compileProject({
1818
+ project: icuProject,
1819
+ compilerOptions,
1820
+ });
1821
+ const tsProject = await typescriptProject({
1822
+ useInMemoryFileSystem: true,
1823
+ compilerOptions: superStrictRuleOutAnyErrorTsSettings,
1824
+ });
1825
+ for (const [fileName, code] of Object.entries(output)) {
1826
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
1827
+ tsProject.createSourceFile(fileName, code);
1828
+ }
1829
+ }
1830
+ const diagnostics = ts
1831
+ .getPreEmitDiagnostics(tsProject.createProgram())
1832
+ .filter((d) => !d.messageText
1833
+ .toString()
1834
+ .includes("Cannot find module 'async_hooks'"));
1835
+ for (const diagnostic of diagnostics) {
1836
+ console.error(diagnostic.messageText, diagnostic.file?.fileName);
1837
+ }
1838
+ expect(diagnostics.length).toEqual(0);
1839
+ return output;
1840
+ };
1841
+ const ref = (name) => ({ type: "variable-reference", name });
1842
+ const literal = (key, value) => ({ type: "literal-match", key, value });
1843
+ const catchall = (key) => ({ type: "catchall-match", key });
1844
+ const text = (value) => ({ type: "text", value });
1845
+ const pound = (offset) => ({
1846
+ type: "expression",
1847
+ arg: ref("count"),
1848
+ annotation: {
1849
+ type: "function-reference",
1850
+ name: "icu:pound",
1851
+ options: offset === undefined
1852
+ ? []
1853
+ : [
1854
+ {
1855
+ name: "offset",
1856
+ value: { type: "literal", value: offset },
1857
+ },
1858
+ ],
1859
+ },
1860
+ });
1861
+ // message text that contains "registry." must not import the registry
1862
+ const textOnly = await compileChecked([
1863
+ {
1864
+ id: "registry_text",
1865
+ declarations: [],
1866
+ messages: [
1867
+ {
1868
+ locale: "en",
1869
+ selectors: [],
1870
+ variants: [
1871
+ { matches: [], pattern: [text("Open the registry. Now.")] },
1872
+ ],
1873
+ },
1874
+ {
1875
+ locale: "de",
1876
+ selectors: [],
1877
+ variants: [
1878
+ {
1879
+ matches: [],
1880
+ pattern: [text("Öffne die registry. Jetzt.")],
1881
+ },
1882
+ ],
1883
+ },
1884
+ ],
1885
+ },
1886
+ // an unknown formatter on a local interpolates the raw value
1887
+ {
1888
+ id: "custom_local",
1889
+ declarations: [
1890
+ { type: "input-variable", name: "amount" },
1891
+ {
1892
+ type: "local-variable",
1893
+ name: "formatted",
1894
+ value: {
1895
+ type: "expression",
1896
+ arg: ref("amount"),
1897
+ annotation: {
1898
+ type: "function-reference",
1899
+ name: "customLocalFormatter",
1900
+ options: [],
1901
+ },
1902
+ },
1903
+ },
1904
+ ],
1905
+ messages: [
1906
+ {
1907
+ locale: "en",
1908
+ selectors: [],
1909
+ variants: [
1910
+ {
1911
+ matches: [],
1912
+ pattern: [
1913
+ text("Total: "),
1914
+ { type: "expression", arg: ref("formatted") },
1915
+ ],
1916
+ },
1917
+ ],
1918
+ },
1919
+ ],
1920
+ },
1921
+ ]);
1922
+ for (const [fileName, code] of Object.entries(textOnly)) {
1923
+ if (fileName.startsWith("messages/")) {
1924
+ expect(code).not.toContain("import * as registry");
1925
+ }
1926
+ }
1927
+ const { m: textMessages } = await importCode(await bundleCode(textOnly, `export * as m from "./paraglide/messages.js"`));
1928
+ expect(textMessages.registry_text({}, { locale: "en" })).toBe("Open the registry. Now.");
1929
+ expect(textMessages.custom_local({ amount: 1234 }, { locale: "en" })).toBe("Total: 1234");
1930
+ // {count, plural, offset:1 =0 {Nobody} =1 {You} one {You and # other} other {You and # others}}
1931
+ // as the ICU1 plugin imports it, and `#` without an offset
1932
+ const output = await compileChecked([
1933
+ {
1934
+ id: "guest_count",
1935
+ declarations: [
1936
+ { type: "input-variable", name: "count" },
1937
+ {
1938
+ type: "local-variable",
1939
+ name: "countPluralOffset1",
1940
+ value: {
1941
+ type: "expression",
1942
+ arg: ref("count"),
1943
+ annotation: {
1944
+ type: "function-reference",
1945
+ name: "plural",
1946
+ options: [
1947
+ { name: "offset", value: { type: "literal", value: "1" } },
1948
+ ],
1949
+ },
1950
+ },
1951
+ },
1952
+ {
1953
+ type: "local-variable",
1954
+ name: "countPluralOffset1Exact",
1955
+ value: { type: "expression", arg: ref("count") },
1956
+ },
1957
+ ],
1958
+ messages: [
1959
+ {
1960
+ locale: "en",
1961
+ selectors: [
1962
+ ref("countPluralOffset1Exact"),
1963
+ ref("countPluralOffset1"),
1964
+ ],
1965
+ variants: [
1966
+ {
1967
+ matches: [
1968
+ literal("countPluralOffset1Exact", "0"),
1969
+ catchall("countPluralOffset1"),
1970
+ ],
1971
+ pattern: [text("Nobody")],
1972
+ },
1973
+ {
1974
+ matches: [
1975
+ literal("countPluralOffset1Exact", "1"),
1976
+ catchall("countPluralOffset1"),
1977
+ ],
1978
+ pattern: [text("You")],
1979
+ },
1980
+ {
1981
+ matches: [
1982
+ catchall("countPluralOffset1Exact"),
1983
+ literal("countPluralOffset1", "one"),
1984
+ ],
1985
+ pattern: [text("You and "), pound("1"), text(" other")],
1986
+ },
1987
+ {
1988
+ matches: [
1989
+ catchall("countPluralOffset1Exact"),
1990
+ catchall("countPluralOffset1"),
1991
+ ],
1992
+ pattern: [text("You and "), pound("1"), text(" others")],
1993
+ },
1994
+ ],
1995
+ },
1996
+ ],
1997
+ },
1998
+ {
1999
+ id: "item_total",
2000
+ declarations: [
2001
+ { type: "input-variable", name: "count" },
2002
+ {
2003
+ type: "local-variable",
2004
+ name: "countPlural",
2005
+ value: {
2006
+ type: "expression",
2007
+ arg: ref("count"),
2008
+ annotation: {
2009
+ type: "function-reference",
2010
+ name: "plural",
2011
+ options: [],
2012
+ },
2013
+ },
2014
+ },
2015
+ ],
2016
+ messages: [
2017
+ {
2018
+ locale: "en",
2019
+ selectors: [ref("countPlural")],
2020
+ variants: [
2021
+ {
2022
+ matches: [literal("countPlural", "one")],
2023
+ pattern: [pound(), text(" item")],
2024
+ },
2025
+ {
2026
+ matches: [catchall("countPlural")],
2027
+ pattern: [pound(), text(" items")],
2028
+ },
2029
+ ],
2030
+ },
2031
+ {
2032
+ locale: "de",
2033
+ selectors: [ref("countPlural")],
2034
+ variants: [
2035
+ {
2036
+ matches: [literal("countPlural", "one")],
2037
+ pattern: [pound(), text(" Artikel")],
2038
+ },
2039
+ {
2040
+ matches: [catchall("countPlural")],
2041
+ pattern: [pound(), text(" Artikel")],
2042
+ },
2043
+ ],
2044
+ },
2045
+ ],
2046
+ },
2047
+ {
2048
+ id: "variable_offset",
2049
+ declarations: [
2050
+ { type: "input-variable", name: "count" },
2051
+ { type: "input-variable", name: "off" },
2052
+ {
2053
+ type: "local-variable",
2054
+ name: "countPlural",
2055
+ value: {
2056
+ type: "expression",
2057
+ arg: ref("count"),
2058
+ annotation: {
2059
+ type: "function-reference",
2060
+ name: "plural",
2061
+ options: [{ name: "offset", value: ref("off") }],
2062
+ },
2063
+ },
2064
+ },
2065
+ ],
2066
+ messages: [
2067
+ {
2068
+ locale: "en",
2069
+ selectors: [ref("countPlural")],
2070
+ variants: [
2071
+ {
2072
+ matches: [literal("countPlural", "one")],
2073
+ pattern: [
2074
+ text("one: "),
2075
+ {
2076
+ type: "expression",
2077
+ arg: ref("count"),
2078
+ annotation: {
2079
+ type: "function-reference",
2080
+ name: "icu:pound",
2081
+ options: [{ name: "offset", value: ref("off") }],
2082
+ },
2083
+ },
2084
+ ],
2085
+ },
2086
+ {
2087
+ matches: [catchall("countPlural")],
2088
+ pattern: [text("other")],
2089
+ },
2090
+ ],
2091
+ },
2092
+ ],
2093
+ },
2094
+ ]);
2095
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
2096
+ // offsets from variables type-check and are converted with Number()
2097
+ expect(m.variable_offset({ count: 3, off: 2 }, { locale: "en" })).toBe("one: 1");
2098
+ expect(m.variable_offset({ count: 3, off: "2" }, { locale: "en" })).toBe("one: 1");
2099
+ expect([0, 1, 2, 3, 1234].map((count) => m.guest_count({ count }, { locale: "en" }))).toEqual([
2100
+ "Nobody",
2101
+ "You",
2102
+ "You and 1 other",
2103
+ "You and 2 others",
2104
+ "You and 1,233 others",
2105
+ ]);
2106
+ expect([1, 2, 1234].map((count) => m.item_total({ count }, { locale: "en" }))).toEqual(["1 item", "2 items", "1,234 items"]);
2107
+ expect(m.item_total({ count: 1234 }, { locale: "de" })).toBe("1.234 Artikel");
2108
+ });
2109
+ });
2110
+ describe.each([
2111
+ { outputStructure: "locale-modules" },
2112
+ { outputStructure: "message-modules" },
2113
+ ])("markup messages with fallbacks ($outputStructure)", ({ outputStructure }) => {
2114
+ test(".parts() type-checks under strict checkJs and renders", async () => {
2115
+ const markupProject = await loadProjectInMemory({
2116
+ blob: await newProject({
2117
+ settings: { baseLocale: "en", locales: ["en", "de"] },
2118
+ }),
2119
+ });
2120
+ const bold = (value) => [
2121
+ { type: "markup-start", name: "b" },
2122
+ { type: "text", value },
2123
+ { type: "markup-end", name: "b" },
2124
+ ];
2125
+ const bundles = [
2126
+ // de falls back to en
2127
+ {
2128
+ id: "markup_fallback",
2129
+ messages: [{ locale: "en", variants: [{ pattern: bold("Hello") }] }],
2130
+ },
2131
+ // en has no markup, de has
2132
+ {
2133
+ id: "markup_mixed",
2134
+ messages: [
2135
+ {
2136
+ locale: "en",
2137
+ variants: [{ pattern: [{ type: "text", value: "Hello" }] }],
2138
+ },
2139
+ { locale: "de", variants: [{ pattern: bold("Hallo") }] },
2140
+ ],
2141
+ },
2142
+ // en (the base locale) has no message and no fallback
2143
+ {
2144
+ id: "markup_missing_base",
2145
+ messages: [{ locale: "de", variants: [{ pattern: bold("Hallo") }] }],
2146
+ },
2147
+ ];
2148
+ for (const bundle of bundles) {
2149
+ await insertBundleNested(markupProject.db, createBundleNested(bundle));
2150
+ }
2151
+ const output = await compileProject({
2152
+ project: markupProject,
2153
+ compilerOptions: {
2154
+ outputStructure,
2155
+ strategy: ["globalVariable", "baseLocale"],
2156
+ },
2157
+ });
2158
+ const tsProject = await typescriptProject({
2159
+ useInMemoryFileSystem: true,
2160
+ compilerOptions: {
2161
+ outDir: "dist",
2162
+ declaration: true,
2163
+ allowJs: true,
2164
+ checkJs: true,
2165
+ noImplicitAny: true,
2166
+ noUnusedLocals: true,
2167
+ noUnusedParameters: true,
2168
+ noImplicitReturns: true,
2169
+ noImplicitThis: true,
2170
+ noUncheckedIndexedAccess: true,
2171
+ noPropertyAccessFromIndexSignature: true,
2172
+ module: ts.ModuleKind.Node16,
2173
+ strict: true,
2174
+ },
2175
+ });
2176
+ for (const [fileName, code] of Object.entries(output)) {
2177
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
2178
+ tsProject.createSourceFile(fileName, code);
2179
+ }
2180
+ }
2181
+ const diagnostics = ts
2182
+ .getPreEmitDiagnostics(tsProject.createProgram())
2183
+ .filter((d) => !d.messageText
2184
+ .toString()
2185
+ .includes("Cannot find module 'async_hooks'"));
2186
+ for (const diagnostic of diagnostics) {
2187
+ console.error(diagnostic.messageText, diagnostic.file?.fileName);
2188
+ }
2189
+ expect(diagnostics.length).toEqual(0);
2190
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
2191
+ const boldParts = (value) => [
2192
+ { type: "markup-start", name: "b", options: {}, attributes: {} },
2193
+ { type: "text", value },
2194
+ { type: "markup-end", name: "b", options: {}, attributes: {} },
2195
+ ];
2196
+ expect(m.markup_fallback.parts({}, { locale: "de" })).toEqual(boldParts("Hello"));
2197
+ expect(m.markup_mixed.parts({}, { locale: "en" })).toEqual([
2198
+ { type: "text", value: "Hello" },
2199
+ ]);
2200
+ expect(m.markup_mixed.parts({}, { locale: "de" })).toEqual(boldParts("Hallo"));
2201
+ expect(m.markup_missing_base.parts({}, { locale: "en" })).toEqual([
2202
+ { type: "text", value: "markup_missing_base" },
2203
+ ]);
2204
+ });
2205
+ });
2206
+ test("messages injected by the middleware in experimentalMiddlewareLocaleSplitting mode can call the registry", async () => {
2207
+ const splitProject = await loadProjectInMemory({
2208
+ blob: await newProject({
2209
+ settings: { baseLocale: "en", locales: ["en", "de"] },
2210
+ }),
2211
+ });
2212
+ const ref = (name) => ({ type: "variable-reference", name });
2213
+ // {count, plural, =0 {No items} other {# items}} as the ICU1 plugin imports it
2214
+ await insertBundleNested(splitProject.db, createBundleNested({
2215
+ id: "item_count",
2216
+ declarations: [
2217
+ { type: "input-variable", name: "count" },
2218
+ {
2219
+ type: "local-variable",
2220
+ name: "countPluralExact",
2221
+ value: { type: "expression", arg: ref("count") },
2222
+ },
2223
+ {
2224
+ type: "local-variable",
2225
+ name: "countPlural",
2226
+ value: {
2227
+ type: "expression",
2228
+ arg: ref("count"),
2229
+ annotation: {
2230
+ type: "function-reference",
2231
+ name: "plural",
2232
+ options: [],
2233
+ },
2234
+ },
2235
+ },
2236
+ ],
2237
+ messages: [
2238
+ {
2239
+ locale: "en",
2240
+ selectors: [ref("countPluralExact"), ref("countPlural")],
2241
+ variants: [
2242
+ {
2243
+ matches: [
2244
+ { type: "literal-match", key: "countPluralExact", value: "0" },
2245
+ { type: "catchall-match", key: "countPlural" },
2246
+ ],
2247
+ pattern: [{ type: "text", value: "No items" }],
2248
+ },
2249
+ {
2250
+ matches: [
2251
+ { type: "catchall-match", key: "countPluralExact" },
2252
+ { type: "catchall-match", key: "countPlural" },
2253
+ ],
2254
+ pattern: [
2255
+ {
2256
+ type: "expression",
2257
+ arg: ref("count"),
2258
+ annotation: {
2259
+ type: "function-reference",
2260
+ name: "icu:pound",
2261
+ options: [],
2262
+ },
2263
+ },
2264
+ { type: "text", value: " items" },
2265
+ ],
2266
+ },
2267
+ ],
2268
+ },
2269
+ ],
2270
+ }));
2271
+ await insertBundleNested(splitProject.db, createBundleNested({
2272
+ id: "plain_text",
2273
+ messages: [
2274
+ {
2275
+ locale: "en",
2276
+ variants: [{ pattern: [{ type: "text", value: "Hello" }] }],
2277
+ },
2278
+ ],
2279
+ }));
2280
+ const output = await compileProject({
2281
+ project: splitProject,
2282
+ compilerOptions: {
2283
+ strategy: ["baseLocale"],
2284
+ experimentalMiddlewareLocaleSplitting: true,
2285
+ disableAsyncLocalStorage: true,
2286
+ },
2287
+ });
2288
+ const { m, paraglideMiddleware } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"
2289
+ export { paraglideMiddleware } from "./paraglide/server.js"`));
2290
+ const render = async (messages) => {
2291
+ const response = await paraglideMiddleware(new Request("https://example.com/", {
2292
+ headers: { "Sec-Fetch-Dest": "document" },
2293
+ }), () => new Response(`<html><head></head><body>${messages(m)}</body></html>`, {
2294
+ headers: { "Content-Type": "text/html" },
2295
+ }));
2296
+ const html = await response.text();
2297
+ const script = html.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1];
2298
+ expect(script).toBeDefined();
2299
+ // the client evaluates the script without registry.js
2300
+ const context = {};
2301
+ vm.runInNewContext(script, context);
2302
+ return { html, ssr: context.__paraglide.ssr };
2303
+ };
2304
+ const withRegistry = await render((m) => m.item_count({ count: 5 }));
2305
+ expect(withRegistry.html).toContain("<body>5 items</body>");
2306
+ expect(withRegistry.ssr.item_count({ count: 5 })).toBe("5 items");
2307
+ expect(withRegistry.ssr.item_count({ count: 1234 })).toBe("1,234 items");
2308
+ expect(withRegistry.ssr.item_count({ count: 0 })).toBe("No items");
2309
+ // messages that don't call the registry get no registry code
2310
+ const withoutRegistry = await render((m) => m.plain_text());
2311
+ expect(withoutRegistry.ssr.plain_text()).toBe("Hello");
2312
+ expect(withoutRegistry.html).not.toContain("const registry");
2313
+ });
2314
+ async function bundleCode(output, file) {
2315
+ output["runtime.js"] = output["runtime.js"].replace('import "@inlang/paraglide-js/urlpattern-polyfill";', "/** @type {any} */const URLPattern = {};").replace('const { AsyncLocalStorage } = await import("async_hooks");', "const AsyncLocalStorage = class {};");
2316
+ const bundle = await rolldown({
2317
+ input: ["main.js"],
2318
+ plugins: [
2319
+ // @ts-expect-error - rollup types are not up to date
2320
+ virtual({
2321
+ ...Object.fromEntries(Object.entries(output).map(([fileName, code]) => [
2322
+ "paraglide/" + fileName,
2323
+ code,
2324
+ ])),
2325
+ "main.js": file,
2326
+ }),
2327
+ ],
2328
+ });
2329
+ const compiled = await bundle.generate({ format: "esm" });
2330
+ const code = compiled.output[0].code;
2331
+ return code;
2332
+ }
2333
+ async function importCode(code) {
2334
+ // The random comment ensures that each code is a unique module
2335
+ // and state is not shared between imports
2336
+ const randomComment = `// ${Math.random()}`;
2337
+ const codeWithComment = `${randomComment}\n${code}`;
2338
+ return await import(`data:application/javascript;base64,${Buffer.from(codeWithComment, "utf8").toString("base64")}`);
2339
+ }
2340
+ const project = await loadProjectInMemory({
2341
+ blob: await newProject({
2342
+ settings: {
2343
+ baseLocale: "en",
2344
+ locales: ["en", "de", "en-US"],
2345
+ },
2346
+ }),
2347
+ });
2348
+ const mockBundles = [
2349
+ createBundleNested({
2350
+ id: "sad_penguin_bundle",
2351
+ messages: [
2352
+ {
2353
+ locale: "en",
2354
+ variants: [{ pattern: [{ type: "text", value: "A simple message." }] }],
2355
+ },
2356
+ {
2357
+ locale: "de",
2358
+ variants: [
2359
+ { pattern: [{ type: "text", value: "Eine einfache Nachricht." }] },
2360
+ ],
2361
+ },
2362
+ ],
2363
+ }),
2364
+ createBundleNested({
2365
+ id: "Sad_penguin_bundle",
2366
+ messages: [
2367
+ {
2368
+ locale: "en",
2369
+ variants: [
2370
+ { pattern: [{ type: "text", value: "Capital Sad penguin" }] },
2371
+ ],
2372
+ },
2373
+ {
2374
+ locale: "de",
2375
+ variants: [
2376
+ {
2377
+ pattern: [
2378
+ { type: "text", value: "Grossgeschriebenes Sad penguin" },
2379
+ ],
2380
+ },
2381
+ ],
2382
+ },
2383
+ ],
2384
+ }),
2385
+ {
2386
+ id: "depressed_dog",
2387
+ declarations: [
2388
+ {
2389
+ type: "input-variable",
2390
+ name: "name",
2391
+ },
2392
+ ],
2393
+ messages: [
2394
+ {
2395
+ id: "depressed_dog_en",
2396
+ bundle_id: "depressed_dog",
2397
+ locale: "en",
2398
+ selectors: [],
2399
+ variants: [
2400
+ {
2401
+ id: "depressed_dog_en_variant_one",
2402
+ message_id: "depressed_dog_en",
2403
+ matches: [],
2404
+ pattern: [
2405
+ { type: "text", value: "Good morning " },
2406
+ {
2407
+ type: "expression",
2408
+ arg: { type: "variable-reference", name: "name" },
2409
+ },
2410
+ { type: "text", value: "!" },
2411
+ ],
2412
+ },
2413
+ ],
2414
+ },
2415
+ {
2416
+ id: "depressed_dog_de",
2417
+ bundle_id: "depressed_dog",
2418
+ locale: "de",
2419
+ selectors: [],
2420
+ variants: [
2421
+ {
2422
+ id: "depressed_dog_de_variant_one",
2423
+ message_id: "depressed_dog_de",
2424
+ matches: [],
2425
+ pattern: [
2426
+ { type: "text", value: "Guten Morgen " },
2427
+ {
2428
+ type: "expression",
2429
+ arg: { type: "variable-reference", name: "name" },
2430
+ },
2431
+ { type: "text", value: "!" },
2432
+ ],
2433
+ },
2434
+ ],
2435
+ },
2436
+ ],
2437
+ },
2438
+ {
2439
+ id: "insane_cats",
2440
+ declarations: [
2441
+ {
2442
+ type: "input-variable",
2443
+ name: "name",
2444
+ },
2445
+ {
2446
+ type: "input-variable",
2447
+ name: "count",
2448
+ },
2449
+ ],
2450
+ messages: [
2451
+ {
2452
+ id: "insane_cats_en",
2453
+ bundle_id: "insane_cats",
2454
+ locale: "en",
2455
+ selectors: [],
2456
+ variants: [
2457
+ {
2458
+ id: "insane_cats_en_variant_one",
2459
+ message_id: "insane_cats_en",
2460
+ matches: [],
2461
+ pattern: [
2462
+ { type: "text", value: "Hello " },
2463
+ {
2464
+ type: "expression",
2465
+ arg: { type: "variable-reference", name: "name" },
2466
+ },
2467
+ { type: "text", value: "! You have " },
2468
+ {
2469
+ type: "expression",
2470
+ arg: { type: "variable-reference", name: "count" },
2471
+ },
2472
+ { type: "text", value: " messages." },
2473
+ ],
2474
+ },
2475
+ ],
2476
+ },
2477
+ {
1418
2478
  id: "insane_cats_de",
1419
- bundleId: "insane_cats",
2479
+ bundle_id: "insane_cats",
1420
2480
  locale: "de",
1421
2481
  selectors: [],
1422
2482
  variants: [
1423
2483
  {
1424
2484
  id: "insane_cats_de_variant_one",
1425
- messageId: "insane_cats_de",
2485
+ message_id: "insane_cats_de",
1426
2486
  matches: [],
1427
2487
  pattern: [
1428
2488
  { type: "text", value: "Hallo " },
@@ -1586,13 +2646,13 @@ const mockBundles = [
1586
2646
  messages: [
1587
2647
  {
1588
2648
  id: "auth_password_error_en",
1589
- bundleId: "auth_password_error",
2649
+ bundle_id: "auth_password_error",
1590
2650
  locale: "en",
1591
2651
  selectors: [],
1592
2652
  variants: [
1593
2653
  {
1594
2654
  id: "auth_password_error_en_variant_invalid",
1595
- messageId: "auth_password_error_en",
2655
+ message_id: "auth_password_error_en",
1596
2656
  matches: [{ type: "literal-match", key: "type", value: "invalid" }],
1597
2657
  pattern: [
1598
2658
  {
@@ -1603,7 +2663,7 @@ const mockBundles = [
1603
2663
  },
1604
2664
  {
1605
2665
  id: "auth_password_error_en_variant_empty",
1606
- messageId: "auth_password_error_en",
2666
+ message_id: "auth_password_error_en",
1607
2667
  matches: [{ type: "literal-match", key: "type", value: "empty" }],
1608
2668
  pattern: [
1609
2669
  {
@@ -1614,7 +2674,7 @@ const mockBundles = [
1614
2674
  },
1615
2675
  {
1616
2676
  id: "auth_password_error_en_variant_min",
1617
- messageId: "auth_password_error_en",
2677
+ message_id: "auth_password_error_en",
1618
2678
  matches: [
1619
2679
  {
1620
2680
  type: "literal-match",
@@ -1631,7 +2691,7 @@ const mockBundles = [
1631
2691
  },
1632
2692
  {
1633
2693
  id: "auth_password_error_en_variant_secure",
1634
- messageId: "auth_password_error_en",
2694
+ message_id: "auth_password_error_en",
1635
2695
  matches: [{ type: "literal-match", key: "type", value: "secure" }],
1636
2696
  pattern: [
1637
2697
  {
@@ -1655,13 +2715,13 @@ const mockBundles = [
1655
2715
  messages: [
1656
2716
  {
1657
2717
  id: "error_with_dash_en",
1658
- bundleId: "error_with_dash",
2718
+ bundle_id: "error_with_dash",
1659
2719
  locale: "en",
1660
2720
  selectors: [],
1661
2721
  variants: [
1662
2722
  {
1663
2723
  id: "error_with_dash_en_variant_network",
1664
- messageId: "error_with_dash_en",
2724
+ message_id: "error_with_dash_en",
1665
2725
  matches: [
1666
2726
  { type: "literal-match", key: "error-type", value: "network" },
1667
2727
  ],
@@ -1669,7 +2729,7 @@ const mockBundles = [
1669
2729
  },
1670
2730
  {
1671
2731
  id: "error_with_dash_en_variant_timeout",
1672
- messageId: "error_with_dash_en",
2732
+ message_id: "error_with_dash_en",
1673
2733
  matches: [
1674
2734
  {
1675
2735
  type: "literal-match",
@@ -1694,13 +2754,13 @@ const mockBundles = [
1694
2754
  messages: [
1695
2755
  {
1696
2756
  id: "auth_password_error_catchall_en",
1697
- bundleId: "auth_password_error_catchall",
2757
+ bundle_id: "auth_password_error_catchall",
1698
2758
  locale: "en",
1699
2759
  selectors: [],
1700
2760
  variants: [
1701
2761
  {
1702
2762
  id: "auth_password_error_catchall_en_variant_invalid",
1703
- messageId: "auth_password_error_catchall_en",
2763
+ message_id: "auth_password_error_catchall_en",
1704
2764
  matches: [{ type: "literal-match", key: "type", value: "invalid" }],
1705
2765
  pattern: [
1706
2766
  {
@@ -1711,7 +2771,7 @@ const mockBundles = [
1711
2771
  },
1712
2772
  {
1713
2773
  id: "auth_password_error_catchall_en_variant_catchall",
1714
- messageId: "auth_password_error_catchall_en",
2774
+ message_id: "auth_password_error_catchall_en",
1715
2775
  matches: [{ type: "catchall-match", key: "type" }],
1716
2776
  pattern: [
1717
2777
  {
@@ -1739,13 +2799,13 @@ const mockBundles = [
1739
2799
  messages: [
1740
2800
  {
1741
2801
  id: "multi_key_status_type_en",
1742
- bundleId: "multi_key_status_type",
2802
+ bundle_id: "multi_key_status_type",
1743
2803
  locale: "en",
1744
2804
  selectors: [],
1745
2805
  variants: [
1746
2806
  {
1747
2807
  id: "multi_key_status_type_en_variant_ready_invalid",
1748
- messageId: "multi_key_status_type_en",
2808
+ message_id: "multi_key_status_type_en",
1749
2809
  matches: [
1750
2810
  { type: "literal-match", key: "type", value: "invalid" },
1751
2811
  { type: "literal-match", key: "status", value: "ready" },
@@ -1754,7 +2814,7 @@ const mockBundles = [
1754
2814
  },
1755
2815
  {
1756
2816
  id: "multi_key_status_type_en_variant_done_empty",
1757
- messageId: "multi_key_status_type_en",
2817
+ message_id: "multi_key_status_type_en",
1758
2818
  matches: [
1759
2819
  { type: "literal-match", key: "type", value: "empty" },
1760
2820
  { type: "literal-match", key: "status", value: "done" },
@@ -1763,7 +2823,7 @@ const mockBundles = [
1763
2823
  },
1764
2824
  {
1765
2825
  id: "multi_key_status_type_en_variant_failed_secure",
1766
- messageId: "multi_key_status_type_en",
2826
+ message_id: "multi_key_status_type_en",
1767
2827
  matches: [
1768
2828
  { type: "literal-match", key: "type", value: "secure" },
1769
2829
  { type: "literal-match", key: "status", value: "failed" },
@@ -1785,19 +2845,19 @@ const mockBundles = [
1785
2845
  messages: [
1786
2846
  {
1787
2847
  id: "empty_matches_catchall_en",
1788
- bundleId: "empty_matches_catchall",
2848
+ bundle_id: "empty_matches_catchall",
1789
2849
  locale: "en",
1790
2850
  selectors: [],
1791
2851
  variants: [
1792
2852
  {
1793
2853
  id: "empty_matches_catchall_en_variant_invalid",
1794
- messageId: "empty_matches_catchall_en",
2854
+ message_id: "empty_matches_catchall_en",
1795
2855
  matches: [{ type: "literal-match", key: "type", value: "invalid" }],
1796
2856
  pattern: [{ type: "text", value: "Invalid" }],
1797
2857
  },
1798
2858
  {
1799
2859
  id: "empty_matches_catchall_en_variant_any",
1800
- messageId: "empty_matches_catchall_en",
2860
+ message_id: "empty_matches_catchall_en",
1801
2861
  matches: [],
1802
2862
  pattern: [{ type: "text", value: "Any" }],
1803
2863
  },
@@ -1816,19 +2876,19 @@ const mockBundles = [
1816
2876
  messages: [
1817
2877
  {
1818
2878
  id: "numeric_input_match_en",
1819
- bundleId: "numeric_input_match",
2879
+ bundle_id: "numeric_input_match",
1820
2880
  locale: "en",
1821
2881
  selectors: [],
1822
2882
  variants: [
1823
2883
  {
1824
2884
  id: "numeric_input_match_en_variant_1",
1825
- messageId: "numeric_input_match_en",
2885
+ message_id: "numeric_input_match_en",
1826
2886
  matches: [{ type: "literal-match", key: "input", value: "1" }],
1827
2887
  pattern: [{ type: "text", value: "Thing 1" }],
1828
2888
  },
1829
2889
  {
1830
2890
  id: "numeric_input_match_en_variant_2",
1831
- messageId: "numeric_input_match_en",
2891
+ message_id: "numeric_input_match_en",
1832
2892
  matches: [{ type: "literal-match", key: "input", value: "2" }],
1833
2893
  pattern: [{ type: "text", value: "Thing 2" }],
1834
2894
  },
@@ -1846,7 +2906,7 @@ function createBundleNested(args) {
1846
2906
  declarations: args.declarations ?? [],
1847
2907
  messages: args.messages.map((message) => ({
1848
2908
  id: args.id + "_" + message.locale,
1849
- bundleId: args.id,
2909
+ bundle_id: args.id,
1850
2910
  locale: message.locale,
1851
2911
  selectors: message.selectors ?? [],
1852
2912
  variants: message.variants.map((variant) => ({
@@ -1855,11 +2915,26 @@ function createBundleNested(args) {
1855
2915
  message.locale +
1856
2916
  "_" +
1857
2917
  Math.random().toString(36).slice(2),
1858
- messageId: args.id,
2918
+ message_id: args.id,
1859
2919
  matches: variant.matches ?? [],
1860
2920
  pattern: variant.pattern,
1861
2921
  })),
1862
2922
  })),
1863
2923
  };
1864
2924
  }
2925
+ test("emitReadme only claims the output is git-ignored when a .gitignore is emitted", async () => {
2926
+ const project = await loadProjectInMemory({
2927
+ blob: await newProject({
2928
+ settings: { locales: ["en"], baseLocale: "en" },
2929
+ }),
2930
+ });
2931
+ const withGitignore = await compileProject({ project });
2932
+ const withoutGitignore = await compileProject({
2933
+ project,
2934
+ compilerOptions: { emitGitIgnore: false },
2935
+ });
2936
+ expect(withGitignore["README.md"]).toContain("git-ignored");
2937
+ expect(withoutGitignore).not.toHaveProperty(".gitignore");
2938
+ expect(withoutGitignore["README.md"]).not.toContain("git-ignored");
2939
+ });
1865
2940
  //# sourceMappingURL=compile-project.test.js.map