@eifi1/ui-kit 0.27.1 → 0.28.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.
Files changed (109) hide show
  1. package/README.md +7 -5
  2. package/dist/chart.d.ts +1 -2
  3. package/dist/components/amount-input.d.ts +1 -2
  4. package/dist/components/button-group.d.ts +1 -2
  5. package/dist/components/calculator.d.ts +1 -2
  6. package/dist/components/confirm-dialog.d.ts +1 -2
  7. package/dist/components/copy-button.d.ts +1 -2
  8. package/dist/components/danger-confirm.d.ts +1 -2
  9. package/dist/components/facing-pair.d.ts +1 -2
  10. package/dist/components/file-button.d.ts +1 -2
  11. package/dist/components/file-dropzone.d.ts +1 -2
  12. package/dist/components/form-actions.d.ts +1 -2
  13. package/dist/components/iban-input.d.ts +1 -2
  14. package/dist/components/language-select.d.ts +1 -2
  15. package/dist/components/legal-page.d.ts +261 -0
  16. package/dist/components/legal-page.js +253 -0
  17. package/dist/components/legal-page.js.map +1 -0
  18. package/dist/components/legal.d.ts +70 -87
  19. package/dist/components/legal.js +187 -10
  20. package/dist/components/legal.js.map +1 -1
  21. package/dist/components/money-field.d.ts +1 -2
  22. package/dist/components/number-field.d.ts +1 -2
  23. package/dist/components/number-input.d.ts +1 -2
  24. package/dist/components/numpad-sheet.d.ts +1 -2
  25. package/dist/components/phone-input.d.ts +1 -2
  26. package/dist/components/series-chart.d.ts +1 -2
  27. package/dist/components/settings-fields.d.ts +1 -2
  28. package/dist/components/share-card.d.ts +1 -2
  29. package/dist/components/text-link.d.ts +1 -2
  30. package/dist/components/time-input.d.ts +1 -2
  31. package/dist/components/translation-review.d.ts +2 -1
  32. package/dist/components/translation-review.js.map +1 -1
  33. package/dist/components/ui.d.ts +1 -2
  34. package/dist/feedback/feedback-attachment.d.ts +1 -2
  35. package/dist/feedback/feedback-context.d.ts +1 -2
  36. package/dist/feedback/feedback-crash.d.ts +1 -2
  37. package/dist/feedback/feedback-dialog.d.ts +1 -2
  38. package/dist/feedback/feedback-inbox.d.ts +1 -2
  39. package/dist/feedback/feedback-labels.d.ts +1 -2
  40. package/dist/feedback/feedback-menu.d.ts +1 -2
  41. package/dist/feedback/feedback-record.d.ts +1 -2
  42. package/dist/feedback/feedback-row-detail.d.ts +1 -2
  43. package/dist/feedback/feedback-status-undo.d.ts +1 -2
  44. package/dist/feedback/feedback-submit.d.ts +1 -2
  45. package/dist/feedback/feedback-swipe.d.ts +1 -2
  46. package/dist/feedback/feedback-table.d.ts +1 -2
  47. package/dist/feedback/feedback-thread.d.ts +1 -2
  48. package/dist/feedback.d.ts +1 -2
  49. package/dist/hooks/use-file-drop.d.ts +1 -2
  50. package/dist/hooks/use-noindex.d.ts +26 -0
  51. package/dist/hooks/use-noindex.js +18 -0
  52. package/dist/hooks/use-noindex.js.map +1 -0
  53. package/dist/i18n/defaults.d.ts +1 -2
  54. package/dist/i18n/german.d.ts +1 -2
  55. package/dist/i18n/german.js +87 -1
  56. package/dist/i18n/german.js.map +1 -1
  57. package/dist/i18n/kit-labels.d.ts +1 -2
  58. package/dist/i18n/kit-labels.js.map +1 -1
  59. package/dist/i18n/languages.d.ts +1 -2
  60. package/dist/i18n/locales/de-CH.d.ts +1 -2
  61. package/dist/i18n/locales/en.d.ts +1 -2
  62. package/dist/i18n/locales/es.d.ts +1 -2
  63. package/dist/i18n/locales/es.js +87 -1
  64. package/dist/i18n/locales/es.js.map +1 -1
  65. package/dist/i18n/locales/fr.d.ts +1 -2
  66. package/dist/i18n/locales/fr.js +89 -1
  67. package/dist/i18n/locales/fr.js.map +1 -1
  68. package/dist/i18n/locales/hu.d.ts +1 -2
  69. package/dist/i18n/locales/hu.js +90 -1
  70. package/dist/i18n/locales/hu.js.map +1 -1
  71. package/dist/i18n/locales/it.d.ts +1 -2
  72. package/dist/i18n/locales/it.js +90 -1
  73. package/dist/i18n/locales/it.js.map +1 -1
  74. package/dist/i18n/locales/zh.d.ts +1 -2
  75. package/dist/i18n/locales/zh.js +87 -1
  76. package/dist/i18n/locales/zh.js.map +1 -1
  77. package/dist/i18n/review.d.ts +24 -5
  78. package/dist/i18n/review.js +24 -3
  79. package/dist/i18n/review.js.map +1 -1
  80. package/dist/index.d.ts +5 -4
  81. package/dist/index.js +11 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/{kit-labels-DQg82eHB.d.ts → kit-labels-SBN0biPl.d.ts} +305 -3
  84. package/dist/lib/translation-review.d.ts +26 -3
  85. package/dist/lib/translation-review.js +10 -2
  86. package/dist/lib/translation-review.js.map +1 -1
  87. package/dist/rhf/fields.d.ts +1 -2
  88. package/dist/rhf/form.d.ts +1 -2
  89. package/dist/rhf.d.ts +1 -2
  90. package/dist/shell/app-shell.d.ts +1 -2
  91. package/dist/shell/top-bar-brand.d.ts +1 -2
  92. package/dist/shell.d.ts +1 -2
  93. package/dist/wizard/stepper-nav.d.ts +1 -2
  94. package/dist/wizard.d.ts +1 -2
  95. package/package.json +1 -1
  96. package/src/components/legal-page.tsx +566 -0
  97. package/src/components/legal.tsx +451 -43
  98. package/src/components/translation-review.tsx +2 -1
  99. package/src/hooks/use-noindex.ts +37 -0
  100. package/src/i18n/german.ts +81 -0
  101. package/src/i18n/kit-labels.tsx +4 -1
  102. package/src/i18n/locales/es.ts +81 -0
  103. package/src/i18n/locales/fr.ts +83 -0
  104. package/src/i18n/locales/hu.ts +84 -0
  105. package/src/i18n/locales/it.ts +84 -0
  106. package/src/i18n/locales/zh.ts +81 -0
  107. package/src/i18n/review.ts +56 -3
  108. package/src/index.ts +32 -2
  109. package/src/lib/translation-review.ts +43 -4
@@ -1,3 +1,5 @@
1
+ import type { LegalOperator, LegalOperatorText } from "../components/legal";
2
+ import { countryName } from "../lib/countries";
1
3
  import type { UiKitLabels } from "./kit-labels";
2
4
 
3
5
  /**
@@ -6,7 +8,7 @@ import type { UiKitLabels } from "./kit-labels";
6
8
  * keksdose reviews its own locale bundles on its /translations page — one verdict per
7
9
  * locale and key, stored with the text the reviewer saw, so a later change re-opens it.
8
10
  * The kit's words appear on the same screens but live in TypeScript, not in the app's
9
- * bundles, and 138 of them are functions (`(count) => "3 results"`; 0.27.0, every one
11
+ * bundles, and 143 of them are functions (`(count) => "3 results"`; 0.28.0, every one
10
12
  * listed in {@link KIT_LABEL_SAMPLES}), which a bundle flattener skips. This turns a
11
13
  * label tree into rows that page can join on.
12
14
  *
@@ -18,6 +20,13 @@ import type { UiKitLabels } from "./kit-labels";
18
20
  * where the wording may depend on them — 1 and 3 — so a reviewer sees singular and
19
21
  * plural. The key names the sample: `combobox.resultCount(1)`,
20
22
  * `shareCard.removeConfirm(name)`, `serverWake.waking()`.
23
+ * - The legal sections that name the operator (0.28) take one object; its sample is a
24
+ * placeholder per field and the key calls it `operator`:
25
+ * `legal.sections.privacy.controller.body(operator)`. Pass the app's operator in
26
+ * `options` and those rows read the real sentence — same keys — so the lawyer
27
+ * reviewing the `legal` area reads what the page says:
28
+ *
29
+ * kitLabelStrings(UI_KIT_LABELS_DE_CH, { operator: OPERATOR, locale: "de-CH" })
21
30
  *
22
31
  * Keys depend only on the samples, never on the locale, so the rows of `de-CH` and of the
23
32
  * English reference line up key by key. Pass exactly the tree the app hands
@@ -29,16 +38,20 @@ import type { UiKitLabels } from "./kit-labels";
29
38
  * A label that throws on its sample becomes a row starting with "⚠", so one broken
30
39
  * override shows up on the page instead of taking the page down.
31
40
  */
32
- export function kitLabelStrings(labels: UiKitLabels): Record<string, string> {
41
+ export function kitLabelStrings(labels: UiKitLabels, options: KitLabelStringsOptions = {}): Record<string, string> {
33
42
  const out: Record<string, string> = {};
43
+ const operator = options.operator && operatorText(options.operator, options.locale);
34
44
  const walk = (node: unknown, path: string) => {
35
45
  if (typeof node === "string") {
36
46
  out[path] = node;
37
47
  } else if (typeof node === "function") {
38
48
  for (const args of KIT_LABEL_SAMPLES[path] ?? [[]]) {
49
+ // The key is the SAMPLE's (`…body(operator)`), whatever the label is called with,
50
+ // so rows with and without the app's operator join on the same keys.
51
+ const called = operator ? args.map((arg) => (arg === LEGAL_OPERATOR_SAMPLE ? operator : arg)) : args;
39
52
  out[`${path}(${args.map(sampleName).filter((a) => a !== "").join(", ")})`] = render(
40
53
  node as (...args: unknown[]) => unknown,
41
- args,
54
+ called,
42
55
  );
43
56
  }
44
57
  } else if (node !== null && typeof node === "object") {
@@ -49,6 +62,24 @@ export function kitLabelStrings(labels: UiKitLabels): Record<string, string> {
49
62
  return out;
50
63
  }
51
64
 
65
+ export interface KitLabelStringsOptions {
66
+ /**
67
+ * The app's operator (0.28): the legal sections that name it — the imprint's operator
68
+ * and contact, the controller, the privacy contact, the governing law — are rendered
69
+ * with the real values instead of `{{name}}`-style placeholders, so a reviewer (the
70
+ * lawyer, under the `legal` area) reads the sentence the page shows. The keys do not
71
+ * change: `legal.sections.impressum.operator.body(operator)` either way.
72
+ */
73
+ operator?: LegalOperator;
74
+ /** The language the operator's country is named in — the locale these rows are
75
+ * for ("de-CH" → "Schweiz"). Default English. */
76
+ locale?: string;
77
+ }
78
+
79
+ function operatorText(operator: LegalOperator, locale = "en"): LegalOperatorText {
80
+ return { ...operator, country: countryName(operator.country, locale) };
81
+ }
82
+
52
83
  function render(label: (...args: unknown[]) => unknown, args: readonly unknown[]): string {
53
84
  try {
54
85
  return String(label(...args));
@@ -61,12 +92,29 @@ function render(label: (...args: unknown[]) => unknown, args: readonly unknown[]
61
92
  * itself, an empty string as `""`, a missing optional argument not at all. */
62
93
  function sampleName(arg: unknown): string {
63
94
  if (arg === undefined) return "";
95
+ if (arg === LEGAL_OPERATOR_SAMPLE) return "operator";
64
96
  if (typeof arg === "string") return /^\{\{(\w+)\}\}$/.exec(arg)?.[1] ?? JSON.stringify(arg);
65
97
  return String(arg);
66
98
  }
67
99
 
68
100
  const one = [[1], [3]] as const;
69
101
 
102
+ /**
103
+ * The legal sections' operator (0.28) as placeholders, one per field — so a translation
104
+ * that drops the city or the email is caught by the same check as a dropped `{{name}}`
105
+ * anywhere else. Named `operator` in the key; {@link kitLabelStrings}' `operator` option
106
+ * swaps the app's real values in for it.
107
+ */
108
+ const LEGAL_OPERATOR_SAMPLE: LegalOperatorText = Object.freeze({
109
+ name: "{{name}}",
110
+ postalCode: "{{postalCode}}",
111
+ city: "{{city}}",
112
+ region: "{{region}}",
113
+ country: "{{country}}",
114
+ email: "{{email}}",
115
+ });
116
+ const operatorSample = [[LEGAL_OPERATOR_SAMPLE]] as const;
117
+
70
118
  /**
71
119
  * The sample arguments of every function label in {@link UiKitLabels}, by dot path.
72
120
  * `src/i18n/__tests__/review.test.ts` fails when a function label has no entry here, so
@@ -240,4 +288,9 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
240
288
  ],
241
289
  "columnMapper.oneOf": [["{{roles}}"]],
242
290
  "columnMapper.missing": [["{{roles}}"]],
291
+ "legal.sections.impressum.operator.body": operatorSample,
292
+ "legal.sections.impressum.contact.body": operatorSample,
293
+ "legal.sections.privacy.controller.body": operatorSample,
294
+ "legal.sections.privacy.contact.body": operatorSample,
295
+ "legal.sections.terms.law.body": operatorSample,
243
296
  };
package/src/index.ts CHANGED
@@ -36,6 +36,8 @@ export type { UseAnnounceReturn, UseAnnounceOptions, AnnounceRegionProps } from
36
36
  export { useOverlayHistory } from "./hooks/use-overlay-history";
37
37
  export { useCloseTransition, OVERLAY_EXIT_MS } from "./hooks/use-close-transition";
38
38
  export { useRowSwipe } from "./hooks/use-row-swipe";
39
+ // 0.28: keksdose's noindex marker, for the legal and auth pages of every app.
40
+ export { useNoIndex } from "./hooks/use-noindex";
39
41
  export type { SwipeStage, RowSwipeOptions, RowSwipeReturn } from "./hooks/use-row-swipe";
40
42
  // Any element as a file drop target, screened like FileButton (kastlan).
41
43
  export { useFileDrop, dragHasFiles } from "./hooks/use-file-drop";
@@ -379,6 +381,7 @@ export * from "./i18n/kit-labels";
379
381
  export * from "./i18n/defaults";
380
382
  // The kit's words as `key → text` rows for an app's translation review (keksdose).
381
383
  export { kitLabelStrings } from "./i18n/review";
384
+ export type { KitLabelStringsOptions } from "./i18n/review";
382
385
  // The seven languages, how an app resolves one, and the kit catalogue loader for it.
383
386
  export * from "./i18n/languages";
384
387
  // CLDR plural categories for an app's i18next catalogue, filled from `_other`.
@@ -396,6 +399,8 @@ export {
396
399
  reviewStatus,
397
400
  keyInArea,
398
401
  keyInAreas,
402
+ // 0.28: the area a key is grouped under — `kit.legal.*` in `legal` (legal round §7.5).
403
+ reviewAreaOf,
399
404
  translationRows,
400
405
  summariseRows,
401
406
  DEFAULT_TRANSLATION_REVIEW_FILTER,
@@ -455,8 +460,33 @@ export type {
455
460
  TranslationReviewGroupBy,
456
461
  } from "./components/translation-review";
457
462
  // The legal pages' shell (0.19, H10): Imprint, Privacy Policy, Terms in every app.
458
- export { LegalLayout, LegalSection, LegalLinks, DEFAULT_LEGAL_LABELS } from "./components/legal";
459
- export type { LegalLabels, LegalLink, LegalLinksProps, LegalSectionProps, LegalLayoutProps } from "./components/legal";
463
+ // 0.28 (docs/legal-harmonization.md): the shared wording, the page frame, the kit's
464
+ // sections, the public footer and the terms checkbox.
465
+ export { LegalLayout, LegalSection, LegalLinks, LegalFooter, DEFAULT_LEGAL_LABELS } from "./components/legal";
466
+ export type {
467
+ LegalLabels,
468
+ LegalLink,
469
+ LegalLinksProps,
470
+ LegalSectionProps,
471
+ LegalLayoutProps,
472
+ LegalFooterProps,
473
+ LegalOperator,
474
+ LegalOperatorText,
475
+ LegalPageKey,
476
+ LegalTextSectionLabels,
477
+ LegalOperatorSectionLabels,
478
+ LegalFramedSectionLabels,
479
+ } from "./components/legal";
480
+ export { LegalPage, LegalKitSection, LegalAcceptCheckbox, LEGAL_SKELETON } from "./components/legal-page";
481
+ export type {
482
+ LegalPageProps,
483
+ LegalKitSectionProps,
484
+ LegalKitSectionKey,
485
+ LegalAcceptCheckboxProps,
486
+ LegalSkeletonEntry,
487
+ LegalSkeletonPage,
488
+ LegalSectionOwner,
489
+ } from "./components/legal-page";
460
490
 
461
491
  // ── 0.22.0: the inputs round (kastlan's, keksdose's and Kurvenschmiede's audits) ──
462
492
  export * from "./components/checkbox-group";
@@ -210,7 +210,10 @@ export function placeholderTokens(text: string): string[] {
210
210
  const found: string[] = [];
211
211
  for (const m of text.matchAll(PLACEHOLDER_RE)) {
212
212
  if (m[1]) found.push(`{{${m[1]}}}`);
213
- else if (m[2]) found.push(`{${m[2]}}`);
213
+ // `{ref:retention}` (ui-kit 0.28's legal cross-reference) keeps its target: a
214
+ // translation that points at another section changed what the sentence says. Every
215
+ // other `{x:spec}` is a format spec, which a translation may change.
216
+ else if (m[2]) found.push(m[2] === "ref" ? m[0] : `{${m[2]}}`);
214
217
  else if (m[3]) found.push(`<${m[3]}>`);
215
218
  else if (m[4]) found.push(`$t(${m[4]})`);
216
219
  }
@@ -245,10 +248,46 @@ export function reviewStatus(
245
248
  return review.verdict === "APPROVED" ? "approved" : "needs_change";
246
249
  }
247
250
 
248
- /** Whether `key` lies in `area`: the key IS the area, or sits under it — `legal.terms`
249
- * and kastlan's `legal:terms` are in `legal`, `legalese.title` is not. */
251
+ /**
252
+ * Whether `key` lies in `area`: the key IS the area, or sits under it — `legal.terms`
253
+ * and kastlan's `legal:terms` are in `legal`, `legalese.title` is not.
254
+ *
255
+ * 0.28: the kit's own words of that area too — `kit.legal.sections.terms.law.body(…)`
256
+ * is in `legal`. All three apps list the kit's labels under `kit.`
257
+ * (`flattenStrings(kitLabelStrings(labels), "kit.")`), and since 0.28 the kit writes
258
+ * the shared legal sections, so a lawyer with a `["legal"]` grant who was not shown
259
+ * `kit.legal.*` would not see half of the Privacy Policy — and the server would refuse
260
+ * the verdicts (docs/legal-harmonization.md §7.5). server-kit's `in_areas` and the apps'
261
+ * copies of it apply the same rule.
262
+ */
250
263
  export function keyInArea(key: string, area: string): boolean {
251
- return key === area || key.startsWith(`${area}.`) || key.startsWith(`${area}:`);
264
+ return (
265
+ key === area || key.startsWith(`${area}.`) || key.startsWith(`${area}:`) || key.startsWith(`kit.${area}.`)
266
+ );
267
+ }
268
+
269
+ /**
270
+ * The area of `areas` that `key` lies in ({@link keyInArea}), or `undefined` — the
271
+ * translation page's grouping by the same rule the filter and the server use, so
272
+ * `kit.legal.sections.privacy.rights.lead` is grouped under `legal` with the app's own
273
+ * legal texts instead of under `kit.legal`:
274
+ *
275
+ * const namespaceOf = (key: string) => reviewAreaOf(key, ["legal"]) ?? ownNamespaceOf(key);
276
+ *
277
+ * Where two areas both hold the key (`legal` and `legal.privacy`), the longer — the more
278
+ * specific — wins, whatever their order. `inArea` is the app's own rule, as for
279
+ * {@link keyInAreas}.
280
+ */
281
+ export function reviewAreaOf(
282
+ key: string,
283
+ areas: readonly string[],
284
+ inArea: (key: string, area: string) => boolean = keyInArea,
285
+ ): string | undefined {
286
+ let found: string | undefined;
287
+ for (const area of areas) {
288
+ if (inArea(key, area) && (found === undefined || area.length > found.length)) found = area;
289
+ }
290
+ return found;
252
291
  }
253
292
 
254
293
  /**