@messagevisor/catalog 0.8.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.
@@ -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 ? (
@@ -756,6 +785,48 @@ function FormatRowsTable(props: {
756
785
  );
757
786
  }
758
787
 
788
+ function renderFormatSourceBadge(row: FormatRow) {
789
+ if (row.source === "inherited" && row.from) {
790
+ return (
791
+ <LabelValueBadge
792
+ label="inherited from"
793
+ value={row.from}
794
+ to={getEntityRoute("locale", row.from, setKey)}
795
+ tone="inheritance"
796
+ compact
797
+ />
798
+ );
799
+ }
800
+
801
+ if (row.source === "target") {
802
+ return <LabelValueBadge label="from" value="target" tone="neutral" compact />;
803
+ }
804
+
805
+ return null;
806
+ }
807
+
808
+ function renderFormatSourceMeta(row: FormatRow) {
809
+ if (row.source === "inherited" && row.from) {
810
+ return (
811
+ <div className="text-[10px] font-normal leading-snug text-faint">
812
+ Inherited from{" "}
813
+ <Link
814
+ to={getEntityRoute("locale", row.from, setKey)}
815
+ className="font-medium text-muted hover:text-primary hover:underline"
816
+ >
817
+ {row.from}
818
+ </Link>
819
+ </div>
820
+ );
821
+ }
822
+
823
+ if (row.source === "target") {
824
+ return <div className="text-[10px] font-normal leading-snug text-faint">Target override</div>;
825
+ }
826
+
827
+ return null;
828
+ }
829
+
759
830
  /** Plain text in the cell. Param stays monospace for paths. */
760
831
  function renderSplitSegment(segment: string, role: "type" | "style" | "param") {
761
832
  const body = segmentBody(segment);
@@ -776,6 +847,23 @@ function FormatRowsTable(props: {
776
847
  );
777
848
  }
778
849
 
850
+ function renderSplitStyleCellContent(plan: FormatSplitRowPlan) {
851
+ const sourceMeta = renderFormatSourceMeta(plan.row);
852
+ const fragmentId = formatStyleFragmentId(plan.parts.type, plan.parts.style);
853
+
854
+ return (
855
+ <div className="flex min-w-0 flex-col items-start gap-1.5">
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>
862
+ {sourceMeta}
863
+ </div>
864
+ );
865
+ }
866
+
779
867
  function bandSurfaceClass(band: number) {
780
868
  return band % 2 === 0 ? "bg-surface" : "bg-elevated/[0.2]";
781
869
  }
@@ -801,8 +889,7 @@ function FormatRowsTable(props: {
801
889
 
802
890
  function renderValueColumn(row: FormatRow, bandAndPaddingClass: string) {
803
891
  const valueText = formatValue(row.value);
804
- const showInheritedBadge = row.source === "inherited" && Boolean(row.from);
805
- const showTargetBadge = row.source === "target";
892
+ const badge = splitPath ? null : renderFormatSourceBadge(row);
806
893
 
807
894
  return (
808
895
  <td className={[bandAndPaddingClass, formatSplitCellBorderClass("value")].join(" ")}>
@@ -815,21 +902,8 @@ function FormatRowsTable(props: {
815
902
  >
816
903
  {highlight ? <SearchHighlight text={valueText} query={highlightNeedle} /> : valueText}
817
904
  </div>
818
- {(showInheritedBadge || showTargetBadge) && (
819
- <div className="flex shrink-0 flex-col items-end justify-center gap-1">
820
- {showInheritedBadge && row.from ? (
821
- <LabelValueBadge
822
- label="inherited from"
823
- value={row.from}
824
- to={getEntityRoute("locale", row.from, setKey)}
825
- tone="inheritance"
826
- compact
827
- />
828
- ) : null}
829
- {showTargetBadge ? (
830
- <LabelValueBadge label="from" value="target" tone="neutral" compact />
831
- ) : null}
832
- </div>
905
+ {badge && (
906
+ <div className="flex shrink-0 flex-col items-end justify-center gap-1">{badge}</div>
833
907
  )}
834
908
  </div>
835
909
  </td>
@@ -881,6 +955,109 @@ function FormatRowsTable(props: {
881
955
  );
882
956
  }
883
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
+
884
1061
  return (
885
1062
  <div className="min-w-0 overflow-x-auto rounded-xl border border-border">
886
1063
  <table className="w-full min-w-[40rem] table-fixed border-collapse bg-surface text-xs">
@@ -896,9 +1073,9 @@ function FormatRowsTable(props: {
896
1073
  </>
897
1074
  ) : (
898
1075
  <>
899
- <col className="min-w-0 w-[22%]" />
900
- <col className="min-w-0 w-[36%]" />
901
- <col className="min-w-0 w-[42%]" />
1076
+ <col className="min-w-0 w-[28%]" />
1077
+ <col className="min-w-0 w-[32%]" />
1078
+ <col className="min-w-0 w-[40%]" />
902
1079
  </>
903
1080
  )
904
1081
  ) : showExampleColumn ? (
@@ -912,9 +1089,9 @@ function FormatRowsTable(props: {
912
1089
  ) : (
913
1090
  <>
914
1091
  <col className="min-w-0 w-[12%]" />
915
- <col className="min-w-0 w-[18%]" />
916
- <col className="min-w-0 w-[32%]" />
917
- <col className="min-w-0 w-[38%]" />
1092
+ <col className="min-w-0 w-[24%]" />
1093
+ <col className="min-w-0 w-[28%]" />
1094
+ <col className="min-w-0 w-[36%]" />
918
1095
  </>
919
1096
  )}
920
1097
  </colgroup>
@@ -1003,7 +1180,7 @@ function FormatRowsTable(props: {
1003
1180
  formatSplitCellBorderClass("style"),
1004
1181
  ].join(" ")}
1005
1182
  >
1006
- {renderSplitSegment(plan.parts.style, "style")}
1183
+ {renderSplitStyleCellContent(plan)}
1007
1184
  </td>
1008
1185
  ) : null}
1009
1186
  {renderSplitExampleColumn(plan, bandClass)}
@@ -2366,12 +2543,18 @@ function LocaleExampleDetails(props: {
2366
2543
  example: EvaluatedLocaleExample;
2367
2544
  setKey?: string;
2368
2545
  localeDirection?: string;
2546
+ computedFormats?: unknown;
2369
2547
  showLocale?: boolean;
2370
2548
  highlightQuery?: string;
2371
2549
  }) {
2372
2550
  const { example, setKey, localeDirection } = props;
2373
2551
  const q = props.highlightQuery?.trim() ?? "";
2374
2552
  const highlight = Boolean(q);
2553
+ const relevantFormats = getRelevantIcuFormats(
2554
+ example.rawMessage || example.originalTranslation,
2555
+ props.computedFormats,
2556
+ example.formats,
2557
+ );
2375
2558
 
2376
2559
  function localeLink(localeKey: string) {
2377
2560
  return highlight ? (
@@ -2523,6 +2706,18 @@ function LocaleExampleDetails(props: {
2523
2706
  )}
2524
2707
  </InputField>
2525
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
+ )}
2526
2721
  </div>
2527
2722
  );
2528
2723
  }
@@ -2531,6 +2726,7 @@ function LocaleExamplesExpandedView(props: {
2531
2726
  examples: EvaluatedLocaleExample[];
2532
2727
  setKey?: string;
2533
2728
  localeDirection?: string;
2729
+ computedFormats?: unknown;
2534
2730
  searchQuery: string;
2535
2731
  }) {
2536
2732
  return (
@@ -2556,6 +2752,7 @@ function LocaleExamplesExpandedView(props: {
2556
2752
  example={example}
2557
2753
  setKey={props.setKey}
2558
2754
  localeDirection={props.localeDirection}
2755
+ computedFormats={props.computedFormats}
2559
2756
  showLocale={false}
2560
2757
  highlightQuery={props.searchQuery}
2561
2758
  />
@@ -2575,6 +2772,7 @@ function LocaleExamplesCompactView(props: {
2575
2772
  examples: EvaluatedLocaleExample[];
2576
2773
  setKey?: string;
2577
2774
  localeDirection?: string;
2775
+ computedFormats?: unknown;
2578
2776
  searchQuery: string;
2579
2777
  }) {
2580
2778
  const [expandedExampleIds, setExpandedExampleIds] = React.useState<string[]>([]);
@@ -2714,6 +2912,7 @@ function LocaleExamplesCompactView(props: {
2714
2912
  example={example}
2715
2913
  setKey={props.setKey}
2716
2914
  localeDirection={props.localeDirection}
2915
+ computedFormats={props.computedFormats}
2717
2916
  showLocale={false}
2718
2917
  highlightQuery={props.searchQuery}
2719
2918
  />
@@ -2849,6 +3048,7 @@ export function LocaleExamplesTab() {
2849
3048
  examples={filteredExamples}
2850
3049
  setKey={setKey}
2851
3050
  localeDirection={localeDirection}
3051
+ computedFormats={detail.computedFormats}
2852
3052
  searchQuery={searchQuery}
2853
3053
  />
2854
3054
  ) : (
@@ -2856,6 +3056,7 @@ export function LocaleExamplesTab() {
2856
3056
  examples={filteredExamples}
2857
3057
  setKey={setKey}
2858
3058
  localeDirection={localeDirection}
3059
+ computedFormats={detail.computedFormats}
2859
3060
  searchQuery={searchQuery}
2860
3061
  />
2861
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
+ }