@messagevisor/catalog 0.3.0 → 0.4.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-BJS9aW0t.js +73 -0
- package/dist/index.html +1 -1
- package/lib/node/index.d.ts +7 -0
- package/lib/node/index.js +29 -14
- package/lib/node/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/lists/EntityList.spec.ts +34 -0
- package/src/components/lists/EntityList.tsx +69 -13
- package/src/components/ui/EntityKey.spec.ts +33 -0
- package/src/components/ui/EntityKey.tsx +91 -12
- package/src/components/ui/SearchHighlight.spec.ts +24 -0
- package/src/components/ui/SearchHighlight.tsx +58 -0
- package/src/node/index.spec.ts +112 -0
- package/src/node/index.ts +34 -15
- package/src/pages/EntityDetailPage.spec.ts +48 -0
- package/src/pages/EntityDetailPage.tsx +87 -89
- package/src/pages/ListPage.tsx +3 -0
- package/src/types.ts +3 -0
- package/src/utils/duplicateSorting.ts +46 -0
- package/dist/assets/index-OXIn4K-M.js +0 -73
package/src/node/index.ts
CHANGED
|
@@ -222,6 +222,7 @@ export interface CatalogExportOptions {
|
|
|
222
222
|
browserRouter?: boolean;
|
|
223
223
|
dev?: boolean;
|
|
224
224
|
devEditors?: CatalogDevEditor[];
|
|
225
|
+
withTranslationSearch?: boolean;
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
export interface CatalogServeOptions {
|
|
@@ -245,6 +246,7 @@ interface CatalogBuildContext {
|
|
|
245
246
|
runtime: CatalogRuntime;
|
|
246
247
|
devEditors: CatalogDevEditor[];
|
|
247
248
|
duplicateResultsBySet: Record<string, CatalogDuplicateTranslationsSetResult>;
|
|
249
|
+
withTranslationSearch: boolean;
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
interface SourceFileInfo {
|
|
@@ -1463,16 +1465,18 @@ async function buildSetCatalog(
|
|
|
1463
1465
|
}
|
|
1464
1466
|
const overrideLocalesList = sortStrings(Array.from(overrideLocalesSet));
|
|
1465
1467
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
+
}
|
|
1476
1480
|
}
|
|
1477
1481
|
}
|
|
1478
1482
|
}
|
|
@@ -1494,12 +1498,14 @@ async function buildSetCatalog(
|
|
|
1494
1498
|
);
|
|
1495
1499
|
}
|
|
1496
1500
|
|
|
1497
|
-
|
|
1498
|
-
const
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
+
if (context.withTranslationSearch) {
|
|
1502
|
+
for (const [prefix, messageMap] of Object.entries(translationShards)) {
|
|
1503
|
+
const shardData: Record<string, string[]> = {};
|
|
1504
|
+
for (const [msgKey, valueSet] of Object.entries(messageMap)) {
|
|
1505
|
+
shardData[msgKey] = Array.from(valueSet);
|
|
1506
|
+
}
|
|
1507
|
+
await writeJson(path.join(outputDirectoryPath, "translations", `${prefix}.json`), shardData);
|
|
1501
1508
|
}
|
|
1502
|
-
await writeJson(path.join(outputDirectoryPath, "translations", `${prefix}.json`), shardData);
|
|
1503
1509
|
}
|
|
1504
1510
|
|
|
1505
1511
|
for (const attributeKey of attributeKeys) {
|
|
@@ -1678,6 +1684,7 @@ export async function exportCatalog(
|
|
|
1678
1684
|
? path.resolve(rootDirectoryPath, options.outDir)
|
|
1679
1685
|
: projectConfig.catalogDirectoryPath;
|
|
1680
1686
|
const dataDirectoryPath = path.join(outputDirectoryPath, "data");
|
|
1687
|
+
const withTranslationSearch = options.withTranslationSearch === true;
|
|
1681
1688
|
|
|
1682
1689
|
await fs.promises.rm(outputDirectoryPath, { recursive: true, force: true });
|
|
1683
1690
|
await fs.promises.mkdir(dataDirectoryPath, { recursive: true });
|
|
@@ -1701,6 +1708,7 @@ export async function exportCatalog(
|
|
|
1701
1708
|
runtime,
|
|
1702
1709
|
devEditors,
|
|
1703
1710
|
duplicateResultsBySet,
|
|
1711
|
+
withTranslationSearch,
|
|
1704
1712
|
};
|
|
1705
1713
|
const executions = await runtime.getProjectSetExecutions(projectConfig, datasource);
|
|
1706
1714
|
const setIndexes: Record<string, CatalogSetIndex> = {};
|
|
@@ -1725,6 +1733,9 @@ export async function exportCatalog(
|
|
|
1725
1733
|
sets: projectConfig.sets,
|
|
1726
1734
|
setKeys: projectConfig.sets ? executions.map((execution) => execution.set) : [],
|
|
1727
1735
|
dev: options.dev ? { editors: devEditors } : undefined,
|
|
1736
|
+
features: {
|
|
1737
|
+
translationSearch: withTranslationSearch,
|
|
1738
|
+
},
|
|
1728
1739
|
links: getRepoLinks(rootDirectoryPath),
|
|
1729
1740
|
paths: {
|
|
1730
1741
|
projectHistory: "data/project/history/page-1.json",
|
|
@@ -2017,6 +2028,10 @@ export function createCatalogApi(runtime: CatalogRuntime) {
|
|
|
2017
2028
|
};
|
|
2018
2029
|
}
|
|
2019
2030
|
|
|
2031
|
+
function isWithTranslationSearchEnabled(parsed: CatalogPluginParsedOptions) {
|
|
2032
|
+
return parsed.withTranslationSearch === true || parsed["with-translation-search"] === true;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2020
2035
|
export function createCatalogPlugin(
|
|
2021
2036
|
runtime: CatalogRuntime,
|
|
2022
2037
|
api: ReturnType<typeof createCatalogApi> = createCatalogApi(runtime),
|
|
@@ -2026,6 +2041,7 @@ export function createCatalogPlugin(
|
|
|
2026
2041
|
handler: async ({ rootDirectoryPath, projectConfig, datasource, parsed }) => {
|
|
2027
2042
|
const allowedSubcommands = ["export", "serve"];
|
|
2028
2043
|
const browserRouter = !(parsed.hashRouter || parsed["hash-router"]);
|
|
2044
|
+
const withTranslationSearch = isWithTranslationSearchEnabled(parsed);
|
|
2029
2045
|
|
|
2030
2046
|
if (!parsed.subcommand) {
|
|
2031
2047
|
await api.exportCatalog(rootDirectoryPath, projectConfig, datasource, {
|
|
@@ -2033,6 +2049,7 @@ export function createCatalogPlugin(
|
|
|
2033
2049
|
copyAssets: !parsed.noAssets,
|
|
2034
2050
|
browserRouter,
|
|
2035
2051
|
dev: true,
|
|
2052
|
+
withTranslationSearch,
|
|
2036
2053
|
});
|
|
2037
2054
|
const server = await api.serveCatalog(rootDirectoryPath, projectConfig, datasource, {
|
|
2038
2055
|
outDir: parsed.outDir,
|
|
@@ -2075,6 +2092,7 @@ export function createCatalogPlugin(
|
|
|
2075
2092
|
copyAssets: !parsed.noAssets,
|
|
2076
2093
|
browserRouter,
|
|
2077
2094
|
dev: true,
|
|
2095
|
+
withTranslationSearch,
|
|
2078
2096
|
});
|
|
2079
2097
|
server.triggerReload();
|
|
2080
2098
|
} catch (error) {
|
|
@@ -2122,6 +2140,7 @@ export function createCatalogPlugin(
|
|
|
2122
2140
|
outDir: parsed.outDir,
|
|
2123
2141
|
copyAssets: !parsed.noAssets,
|
|
2124
2142
|
browserRouter,
|
|
2143
|
+
withTranslationSearch,
|
|
2125
2144
|
});
|
|
2126
2145
|
}
|
|
2127
2146
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DuplicateTranslationValue } from "../types";
|
|
2
|
+
import { sortDuplicateValues } from "../utils/duplicateSorting";
|
|
3
|
+
|
|
4
|
+
function duplicate(value: string, count: number): DuplicateTranslationValue {
|
|
5
|
+
const messageKeys = Array.from({ length: count }, (_, index) => `message.${index + 1}`);
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
value,
|
|
9
|
+
messageKeys,
|
|
10
|
+
sources: messageKeys.map((messageKey) => ({ messageKey, locale: "en" })),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("Locale duplicate sorting", function () {
|
|
15
|
+
it("sorts by message count descending by default", function () {
|
|
16
|
+
const sorted = sortDuplicateValues(
|
|
17
|
+
[duplicate("two", 2), duplicate("four", 4), duplicate("three", 3)],
|
|
18
|
+
{ column: "messages", direction: "desc" },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(sorted.map((item) => item.value)).toEqual(["four", "three", "two"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("sorts duplicate values alphabetically", function () {
|
|
25
|
+
const sorted = sortDuplicateValues(
|
|
26
|
+
[duplicate("Banana", 2), duplicate("Apple", 3), duplicate("Cherry", 2)],
|
|
27
|
+
{ column: "value", direction: "asc" },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(sorted.map((item) => item.value)).toEqual(["Apple", "Banana", "Cherry"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("can reverse both sortable columns", function () {
|
|
34
|
+
expect(
|
|
35
|
+
sortDuplicateValues([duplicate("two", 2), duplicate("four", 4)], {
|
|
36
|
+
column: "messages",
|
|
37
|
+
direction: "asc",
|
|
38
|
+
}).map((item) => item.value),
|
|
39
|
+
).toEqual(["two", "four"]);
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
sortDuplicateValues([duplicate("Apple", 2), duplicate("Banana", 2)], {
|
|
43
|
+
column: "value",
|
|
44
|
+
direction: "desc",
|
|
45
|
+
}).map((item) => item.value),
|
|
46
|
+
).toEqual(["Banana", "Apple"]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -34,6 +34,7 @@ import { EntityKey } from "../components/ui/EntityKey";
|
|
|
34
34
|
import { LabelValueBadge } from "../components/ui/LabelValueBadge";
|
|
35
35
|
import { EmptyState } from "../components/ui/EmptyState";
|
|
36
36
|
import { Input } from "../components/ui/Input";
|
|
37
|
+
import { SearchHighlight } from "../components/ui/SearchHighlight";
|
|
37
38
|
import { FieldGrid } from "../components/details/FieldGrid";
|
|
38
39
|
import { ConditionTree } from "../components/details/ConditionTree";
|
|
39
40
|
import { GroupSegmentTree } from "../components/details/GroupSegmentTree";
|
|
@@ -43,6 +44,12 @@ import { UsageLinks } from "../components/details/UsageLinks";
|
|
|
43
44
|
import { HistoryTimeline } from "../components/history/HistoryTimeline";
|
|
44
45
|
import { useCatalog } from "../context/CatalogContext";
|
|
45
46
|
import { hashTranslationValue } from "../utils/hashTranslationValue";
|
|
47
|
+
import {
|
|
48
|
+
getNextDuplicateValuesSort,
|
|
49
|
+
sortDuplicateValues,
|
|
50
|
+
type DuplicateValuesSort,
|
|
51
|
+
type SortDirection,
|
|
52
|
+
} from "../utils/duplicateSorting";
|
|
46
53
|
import type { ParsedQuery } from "../utils/searchQuery";
|
|
47
54
|
import { parseQuery } from "../utils/searchQuery";
|
|
48
55
|
|
|
@@ -94,51 +101,6 @@ function slugifyFragment(value: string) {
|
|
|
94
101
|
.replace(/^-+|-+$/g, "");
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
function escapeRegExp(value: string) {
|
|
98
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function ExamplesSearchHighlight(props: { text: string; query: string }) {
|
|
102
|
-
const q = props.query.trim();
|
|
103
|
-
if (!q) {
|
|
104
|
-
return <>{props.text}</>;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const escaped = escapeRegExp(q);
|
|
108
|
-
const regex = new RegExp(escaped, "gi");
|
|
109
|
-
const parts: React.ReactNode[] = [];
|
|
110
|
-
let lastIndex = 0;
|
|
111
|
-
let key = 0;
|
|
112
|
-
|
|
113
|
-
for (const match of props.text.matchAll(regex)) {
|
|
114
|
-
if (match.index !== undefined && match.index > lastIndex) {
|
|
115
|
-
parts.push(props.text.slice(lastIndex, match.index));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (match.index !== undefined) {
|
|
119
|
-
parts.push(
|
|
120
|
-
<mark
|
|
121
|
-
key={`hm-${match.index}-${key++}`}
|
|
122
|
-
className={[
|
|
123
|
-
"rounded-[3px] bg-amber-100 px-0.5 py-px text-inherit",
|
|
124
|
-
"shadow-[inset_0_-2px_0_0_rgba(251,191,36,0.35)] ring-1 ring-amber-400/25 ring-inset",
|
|
125
|
-
"transition-[background-color,box-shadow] duration-150",
|
|
126
|
-
].join(" ")}
|
|
127
|
-
>
|
|
128
|
-
{match[0]}
|
|
129
|
-
</mark>,
|
|
130
|
-
);
|
|
131
|
-
lastIndex = match.index + match[0].length;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (lastIndex < props.text.length) {
|
|
136
|
-
parts.push(props.text.slice(lastIndex));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return <>{parts}</>;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
104
|
function isEntityPath(value: string | undefined): value is EntityPath {
|
|
143
105
|
return (
|
|
144
106
|
value === "locales" ||
|
|
@@ -785,7 +747,7 @@ function FormatRowsTable(props: {
|
|
|
785
747
|
function segmentBody(segment: string) {
|
|
786
748
|
return segment ? (
|
|
787
749
|
highlight ? (
|
|
788
|
-
<
|
|
750
|
+
<SearchHighlight text={segment} query={highlightNeedle} />
|
|
789
751
|
) : (
|
|
790
752
|
segment
|
|
791
753
|
)
|
|
@@ -851,11 +813,7 @@ function FormatRowsTable(props: {
|
|
|
851
813
|
row.source === "inherited" ? "text-muted" : "",
|
|
852
814
|
].join(" ")}
|
|
853
815
|
>
|
|
854
|
-
{highlight ?
|
|
855
|
-
<ExamplesSearchHighlight text={valueText} query={highlightNeedle} />
|
|
856
|
-
) : (
|
|
857
|
-
valueText
|
|
858
|
-
)}
|
|
816
|
+
{highlight ? <SearchHighlight text={valueText} query={highlightNeedle} /> : valueText}
|
|
859
817
|
</div>
|
|
860
818
|
{(showInheritedBadge || showTargetBadge) && (
|
|
861
819
|
<div className="flex shrink-0 flex-col items-end justify-center gap-1">
|
|
@@ -881,7 +839,7 @@ function FormatRowsTable(props: {
|
|
|
881
839
|
function renderExampleCellContent(preview: string | undefined) {
|
|
882
840
|
return preview ? (
|
|
883
841
|
highlight ? (
|
|
884
|
-
<
|
|
842
|
+
<SearchHighlight text={preview} query={highlightNeedle} />
|
|
885
843
|
) : (
|
|
886
844
|
preview
|
|
887
845
|
)
|
|
@@ -1078,7 +1036,7 @@ function FormatRowsTable(props: {
|
|
|
1078
1036
|
<td className={flatFormatClass}>
|
|
1079
1037
|
<div className="whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
1080
1038
|
{highlight ? (
|
|
1081
|
-
<
|
|
1039
|
+
<SearchHighlight text={row.path} query={highlightNeedle} />
|
|
1082
1040
|
) : (
|
|
1083
1041
|
row.path
|
|
1084
1042
|
)}
|
|
@@ -1959,7 +1917,7 @@ function ExampleTitle(props: {
|
|
|
1959
1917
|
}) {
|
|
1960
1918
|
const titleContent =
|
|
1961
1919
|
props.highlightQuery?.trim() && props.title ? (
|
|
1962
|
-
<
|
|
1920
|
+
<SearchHighlight text={props.title} query={props.highlightQuery} />
|
|
1963
1921
|
) : (
|
|
1964
1922
|
props.title
|
|
1965
1923
|
);
|
|
@@ -1975,7 +1933,7 @@ function ExampleTitle(props: {
|
|
|
1975
1933
|
{props.description?.trim() ? (
|
|
1976
1934
|
props.highlightQuery?.trim() ? (
|
|
1977
1935
|
<div className="text-sm text-muted whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
1978
|
-
<
|
|
1936
|
+
<SearchHighlight text={props.description.trim()} query={props.highlightQuery} />
|
|
1979
1937
|
</div>
|
|
1980
1938
|
) : (
|
|
1981
1939
|
<div className="text-sm text-muted">
|
|
@@ -2009,7 +1967,7 @@ function ExampleTable(props: {
|
|
|
2009
1967
|
.join(" ")}
|
|
2010
1968
|
style={props.direction ? { unicodeBidi: "plaintext" } : undefined}
|
|
2011
1969
|
>
|
|
2012
|
-
<
|
|
1970
|
+
<SearchHighlight
|
|
2013
1971
|
text={
|
|
2014
1972
|
typeof props.evaluatedTranslation === "string"
|
|
2015
1973
|
? props.evaluatedTranslation
|
|
@@ -2420,7 +2378,7 @@ function LocaleExampleDetails(props: {
|
|
|
2420
2378
|
to={getEntityRoute("locale", localeKey, setKey)}
|
|
2421
2379
|
className="font-medium text-primary hover:underline"
|
|
2422
2380
|
>
|
|
2423
|
-
<
|
|
2381
|
+
<SearchHighlight text={localeKey} query={q} />
|
|
2424
2382
|
</Link>
|
|
2425
2383
|
) : (
|
|
2426
2384
|
<SourceLocaleLink localeKey={localeKey} />
|
|
@@ -2444,11 +2402,7 @@ function LocaleExampleDetails(props: {
|
|
|
2444
2402
|
to={getEntityRoute("message", example.message, setKey)}
|
|
2445
2403
|
className="font-medium text-primary hover:underline"
|
|
2446
2404
|
>
|
|
2447
|
-
{highlight ?
|
|
2448
|
-
<ExamplesSearchHighlight text={example.message} query={q} />
|
|
2449
|
-
) : (
|
|
2450
|
-
example.message
|
|
2451
|
-
)}
|
|
2405
|
+
{highlight ? <SearchHighlight text={example.message} query={q} /> : example.message}
|
|
2452
2406
|
</Link>
|
|
2453
2407
|
</InputField>
|
|
2454
2408
|
|
|
@@ -2466,7 +2420,7 @@ function LocaleExampleDetails(props: {
|
|
|
2466
2420
|
.join(" ")}
|
|
2467
2421
|
style={localeDirection ? { unicodeBidi: "plaintext" } : undefined}
|
|
2468
2422
|
>
|
|
2469
|
-
<
|
|
2423
|
+
<SearchHighlight text={example.originalTranslation} query={q} />
|
|
2470
2424
|
</div>
|
|
2471
2425
|
) : (
|
|
2472
2426
|
<TranslationValueBlock
|
|
@@ -2508,7 +2462,7 @@ function LocaleExampleDetails(props: {
|
|
|
2508
2462
|
>
|
|
2509
2463
|
{highlight ? (
|
|
2510
2464
|
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2511
|
-
<
|
|
2465
|
+
<SearchHighlight text={example.rawMessage} query={q} />
|
|
2512
2466
|
</pre>
|
|
2513
2467
|
) : (
|
|
2514
2468
|
<CodeBlock value={example.rawMessage} />
|
|
@@ -2521,7 +2475,7 @@ function LocaleExampleDetails(props: {
|
|
|
2521
2475
|
<InputField label="Values">
|
|
2522
2476
|
{highlight ? (
|
|
2523
2477
|
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2524
|
-
<
|
|
2478
|
+
<SearchHighlight text={JSON.stringify(example.values, null, 2)} query={q} />
|
|
2525
2479
|
</pre>
|
|
2526
2480
|
) : (
|
|
2527
2481
|
<JsonValueBlock value={example.values} />
|
|
@@ -2533,7 +2487,7 @@ function LocaleExampleDetails(props: {
|
|
|
2533
2487
|
<InputField label="Context">
|
|
2534
2488
|
{highlight ? (
|
|
2535
2489
|
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2536
|
-
<
|
|
2490
|
+
<SearchHighlight text={JSON.stringify(example.context, null, 2)} query={q} />
|
|
2537
2491
|
</pre>
|
|
2538
2492
|
) : (
|
|
2539
2493
|
<JsonValueBlock value={example.context} />
|
|
@@ -2544,11 +2498,7 @@ function LocaleExampleDetails(props: {
|
|
|
2544
2498
|
{typeof example.timeZone !== "undefined" && (
|
|
2545
2499
|
<InputField label="Time zone">
|
|
2546
2500
|
<span className="font-mono text-sm text-text">
|
|
2547
|
-
{highlight ?
|
|
2548
|
-
<ExamplesSearchHighlight text={example.timeZone} query={q} />
|
|
2549
|
-
) : (
|
|
2550
|
-
example.timeZone
|
|
2551
|
-
)}
|
|
2501
|
+
{highlight ? <SearchHighlight text={example.timeZone} query={q} /> : example.timeZone}
|
|
2552
2502
|
</span>
|
|
2553
2503
|
</InputField>
|
|
2554
2504
|
)}
|
|
@@ -2556,11 +2506,7 @@ function LocaleExampleDetails(props: {
|
|
|
2556
2506
|
{typeof example.currency !== "undefined" && (
|
|
2557
2507
|
<InputField label="Currency">
|
|
2558
2508
|
<span className="font-mono text-sm text-text">
|
|
2559
|
-
{highlight ?
|
|
2560
|
-
<ExamplesSearchHighlight text={example.currency} query={q} />
|
|
2561
|
-
) : (
|
|
2562
|
-
example.currency
|
|
2563
|
-
)}
|
|
2509
|
+
{highlight ? <SearchHighlight text={example.currency} query={q} /> : example.currency}
|
|
2564
2510
|
</span>
|
|
2565
2511
|
</InputField>
|
|
2566
2512
|
)}
|
|
@@ -2569,7 +2515,7 @@ function LocaleExampleDetails(props: {
|
|
|
2569
2515
|
<InputField label="Formats">
|
|
2570
2516
|
{highlight ? (
|
|
2571
2517
|
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2572
|
-
<
|
|
2518
|
+
<SearchHighlight text={JSON.stringify(example.formats, null, 2)} query={q} />
|
|
2573
2519
|
</pre>
|
|
2574
2520
|
) : (
|
|
2575
2521
|
<JsonValueBlock value={example.formats} />
|
|
@@ -2712,14 +2658,14 @@ function LocaleExamplesCompactView(props: {
|
|
|
2712
2658
|
onClick={() => toggleExample(exampleId)}
|
|
2713
2659
|
>
|
|
2714
2660
|
<td className="border-b border-border px-3 py-2 font-medium text-muted">
|
|
2715
|
-
<
|
|
2661
|
+
<SearchHighlight
|
|
2716
2662
|
text={getLocaleExampleCompactLabel(example)}
|
|
2717
2663
|
query={props.searchQuery}
|
|
2718
2664
|
/>
|
|
2719
2665
|
</td>
|
|
2720
2666
|
<td className="min-w-0 border-b border-border px-3 py-2 text-muted">
|
|
2721
2667
|
<div className="whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
2722
|
-
<
|
|
2668
|
+
<SearchHighlight
|
|
2723
2669
|
text={example.description || "—"}
|
|
2724
2670
|
query={props.searchQuery}
|
|
2725
2671
|
/>
|
|
@@ -2734,7 +2680,7 @@ function LocaleExamplesCompactView(props: {
|
|
|
2734
2680
|
style={props.localeDirection ? { unicodeBidi: "plaintext" } : undefined}
|
|
2735
2681
|
>
|
|
2736
2682
|
<div className="whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
2737
|
-
<
|
|
2683
|
+
<SearchHighlight
|
|
2738
2684
|
text={
|
|
2739
2685
|
typeof example.evaluatedTranslation === "string"
|
|
2740
2686
|
? example.evaluatedTranslation
|
|
@@ -2752,7 +2698,7 @@ function LocaleExamplesCompactView(props: {
|
|
|
2752
2698
|
<div className="group flex items-center gap-2">
|
|
2753
2699
|
<h3 className="text-sm font-semibold">
|
|
2754
2700
|
{props.searchQuery.trim() ? (
|
|
2755
|
-
<
|
|
2701
|
+
<SearchHighlight
|
|
2756
2702
|
text={getLocaleExampleTitle(example)}
|
|
2757
2703
|
query={props.searchQuery}
|
|
2758
2704
|
/>
|
|
@@ -2939,12 +2885,24 @@ function filterDuplicateValuesBySearch(
|
|
|
2939
2885
|
});
|
|
2940
2886
|
}
|
|
2941
2887
|
|
|
2888
|
+
function SortArrow(props: { active: boolean; direction: SortDirection }) {
|
|
2889
|
+
return (
|
|
2890
|
+
<span className={props.active ? "text-primary" : "text-muted/60"} aria-hidden="true">
|
|
2891
|
+
{props.active ? (props.direction === "asc" ? "↑" : "↓") : "↕"}
|
|
2892
|
+
</span>
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2942
2896
|
export function LocaleDuplicatesTab() {
|
|
2943
2897
|
const { detail, setKey } = useEntityDetail();
|
|
2944
2898
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
2945
2899
|
const [duplicates, setDuplicates] = React.useState<LocaleDuplicates | null>(null);
|
|
2946
2900
|
const [error, setError] = React.useState<string | null>(null);
|
|
2947
2901
|
const [expandedDuplicateHashes, setExpandedDuplicateHashes] = React.useState<string[]>([]);
|
|
2902
|
+
const [sort, setSort] = React.useState<DuplicateValuesSort>({
|
|
2903
|
+
column: "messages",
|
|
2904
|
+
direction: "desc",
|
|
2905
|
+
});
|
|
2948
2906
|
const localeDirection = (detail.entity as Record<string, any>).direction as string | undefined;
|
|
2949
2907
|
const searchQuery = searchParams.get("q") ?? "";
|
|
2950
2908
|
|
|
@@ -3027,9 +2985,9 @@ export function LocaleDuplicatesTab() {
|
|
|
3027
2985
|
return <p className="text-sm text-muted">No duplicate translations found for this locale.</p>;
|
|
3028
2986
|
}
|
|
3029
2987
|
|
|
3030
|
-
const visibleDuplicateValues =
|
|
3031
|
-
duplicates.duplicateValues,
|
|
3032
|
-
|
|
2988
|
+
const visibleDuplicateValues = sortDuplicateValues(
|
|
2989
|
+
filterDuplicateValuesBySearch(duplicates.duplicateValues, searchQuery),
|
|
2990
|
+
sort,
|
|
3033
2991
|
);
|
|
3034
2992
|
|
|
3035
2993
|
return (
|
|
@@ -3075,8 +3033,48 @@ export function LocaleDuplicatesTab() {
|
|
|
3075
3033
|
</colgroup>
|
|
3076
3034
|
<thead className="bg-elevated text-left text-[11px] uppercase tracking-wide text-muted">
|
|
3077
3035
|
<tr>
|
|
3078
|
-
<th
|
|
3079
|
-
|
|
3036
|
+
<th
|
|
3037
|
+
className="border-b border-border px-3 py-2 font-semibold"
|
|
3038
|
+
aria-sort={
|
|
3039
|
+
sort.column === "value"
|
|
3040
|
+
? sort.direction === "asc"
|
|
3041
|
+
? "ascending"
|
|
3042
|
+
: "descending"
|
|
3043
|
+
: "none"
|
|
3044
|
+
}
|
|
3045
|
+
>
|
|
3046
|
+
<button
|
|
3047
|
+
type="button"
|
|
3048
|
+
className="inline-flex cursor-pointer items-center gap-1 text-left font-semibold uppercase tracking-wide text-muted transition-colors hover:text-text"
|
|
3049
|
+
onClick={() =>
|
|
3050
|
+
setSort((current) => getNextDuplicateValuesSort(current, "value"))
|
|
3051
|
+
}
|
|
3052
|
+
>
|
|
3053
|
+
<span>Duplicate value</span>
|
|
3054
|
+
<SortArrow active={sort.column === "value"} direction={sort.direction} />
|
|
3055
|
+
</button>
|
|
3056
|
+
</th>
|
|
3057
|
+
<th
|
|
3058
|
+
className="border-b border-border px-3 py-2 font-semibold"
|
|
3059
|
+
aria-sort={
|
|
3060
|
+
sort.column === "messages"
|
|
3061
|
+
? sort.direction === "asc"
|
|
3062
|
+
? "ascending"
|
|
3063
|
+
: "descending"
|
|
3064
|
+
: "none"
|
|
3065
|
+
}
|
|
3066
|
+
>
|
|
3067
|
+
<button
|
|
3068
|
+
type="button"
|
|
3069
|
+
className="inline-flex cursor-pointer items-center gap-1 text-left font-semibold uppercase tracking-wide text-muted transition-colors hover:text-text"
|
|
3070
|
+
onClick={() =>
|
|
3071
|
+
setSort((current) => getNextDuplicateValuesSort(current, "messages"))
|
|
3072
|
+
}
|
|
3073
|
+
>
|
|
3074
|
+
<span>Messages</span>
|
|
3075
|
+
<SortArrow active={sort.column === "messages"} direction={sort.direction} />
|
|
3076
|
+
</button>
|
|
3077
|
+
</th>
|
|
3080
3078
|
</tr>
|
|
3081
3079
|
</thead>
|
|
3082
3080
|
<tbody>
|
|
@@ -3106,7 +3104,7 @@ export function LocaleDuplicatesTab() {
|
|
|
3106
3104
|
style={localeDirection ? { unicodeBidi: "plaintext" } : undefined}
|
|
3107
3105
|
>
|
|
3108
3106
|
<div className="truncate">
|
|
3109
|
-
<
|
|
3107
|
+
<SearchHighlight text={duplicate.value} query={searchQuery} />
|
|
3110
3108
|
</div>
|
|
3111
3109
|
</td>
|
|
3112
3110
|
<td className="min-w-0 border-b border-border px-3 py-2 text-muted">
|
|
@@ -3152,7 +3150,7 @@ export function LocaleDuplicatesTab() {
|
|
|
3152
3150
|
to={getEntityRoute("message", messageKey, setKey)}
|
|
3153
3151
|
className="font-medium text-primary [overflow-wrap:anywhere] hover:underline"
|
|
3154
3152
|
>
|
|
3155
|
-
<
|
|
3153
|
+
<SearchHighlight
|
|
3156
3154
|
text={messageKey}
|
|
3157
3155
|
query={searchQuery}
|
|
3158
3156
|
/>
|
|
@@ -3167,7 +3165,7 @@ export function LocaleDuplicatesTab() {
|
|
|
3167
3165
|
className="font-medium text-primary hover:underline"
|
|
3168
3166
|
onClick={(event) => event.stopPropagation()}
|
|
3169
3167
|
>
|
|
3170
|
-
<
|
|
3168
|
+
<SearchHighlight
|
|
3171
3169
|
text={sourceLocale}
|
|
3172
3170
|
query={searchQuery}
|
|
3173
3171
|
/>
|
package/src/pages/ListPage.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import type { CatalogIndex, EntityPath } from "../types";
|
|
|
7
7
|
import { EntityList } from "../components/lists/EntityList";
|
|
8
8
|
import { PageHeader } from "../components/layout/PageHeader";
|
|
9
9
|
import { EmptyState } from "../components/ui/EmptyState";
|
|
10
|
+
import { useCatalog } from "../context/CatalogContext";
|
|
10
11
|
|
|
11
12
|
function isEntityPath(value: string | undefined): value is EntityPath {
|
|
12
13
|
return (
|
|
@@ -20,6 +21,7 @@ function isEntityPath(value: string | undefined): value is EntityPath {
|
|
|
20
21
|
|
|
21
22
|
export function ListPage() {
|
|
22
23
|
const { entityPath, setKey } = useParams();
|
|
24
|
+
const { manifest } = useCatalog();
|
|
23
25
|
const [index, setIndex] = React.useState<CatalogIndex | null>(null);
|
|
24
26
|
const [error, setError] = React.useState<string | null>(null);
|
|
25
27
|
|
|
@@ -53,6 +55,7 @@ export function ListPage() {
|
|
|
53
55
|
entities={index.entities[type]}
|
|
54
56
|
setKey={setKey}
|
|
55
57
|
allEntities={index.entities}
|
|
58
|
+
translationSearchEnabled={manifest.features?.translationSearch === true}
|
|
56
59
|
/>
|
|
57
60
|
</div>
|
|
58
61
|
);
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DuplicateTranslationValue } from "../types";
|
|
2
|
+
|
|
3
|
+
export type DuplicateValuesSortColumn = "value" | "messages";
|
|
4
|
+
export type SortDirection = "asc" | "desc";
|
|
5
|
+
|
|
6
|
+
export interface DuplicateValuesSort {
|
|
7
|
+
column: DuplicateValuesSortColumn;
|
|
8
|
+
direction: SortDirection;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sortDuplicateValues(
|
|
12
|
+
duplicateValues: DuplicateTranslationValue[],
|
|
13
|
+
sort: DuplicateValuesSort,
|
|
14
|
+
): DuplicateTranslationValue[] {
|
|
15
|
+
return duplicateValues.slice().sort((left, right) => {
|
|
16
|
+
const direction = sort.direction === "asc" ? 1 : -1;
|
|
17
|
+
const fallback = left.value.localeCompare(right.value);
|
|
18
|
+
|
|
19
|
+
if (sort.column === "messages") {
|
|
20
|
+
const result = left.messageKeys.length - right.messageKeys.length;
|
|
21
|
+
return (result || fallback) * direction;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
(left.value.localeCompare(right.value) ||
|
|
26
|
+
left.messageKeys.length - right.messageKeys.length) * direction
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getNextDuplicateValuesSort(
|
|
32
|
+
current: DuplicateValuesSort,
|
|
33
|
+
column: DuplicateValuesSortColumn,
|
|
34
|
+
): DuplicateValuesSort {
|
|
35
|
+
if (current.column !== column) {
|
|
36
|
+
return {
|
|
37
|
+
column,
|
|
38
|
+
direction: column === "messages" ? "desc" : "asc",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
column,
|
|
44
|
+
direction: current.direction === "asc" ? "desc" : "asc",
|
|
45
|
+
};
|
|
46
|
+
}
|