@messagevisor/catalog 0.9.0 → 0.13.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-Bic-2mlS.css +1 -0
- package/dist/assets/index-C8ldlvUw.js +73 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/pages/EntityDetailPage.tsx +163 -1
- package/src/utils/relevantIcuFormats.spec.ts +99 -0
- package/src/utils/relevantIcuFormats.ts +129 -0
- package/dist/assets/index-9TVwIAiT.js +0 -73
- package/dist/assets/index-BgEITLIy.css +0 -1
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Messagevisor Catalog</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-C8ldlvUw.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bic-2mlS.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@messagevisor/catalog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Static catalog UI for Messagevisor projects",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"typescript": "^5.7.2",
|
|
58
58
|
"vite": "^6.0.7"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "398840e9cad048e3fc62433a81083eeeec816fd6"
|
|
61
61
|
}
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
type DuplicateValuesSort,
|
|
51
51
|
type SortDirection,
|
|
52
52
|
} from "../utils/duplicateSorting";
|
|
53
|
+
import { getRelevantIcuFormats } from "../utils/relevantIcuFormats";
|
|
53
54
|
import type { ParsedQuery } from "../utils/searchQuery";
|
|
54
55
|
import { parseQuery } from "../utils/searchQuery";
|
|
55
56
|
|
|
@@ -342,6 +343,19 @@ function orderedFormatTypePillKeys(typesFromData: string[]): string[] {
|
|
|
342
343
|
return [...FORMAT_TYPE_PRIMARY_PILLS, ...rest];
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
function orderedFormatSectionKeys(typesFromData: string[]): string[] {
|
|
347
|
+
const available = new Set(typesFromData);
|
|
348
|
+
const primary = FORMAT_TYPE_PRIMARY_PILLS.filter((type) => available.has(type));
|
|
349
|
+
const rest = typesFromData
|
|
350
|
+
.filter((type) => !FORMAT_TYPE_PRIMARY_PILLS.includes(type))
|
|
351
|
+
.sort((a, b) => a.localeCompare(b));
|
|
352
|
+
return [...primary, ...rest];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function formatStyleFragmentId(type: string, style: string): string {
|
|
356
|
+
return `format-${slugifyFragment(`${type}-${style || "default"}`)}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
345
359
|
function setSearchParam(searchParams: URLSearchParams, key: string, value?: string) {
|
|
346
360
|
const next = new URLSearchParams(searchParams);
|
|
347
361
|
|
|
@@ -729,6 +743,8 @@ function FormatRowsTable(props: {
|
|
|
729
743
|
);
|
|
730
744
|
}
|
|
731
745
|
|
|
746
|
+
useScrollToHash([splitPath, q, props.selectedFormatType, rows.length, visibleRows.length]);
|
|
747
|
+
|
|
732
748
|
if (rows.length === 0) {
|
|
733
749
|
return <p className="text-sm text-muted">No formats found.</p>;
|
|
734
750
|
}
|
|
@@ -743,6 +759,19 @@ function FormatRowsTable(props: {
|
|
|
743
759
|
}
|
|
744
760
|
|
|
745
761
|
const splitPlans = splitPath ? buildFormatSplitRowPlans(visibleRows) : null;
|
|
762
|
+
const splitSectionKeys = splitPath
|
|
763
|
+
? orderedFormatSectionKeys(collectSortedFormatTypes(visibleRows))
|
|
764
|
+
: [];
|
|
765
|
+
const splitRowsByType = splitPath
|
|
766
|
+
? visibleRows.reduce<Record<string, FormatRow[]>>((groups, row) => {
|
|
767
|
+
const type = splitFormatPath(row.path).type;
|
|
768
|
+
if (!groups[type]) {
|
|
769
|
+
groups[type] = [];
|
|
770
|
+
}
|
|
771
|
+
groups[type].push(row);
|
|
772
|
+
return groups;
|
|
773
|
+
}, {})
|
|
774
|
+
: {};
|
|
746
775
|
|
|
747
776
|
function segmentBody(segment: string) {
|
|
748
777
|
return segment ? (
|
|
@@ -820,10 +849,16 @@ function FormatRowsTable(props: {
|
|
|
820
849
|
|
|
821
850
|
function renderSplitStyleCellContent(plan: FormatSplitRowPlan) {
|
|
822
851
|
const sourceMeta = renderFormatSourceMeta(plan.row);
|
|
852
|
+
const fragmentId = formatStyleFragmentId(plan.parts.type, plan.parts.style);
|
|
823
853
|
|
|
824
854
|
return (
|
|
825
855
|
<div className="flex min-w-0 flex-col items-start gap-1.5">
|
|
826
|
-
<
|
|
856
|
+
<a
|
|
857
|
+
href={`#${fragmentId}`}
|
|
858
|
+
className="w-full min-w-0 rounded-sm text-text hover:text-primary hover:underline"
|
|
859
|
+
>
|
|
860
|
+
{renderSplitSegment(plan.parts.style, "style")}
|
|
861
|
+
</a>
|
|
827
862
|
{sourceMeta}
|
|
828
863
|
</div>
|
|
829
864
|
);
|
|
@@ -920,6 +955,109 @@ function FormatRowsTable(props: {
|
|
|
920
955
|
);
|
|
921
956
|
}
|
|
922
957
|
|
|
958
|
+
function renderSplitTable(plans: FormatSplitRowPlan[]) {
|
|
959
|
+
return (
|
|
960
|
+
<div className="min-w-0 overflow-x-auto rounded-lg border border-border">
|
|
961
|
+
<table className="w-full min-w-[34rem] table-fixed border-collapse bg-surface text-xs">
|
|
962
|
+
<colgroup>
|
|
963
|
+
{showExampleColumn ? (
|
|
964
|
+
<>
|
|
965
|
+
<col className="min-w-0 w-[24%]" />
|
|
966
|
+
<col className="min-w-0 w-[18%]" />
|
|
967
|
+
<col className="min-w-0 w-[28%]" />
|
|
968
|
+
<col className="min-w-0 w-[30%]" />
|
|
969
|
+
</>
|
|
970
|
+
) : (
|
|
971
|
+
<>
|
|
972
|
+
<col className="min-w-0 w-[30%]" />
|
|
973
|
+
<col className="min-w-0 w-[30%]" />
|
|
974
|
+
<col className="min-w-0 w-[40%]" />
|
|
975
|
+
</>
|
|
976
|
+
)}
|
|
977
|
+
</colgroup>
|
|
978
|
+
<thead className="bg-elevated text-left text-[11px] uppercase tracking-wide text-muted">
|
|
979
|
+
<tr>
|
|
980
|
+
<th className="align-middle border-b border-r border-border/50 px-3 py-2 font-semibold">
|
|
981
|
+
Style
|
|
982
|
+
</th>
|
|
983
|
+
{showExampleColumn ? (
|
|
984
|
+
<th className="align-middle border-b border-r border-border/50 px-3 py-2 font-semibold">
|
|
985
|
+
Example
|
|
986
|
+
</th>
|
|
987
|
+
) : null}
|
|
988
|
+
<th className="align-middle border-b border-r border-border/40 px-3 py-2 font-semibold">
|
|
989
|
+
Param
|
|
990
|
+
</th>
|
|
991
|
+
<th className="align-middle border-b border-border px-3 py-2 font-semibold">Value</th>
|
|
992
|
+
</tr>
|
|
993
|
+
</thead>
|
|
994
|
+
<tbody>
|
|
995
|
+
{plans.map((plan) => {
|
|
996
|
+
const bandClass = bandSurfaceClass(plan.typeBand);
|
|
997
|
+
|
|
998
|
+
return (
|
|
999
|
+
<tr key={plan.row.path}>
|
|
1000
|
+
{plan.showStyleCell ? (
|
|
1001
|
+
<td
|
|
1002
|
+
id={formatStyleFragmentId(plan.parts.type, plan.parts.style)}
|
|
1003
|
+
rowSpan={plan.styleRowSpan}
|
|
1004
|
+
className={[
|
|
1005
|
+
"align-middle min-w-0 scroll-mt-2 px-3 py-2 font-medium",
|
|
1006
|
+
bandClass,
|
|
1007
|
+
formatSplitCellBorderClass("style"),
|
|
1008
|
+
].join(" ")}
|
|
1009
|
+
>
|
|
1010
|
+
{renderSplitStyleCellContent(plan)}
|
|
1011
|
+
</td>
|
|
1012
|
+
) : null}
|
|
1013
|
+
{renderSplitExampleColumn(plan, bandClass)}
|
|
1014
|
+
<td
|
|
1015
|
+
className={[
|
|
1016
|
+
"align-middle min-w-0 px-3 py-2 font-medium text-muted",
|
|
1017
|
+
bandClass,
|
|
1018
|
+
formatSplitCellBorderClass("param"),
|
|
1019
|
+
].join(" ")}
|
|
1020
|
+
>
|
|
1021
|
+
{renderSplitSegment(plan.parts.param, "param")}
|
|
1022
|
+
</td>
|
|
1023
|
+
{renderValueColumn(
|
|
1024
|
+
plan.row,
|
|
1025
|
+
["align-middle min-w-0 px-3 py-2", bandClass].join(" "),
|
|
1026
|
+
)}
|
|
1027
|
+
</tr>
|
|
1028
|
+
);
|
|
1029
|
+
})}
|
|
1030
|
+
</tbody>
|
|
1031
|
+
</table>
|
|
1032
|
+
</div>
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (splitPath) {
|
|
1037
|
+
return (
|
|
1038
|
+
<div className="space-y-5">
|
|
1039
|
+
{splitSectionKeys.map((typeKey) => {
|
|
1040
|
+
const sectionRows = splitRowsByType[typeKey] || [];
|
|
1041
|
+
const sectionPlans = buildFormatSplitRowPlans(sectionRows);
|
|
1042
|
+
const styleCount = new Set(
|
|
1043
|
+
sectionRows.map((row) => splitFormatPath(row.path).style).filter(Boolean),
|
|
1044
|
+
).size;
|
|
1045
|
+
return (
|
|
1046
|
+
<section key={typeKey} className="space-y-2">
|
|
1047
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
1048
|
+
<h3 className="text-sm font-semibold text-text">{typeKey}</h3>
|
|
1049
|
+
<span className="text-xs text-muted">
|
|
1050
|
+
{styleCount} {styleCount === 1 ? "style" : "styles"}
|
|
1051
|
+
</span>
|
|
1052
|
+
</div>
|
|
1053
|
+
{renderSplitTable(sectionPlans)}
|
|
1054
|
+
</section>
|
|
1055
|
+
);
|
|
1056
|
+
})}
|
|
1057
|
+
</div>
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
923
1061
|
return (
|
|
924
1062
|
<div className="min-w-0 overflow-x-auto rounded-xl border border-border">
|
|
925
1063
|
<table className="w-full min-w-[40rem] table-fixed border-collapse bg-surface text-xs">
|
|
@@ -2405,12 +2543,18 @@ function LocaleExampleDetails(props: {
|
|
|
2405
2543
|
example: EvaluatedLocaleExample;
|
|
2406
2544
|
setKey?: string;
|
|
2407
2545
|
localeDirection?: string;
|
|
2546
|
+
computedFormats?: unknown;
|
|
2408
2547
|
showLocale?: boolean;
|
|
2409
2548
|
highlightQuery?: string;
|
|
2410
2549
|
}) {
|
|
2411
2550
|
const { example, setKey, localeDirection } = props;
|
|
2412
2551
|
const q = props.highlightQuery?.trim() ?? "";
|
|
2413
2552
|
const highlight = Boolean(q);
|
|
2553
|
+
const relevantFormats = getRelevantIcuFormats(
|
|
2554
|
+
example.rawMessage || example.originalTranslation,
|
|
2555
|
+
props.computedFormats,
|
|
2556
|
+
example.formats,
|
|
2557
|
+
);
|
|
2414
2558
|
|
|
2415
2559
|
function localeLink(localeKey: string) {
|
|
2416
2560
|
return highlight ? (
|
|
@@ -2562,6 +2706,18 @@ function LocaleExampleDetails(props: {
|
|
|
2562
2706
|
)}
|
|
2563
2707
|
</InputField>
|
|
2564
2708
|
)}
|
|
2709
|
+
|
|
2710
|
+
{typeof relevantFormats !== "undefined" && (
|
|
2711
|
+
<InputField label="Relevant formats">
|
|
2712
|
+
{highlight ? (
|
|
2713
|
+
<pre className="max-w-full whitespace-pre-wrap rounded border border-border bg-elevated p-4 text-xs text-text [overflow-wrap:anywhere]">
|
|
2714
|
+
<SearchHighlight text={JSON.stringify(relevantFormats, null, 2)} query={q} />
|
|
2715
|
+
</pre>
|
|
2716
|
+
) : (
|
|
2717
|
+
<JsonValueBlock value={relevantFormats} />
|
|
2718
|
+
)}
|
|
2719
|
+
</InputField>
|
|
2720
|
+
)}
|
|
2565
2721
|
</div>
|
|
2566
2722
|
);
|
|
2567
2723
|
}
|
|
@@ -2570,6 +2726,7 @@ function LocaleExamplesExpandedView(props: {
|
|
|
2570
2726
|
examples: EvaluatedLocaleExample[];
|
|
2571
2727
|
setKey?: string;
|
|
2572
2728
|
localeDirection?: string;
|
|
2729
|
+
computedFormats?: unknown;
|
|
2573
2730
|
searchQuery: string;
|
|
2574
2731
|
}) {
|
|
2575
2732
|
return (
|
|
@@ -2595,6 +2752,7 @@ function LocaleExamplesExpandedView(props: {
|
|
|
2595
2752
|
example={example}
|
|
2596
2753
|
setKey={props.setKey}
|
|
2597
2754
|
localeDirection={props.localeDirection}
|
|
2755
|
+
computedFormats={props.computedFormats}
|
|
2598
2756
|
showLocale={false}
|
|
2599
2757
|
highlightQuery={props.searchQuery}
|
|
2600
2758
|
/>
|
|
@@ -2614,6 +2772,7 @@ function LocaleExamplesCompactView(props: {
|
|
|
2614
2772
|
examples: EvaluatedLocaleExample[];
|
|
2615
2773
|
setKey?: string;
|
|
2616
2774
|
localeDirection?: string;
|
|
2775
|
+
computedFormats?: unknown;
|
|
2617
2776
|
searchQuery: string;
|
|
2618
2777
|
}) {
|
|
2619
2778
|
const [expandedExampleIds, setExpandedExampleIds] = React.useState<string[]>([]);
|
|
@@ -2753,6 +2912,7 @@ function LocaleExamplesCompactView(props: {
|
|
|
2753
2912
|
example={example}
|
|
2754
2913
|
setKey={props.setKey}
|
|
2755
2914
|
localeDirection={props.localeDirection}
|
|
2915
|
+
computedFormats={props.computedFormats}
|
|
2756
2916
|
showLocale={false}
|
|
2757
2917
|
highlightQuery={props.searchQuery}
|
|
2758
2918
|
/>
|
|
@@ -2888,6 +3048,7 @@ export function LocaleExamplesTab() {
|
|
|
2888
3048
|
examples={filteredExamples}
|
|
2889
3049
|
setKey={setKey}
|
|
2890
3050
|
localeDirection={localeDirection}
|
|
3051
|
+
computedFormats={detail.computedFormats}
|
|
2891
3052
|
searchQuery={searchQuery}
|
|
2892
3053
|
/>
|
|
2893
3054
|
) : (
|
|
@@ -2895,6 +3056,7 @@ export function LocaleExamplesTab() {
|
|
|
2895
3056
|
examples={filteredExamples}
|
|
2896
3057
|
setKey={setKey}
|
|
2897
3058
|
localeDirection={localeDirection}
|
|
3059
|
+
computedFormats={detail.computedFormats}
|
|
2898
3060
|
searchQuery={searchQuery}
|
|
2899
3061
|
/>
|
|
2900
3062
|
)}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { extractIcuFormatStyleReferences, getRelevantIcuFormats } from "./relevantIcuFormats";
|
|
2
|
+
|
|
3
|
+
describe("relevant ICU formats", function () {
|
|
4
|
+
const computedFormats = {
|
|
5
|
+
number: {
|
|
6
|
+
decimal: { maximumFractionDigits: 2 },
|
|
7
|
+
roundingExpand: { maximumFractionDigits: 2, roundingMode: "expand" },
|
|
8
|
+
},
|
|
9
|
+
date: {
|
|
10
|
+
fullStyle: { dateStyle: "full" },
|
|
11
|
+
},
|
|
12
|
+
time: {
|
|
13
|
+
seconds: { hour: "numeric", minute: "2-digit", second: "2-digit" },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
it("finds a named number style in a raw message", function () {
|
|
18
|
+
expect(
|
|
19
|
+
getRelevantIcuFormats("Value: {value, number, roundingExpand}", computedFormats),
|
|
20
|
+
).toEqual({
|
|
21
|
+
number: {
|
|
22
|
+
roundingExpand: { maximumFractionDigits: 2, roundingMode: "expand" },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("can use the original translation from a message-key example", function () {
|
|
28
|
+
expect(getRelevantIcuFormats("Amount: {amount, number, decimal}", computedFormats)).toEqual({
|
|
29
|
+
number: {
|
|
30
|
+
decimal: { maximumFractionDigits: 2 },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("includes date and time styles", function () {
|
|
36
|
+
expect(
|
|
37
|
+
getRelevantIcuFormats(
|
|
38
|
+
"When: {when, date, fullStyle} at {when, time, seconds}",
|
|
39
|
+
computedFormats,
|
|
40
|
+
),
|
|
41
|
+
).toEqual({
|
|
42
|
+
date: {
|
|
43
|
+
fullStyle: { dateStyle: "full" },
|
|
44
|
+
},
|
|
45
|
+
time: {
|
|
46
|
+
seconds: { hour: "numeric", minute: "2-digit", second: "2-digit" },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("deduplicates repeated styles while preserving first-seen type order", function () {
|
|
52
|
+
expect(
|
|
53
|
+
extractIcuFormatStyleReferences(
|
|
54
|
+
"{amount, number, decimal} then {otherAmount, number, decimal}",
|
|
55
|
+
),
|
|
56
|
+
).toEqual({
|
|
57
|
+
number: ["decimal"],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("finds style references nested in ICU plural and select branches", function () {
|
|
62
|
+
expect(
|
|
63
|
+
getRelevantIcuFormats(
|
|
64
|
+
"{count, plural, one {{amount, number, decimal}} other {{when, date, fullStyle}}}",
|
|
65
|
+
computedFormats,
|
|
66
|
+
),
|
|
67
|
+
).toEqual({
|
|
68
|
+
number: {
|
|
69
|
+
decimal: { maximumFractionDigits: 2 },
|
|
70
|
+
},
|
|
71
|
+
date: {
|
|
72
|
+
fullStyle: { dateStyle: "full" },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("ignores unknown styles, skeletons, and non-format ICU constructs", function () {
|
|
78
|
+
expect(
|
|
79
|
+
getRelevantIcuFormats(
|
|
80
|
+
"{amount, number, missing} {when, date, ::yyyyMMdd} {status, select, open {Open} other {Other}}",
|
|
81
|
+
computedFormats,
|
|
82
|
+
),
|
|
83
|
+
).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("applies inline example formats as style-level overrides", function () {
|
|
87
|
+
expect(
|
|
88
|
+
getRelevantIcuFormats("{amount, number, decimal}", computedFormats, {
|
|
89
|
+
number: {
|
|
90
|
+
decimal: { maximumFractionDigits: 4 },
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
).toEqual({
|
|
94
|
+
number: {
|
|
95
|
+
decimal: { maximumFractionDigits: 4 },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
type FormatBucket = Record<string, unknown>;
|
|
2
|
+
type FormatPresetsLike = Record<string, FormatBucket | unknown>;
|
|
3
|
+
|
|
4
|
+
const ICU_FORMAT_TYPES = ["number", "date", "time"] as const;
|
|
5
|
+
|
|
6
|
+
type IcuFormatType = (typeof ICU_FORMAT_TYPES)[number];
|
|
7
|
+
|
|
8
|
+
const ICU_FORMAT_TYPE_SET = new Set<string>(ICU_FORMAT_TYPES);
|
|
9
|
+
|
|
10
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mergeFormatPresetsLike(
|
|
15
|
+
parent?: FormatPresetsLike,
|
|
16
|
+
child?: FormatPresetsLike,
|
|
17
|
+
): FormatPresetsLike | undefined {
|
|
18
|
+
if (!isPlainObject(parent)) {
|
|
19
|
+
return isPlainObject(child) ? child : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!isPlainObject(child)) {
|
|
23
|
+
return parent;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result: FormatPresetsLike = { ...parent };
|
|
27
|
+
|
|
28
|
+
for (const typeKey of Object.keys(child)) {
|
|
29
|
+
const parentStyles = result[typeKey];
|
|
30
|
+
const childStyles = child[typeKey];
|
|
31
|
+
|
|
32
|
+
if (!isPlainObject(parentStyles) || !isPlainObject(childStyles)) {
|
|
33
|
+
result[typeKey] = childStyles;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
result[typeKey] = {
|
|
38
|
+
...parentStyles,
|
|
39
|
+
...childStyles,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeIcuStyleName(value: string) {
|
|
47
|
+
const style = value.trim();
|
|
48
|
+
|
|
49
|
+
if (!style || style.startsWith("::")) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return style.replace(/,$/, "").trim() || undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function extractIcuFormatStyleReferences(message: string | undefined) {
|
|
57
|
+
const references: Partial<Record<IcuFormatType, string[]>> = {};
|
|
58
|
+
|
|
59
|
+
if (!message) {
|
|
60
|
+
return references;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pattern = /\{[^{}]*,\s*(number|date|time)\s*,\s*([^{}]+?)\}/g;
|
|
64
|
+
let match: RegExpExecArray | null;
|
|
65
|
+
|
|
66
|
+
while ((match = pattern.exec(message))) {
|
|
67
|
+
const type = match[1] as IcuFormatType;
|
|
68
|
+
const style = normalizeIcuStyleName(match[2]);
|
|
69
|
+
|
|
70
|
+
if (!style) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!references[type]) {
|
|
75
|
+
references[type] = [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!references[type]!.includes(style)) {
|
|
79
|
+
references[type]!.push(style);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return references;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getRelevantIcuFormats(
|
|
87
|
+
message: string | undefined,
|
|
88
|
+
computedFormats: unknown,
|
|
89
|
+
exampleFormats?: unknown,
|
|
90
|
+
) {
|
|
91
|
+
const effectiveFormats = mergeFormatPresetsLike(
|
|
92
|
+
isPlainObject(computedFormats) ? computedFormats : undefined,
|
|
93
|
+
isPlainObject(exampleFormats) ? exampleFormats : undefined,
|
|
94
|
+
);
|
|
95
|
+
const references = extractIcuFormatStyleReferences(message);
|
|
96
|
+
const relevant: Partial<Record<IcuFormatType, Record<string, unknown>>> = {};
|
|
97
|
+
|
|
98
|
+
if (!effectiveFormats) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const typeKey of Object.keys(references)) {
|
|
103
|
+
if (!ICU_FORMAT_TYPE_SET.has(typeKey)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const type = typeKey as IcuFormatType;
|
|
108
|
+
const styles = references[type] || [];
|
|
109
|
+
const availableStyles = effectiveFormats[type];
|
|
110
|
+
|
|
111
|
+
if (!isPlainObject(availableStyles)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const style of styles) {
|
|
116
|
+
if (!Object.prototype.hasOwnProperty.call(availableStyles, style)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!relevant[type]) {
|
|
121
|
+
relevant[type] = {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
relevant[type]![style] = availableStyles[style];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Object.keys(relevant).length > 0 ? relevant : undefined;
|
|
129
|
+
}
|