@messagevisor/catalog 0.2.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/assets/index-DrsX4U8c.css +1 -0
- package/dist/index.html +2 -2
- 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/details/FieldGrid.tsx +1 -1
- package/src/components/details/GroupSegmentTree.tsx +2 -1
- package/src/components/details/UsageLinks.tsx +3 -2
- package/src/components/history/HistoryTimeline.tsx +4 -3
- package/src/components/layout/PageHeader.tsx +13 -3
- package/src/components/lists/EntityList.spec.ts +34 -0
- package/src/components/lists/EntityList.tsx +70 -13
- package/src/components/ui/EntityKey.spec.ts +33 -0
- package/src/components/ui/EntityKey.tsx +99 -0
- 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 +101 -94
- package/src/pages/ListPage.tsx +3 -0
- package/src/types.ts +3 -0
- package/src/utils/duplicateSorting.ts +46 -0
- package/dist/assets/index-BoO0zn_O.js +0 -73
- package/dist/assets/index-Cn0qFwkD.css +0 -1
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
|
+
});
|
|
@@ -30,9 +30,11 @@ import { PageHeader } from "../components/layout/PageHeader";
|
|
|
30
30
|
import { Tabs } from "../components/layout/Tabs";
|
|
31
31
|
import { Badge } from "../components/ui/Badge";
|
|
32
32
|
import { CodeBlock } from "../components/ui/CodeBlock";
|
|
33
|
+
import { EntityKey } from "../components/ui/EntityKey";
|
|
33
34
|
import { LabelValueBadge } from "../components/ui/LabelValueBadge";
|
|
34
35
|
import { EmptyState } from "../components/ui/EmptyState";
|
|
35
36
|
import { Input } from "../components/ui/Input";
|
|
37
|
+
import { SearchHighlight } from "../components/ui/SearchHighlight";
|
|
36
38
|
import { FieldGrid } from "../components/details/FieldGrid";
|
|
37
39
|
import { ConditionTree } from "../components/details/ConditionTree";
|
|
38
40
|
import { GroupSegmentTree } from "../components/details/GroupSegmentTree";
|
|
@@ -42,6 +44,12 @@ import { UsageLinks } from "../components/details/UsageLinks";
|
|
|
42
44
|
import { HistoryTimeline } from "../components/history/HistoryTimeline";
|
|
43
45
|
import { useCatalog } from "../context/CatalogContext";
|
|
44
46
|
import { hashTranslationValue } from "../utils/hashTranslationValue";
|
|
47
|
+
import {
|
|
48
|
+
getNextDuplicateValuesSort,
|
|
49
|
+
sortDuplicateValues,
|
|
50
|
+
type DuplicateValuesSort,
|
|
51
|
+
type SortDirection,
|
|
52
|
+
} from "../utils/duplicateSorting";
|
|
45
53
|
import type { ParsedQuery } from "../utils/searchQuery";
|
|
46
54
|
import { parseQuery } from "../utils/searchQuery";
|
|
47
55
|
|
|
@@ -93,51 +101,6 @@ function slugifyFragment(value: string) {
|
|
|
93
101
|
.replace(/^-+|-+$/g, "");
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
function escapeRegExp(value: string) {
|
|
97
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function ExamplesSearchHighlight(props: { text: string; query: string }) {
|
|
101
|
-
const q = props.query.trim();
|
|
102
|
-
if (!q) {
|
|
103
|
-
return <>{props.text}</>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const escaped = escapeRegExp(q);
|
|
107
|
-
const regex = new RegExp(escaped, "gi");
|
|
108
|
-
const parts: React.ReactNode[] = [];
|
|
109
|
-
let lastIndex = 0;
|
|
110
|
-
let key = 0;
|
|
111
|
-
|
|
112
|
-
for (const match of props.text.matchAll(regex)) {
|
|
113
|
-
if (match.index !== undefined && match.index > lastIndex) {
|
|
114
|
-
parts.push(props.text.slice(lastIndex, match.index));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (match.index !== undefined) {
|
|
118
|
-
parts.push(
|
|
119
|
-
<mark
|
|
120
|
-
key={`hm-${match.index}-${key++}`}
|
|
121
|
-
className={[
|
|
122
|
-
"rounded-[3px] bg-amber-100 px-0.5 py-px text-inherit",
|
|
123
|
-
"shadow-[inset_0_-2px_0_0_rgba(251,191,36,0.35)] ring-1 ring-amber-400/25 ring-inset",
|
|
124
|
-
"transition-[background-color,box-shadow] duration-150",
|
|
125
|
-
].join(" ")}
|
|
126
|
-
>
|
|
127
|
-
{match[0]}
|
|
128
|
-
</mark>,
|
|
129
|
-
);
|
|
130
|
-
lastIndex = match.index + match[0].length;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (lastIndex < props.text.length) {
|
|
135
|
-
parts.push(props.text.slice(lastIndex));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return <>{parts}</>;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
104
|
function isEntityPath(value: string | undefined): value is EntityPath {
|
|
142
105
|
return (
|
|
143
106
|
value === "locales" ||
|
|
@@ -784,7 +747,7 @@ function FormatRowsTable(props: {
|
|
|
784
747
|
function segmentBody(segment: string) {
|
|
785
748
|
return segment ? (
|
|
786
749
|
highlight ? (
|
|
787
|
-
<
|
|
750
|
+
<SearchHighlight text={segment} query={highlightNeedle} />
|
|
788
751
|
) : (
|
|
789
752
|
segment
|
|
790
753
|
)
|
|
@@ -850,11 +813,7 @@ function FormatRowsTable(props: {
|
|
|
850
813
|
row.source === "inherited" ? "text-muted" : "",
|
|
851
814
|
].join(" ")}
|
|
852
815
|
>
|
|
853
|
-
{highlight ?
|
|
854
|
-
<ExamplesSearchHighlight text={valueText} query={highlightNeedle} />
|
|
855
|
-
) : (
|
|
856
|
-
valueText
|
|
857
|
-
)}
|
|
816
|
+
{highlight ? <SearchHighlight text={valueText} query={highlightNeedle} /> : valueText}
|
|
858
817
|
</div>
|
|
859
818
|
{(showInheritedBadge || showTargetBadge) && (
|
|
860
819
|
<div className="flex shrink-0 flex-col items-end justify-center gap-1">
|
|
@@ -880,7 +839,7 @@ function FormatRowsTable(props: {
|
|
|
880
839
|
function renderExampleCellContent(preview: string | undefined) {
|
|
881
840
|
return preview ? (
|
|
882
841
|
highlight ? (
|
|
883
|
-
<
|
|
842
|
+
<SearchHighlight text={preview} query={highlightNeedle} />
|
|
884
843
|
) : (
|
|
885
844
|
preview
|
|
886
845
|
)
|
|
@@ -1077,7 +1036,7 @@ function FormatRowsTable(props: {
|
|
|
1077
1036
|
<td className={flatFormatClass}>
|
|
1078
1037
|
<div className="whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
1079
1038
|
{highlight ? (
|
|
1080
|
-
<
|
|
1039
|
+
<SearchHighlight text={row.path} query={highlightNeedle} />
|
|
1081
1040
|
) : (
|
|
1082
1041
|
row.path
|
|
1083
1042
|
)}
|
|
@@ -1195,7 +1154,15 @@ export function EntityDetailPage() {
|
|
|
1195
1154
|
return (
|
|
1196
1155
|
<div>
|
|
1197
1156
|
<PageHeader
|
|
1198
|
-
title={
|
|
1157
|
+
title={
|
|
1158
|
+
<span className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
1159
|
+
<span className="whitespace-nowrap">{entityLabels[type].singular}:</span>
|
|
1160
|
+
<EntityKey
|
|
1161
|
+
value={detail.key}
|
|
1162
|
+
className="min-w-0 text-[1.45rem] font-extrabold leading-tight"
|
|
1163
|
+
/>
|
|
1164
|
+
</span>
|
|
1165
|
+
}
|
|
1199
1166
|
description={
|
|
1200
1167
|
<div className="flex flex-wrap items-center gap-2">
|
|
1201
1168
|
{entity.archived && <Badge tone="danger">archived</Badge>}
|
|
@@ -1638,7 +1605,7 @@ function MessageTranslationOverridesDetails(props: {
|
|
|
1638
1605
|
to={`${getEntityRoute("message", props.messageKey, props.setKey)}/overrides#${key}`}
|
|
1639
1606
|
className="text-sm font-semibold text-primary hover:underline"
|
|
1640
1607
|
>
|
|
1641
|
-
{key}
|
|
1608
|
+
<EntityKey value={key} className="font-semibold" />
|
|
1642
1609
|
</Link>
|
|
1643
1610
|
{row.source === "inherited" && row.from && (
|
|
1644
1611
|
<LabelValueBadge
|
|
@@ -1800,8 +1767,8 @@ export function MessageOverridesTab() {
|
|
|
1800
1767
|
<section key={override.key} className="space-y-4">
|
|
1801
1768
|
<div className="space-y-3">
|
|
1802
1769
|
<div className="group flex items-center gap-2">
|
|
1803
|
-
<h2 id={override.key} className="font-semibold">
|
|
1804
|
-
{override.key}
|
|
1770
|
+
<h2 id={override.key} className="font-semibold [overflow-wrap:anywhere]">
|
|
1771
|
+
<EntityKey value={override.key} className="font-semibold" />
|
|
1805
1772
|
</h2>
|
|
1806
1773
|
<ExamplePermalink targetId={override.key} />
|
|
1807
1774
|
</div>
|
|
@@ -1950,7 +1917,7 @@ function ExampleTitle(props: {
|
|
|
1950
1917
|
}) {
|
|
1951
1918
|
const titleContent =
|
|
1952
1919
|
props.highlightQuery?.trim() && props.title ? (
|
|
1953
|
-
<
|
|
1920
|
+
<SearchHighlight text={props.title} query={props.highlightQuery} />
|
|
1954
1921
|
) : (
|
|
1955
1922
|
props.title
|
|
1956
1923
|
);
|
|
@@ -1966,7 +1933,7 @@ function ExampleTitle(props: {
|
|
|
1966
1933
|
{props.description?.trim() ? (
|
|
1967
1934
|
props.highlightQuery?.trim() ? (
|
|
1968
1935
|
<div className="text-sm text-muted whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
1969
|
-
<
|
|
1936
|
+
<SearchHighlight text={props.description.trim()} query={props.highlightQuery} />
|
|
1970
1937
|
</div>
|
|
1971
1938
|
) : (
|
|
1972
1939
|
<div className="text-sm text-muted">
|
|
@@ -2000,7 +1967,7 @@ function ExampleTable(props: {
|
|
|
2000
1967
|
.join(" ")}
|
|
2001
1968
|
style={props.direction ? { unicodeBidi: "plaintext" } : undefined}
|
|
2002
1969
|
>
|
|
2003
|
-
<
|
|
1970
|
+
<SearchHighlight
|
|
2004
1971
|
text={
|
|
2005
1972
|
typeof props.evaluatedTranslation === "string"
|
|
2006
1973
|
? props.evaluatedTranslation
|
|
@@ -2411,7 +2378,7 @@ function LocaleExampleDetails(props: {
|
|
|
2411
2378
|
to={getEntityRoute("locale", localeKey, setKey)}
|
|
2412
2379
|
className="font-medium text-primary hover:underline"
|
|
2413
2380
|
>
|
|
2414
|
-
<
|
|
2381
|
+
<SearchHighlight text={localeKey} query={q} />
|
|
2415
2382
|
</Link>
|
|
2416
2383
|
) : (
|
|
2417
2384
|
<SourceLocaleLink localeKey={localeKey} />
|
|
@@ -2435,11 +2402,7 @@ function LocaleExampleDetails(props: {
|
|
|
2435
2402
|
to={getEntityRoute("message", example.message, setKey)}
|
|
2436
2403
|
className="font-medium text-primary hover:underline"
|
|
2437
2404
|
>
|
|
2438
|
-
{highlight ?
|
|
2439
|
-
<ExamplesSearchHighlight text={example.message} query={q} />
|
|
2440
|
-
) : (
|
|
2441
|
-
example.message
|
|
2442
|
-
)}
|
|
2405
|
+
{highlight ? <SearchHighlight text={example.message} query={q} /> : example.message}
|
|
2443
2406
|
</Link>
|
|
2444
2407
|
</InputField>
|
|
2445
2408
|
|
|
@@ -2457,7 +2420,7 @@ function LocaleExampleDetails(props: {
|
|
|
2457
2420
|
.join(" ")}
|
|
2458
2421
|
style={localeDirection ? { unicodeBidi: "plaintext" } : undefined}
|
|
2459
2422
|
>
|
|
2460
|
-
<
|
|
2423
|
+
<SearchHighlight text={example.originalTranslation} query={q} />
|
|
2461
2424
|
</div>
|
|
2462
2425
|
) : (
|
|
2463
2426
|
<TranslationValueBlock
|
|
@@ -2499,7 +2462,7 @@ function LocaleExampleDetails(props: {
|
|
|
2499
2462
|
>
|
|
2500
2463
|
{highlight ? (
|
|
2501
2464
|
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2502
|
-
<
|
|
2465
|
+
<SearchHighlight text={example.rawMessage} query={q} />
|
|
2503
2466
|
</pre>
|
|
2504
2467
|
) : (
|
|
2505
2468
|
<CodeBlock value={example.rawMessage} />
|
|
@@ -2512,7 +2475,7 @@ function LocaleExampleDetails(props: {
|
|
|
2512
2475
|
<InputField label="Values">
|
|
2513
2476
|
{highlight ? (
|
|
2514
2477
|
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2515
|
-
<
|
|
2478
|
+
<SearchHighlight text={JSON.stringify(example.values, null, 2)} query={q} />
|
|
2516
2479
|
</pre>
|
|
2517
2480
|
) : (
|
|
2518
2481
|
<JsonValueBlock value={example.values} />
|
|
@@ -2524,7 +2487,7 @@ function LocaleExampleDetails(props: {
|
|
|
2524
2487
|
<InputField label="Context">
|
|
2525
2488
|
{highlight ? (
|
|
2526
2489
|
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2527
|
-
<
|
|
2490
|
+
<SearchHighlight text={JSON.stringify(example.context, null, 2)} query={q} />
|
|
2528
2491
|
</pre>
|
|
2529
2492
|
) : (
|
|
2530
2493
|
<JsonValueBlock value={example.context} />
|
|
@@ -2535,11 +2498,7 @@ function LocaleExampleDetails(props: {
|
|
|
2535
2498
|
{typeof example.timeZone !== "undefined" && (
|
|
2536
2499
|
<InputField label="Time zone">
|
|
2537
2500
|
<span className="font-mono text-sm text-text">
|
|
2538
|
-
{highlight ?
|
|
2539
|
-
<ExamplesSearchHighlight text={example.timeZone} query={q} />
|
|
2540
|
-
) : (
|
|
2541
|
-
example.timeZone
|
|
2542
|
-
)}
|
|
2501
|
+
{highlight ? <SearchHighlight text={example.timeZone} query={q} /> : example.timeZone}
|
|
2543
2502
|
</span>
|
|
2544
2503
|
</InputField>
|
|
2545
2504
|
)}
|
|
@@ -2547,11 +2506,7 @@ function LocaleExampleDetails(props: {
|
|
|
2547
2506
|
{typeof example.currency !== "undefined" && (
|
|
2548
2507
|
<InputField label="Currency">
|
|
2549
2508
|
<span className="font-mono text-sm text-text">
|
|
2550
|
-
{highlight ?
|
|
2551
|
-
<ExamplesSearchHighlight text={example.currency} query={q} />
|
|
2552
|
-
) : (
|
|
2553
|
-
example.currency
|
|
2554
|
-
)}
|
|
2509
|
+
{highlight ? <SearchHighlight text={example.currency} query={q} /> : example.currency}
|
|
2555
2510
|
</span>
|
|
2556
2511
|
</InputField>
|
|
2557
2512
|
)}
|
|
@@ -2560,7 +2515,7 @@ function LocaleExampleDetails(props: {
|
|
|
2560
2515
|
<InputField label="Formats">
|
|
2561
2516
|
{highlight ? (
|
|
2562
2517
|
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2563
|
-
<
|
|
2518
|
+
<SearchHighlight text={JSON.stringify(example.formats, null, 2)} query={q} />
|
|
2564
2519
|
</pre>
|
|
2565
2520
|
) : (
|
|
2566
2521
|
<JsonValueBlock value={example.formats} />
|
|
@@ -2703,14 +2658,14 @@ function LocaleExamplesCompactView(props: {
|
|
|
2703
2658
|
onClick={() => toggleExample(exampleId)}
|
|
2704
2659
|
>
|
|
2705
2660
|
<td className="border-b border-border px-3 py-2 font-medium text-muted">
|
|
2706
|
-
<
|
|
2661
|
+
<SearchHighlight
|
|
2707
2662
|
text={getLocaleExampleCompactLabel(example)}
|
|
2708
2663
|
query={props.searchQuery}
|
|
2709
2664
|
/>
|
|
2710
2665
|
</td>
|
|
2711
2666
|
<td className="min-w-0 border-b border-border px-3 py-2 text-muted">
|
|
2712
2667
|
<div className="whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
2713
|
-
<
|
|
2668
|
+
<SearchHighlight
|
|
2714
2669
|
text={example.description || "—"}
|
|
2715
2670
|
query={props.searchQuery}
|
|
2716
2671
|
/>
|
|
@@ -2725,7 +2680,7 @@ function LocaleExamplesCompactView(props: {
|
|
|
2725
2680
|
style={props.localeDirection ? { unicodeBidi: "plaintext" } : undefined}
|
|
2726
2681
|
>
|
|
2727
2682
|
<div className="whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
2728
|
-
<
|
|
2683
|
+
<SearchHighlight
|
|
2729
2684
|
text={
|
|
2730
2685
|
typeof example.evaluatedTranslation === "string"
|
|
2731
2686
|
? example.evaluatedTranslation
|
|
@@ -2743,7 +2698,7 @@ function LocaleExamplesCompactView(props: {
|
|
|
2743
2698
|
<div className="group flex items-center gap-2">
|
|
2744
2699
|
<h3 className="text-sm font-semibold">
|
|
2745
2700
|
{props.searchQuery.trim() ? (
|
|
2746
|
-
<
|
|
2701
|
+
<SearchHighlight
|
|
2747
2702
|
text={getLocaleExampleTitle(example)}
|
|
2748
2703
|
query={props.searchQuery}
|
|
2749
2704
|
/>
|
|
@@ -2930,12 +2885,24 @@ function filterDuplicateValuesBySearch(
|
|
|
2930
2885
|
});
|
|
2931
2886
|
}
|
|
2932
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
|
+
|
|
2933
2896
|
export function LocaleDuplicatesTab() {
|
|
2934
2897
|
const { detail, setKey } = useEntityDetail();
|
|
2935
2898
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
2936
2899
|
const [duplicates, setDuplicates] = React.useState<LocaleDuplicates | null>(null);
|
|
2937
2900
|
const [error, setError] = React.useState<string | null>(null);
|
|
2938
2901
|
const [expandedDuplicateHashes, setExpandedDuplicateHashes] = React.useState<string[]>([]);
|
|
2902
|
+
const [sort, setSort] = React.useState<DuplicateValuesSort>({
|
|
2903
|
+
column: "messages",
|
|
2904
|
+
direction: "desc",
|
|
2905
|
+
});
|
|
2939
2906
|
const localeDirection = (detail.entity as Record<string, any>).direction as string | undefined;
|
|
2940
2907
|
const searchQuery = searchParams.get("q") ?? "";
|
|
2941
2908
|
|
|
@@ -3018,9 +2985,9 @@ export function LocaleDuplicatesTab() {
|
|
|
3018
2985
|
return <p className="text-sm text-muted">No duplicate translations found for this locale.</p>;
|
|
3019
2986
|
}
|
|
3020
2987
|
|
|
3021
|
-
const visibleDuplicateValues =
|
|
3022
|
-
duplicates.duplicateValues,
|
|
3023
|
-
|
|
2988
|
+
const visibleDuplicateValues = sortDuplicateValues(
|
|
2989
|
+
filterDuplicateValuesBySearch(duplicates.duplicateValues, searchQuery),
|
|
2990
|
+
sort,
|
|
3024
2991
|
);
|
|
3025
2992
|
|
|
3026
2993
|
return (
|
|
@@ -3066,8 +3033,48 @@ export function LocaleDuplicatesTab() {
|
|
|
3066
3033
|
</colgroup>
|
|
3067
3034
|
<thead className="bg-elevated text-left text-[11px] uppercase tracking-wide text-muted">
|
|
3068
3035
|
<tr>
|
|
3069
|
-
<th
|
|
3070
|
-
|
|
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>
|
|
3071
3078
|
</tr>
|
|
3072
3079
|
</thead>
|
|
3073
3080
|
<tbody>
|
|
@@ -3097,7 +3104,7 @@ export function LocaleDuplicatesTab() {
|
|
|
3097
3104
|
style={localeDirection ? { unicodeBidi: "plaintext" } : undefined}
|
|
3098
3105
|
>
|
|
3099
3106
|
<div className="truncate">
|
|
3100
|
-
<
|
|
3107
|
+
<SearchHighlight text={duplicate.value} query={searchQuery} />
|
|
3101
3108
|
</div>
|
|
3102
3109
|
</td>
|
|
3103
3110
|
<td className="min-w-0 border-b border-border px-3 py-2 text-muted">
|
|
@@ -3141,9 +3148,9 @@ export function LocaleDuplicatesTab() {
|
|
|
3141
3148
|
<td className="min-w-0 border-b border-border px-3 py-2">
|
|
3142
3149
|
<Link
|
|
3143
3150
|
to={getEntityRoute("message", messageKey, setKey)}
|
|
3144
|
-
className="font-medium text-primary hover:underline"
|
|
3151
|
+
className="font-medium text-primary [overflow-wrap:anywhere] hover:underline"
|
|
3145
3152
|
>
|
|
3146
|
-
<
|
|
3153
|
+
<SearchHighlight
|
|
3147
3154
|
text={messageKey}
|
|
3148
3155
|
query={searchQuery}
|
|
3149
3156
|
/>
|
|
@@ -3158,7 +3165,7 @@ export function LocaleDuplicatesTab() {
|
|
|
3158
3165
|
className="font-medium text-primary hover:underline"
|
|
3159
3166
|
onClick={(event) => event.stopPropagation()}
|
|
3160
3167
|
>
|
|
3161
|
-
<
|
|
3168
|
+
<SearchHighlight
|
|
3162
3169
|
text={sourceLocale}
|
|
3163
3170
|
query={searchQuery}
|
|
3164
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
|
);
|