@messagevisor/catalog 0.4.0 → 0.6.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.
@@ -46,6 +46,25 @@ async function pathExists(root: string, relativePath: string) {
46
46
  }
47
47
  }
48
48
 
49
+ function stripAnsi(value: string) {
50
+ return value.replace(/\x1b\[[0-9;]*m/g, "").replace(/%s/g, "");
51
+ }
52
+
53
+ async function captureConsoleLog(callback: () => Promise<void>) {
54
+ const logs: string[] = [];
55
+ const spy = jest.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
56
+ logs.push(args.map(String).join(" "));
57
+ });
58
+
59
+ try {
60
+ await callback();
61
+ } finally {
62
+ spy.mockRestore();
63
+ }
64
+
65
+ return stripAnsi(logs.join("\n"));
66
+ }
67
+
49
68
  async function createProject() {
50
69
  const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-catalog-"));
51
70
  const interpolationModulePath = path.join(
@@ -72,6 +91,8 @@ async function createProject() {
72
91
  "direction: ltr",
73
92
  "formats:",
74
93
  " number:",
94
+ " decimal:",
95
+ " maximumFractionDigits: 2",
75
96
  " money:",
76
97
  " style: currency",
77
98
  " currency: USD",
@@ -228,14 +249,6 @@ describe("catalog", function () {
228
249
  const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
229
250
  const index = await readJson<any>(root, "catalog-out/data/root/index.json");
230
251
  const locale = await readJson<any>(root, "catalog-out/data/root/entities/locale/en-US.json");
231
- const localeDuplicates = await readJson<any>(
232
- root,
233
- "catalog-out/data/root/duplicates/locales/en-US.json",
234
- );
235
- const emptyLocaleDuplicates = await readJson<any>(
236
- root,
237
- "catalog-out/data/root/duplicates/locales/nl.json",
238
- );
239
252
  const message = await readJson<any>(
240
253
  root,
241
254
  "catalog-out/data/root/entities/message/common.welcome.json",
@@ -251,11 +264,14 @@ describe("catalog", function () {
251
264
  expect(manifest.sets).toBe(false);
252
265
  expect(manifest.router).toBe("browser");
253
266
  expect(manifest.dev).toBeUndefined();
254
- expect(manifest.features).toEqual({ translationSearch: false });
267
+ expect(manifest.features).toEqual({ translationSearch: false, duplicates: false });
255
268
  expect(manifest.paths.root).toBe("data/root/index.json");
256
269
  await expect(pathExists(root, "catalog-out/data/root/translations/77656c.json")).resolves.toBe(
257
270
  false,
258
271
  );
272
+ await expect(
273
+ pathExists(root, "catalog-out/data/root/duplicates/locales/en-US.json"),
274
+ ).resolves.toBe(false);
259
275
  expect(index.counts.message).toBe(2);
260
276
  expect(
261
277
  index.entities.message.find((entry: any) => entry.key === "common.welcome").targets,
@@ -270,19 +286,16 @@ describe("catalog", function () {
270
286
  "web",
271
287
  ]);
272
288
  expect(index.entities.target.find((entry: any) => entry.key === "web").messageCount).toBe(2);
273
- expect(locale.computedFormats.number.money).toEqual({
274
- style: "currency",
275
- currency: "USD",
276
- currencyDisplay: "code",
277
- });
289
+ expect(locale.computedFormats.number.decimal).toEqual({ maximumFractionDigits: 2 });
290
+ expect(locale.computedFormats.number.money).toEqual({ currencyDisplay: "code" });
278
291
  expect(locale.entity.examples).toHaveLength(1);
279
292
  expect(locale.entity.direction).toBe("ltr");
280
293
  expect(locale.entity.promotable).toBe(false);
281
294
  expect(locale.formatRows).toEqual(
282
295
  expect.arrayContaining([
283
296
  expect.objectContaining({
284
- path: "number.money.style",
285
- value: "currency",
297
+ path: "number.decimal.maximumFractionDigits",
298
+ value: 2,
286
299
  source: "inherited",
287
300
  from: "en",
288
301
  examplePreview: expect.any(String),
@@ -322,31 +335,6 @@ describe("catalog", function () {
322
335
  ]),
323
336
  );
324
337
  expect(locale.targetFormats.web.number.money.minimumFractionDigits).toBe(2);
325
- expect(localeDuplicates).toEqual({
326
- locale: "en-US",
327
- summary: {
328
- duplicateValues: 1,
329
- duplicateMessageKeys: 2,
330
- },
331
- duplicateValues: [
332
- {
333
- value: "Welcome",
334
- messageKeys: ["common.draft", "common.welcome"],
335
- sources: [
336
- { messageKey: "common.draft", locale: "en" },
337
- { messageKey: "common.welcome", locale: "en" },
338
- ],
339
- },
340
- ],
341
- });
342
- expect(emptyLocaleDuplicates).toEqual({
343
- locale: "nl",
344
- summary: {
345
- duplicateValues: 0,
346
- duplicateMessageKeys: 0,
347
- },
348
- duplicateValues: [],
349
- });
350
338
  expect(target.formatRowsByLocale["en-US"]).toEqual(
351
339
  expect.arrayContaining([
352
340
  expect.objectContaining({
@@ -357,8 +345,8 @@ describe("catalog", function () {
357
345
  examplePreview: expect.any(String),
358
346
  }),
359
347
  expect.objectContaining({
360
- path: "number.money.style",
361
- value: "currency",
348
+ path: "number.decimal.maximumFractionDigits",
349
+ value: 2,
362
350
  source: "inherited",
363
351
  from: "en",
364
352
  examplePreview: expect.any(String),
@@ -404,6 +392,56 @@ describe("catalog", function () {
404
392
  expect(history.entries).toEqual([]);
405
393
  });
406
394
 
395
+ it("exports locale duplicate reports only when opted in", async function () {
396
+ const root = await createProject();
397
+ roots.push(root);
398
+ const projectConfig = getProjectConfig(root);
399
+ const datasource = new Datasource(projectConfig, root);
400
+
401
+ await catalogApi.exportCatalog(root, projectConfig, datasource, {
402
+ outDir: "catalog-out",
403
+ copyAssets: false,
404
+ withDuplicates: true,
405
+ });
406
+
407
+ const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
408
+ const localeDuplicates = await readJson<any>(
409
+ root,
410
+ "catalog-out/data/root/duplicates/locales/en-US.json",
411
+ );
412
+ const emptyLocaleDuplicates = await readJson<any>(
413
+ root,
414
+ "catalog-out/data/root/duplicates/locales/nl.json",
415
+ );
416
+
417
+ expect(manifest.features).toEqual({ translationSearch: false, duplicates: true });
418
+ expect(localeDuplicates).toEqual({
419
+ locale: "en-US",
420
+ summary: {
421
+ duplicateValues: 1,
422
+ duplicateMessageKeys: 2,
423
+ },
424
+ duplicateValues: [
425
+ {
426
+ value: "Welcome",
427
+ messageKeys: ["common.draft", "common.welcome"],
428
+ sources: [
429
+ { messageKey: "common.draft", locale: "en" },
430
+ { messageKey: "common.welcome", locale: "en" },
431
+ ],
432
+ },
433
+ ],
434
+ });
435
+ expect(emptyLocaleDuplicates).toEqual({
436
+ locale: "nl",
437
+ summary: {
438
+ duplicateValues: 0,
439
+ duplicateMessageKeys: 0,
440
+ },
441
+ duplicateValues: [],
442
+ });
443
+ });
444
+
407
445
  it("exports translation search shards only when opted in", async function () {
408
446
  const root = await createProject();
409
447
  roots.push(root);
@@ -419,11 +457,63 @@ describe("catalog", function () {
419
457
  const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
420
458
  const shard = await readJson<any>(root, "catalog-out/data/root/translations/77656c.json");
421
459
 
422
- expect(manifest.features).toEqual({ translationSearch: true });
460
+ expect(manifest.features).toEqual({ translationSearch: true, duplicates: false });
423
461
  expect(shard["common.welcome"]).toEqual(expect.arrayContaining(["welcome", "welcome pro"]));
424
462
  expect(shard["common.draft"]).toEqual(["welcome"]);
425
463
  });
426
464
 
465
+ it("prints progress output for default catalog export", async function () {
466
+ const root = await createProject();
467
+ roots.push(root);
468
+ const projectConfig = getProjectConfig(root);
469
+ const datasource = new Datasource(projectConfig, root);
470
+
471
+ const output = await captureConsoleLog(async () => {
472
+ await catalogApi.exportCatalog(root, projectConfig, datasource, {
473
+ outDir: "catalog-out",
474
+ copyAssets: false,
475
+ });
476
+ });
477
+
478
+ expect(output).toContain("Generating Messagevisor catalog");
479
+ expect(output).toContain("Output: catalog-out");
480
+ expect(output).toContain("Router: browser");
481
+ expect(output).toContain("Features: none");
482
+ expect(output).toContain("Preparing output directory");
483
+ expect(output).toContain("Reading Git history");
484
+ expect(output).toContain("Discovering project sets");
485
+ expect(output).toContain("Writing project history");
486
+ expect(output).toContain("Root catalog");
487
+ expect(output).toContain("Processing entities");
488
+ expect(output).toContain("Writing messages");
489
+ expect(output).toContain("Writing manifest");
490
+ expect(output).toContain("Catalog exported to catalog-out");
491
+ expect(output).toContain("Time:");
492
+ expect(output).not.toContain("Scanning duplicate translations");
493
+ expect(output).not.toContain("Building translation search shards");
494
+ });
495
+
496
+ it("prints optional catalog progress only when feature work is enabled", async function () {
497
+ const root = await createProject();
498
+ roots.push(root);
499
+ const projectConfig = getProjectConfig(root);
500
+ const datasource = new Datasource(projectConfig, root);
501
+
502
+ const output = await captureConsoleLog(async () => {
503
+ await catalogApi.exportCatalog(root, projectConfig, datasource, {
504
+ outDir: "catalog-out",
505
+ copyAssets: false,
506
+ withDuplicates: true,
507
+ withTranslationSearch: true,
508
+ });
509
+ });
510
+
511
+ expect(output).toContain("Features: translation search, duplicates");
512
+ expect(output).toContain("Scanning duplicate translations");
513
+ expect(output).toContain("Writing duplicate reports");
514
+ expect(output).toContain("Building translation search shards");
515
+ });
516
+
427
517
  it("streams Git history into project, entity, and last-modified catalog data", async function () {
428
518
  const root = await createProject();
429
519
  roots.push(root);
@@ -701,22 +791,41 @@ describe("catalog", function () {
701
791
  const admin = await readJson<any>(root, "catalog-out/data/sets/admin/index.json");
702
792
 
703
793
  expect(manifest.sets).toBe(true);
704
- expect(manifest.features).toEqual({ translationSearch: false });
794
+ expect(manifest.features).toEqual({ translationSearch: false, duplicates: false });
705
795
  expect(manifest.setKeys).toEqual(["admin", "storefront"]);
706
796
  await expect(
707
797
  pathExists(root, "catalog-out/data/sets/storefront/translations/73746f.json"),
708
798
  ).resolves.toBe(false);
799
+ await expect(
800
+ pathExists(root, "catalog-out/data/sets/storefront/duplicates/locales/en.json"),
801
+ ).resolves.toBe(false);
802
+
803
+ expect(storefront.counts.message).toBe(2);
804
+ expect(admin.counts.message).toBe(2);
805
+ await expect(
806
+ readJson<any>(root, "catalog-out/data/sets/storefront/entities/message/common.welcome.json"),
807
+ ).resolves.toMatchObject({
808
+ key: "common.welcome",
809
+ entity: { translations: { en: "storefront" } },
810
+ });
811
+
812
+ await catalogApi.exportCatalog(root, projectConfig, datasource, {
813
+ outDir: "catalog-with-duplicates",
814
+ copyAssets: false,
815
+ withDuplicates: true,
816
+ });
817
+
818
+ const optInManifest = await readJson<any>(root, "catalog-with-duplicates/data/manifest.json");
709
819
  const storefrontDuplicates = await readJson<any>(
710
820
  root,
711
- "catalog-out/data/sets/storefront/duplicates/locales/en.json",
821
+ "catalog-with-duplicates/data/sets/storefront/duplicates/locales/en.json",
712
822
  );
713
823
  const adminDuplicates = await readJson<any>(
714
824
  root,
715
- "catalog-out/data/sets/admin/duplicates/locales/en.json",
825
+ "catalog-with-duplicates/data/sets/admin/duplicates/locales/en.json",
716
826
  );
717
827
 
718
- expect(storefront.counts.message).toBe(2);
719
- expect(admin.counts.message).toBe(2);
828
+ expect(optInManifest.features).toEqual({ translationSearch: false, duplicates: true });
720
829
  expect(storefrontDuplicates.duplicateValues).toEqual([
721
830
  {
722
831
  value: "storefront",
@@ -737,12 +846,38 @@ describe("catalog", function () {
737
846
  ],
738
847
  },
739
848
  ]);
740
- await expect(
741
- readJson<any>(root, "catalog-out/data/sets/storefront/entities/message/common.welcome.json"),
742
- ).resolves.toMatchObject({
743
- key: "common.welcome",
744
- entity: { translations: { en: "storefront" } },
849
+ });
850
+
851
+ it("prints set names while exporting set project catalogs", async function () {
852
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-catalog-"));
853
+ roots.push(root);
854
+
855
+ await writeFile(root, "messagevisor.config.js", "module.exports = { sets: true };\n");
856
+
857
+ for (const set of ["storefront", "admin"]) {
858
+ await writeFile(root, `sets/${set}/locales/en.yml`, "description: English\n");
859
+ await writeFile(
860
+ root,
861
+ `sets/${set}/messages/common/welcome.yml`,
862
+ `description: Welcome\ntranslations:\n en: ${set}\n`,
863
+ );
864
+ }
865
+
866
+ const projectConfig = getProjectConfig(root);
867
+ const datasource = new Datasource(projectConfig, root);
868
+
869
+ const output = await captureConsoleLog(async () => {
870
+ await catalogApi.exportCatalog(root, projectConfig, datasource, {
871
+ outDir: "catalog-out",
872
+ copyAssets: false,
873
+ });
745
874
  });
875
+
876
+ expect(output).toContain("Sets: enabled");
877
+ expect(output).toContain("Discovering project sets");
878
+ expect(output).toContain('Set "admin"');
879
+ expect(output).toContain('Set "storefront"');
880
+ expect(output).toContain("Processing entities");
746
881
  });
747
882
 
748
883
  it("exports set translation search shards when opted in", async function () {
@@ -779,7 +914,7 @@ describe("catalog", function () {
779
914
  "catalog-out/data/sets/admin/translations/61646d.json",
780
915
  );
781
916
 
782
- expect(manifest.features).toEqual({ translationSearch: true });
917
+ expect(manifest.features).toEqual({ translationSearch: true, duplicates: false });
783
918
  expect(storefrontShard).toEqual({ "common.welcome": ["storefront"] });
784
919
  expect(adminShard).toEqual({ "common.welcome": ["admin"] });
785
920
  });
@@ -989,6 +1124,19 @@ describe("catalog plugin", function () {
989
1124
  );
990
1125
  });
991
1126
 
1127
+ it("forwards duplicates option for dev catalog mode", async function () {
1128
+ const { handler } = createPlugin();
1129
+
1130
+ await handler({ _: ["catalog"], withDuplicates: true });
1131
+
1132
+ expect(exportMock).toHaveBeenLastCalledWith(
1133
+ expect.any(String),
1134
+ expect.any(Object),
1135
+ expect.any(Object),
1136
+ expect.objectContaining({ withDuplicates: true, dev: true }),
1137
+ );
1138
+ });
1139
+
992
1140
  it("forwards translation search option for export subcommand", async function () {
993
1141
  const { handler } = createPlugin();
994
1142
 
@@ -1006,16 +1154,45 @@ describe("catalog plugin", function () {
1006
1154
  );
1007
1155
  });
1008
1156
 
1157
+ it("forwards duplicates option for export subcommand", async function () {
1158
+ const { handler } = createPlugin();
1159
+
1160
+ await handler({
1161
+ _: ["catalog", "export"],
1162
+ subcommand: "export",
1163
+ "with-duplicates": true,
1164
+ });
1165
+
1166
+ expect(exportMock).toHaveBeenLastCalledWith(
1167
+ expect.any(String),
1168
+ expect.any(Object),
1169
+ expect.any(Object),
1170
+ expect.objectContaining({ withDuplicates: true }),
1171
+ );
1172
+ });
1173
+
1009
1174
  it("forwards long and short port options for serve subcommand", async function () {
1010
1175
  const { handler } = createPlugin();
1011
1176
 
1012
- await handler({ _: ["catalog", "serve"], subcommand: "serve", port: 3103 });
1177
+ await handler({
1178
+ _: ["catalog", "serve"],
1179
+ subcommand: "serve",
1180
+ port: 3103,
1181
+ "with-duplicates": true,
1182
+ "with-translation-search": true,
1183
+ });
1013
1184
  expect(serveMock).toHaveBeenLastCalledWith(
1014
1185
  expect.any(String),
1015
1186
  expect.any(Object),
1016
1187
  expect.any(Object),
1017
1188
  expect.not.objectContaining({ withTranslationSearch: true }),
1018
1189
  );
1190
+ expect(serveMock).toHaveBeenLastCalledWith(
1191
+ expect.any(String),
1192
+ expect.any(Object),
1193
+ expect.any(Object),
1194
+ expect.not.objectContaining({ withDuplicates: true }),
1195
+ );
1019
1196
  expect(serveMock).toHaveBeenLastCalledWith(
1020
1197
  expect.any(String),
1021
1198
  expect.any(Object),
@@ -1044,4 +1221,17 @@ describe("catalog plugin", function () {
1044
1221
  expect.objectContaining({ port: undefined }),
1045
1222
  );
1046
1223
  });
1224
+
1225
+ it("does not print catalog generation progress for serve subcommand", async function () {
1226
+ const { handler } = createPlugin();
1227
+
1228
+ const output = await captureConsoleLog(async () => {
1229
+ await handler({ _: ["catalog", "serve"], subcommand: "serve" });
1230
+ });
1231
+
1232
+ expect(output).not.toContain("Generating Messagevisor catalog");
1233
+ expect(output).not.toContain("Processing entities");
1234
+ expect(serveMock).toHaveBeenCalledTimes(1);
1235
+ expect(exportMock).not.toHaveBeenCalled();
1236
+ });
1047
1237
  });