@messagevisor/catalog 0.6.0 → 0.8.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.
@@ -22,6 +22,8 @@ interface EntityListHighlightTerms {
22
22
  lastModified: string[];
23
23
  }
24
24
 
25
+ const LIST_SEARCH_QUERY_DEBOUNCE_MS = 450;
26
+
25
27
  function matchesQuery(
26
28
  entity: EntitySummary,
27
29
  parsed: ParsedQuery,
@@ -260,6 +262,168 @@ function setSearchParam(searchParams: URLSearchParams, key: string, value?: stri
260
262
  return next;
261
263
  }
262
264
 
265
+ const EntityListSearchControls = React.memo(function EntityListSearchControls({
266
+ type,
267
+ query,
268
+ firstTargetKey,
269
+ firstLocaleKey,
270
+ translationSearchEnabled,
271
+ onQueryCommit,
272
+ onHintClick,
273
+ }: {
274
+ type: EntityType;
275
+ query: string;
276
+ firstTargetKey: string | undefined;
277
+ firstLocaleKey: string | undefined;
278
+ translationSearchEnabled: boolean;
279
+ onQueryCommit: (value: string) => void;
280
+ onHintClick: (hint: string) => void;
281
+ }) {
282
+ const [inputValue, setInputValue] = React.useState(query);
283
+ const [showHints, setShowHints] = React.useState(false);
284
+ const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
285
+ const idleRef = React.useRef<number | null>(null);
286
+ const animationFrameRef = React.useRef<number | null>(null);
287
+ const postPaintTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
288
+ const hasHintsDefined =
289
+ getQueryHints(type, firstTargetKey, firstLocaleKey, translationSearchEnabled) !== null;
290
+
291
+ const clearPendingCommit = React.useCallback(() => {
292
+ if (debounceRef.current) {
293
+ clearTimeout(debounceRef.current);
294
+ debounceRef.current = null;
295
+ }
296
+
297
+ if (idleRef.current !== null && "cancelIdleCallback" in window) {
298
+ window.cancelIdleCallback(idleRef.current);
299
+ idleRef.current = null;
300
+ }
301
+
302
+ if (animationFrameRef.current !== null) {
303
+ window.cancelAnimationFrame(animationFrameRef.current);
304
+ animationFrameRef.current = null;
305
+ }
306
+
307
+ if (postPaintTimeoutRef.current) {
308
+ clearTimeout(postPaintTimeoutRef.current);
309
+ postPaintTimeoutRef.current = null;
310
+ }
311
+ }, []);
312
+
313
+ const commitAfterBrowserWork = React.useCallback(
314
+ (value: string) => {
315
+ if ("requestIdleCallback" in window) {
316
+ idleRef.current = window.requestIdleCallback(
317
+ () => {
318
+ idleRef.current = null;
319
+ onQueryCommit(value);
320
+ },
321
+ { timeout: 700 },
322
+ );
323
+ return;
324
+ }
325
+
326
+ animationFrameRef.current = window.requestAnimationFrame(() => {
327
+ animationFrameRef.current = null;
328
+ postPaintTimeoutRef.current = setTimeout(() => {
329
+ postPaintTimeoutRef.current = null;
330
+ onQueryCommit(value);
331
+ }, 0);
332
+ });
333
+ },
334
+ [onQueryCommit],
335
+ );
336
+
337
+ const scheduleQueryCommit = React.useCallback(
338
+ (value: string) => {
339
+ clearPendingCommit();
340
+ debounceRef.current = setTimeout(() => {
341
+ debounceRef.current = null;
342
+ commitAfterBrowserWork(value);
343
+ }, LIST_SEARCH_QUERY_DEBOUNCE_MS);
344
+ },
345
+ [clearPendingCommit, commitAfterBrowserWork],
346
+ );
347
+
348
+ const flushQueryCommit = React.useCallback(
349
+ (value: string) => {
350
+ clearPendingCommit();
351
+ onQueryCommit(value);
352
+ },
353
+ [clearPendingCommit, onQueryCommit],
354
+ );
355
+
356
+ React.useEffect(() => {
357
+ clearPendingCommit();
358
+ setInputValue(query);
359
+ }, [query, clearPendingCommit]);
360
+
361
+ React.useEffect(() => {
362
+ return () => {
363
+ clearPendingCommit();
364
+ };
365
+ }, [clearPendingCommit]);
366
+
367
+ return (
368
+ <div>
369
+ <div className="relative">
370
+ <Input
371
+ value={inputValue}
372
+ onChange={(event) => {
373
+ const val = event.target.value;
374
+ setInputValue(val);
375
+ scheduleQueryCommit(val);
376
+ }}
377
+ onBlur={(event) => {
378
+ flushQueryCommit(event.target.value);
379
+ }}
380
+ onKeyDown={(event) => {
381
+ if (event.key === "Enter") {
382
+ flushQueryCommit(event.currentTarget.value);
383
+ }
384
+ }}
385
+ placeholder={`Search ${entityLabels[type].plural.toLowerCase()}...`}
386
+ className={hasHintsDefined ? "pr-10" : ""}
387
+ />
388
+ {hasHintsDefined && (
389
+ <button
390
+ type="button"
391
+ onClick={() => setShowHints((v) => !v)}
392
+ aria-label={showHints ? "Hide advanced search hints" : "Show advanced search hints"}
393
+ className={[
394
+ "absolute right-3 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full border text-xs font-bold transition-colors",
395
+ showHints
396
+ ? "border-primary bg-primary/10 text-primary"
397
+ : "border-border bg-surface text-muted hover:border-primary hover:text-primary",
398
+ ].join(" ")}
399
+ >
400
+ ?
401
+ </button>
402
+ )}
403
+ </div>
404
+
405
+ {/* Animated slide-down hints panel, aligned to input's inner text area */}
406
+ <div
407
+ className={[
408
+ "grid transition-all duration-200 ease-in-out",
409
+ showHints ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
410
+ ].join(" ")}
411
+ >
412
+ <div className="overflow-hidden pl-5">
413
+ <QueryHints
414
+ type={type}
415
+ query={query}
416
+ firstTargetKey={firstTargetKey}
417
+ firstLocaleKey={firstLocaleKey}
418
+ translationSearchEnabled={translationSearchEnabled}
419
+ onHintClick={onHintClick}
420
+ />
421
+ </div>
422
+ </div>
423
+ </div>
424
+ );
425
+ });
426
+
263
427
  // ---- Component ----
264
428
 
265
429
  export function EntityList(props: {
@@ -271,23 +435,39 @@ export function EntityList(props: {
271
435
  }) {
272
436
  const [searchParams, setSearchParams] = useSearchParams();
273
437
  const [showAll, setShowAll] = React.useState(false);
274
- const [showHints, setShowHints] = React.useState(false);
275
438
  const [translationShard, setTranslationShard] = React.useState<TranslationShard | null>(null);
276
439
  const [loadedShardKey, setLoadedShardKey] = React.useState<string | null>(null);
277
- const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
440
+ const translationShardDebounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
278
441
  const query = searchParams.get("q") || "";
279
- const [inputValue, setInputValue] = React.useState(query);
442
+ const searchParamsRef = React.useRef(searchParams);
280
443
 
281
- // Sync input display when the URL query changes externally (hint clicks, navigation)
282
444
  React.useEffect(() => {
283
- setInputValue(query);
284
- }, [query]);
445
+ searchParamsRef.current = searchParams;
446
+ }, [searchParams]);
447
+
448
+ const commitSearchQuery = React.useCallback(
449
+ (value: string) => {
450
+ const nextQuery = value.trim() ? value : undefined;
451
+ if ((searchParamsRef.current.get("q") || "") === (nextQuery || "")) return;
452
+ React.startTransition(() => {
453
+ setSearchParams(setSearchParam(searchParamsRef.current, "q", nextQuery));
454
+ });
455
+ },
456
+ [setSearchParams],
457
+ );
458
+
459
+ React.useEffect(() => {
460
+ return () => {
461
+ if (translationShardDebounceRef.current) {
462
+ clearTimeout(translationShardDebounceRef.current);
463
+ translationShardDebounceRef.current = null;
464
+ }
465
+ };
466
+ }, []);
285
467
 
286
468
  const firstTargetKey = props.allEntities?.target?.find((e) => !e.archived)?.key;
287
469
  const firstLocaleKey = props.allEntities?.locale?.find((e) => !e.archived)?.key;
288
470
  const translationSearchEnabled = props.translationSearchEnabled === true;
289
- const hasHintsDefined =
290
- getQueryHints(props.type, firstTargetKey, firstLocaleKey, translationSearchEnabled) !== null;
291
471
 
292
472
  // Compute the 3-char shard prefix needed for the current query
293
473
  const _translationQual = parseQuery(query).qualifiers.find((q) => q.key === "translation");
@@ -298,7 +478,9 @@ export function EntityList(props: {
298
478
 
299
479
  // Debounced fetch: only triggers when the 3-char prefix changes
300
480
  React.useEffect(() => {
301
- if (debounceRef.current) clearTimeout(debounceRef.current);
481
+ if (translationShardDebounceRef.current) {
482
+ clearTimeout(translationShardDebounceRef.current);
483
+ }
302
484
 
303
485
  if (!neededShardKey) {
304
486
  setTranslationShard(null);
@@ -308,8 +490,8 @@ export function EntityList(props: {
308
490
 
309
491
  if (neededShardKey === loadedShardKey) return;
310
492
 
311
- debounceRef.current = setTimeout(() => {
312
- debounceRef.current = null;
493
+ translationShardDebounceRef.current = setTimeout(() => {
494
+ translationShardDebounceRef.current = null;
313
495
  fetchTranslationShard(neededShardKey, props.setKey).then((data) => {
314
496
  setTranslationShard(data);
315
497
  setLoadedShardKey(neededShardKey);
@@ -317,7 +499,10 @@ export function EntityList(props: {
317
499
  }, 300);
318
500
 
319
501
  return () => {
320
- if (debounceRef.current) clearTimeout(debounceRef.current);
502
+ if (translationShardDebounceRef.current) {
503
+ clearTimeout(translationShardDebounceRef.current);
504
+ translationShardDebounceRef.current = null;
505
+ }
321
506
  };
322
507
  }, [neededShardKey, loadedShardKey, props.setKey]);
323
508
  const sortDirection = getSortDirection(searchParams.get("sort"));
@@ -359,7 +544,7 @@ export function EntityList(props: {
359
544
  : current
360
545
  ? `${current} ${hint}`
361
546
  : hint;
362
- setSearchParams(setSearchParam(searchParams, "q", next || undefined));
547
+ setSearchParams(setSearchParam(searchParamsRef.current, "q", next || undefined));
363
548
  }
364
549
 
365
550
  return (
@@ -367,56 +552,15 @@ export function EntityList(props: {
367
552
  <div className="px-6 pt-1">
368
553
  <div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_auto]">
369
554
  {/* Input + slide-down hints, confined to the first column */}
370
- <div>
371
- <div className="relative">
372
- <Input
373
- value={inputValue}
374
- onChange={(event) => {
375
- const val = event.target.value;
376
- setInputValue(val);
377
- setSearchParams(setSearchParam(searchParams, "q", val.trim() ? val : undefined));
378
- }}
379
- placeholder={`Search ${entityLabels[props.type].plural.toLowerCase()}...`}
380
- className={hasHintsDefined ? "pr-10" : ""}
381
- />
382
- {hasHintsDefined && (
383
- <button
384
- type="button"
385
- onClick={() => setShowHints((v) => !v)}
386
- aria-label={
387
- showHints ? "Hide advanced search hints" : "Show advanced search hints"
388
- }
389
- className={[
390
- "absolute right-3 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full border text-xs font-bold transition-colors",
391
- showHints
392
- ? "border-primary bg-primary/10 text-primary"
393
- : "border-border bg-surface text-muted hover:border-primary hover:text-primary",
394
- ].join(" ")}
395
- >
396
- ?
397
- </button>
398
- )}
399
- </div>
400
-
401
- {/* Animated slide-down hints panel, aligned to input's inner text area */}
402
- <div
403
- className={[
404
- "grid transition-all duration-200 ease-in-out",
405
- showHints ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
406
- ].join(" ")}
407
- >
408
- <div className="overflow-hidden pl-5">
409
- <QueryHints
410
- type={props.type}
411
- query={query}
412
- firstTargetKey={firstTargetKey}
413
- firstLocaleKey={firstLocaleKey}
414
- translationSearchEnabled={translationSearchEnabled}
415
- onHintClick={handleHintClick}
416
- />
417
- </div>
418
- </div>
419
- </div>
555
+ <EntityListSearchControls
556
+ type={props.type}
557
+ query={query}
558
+ firstTargetKey={firstTargetKey}
559
+ firstLocaleKey={firstLocaleKey}
560
+ translationSearchEnabled={translationSearchEnabled}
561
+ onQueryCommit={commitSearchQuery}
562
+ onHintClick={handleHintClick}
563
+ />
420
564
 
421
565
  <button
422
566
  type="button"
@@ -9,7 +9,12 @@ import { Datasource } from "../../../core/src/datasource";
9
9
  import { resolveExamples } from "../../../core/src/examples";
10
10
  import { findDuplicateTranslations } from "../../../core/src/find-duplicates";
11
11
  import { getProjectSetExecutions } from "../../../core/src/sets";
12
- import { createCatalogApi, createCatalogPlugin, type CatalogRuntime } from "./index";
12
+ import {
13
+ __catalogDevInternals,
14
+ createCatalogApi,
15
+ createCatalogPlugin,
16
+ type CatalogRuntime,
17
+ } from "./index";
13
18
 
14
19
  const catalogApi = createCatalogApi({
15
20
  mergeFormats,
@@ -272,6 +277,12 @@ describe("catalog", function () {
272
277
  await expect(
273
278
  pathExists(root, "catalog-out/data/root/duplicates/locales/en-US.json"),
274
279
  ).resolves.toBe(false);
280
+ await expect(
281
+ pathExists(root, "catalog-out/data/root/history/message/common.welcome/page-1.json"),
282
+ ).resolves.toBe(false);
283
+ await expect(
284
+ pathExists(root, "catalog-out/data/root/history/message/common.draft/page-1.json"),
285
+ ).resolves.toBe(false);
275
286
  expect(index.counts.message).toBe(2);
276
287
  expect(
277
288
  index.entities.message.find((entry: any) => entry.key === "common.welcome").targets,
@@ -486,6 +497,8 @@ describe("catalog", function () {
486
497
  expect(output).toContain("Root catalog");
487
498
  expect(output).toContain("Processing entities");
488
499
  expect(output).toContain("Writing messages");
500
+ expect(output).toContain("Writing message details");
501
+ expect(output).toContain("Writing message history pages");
489
502
  expect(output).toContain("Writing manifest");
490
503
  expect(output).toContain("Catalog exported to catalog-out");
491
504
  expect(output).toContain("Time:");
@@ -514,6 +527,62 @@ describe("catalog", function () {
514
527
  expect(output).toContain("Building translation search shards");
515
528
  });
516
529
 
530
+ it("exports many messages deterministically without empty history files", async function () {
531
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-catalog-"));
532
+ roots.push(root);
533
+
534
+ await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
535
+ await writeFile(root, "locales/en.yml", "description: English\n");
536
+
537
+ const messageCount = 1200;
538
+ await Promise.all(
539
+ Array.from({ length: messageCount }, (_, index) => {
540
+ const key = String(index).padStart(4, "0");
541
+ return writeFile(
542
+ root,
543
+ `messages/bulk/${key}.yml`,
544
+ `description: Bulk ${key}\ntranslations:\n en: Bulk ${key}\n`,
545
+ );
546
+ }),
547
+ );
548
+
549
+ const projectConfig = getProjectConfig(root);
550
+ const datasource = new Datasource(projectConfig, root);
551
+
552
+ const output = await captureConsoleLog(async () => {
553
+ await catalogApi.exportCatalog(root, projectConfig, datasource, {
554
+ outDir: "catalog-out",
555
+ copyAssets: false,
556
+ });
557
+ });
558
+
559
+ const index = await readJson<any>(root, "catalog-out/data/root/index.json");
560
+ const firstMessage = await readJson<any>(
561
+ root,
562
+ "catalog-out/data/root/entities/message/bulk.0000.json",
563
+ );
564
+ const lastMessage = await readJson<any>(
565
+ root,
566
+ "catalog-out/data/root/entities/message/bulk.1199.json",
567
+ );
568
+
569
+ expect(index.counts.message).toBe(messageCount);
570
+ expect(index.entities.message.slice(0, 3).map((entry: any) => entry.key)).toEqual([
571
+ "bulk.0000",
572
+ "bulk.0001",
573
+ "bulk.0002",
574
+ ]);
575
+ expect(firstMessage.sourcePath).toBe("messages/bulk/0000.yml");
576
+ expect(lastMessage.translations).toEqual([
577
+ { locale: "en", value: "Bulk 1199", source: "direct" },
578
+ ]);
579
+ await expect(
580
+ pathExists(root, "catalog-out/data/root/history/message/bulk.0000/page-1.json"),
581
+ ).resolves.toBe(false);
582
+ expect(output).toContain("1200 messages");
583
+ expect(output).toContain("1200 empty histories skipped");
584
+ });
585
+
517
586
  it("streams Git history into project, entity, and last-modified catalog data", async function () {
518
587
  const root = await createProject();
519
588
  roots.push(root);
@@ -583,9 +652,12 @@ describe("catalog", function () {
583
652
  ]),
584
653
  );
585
654
  expect(messageHistory.entries).toHaveLength(2);
586
- expect(spacedMessageHistory.entries[0].entities).toEqual(
587
- expect.arrayContaining([{ type: "message", key: "common.with space" }]),
588
- );
655
+ expect(messageHistory.entries[0].entities).toEqual([
656
+ { type: "message", key: "common.welcome" },
657
+ ]);
658
+ expect(spacedMessageHistory.entries[0].entities).toEqual([
659
+ { type: "message", key: "common.with space" },
660
+ ]);
589
661
  expect(message.lastModified).toMatchObject({
590
662
  author: "Catalog Tester",
591
663
  commit: projectHistory.entries[0].commit,
@@ -597,6 +669,67 @@ describe("catalog", function () {
597
669
  });
598
670
  });
599
671
 
672
+ it("keeps large commit entity lists out of per-entity history files", async function () {
673
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-catalog-"));
674
+ roots.push(root);
675
+
676
+ await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
677
+ await writeFile(root, "locales/en.yml", "description: English\n");
678
+
679
+ const messageCount = 1200;
680
+ await Promise.all(
681
+ Array.from({ length: messageCount }, (_, index) => {
682
+ const key = String(index).padStart(4, "0");
683
+ return writeFile(
684
+ root,
685
+ `messages/bulk/${key}.yml`,
686
+ `description: Bulk ${key}\ntranslations:\n en: Bulk ${key}\n`,
687
+ );
688
+ }),
689
+ );
690
+
691
+ git(root, ["init"]);
692
+ git(root, ["add", "."]);
693
+ gitCommit(root, "large message import");
694
+
695
+ const projectConfig = getProjectConfig(root);
696
+ const datasource = new Datasource(projectConfig, root);
697
+
698
+ await catalogApi.exportCatalog(root, projectConfig, datasource, {
699
+ outDir: "catalog-out",
700
+ copyAssets: false,
701
+ });
702
+
703
+ const projectHistory = await readJson<any>(
704
+ root,
705
+ "catalog-out/data/project/history/page-1.json",
706
+ );
707
+ const firstMessageHistory = await readJson<any>(
708
+ root,
709
+ "catalog-out/data/root/history/message/bulk.0000/page-1.json",
710
+ );
711
+ const lastMessageHistory = await readJson<any>(
712
+ root,
713
+ "catalog-out/data/root/history/message/bulk.1199/page-1.json",
714
+ );
715
+
716
+ expect(projectHistory.entries[0].entities).toHaveLength(messageCount + 1);
717
+ expect(projectHistory.entries[0].entities).toEqual(
718
+ expect.arrayContaining([
719
+ { type: "locale", key: "en" },
720
+ { type: "message", key: "bulk.0000" },
721
+ { type: "message", key: "bulk.1199" },
722
+ ]),
723
+ );
724
+ expect(firstMessageHistory.entries[0]).toMatchObject({
725
+ commit: projectHistory.entries[0].commit,
726
+ author: projectHistory.entries[0].author,
727
+ timestamp: projectHistory.entries[0].timestamp,
728
+ entities: [{ type: "message", key: "bulk.0000" }],
729
+ });
730
+ expect(lastMessageHistory.entries[0].entities).toEqual([{ type: "message", key: "bulk.1199" }]);
731
+ });
732
+
600
733
  it("exports branch-aware repository links and hash router mode when requested", async function () {
601
734
  const root = await createProject();
602
735
  roots.push(root);
@@ -1046,6 +1179,104 @@ describe("catalog", function () {
1046
1179
 
1047
1180
  expect(viteConfigSource).toContain('base: "/"');
1048
1181
  });
1182
+
1183
+ it("watches only catalog input roots in dev mode", async function () {
1184
+ const root = await createProject();
1185
+ roots.push(root);
1186
+ const projectConfig = getProjectConfig(root);
1187
+
1188
+ const watchPaths = __catalogDevInternals.getCatalogInputWatchPaths(root, projectConfig);
1189
+
1190
+ expect(watchPaths).toEqual(
1191
+ expect.arrayContaining([
1192
+ path.join(root, "messagevisor.config.js"),
1193
+ projectConfig.localesDirectoryPath,
1194
+ projectConfig.messagesDirectoryPath,
1195
+ projectConfig.attributesDirectoryPath,
1196
+ projectConfig.segmentsDirectoryPath,
1197
+ projectConfig.targetsDirectoryPath,
1198
+ projectConfig.testsDirectoryPath,
1199
+ ]),
1200
+ );
1201
+ expect(watchPaths).not.toContain(projectConfig.catalogDirectoryPath);
1202
+ expect(watchPaths).not.toContain(path.join(root, "node_modules"));
1203
+ });
1204
+
1205
+ it("plans incremental message rebuilds only for safe dev changes", async function () {
1206
+ const root = await createProject();
1207
+ roots.push(root);
1208
+ const projectConfig = getProjectConfig(root);
1209
+ const messagePath = path.join(root, "messages/common/welcome.yml");
1210
+ const localePath = path.join(root, "locales/en-US.yml");
1211
+
1212
+ expect(
1213
+ __catalogDevInternals.classifyCatalogDevChanges(root, projectConfig, [messagePath], {
1214
+ withTranslationSearch: false,
1215
+ withDuplicates: false,
1216
+ }),
1217
+ ).toEqual(
1218
+ expect.objectContaining({
1219
+ kind: "message",
1220
+ messageKeys: ["common.welcome"],
1221
+ }),
1222
+ );
1223
+
1224
+ expect(
1225
+ __catalogDevInternals.classifyCatalogDevChanges(root, projectConfig, [messagePath], {
1226
+ withTranslationSearch: true,
1227
+ withDuplicates: false,
1228
+ }),
1229
+ ).toEqual(expect.objectContaining({ kind: "full" }));
1230
+
1231
+ expect(
1232
+ __catalogDevInternals.classifyCatalogDevChanges(root, projectConfig, [localePath], {
1233
+ withTranslationSearch: false,
1234
+ withDuplicates: false,
1235
+ }),
1236
+ ).toEqual(expect.objectContaining({ kind: "full" }));
1237
+ });
1238
+
1239
+ it("plans set-scoped dev rebuilds for set project input changes", async function () {
1240
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-catalog-"));
1241
+ roots.push(root);
1242
+
1243
+ await writeFile(root, "messagevisor.config.js", "module.exports = { sets: true };\n");
1244
+ await writeFile(root, "sets/storefront/locales/en.yml", "description: English\n");
1245
+ await writeFile(
1246
+ root,
1247
+ "sets/storefront/messages/common/welcome.yml",
1248
+ "description: Welcome\ntranslations:\n en: Welcome\n",
1249
+ );
1250
+
1251
+ const projectConfig = getProjectConfig(root);
1252
+ const localePath = path.join(root, "sets/storefront/locales/en.yml");
1253
+ const messagePath = path.join(root, "sets/storefront/messages/common/welcome.yml");
1254
+
1255
+ expect(__catalogDevInternals.getCatalogInputWatchPaths(root, projectConfig)).toEqual(
1256
+ expect.arrayContaining([
1257
+ path.join(root, "messagevisor.config.js"),
1258
+ projectConfig.setsDirectoryPath,
1259
+ ]),
1260
+ );
1261
+ expect(
1262
+ __catalogDevInternals.classifyCatalogDevChanges(root, projectConfig, [messagePath], {
1263
+ withTranslationSearch: false,
1264
+ withDuplicates: false,
1265
+ }),
1266
+ ).toEqual(
1267
+ expect.objectContaining({
1268
+ kind: "message",
1269
+ set: "storefront",
1270
+ messageKeys: ["common.welcome"],
1271
+ }),
1272
+ );
1273
+ expect(
1274
+ __catalogDevInternals.classifyCatalogDevChanges(root, projectConfig, [localePath], {
1275
+ withTranslationSearch: false,
1276
+ withDuplicates: false,
1277
+ }),
1278
+ ).toEqual(expect.objectContaining({ kind: "set", set: "storefront" }));
1279
+ });
1049
1280
  });
1050
1281
 
1051
1282
  describe("catalog plugin", function () {