@messagevisor/catalog 0.5.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(
@@ -230,14 +249,6 @@ describe("catalog", function () {
230
249
  const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
231
250
  const index = await readJson<any>(root, "catalog-out/data/root/index.json");
232
251
  const locale = await readJson<any>(root, "catalog-out/data/root/entities/locale/en-US.json");
233
- const localeDuplicates = await readJson<any>(
234
- root,
235
- "catalog-out/data/root/duplicates/locales/en-US.json",
236
- );
237
- const emptyLocaleDuplicates = await readJson<any>(
238
- root,
239
- "catalog-out/data/root/duplicates/locales/nl.json",
240
- );
241
252
  const message = await readJson<any>(
242
253
  root,
243
254
  "catalog-out/data/root/entities/message/common.welcome.json",
@@ -253,11 +264,14 @@ describe("catalog", function () {
253
264
  expect(manifest.sets).toBe(false);
254
265
  expect(manifest.router).toBe("browser");
255
266
  expect(manifest.dev).toBeUndefined();
256
- expect(manifest.features).toEqual({ translationSearch: false });
267
+ expect(manifest.features).toEqual({ translationSearch: false, duplicates: false });
257
268
  expect(manifest.paths.root).toBe("data/root/index.json");
258
269
  await expect(pathExists(root, "catalog-out/data/root/translations/77656c.json")).resolves.toBe(
259
270
  false,
260
271
  );
272
+ await expect(
273
+ pathExists(root, "catalog-out/data/root/duplicates/locales/en-US.json"),
274
+ ).resolves.toBe(false);
261
275
  expect(index.counts.message).toBe(2);
262
276
  expect(
263
277
  index.entities.message.find((entry: any) => entry.key === "common.welcome").targets,
@@ -321,31 +335,6 @@ describe("catalog", function () {
321
335
  ]),
322
336
  );
323
337
  expect(locale.targetFormats.web.number.money.minimumFractionDigits).toBe(2);
324
- expect(localeDuplicates).toEqual({
325
- locale: "en-US",
326
- summary: {
327
- duplicateValues: 1,
328
- duplicateMessageKeys: 2,
329
- },
330
- duplicateValues: [
331
- {
332
- value: "Welcome",
333
- messageKeys: ["common.draft", "common.welcome"],
334
- sources: [
335
- { messageKey: "common.draft", locale: "en" },
336
- { messageKey: "common.welcome", locale: "en" },
337
- ],
338
- },
339
- ],
340
- });
341
- expect(emptyLocaleDuplicates).toEqual({
342
- locale: "nl",
343
- summary: {
344
- duplicateValues: 0,
345
- duplicateMessageKeys: 0,
346
- },
347
- duplicateValues: [],
348
- });
349
338
  expect(target.formatRowsByLocale["en-US"]).toEqual(
350
339
  expect.arrayContaining([
351
340
  expect.objectContaining({
@@ -403,6 +392,56 @@ describe("catalog", function () {
403
392
  expect(history.entries).toEqual([]);
404
393
  });
405
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
+
406
445
  it("exports translation search shards only when opted in", async function () {
407
446
  const root = await createProject();
408
447
  roots.push(root);
@@ -418,11 +457,63 @@ describe("catalog", function () {
418
457
  const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
419
458
  const shard = await readJson<any>(root, "catalog-out/data/root/translations/77656c.json");
420
459
 
421
- expect(manifest.features).toEqual({ translationSearch: true });
460
+ expect(manifest.features).toEqual({ translationSearch: true, duplicates: false });
422
461
  expect(shard["common.welcome"]).toEqual(expect.arrayContaining(["welcome", "welcome pro"]));
423
462
  expect(shard["common.draft"]).toEqual(["welcome"]);
424
463
  });
425
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
+
426
517
  it("streams Git history into project, entity, and last-modified catalog data", async function () {
427
518
  const root = await createProject();
428
519
  roots.push(root);
@@ -700,22 +791,41 @@ describe("catalog", function () {
700
791
  const admin = await readJson<any>(root, "catalog-out/data/sets/admin/index.json");
701
792
 
702
793
  expect(manifest.sets).toBe(true);
703
- expect(manifest.features).toEqual({ translationSearch: false });
794
+ expect(manifest.features).toEqual({ translationSearch: false, duplicates: false });
704
795
  expect(manifest.setKeys).toEqual(["admin", "storefront"]);
705
796
  await expect(
706
797
  pathExists(root, "catalog-out/data/sets/storefront/translations/73746f.json"),
707
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");
708
819
  const storefrontDuplicates = await readJson<any>(
709
820
  root,
710
- "catalog-out/data/sets/storefront/duplicates/locales/en.json",
821
+ "catalog-with-duplicates/data/sets/storefront/duplicates/locales/en.json",
711
822
  );
712
823
  const adminDuplicates = await readJson<any>(
713
824
  root,
714
- "catalog-out/data/sets/admin/duplicates/locales/en.json",
825
+ "catalog-with-duplicates/data/sets/admin/duplicates/locales/en.json",
715
826
  );
716
827
 
717
- expect(storefront.counts.message).toBe(2);
718
- expect(admin.counts.message).toBe(2);
828
+ expect(optInManifest.features).toEqual({ translationSearch: false, duplicates: true });
719
829
  expect(storefrontDuplicates.duplicateValues).toEqual([
720
830
  {
721
831
  value: "storefront",
@@ -736,12 +846,38 @@ describe("catalog", function () {
736
846
  ],
737
847
  },
738
848
  ]);
739
- await expect(
740
- readJson<any>(root, "catalog-out/data/sets/storefront/entities/message/common.welcome.json"),
741
- ).resolves.toMatchObject({
742
- key: "common.welcome",
743
- 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
+ });
744
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");
745
881
  });
746
882
 
747
883
  it("exports set translation search shards when opted in", async function () {
@@ -778,7 +914,7 @@ describe("catalog", function () {
778
914
  "catalog-out/data/sets/admin/translations/61646d.json",
779
915
  );
780
916
 
781
- expect(manifest.features).toEqual({ translationSearch: true });
917
+ expect(manifest.features).toEqual({ translationSearch: true, duplicates: false });
782
918
  expect(storefrontShard).toEqual({ "common.welcome": ["storefront"] });
783
919
  expect(adminShard).toEqual({ "common.welcome": ["admin"] });
784
920
  });
@@ -988,6 +1124,19 @@ describe("catalog plugin", function () {
988
1124
  );
989
1125
  });
990
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
+
991
1140
  it("forwards translation search option for export subcommand", async function () {
992
1141
  const { handler } = createPlugin();
993
1142
 
@@ -1005,16 +1154,45 @@ describe("catalog plugin", function () {
1005
1154
  );
1006
1155
  });
1007
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
+
1008
1174
  it("forwards long and short port options for serve subcommand", async function () {
1009
1175
  const { handler } = createPlugin();
1010
1176
 
1011
- 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
+ });
1012
1184
  expect(serveMock).toHaveBeenLastCalledWith(
1013
1185
  expect.any(String),
1014
1186
  expect.any(Object),
1015
1187
  expect.any(Object),
1016
1188
  expect.not.objectContaining({ withTranslationSearch: true }),
1017
1189
  );
1190
+ expect(serveMock).toHaveBeenLastCalledWith(
1191
+ expect.any(String),
1192
+ expect.any(Object),
1193
+ expect.any(Object),
1194
+ expect.not.objectContaining({ withDuplicates: true }),
1195
+ );
1018
1196
  expect(serveMock).toHaveBeenLastCalledWith(
1019
1197
  expect.any(String),
1020
1198
  expect.any(Object),
@@ -1043,4 +1221,17 @@ describe("catalog plugin", function () {
1043
1221
  expect.objectContaining({ port: undefined }),
1044
1222
  );
1045
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
+ });
1046
1237
  });