@eifi1/ui-kit 0.17.0 → 0.19.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 (210) hide show
  1. package/README.md +83 -4
  2. package/dist/chart.d.ts +13 -1
  3. package/dist/components/account-chips.d.ts +153 -0
  4. package/dist/components/account-chips.js +135 -0
  5. package/dist/components/account-chips.js.map +1 -0
  6. package/dist/components/account-settings-labels.d.ts +10 -2
  7. package/dist/components/account-settings-labels.js +3 -0
  8. package/dist/components/account-settings-labels.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +13 -1
  10. package/dist/components/button-group.d.ts +13 -1
  11. package/dist/components/calculator.d.ts +13 -1
  12. package/dist/components/chip.d.ts +25 -1
  13. package/dist/components/chip.js +143 -6
  14. package/dist/components/chip.js.map +1 -1
  15. package/dist/components/confirm-dialog.d.ts +42 -1
  16. package/dist/components/confirm-dialog.js +36 -5
  17. package/dist/components/confirm-dialog.js.map +1 -1
  18. package/dist/components/copy-button.d.ts +13 -1
  19. package/dist/components/danger-confirm.d.ts +55 -4
  20. package/dist/components/danger-confirm.js +67 -30
  21. package/dist/components/danger-confirm.js.map +1 -1
  22. package/dist/components/data-table-cells.d.ts +1 -1
  23. package/dist/components/data-table-filter-popover.d.ts +1 -1
  24. package/dist/components/data-table-filters.d.ts +1 -1
  25. package/dist/components/data-table.d.ts +1 -1
  26. package/dist/components/data-table.js +2 -0
  27. package/dist/components/data-table.js.map +1 -1
  28. package/dist/components/facing-pair.d.ts +13 -1
  29. package/dist/components/file-button.d.ts +13 -1
  30. package/dist/components/file-dropzone.d.ts +13 -1
  31. package/dist/components/floating-panel.d.ts +4 -1
  32. package/dist/components/floating-panel.js +15 -1
  33. package/dist/components/floating-panel.js.map +1 -1
  34. package/dist/components/form-actions.d.ts +13 -1
  35. package/dist/components/form-actions.js +3 -0
  36. package/dist/components/form-actions.js.map +1 -1
  37. package/dist/components/legal.d.ts +84 -0
  38. package/dist/components/legal.js +55 -0
  39. package/dist/components/legal.js.map +1 -0
  40. package/dist/components/money-field.d.ts +13 -1
  41. package/dist/components/number-field.d.ts +14 -2
  42. package/dist/components/number-field.js +23 -22
  43. package/dist/components/number-field.js.map +1 -1
  44. package/dist/components/number-input.d.ts +13 -1
  45. package/dist/components/numpad-sheet.d.ts +13 -1
  46. package/dist/components/passkeys-setting.d.ts +27 -1
  47. package/dist/components/passkeys-setting.js +31 -5
  48. package/dist/components/passkeys-setting.js.map +1 -1
  49. package/dist/components/progress-bar.d.ts +15 -1
  50. package/dist/components/progress-bar.js +8 -1
  51. package/dist/components/progress-bar.js.map +1 -1
  52. package/dist/components/reauth-dialog.d.ts +90 -0
  53. package/dist/components/reauth-dialog.js +158 -0
  54. package/dist/components/reauth-dialog.js.map +1 -0
  55. package/dist/components/series-chart.d.ts +13 -1
  56. package/dist/components/server-wake.d.ts +60 -0
  57. package/dist/components/server-wake.js +79 -0
  58. package/dist/components/server-wake.js.map +1 -0
  59. package/dist/components/settings-fields.d.ts +13 -1
  60. package/dist/components/share-card.d.ts +64 -0
  61. package/dist/components/share-card.js +350 -0
  62. package/dist/components/share-card.js.map +1 -0
  63. package/dist/components/status-dot.d.ts +2 -0
  64. package/dist/components/text-link.d.ts +13 -1
  65. package/dist/components/time-input.d.ts +13 -1
  66. package/dist/components/toast.d.ts +10 -1
  67. package/dist/components/toast.js +42 -2
  68. package/dist/components/toast.js.map +1 -1
  69. package/dist/components/toggle-group.js +5 -2
  70. package/dist/components/toggle-group.js.map +1 -1
  71. package/dist/components/translation-review-editor.d.ts +56 -0
  72. package/dist/components/translation-review-editor.js +159 -0
  73. package/dist/components/translation-review-editor.js.map +1 -0
  74. package/dist/components/translation-review-labels.d.ts +92 -0
  75. package/dist/components/translation-review-labels.js +67 -0
  76. package/dist/components/translation-review-labels.js.map +1 -0
  77. package/dist/components/translation-review-parts.d.ts +97 -0
  78. package/dist/components/translation-review-parts.js +163 -0
  79. package/dist/components/translation-review-parts.js.map +1 -0
  80. package/dist/components/translation-review.d.ts +94 -0
  81. package/dist/components/translation-review.js +344 -0
  82. package/dist/components/translation-review.js.map +1 -0
  83. package/dist/components/ui.d.ts +13 -1
  84. package/dist/components/ui.js +43 -28
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/components/use-table-state.d.ts +1 -1
  87. package/dist/components/user-avatar.d.ts +2 -0
  88. package/dist/components/write-lock.d.ts +68 -0
  89. package/dist/components/write-lock.js +33 -0
  90. package/dist/components/write-lock.js.map +1 -0
  91. package/dist/{data-table-CpkWUJBY.d.ts → data-table-3mi2R3so.d.ts} +19 -1
  92. package/dist/data-table.d.ts +1 -1
  93. package/dist/hooks/use-file-drop.d.ts +13 -1
  94. package/dist/hooks/use-overlay-history.js +16 -0
  95. package/dist/hooks/use-overlay-history.js.map +1 -1
  96. package/dist/i18n/defaults.d.ts +13 -1
  97. package/dist/i18n/defaults.js +15 -1
  98. package/dist/i18n/defaults.js.map +1 -1
  99. package/dist/i18n/german.d.ts +81 -0
  100. package/dist/i18n/{locales/de.js → german.js} +110 -9
  101. package/dist/i18n/german.js.map +1 -0
  102. package/dist/i18n/kit-labels.d.ts +13 -1
  103. package/dist/i18n/kit-labels.js +5 -0
  104. package/dist/i18n/kit-labels.js.map +1 -1
  105. package/dist/i18n/languages.d.ts +243 -0
  106. package/dist/i18n/languages.js +70 -0
  107. package/dist/i18n/languages.js.map +1 -0
  108. package/dist/i18n/locales/de-CH.d.ts +26 -8
  109. package/dist/i18n/locales/de-CH.js +7 -3
  110. package/dist/i18n/locales/de-CH.js.map +1 -1
  111. package/dist/i18n/locales/en.d.ts +13 -1
  112. package/dist/i18n/locales/en.js +8 -0
  113. package/dist/i18n/locales/en.js.map +1 -1
  114. package/dist/i18n/locales/es.d.ts +13 -1
  115. package/dist/i18n/locales/es.js +127 -27
  116. package/dist/i18n/locales/es.js.map +1 -1
  117. package/dist/i18n/locales/fr.d.ts +13 -1
  118. package/dist/i18n/locales/fr.js +101 -1
  119. package/dist/i18n/locales/fr.js.map +1 -1
  120. package/dist/i18n/locales/hu.d.ts +13 -1
  121. package/dist/i18n/locales/hu.js +105 -1
  122. package/dist/i18n/locales/hu.js.map +1 -1
  123. package/dist/i18n/locales/it.d.ts +13 -1
  124. package/dist/i18n/locales/it.js +134 -34
  125. package/dist/i18n/locales/it.js.map +1 -1
  126. package/dist/i18n/locales/zh.d.ts +13 -1
  127. package/dist/i18n/locales/zh.js +101 -1
  128. package/dist/i18n/locales/zh.js.map +1 -1
  129. package/dist/i18n/review.d.ts +102 -0
  130. package/dist/i18n/review.js +158 -0
  131. package/dist/i18n/review.js.map +1 -0
  132. package/dist/i18n/swiss.d.ts +3 -3
  133. package/dist/i18n/swiss.js.map +1 -1
  134. package/dist/index.d.ts +17 -4
  135. package/dist/index.js +97 -1
  136. package/dist/index.js.map +1 -1
  137. package/dist/{kit-labels-C4uB_zmg.d.ts → kit-labels-D1J2_jyD.d.ts} +236 -9
  138. package/dist/lib/floating-stack.d.ts +31 -0
  139. package/dist/lib/floating-stack.js +42 -0
  140. package/dist/lib/floating-stack.js.map +1 -0
  141. package/dist/lib/server-wake.d.ts +146 -0
  142. package/dist/lib/server-wake.js +122 -0
  143. package/dist/lib/server-wake.js.map +1 -0
  144. package/dist/lib/translation-review.d.ts +232 -0
  145. package/dist/lib/translation-review.js +194 -0
  146. package/dist/lib/translation-review.js.map +1 -0
  147. package/dist/rhf/fields.d.ts +13 -1
  148. package/dist/rhf/form.d.ts +13 -1
  149. package/dist/rhf.d.ts +13 -1
  150. package/dist/search/global-search.d.ts +3 -1
  151. package/dist/search/global-search.js.map +1 -1
  152. package/dist/shell/app-shell.d.ts +13 -1
  153. package/dist/shell/top-bar-brand.d.ts +13 -1
  154. package/dist/shell/topbar-action-menu.d.ts +2 -0
  155. package/dist/shell.d.ts +13 -1
  156. package/dist/wizard/stepper-nav.d.ts +13 -1
  157. package/dist/wizard.d.ts +13 -1
  158. package/package.json +1 -1
  159. package/src/components/account-chips.tsx +302 -0
  160. package/src/components/account-settings-labels.ts +16 -2
  161. package/src/components/chip.tsx +239 -6
  162. package/src/components/confirm-dialog.tsx +82 -7
  163. package/src/components/danger-confirm.tsx +118 -33
  164. package/src/components/data-table.tsx +20 -0
  165. package/src/components/floating-panel.tsx +19 -1
  166. package/src/components/form-actions.tsx +22 -3
  167. package/src/components/legal.tsx +159 -0
  168. package/src/components/number-field.tsx +32 -26
  169. package/src/components/passkeys-setting.tsx +68 -7
  170. package/src/components/progress-bar.tsx +21 -1
  171. package/src/components/reauth-dialog.tsx +273 -0
  172. package/src/components/server-wake.tsx +156 -0
  173. package/src/components/share-card.tsx +615 -0
  174. package/src/components/toast.tsx +72 -2
  175. package/src/components/toggle-group.tsx +5 -2
  176. package/src/components/translation-review-editor.tsx +261 -0
  177. package/src/components/translation-review-labels.ts +152 -0
  178. package/src/components/translation-review-parts.tsx +269 -0
  179. package/src/components/translation-review.tsx +502 -0
  180. package/src/components/ui.tsx +89 -32
  181. package/src/components/write-lock.tsx +96 -0
  182. package/src/hooks/use-overlay-history.ts +51 -0
  183. package/src/i18n/defaults.ts +14 -0
  184. package/src/i18n/{locales/de.ts → german.ts} +120 -13
  185. package/src/i18n/kit-labels.tsx +29 -2
  186. package/src/i18n/languages.ts +285 -0
  187. package/src/i18n/locales/de-CH.ts +17 -8
  188. package/src/i18n/locales/en.ts +8 -0
  189. package/src/i18n/locales/es.ts +128 -26
  190. package/src/i18n/locales/fr.ts +103 -0
  191. package/src/i18n/locales/hu.ts +107 -0
  192. package/src/i18n/locales/it.ts +135 -33
  193. package/src/i18n/locales/zh.ts +102 -0
  194. package/src/i18n/review.ts +193 -0
  195. package/src/i18n/swiss.ts +3 -3
  196. package/src/index.ts +86 -1
  197. package/src/lib/floating-stack.ts +66 -0
  198. package/src/lib/server-wake.ts +291 -0
  199. package/src/lib/translation-review.ts +465 -0
  200. package/src/search/global-search.tsx +3 -1
  201. package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
  202. package/dist/i18n/locales/de-CH-informal.js +0 -8
  203. package/dist/i18n/locales/de-CH-informal.js.map +0 -1
  204. package/dist/i18n/locales/de-informal.d.ts +0 -82
  205. package/dist/i18n/locales/de-informal.js +0 -37
  206. package/dist/i18n/locales/de-informal.js.map +0 -1
  207. package/dist/i18n/locales/de.d.ts +0 -66
  208. package/dist/i18n/locales/de.js.map +0 -1
  209. package/src/i18n/locales/de-CH-informal.ts +0 -10
  210. package/src/i18n/locales/de-informal.ts +0 -61
@@ -0,0 +1,465 @@
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
+
25
+ // ── Shapes ──────────────────────────────────────────────────────────────────────
26
+
27
+ export type TranslationVerdict = "APPROVED" | "NEEDS_CHANGE";
28
+
29
+ /** One stored verdict: what a reviewer said about one string in one locale. */
30
+ export interface TranslationReview {
31
+ locale: string;
32
+ key: string;
33
+ /** The wording that was judged — compared with today's to notice a change. Empty for
34
+ * a string the locale lacked (a suggested translation for a `missing` row). */
35
+ text: string;
36
+ /** The reference wording at the time. `null` from a server that did not store it. */
37
+ referenceText: string | null;
38
+ verdict: TranslationVerdict;
39
+ note: string | null;
40
+ /** The reviewer's wording — or, for a missing string, the translation to add. */
41
+ suggestion: string | null;
42
+ /** `null` once the reviewer's account has been erased. */
43
+ reviewerName: string | null;
44
+ /** ISO timestamp. */
45
+ reviewedAt: string;
46
+ }
47
+
48
+ /** What a save sends: the verdict plus the wording it is given on. */
49
+ export interface TranslationReviewWrite {
50
+ locale: string;
51
+ key: string;
52
+ text: string;
53
+ referenceText: string | null;
54
+ verdict: TranslationVerdict;
55
+ note: string | null;
56
+ suggestion: string | null;
57
+ }
58
+
59
+ /** What a reset sends: back to "unreviewed". */
60
+ export interface TranslationReviewKey {
61
+ locale: string;
62
+ key: string;
63
+ }
64
+
65
+ /**
66
+ * Where a string stands, in the order a reviewer works through them: what is not there
67
+ * at all, what nobody has read, what changed after it was read, what was sent back, and
68
+ * what is done. `missing` never occurs for an app whose locales have every key
69
+ * (keksdose); kastlan's French lacks 172 screen texts.
70
+ */
71
+ export type ReviewStatus = "missing" | "unreviewed" | "changed" | "needs_change" | "approved";
72
+
73
+ export const REVIEW_STATUSES: readonly ReviewStatus[] = [
74
+ "missing",
75
+ "unreviewed",
76
+ "changed",
77
+ "needs_change",
78
+ "approved",
79
+ ];
80
+
81
+ export interface TranslationRow {
82
+ /** `${locale}|${key}` — unique across every locale on a page. A bar, not a colon:
83
+ * kastlan's keys contain colons (`common:actions.save`). */
84
+ id: string;
85
+ key: string;
86
+ /** The key's group: `budget` for `budget.rta`, `common` for `common:actions.save`,
87
+ * `kit` for the kit's own words. See {@link keyNamespace}. */
88
+ namespace: string;
89
+ /** Where the string comes from, in the app's own words — kastlan's `screen` and
90
+ * `documents`, keksdose's one source. `""` when the app does not say. */
91
+ source: string;
92
+ locale: string;
93
+ /** The wording users see today; empty when the locale lacks the string. */
94
+ text: string;
95
+ /** The same key in the reference locale. */
96
+ reference: string;
97
+ status: ReviewStatus;
98
+ review: TranslationReview | null;
99
+ /** The interpolations and tags differ from the reference's — `{{count}}` dropped, a
100
+ * `<1>` link lost, a `{date}` renamed. A machine can check that much, so it does,
101
+ * before a person reads. Never set on a missing string: it has nothing to lose. */
102
+ placeholderMismatch: boolean;
103
+ }
104
+
105
+ // ── The wire contract ───────────────────────────────────────────────────────────
106
+
107
+ /** A verdict as `GET /translations/reviews` returns it (H1's shared contract). Typed
108
+ * structurally, so an app's generated OpenAPI type fits without a cast. */
109
+ export interface ApiTranslationReview {
110
+ locale: string;
111
+ key: string;
112
+ text: string;
113
+ reference_text: string | null;
114
+ verdict: TranslationVerdict;
115
+ note: string | null;
116
+ suggestion: string | null;
117
+ reviewer_name: string | null;
118
+ reviewed_at: string;
119
+ }
120
+
121
+ /** One item of `PUT /translations/reviews`. */
122
+ export interface ApiTranslationReviewWrite {
123
+ locale: string;
124
+ key: string;
125
+ text: string;
126
+ reference_text: string | null;
127
+ verdict: TranslationVerdict;
128
+ note: string | null;
129
+ suggestion: string | null;
130
+ }
131
+
132
+ /** A verdict from the server, in the kit's spelling. */
133
+ export function fromApiReview(review: ApiTranslationReview): TranslationReview {
134
+ return {
135
+ locale: review.locale,
136
+ key: review.key,
137
+ text: review.text,
138
+ referenceText: review.reference_text,
139
+ verdict: review.verdict,
140
+ note: review.note,
141
+ suggestion: review.suggestion,
142
+ reviewerName: review.reviewer_name,
143
+ reviewedAt: review.reviewed_at,
144
+ };
145
+ }
146
+
147
+ /** A save, in the server's spelling. `TranslationReviewKey` needs no mapping — it is
148
+ * `{ locale, key }` on both sides. */
149
+ export function toApiWrite(write: TranslationReviewWrite): ApiTranslationReviewWrite {
150
+ return {
151
+ locale: write.locale,
152
+ key: write.key,
153
+ text: write.text,
154
+ reference_text: write.referenceText,
155
+ verdict: write.verdict,
156
+ note: write.note,
157
+ suggestion: write.suggestion,
158
+ };
159
+ }
160
+
161
+ // ── Strings ─────────────────────────────────────────────────────────────────────
162
+
163
+ /**
164
+ * A nested bundle as `key → string`, in the bundle's own order. Non-string leaves are
165
+ * skipped rather than stringified into something nobody wrote.
166
+ *
167
+ * `prefix` is put in front of every key AS IS, so it names the separator too:
168
+ * `flattenStrings(bundle)` is keksdose's `budget.rta`, `flattenStrings(tree, "common:")`
169
+ * kastlan's `common:actions.save`, and `flattenStrings(kitLabelStrings(labels), "kit.")`
170
+ * the kit's own words as `kit.combobox.resultCount(3)` — already flat, so only the prefix
171
+ * is added.
172
+ */
173
+ export function flattenStrings(tree: Readonly<Record<string, unknown>>, prefix = ""): Record<string, string> {
174
+ const out: Record<string, string> = {};
175
+ for (const [key, value] of Object.entries(tree)) {
176
+ const path = `${prefix}${key}`;
177
+ if (typeof value === "string") out[path] = value;
178
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
179
+ Object.assign(out, flattenStrings(value as Record<string, unknown>, `${path}.`));
180
+ }
181
+ }
182
+ return out;
183
+ }
184
+
185
+ /**
186
+ * The key's group, for the namespace filter: the part before a colon when there is one
187
+ * (kastlan's `common:actions.save` → `common`, `doc_text.invoice:due` →
188
+ * `doc_text.invoice`), else the first dotted segment (keksdose's `budget.rta` →
189
+ * `budget`, the kit's `kit.combobox.resultCount(3)` → `kit`). Only the part before a
190
+ * sample's `(` is looked at, so a colon inside one cannot move the split.
191
+ */
192
+ export function keyNamespace(key: string): string {
193
+ const head = key.split("(", 1)[0];
194
+ const colon = head.indexOf(":");
195
+ return colon >= 0 ? head.slice(0, colon) : head.split(".", 1)[0];
196
+ }
197
+
198
+ // i18next `{{x}}` (formatting options ignored: `{{x, number}}` is `{{x}}`); a Python
199
+ // format field `{x}` / `{0}` / `{amount:.2f}` (kastlan's document texts), but not a
200
+ // LaTeX argument (`\textbf{x}` — a letter or backslash right before the brace); a
201
+ // numbered or named tag (`<1>`, `<strong>`), not prose in angle brackets ("<deleted
202
+ // user>"); and a nested `$t(key)`. `{{x}}` is tried first at its position and consumed
203
+ // whole, so it is never counted a second time as `{x}`.
204
+ const PLACEHOLDER_RE =
205
+ /\{\{\s*([^}\s,]+)[^}]*\}\}|(?<![\\A-Za-z])\{([a-z_][a-z0-9_]*|\d+)(?:[!:][^{}]*)?\}|<\/?(\d+|[a-z][a-z0-9]*)\s*\/?>|\$t\(([^)]+)\)/g;
206
+
207
+ /** The placeholders in a string, as a sorted multiset: `{{count}}`, `{date}`, `<1>`,
208
+ * `$t(common.save)`. */
209
+ export function placeholderTokens(text: string): string[] {
210
+ const found: string[] = [];
211
+ for (const m of text.matchAll(PLACEHOLDER_RE)) {
212
+ if (m[1]) found.push(`{{${m[1]}}}`);
213
+ else if (m[2]) found.push(`{${m[2]}}`);
214
+ else if (m[3]) found.push(`<${m[3]}>`);
215
+ else if (m[4]) found.push(`$t(${m[4]})`);
216
+ }
217
+ return found.sort();
218
+ }
219
+
220
+ /** Whether `text` has other placeholders than `reference` — dropped, renamed or added.
221
+ * Moving one is fine: a translation reorders a sentence. */
222
+ export function placeholderMismatch(text: string, reference: string): boolean {
223
+ const a = placeholderTokens(text);
224
+ const b = placeholderTokens(reference);
225
+ return a.length !== b.length || a.some((p, i) => p !== b[i]);
226
+ }
227
+
228
+ // ── Status ──────────────────────────────────────────────────────────────────────
229
+
230
+ /**
231
+ * A verdict counts only for the wording it was given on. If the string or its reference
232
+ * changed since, it is `changed` — read it again — whatever the verdict was. A string the
233
+ * locale lacks is `missing` until somebody suggests its translation (stored as
234
+ * NEEDS_CHANGE on the empty text, so it reads `needs_change`, and `changed` once a
235
+ * developer adds the string).
236
+ */
237
+ export function reviewStatus(
238
+ review: TranslationReview | null | undefined,
239
+ text: string,
240
+ reference: string,
241
+ ): ReviewStatus {
242
+ if (!review) return text === "" && reference !== "" ? "missing" : "unreviewed";
243
+ if (review.text !== text) return "changed";
244
+ if (review.referenceText != null && review.referenceText !== reference) return "changed";
245
+ return review.verdict === "APPROVED" ? "approved" : "needs_change";
246
+ }
247
+
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. */
250
+ export function keyInArea(key: string, area: string): boolean {
251
+ return key === area || key.startsWith(`${area}.`) || key.startsWith(`${area}:`);
252
+ }
253
+
254
+ /**
255
+ * Whether `key` lies in one of `areas` — the scope a reviewer may be limited to
256
+ * (keksdose: `["legal"]`, a lawyer who judges the Imprint, the Privacy Policy and the
257
+ * Terms and sees nothing else). `null`/`undefined` is every area. The server applies the
258
+ * same rule and refuses verdicts outside; this decides only what is listed.
259
+ */
260
+ export function keyInAreas(
261
+ key: string,
262
+ areas: readonly string[] | null | undefined,
263
+ inArea: (key: string, area: string) => boolean = keyInArea,
264
+ ): boolean {
265
+ return !areas || areas.some((area) => inArea(key, area));
266
+ }
267
+
268
+ export interface TranslationRowsInput {
269
+ locale: string;
270
+ /** The locale's strings, `key → text` ({@link flattenStrings}). */
271
+ strings: Readonly<Record<string, string>>;
272
+ /** The reference locale's strings — whichever locale THIS app reads `locale` against. */
273
+ reference: Readonly<Record<string, string>>;
274
+ /** The verdicts: this locale's by key, or the whole list as the server sent it (other
275
+ * locales' verdicts are skipped). */
276
+ reviews?: ReadonlyMap<string, TranslationReview> | readonly TranslationReview[];
277
+ /** Default {@link keyNamespace}. */
278
+ namespaceOf?: (key: string) => string;
279
+ /** The row's source, an app-defined word (kastlan: `doc_text.` keys are `documents`,
280
+ * the rest `screen`). Default: none (`""`). */
281
+ sourceOf?: (key: string) => string;
282
+ /** Keep only keys inside these areas ({@link keyInAreas}); `null` keeps every key. */
283
+ areas?: readonly string[] | null;
284
+ /** Default {@link keyInArea}. */
285
+ inArea?: (key: string, area: string) => boolean;
286
+ }
287
+
288
+ /**
289
+ * Every key of one locale, with its status: the reference's keys first, in its order, so
290
+ * rows read the way the app's authors wrote them; a key only this locale has is appended
291
+ * rather than dropped (a parity test should forbid it, but the page should not lie if one
292
+ * slips through).
293
+ */
294
+ export function translationRows({
295
+ locale,
296
+ strings,
297
+ reference,
298
+ reviews,
299
+ namespaceOf: groupOf = keyNamespace,
300
+ sourceOf,
301
+ areas,
302
+ inArea,
303
+ }: TranslationRowsInput): TranslationRow[] {
304
+ const byKey = new Map<string, TranslationReview>();
305
+ if (reviews instanceof Map) {
306
+ for (const [key, review] of reviews) byKey.set(key, review);
307
+ } else if (reviews) {
308
+ for (const review of reviews as readonly TranslationReview[]) {
309
+ if (review.locale === locale) byKey.set(review.key, review);
310
+ }
311
+ }
312
+ const keys = Object.keys(reference);
313
+ for (const key of Object.keys(strings)) if (!(key in reference)) keys.push(key);
314
+ return keys
315
+ .filter((key) => keyInAreas(key, areas, inArea))
316
+ .map((key) => {
317
+ const text = strings[key] ?? "";
318
+ const ref = reference[key] ?? "";
319
+ const review = byKey.get(key) ?? null;
320
+ return {
321
+ id: `${locale}|${key}`,
322
+ key,
323
+ namespace: groupOf(key),
324
+ source: sourceOf?.(key) ?? "",
325
+ locale,
326
+ text,
327
+ reference: ref,
328
+ status: reviewStatus(review, text, ref),
329
+ review,
330
+ placeholderMismatch: text !== "" && placeholderMismatch(text, ref),
331
+ };
332
+ });
333
+ }
334
+
335
+ /** How many rows stand where, and how many there are. */
336
+ export type TranslationSummary = Record<ReviewStatus, number> & { total: number };
337
+
338
+ export function summariseRows(rows: readonly TranslationRow[]): TranslationSummary {
339
+ const summary: TranslationSummary = {
340
+ missing: 0,
341
+ unreviewed: 0,
342
+ changed: 0,
343
+ needs_change: 0,
344
+ approved: 0,
345
+ total: rows.length,
346
+ };
347
+ for (const row of rows) summary[row.status] += 1;
348
+ return summary;
349
+ }
350
+
351
+ // ── Filtering ───────────────────────────────────────────────────────────────────
352
+
353
+ /** The panel's filters. `"all"` switches one off. */
354
+ export interface TranslationReviewFilter {
355
+ status: ReviewStatus | "all";
356
+ source: string;
357
+ namespace: string;
358
+ /** Searched in the key, the text and the reference, case-insensitively. */
359
+ query: string;
360
+ placeholdersOnly: boolean;
361
+ }
362
+
363
+ export const DEFAULT_TRANSLATION_REVIEW_FILTER: TranslationReviewFilter = {
364
+ status: "all",
365
+ source: "all",
366
+ namespace: "all",
367
+ query: "",
368
+ placeholdersOnly: false,
369
+ };
370
+
371
+ export function filterTranslationRows(
372
+ rows: readonly TranslationRow[],
373
+ filter: Partial<TranslationReviewFilter>,
374
+ ): TranslationRow[] {
375
+ const f = { ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...filter };
376
+ const q = f.query.trim().toLowerCase();
377
+ return rows.filter(
378
+ (row) =>
379
+ (f.status === "all" || row.status === f.status) &&
380
+ (f.source === "all" || row.source === f.source) &&
381
+ (f.namespace === "all" || row.namespace === f.namespace) &&
382
+ (!f.placeholdersOnly || row.placeholderMismatch) &&
383
+ (!q ||
384
+ row.key.toLowerCase().includes(q) ||
385
+ row.text.toLowerCase().includes(q) ||
386
+ row.reference.toLowerCase().includes(q)),
387
+ );
388
+ }
389
+
390
+ // ── Writing ─────────────────────────────────────────────────────────────────────
391
+
392
+ /** What gets stored for a verdict: the wording it was given on, so a later change of the
393
+ * string (or of its reference) shows up as `changed`. */
394
+ export function reviewWrite(
395
+ row: TranslationRow,
396
+ verdict: TranslationVerdict,
397
+ note: string | null = null,
398
+ suggestion: string | null = null,
399
+ ): TranslationReviewWrite {
400
+ return {
401
+ locale: row.locale,
402
+ key: row.key,
403
+ text: row.text,
404
+ referenceText: row.reference,
405
+ verdict,
406
+ note,
407
+ suggestion,
408
+ };
409
+ }
410
+
411
+ /** The app's cached verdict list with `written` in place of what was said before — a
412
+ * save answers with the rows it wrote, so a page patches its copy instead of
413
+ * downloading every verdict again after each click. Any spelling with `locale` and
414
+ * `key` (the API's or the kit's). */
415
+ export function mergeReviews<T extends TranslationReviewKey>(prev: readonly T[], written: readonly T[]): T[] {
416
+ const id = (r: TranslationReviewKey) => `${r.locale}|${r.key}`;
417
+ const replaced = new Set(written.map(id));
418
+ return [...prev.filter((r) => !replaced.has(id(r))), ...written];
419
+ }
420
+
421
+ /** The cached list without the verdicts on `cleared`. */
422
+ export function dropReviews<T extends TranslationReviewKey>(
423
+ prev: readonly T[],
424
+ cleared: readonly TranslationReviewKey[],
425
+ ): T[] {
426
+ const gone = new Set(cleared.map((r) => `${r.locale}|${r.key}`));
427
+ return prev.filter((r) => !gone.has(`${r.locale}|${r.key}`));
428
+ }
429
+
430
+ // ── Export ──────────────────────────────────────────────────────────────────────
431
+
432
+ /**
433
+ * One entry of the "Export corrections" file — enough for a developer to apply the change
434
+ * to the bundle without opening the page. snake_case `reviewed_at` on purpose: this is a
435
+ * FILE, the one keksdose and kastlan already write, beside an API spelled that way.
436
+ */
437
+ export interface TranslationCorrection {
438
+ locale: string;
439
+ key: string;
440
+ status: ReviewStatus;
441
+ current: string;
442
+ reference: string;
443
+ suggestion: string | null;
444
+ note: string | null;
445
+ reviewer: string | null;
446
+ reviewed_at: string | null;
447
+ }
448
+
449
+ /** The rows sent back (verdict NEEDS_CHANGE — a suggested translation for a missing
450
+ * string included), as the export file lists them. */
451
+ export function translationCorrections(rows: readonly TranslationRow[]): TranslationCorrection[] {
452
+ return rows
453
+ .filter((row) => row.review?.verdict === "NEEDS_CHANGE")
454
+ .map((row) => ({
455
+ locale: row.locale,
456
+ key: row.key,
457
+ status: row.status,
458
+ current: row.text,
459
+ reference: row.reference,
460
+ suggestion: row.review?.suggestion ?? null,
461
+ note: row.review?.note ?? null,
462
+ reviewer: row.review?.reviewerName ?? null,
463
+ reviewed_at: row.review?.reviewedAt ?? null,
464
+ }));
465
+ }
@@ -43,7 +43,9 @@ export interface GlobalSearchSource {
43
43
  /** Stable key — the effect that runs the source is keyed by it, not by identity. */
44
44
  id: string;
45
45
  /** The group its results are shown under (an entry's own `group` is ignored) — unless
46
- * `groups` is set, where it is only the home of entries that name no declared group. */
46
+ * `groups` is set, where it is only the home of entries that name no declared group.
47
+ * If every entry names a declared group, it is never shown: a catch-all heading such
48
+ * as "Other" is the honest value, so a stray type would still surface, labelled. */
47
49
  group: string;
48
50
  /**
49
51
  * Split ONE request into several groups — kastlan's record search answers with
@@ -1,61 +0,0 @@
1
- import { a$ as UiKitLabels } from '../../kit-labels-C4uB_zmg.js';
2
- import 'react';
3
- import '../../components/data-table-labels.js';
4
- import '../../components/mini-calendar.js';
5
- import '../../components/calendar-heatmap.js';
6
- import '../../components/popover.js';
7
- import '../../components/chip.js';
8
- import 'lucide-react';
9
- import '../../components/field-sync.js';
10
- import '../../components/tooltip.js';
11
- import '../../hooks/use-anchored-rect.js';
12
- import '../../lib/clipping.js';
13
- import '../../wizard/types.js';
14
- import '../../tour/tour.js';
15
- import '../../search/command-palette.js';
16
- import '../../search/global-search.js';
17
- import '../../search/search-index.js';
18
- import '../../components/month-picker.js';
19
- import '../../components/page-contents.js';
20
- import '../../components/series-chart-labels.js';
21
- import '../../components/pie-chart-labels.js';
22
- import '../../components/sparkline.js';
23
- import '../../components/stat-tile.js';
24
- import '../../components/signature-pad.js';
25
- import '../../components/password-strength.js';
26
- import '../../components/danger-confirm.js';
27
- import '../../components/swatch-picker.js';
28
- import '../../components/tile-radio.js';
29
- import '../../components/icon-picker.js';
30
- import '../../components/dialog-frame.js';
31
- import '../../components/modal.js';
32
- import '../../hooks/use-search-param-state.js';
33
- import '../../components/measured-grid.js';
34
- import '../../feedback-attachment-WFiXJ8We.js';
35
- import '../../feedback/feedback-thread.js';
36
- import '../../feedback/feedback-inbox.js';
37
- import '../../components/account-settings-labels.js';
38
- import '../../components/confirm-dialog.js';
39
- import '../../components/floating-panel.js';
40
- import '../../components/bulk-action-bar.js';
41
- import '../../components/list.js';
42
- import '../../components/signed-amount.js';
43
- import '../../components/breadcrumbs.js';
44
- import '../../components/toast.js';
45
- import '../../components/description-list.js';
46
- import '../../components/line-items.js';
47
- import '../../components/progress-bar.js';
48
- import '../../components/error-boundary.js';
49
- import '../../components/authed-image.js';
50
- import '../../hooks/use-authed-src.js';
51
- import '../../components/image-grid.js';
52
- import '../../components/lightbox.js';
53
-
54
- /**
55
- * Swiss Standard German, informal ("du"): the informal German labels with Swiss
56
- * spelling ("ss" for every "ß") and `de-CH` number formatting (1’234.5) — derived the
57
- * same way `de-CH` is from `de`, so both stay in sync with their source.
58
- */
59
- declare const UI_KIT_LABELS_DE_CH_INFORMAL: UiKitLabels;
60
-
61
- export { UI_KIT_LABELS_DE_CH_INFORMAL };
@@ -1,8 +0,0 @@
1
- "use client";
2
- import { swiss } from "../swiss.js";
3
- import { uiKitLabelsDeInformal } from "./de-informal.js";
4
- const UI_KIT_LABELS_DE_CH_INFORMAL = swiss(uiKitLabelsDeInformal("de-CH"));
5
- export {
6
- UI_KIT_LABELS_DE_CH_INFORMAL
7
- };
8
- //# sourceMappingURL=de-CH-informal.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../src/i18n/locales/de-CH-informal.ts"],"sourcesContent":["import type { UiKitLabels } from \"../kit-labels\";\nimport { swiss } from \"../swiss\";\nimport { uiKitLabelsDeInformal } from \"./de-informal\";\n\n/**\n * Swiss Standard German, informal (\"du\"): the informal German labels with Swiss\n * spelling (\"ss\" for every \"ß\") and `de-CH` number formatting (1’234.5) — derived the\n * same way `de-CH` is from `de`, so both stay in sync with their source.\n */\nexport const UI_KIT_LABELS_DE_CH_INFORMAL: UiKitLabels = swiss(uiKitLabelsDeInformal(\"de-CH\"));\n"],"mappings":";AACA,SAAS,aAAa;AACtB,SAAS,6BAA6B;AAO/B,MAAM,+BAA4C,MAAM,sBAAsB,OAAO,CAAC;","names":[]}
@@ -1,82 +0,0 @@
1
- import { a$ as UiKitLabels } from '../../kit-labels-C4uB_zmg.js';
2
- import 'react';
3
- import '../../components/data-table-labels.js';
4
- import '../../components/mini-calendar.js';
5
- import '../../components/calendar-heatmap.js';
6
- import '../../components/popover.js';
7
- import '../../components/chip.js';
8
- import 'lucide-react';
9
- import '../../components/field-sync.js';
10
- import '../../components/tooltip.js';
11
- import '../../hooks/use-anchored-rect.js';
12
- import '../../lib/clipping.js';
13
- import '../../wizard/types.js';
14
- import '../../tour/tour.js';
15
- import '../../search/command-palette.js';
16
- import '../../search/global-search.js';
17
- import '../../search/search-index.js';
18
- import '../../components/month-picker.js';
19
- import '../../components/page-contents.js';
20
- import '../../components/series-chart-labels.js';
21
- import '../../components/pie-chart-labels.js';
22
- import '../../components/sparkline.js';
23
- import '../../components/stat-tile.js';
24
- import '../../components/signature-pad.js';
25
- import '../../components/password-strength.js';
26
- import '../../components/danger-confirm.js';
27
- import '../../components/swatch-picker.js';
28
- import '../../components/tile-radio.js';
29
- import '../../components/icon-picker.js';
30
- import '../../components/dialog-frame.js';
31
- import '../../components/modal.js';
32
- import '../../hooks/use-search-param-state.js';
33
- import '../../components/measured-grid.js';
34
- import '../../feedback-attachment-WFiXJ8We.js';
35
- import '../../feedback/feedback-thread.js';
36
- import '../../feedback/feedback-inbox.js';
37
- import '../../components/account-settings-labels.js';
38
- import '../../components/confirm-dialog.js';
39
- import '../../components/floating-panel.js';
40
- import '../../components/bulk-action-bar.js';
41
- import '../../components/list.js';
42
- import '../../components/signed-amount.js';
43
- import '../../components/breadcrumbs.js';
44
- import '../../components/toast.js';
45
- import '../../components/description-list.js';
46
- import '../../components/line-items.js';
47
- import '../../components/progress-bar.js';
48
- import '../../components/error-boundary.js';
49
- import '../../components/authed-image.js';
50
- import '../../hooks/use-authed-src.js';
51
- import '../../components/image-grid.js';
52
- import '../../components/lightbox.js';
53
-
54
- /**
55
- * The kit's words in informal German ("du"), for apps that address their users that way
56
- * everywhere else: `<UiKitProvider labels={UI_KIT_LABELS_DE_INFORMAL}>`.
57
- *
58
- * Built on the formal catalogue ({@link uiKitLabelsDe}) rather than beside it: most of
59
- * the kit's German never addresses anyone — buttons are infinitives ("Speichern"),
60
- * status lines are impersonal ("Wird geladen…"), hints use the infinitive
61
- * ("Mindestens 3 Zeichen eingeben", "Bitte erneut versuchen") — so it is the same text
62
- * in both registers and stays in sync with `de.ts` by construction. Only the sentences
63
- * that speak TO the user are rewritten here, by hand: du/Sie is grammar (verb forms,
64
- * imperatives, possessives), not a substitution a regex could do.
65
- *
66
- * Every key of `de.ts` was reviewed. The overridden ones — the complete list of
67
- * sentences that say "Sie" / "Ihr" / a Sie-imperative in the formal catalogue:
68
- * - `miniCalendar.startSelected`, `miniCalendar.rangeSelected`
69
- * - `dangerConfirm.phrase`
70
- * - `wizard.confirmCancel`, `wizard.missingRequired`
71
- * - `tour.awaitClickHint`
72
- * - `signaturePad.instructions`, `signaturePad.typedFallbackHint`
73
- *
74
- * A key added to `de.ts` later is inherited as is; if it addresses the user, add its
75
- * du form here (the "no formal address" test in locales.test.ts catches a "Sie").
76
- * Conventions as in `de.ts`: „…“ quotes, `Intl` digits (via the `numberLocale`
77
- * argument, e.g. `"de-AT"`), number agreement.
78
- */
79
- declare function uiKitLabelsDeInformal(numberLocale?: string): UiKitLabels;
80
- declare const UI_KIT_LABELS_DE_INFORMAL: UiKitLabels;
81
-
82
- export { UI_KIT_LABELS_DE_INFORMAL, uiKitLabelsDeInformal };
@@ -1,37 +0,0 @@
1
- "use client";
2
- import { uiKitLabelsDe } from "./de.js";
3
- function uiKitLabelsDeInformal(numberLocale = "de-DE") {
4
- const de = uiKitLabelsDe(numberLocale);
5
- return {
6
- ...de,
7
- miniCalendar: {
8
- ...de.miniCalendar,
9
- startSelected: (date) => `${date} als Startdatum gew\xE4hlt. W\xE4hle jetzt ein Enddatum.`,
10
- rangeSelected: (from, to) => `Zeitraum ${from} bis ${to} gew\xE4hlt. W\xE4hle ein Startdatum, um neu zu beginnen.`
11
- },
12
- dangerConfirm: {
13
- ...de.dangerConfirm,
14
- phrase: (phrase) => `Gib zur Best\xE4tigung \u201E${phrase}\u201C ein`
15
- },
16
- wizard: {
17
- ...de.wizard,
18
- confirmCancel: "Deine Eingaben gehen verloren.",
19
- missingRequired: "Bitte f\xFClle alle Pflichtfelder aus."
20
- },
21
- tour: {
22
- ...de.tour,
23
- awaitClickHint: "Klick auf das hervorgehobene Element, um fortzufahren"
24
- },
25
- signaturePad: {
26
- ...de.signaturePad,
27
- instructions: "Unterschreibe im Feld mit der Maus, dem Finger oder einem Stift.",
28
- typedFallbackHint: "Wenn du nicht zeichnen kannst, gib stattdessen deinen Namen ein."
29
- }
30
- };
31
- }
32
- const UI_KIT_LABELS_DE_INFORMAL = uiKitLabelsDeInformal();
33
- export {
34
- UI_KIT_LABELS_DE_INFORMAL,
35
- uiKitLabelsDeInformal
36
- };
37
- //# sourceMappingURL=de-informal.js.map