@eifi1/ui-kit 0.18.0 → 0.20.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 (123) hide show
  1. package/README.md +67 -6
  2. package/dist/chart.d.ts +4 -1
  3. package/dist/components/amount-input.d.ts +4 -1
  4. package/dist/components/button-group.d.ts +4 -1
  5. package/dist/components/calculator.d.ts +4 -1
  6. package/dist/components/copy-button.d.ts +4 -1
  7. package/dist/components/facing-pair.d.ts +4 -1
  8. package/dist/components/file-button.d.ts +4 -1
  9. package/dist/components/file-dropzone.d.ts +4 -1
  10. package/dist/components/floating-panel.d.ts +4 -1
  11. package/dist/components/floating-panel.js +15 -1
  12. package/dist/components/floating-panel.js.map +1 -1
  13. package/dist/components/form-actions.d.ts +4 -1
  14. package/dist/components/legal.d.ts +84 -0
  15. package/dist/components/legal.js +55 -0
  16. package/dist/components/legal.js.map +1 -0
  17. package/dist/components/money-field.d.ts +4 -1
  18. package/dist/components/number-field.d.ts +4 -1
  19. package/dist/components/number-input.d.ts +4 -1
  20. package/dist/components/numpad-sheet.d.ts +4 -1
  21. package/dist/components/series-chart.d.ts +4 -1
  22. package/dist/components/server-wake.d.ts +4 -2
  23. package/dist/components/server-wake.js +23 -2
  24. package/dist/components/server-wake.js.map +1 -1
  25. package/dist/components/settings-fields.d.ts +4 -1
  26. package/dist/components/share-card.d.ts +4 -1
  27. package/dist/components/text-link.d.ts +4 -1
  28. package/dist/components/time-input.d.ts +4 -1
  29. package/dist/components/translation-review-editor.d.ts +56 -0
  30. package/dist/components/translation-review-editor.js +159 -0
  31. package/dist/components/translation-review-editor.js.map +1 -0
  32. package/dist/components/translation-review-labels.d.ts +92 -0
  33. package/dist/components/translation-review-labels.js +67 -0
  34. package/dist/components/translation-review-labels.js.map +1 -0
  35. package/dist/components/translation-review-parts.d.ts +97 -0
  36. package/dist/components/translation-review-parts.js +163 -0
  37. package/dist/components/translation-review-parts.js.map +1 -0
  38. package/dist/components/translation-review.d.ts +94 -0
  39. package/dist/components/translation-review.js +344 -0
  40. package/dist/components/translation-review.js.map +1 -0
  41. package/dist/components/ui.d.ts +4 -1
  42. package/dist/hooks/use-file-drop.d.ts +4 -1
  43. package/dist/i18n/defaults.d.ts +4 -1
  44. package/dist/i18n/defaults.js +5 -1
  45. package/dist/i18n/defaults.js.map +1 -1
  46. package/dist/i18n/german.d.ts +4 -1
  47. package/dist/i18n/german.js +51 -0
  48. package/dist/i18n/german.js.map +1 -1
  49. package/dist/i18n/kit-labels.d.ts +4 -1
  50. package/dist/i18n/kit-labels.js.map +1 -1
  51. package/dist/i18n/languages.d.ts +268 -0
  52. package/dist/i18n/languages.js +112 -0
  53. package/dist/i18n/languages.js.map +1 -0
  54. package/dist/i18n/locales/de-CH.d.ts +4 -1
  55. package/dist/i18n/locales/en.d.ts +4 -1
  56. package/dist/i18n/locales/en.js +8 -0
  57. package/dist/i18n/locales/en.js.map +1 -1
  58. package/dist/i18n/locales/es.d.ts +4 -1
  59. package/dist/i18n/locales/es.js +83 -33
  60. package/dist/i18n/locales/es.js.map +1 -1
  61. package/dist/i18n/locales/fr.d.ts +4 -1
  62. package/dist/i18n/locales/fr.js +50 -0
  63. package/dist/i18n/locales/fr.js.map +1 -1
  64. package/dist/i18n/locales/hu.d.ts +4 -1
  65. package/dist/i18n/locales/hu.js +51 -0
  66. package/dist/i18n/locales/hu.js.map +1 -1
  67. package/dist/i18n/locales/it.d.ts +4 -1
  68. package/dist/i18n/locales/it.js +90 -40
  69. package/dist/i18n/locales/it.js.map +1 -1
  70. package/dist/i18n/locales/zh.d.ts +4 -1
  71. package/dist/i18n/locales/zh.js +51 -0
  72. package/dist/i18n/locales/zh.js.map +1 -1
  73. package/dist/i18n/plurals.d.ts +23 -0
  74. package/dist/i18n/plurals.js +26 -0
  75. package/dist/i18n/plurals.js.map +1 -0
  76. package/dist/i18n/review.d.ts +102 -0
  77. package/dist/i18n/review.js +158 -0
  78. package/dist/i18n/review.js.map +1 -0
  79. package/dist/index.d.ts +10 -1
  80. package/dist/index.js +66 -0
  81. package/dist/index.js.map +1 -1
  82. package/dist/{kit-labels-CPbsjlXr.d.ts → kit-labels-D1J2_jyD.d.ts} +7 -0
  83. package/dist/lib/floating-stack.d.ts +31 -0
  84. package/dist/lib/floating-stack.js +42 -0
  85. package/dist/lib/floating-stack.js.map +1 -0
  86. package/dist/lib/server-wake.d.ts +24 -8
  87. package/dist/lib/server-wake.js +6 -5
  88. package/dist/lib/server-wake.js.map +1 -1
  89. package/dist/lib/translation-review.d.ts +232 -0
  90. package/dist/lib/translation-review.js +194 -0
  91. package/dist/lib/translation-review.js.map +1 -0
  92. package/dist/rhf/fields.d.ts +4 -1
  93. package/dist/rhf/form.d.ts +4 -1
  94. package/dist/rhf.d.ts +4 -1
  95. package/dist/shell/app-shell.d.ts +4 -1
  96. package/dist/shell/top-bar-brand.d.ts +4 -1
  97. package/dist/shell.d.ts +4 -1
  98. package/dist/wizard/stepper-nav.d.ts +4 -1
  99. package/dist/wizard.d.ts +4 -1
  100. package/package.json +1 -1
  101. package/src/components/floating-panel.tsx +19 -1
  102. package/src/components/legal.tsx +159 -0
  103. package/src/components/server-wake.tsx +37 -5
  104. package/src/components/translation-review-editor.tsx +261 -0
  105. package/src/components/translation-review-labels.ts +152 -0
  106. package/src/components/translation-review-parts.tsx +269 -0
  107. package/src/components/translation-review.tsx +502 -0
  108. package/src/i18n/defaults.ts +4 -0
  109. package/src/i18n/german.ts +52 -0
  110. package/src/i18n/kit-labels.tsx +7 -0
  111. package/src/i18n/languages.ts +355 -0
  112. package/src/i18n/locales/en.ts +8 -0
  113. package/src/i18n/locales/es.ts +84 -33
  114. package/src/i18n/locales/fr.ts +51 -0
  115. package/src/i18n/locales/hu.ts +52 -0
  116. package/src/i18n/locales/it.ts +91 -40
  117. package/src/i18n/locales/zh.ts +52 -0
  118. package/src/i18n/plurals.ts +44 -0
  119. package/src/i18n/review.ts +193 -0
  120. package/src/index.ts +65 -0
  121. package/src/lib/floating-stack.ts +66 -0
  122. package/src/lib/server-wake.ts +34 -13
  123. package/src/lib/translation-review.ts +465 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The pure half of an app's translation review (H1 in docs/i18n-harmonization.md): from
3
+ * the strings the app ships and the verdicts its server stores to one row per (locale,
4
+ * key) with a status — keksdose's `features/translations/translation-catalogue.ts` and
5
+ * kastlan's copy of it, which had grown apart by a fifth status and a second kind of key.
6
+ *
7
+ * WHY THE STRINGS ARE THE CATALOGUE. Nothing on the server lists which strings exist: a
8
+ * verdict only says what somebody thought of one. So the rows are built from what the app
9
+ * ships — its locale bundles, kastlan's document texts, the kit's own words
10
+ * (`kitLabelStrings`) — and the review can never offer a string users do not see. A
11
+ * verdict is stored WITH the wording it was given on, and a string that changed since is
12
+ * noticed by comparing the two: `changed`, read it again, whatever the verdict was.
13
+ *
14
+ * WHAT STAYS IN THE APP. Which locale is read against which (keksdose: everything against
15
+ * English, English against de-CH; kastlan: against German, German against English), where
16
+ * the strings come from, who may review what (the locales and areas its server answers
17
+ * with), and every request. The kit takes those as input and never derives them.
18
+ *
19
+ * camelCase, like the rest of the kit. The contract's wire shape is snake_case
20
+ * (`reference_text`, `reviewer_name`, `reviewed_at`); {@link fromApiReview} and
21
+ * {@link toApiWrite} are the two lines each app would otherwise write — both apps speak
22
+ * the same contract, so the mapping is written once, here.
23
+ */
24
+ type TranslationVerdict = "APPROVED" | "NEEDS_CHANGE";
25
+ /** One stored verdict: what a reviewer said about one string in one locale. */
26
+ interface TranslationReview {
27
+ locale: string;
28
+ key: string;
29
+ /** The wording that was judged — compared with today's to notice a change. Empty for
30
+ * a string the locale lacked (a suggested translation for a `missing` row). */
31
+ text: string;
32
+ /** The reference wording at the time. `null` from a server that did not store it. */
33
+ referenceText: string | null;
34
+ verdict: TranslationVerdict;
35
+ note: string | null;
36
+ /** The reviewer's wording — or, for a missing string, the translation to add. */
37
+ suggestion: string | null;
38
+ /** `null` once the reviewer's account has been erased. */
39
+ reviewerName: string | null;
40
+ /** ISO timestamp. */
41
+ reviewedAt: string;
42
+ }
43
+ /** What a save sends: the verdict plus the wording it is given on. */
44
+ interface TranslationReviewWrite {
45
+ locale: string;
46
+ key: string;
47
+ text: string;
48
+ referenceText: string | null;
49
+ verdict: TranslationVerdict;
50
+ note: string | null;
51
+ suggestion: string | null;
52
+ }
53
+ /** What a reset sends: back to "unreviewed". */
54
+ interface TranslationReviewKey {
55
+ locale: string;
56
+ key: string;
57
+ }
58
+ /**
59
+ * Where a string stands, in the order a reviewer works through them: what is not there
60
+ * at all, what nobody has read, what changed after it was read, what was sent back, and
61
+ * what is done. `missing` never occurs for an app whose locales have every key
62
+ * (keksdose); kastlan's French lacks 172 screen texts.
63
+ */
64
+ type ReviewStatus = "missing" | "unreviewed" | "changed" | "needs_change" | "approved";
65
+ declare const REVIEW_STATUSES: readonly ReviewStatus[];
66
+ interface TranslationRow {
67
+ /** `${locale}|${key}` — unique across every locale on a page. A bar, not a colon:
68
+ * kastlan's keys contain colons (`common:actions.save`). */
69
+ id: string;
70
+ key: string;
71
+ /** The key's group: `budget` for `budget.rta`, `common` for `common:actions.save`,
72
+ * `kit` for the kit's own words. See {@link keyNamespace}. */
73
+ namespace: string;
74
+ /** Where the string comes from, in the app's own words — kastlan's `screen` and
75
+ * `documents`, keksdose's one source. `""` when the app does not say. */
76
+ source: string;
77
+ locale: string;
78
+ /** The wording users see today; empty when the locale lacks the string. */
79
+ text: string;
80
+ /** The same key in the reference locale. */
81
+ reference: string;
82
+ status: ReviewStatus;
83
+ review: TranslationReview | null;
84
+ /** The interpolations and tags differ from the reference's — `{{count}}` dropped, a
85
+ * `<1>` link lost, a `{date}` renamed. A machine can check that much, so it does,
86
+ * before a person reads. Never set on a missing string: it has nothing to lose. */
87
+ placeholderMismatch: boolean;
88
+ }
89
+ /** A verdict as `GET /translations/reviews` returns it (H1's shared contract). Typed
90
+ * structurally, so an app's generated OpenAPI type fits without a cast. */
91
+ interface ApiTranslationReview {
92
+ locale: string;
93
+ key: string;
94
+ text: string;
95
+ reference_text: string | null;
96
+ verdict: TranslationVerdict;
97
+ note: string | null;
98
+ suggestion: string | null;
99
+ reviewer_name: string | null;
100
+ reviewed_at: string;
101
+ }
102
+ /** One item of `PUT /translations/reviews`. */
103
+ interface ApiTranslationReviewWrite {
104
+ locale: string;
105
+ key: string;
106
+ text: string;
107
+ reference_text: string | null;
108
+ verdict: TranslationVerdict;
109
+ note: string | null;
110
+ suggestion: string | null;
111
+ }
112
+ /** A verdict from the server, in the kit's spelling. */
113
+ declare function fromApiReview(review: ApiTranslationReview): TranslationReview;
114
+ /** A save, in the server's spelling. `TranslationReviewKey` needs no mapping — it is
115
+ * `{ locale, key }` on both sides. */
116
+ declare function toApiWrite(write: TranslationReviewWrite): ApiTranslationReviewWrite;
117
+ /**
118
+ * A nested bundle as `key → string`, in the bundle's own order. Non-string leaves are
119
+ * skipped rather than stringified into something nobody wrote.
120
+ *
121
+ * `prefix` is put in front of every key AS IS, so it names the separator too:
122
+ * `flattenStrings(bundle)` is keksdose's `budget.rta`, `flattenStrings(tree, "common:")`
123
+ * kastlan's `common:actions.save`, and `flattenStrings(kitLabelStrings(labels), "kit.")`
124
+ * the kit's own words as `kit.combobox.resultCount(3)` — already flat, so only the prefix
125
+ * is added.
126
+ */
127
+ declare function flattenStrings(tree: Readonly<Record<string, unknown>>, prefix?: string): Record<string, string>;
128
+ /**
129
+ * The key's group, for the namespace filter: the part before a colon when there is one
130
+ * (kastlan's `common:actions.save` → `common`, `doc_text.invoice:due` →
131
+ * `doc_text.invoice`), else the first dotted segment (keksdose's `budget.rta` →
132
+ * `budget`, the kit's `kit.combobox.resultCount(3)` → `kit`). Only the part before a
133
+ * sample's `(` is looked at, so a colon inside one cannot move the split.
134
+ */
135
+ declare function keyNamespace(key: string): string;
136
+ /** The placeholders in a string, as a sorted multiset: `{{count}}`, `{date}`, `<1>`,
137
+ * `$t(common.save)`. */
138
+ declare function placeholderTokens(text: string): string[];
139
+ /** Whether `text` has other placeholders than `reference` — dropped, renamed or added.
140
+ * Moving one is fine: a translation reorders a sentence. */
141
+ declare function placeholderMismatch(text: string, reference: string): boolean;
142
+ /**
143
+ * A verdict counts only for the wording it was given on. If the string or its reference
144
+ * changed since, it is `changed` — read it again — whatever the verdict was. A string the
145
+ * locale lacks is `missing` until somebody suggests its translation (stored as
146
+ * NEEDS_CHANGE on the empty text, so it reads `needs_change`, and `changed` once a
147
+ * developer adds the string).
148
+ */
149
+ declare function reviewStatus(review: TranslationReview | null | undefined, text: string, reference: string): ReviewStatus;
150
+ /** Whether `key` lies in `area`: the key IS the area, or sits under it — `legal.terms`
151
+ * and kastlan's `legal:terms` are in `legal`, `legalese.title` is not. */
152
+ declare function keyInArea(key: string, area: string): boolean;
153
+ /**
154
+ * Whether `key` lies in one of `areas` — the scope a reviewer may be limited to
155
+ * (keksdose: `["legal"]`, a lawyer who judges the Imprint, the Privacy Policy and the
156
+ * Terms and sees nothing else). `null`/`undefined` is every area. The server applies the
157
+ * same rule and refuses verdicts outside; this decides only what is listed.
158
+ */
159
+ declare function keyInAreas(key: string, areas: readonly string[] | null | undefined, inArea?: (key: string, area: string) => boolean): boolean;
160
+ interface TranslationRowsInput {
161
+ locale: string;
162
+ /** The locale's strings, `key → text` ({@link flattenStrings}). */
163
+ strings: Readonly<Record<string, string>>;
164
+ /** The reference locale's strings — whichever locale THIS app reads `locale` against. */
165
+ reference: Readonly<Record<string, string>>;
166
+ /** The verdicts: this locale's by key, or the whole list as the server sent it (other
167
+ * locales' verdicts are skipped). */
168
+ reviews?: ReadonlyMap<string, TranslationReview> | readonly TranslationReview[];
169
+ /** Default {@link keyNamespace}. */
170
+ namespaceOf?: (key: string) => string;
171
+ /** The row's source, an app-defined word (kastlan: `doc_text.` keys are `documents`,
172
+ * the rest `screen`). Default: none (`""`). */
173
+ sourceOf?: (key: string) => string;
174
+ /** Keep only keys inside these areas ({@link keyInAreas}); `null` keeps every key. */
175
+ areas?: readonly string[] | null;
176
+ /** Default {@link keyInArea}. */
177
+ inArea?: (key: string, area: string) => boolean;
178
+ }
179
+ /**
180
+ * Every key of one locale, with its status: the reference's keys first, in its order, so
181
+ * rows read the way the app's authors wrote them; a key only this locale has is appended
182
+ * rather than dropped (a parity test should forbid it, but the page should not lie if one
183
+ * slips through).
184
+ */
185
+ declare function translationRows({ locale, strings, reference, reviews, namespaceOf: groupOf, sourceOf, areas, inArea, }: TranslationRowsInput): TranslationRow[];
186
+ /** How many rows stand where, and how many there are. */
187
+ type TranslationSummary = Record<ReviewStatus, number> & {
188
+ total: number;
189
+ };
190
+ declare function summariseRows(rows: readonly TranslationRow[]): TranslationSummary;
191
+ /** The panel's filters. `"all"` switches one off. */
192
+ interface TranslationReviewFilter {
193
+ status: ReviewStatus | "all";
194
+ source: string;
195
+ namespace: string;
196
+ /** Searched in the key, the text and the reference, case-insensitively. */
197
+ query: string;
198
+ placeholdersOnly: boolean;
199
+ }
200
+ declare const DEFAULT_TRANSLATION_REVIEW_FILTER: TranslationReviewFilter;
201
+ declare function filterTranslationRows(rows: readonly TranslationRow[], filter: Partial<TranslationReviewFilter>): TranslationRow[];
202
+ /** What gets stored for a verdict: the wording it was given on, so a later change of the
203
+ * string (or of its reference) shows up as `changed`. */
204
+ declare function reviewWrite(row: TranslationRow, verdict: TranslationVerdict, note?: string | null, suggestion?: string | null): TranslationReviewWrite;
205
+ /** The app's cached verdict list with `written` in place of what was said before — a
206
+ * save answers with the rows it wrote, so a page patches its copy instead of
207
+ * downloading every verdict again after each click. Any spelling with `locale` and
208
+ * `key` (the API's or the kit's). */
209
+ declare function mergeReviews<T extends TranslationReviewKey>(prev: readonly T[], written: readonly T[]): T[];
210
+ /** The cached list without the verdicts on `cleared`. */
211
+ declare function dropReviews<T extends TranslationReviewKey>(prev: readonly T[], cleared: readonly TranslationReviewKey[]): T[];
212
+ /**
213
+ * One entry of the "Export corrections" file — enough for a developer to apply the change
214
+ * to the bundle without opening the page. snake_case `reviewed_at` on purpose: this is a
215
+ * FILE, the one keksdose and kastlan already write, beside an API spelled that way.
216
+ */
217
+ interface TranslationCorrection {
218
+ locale: string;
219
+ key: string;
220
+ status: ReviewStatus;
221
+ current: string;
222
+ reference: string;
223
+ suggestion: string | null;
224
+ note: string | null;
225
+ reviewer: string | null;
226
+ reviewed_at: string | null;
227
+ }
228
+ /** The rows sent back (verdict NEEDS_CHANGE — a suggested translation for a missing
229
+ * string included), as the export file lists them. */
230
+ declare function translationCorrections(rows: readonly TranslationRow[]): TranslationCorrection[];
231
+
232
+ export { type ApiTranslationReview, type ApiTranslationReviewWrite, DEFAULT_TRANSLATION_REVIEW_FILTER, REVIEW_STATUSES, type ReviewStatus, type TranslationCorrection, type TranslationReview, type TranslationReviewFilter, type TranslationReviewKey, type TranslationReviewWrite, type TranslationRow, type TranslationRowsInput, type TranslationSummary, type TranslationVerdict, dropReviews, filterTranslationRows, flattenStrings, fromApiReview, keyInArea, keyInAreas, keyNamespace, mergeReviews, placeholderMismatch, placeholderTokens, reviewStatus, reviewWrite, summariseRows, toApiWrite, translationCorrections, translationRows };
@@ -0,0 +1,194 @@
1
+ "use client";
2
+ const REVIEW_STATUSES = [
3
+ "missing",
4
+ "unreviewed",
5
+ "changed",
6
+ "needs_change",
7
+ "approved"
8
+ ];
9
+ function fromApiReview(review) {
10
+ return {
11
+ locale: review.locale,
12
+ key: review.key,
13
+ text: review.text,
14
+ referenceText: review.reference_text,
15
+ verdict: review.verdict,
16
+ note: review.note,
17
+ suggestion: review.suggestion,
18
+ reviewerName: review.reviewer_name,
19
+ reviewedAt: review.reviewed_at
20
+ };
21
+ }
22
+ function toApiWrite(write) {
23
+ return {
24
+ locale: write.locale,
25
+ key: write.key,
26
+ text: write.text,
27
+ reference_text: write.referenceText,
28
+ verdict: write.verdict,
29
+ note: write.note,
30
+ suggestion: write.suggestion
31
+ };
32
+ }
33
+ function flattenStrings(tree, prefix = "") {
34
+ const out = {};
35
+ for (const [key, value] of Object.entries(tree)) {
36
+ const path = `${prefix}${key}`;
37
+ if (typeof value === "string") out[path] = value;
38
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
39
+ Object.assign(out, flattenStrings(value, `${path}.`));
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ function keyNamespace(key) {
45
+ const head = key.split("(", 1)[0];
46
+ const colon = head.indexOf(":");
47
+ return colon >= 0 ? head.slice(0, colon) : head.split(".", 1)[0];
48
+ }
49
+ const PLACEHOLDER_RE = /\{\{\s*([^}\s,]+)[^}]*\}\}|(?<![\\A-Za-z])\{([a-z_][a-z0-9_]*|\d+)(?:[!:][^{}]*)?\}|<\/?(\d+|[a-z][a-z0-9]*)\s*\/?>|\$t\(([^)]+)\)/g;
50
+ function placeholderTokens(text) {
51
+ const found = [];
52
+ for (const m of text.matchAll(PLACEHOLDER_RE)) {
53
+ if (m[1]) found.push(`{{${m[1]}}}`);
54
+ else if (m[2]) found.push(`{${m[2]}}`);
55
+ else if (m[3]) found.push(`<${m[3]}>`);
56
+ else if (m[4]) found.push(`$t(${m[4]})`);
57
+ }
58
+ return found.sort();
59
+ }
60
+ function placeholderMismatch(text, reference) {
61
+ const a = placeholderTokens(text);
62
+ const b = placeholderTokens(reference);
63
+ return a.length !== b.length || a.some((p, i) => p !== b[i]);
64
+ }
65
+ function reviewStatus(review, text, reference) {
66
+ if (!review) return text === "" && reference !== "" ? "missing" : "unreviewed";
67
+ if (review.text !== text) return "changed";
68
+ if (review.referenceText != null && review.referenceText !== reference) return "changed";
69
+ return review.verdict === "APPROVED" ? "approved" : "needs_change";
70
+ }
71
+ function keyInArea(key, area) {
72
+ return key === area || key.startsWith(`${area}.`) || key.startsWith(`${area}:`);
73
+ }
74
+ function keyInAreas(key, areas, inArea = keyInArea) {
75
+ return !areas || areas.some((area) => inArea(key, area));
76
+ }
77
+ function translationRows({
78
+ locale,
79
+ strings,
80
+ reference,
81
+ reviews,
82
+ namespaceOf: groupOf = keyNamespace,
83
+ sourceOf,
84
+ areas,
85
+ inArea
86
+ }) {
87
+ const byKey = /* @__PURE__ */ new Map();
88
+ if (reviews instanceof Map) {
89
+ for (const [key, review] of reviews) byKey.set(key, review);
90
+ } else if (reviews) {
91
+ for (const review of reviews) {
92
+ if (review.locale === locale) byKey.set(review.key, review);
93
+ }
94
+ }
95
+ const keys = Object.keys(reference);
96
+ for (const key of Object.keys(strings)) if (!(key in reference)) keys.push(key);
97
+ return keys.filter((key) => keyInAreas(key, areas, inArea)).map((key) => {
98
+ const text = strings[key] ?? "";
99
+ const ref = reference[key] ?? "";
100
+ const review = byKey.get(key) ?? null;
101
+ return {
102
+ id: `${locale}|${key}`,
103
+ key,
104
+ namespace: groupOf(key),
105
+ source: sourceOf?.(key) ?? "",
106
+ locale,
107
+ text,
108
+ reference: ref,
109
+ status: reviewStatus(review, text, ref),
110
+ review,
111
+ placeholderMismatch: text !== "" && placeholderMismatch(text, ref)
112
+ };
113
+ });
114
+ }
115
+ function summariseRows(rows) {
116
+ const summary = {
117
+ missing: 0,
118
+ unreviewed: 0,
119
+ changed: 0,
120
+ needs_change: 0,
121
+ approved: 0,
122
+ total: rows.length
123
+ };
124
+ for (const row of rows) summary[row.status] += 1;
125
+ return summary;
126
+ }
127
+ const DEFAULT_TRANSLATION_REVIEW_FILTER = {
128
+ status: "all",
129
+ source: "all",
130
+ namespace: "all",
131
+ query: "",
132
+ placeholdersOnly: false
133
+ };
134
+ function filterTranslationRows(rows, filter) {
135
+ const f = { ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...filter };
136
+ const q = f.query.trim().toLowerCase();
137
+ return rows.filter(
138
+ (row) => (f.status === "all" || row.status === f.status) && (f.source === "all" || row.source === f.source) && (f.namespace === "all" || row.namespace === f.namespace) && (!f.placeholdersOnly || row.placeholderMismatch) && (!q || row.key.toLowerCase().includes(q) || row.text.toLowerCase().includes(q) || row.reference.toLowerCase().includes(q))
139
+ );
140
+ }
141
+ function reviewWrite(row, verdict, note = null, suggestion = null) {
142
+ return {
143
+ locale: row.locale,
144
+ key: row.key,
145
+ text: row.text,
146
+ referenceText: row.reference,
147
+ verdict,
148
+ note,
149
+ suggestion
150
+ };
151
+ }
152
+ function mergeReviews(prev, written) {
153
+ const id = (r) => `${r.locale}|${r.key}`;
154
+ const replaced = new Set(written.map(id));
155
+ return [...prev.filter((r) => !replaced.has(id(r))), ...written];
156
+ }
157
+ function dropReviews(prev, cleared) {
158
+ const gone = new Set(cleared.map((r) => `${r.locale}|${r.key}`));
159
+ return prev.filter((r) => !gone.has(`${r.locale}|${r.key}`));
160
+ }
161
+ function translationCorrections(rows) {
162
+ return rows.filter((row) => row.review?.verdict === "NEEDS_CHANGE").map((row) => ({
163
+ locale: row.locale,
164
+ key: row.key,
165
+ status: row.status,
166
+ current: row.text,
167
+ reference: row.reference,
168
+ suggestion: row.review?.suggestion ?? null,
169
+ note: row.review?.note ?? null,
170
+ reviewer: row.review?.reviewerName ?? null,
171
+ reviewed_at: row.review?.reviewedAt ?? null
172
+ }));
173
+ }
174
+ export {
175
+ DEFAULT_TRANSLATION_REVIEW_FILTER,
176
+ REVIEW_STATUSES,
177
+ dropReviews,
178
+ filterTranslationRows,
179
+ flattenStrings,
180
+ fromApiReview,
181
+ keyInArea,
182
+ keyInAreas,
183
+ keyNamespace,
184
+ mergeReviews,
185
+ placeholderMismatch,
186
+ placeholderTokens,
187
+ reviewStatus,
188
+ reviewWrite,
189
+ summariseRows,
190
+ toApiWrite,
191
+ translationCorrections,
192
+ translationRows
193
+ };
194
+ //# sourceMappingURL=translation-review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/translation-review.ts"],"sourcesContent":["/**\n * The pure half of an app's translation review (H1 in docs/i18n-harmonization.md): from\n * the strings the app ships and the verdicts its server stores to one row per (locale,\n * key) with a status — keksdose's `features/translations/translation-catalogue.ts` and\n * kastlan's copy of it, which had grown apart by a fifth status and a second kind of key.\n *\n * WHY THE STRINGS ARE THE CATALOGUE. Nothing on the server lists which strings exist: a\n * verdict only says what somebody thought of one. So the rows are built from what the app\n * ships — its locale bundles, kastlan's document texts, the kit's own words\n * (`kitLabelStrings`) — and the review can never offer a string users do not see. A\n * verdict is stored WITH the wording it was given on, and a string that changed since is\n * noticed by comparing the two: `changed`, read it again, whatever the verdict was.\n *\n * WHAT STAYS IN THE APP. Which locale is read against which (keksdose: everything against\n * English, English against de-CH; kastlan: against German, German against English), where\n * the strings come from, who may review what (the locales and areas its server answers\n * with), and every request. The kit takes those as input and never derives them.\n *\n * camelCase, like the rest of the kit. The contract's wire shape is snake_case\n * (`reference_text`, `reviewer_name`, `reviewed_at`); {@link fromApiReview} and\n * {@link toApiWrite} are the two lines each app would otherwise write — both apps speak\n * the same contract, so the mapping is written once, here.\n */\n\n// ── Shapes ──────────────────────────────────────────────────────────────────────\n\nexport type TranslationVerdict = \"APPROVED\" | \"NEEDS_CHANGE\";\n\n/** One stored verdict: what a reviewer said about one string in one locale. */\nexport interface TranslationReview {\n locale: string;\n key: string;\n /** The wording that was judged — compared with today's to notice a change. Empty for\n * a string the locale lacked (a suggested translation for a `missing` row). */\n text: string;\n /** The reference wording at the time. `null` from a server that did not store it. */\n referenceText: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n /** The reviewer's wording — or, for a missing string, the translation to add. */\n suggestion: string | null;\n /** `null` once the reviewer's account has been erased. */\n reviewerName: string | null;\n /** ISO timestamp. */\n reviewedAt: string;\n}\n\n/** What a save sends: the verdict plus the wording it is given on. */\nexport interface TranslationReviewWrite {\n locale: string;\n key: string;\n text: string;\n referenceText: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n}\n\n/** What a reset sends: back to \"unreviewed\". */\nexport interface TranslationReviewKey {\n locale: string;\n key: string;\n}\n\n/**\n * Where a string stands, in the order a reviewer works through them: what is not there\n * at all, what nobody has read, what changed after it was read, what was sent back, and\n * what is done. `missing` never occurs for an app whose locales have every key\n * (keksdose); kastlan's French lacks 172 screen texts.\n */\nexport type ReviewStatus = \"missing\" | \"unreviewed\" | \"changed\" | \"needs_change\" | \"approved\";\n\nexport const REVIEW_STATUSES: readonly ReviewStatus[] = [\n \"missing\",\n \"unreviewed\",\n \"changed\",\n \"needs_change\",\n \"approved\",\n];\n\nexport interface TranslationRow {\n /** `${locale}|${key}` — unique across every locale on a page. A bar, not a colon:\n * kastlan's keys contain colons (`common:actions.save`). */\n id: string;\n key: string;\n /** The key's group: `budget` for `budget.rta`, `common` for `common:actions.save`,\n * `kit` for the kit's own words. See {@link keyNamespace}. */\n namespace: string;\n /** Where the string comes from, in the app's own words — kastlan's `screen` and\n * `documents`, keksdose's one source. `\"\"` when the app does not say. */\n source: string;\n locale: string;\n /** The wording users see today; empty when the locale lacks the string. */\n text: string;\n /** The same key in the reference locale. */\n reference: string;\n status: ReviewStatus;\n review: TranslationReview | null;\n /** The interpolations and tags differ from the reference's — `{{count}}` dropped, a\n * `<1>` link lost, a `{date}` renamed. A machine can check that much, so it does,\n * before a person reads. Never set on a missing string: it has nothing to lose. */\n placeholderMismatch: boolean;\n}\n\n// ── The wire contract ───────────────────────────────────────────────────────────\n\n/** A verdict as `GET /translations/reviews` returns it (H1's shared contract). Typed\n * structurally, so an app's generated OpenAPI type fits without a cast. */\nexport interface ApiTranslationReview {\n locale: string;\n key: string;\n text: string;\n reference_text: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n reviewer_name: string | null;\n reviewed_at: string;\n}\n\n/** One item of `PUT /translations/reviews`. */\nexport interface ApiTranslationReviewWrite {\n locale: string;\n key: string;\n text: string;\n reference_text: string | null;\n verdict: TranslationVerdict;\n note: string | null;\n suggestion: string | null;\n}\n\n/** A verdict from the server, in the kit's spelling. */\nexport function fromApiReview(review: ApiTranslationReview): TranslationReview {\n return {\n locale: review.locale,\n key: review.key,\n text: review.text,\n referenceText: review.reference_text,\n verdict: review.verdict,\n note: review.note,\n suggestion: review.suggestion,\n reviewerName: review.reviewer_name,\n reviewedAt: review.reviewed_at,\n };\n}\n\n/** A save, in the server's spelling. `TranslationReviewKey` needs no mapping — it is\n * `{ locale, key }` on both sides. */\nexport function toApiWrite(write: TranslationReviewWrite): ApiTranslationReviewWrite {\n return {\n locale: write.locale,\n key: write.key,\n text: write.text,\n reference_text: write.referenceText,\n verdict: write.verdict,\n note: write.note,\n suggestion: write.suggestion,\n };\n}\n\n// ── Strings ─────────────────────────────────────────────────────────────────────\n\n/**\n * A nested bundle as `key → string`, in the bundle's own order. Non-string leaves are\n * skipped rather than stringified into something nobody wrote.\n *\n * `prefix` is put in front of every key AS IS, so it names the separator too:\n * `flattenStrings(bundle)` is keksdose's `budget.rta`, `flattenStrings(tree, \"common:\")`\n * kastlan's `common:actions.save`, and `flattenStrings(kitLabelStrings(labels), \"kit.\")`\n * the kit's own words as `kit.combobox.resultCount(3)` — already flat, so only the prefix\n * is added.\n */\nexport function flattenStrings(tree: Readonly<Record<string, unknown>>, prefix = \"\"): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(tree)) {\n const path = `${prefix}${key}`;\n if (typeof value === \"string\") out[path] = value;\n else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n Object.assign(out, flattenStrings(value as Record<string, unknown>, `${path}.`));\n }\n }\n return out;\n}\n\n/**\n * The key's group, for the namespace filter: the part before a colon when there is one\n * (kastlan's `common:actions.save` → `common`, `doc_text.invoice:due` →\n * `doc_text.invoice`), else the first dotted segment (keksdose's `budget.rta` →\n * `budget`, the kit's `kit.combobox.resultCount(3)` → `kit`). Only the part before a\n * sample's `(` is looked at, so a colon inside one cannot move the split.\n */\nexport function keyNamespace(key: string): string {\n const head = key.split(\"(\", 1)[0];\n const colon = head.indexOf(\":\");\n return colon >= 0 ? head.slice(0, colon) : head.split(\".\", 1)[0];\n}\n\n// i18next `{{x}}` (formatting options ignored: `{{x, number}}` is `{{x}}`); a Python\n// format field `{x}` / `{0}` / `{amount:.2f}` (kastlan's document texts), but not a\n// LaTeX argument (`\\textbf{x}` — a letter or backslash right before the brace); a\n// numbered or named tag (`<1>`, `<strong>`), not prose in angle brackets (\"<deleted\n// user>\"); and a nested `$t(key)`. `{{x}}` is tried first at its position and consumed\n// whole, so it is never counted a second time as `{x}`.\nconst PLACEHOLDER_RE =\n /\\{\\{\\s*([^}\\s,]+)[^}]*\\}\\}|(?<![\\\\A-Za-z])\\{([a-z_][a-z0-9_]*|\\d+)(?:[!:][^{}]*)?\\}|<\\/?(\\d+|[a-z][a-z0-9]*)\\s*\\/?>|\\$t\\(([^)]+)\\)/g;\n\n/** The placeholders in a string, as a sorted multiset: `{{count}}`, `{date}`, `<1>`,\n * `$t(common.save)`. */\nexport function placeholderTokens(text: string): string[] {\n const found: string[] = [];\n for (const m of text.matchAll(PLACEHOLDER_RE)) {\n if (m[1]) found.push(`{{${m[1]}}}`);\n else if (m[2]) found.push(`{${m[2]}}`);\n else if (m[3]) found.push(`<${m[3]}>`);\n else if (m[4]) found.push(`$t(${m[4]})`);\n }\n return found.sort();\n}\n\n/** Whether `text` has other placeholders than `reference` — dropped, renamed or added.\n * Moving one is fine: a translation reorders a sentence. */\nexport function placeholderMismatch(text: string, reference: string): boolean {\n const a = placeholderTokens(text);\n const b = placeholderTokens(reference);\n return a.length !== b.length || a.some((p, i) => p !== b[i]);\n}\n\n// ── Status ──────────────────────────────────────────────────────────────────────\n\n/**\n * A verdict counts only for the wording it was given on. If the string or its reference\n * changed since, it is `changed` — read it again — whatever the verdict was. A string the\n * locale lacks is `missing` until somebody suggests its translation (stored as\n * NEEDS_CHANGE on the empty text, so it reads `needs_change`, and `changed` once a\n * developer adds the string).\n */\nexport function reviewStatus(\n review: TranslationReview | null | undefined,\n text: string,\n reference: string,\n): ReviewStatus {\n if (!review) return text === \"\" && reference !== \"\" ? \"missing\" : \"unreviewed\";\n if (review.text !== text) return \"changed\";\n if (review.referenceText != null && review.referenceText !== reference) return \"changed\";\n return review.verdict === \"APPROVED\" ? \"approved\" : \"needs_change\";\n}\n\n/** Whether `key` lies in `area`: the key IS the area, or sits under it — `legal.terms`\n * and kastlan's `legal:terms` are in `legal`, `legalese.title` is not. */\nexport function keyInArea(key: string, area: string): boolean {\n return key === area || key.startsWith(`${area}.`) || key.startsWith(`${area}:`);\n}\n\n/**\n * Whether `key` lies in one of `areas` — the scope a reviewer may be limited to\n * (keksdose: `[\"legal\"]`, a lawyer who judges the Imprint, the Privacy Policy and the\n * Terms and sees nothing else). `null`/`undefined` is every area. The server applies the\n * same rule and refuses verdicts outside; this decides only what is listed.\n */\nexport function keyInAreas(\n key: string,\n areas: readonly string[] | null | undefined,\n inArea: (key: string, area: string) => boolean = keyInArea,\n): boolean {\n return !areas || areas.some((area) => inArea(key, area));\n}\n\nexport interface TranslationRowsInput {\n locale: string;\n /** The locale's strings, `key → text` ({@link flattenStrings}). */\n strings: Readonly<Record<string, string>>;\n /** The reference locale's strings — whichever locale THIS app reads `locale` against. */\n reference: Readonly<Record<string, string>>;\n /** The verdicts: this locale's by key, or the whole list as the server sent it (other\n * locales' verdicts are skipped). */\n reviews?: ReadonlyMap<string, TranslationReview> | readonly TranslationReview[];\n /** Default {@link keyNamespace}. */\n namespaceOf?: (key: string) => string;\n /** The row's source, an app-defined word (kastlan: `doc_text.` keys are `documents`,\n * the rest `screen`). Default: none (`\"\"`). */\n sourceOf?: (key: string) => string;\n /** Keep only keys inside these areas ({@link keyInAreas}); `null` keeps every key. */\n areas?: readonly string[] | null;\n /** Default {@link keyInArea}. */\n inArea?: (key: string, area: string) => boolean;\n}\n\n/**\n * Every key of one locale, with its status: the reference's keys first, in its order, so\n * rows read the way the app's authors wrote them; a key only this locale has is appended\n * rather than dropped (a parity test should forbid it, but the page should not lie if one\n * slips through).\n */\nexport function translationRows({\n locale,\n strings,\n reference,\n reviews,\n namespaceOf: groupOf = keyNamespace,\n sourceOf,\n areas,\n inArea,\n}: TranslationRowsInput): TranslationRow[] {\n const byKey = new Map<string, TranslationReview>();\n if (reviews instanceof Map) {\n for (const [key, review] of reviews) byKey.set(key, review);\n } else if (reviews) {\n for (const review of reviews as readonly TranslationReview[]) {\n if (review.locale === locale) byKey.set(review.key, review);\n }\n }\n const keys = Object.keys(reference);\n for (const key of Object.keys(strings)) if (!(key in reference)) keys.push(key);\n return keys\n .filter((key) => keyInAreas(key, areas, inArea))\n .map((key) => {\n const text = strings[key] ?? \"\";\n const ref = reference[key] ?? \"\";\n const review = byKey.get(key) ?? null;\n return {\n id: `${locale}|${key}`,\n key,\n namespace: groupOf(key),\n source: sourceOf?.(key) ?? \"\",\n locale,\n text,\n reference: ref,\n status: reviewStatus(review, text, ref),\n review,\n placeholderMismatch: text !== \"\" && placeholderMismatch(text, ref),\n };\n });\n}\n\n/** How many rows stand where, and how many there are. */\nexport type TranslationSummary = Record<ReviewStatus, number> & { total: number };\n\nexport function summariseRows(rows: readonly TranslationRow[]): TranslationSummary {\n const summary: TranslationSummary = {\n missing: 0,\n unreviewed: 0,\n changed: 0,\n needs_change: 0,\n approved: 0,\n total: rows.length,\n };\n for (const row of rows) summary[row.status] += 1;\n return summary;\n}\n\n// ── Filtering ───────────────────────────────────────────────────────────────────\n\n/** The panel's filters. `\"all\"` switches one off. */\nexport interface TranslationReviewFilter {\n status: ReviewStatus | \"all\";\n source: string;\n namespace: string;\n /** Searched in the key, the text and the reference, case-insensitively. */\n query: string;\n placeholdersOnly: boolean;\n}\n\nexport const DEFAULT_TRANSLATION_REVIEW_FILTER: TranslationReviewFilter = {\n status: \"all\",\n source: \"all\",\n namespace: \"all\",\n query: \"\",\n placeholdersOnly: false,\n};\n\nexport function filterTranslationRows(\n rows: readonly TranslationRow[],\n filter: Partial<TranslationReviewFilter>,\n): TranslationRow[] {\n const f = { ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...filter };\n const q = f.query.trim().toLowerCase();\n return rows.filter(\n (row) =>\n (f.status === \"all\" || row.status === f.status) &&\n (f.source === \"all\" || row.source === f.source) &&\n (f.namespace === \"all\" || row.namespace === f.namespace) &&\n (!f.placeholdersOnly || row.placeholderMismatch) &&\n (!q ||\n row.key.toLowerCase().includes(q) ||\n row.text.toLowerCase().includes(q) ||\n row.reference.toLowerCase().includes(q)),\n );\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────────────\n\n/** What gets stored for a verdict: the wording it was given on, so a later change of the\n * string (or of its reference) shows up as `changed`. */\nexport function reviewWrite(\n row: TranslationRow,\n verdict: TranslationVerdict,\n note: string | null = null,\n suggestion: string | null = null,\n): TranslationReviewWrite {\n return {\n locale: row.locale,\n key: row.key,\n text: row.text,\n referenceText: row.reference,\n verdict,\n note,\n suggestion,\n };\n}\n\n/** The app's cached verdict list with `written` in place of what was said before — a\n * save answers with the rows it wrote, so a page patches its copy instead of\n * downloading every verdict again after each click. Any spelling with `locale` and\n * `key` (the API's or the kit's). */\nexport function mergeReviews<T extends TranslationReviewKey>(prev: readonly T[], written: readonly T[]): T[] {\n const id = (r: TranslationReviewKey) => `${r.locale}|${r.key}`;\n const replaced = new Set(written.map(id));\n return [...prev.filter((r) => !replaced.has(id(r))), ...written];\n}\n\n/** The cached list without the verdicts on `cleared`. */\nexport function dropReviews<T extends TranslationReviewKey>(\n prev: readonly T[],\n cleared: readonly TranslationReviewKey[],\n): T[] {\n const gone = new Set(cleared.map((r) => `${r.locale}|${r.key}`));\n return prev.filter((r) => !gone.has(`${r.locale}|${r.key}`));\n}\n\n// ── Export ──────────────────────────────────────────────────────────────────────\n\n/**\n * One entry of the \"Export corrections\" file — enough for a developer to apply the change\n * to the bundle without opening the page. snake_case `reviewed_at` on purpose: this is a\n * FILE, the one keksdose and kastlan already write, beside an API spelled that way.\n */\nexport interface TranslationCorrection {\n locale: string;\n key: string;\n status: ReviewStatus;\n current: string;\n reference: string;\n suggestion: string | null;\n note: string | null;\n reviewer: string | null;\n reviewed_at: string | null;\n}\n\n/** The rows sent back (verdict NEEDS_CHANGE — a suggested translation for a missing\n * string included), as the export file lists them. */\nexport function translationCorrections(rows: readonly TranslationRow[]): TranslationCorrection[] {\n return rows\n .filter((row) => row.review?.verdict === \"NEEDS_CHANGE\")\n .map((row) => ({\n locale: row.locale,\n key: row.key,\n status: row.status,\n current: row.text,\n reference: row.reference,\n suggestion: row.review?.suggestion ?? null,\n note: row.review?.note ?? null,\n reviewer: row.review?.reviewerName ?? null,\n reviewed_at: row.review?.reviewedAt ?? null,\n }));\n}\n"],"mappings":";AAwEO,MAAM,kBAA2C;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAsDO,SAAS,cAAc,QAAiD;AAC7E,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,IACZ,MAAM,OAAO;AAAA,IACb,eAAe,OAAO;AAAA,IACtB,SAAS,OAAO;AAAA,IAChB,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,IACnB,cAAc,OAAO;AAAA,IACrB,YAAY,OAAO;AAAA,EACrB;AACF;AAIO,SAAS,WAAW,OAA0D;AACnF,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,KAAK,MAAM;AAAA,IACX,MAAM,MAAM;AAAA,IACZ,gBAAgB,MAAM;AAAA,IACtB,SAAS,MAAM;AAAA,IACf,MAAM,MAAM;AAAA,IACZ,YAAY,MAAM;AAAA,EACpB;AACF;AAcO,SAAS,eAAe,MAAyC,SAAS,IAA4B;AAC3G,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,UAAM,OAAO,GAAG,MAAM,GAAG,GAAG;AAC5B,QAAI,OAAO,UAAU,SAAU,KAAI,IAAI,IAAI;AAAA,aAClC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACpE,aAAO,OAAO,KAAK,eAAe,OAAkC,GAAG,IAAI,GAAG,CAAC;AAAA,IACjF;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,aAAa,KAAqB;AAChD,QAAM,OAAO,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;AAChC,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,SAAO,SAAS,IAAI,KAAK,MAAM,GAAG,KAAK,IAAI,KAAK,MAAM,KAAK,CAAC,EAAE,CAAC;AACjE;AAQA,MAAM,iBACJ;AAIK,SAAS,kBAAkB,MAAwB;AACxD,QAAM,QAAkB,CAAC;AACzB,aAAW,KAAK,KAAK,SAAS,cAAc,GAAG;AAC7C,QAAI,EAAE,CAAC,EAAG,OAAM,KAAK,KAAK,EAAE,CAAC,CAAC,IAAI;AAAA,aACzB,EAAE,CAAC,EAAG,OAAM,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG;AAAA,aAC5B,EAAE,CAAC,EAAG,OAAM,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG;AAAA,aAC5B,EAAE,CAAC,EAAG,OAAM,KAAK,MAAM,EAAE,CAAC,CAAC,GAAG;AAAA,EACzC;AACA,SAAO,MAAM,KAAK;AACpB;AAIO,SAAS,oBAAoB,MAAc,WAA4B;AAC5E,QAAM,IAAI,kBAAkB,IAAI;AAChC,QAAM,IAAI,kBAAkB,SAAS;AACrC,SAAO,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,CAAC,GAAG,MAAM,MAAM,EAAE,CAAC,CAAC;AAC7D;AAWO,SAAS,aACd,QACA,MACA,WACc;AACd,MAAI,CAAC,OAAQ,QAAO,SAAS,MAAM,cAAc,KAAK,YAAY;AAClE,MAAI,OAAO,SAAS,KAAM,QAAO;AACjC,MAAI,OAAO,iBAAiB,QAAQ,OAAO,kBAAkB,UAAW,QAAO;AAC/E,SAAO,OAAO,YAAY,aAAa,aAAa;AACtD;AAIO,SAAS,UAAU,KAAa,MAAuB;AAC5D,SAAO,QAAQ,QAAQ,IAAI,WAAW,GAAG,IAAI,GAAG,KAAK,IAAI,WAAW,GAAG,IAAI,GAAG;AAChF;AAQO,SAAS,WACd,KACA,OACA,SAAiD,WACxC;AACT,SAAO,CAAC,SAAS,MAAM,KAAK,CAAC,SAAS,OAAO,KAAK,IAAI,CAAC;AACzD;AA4BO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa,UAAU;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AACF,GAA2C;AACzC,QAAM,QAAQ,oBAAI,IAA+B;AACjD,MAAI,mBAAmB,KAAK;AAC1B,eAAW,CAAC,KAAK,MAAM,KAAK,QAAS,OAAM,IAAI,KAAK,MAAM;AAAA,EAC5D,WAAW,SAAS;AAClB,eAAW,UAAU,SAAyC;AAC5D,UAAI,OAAO,WAAW,OAAQ,OAAM,IAAI,OAAO,KAAK,MAAM;AAAA,IAC5D;AAAA,EACF;AACA,QAAM,OAAO,OAAO,KAAK,SAAS;AAClC,aAAW,OAAO,OAAO,KAAK,OAAO,EAAG,KAAI,EAAE,OAAO,WAAY,MAAK,KAAK,GAAG;AAC9E,SAAO,KACJ,OAAO,CAAC,QAAQ,WAAW,KAAK,OAAO,MAAM,CAAC,EAC9C,IAAI,CAAC,QAAQ;AACZ,UAAM,OAAO,QAAQ,GAAG,KAAK;AAC7B,UAAM,MAAM,UAAU,GAAG,KAAK;AAC9B,UAAM,SAAS,MAAM,IAAI,GAAG,KAAK;AACjC,WAAO;AAAA,MACL,IAAI,GAAG,MAAM,IAAI,GAAG;AAAA,MACpB;AAAA,MACA,WAAW,QAAQ,GAAG;AAAA,MACtB,QAAQ,WAAW,GAAG,KAAK;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX,QAAQ,aAAa,QAAQ,MAAM,GAAG;AAAA,MACtC;AAAA,MACA,qBAAqB,SAAS,MAAM,oBAAoB,MAAM,GAAG;AAAA,IACnE;AAAA,EACF,CAAC;AACL;AAKO,SAAS,cAAc,MAAqD;AACjF,QAAM,UAA8B;AAAA,IAClC,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,cAAc;AAAA,IACd,UAAU;AAAA,IACV,OAAO,KAAK;AAAA,EACd;AACA,aAAW,OAAO,KAAM,SAAQ,IAAI,MAAM,KAAK;AAC/C,SAAO;AACT;AAcO,MAAM,oCAA6D;AAAA,EACxE,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,OAAO;AAAA,EACP,kBAAkB;AACpB;AAEO,SAAS,sBACd,MACA,QACkB;AAClB,QAAM,IAAI,EAAE,GAAG,mCAAmC,GAAG,OAAO;AAC5D,QAAM,IAAI,EAAE,MAAM,KAAK,EAAE,YAAY;AACrC,SAAO,KAAK;AAAA,IACV,CAAC,SACE,EAAE,WAAW,SAAS,IAAI,WAAW,EAAE,YACvC,EAAE,WAAW,SAAS,IAAI,WAAW,EAAE,YACvC,EAAE,cAAc,SAAS,IAAI,cAAc,EAAE,eAC7C,CAAC,EAAE,oBAAoB,IAAI,yBAC3B,CAAC,KACA,IAAI,IAAI,YAAY,EAAE,SAAS,CAAC,KAChC,IAAI,KAAK,YAAY,EAAE,SAAS,CAAC,KACjC,IAAI,UAAU,YAAY,EAAE,SAAS,CAAC;AAAA,EAC5C;AACF;AAMO,SAAS,YACd,KACA,SACA,OAAsB,MACtB,aAA4B,MACJ;AACxB,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,KAAK,IAAI;AAAA,IACT,MAAM,IAAI;AAAA,IACV,eAAe,IAAI;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAMO,SAAS,aAA6C,MAAoB,SAA4B;AAC3G,QAAM,KAAK,CAAC,MAA4B,GAAG,EAAE,MAAM,IAAI,EAAE,GAAG;AAC5D,QAAM,WAAW,IAAI,IAAI,QAAQ,IAAI,EAAE,CAAC;AACxC,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,OAAO;AACjE;AAGO,SAAS,YACd,MACA,SACK;AACL,QAAM,OAAO,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC;AAC/D,SAAO,KAAK,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,EAAE,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC;AAC7D;AAuBO,SAAS,uBAAuB,MAA0D;AAC/F,SAAO,KACJ,OAAO,CAAC,QAAQ,IAAI,QAAQ,YAAY,cAAc,EACtD,IAAI,CAAC,SAAS;AAAA,IACb,QAAQ,IAAI;AAAA,IACZ,KAAK,IAAI;AAAA,IACT,QAAQ,IAAI;AAAA,IACZ,SAAS,IAAI;AAAA,IACb,WAAW,IAAI;AAAA,IACf,YAAY,IAAI,QAAQ,cAAc;AAAA,IACtC,MAAM,IAAI,QAAQ,QAAQ;AAAA,IAC1B,UAAU,IAAI,QAAQ,gBAAgB;AAAA,IACtC,aAAa,IAAI,QAAQ,cAAc;AAAA,EACzC,EAAE;AACN;","names":[]}
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ReactElement } from 'react';
3
3
  import { FieldValues, FieldPath, ControllerProps, ControllerRenderProps, ControllerFieldState } from 'react-hook-form';
4
- import { aQ as SelectProps, aE as InputProps, b9 as TextareaProps } from '../kit-labels-CPbsjlXr.js';
4
+ import { aQ as SelectProps, aE as InputProps, b9 as TextareaProps } from '../kit-labels-D1J2_jyD.js';
5
5
  import { NumberFieldProps } from '../components/number-field.js';
6
6
  import { DatePickerProps } from '../components/date-picker.js';
7
7
  import { CheckboxProps } from '../components/checkbox.js';
@@ -59,6 +59,7 @@ import '../hooks/use-authed-src.js';
59
59
  import '../components/image-grid.js';
60
60
  import '../components/lightbox.js';
61
61
  import '../components/write-lock.js';
62
+ import '../components/legal.js';
62
63
  import '../components/account-chips.js';
63
64
  import '../lib/format.js';
64
65
  import '../data-table-3mi2R3so.js';
@@ -67,6 +68,8 @@ import '../components/swipeable-row.js';
67
68
  import '../components/reauth-dialog.js';
68
69
  import '../components/server-wake.js';
69
70
  import '../lib/server-wake.js';
71
+ import '../components/translation-review-labels.js';
72
+ import '../lib/translation-review.js';
70
73
  import '../components/number-input.js';
71
74
  import '../components/calculator.js';
72
75
  import '../components/numpad-sheet.js';
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { ReactElement, ComponentProps } from 'react';
3
3
  import * as react_hook_form from 'react-hook-form';
4
4
  import { FieldValues, FieldPath, ControllerProps } from 'react-hook-form';
5
- import { aJ as LabelProps } from '../kit-labels-CPbsjlXr.js';
5
+ import { aJ as LabelProps } from '../kit-labels-D1J2_jyD.js';
6
6
  import '../components/data-table-labels.js';
7
7
  import '../components/mini-calendar.js';
8
8
  import '../components/calendar-heatmap.js';
@@ -54,6 +54,7 @@ import '../hooks/use-authed-src.js';
54
54
  import '../components/image-grid.js';
55
55
  import '../components/lightbox.js';
56
56
  import '../components/write-lock.js';
57
+ import '../components/legal.js';
57
58
  import '../components/account-chips.js';
58
59
  import '../lib/format.js';
59
60
  import '../data-table-3mi2R3so.js';
@@ -62,6 +63,8 @@ import '../components/swipeable-row.js';
62
63
  import '../components/reauth-dialog.js';
63
64
  import '../components/server-wake.js';
64
65
  import '../lib/server-wake.js';
66
+ import '../components/translation-review-labels.js';
67
+ import '../lib/translation-review.js';
65
68
 
66
69
  /** The form's `useForm()` return, spread in: `<Form {...form}>`. react-hook-form's own
67
70
  * `FormProvider`, under the name the shadcn file gave it. */
package/dist/rhf.d.ts CHANGED
@@ -4,7 +4,7 @@ export { RhfCheckbox, RhfCheckboxProps, RhfCombobox, RhfComboboxProps, RhfDateFi
4
4
  export { RhfLineItemCellContext, RhfLineItems, RhfLineItemsColumn, RhfLineItemsProps } from './rhf/line-items.js';
5
5
  import 'react';
6
6
  import 'react-hook-form';
7
- import './kit-labels-CPbsjlXr.js';
7
+ import './kit-labels-D1J2_jyD.js';
8
8
  import './components/data-table-labels.js';
9
9
  import './components/mini-calendar.js';
10
10
  import './components/calendar-heatmap.js';
@@ -56,6 +56,7 @@ import './hooks/use-authed-src.js';
56
56
  import './components/image-grid.js';
57
57
  import './components/lightbox.js';
58
58
  import './components/write-lock.js';
59
+ import './components/legal.js';
59
60
  import './components/account-chips.js';
60
61
  import './lib/format.js';
61
62
  import './data-table-3mi2R3so.js';
@@ -64,6 +65,8 @@ import './components/swipeable-row.js';
64
65
  import './components/reauth-dialog.js';
65
66
  import './components/server-wake.js';
66
67
  import './lib/server-wake.js';
68
+ import './components/translation-review-labels.js';
69
+ import './lib/translation-review.js';
67
70
  import './components/number-field.js';
68
71
  import './components/number-input.js';
69
72
  import './components/calculator.js';
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
  import { LucideIcon } from 'lucide-react';
4
- import { aF as KitLinkComponent } from '../kit-labels-CPbsjlXr.js';
4
+ import { aF as KitLinkComponent } from '../kit-labels-D1J2_jyD.js';
5
5
  import '../components/data-table-labels.js';
6
6
  import '../components/mini-calendar.js';
7
7
  import '../components/calendar-heatmap.js';
@@ -52,6 +52,7 @@ import '../hooks/use-authed-src.js';
52
52
  import '../components/image-grid.js';
53
53
  import '../components/lightbox.js';
54
54
  import '../components/write-lock.js';
55
+ import '../components/legal.js';
55
56
  import '../components/account-chips.js';
56
57
  import '../lib/format.js';
57
58
  import '../data-table-3mi2R3so.js';
@@ -60,6 +61,8 @@ import '../components/swipeable-row.js';
60
61
  import '../components/reauth-dialog.js';
61
62
  import '../components/server-wake.js';
62
63
  import '../lib/server-wake.js';
64
+ import '../components/translation-review-labels.js';
65
+ import '../lib/translation-review.js';
63
66
 
64
67
  interface AppShellSubItem {
65
68
  to: string;
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { aG as KitLinkProps, aF as KitLinkComponent } from '../kit-labels-CPbsjlXr.js';
3
+ import { aG as KitLinkProps, aF as KitLinkComponent } from '../kit-labels-D1J2_jyD.js';
4
4
  import '../components/data-table-labels.js';
5
5
  import '../components/mini-calendar.js';
6
6
  import '../components/calendar-heatmap.js';
@@ -52,6 +52,7 @@ import '../hooks/use-authed-src.js';
52
52
  import '../components/image-grid.js';
53
53
  import '../components/lightbox.js';
54
54
  import '../components/write-lock.js';
55
+ import '../components/legal.js';
55
56
  import '../components/account-chips.js';
56
57
  import '../lib/format.js';
57
58
  import '../data-table-3mi2R3so.js';
@@ -60,6 +61,8 @@ import '../components/swipeable-row.js';
60
61
  import '../components/reauth-dialog.js';
61
62
  import '../components/server-wake.js';
62
63
  import '../lib/server-wake.js';
64
+ import '../components/translation-review-labels.js';
65
+ import '../lib/translation-review.js';
63
66
 
64
67
  interface TopBarBrandProps extends Omit<KitLinkProps, "href" | "children"> {
65
68
  /** The brand mark — an SVG, sized by the caller or `h-6 w-auto` by default. */
package/dist/shell.d.ts CHANGED
@@ -13,7 +13,7 @@ import './theme/theme-store.js';
13
13
  import 'zustand/middleware';
14
14
  import 'zustand';
15
15
  import 'lucide-react';
16
- import './kit-labels-CPbsjlXr.js';
16
+ import './kit-labels-D1J2_jyD.js';
17
17
  import './components/data-table-labels.js';
18
18
  import './components/mini-calendar.js';
19
19
  import './components/calendar-heatmap.js';
@@ -64,6 +64,7 @@ import './hooks/use-authed-src.js';
64
64
  import './components/image-grid.js';
65
65
  import './components/lightbox.js';
66
66
  import './components/write-lock.js';
67
+ import './components/legal.js';
67
68
  import './components/account-chips.js';
68
69
  import './lib/format.js';
69
70
  import './data-table-3mi2R3so.js';
@@ -72,6 +73,8 @@ import './components/swipeable-row.js';
72
73
  import './components/reauth-dialog.js';
73
74
  import './components/server-wake.js';
74
75
  import './lib/server-wake.js';
76
+ import './components/translation-review-labels.js';
77
+ import './lib/translation-review.js';
75
78
  import './components/menu-item.js';
76
79
  import './components/user-avatar.js';
77
80
  import './components/status-dot.js';