@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.
- package/dist/assets/index-4rkVIXGk.js +73 -0
- package/dist/index.html +1 -1
- package/lib/node/index.d.ts +53 -0
- package/lib/node/index.js +1019 -396
- package/lib/node/index.js.map +1 -1
- package/package.json +2 -2
- package/src/api.spec.ts +46 -1
- package/src/api.ts +29 -1
- package/src/components/lists/EntityList.tsx +207 -63
- package/src/node/index.spec.ts +235 -4
- package/src/node/index.ts +891 -96
- package/dist/assets/index-DJ8oQlZp.js +0 -73
|
@@ -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
|
|
440
|
+
const translationShardDebounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
278
441
|
const query = searchParams.get("q") || "";
|
|
279
|
-
const
|
|
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
|
-
|
|
284
|
-
}, [
|
|
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 (
|
|
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
|
-
|
|
312
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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"
|
package/src/node/index.spec.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
587
|
-
|
|
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 () {
|