@messagevisor/core 0.0.1 → 0.1.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 (211) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +7 -0
  4. package/jest.config.js +8 -0
  5. package/lib/benchmark/index.d.ts +2 -0
  6. package/lib/benchmark/index.js +417 -0
  7. package/lib/benchmark/index.js.map +1 -0
  8. package/lib/builder/index.d.ts +70 -0
  9. package/lib/builder/index.js +831 -0
  10. package/lib/builder/index.js.map +1 -0
  11. package/lib/cli/index.d.ts +28 -0
  12. package/lib/cli/index.js +182 -0
  13. package/lib/cli/index.js.map +1 -0
  14. package/lib/config/index.d.ts +61 -0
  15. package/lib/config/index.js +255 -0
  16. package/lib/config/index.js.map +1 -0
  17. package/lib/create/index.d.ts +2 -0
  18. package/lib/create/index.js +405 -0
  19. package/lib/create/index.js.map +1 -0
  20. package/lib/datasource/filesystemAdapter.d.ts +44 -0
  21. package/lib/datasource/filesystemAdapter.js +424 -0
  22. package/lib/datasource/filesystemAdapter.js.map +1 -0
  23. package/lib/datasource/index.d.ts +39 -0
  24. package/lib/datasource/index.js +96 -0
  25. package/lib/datasource/index.js.map +1 -0
  26. package/lib/error.d.ts +6 -0
  27. package/lib/error.js +49 -0
  28. package/lib/error.js.map +1 -0
  29. package/lib/evaluate/cli.d.ts +8 -0
  30. package/lib/evaluate/cli.js +179 -0
  31. package/lib/evaluate/cli.js.map +1 -0
  32. package/lib/evaluate/index.d.ts +10 -0
  33. package/lib/evaluate/index.js +131 -0
  34. package/lib/evaluate/index.js.map +1 -0
  35. package/lib/examples/coerceExampleIsoDates.d.ts +12 -0
  36. package/lib/examples/coerceExampleIsoDates.js +81 -0
  37. package/lib/examples/coerceExampleIsoDates.js.map +1 -0
  38. package/lib/examples/index.d.ts +63 -0
  39. package/lib/examples/index.js +713 -0
  40. package/lib/examples/index.js.map +1 -0
  41. package/lib/exporter/index.d.ts +60 -0
  42. package/lib/exporter/index.js +610 -0
  43. package/lib/exporter/index.js.map +1 -0
  44. package/lib/find-duplicates/index.d.ts +41 -0
  45. package/lib/find-duplicates/index.js +297 -0
  46. package/lib/find-duplicates/index.js.map +1 -0
  47. package/lib/generate-code/index.d.ts +11 -0
  48. package/lib/generate-code/index.js +157 -0
  49. package/lib/generate-code/index.js.map +1 -0
  50. package/lib/generate-code/typescript.d.ts +14 -0
  51. package/lib/generate-code/typescript.js +307 -0
  52. package/lib/generate-code/typescript.js.map +1 -0
  53. package/lib/importer/index.d.ts +64 -0
  54. package/lib/importer/index.js +1092 -0
  55. package/lib/importer/index.js.map +1 -0
  56. package/lib/index.d.ts +18 -0
  57. package/lib/index.js +35 -0
  58. package/lib/index.js.map +1 -0
  59. package/lib/info/index.d.ts +17 -0
  60. package/lib/info/index.js +132 -0
  61. package/lib/info/index.js.map +1 -0
  62. package/lib/init/index.d.ts +30 -0
  63. package/lib/init/index.js +348 -0
  64. package/lib/init/index.js.map +1 -0
  65. package/lib/lint/index.d.ts +1 -0
  66. package/lib/lint/index.js +6 -0
  67. package/lib/lint/index.js.map +1 -0
  68. package/lib/linter/attributeSchema.d.ts +7 -0
  69. package/lib/linter/attributeSchema.js +36 -0
  70. package/lib/linter/attributeSchema.js.map +1 -0
  71. package/lib/linter/checkLocaleCircularDependency.d.ts +7 -0
  72. package/lib/linter/checkLocaleCircularDependency.js +42 -0
  73. package/lib/linter/checkLocaleCircularDependency.js.map +1 -0
  74. package/lib/linter/conditionSchema.d.ts +3 -0
  75. package/lib/linter/conditionSchema.js +283 -0
  76. package/lib/linter/conditionSchema.js.map +1 -0
  77. package/lib/linter/formatSchema.d.ts +325 -0
  78. package/lib/linter/formatSchema.js +165 -0
  79. package/lib/linter/formatSchema.js.map +1 -0
  80. package/lib/linter/icuStyleLint.d.ts +6 -0
  81. package/lib/linter/icuStyleLint.js +226 -0
  82. package/lib/linter/icuStyleLint.js.map +1 -0
  83. package/lib/linter/index.d.ts +34 -0
  84. package/lib/linter/index.js +557 -0
  85. package/lib/linter/index.js.map +1 -0
  86. package/lib/linter/localeSchema.d.ts +672 -0
  87. package/lib/linter/localeSchema.js +50 -0
  88. package/lib/linter/localeSchema.js.map +1 -0
  89. package/lib/linter/messageSchema.d.ts +35 -0
  90. package/lib/linter/messageSchema.js +115 -0
  91. package/lib/linter/messageSchema.js.map +1 -0
  92. package/lib/linter/printError.d.ts +8 -0
  93. package/lib/linter/printError.js +41 -0
  94. package/lib/linter/printError.js.map +1 -0
  95. package/lib/linter/schema.d.ts +33 -0
  96. package/lib/linter/schema.js +192 -0
  97. package/lib/linter/schema.js.map +1 -0
  98. package/lib/linter/segmentSchema.d.ts +8 -0
  99. package/lib/linter/segmentSchema.js +18 -0
  100. package/lib/linter/segmentSchema.js.map +1 -0
  101. package/lib/linter/targetSchema.d.ts +337 -0
  102. package/lib/linter/targetSchema.js +39 -0
  103. package/lib/linter/targetSchema.js.map +1 -0
  104. package/lib/linter/testSchema.d.ts +71 -0
  105. package/lib/linter/testSchema.js +165 -0
  106. package/lib/linter/testSchema.js.map +1 -0
  107. package/lib/linter/zodHelpers.d.ts +2 -0
  108. package/lib/linter/zodHelpers.js +15 -0
  109. package/lib/linter/zodHelpers.js.map +1 -0
  110. package/lib/list/index.d.ts +8 -0
  111. package/lib/list/index.js +524 -0
  112. package/lib/list/index.js.map +1 -0
  113. package/lib/matrix.d.ts +4 -0
  114. package/lib/matrix.js +66 -0
  115. package/lib/matrix.js.map +1 -0
  116. package/lib/promoter/index.d.ts +65 -0
  117. package/lib/promoter/index.js +1208 -0
  118. package/lib/promoter/index.js.map +1 -0
  119. package/lib/prune/index.d.ts +37 -0
  120. package/lib/prune/index.js +673 -0
  121. package/lib/prune/index.js.map +1 -0
  122. package/lib/sets.d.ts +10 -0
  123. package/lib/sets.js +120 -0
  124. package/lib/sets.js.map +1 -0
  125. package/lib/tester/cliFormat.d.ts +8 -0
  126. package/lib/tester/cliFormat.js +15 -0
  127. package/lib/tester/cliFormat.js.map +1 -0
  128. package/lib/tester/index.d.ts +35 -0
  129. package/lib/tester/index.js +713 -0
  130. package/lib/tester/index.js.map +1 -0
  131. package/lib/tester/matrix.d.ts +14 -0
  132. package/lib/tester/matrix.js +76 -0
  133. package/lib/tester/matrix.js.map +1 -0
  134. package/lib/tester/prettyDuration.d.ts +1 -0
  135. package/lib/tester/prettyDuration.js +30 -0
  136. package/lib/tester/prettyDuration.js.map +1 -0
  137. package/lib/tester/printTestResult.d.ts +2 -0
  138. package/lib/tester/printTestResult.js +32 -0
  139. package/lib/tester/printTestResult.js.map +1 -0
  140. package/lib/tester/types.d.ts +29 -0
  141. package/lib/tester/types.js +3 -0
  142. package/lib/tester/types.js.map +1 -0
  143. package/package.json +41 -13
  144. package/src/benchmark/index.spec.ts +375 -0
  145. package/src/benchmark/index.ts +433 -0
  146. package/src/builder/index.spec.ts +822 -0
  147. package/src/builder/index.ts +920 -0
  148. package/src/cli/index.spec.ts +54 -0
  149. package/src/cli/index.ts +150 -0
  150. package/src/config/index.spec.ts +70 -0
  151. package/src/config/index.ts +259 -0
  152. package/src/create/index.spec.ts +272 -0
  153. package/src/create/index.ts +295 -0
  154. package/src/datasource/filesystemAdapter.ts +313 -0
  155. package/src/datasource/index.ts +135 -0
  156. package/src/error.ts +33 -0
  157. package/src/evaluate/cli.spec.ts +368 -0
  158. package/src/evaluate/cli.ts +130 -0
  159. package/src/evaluate/index.ts +161 -0
  160. package/src/examples/coerceExampleIsoDates.spec.ts +81 -0
  161. package/src/examples/coerceExampleIsoDates.ts +98 -0
  162. package/src/examples/index.spec.ts +453 -0
  163. package/src/examples/index.ts +854 -0
  164. package/src/exporter/index.spec.ts +443 -0
  165. package/src/exporter/index.ts +643 -0
  166. package/src/find-duplicates/index.spec.ts +289 -0
  167. package/src/find-duplicates/index.ts +314 -0
  168. package/src/generate-code/index.ts +92 -0
  169. package/src/generate-code/typescript.spec.ts +241 -0
  170. package/src/generate-code/typescript.ts +284 -0
  171. package/src/importer/index.spec.ts +1101 -0
  172. package/src/importer/index.ts +1190 -0
  173. package/src/index.ts +18 -0
  174. package/src/info/index.ts +67 -0
  175. package/src/init/index.spec.ts +279 -0
  176. package/src/init/index.ts +292 -0
  177. package/src/lint/index.ts +1 -0
  178. package/src/linter/attributeSchema.ts +38 -0
  179. package/src/linter/checkLocaleCircularDependency.ts +51 -0
  180. package/src/linter/conditionSchema.ts +386 -0
  181. package/src/linter/formatSchema.ts +170 -0
  182. package/src/linter/icuStyleLint.ts +312 -0
  183. package/src/linter/index.spec.ts +824 -0
  184. package/src/linter/index.ts +460 -0
  185. package/src/linter/localeSchema.ts +70 -0
  186. package/src/linter/messageSchema.ts +152 -0
  187. package/src/linter/printError.ts +52 -0
  188. package/src/linter/schema.ts +230 -0
  189. package/src/linter/segmentSchema.ts +15 -0
  190. package/src/linter/targetSchema.ts +50 -0
  191. package/src/linter/testSchema.spec.ts +405 -0
  192. package/src/linter/testSchema.ts +239 -0
  193. package/src/linter/zodHelpers.ts +16 -0
  194. package/src/list/index.spec.ts +431 -0
  195. package/src/list/index.ts +463 -0
  196. package/src/matrix.ts +69 -0
  197. package/src/promoter/index.spec.ts +584 -0
  198. package/src/promoter/index.ts +1267 -0
  199. package/src/prune/index.spec.ts +418 -0
  200. package/src/prune/index.ts +693 -0
  201. package/src/sets.ts +74 -0
  202. package/src/tester/cliFormat.ts +11 -0
  203. package/src/tester/featurevisorIntegration.spec.ts +101 -0
  204. package/src/tester/index.spec.ts +577 -0
  205. package/src/tester/index.ts +679 -0
  206. package/src/tester/matrix.ts +106 -0
  207. package/src/tester/prettyDuration.ts +34 -0
  208. package/src/tester/printTestResult.ts +40 -0
  209. package/src/tester/types.ts +32 -0
  210. package/tsconfig.cjs.json +11 -0
  211. package/tsconfig.typecheck.json +4 -0
@@ -0,0 +1,443 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+
5
+ import { getProjectConfig } from "../config";
6
+ import { Datasource } from "../datasource";
7
+ import { exportPlugin, exportProject, exportProjectSets, toCsv } from "./index";
8
+
9
+ async function writeFile(root: string, relativePath: string, content: string) {
10
+ const filePath = path.join(root, relativePath);
11
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
12
+ await fs.promises.writeFile(filePath, content);
13
+ }
14
+
15
+ async function createProject() {
16
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-export-"));
17
+
18
+ await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
19
+ await writeFile(root, "locales/en.yml", "description: English\n");
20
+ await writeFile(
21
+ root,
22
+ "locales/en-US.yml",
23
+ "description: English US\ninheritTranslationsFrom: en\n",
24
+ );
25
+ await writeFile(root, "locales/nl.yml", "description: Dutch\n");
26
+ await writeFile(
27
+ root,
28
+ "targets/web.yml",
29
+ "description: Web\nincludeMessages:\n - common*\n - dashboard*\nexcludeMessages:\n - common.hidden\nlocales:\n - en-US\n - nl\n",
30
+ );
31
+ await writeFile(
32
+ root,
33
+ "targets/admin.yml",
34
+ "description: Admin\nincludeMessages:\n - admin*\nlocales:\n - en-US\n",
35
+ );
36
+ await writeFile(
37
+ root,
38
+ "segments/pro.yml",
39
+ "description: Pro\nconditions:\n - attribute: plan\n operator: equals\n value: pro\n",
40
+ );
41
+ await writeFile(root, "attributes/plan.yml", "description: Plan\ntype: string\n");
42
+ await writeFile(
43
+ root,
44
+ "messages/common/welcome.yml",
45
+ "description: Welcome, translator\nsummary: Welcome summary\ntranslations:\n en: Welcome\n nl: Welkom\noverrides:\n - key: pro\n description: Welcome for Pro users\n summary: Pro welcome summary\n segments: pro\n translations:\n en: Welcome pro\n nl: Welkom pro\n",
46
+ );
47
+ await writeFile(
48
+ root,
49
+ "messages/common/goodbye.yml",
50
+ "description: Goodbye\ntranslations:\n en: Goodbye\n",
51
+ );
52
+ await writeFile(
53
+ root,
54
+ "messages/common/hidden.yml",
55
+ "description: Hidden\ntranslations:\n en: Hidden\n nl: Verborgen\n",
56
+ );
57
+ await writeFile(
58
+ root,
59
+ "messages/common/draft.yml",
60
+ "description: Draft\ntranslations:\n en: Draft\n nl: Concept\n",
61
+ );
62
+ await writeFile(
63
+ root,
64
+ "messages/common/archived.yml",
65
+ "description: Archived\narchived: true\ntranslations:\n en: Archived\n nl: Gearchiveerd\n",
66
+ );
67
+ await writeFile(
68
+ root,
69
+ "messages/dashboard/quote.yml",
70
+ 'description: "Quote, newline"\ntranslations:\n en: "Hello, \\"Ada\\"\\nWelcome"\n nl: "Hallo, \\"Ada\\"\\nWelkom"\n',
71
+ );
72
+ await writeFile(
73
+ root,
74
+ "messages/admin/secret.yml",
75
+ "description: Admin secret\ntranslations:\n en: Secret\n",
76
+ );
77
+
78
+ return root;
79
+ }
80
+
81
+ async function createSetsProject() {
82
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-export-sets-"));
83
+
84
+ await writeFile(root, "messagevisor.config.js", "module.exports = { sets: true };\n");
85
+
86
+ for (const set of ["dev", "production"]) {
87
+ await writeFile(root, `sets/${set}/locales/en.yml`, "description: English\n");
88
+ await writeFile(root, `sets/${set}/locales/nl.yml`, "description: Dutch\n");
89
+ await writeFile(
90
+ root,
91
+ `sets/${set}/targets/web.yml`,
92
+ "description: Web\nincludeMessages:\n - common*\nlocales:\n - en\n - nl\n",
93
+ );
94
+ await writeFile(
95
+ root,
96
+ `sets/${set}/messages/common/welcome.yml`,
97
+ `description: Welcome ${set}\ntranslations:\n en: Welcome ${set}\n nl: Welkom ${set}\n`,
98
+ );
99
+ }
100
+
101
+ return root;
102
+ }
103
+
104
+ function getDatasource(root: string) {
105
+ const projectConfig = getProjectConfig(root);
106
+ const datasource = new Datasource(projectConfig, root);
107
+
108
+ return {
109
+ projectConfig,
110
+ datasource,
111
+ };
112
+ }
113
+
114
+ describe("exportProject", function () {
115
+ it("validates export separator configuration and entity filenames", async function () {
116
+ const invalidRoot = await fs.promises.mkdtemp(
117
+ path.join(os.tmpdir(), "messagevisor-export-config-"),
118
+ );
119
+
120
+ await writeFile(
121
+ invalidRoot,
122
+ "messagevisor.config.js",
123
+ 'module.exports = { namespaceCharacter: ".", exportOverrideKeySeparator: "." };\n',
124
+ );
125
+
126
+ expect(() => getProjectConfig(invalidRoot)).toThrow(
127
+ 'Invalid exportOverrideKeySeparator: it cannot be the same as namespaceCharacter ".".',
128
+ );
129
+
130
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-export-config-"));
131
+
132
+ await writeFile(
133
+ root,
134
+ "messagevisor.config.js",
135
+ 'module.exports = { exportOverrideKeySeparator: ":" };\n',
136
+ );
137
+ await writeFile(
138
+ root,
139
+ "messages/common/wel:come.yml",
140
+ "description: Bad\ntranslations:\n en: Bad\n",
141
+ );
142
+
143
+ const { datasource } = getDatasource(root);
144
+
145
+ await expect(datasource.listMessages()).rejects.toThrow(
146
+ 'exportOverrideKeySeparator ":" is not allowed in directory or file names',
147
+ );
148
+ });
149
+
150
+ it("exports messages, summaries, overrides, inherited translations, and escaped CSV", async function () {
151
+ const root = await createProject();
152
+ const { projectConfig, datasource } = getDatasource(root);
153
+
154
+ const result = await exportProject(projectConfig, datasource, {
155
+ print: true,
156
+ target: "web",
157
+ locale: ["en-US", "nl"],
158
+ });
159
+
160
+ expect(result.filePath).toBeUndefined();
161
+ expect(result.csv).toContain("messageKey,messageDescription,en-US,en-USStatus,nl,nlStatus");
162
+ expect(result.csv).toContain("common.welcome,Welcome summary,Welcome,inherited,Welkom,direct");
163
+ expect(result.csv).toContain(
164
+ "common.welcome:pro,Pro welcome summary,Welcome pro,inherited,Welkom pro,direct",
165
+ );
166
+ expect(result.csv).toContain("common.goodbye,Goodbye,Goodbye,inherited,,missing");
167
+ expect(result.csv).toContain('dashboard.quote,"Quote, newline","Hello, ""Ada""');
168
+ expect(result.csv).toContain('Welcome"');
169
+ expect(result.csv).not.toContain("common.hidden");
170
+ expect(result.csv).toContain("common.draft");
171
+ expect(result.csv).not.toContain("common.archived");
172
+ expect(fs.existsSync(path.join(root, "exports"))).toEqual(false);
173
+ });
174
+
175
+ it("exports all locales by default and repeated locale filters in order", async function () {
176
+ const root = await createProject();
177
+ const { projectConfig, datasource } = getDatasource(root);
178
+
179
+ const allLocales = await exportProject(projectConfig, datasource, {
180
+ print: true,
181
+ includeMessages: "common.welcome",
182
+ withoutDescription: true,
183
+ withoutStatus: true,
184
+ });
185
+ const selectedLocales = await exportProject(projectConfig, datasource, {
186
+ print: true,
187
+ locale: ["en", "nl"],
188
+ includeMessages: "common.welcome",
189
+ withoutDescription: true,
190
+ withoutStatus: true,
191
+ });
192
+
193
+ expect(allLocales.csv.split("\n")[0]).toEqual("messageKey,en,en-US,nl");
194
+ expect(allLocales.summary.locales).toEqual(["en", "en-US", "nl"]);
195
+ expect(selectedLocales.csv.split("\n")[0]).toEqual("messageKey,en,nl");
196
+ expect(selectedLocales.summary.locales).toEqual(["en", "nl"]);
197
+ });
198
+
199
+ it("escapes multiline, quoted, delimiter-heavy translations exactly", function () {
200
+ const csv = toCsv(
201
+ ["messageKey", "messageDescription", "en"],
202
+ [
203
+ [
204
+ "common.multiline",
205
+ ' Leading description; with "quotes" ',
206
+ ' Leading\nLine; "quoted"\r\nTrailing ',
207
+ ],
208
+ ],
209
+ {
210
+ delimiter: ";",
211
+ },
212
+ );
213
+
214
+ expect(csv).toEqual(
215
+ [
216
+ "messageKey;messageDescription;en",
217
+ 'common.multiline;" Leading description; with ""quotes"" ";" Leading\nLine; ""quoted""\r\nTrailing "',
218
+ ].join("\n"),
219
+ );
220
+ });
221
+
222
+ it("filters by locale, include/exclude messages, and excludes overrides", async function () {
223
+ const root = await createProject();
224
+ const { projectConfig, datasource } = getDatasource(root);
225
+
226
+ const result = await exportProject(projectConfig, datasource, {
227
+ print: true,
228
+ locale: "nl",
229
+ includeMessages: "common*",
230
+ excludeMessages: "common.goodbye",
231
+ excludeOverrides: true,
232
+ withoutDescription: true,
233
+ withoutStatus: true,
234
+ });
235
+
236
+ expect(result.csv).toEqual(
237
+ [
238
+ "messageKey,nl",
239
+ "common.draft,Concept",
240
+ "common.hidden,Verborgen",
241
+ "common.welcome,Welkom",
242
+ ].join("\n"),
243
+ );
244
+ });
245
+
246
+ it("supports inherited and directly untranslated filters", async function () {
247
+ const root = await createProject();
248
+ const { projectConfig, datasource } = getDatasource(root);
249
+
250
+ const inherited = await exportProject(projectConfig, datasource, {
251
+ print: true,
252
+ locale: "en-US",
253
+ includeMessages: "common.goodbye",
254
+ onlyUntranslated: true,
255
+ });
256
+ const direct = await exportProject(projectConfig, datasource, {
257
+ print: true,
258
+ locale: "en-US",
259
+ includeMessages: "common.goodbye",
260
+ onlyDirectlyUntranslated: true,
261
+ });
262
+
263
+ expect(inherited.csv).toEqual("messageKey,messageDescription,en-US,en-USStatus");
264
+ expect(direct.csv).toContain("common.goodbye,Goodbye,Goodbye,inherited");
265
+ });
266
+
267
+ it("accepts summary on messages and overrides while falling back to description when absent", async function () {
268
+ const root = await createProject();
269
+ const { projectConfig, datasource } = getDatasource(root);
270
+
271
+ const result = await exportProject(projectConfig, datasource, {
272
+ print: true,
273
+ locale: "en-US",
274
+ includeMessages: ["common.welcome", "common.goodbye"],
275
+ });
276
+
277
+ expect(result.csv).toContain("common.welcome,Welcome summary,Welcome,inherited");
278
+ expect(result.csv).toContain("common.welcome:pro,Pro welcome summary,Welcome pro,inherited");
279
+ expect(result.csv).toContain("common.goodbye,Goodbye,Goodbye,inherited");
280
+ });
281
+
282
+ it("fails when both untranslated filters are requested", async function () {
283
+ const root = await createProject();
284
+ const { projectConfig, datasource } = getDatasource(root);
285
+
286
+ await expect(
287
+ exportProject(projectConfig, datasource, {
288
+ print: true,
289
+ onlyUntranslated: true,
290
+ onlyDirectlyUntranslated: true,
291
+ }),
292
+ ).rejects.toThrow("Use either --onlyUntranslated or --onlyDirectlyUntranslated, not both.");
293
+ });
294
+
295
+ it("writes exports and creates collision-safe file names", async function () {
296
+ const root = await createProject();
297
+ const { projectConfig, datasource } = getDatasource(root);
298
+ const now = new Date(2026, 3, 19, 12, 34, 56);
299
+
300
+ const first = await exportProject(projectConfig, datasource, {
301
+ now,
302
+ locale: "en",
303
+ includeMessages: "common.welcome",
304
+ });
305
+ const second = await exportProject(projectConfig, datasource, {
306
+ now,
307
+ locale: "en",
308
+ includeMessages: "common.welcome",
309
+ });
310
+
311
+ expect(path.basename(first.filePath || "")).toEqual("messagevisor-export-20260419T123456.csv");
312
+ expect(path.basename(second.filePath || "")).toEqual(
313
+ "messagevisor-export-20260419T123456-1.csv",
314
+ );
315
+ expect(await fs.promises.readFile(first.filePath || "", "utf8")).toContain("common.welcome");
316
+ expect(first.summary).toEqual({
317
+ messageRows: 1,
318
+ overrideRows: 1,
319
+ totalRows: 2,
320
+ locales: ["en"],
321
+ sets: [],
322
+ });
323
+ });
324
+
325
+ it("supports output paths, force overwrites, and CSV dialect options", async function () {
326
+ const root = await createProject();
327
+ const { projectConfig, datasource } = getDatasource(root);
328
+ const output = "translator/custom.csv";
329
+
330
+ const first = await exportProject(projectConfig, datasource, {
331
+ output,
332
+ force: true,
333
+ locale: "en",
334
+ includeMessages: "dashboard.quote",
335
+ delimiter: ";",
336
+ bom: true,
337
+ lineEnding: "crlf",
338
+ });
339
+
340
+ await expect(
341
+ exportProject(projectConfig, datasource, {
342
+ output,
343
+ locale: "en",
344
+ includeMessages: "dashboard.quote",
345
+ }),
346
+ ).rejects.toThrow("Pass --force to overwrite.");
347
+
348
+ const content = await fs.promises.readFile(first.filePath || "", "utf8");
349
+
350
+ expect(first.filePath).toEqual(path.join(root, "translator/custom.csv"));
351
+ expect(content.startsWith("\uFEFFmessageKey;messageDescription;en;enStatus\r\n")).toEqual(true);
352
+ expect(content).toContain('"Hello, ""Ada""\nWelcome";direct');
353
+ });
354
+
355
+ it("validates mutually exclusive print/output and delimiter length", async function () {
356
+ const root = await createProject();
357
+ const { projectConfig, datasource } = getDatasource(root);
358
+
359
+ await expect(
360
+ exportProject(projectConfig, datasource, {
361
+ print: true,
362
+ output: "exports/out.csv",
363
+ }),
364
+ ).rejects.toThrow("Use either --print or --output, not both.");
365
+ await expect(
366
+ exportProject(projectConfig, datasource, {
367
+ print: true,
368
+ delimiter: "::",
369
+ }),
370
+ ).rejects.toThrow("--delimiter must be a single character.");
371
+ });
372
+ });
373
+
374
+ describe("exportPlugin", function () {
375
+ it("prints expected option errors without throwing", async function () {
376
+ const root = await createProject();
377
+ const { projectConfig, datasource } = getDatasource(root);
378
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
379
+
380
+ try {
381
+ await expect(
382
+ exportPlugin.handler({
383
+ projectConfig,
384
+ datasource,
385
+ parsed: {
386
+ print: true,
387
+ output: "exports/out.csv",
388
+ },
389
+ }),
390
+ ).resolves.toEqual(false);
391
+
392
+ expect(errorSpy).toHaveBeenCalledWith("Use either --print or --output, not both.");
393
+ } finally {
394
+ errorSpy.mockRestore();
395
+ }
396
+ });
397
+ });
398
+
399
+ describe("exportProjectSets", function () {
400
+ it("exports all sets by default with a set column", async function () {
401
+ const root = await createSetsProject();
402
+ const { projectConfig, datasource } = getDatasource(root);
403
+
404
+ const result = await exportProjectSets(projectConfig, datasource, {
405
+ print: true,
406
+ locale: "en",
407
+ });
408
+
409
+ expect(result.csv).toEqual(
410
+ [
411
+ "set,messageKey,messageDescription,en,enStatus",
412
+ "dev,common.welcome,Welcome dev,Welcome dev,direct",
413
+ "production,common.welcome,Welcome production,Welcome production,direct",
414
+ ].join("\n"),
415
+ );
416
+ expect(result.summary).toEqual({
417
+ messageRows: 2,
418
+ overrideRows: 0,
419
+ totalRows: 2,
420
+ locales: ["en"],
421
+ sets: ["dev", "production"],
422
+ });
423
+ });
424
+
425
+ it("exports selected sets only", async function () {
426
+ const root = await createSetsProject();
427
+ const { projectConfig, datasource } = getDatasource(root);
428
+
429
+ const result = await exportProjectSets(projectConfig, datasource, {
430
+ print: true,
431
+ set: "production",
432
+ target: "web",
433
+ locale: ["en", "nl"],
434
+ });
435
+
436
+ expect(result.csv).toEqual(
437
+ [
438
+ "set,messageKey,messageDescription,en,enStatus,nl,nlStatus",
439
+ "production,common.welcome,Welcome production,Welcome production,direct,Welkom production,direct",
440
+ ].join("\n"),
441
+ );
442
+ });
443
+ });