@eifi1/ui-kit 0.27.0 → 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.
- package/README.md +7 -5
- package/dist/chart.d.ts +1 -2
- package/dist/components/amount-input.d.ts +1 -2
- package/dist/components/button-group.d.ts +1 -2
- package/dist/components/calculator.d.ts +1 -2
- package/dist/components/confirm-dialog.d.ts +1 -2
- package/dist/components/copy-button.d.ts +1 -2
- package/dist/components/danger-confirm.d.ts +1 -2
- package/dist/components/facing-pair.d.ts +1 -2
- package/dist/components/file-button.d.ts +1 -2
- package/dist/components/file-dropzone.d.ts +1 -2
- package/dist/components/form-actions.d.ts +1 -2
- package/dist/components/iban-input.d.ts +1 -2
- package/dist/components/language-select.d.ts +1 -2
- package/dist/components/legal-page.d.ts +261 -0
- package/dist/components/legal-page.js +253 -0
- package/dist/components/legal-page.js.map +1 -0
- package/dist/components/legal.d.ts +70 -84
- package/dist/components/legal.js +190 -19
- package/dist/components/legal.js.map +1 -1
- package/dist/components/money-field.d.ts +1 -2
- package/dist/components/number-field.d.ts +1 -2
- package/dist/components/number-input.d.ts +1 -2
- package/dist/components/numpad-sheet.d.ts +1 -2
- package/dist/components/phone-input.d.ts +1 -2
- package/dist/components/series-chart.d.ts +1 -2
- package/dist/components/settings-fields.d.ts +1 -2
- package/dist/components/share-card.d.ts +1 -2
- package/dist/components/text-link.d.ts +1 -2
- package/dist/components/time-input.d.ts +1 -2
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui.d.ts +1 -2
- package/dist/feedback/feedback-attachment.d.ts +1 -2
- package/dist/feedback/feedback-context.d.ts +1 -2
- package/dist/feedback/feedback-crash.d.ts +1 -2
- package/dist/feedback/feedback-dialog.d.ts +1 -2
- package/dist/feedback/feedback-inbox.d.ts +1 -2
- package/dist/feedback/feedback-labels.d.ts +1 -2
- package/dist/feedback/feedback-menu.d.ts +1 -2
- package/dist/feedback/feedback-record.d.ts +1 -2
- package/dist/feedback/feedback-row-detail.d.ts +1 -2
- package/dist/feedback/feedback-status-undo.d.ts +1 -2
- package/dist/feedback/feedback-submit.d.ts +1 -2
- package/dist/feedback/feedback-swipe.d.ts +1 -2
- package/dist/feedback/feedback-table.d.ts +1 -2
- package/dist/feedback/feedback-thread.d.ts +1 -2
- package/dist/feedback.d.ts +1 -2
- package/dist/hooks/use-file-drop.d.ts +1 -2
- package/dist/hooks/use-noindex.d.ts +26 -0
- package/dist/hooks/use-noindex.js +18 -0
- package/dist/hooks/use-noindex.js.map +1 -0
- package/dist/i18n/defaults.d.ts +1 -2
- package/dist/i18n/german.d.ts +1 -2
- package/dist/i18n/german.js +87 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +1 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +1 -2
- package/dist/i18n/locales/de-CH.d.ts +1 -2
- package/dist/i18n/locales/en.d.ts +1 -2
- package/dist/i18n/locales/es.d.ts +1 -2
- package/dist/i18n/locales/es.js +87 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +1 -2
- package/dist/i18n/locales/fr.js +89 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +1 -2
- package/dist/i18n/locales/hu.js +90 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +1 -2
- package/dist/i18n/locales/it.js +90 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +1 -2
- package/dist/i18n/locales/zh.js +87 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +24 -5
- package/dist/i18n/review.js +24 -3
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-DQg82eHB.d.ts → kit-labels-SBN0biPl.d.ts} +305 -3
- package/dist/lib/translation-review.d.ts +26 -3
- package/dist/lib/translation-review.js +10 -2
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/form.d.ts +1 -2
- package/dist/rhf.d.ts +1 -2
- package/dist/shell/app-shell.d.ts +1 -2
- package/dist/shell/top-bar-brand.d.ts +1 -2
- package/dist/shell.d.ts +1 -2
- package/dist/wizard/stepper-nav.d.ts +1 -2
- package/dist/wizard.d.ts +1 -2
- package/package.json +1 -1
- package/src/components/legal-page.tsx +566 -0
- package/src/components/legal.tsx +458 -49
- package/src/components/translation-review.tsx +2 -1
- package/src/hooks/use-noindex.ts +37 -0
- package/src/i18n/german.ts +81 -0
- package/src/i18n/kit-labels.tsx +4 -1
- package/src/i18n/locales/es.ts +81 -0
- package/src/i18n/locales/fr.ts +83 -0
- package/src/i18n/locales/hu.ts +84 -0
- package/src/i18n/locales/it.ts +84 -0
- package/src/i18n/locales/zh.ts +81 -0
- package/src/i18n/review.ts +56 -3
- package/src/index.ts +32 -2
- package/src/lib/translation-review.ts +43 -4
package/src/i18n/review.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
249
|
-
*
|
|
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
|
|
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
|
/**
|