@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.
- package/dist/assets/index-DJ8oQlZp.js +73 -0
- package/dist/index.html +1 -1
- package/lib/node/index.d.ts +3 -0
- package/lib/node/index.js +330 -152
- package/lib/node/index.js.map +1 -1
- package/package.json +2 -2
- package/src/node/index.spec.ts +246 -56
- package/src/node/index.ts +267 -26
- package/src/pages/EntityDetailPage.tsx +15 -4
- package/src/types.ts +1 -0
- package/dist/assets/index-BJS9aW0t.js +0 -73
package/src/node/index.ts
CHANGED
|
@@ -18,6 +18,117 @@ import type {
|
|
|
18
18
|
|
|
19
19
|
import { attachFormatExamplePreviews } from "./formatExamplePreview";
|
|
20
20
|
|
|
21
|
+
const CLI_FORMAT_GREEN = "\x1b[32m%s\x1b[0m";
|
|
22
|
+
const CLI_FORMAT_DIM = "\x1b[2m%s\x1b[0m";
|
|
23
|
+
const CLI_FORMAT_BOLD = "\x1b[1m%s\x1b[0m";
|
|
24
|
+
|
|
25
|
+
function colorize(value: string, colorCode: number) {
|
|
26
|
+
return `\x1b[${colorCode}m${value}\x1b[0m`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function prettyDuration(diffInMs: number) {
|
|
30
|
+
let diff = Math.abs(diffInMs);
|
|
31
|
+
|
|
32
|
+
if (diff === 0) {
|
|
33
|
+
return "0ms";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ms = diff % 1000;
|
|
37
|
+
diff = (diff - ms) / 1000;
|
|
38
|
+
const secs = diff % 60;
|
|
39
|
+
diff = (diff - secs) / 60;
|
|
40
|
+
const mins = diff % 60;
|
|
41
|
+
const hrs = (diff - mins) / 60;
|
|
42
|
+
|
|
43
|
+
let result = "";
|
|
44
|
+
|
|
45
|
+
if (hrs) {
|
|
46
|
+
result += ` ${hrs}h`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (mins) {
|
|
50
|
+
result += ` ${mins}m`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (secs) {
|
|
54
|
+
result += ` ${secs}s`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (ms) {
|
|
58
|
+
result += ` ${ms}ms`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pluralize(count: number, singular: string, plural = `${singular}s`) {
|
|
65
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatCatalogPath(rootDirectoryPath: string, filePath: string) {
|
|
69
|
+
const relativePath = path.relative(rootDirectoryPath, filePath);
|
|
70
|
+
|
|
71
|
+
if (relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
72
|
+
return relativePath;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return filePath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class CatalogProgressReporter {
|
|
79
|
+
private readonly startedAt = Date.now();
|
|
80
|
+
|
|
81
|
+
constructor(
|
|
82
|
+
private readonly rootDirectoryPath: string,
|
|
83
|
+
private readonly outputDirectoryPath: string,
|
|
84
|
+
) {}
|
|
85
|
+
|
|
86
|
+
start(options: { browserRouter: boolean; sets: boolean; features: string[] }) {
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log(CLI_FORMAT_BOLD, "Generating Messagevisor catalog");
|
|
89
|
+
console.log(
|
|
90
|
+
` ${colorize("Output", 36)}: ${formatCatalogPath(
|
|
91
|
+
this.rootDirectoryPath,
|
|
92
|
+
this.outputDirectoryPath,
|
|
93
|
+
)}`,
|
|
94
|
+
);
|
|
95
|
+
console.log(` ${colorize("Router", 36)}: ${options.browserRouter ? "browser" : "hash"}`);
|
|
96
|
+
console.log(` ${colorize("Sets", 36)}: ${options.sets ? "enabled" : "none"}`);
|
|
97
|
+
console.log(` ${colorize("Features", 36)}: ${options.features.join(", ") || "none"}`);
|
|
98
|
+
console.log("");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
step(label: string, detail?: string) {
|
|
102
|
+
const suffix = detail ? `: ${colorize(detail, 2)}` : "";
|
|
103
|
+
console.log(` ${colorize("•", 36)} ${label}${suffix}`);
|
|
104
|
+
return Date.now();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
done(startedAt: number, detail?: string) {
|
|
108
|
+
const suffix = detail ? ` ${detail}` : "";
|
|
109
|
+
console.log(CLI_FORMAT_DIM, ` done in ${prettyDuration(Date.now() - startedAt)}${suffix}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setStart(set: string | undefined) {
|
|
113
|
+
console.log("");
|
|
114
|
+
if (set) {
|
|
115
|
+
console.log(CLI_FORMAT_BOLD, `Set "${set}"`);
|
|
116
|
+
} else {
|
|
117
|
+
console.log(CLI_FORMAT_BOLD, "Root catalog");
|
|
118
|
+
}
|
|
119
|
+
return Date.now();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
complete() {
|
|
123
|
+
console.log("");
|
|
124
|
+
console.log(
|
|
125
|
+
CLI_FORMAT_GREEN,
|
|
126
|
+
`Catalog exported to ${formatCatalogPath(this.rootDirectoryPath, this.outputDirectoryPath)}`,
|
|
127
|
+
);
|
|
128
|
+
console.log(CLI_FORMAT_BOLD, `Time: ${prettyDuration(Date.now() - this.startedAt)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
21
132
|
export interface CatalogPluginParsedOptions {
|
|
22
133
|
_: string[];
|
|
23
134
|
[key: string]: any;
|
|
@@ -223,6 +334,7 @@ export interface CatalogExportOptions {
|
|
|
223
334
|
dev?: boolean;
|
|
224
335
|
devEditors?: CatalogDevEditor[];
|
|
225
336
|
withTranslationSearch?: boolean;
|
|
337
|
+
withDuplicates?: boolean;
|
|
226
338
|
}
|
|
227
339
|
|
|
228
340
|
export interface CatalogServeOptions {
|
|
@@ -247,6 +359,8 @@ interface CatalogBuildContext {
|
|
|
247
359
|
devEditors: CatalogDevEditor[];
|
|
248
360
|
duplicateResultsBySet: Record<string, CatalogDuplicateTranslationsSetResult>;
|
|
249
361
|
withTranslationSearch: boolean;
|
|
362
|
+
withDuplicates: boolean;
|
|
363
|
+
progress: CatalogProgressReporter;
|
|
250
364
|
}
|
|
251
365
|
|
|
252
366
|
interface SourceFileInfo {
|
|
@@ -1168,6 +1282,8 @@ async function buildSetCatalog(
|
|
|
1168
1282
|
outputRelativeDirectory: string,
|
|
1169
1283
|
) {
|
|
1170
1284
|
const outputDirectoryPath = path.join(context.dataDirectoryPath, outputRelativeDirectory);
|
|
1285
|
+
const setStartedAt = context.progress.setStart(set);
|
|
1286
|
+
const entitiesStartedAt = context.progress.step("Processing entities");
|
|
1171
1287
|
const [localeKeys, messageKeys, attributeKeys, segmentKeys, targetKeys] = await Promise.all([
|
|
1172
1288
|
datasource.listLocales(),
|
|
1173
1289
|
datasource.listMessages(),
|
|
@@ -1182,6 +1298,18 @@ async function buildSetCatalog(
|
|
|
1182
1298
|
readAll<Segment>(segmentKeys, (key) => datasource.readSegment(key)),
|
|
1183
1299
|
readAll<Target>(targetKeys, (key) => datasource.readTarget(key)),
|
|
1184
1300
|
]);
|
|
1301
|
+
context.progress.done(
|
|
1302
|
+
entitiesStartedAt,
|
|
1303
|
+
`(${[
|
|
1304
|
+
pluralize(localeKeys.length, "locale"),
|
|
1305
|
+
pluralize(messageKeys.length, "message"),
|
|
1306
|
+
pluralize(attributeKeys.length, "attribute"),
|
|
1307
|
+
pluralize(segmentKeys.length, "segment"),
|
|
1308
|
+
pluralize(targetKeys.length, "target"),
|
|
1309
|
+
].join(", ")})`,
|
|
1310
|
+
);
|
|
1311
|
+
|
|
1312
|
+
const relationshipsStartedAt = context.progress.step("Mapping relationships");
|
|
1185
1313
|
const messageTargets: Record<string, string[]> = {};
|
|
1186
1314
|
const targetMessages: Record<string, string[]> = {};
|
|
1187
1315
|
const localeTargets: Record<string, Set<string>> = {};
|
|
@@ -1282,6 +1410,7 @@ async function buildSetCatalog(
|
|
|
1282
1410
|
}
|
|
1283
1411
|
}
|
|
1284
1412
|
}
|
|
1413
|
+
context.progress.done(relationshipsStartedAt);
|
|
1285
1414
|
|
|
1286
1415
|
const history = set ? context.historyIndex.bySet[set] || [] : context.historyIndex.entries;
|
|
1287
1416
|
const localeDirections = getLocaleDirections(locales);
|
|
@@ -1310,8 +1439,11 @@ async function buildSetCatalog(
|
|
|
1310
1439
|
},
|
|
1311
1440
|
};
|
|
1312
1441
|
|
|
1442
|
+
const historyStartedAt = context.progress.step("Writing history pages");
|
|
1313
1443
|
await writeHistoryPages(path.join(outputDirectoryPath, "history"), history);
|
|
1444
|
+
context.progress.done(historyStartedAt, `(${pluralize(history.length, "entry", "entries")})`);
|
|
1314
1445
|
|
|
1446
|
+
const examplesStartedAt = context.progress.step("Evaluating examples");
|
|
1315
1447
|
const evaluatedMessageExamplesByKey = (
|
|
1316
1448
|
await context.runtime.resolveExamples(projectConfig, datasource, {
|
|
1317
1449
|
onlyMessages: true,
|
|
@@ -1345,7 +1477,21 @@ async function buildSetCatalog(
|
|
|
1345
1477
|
});
|
|
1346
1478
|
return accumulator;
|
|
1347
1479
|
}, {});
|
|
1480
|
+
context.progress.done(
|
|
1481
|
+
examplesStartedAt,
|
|
1482
|
+
`(${pluralize(
|
|
1483
|
+
Object.values(evaluatedMessageExamplesByKey).reduce(
|
|
1484
|
+
(total, items) => total + items.length,
|
|
1485
|
+
0,
|
|
1486
|
+
),
|
|
1487
|
+
"message example",
|
|
1488
|
+
)}, ${pluralize(
|
|
1489
|
+
Object.values(evaluatedLocaleExamplesByKey).reduce((total, items) => total + items.length, 0),
|
|
1490
|
+
"locale example",
|
|
1491
|
+
)})`,
|
|
1492
|
+
);
|
|
1348
1493
|
|
|
1494
|
+
const localesStartedAt = context.progress.step("Writing locales");
|
|
1349
1495
|
for (const localeKey of localeKeys) {
|
|
1350
1496
|
const locale = locales[localeKey];
|
|
1351
1497
|
const sourceFileInfo = getSourceFileInfo(
|
|
@@ -1383,15 +1529,25 @@ async function buildSetCatalog(
|
|
|
1383
1529
|
path.join(outputDirectoryPath, "entities", "locale", `${encodeKey(localeKey)}.json`),
|
|
1384
1530
|
detail,
|
|
1385
1531
|
);
|
|
1386
|
-
await writeJson(
|
|
1387
|
-
path.join(outputDirectoryPath, "duplicates", "locales", `${encodeKey(localeKey)}.json`),
|
|
1388
|
-
toLocaleDuplicatesFile(localeKey, duplicatesByLocale),
|
|
1389
|
-
);
|
|
1390
1532
|
await writeHistoryPages(
|
|
1391
1533
|
path.join(outputDirectoryPath, "history", "locale", encodeKey(localeKey)),
|
|
1392
1534
|
getHistoryForEntity(context.historyIndex, "locale", localeKey, set || undefined),
|
|
1393
1535
|
);
|
|
1394
1536
|
}
|
|
1537
|
+
context.progress.done(localesStartedAt, `(${pluralize(localeKeys.length, "locale")})`);
|
|
1538
|
+
|
|
1539
|
+
if (context.withDuplicates) {
|
|
1540
|
+
const duplicatesStartedAt = context.progress.step("Writing duplicate reports");
|
|
1541
|
+
|
|
1542
|
+
for (const localeKey of localeKeys) {
|
|
1543
|
+
await writeJson(
|
|
1544
|
+
path.join(outputDirectoryPath, "duplicates", "locales", `${encodeKey(localeKey)}.json`),
|
|
1545
|
+
toLocaleDuplicatesFile(localeKey, duplicatesByLocale),
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
context.progress.done(duplicatesStartedAt, `(${pluralize(localeKeys.length, "locale")})`);
|
|
1550
|
+
}
|
|
1395
1551
|
|
|
1396
1552
|
// translationShards[3charPrefix][messageKey] = Set<lowercased value>
|
|
1397
1553
|
const translationShards: Record<string, Record<string, Set<string>>> = {};
|
|
@@ -1411,6 +1567,7 @@ async function buildSetCatalog(
|
|
|
1411
1567
|
}
|
|
1412
1568
|
}
|
|
1413
1569
|
|
|
1570
|
+
const messagesStartedAt = context.progress.step("Writing messages");
|
|
1414
1571
|
for (const messageKey of messageKeys) {
|
|
1415
1572
|
const message = messages[messageKey];
|
|
1416
1573
|
const overrides = (message.overrides || []).map((override: Override) => {
|
|
@@ -1465,22 +1622,6 @@ async function buildSetCatalog(
|
|
|
1465
1622
|
}
|
|
1466
1623
|
const overrideLocalesList = sortStrings(Array.from(overrideLocalesSet));
|
|
1467
1624
|
|
|
1468
|
-
if (context.withTranslationSearch) {
|
|
1469
|
-
// Build translation shards (direct + inherited + override, all locales combined)
|
|
1470
|
-
for (const localeKey of localeKeys) {
|
|
1471
|
-
const row = resolveTranslationRow(message.translations, localeKey, locales);
|
|
1472
|
-
if (row.source !== "missing" && row.value) {
|
|
1473
|
-
addToTranslationShard(messageKey, row.value);
|
|
1474
|
-
}
|
|
1475
|
-
for (const override of overrides) {
|
|
1476
|
-
const overrideRow = resolveTranslationRow(override.translations, localeKey, locales);
|
|
1477
|
-
if (overrideRow.source !== "missing" && overrideRow.value) {
|
|
1478
|
-
addToTranslationShard(messageKey, overrideRow.value);
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
1625
|
index.entities.message.push(
|
|
1485
1626
|
getEntitySummary(message, "message", messageKey, context.historyIndex, set || undefined, {
|
|
1486
1627
|
targets: sortStrings(messageTargets[messageKey] || []),
|
|
@@ -1497,8 +1638,29 @@ async function buildSetCatalog(
|
|
|
1497
1638
|
getHistoryForEntity(context.historyIndex, "message", messageKey, set || undefined),
|
|
1498
1639
|
);
|
|
1499
1640
|
}
|
|
1641
|
+
context.progress.done(messagesStartedAt, `(${pluralize(messageKeys.length, "message")})`);
|
|
1500
1642
|
|
|
1501
1643
|
if (context.withTranslationSearch) {
|
|
1644
|
+
const translationSearchStartedAt = context.progress.step("Building translation search shards");
|
|
1645
|
+
|
|
1646
|
+
for (const messageKey of messageKeys) {
|
|
1647
|
+
const message = messages[messageKey];
|
|
1648
|
+
|
|
1649
|
+
for (const localeKey of localeKeys) {
|
|
1650
|
+
const row = resolveTranslationRow(message.translations, localeKey, locales);
|
|
1651
|
+
if (row.source !== "missing" && row.value) {
|
|
1652
|
+
addToTranslationShard(messageKey, row.value);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
for (const override of message.overrides || []) {
|
|
1656
|
+
const overrideRow = resolveTranslationRow(override.translations, localeKey, locales);
|
|
1657
|
+
if (overrideRow.source !== "missing" && overrideRow.value) {
|
|
1658
|
+
addToTranslationShard(messageKey, overrideRow.value);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1502
1664
|
for (const [prefix, messageMap] of Object.entries(translationShards)) {
|
|
1503
1665
|
const shardData: Record<string, string[]> = {};
|
|
1504
1666
|
for (const [msgKey, valueSet] of Object.entries(messageMap)) {
|
|
@@ -1506,8 +1668,13 @@ async function buildSetCatalog(
|
|
|
1506
1668
|
}
|
|
1507
1669
|
await writeJson(path.join(outputDirectoryPath, "translations", `${prefix}.json`), shardData);
|
|
1508
1670
|
}
|
|
1671
|
+
context.progress.done(
|
|
1672
|
+
translationSearchStartedAt,
|
|
1673
|
+
`(${pluralize(Object.keys(translationShards).length, "shard")})`,
|
|
1674
|
+
);
|
|
1509
1675
|
}
|
|
1510
1676
|
|
|
1677
|
+
const attributesStartedAt = context.progress.step("Writing attributes");
|
|
1511
1678
|
for (const attributeKey of attributeKeys) {
|
|
1512
1679
|
const attribute = attributes[attributeKey];
|
|
1513
1680
|
const sourceFileInfo = getSourceFileInfo(
|
|
@@ -1556,7 +1723,9 @@ async function buildSetCatalog(
|
|
|
1556
1723
|
getHistoryForEntity(context.historyIndex, "attribute", attributeKey, set || undefined),
|
|
1557
1724
|
);
|
|
1558
1725
|
}
|
|
1726
|
+
context.progress.done(attributesStartedAt, `(${pluralize(attributeKeys.length, "attribute")})`);
|
|
1559
1727
|
|
|
1728
|
+
const segmentsStartedAt = context.progress.step("Writing segments");
|
|
1560
1729
|
for (const segmentKey of segmentKeys) {
|
|
1561
1730
|
const segment = segments[segmentKey];
|
|
1562
1731
|
const usedAttributes = new Set<string>();
|
|
@@ -1595,7 +1764,9 @@ async function buildSetCatalog(
|
|
|
1595
1764
|
getHistoryForEntity(context.historyIndex, "segment", segmentKey, set || undefined),
|
|
1596
1765
|
);
|
|
1597
1766
|
}
|
|
1767
|
+
context.progress.done(segmentsStartedAt, `(${pluralize(segmentKeys.length, "segment")})`);
|
|
1598
1768
|
|
|
1769
|
+
const targetsStartedAt = context.progress.step("Writing targets");
|
|
1599
1770
|
for (const targetKey of targetKeys) {
|
|
1600
1771
|
const target = targets[targetKey];
|
|
1601
1772
|
const targetLocaleKeys = target.locales?.length ? target.locales : localeKeys;
|
|
@@ -1641,12 +1812,16 @@ async function buildSetCatalog(
|
|
|
1641
1812
|
getHistoryForEntity(context.historyIndex, "target", targetKey, set || undefined),
|
|
1642
1813
|
);
|
|
1643
1814
|
}
|
|
1815
|
+
context.progress.done(targetsStartedAt, `(${pluralize(targetKeys.length, "target")})`);
|
|
1644
1816
|
|
|
1817
|
+
const indexStartedAt = context.progress.step("Writing catalog index");
|
|
1645
1818
|
for (const type of Object.keys(index.entities) as CatalogEntityType[]) {
|
|
1646
1819
|
index.entities[type].sort((a, b) => a.key.localeCompare(b.key));
|
|
1647
1820
|
}
|
|
1648
1821
|
|
|
1649
1822
|
await writeJson(path.join(outputDirectoryPath, "index.json"), index);
|
|
1823
|
+
context.progress.done(indexStartedAt);
|
|
1824
|
+
context.progress.done(setStartedAt, "total");
|
|
1650
1825
|
|
|
1651
1826
|
return index;
|
|
1652
1827
|
}
|
|
@@ -1685,20 +1860,64 @@ export async function exportCatalog(
|
|
|
1685
1860
|
: projectConfig.catalogDirectoryPath;
|
|
1686
1861
|
const dataDirectoryPath = path.join(outputDirectoryPath, "data");
|
|
1687
1862
|
const withTranslationSearch = options.withTranslationSearch === true;
|
|
1863
|
+
const withDuplicates = options.withDuplicates === true;
|
|
1864
|
+
const progress = new CatalogProgressReporter(rootDirectoryPath, outputDirectoryPath);
|
|
1865
|
+
|
|
1866
|
+
progress.start({
|
|
1867
|
+
browserRouter: options.browserRouter !== false,
|
|
1868
|
+
sets: projectConfig.sets === true,
|
|
1869
|
+
features: [
|
|
1870
|
+
...(withTranslationSearch ? ["translation search"] : []),
|
|
1871
|
+
...(withDuplicates ? ["duplicates"] : []),
|
|
1872
|
+
],
|
|
1873
|
+
});
|
|
1688
1874
|
|
|
1875
|
+
let stepStartedAt = progress.step("Preparing output directory");
|
|
1689
1876
|
await fs.promises.rm(outputDirectoryPath, { recursive: true, force: true });
|
|
1690
1877
|
await fs.promises.mkdir(dataDirectoryPath, { recursive: true });
|
|
1878
|
+
progress.done(stepStartedAt);
|
|
1691
1879
|
|
|
1692
1880
|
if (options.copyAssets !== false) {
|
|
1881
|
+
stepStartedAt = progress.step("Copying Catalog UI assets");
|
|
1693
1882
|
await copyCatalogAssets(outputDirectoryPath);
|
|
1883
|
+
progress.done(stepStartedAt);
|
|
1694
1884
|
}
|
|
1695
1885
|
|
|
1696
1886
|
const devEditors = options.dev ? options.devEditors || detectDevEditors() : [];
|
|
1887
|
+
stepStartedAt = progress.step("Reading Git history");
|
|
1697
1888
|
const historyIndex = await getGitHistoryIndex(rootDirectoryPath, projectConfig);
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
);
|
|
1889
|
+
progress.done(stepStartedAt, `(${pluralize(historyIndex.entries.length, "commit")})`);
|
|
1890
|
+
|
|
1891
|
+
stepStartedAt = progress.step("Resolving repository links");
|
|
1892
|
+
const links = getRepoLinks(rootDirectoryPath);
|
|
1893
|
+
progress.done(stepStartedAt);
|
|
1894
|
+
|
|
1895
|
+
let duplicateResultsBySet: Record<string, CatalogDuplicateTranslationsSetResult> = {};
|
|
1896
|
+
if (withDuplicates) {
|
|
1897
|
+
stepStartedAt = progress.step("Scanning duplicate translations");
|
|
1898
|
+
duplicateResultsBySet = Object.fromEntries(
|
|
1899
|
+
(await runtime.findDuplicateTranslations(projectConfig, datasource)).results.map((result) => [
|
|
1900
|
+
getDuplicateSetKey(result.set),
|
|
1901
|
+
result,
|
|
1902
|
+
]),
|
|
1903
|
+
);
|
|
1904
|
+
progress.done(
|
|
1905
|
+
stepStartedAt,
|
|
1906
|
+
`(${pluralize(
|
|
1907
|
+
Object.values(duplicateResultsBySet).reduce(
|
|
1908
|
+
(total, result) =>
|
|
1909
|
+
total +
|
|
1910
|
+
result.locales.reduce(
|
|
1911
|
+
(localeTotal, localeResult) => localeTotal + localeResult.duplicateValues.length,
|
|
1912
|
+
0,
|
|
1913
|
+
),
|
|
1914
|
+
0,
|
|
1915
|
+
),
|
|
1916
|
+
"duplicate value",
|
|
1917
|
+
)})`,
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1702
1921
|
const context: CatalogBuildContext = {
|
|
1703
1922
|
rootDirectoryPath,
|
|
1704
1923
|
repositoryRootDirectoryPath: getRepositoryRootDirectoryPath(rootDirectoryPath),
|
|
@@ -1709,11 +1928,22 @@ export async function exportCatalog(
|
|
|
1709
1928
|
devEditors,
|
|
1710
1929
|
duplicateResultsBySet,
|
|
1711
1930
|
withTranslationSearch,
|
|
1931
|
+
withDuplicates,
|
|
1932
|
+
progress,
|
|
1712
1933
|
};
|
|
1934
|
+
stepStartedAt = progress.step("Discovering project sets");
|
|
1713
1935
|
const executions = await runtime.getProjectSetExecutions(projectConfig, datasource);
|
|
1936
|
+
progress.done(
|
|
1937
|
+
stepStartedAt,
|
|
1938
|
+
projectConfig.sets
|
|
1939
|
+
? `(${executions.map((execution) => execution.set).join(", ") || "none"})`
|
|
1940
|
+
: "(root)",
|
|
1941
|
+
);
|
|
1714
1942
|
const setIndexes: Record<string, CatalogSetIndex> = {};
|
|
1715
1943
|
|
|
1944
|
+
stepStartedAt = progress.step("Writing project history");
|
|
1716
1945
|
await writeHistoryPages(path.join(dataDirectoryPath, "project", "history"), historyIndex.entries);
|
|
1946
|
+
progress.done(stepStartedAt, `(${pluralize(historyIndex.entries.length, "entry", "entries")})`);
|
|
1717
1947
|
|
|
1718
1948
|
for (const execution of executions) {
|
|
1719
1949
|
const outputRelativeDirectory = projectConfig.sets ? path.join("sets", execution.set) : "root";
|
|
@@ -1726,6 +1956,7 @@ export async function exportCatalog(
|
|
|
1726
1956
|
);
|
|
1727
1957
|
}
|
|
1728
1958
|
|
|
1959
|
+
stepStartedAt = progress.step("Writing manifest");
|
|
1729
1960
|
const manifest = {
|
|
1730
1961
|
schemaVersion: CATALOG_SCHEMA_VERSION,
|
|
1731
1962
|
generatedAt: new Date().toISOString(),
|
|
@@ -1735,8 +1966,9 @@ export async function exportCatalog(
|
|
|
1735
1966
|
dev: options.dev ? { editors: devEditors } : undefined,
|
|
1736
1967
|
features: {
|
|
1737
1968
|
translationSearch: withTranslationSearch,
|
|
1969
|
+
duplicates: withDuplicates,
|
|
1738
1970
|
},
|
|
1739
|
-
links
|
|
1971
|
+
links,
|
|
1740
1972
|
paths: {
|
|
1741
1973
|
projectHistory: "data/project/history/page-1.json",
|
|
1742
1974
|
root: projectConfig.sets ? undefined : "data/root/index.json",
|
|
@@ -1753,8 +1985,9 @@ export async function exportCatalog(
|
|
|
1753
1985
|
};
|
|
1754
1986
|
|
|
1755
1987
|
await writeJson(path.join(dataDirectoryPath, "manifest.json"), manifest);
|
|
1988
|
+
progress.done(stepStartedAt);
|
|
1756
1989
|
|
|
1757
|
-
|
|
1990
|
+
progress.complete();
|
|
1758
1991
|
|
|
1759
1992
|
return {
|
|
1760
1993
|
outputDirectoryPath,
|
|
@@ -2032,6 +2265,10 @@ function isWithTranslationSearchEnabled(parsed: CatalogPluginParsedOptions) {
|
|
|
2032
2265
|
return parsed.withTranslationSearch === true || parsed["with-translation-search"] === true;
|
|
2033
2266
|
}
|
|
2034
2267
|
|
|
2268
|
+
function isWithDuplicatesEnabled(parsed: CatalogPluginParsedOptions) {
|
|
2269
|
+
return parsed.withDuplicates === true || parsed["with-duplicates"] === true;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2035
2272
|
export function createCatalogPlugin(
|
|
2036
2273
|
runtime: CatalogRuntime,
|
|
2037
2274
|
api: ReturnType<typeof createCatalogApi> = createCatalogApi(runtime),
|
|
@@ -2042,6 +2279,7 @@ export function createCatalogPlugin(
|
|
|
2042
2279
|
const allowedSubcommands = ["export", "serve"];
|
|
2043
2280
|
const browserRouter = !(parsed.hashRouter || parsed["hash-router"]);
|
|
2044
2281
|
const withTranslationSearch = isWithTranslationSearchEnabled(parsed);
|
|
2282
|
+
const withDuplicates = isWithDuplicatesEnabled(parsed);
|
|
2045
2283
|
|
|
2046
2284
|
if (!parsed.subcommand) {
|
|
2047
2285
|
await api.exportCatalog(rootDirectoryPath, projectConfig, datasource, {
|
|
@@ -2050,6 +2288,7 @@ export function createCatalogPlugin(
|
|
|
2050
2288
|
browserRouter,
|
|
2051
2289
|
dev: true,
|
|
2052
2290
|
withTranslationSearch,
|
|
2291
|
+
withDuplicates,
|
|
2053
2292
|
});
|
|
2054
2293
|
const server = await api.serveCatalog(rootDirectoryPath, projectConfig, datasource, {
|
|
2055
2294
|
outDir: parsed.outDir,
|
|
@@ -2093,6 +2332,7 @@ export function createCatalogPlugin(
|
|
|
2093
2332
|
browserRouter,
|
|
2094
2333
|
dev: true,
|
|
2095
2334
|
withTranslationSearch,
|
|
2335
|
+
withDuplicates,
|
|
2096
2336
|
});
|
|
2097
2337
|
server.triggerReload();
|
|
2098
2338
|
} catch (error) {
|
|
@@ -2141,6 +2381,7 @@ export function createCatalogPlugin(
|
|
|
2141
2381
|
copyAssets: !parsed.noAssets,
|
|
2142
2382
|
browserRouter,
|
|
2143
2383
|
withTranslationSearch,
|
|
2384
|
+
withDuplicates,
|
|
2144
2385
|
});
|
|
2145
2386
|
}
|
|
2146
2387
|
|
|
@@ -1149,7 +1149,8 @@ export function EntityDetailPage() {
|
|
|
1149
1149
|
}
|
|
1150
1150
|
|
|
1151
1151
|
const entity = detail.entity as Record<string, any>;
|
|
1152
|
-
const
|
|
1152
|
+
const { manifest } = useCatalog();
|
|
1153
|
+
const tabs = getTabs(type, baseRoute, manifest.features?.duplicates === true);
|
|
1153
1154
|
|
|
1154
1155
|
return (
|
|
1155
1156
|
<div>
|
|
@@ -1178,7 +1179,7 @@ export function EntityDetailPage() {
|
|
|
1178
1179
|
);
|
|
1179
1180
|
}
|
|
1180
1181
|
|
|
1181
|
-
function getTabs(type: string, baseRoute: string) {
|
|
1182
|
+
function getTabs(type: string, baseRoute: string, duplicatesEnabled = false) {
|
|
1182
1183
|
const shared = [
|
|
1183
1184
|
{ label: "Overview", to: baseRoute, end: true },
|
|
1184
1185
|
{ label: "History", to: `${baseRoute}/history` },
|
|
@@ -1189,7 +1190,7 @@ function getTabs(type: string, baseRoute: string) {
|
|
|
1189
1190
|
shared[0],
|
|
1190
1191
|
{ label: "Formats", to: `${baseRoute}/formats` },
|
|
1191
1192
|
{ label: "Examples", to: `${baseRoute}/examples` },
|
|
1192
|
-
{ label: "Duplicates", to: `${baseRoute}/duplicates` },
|
|
1193
|
+
...(duplicatesEnabled ? [{ label: "Duplicates", to: `${baseRoute}/duplicates` }] : []),
|
|
1193
1194
|
shared[1],
|
|
1194
1195
|
];
|
|
1195
1196
|
}
|
|
@@ -2895,6 +2896,7 @@ function SortArrow(props: { active: boolean; direction: SortDirection }) {
|
|
|
2895
2896
|
|
|
2896
2897
|
export function LocaleDuplicatesTab() {
|
|
2897
2898
|
const { detail, setKey } = useEntityDetail();
|
|
2899
|
+
const { manifest } = useCatalog();
|
|
2898
2900
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
2899
2901
|
const [duplicates, setDuplicates] = React.useState<LocaleDuplicates | null>(null);
|
|
2900
2902
|
const [error, setError] = React.useState<string | null>(null);
|
|
@@ -2905,6 +2907,7 @@ export function LocaleDuplicatesTab() {
|
|
|
2905
2907
|
});
|
|
2906
2908
|
const localeDirection = (detail.entity as Record<string, any>).direction as string | undefined;
|
|
2907
2909
|
const searchQuery = searchParams.get("q") ?? "";
|
|
2910
|
+
const duplicatesEnabled = manifest.features?.duplicates === true;
|
|
2908
2911
|
|
|
2909
2912
|
function toggleDuplicateValue(duplicate: DuplicateTranslationValue) {
|
|
2910
2913
|
const duplicateHash = hashTranslationValue(duplicate.value);
|
|
@@ -2922,6 +2925,10 @@ export function LocaleDuplicatesTab() {
|
|
|
2922
2925
|
}
|
|
2923
2926
|
|
|
2924
2927
|
React.useEffect(() => {
|
|
2928
|
+
if (!duplicatesEnabled) {
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2925
2932
|
let cancelled = false;
|
|
2926
2933
|
|
|
2927
2934
|
setDuplicates(null);
|
|
@@ -2943,7 +2950,7 @@ export function LocaleDuplicatesTab() {
|
|
|
2943
2950
|
return () => {
|
|
2944
2951
|
cancelled = true;
|
|
2945
2952
|
};
|
|
2946
|
-
}, [detail.key, setKey]);
|
|
2953
|
+
}, [detail.key, duplicatesEnabled, setKey]);
|
|
2947
2954
|
|
|
2948
2955
|
React.useEffect(() => {
|
|
2949
2956
|
if (!duplicates || typeof window === "undefined") {
|
|
@@ -2973,6 +2980,10 @@ export function LocaleDuplicatesTab() {
|
|
|
2973
2980
|
|
|
2974
2981
|
useScrollToHash([duplicates?.duplicateValues.length, expandedDuplicateHashes.length]);
|
|
2975
2982
|
|
|
2983
|
+
if (!duplicatesEnabled) {
|
|
2984
|
+
return <Navigate to=".." replace />;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2976
2987
|
if (error) {
|
|
2977
2988
|
return <EmptyState title="Unable to load duplicate translations" description={error} />;
|
|
2978
2989
|
}
|