@eifi1/ui-kit 0.53.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/account/delete-account-setting.d.ts +3 -2
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +36 -0
- package/dist/auth/auth-errors.js +20 -0
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -2
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/currency-option-list.d.ts +2 -1
- package/dist/components/currency-option-list.js +10 -3
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.d.ts +3 -1
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +22 -1
- package/dist/components/danger-confirm.js +50 -28
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +2 -1
- package/dist/components/data-table-filter-popover.js +3 -2
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +33 -3
- package/dist/components/data-table-filters.js +6 -4
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +3 -1
- package/dist/components/dialog-frame.js +6 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +27 -0
- package/dist/components/modal.js +5 -3
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/stat-tile.d.ts +21 -2
- package/dist/components/stat-tile.js +4 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/currencies.d.ts +16 -2
- package/dist/currencies.js +2 -1
- package/dist/currencies.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-context-labels.js +2 -1
- package/dist/feedback/feedback-context-labels.js.map +1 -1
- package/dist/feedback/feedback-context.d.ts +19 -3
- package/dist/feedback/feedback-context.js +12 -1
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-diagnostics.d.ts +17 -0
- package/dist/feedback/feedback-diagnostics.js +27 -0
- package/dist/feedback/feedback-diagnostics.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +10 -0
- package/dist/feedback/feedback-dialog.js +6 -4
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-record.d.ts +5 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +1 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +3 -0
- package/dist/feedback/feedback-row-detail.js +3 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
- package/dist/feedback/feedback-submit-dialog.js +15 -3
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.d.ts +30 -2
- package/dist/feedback/feedback-submit.js +5 -2
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/hooks/use-hotkey.js +11 -1
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/i18n/german.js +3 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/locales/en.js +3 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +3 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +3 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +3 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +3 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +3 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/dates.d.ts +7 -1
- package/dist/lib/dates.js +8 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/format.d.ts +37 -2
- package/dist/lib/format.js +25 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/intl-cache.d.ts +4 -2
- package/dist/lib/intl-cache.js.map +1 -1
- package/dist/lib/translation-review.d.ts +26 -0
- package/dist/lib/translation-review.js +14 -0
- package/dist/lib/translation-review.js.map +1 -1
- package/package.json +1 -1
- package/src/account/delete-account-setting.tsx +3 -2
- package/src/auth/auth-errors.ts +69 -0
- package/src/components/amount-input.tsx +2 -1
- package/src/components/calendar-heatmap.tsx +8 -2
- package/src/components/currency-option-list.tsx +12 -4
- package/src/components/currency-select.tsx +3 -1
- package/src/components/danger-confirm.tsx +80 -27
- package/src/components/data-table-filter-popover.tsx +19 -11
- package/src/components/data-table-filters.ts +36 -8
- package/src/components/dialog-frame.tsx +12 -4
- package/src/components/mini-calendar.tsx +5 -2
- package/src/components/modal.tsx +36 -7
- package/src/components/number-input.tsx +4 -3
- package/src/components/stat-tile.tsx +32 -3
- package/src/currencies.ts +23 -3
- package/src/feedback/feedback-context-labels.ts +1 -0
- package/src/feedback/feedback-context.tsx +34 -2
- package/src/feedback/feedback-diagnostics.tsx +41 -0
- package/src/feedback/feedback-dialog.tsx +16 -4
- package/src/feedback/feedback-record.ts +5 -0
- package/src/feedback/feedback-row-detail-labels.ts +1 -0
- package/src/feedback/feedback-row-detail.tsx +13 -0
- package/src/feedback/feedback-submit-dialog.tsx +15 -2
- package/src/feedback/feedback-submit.tsx +36 -3
- package/src/hooks/use-hotkey.ts +24 -1
- package/src/i18n/german.ts +2 -0
- package/src/i18n/locales/es.ts +2 -0
- package/src/i18n/locales/fr.ts +2 -0
- package/src/i18n/locales/hu.ts +2 -0
- package/src/i18n/locales/it.ts +2 -0
- package/src/i18n/locales/zh.ts +2 -0
- package/src/index.ts +14 -2
- package/src/lib/dates.ts +24 -2
- package/src/lib/format.ts +66 -3
- package/src/lib/intl-cache.ts +4 -2
- package/src/lib/translation-review.ts +52 -0
package/src/i18n/german.ts
CHANGED
|
@@ -158,6 +158,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
158
158
|
feedbackContext: {
|
|
159
159
|
user: "Nutzer",
|
|
160
160
|
attachUrl: "Aktuelle Seiten-URL anhängen",
|
|
161
|
+
diagnostics: "Mitgesendete Diagnose",
|
|
161
162
|
},
|
|
162
163
|
feedbackPage: {
|
|
163
164
|
title: "Feedback-Eingang",
|
|
@@ -190,6 +191,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
190
191
|
cancel: "Abbrechen",
|
|
191
192
|
url: "URL",
|
|
192
193
|
copyUrl: "URL kopieren",
|
|
194
|
+
diagnostics: "Diagnose",
|
|
193
195
|
attachment: "Anhang",
|
|
194
196
|
download: (name) => `${name} herunterladen`,
|
|
195
197
|
downloadFailed: "Der Anhang konnte nicht heruntergeladen werden.",
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -150,6 +150,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
150
150
|
feedbackContext: {
|
|
151
151
|
user: "Usuario",
|
|
152
152
|
attachUrl: "Adjuntar la URL de la página actual",
|
|
153
|
+
diagnostics: "Diagnóstico enviado con el informe",
|
|
153
154
|
},
|
|
154
155
|
feedbackPage: {
|
|
155
156
|
title: "Comentarios recibidos",
|
|
@@ -182,6 +183,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
182
183
|
cancel: "Cancelar",
|
|
183
184
|
url: "URL",
|
|
184
185
|
copyUrl: "Copiar URL",
|
|
186
|
+
diagnostics: "Diagnóstico",
|
|
185
187
|
attachment: "Adjunto",
|
|
186
188
|
download: (name) => `Descargar ${name}`,
|
|
187
189
|
downloadFailed: "No se ha podido descargar el adjunto.",
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -151,6 +151,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
151
151
|
feedbackContext: {
|
|
152
152
|
user: "Utilisateur",
|
|
153
153
|
attachUrl: "Joindre l’URL de la page actuelle",
|
|
154
|
+
diagnostics: "Diagnostic envoyé avec le retour",
|
|
154
155
|
},
|
|
155
156
|
feedbackPage: {
|
|
156
157
|
title: "Retours reçus",
|
|
@@ -183,6 +184,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
183
184
|
cancel: "Annuler",
|
|
184
185
|
url: "URL",
|
|
185
186
|
copyUrl: "Copier l’URL",
|
|
187
|
+
diagnostics: "Diagnostic",
|
|
186
188
|
attachment: "Pièce jointe",
|
|
187
189
|
download: (name) => `Télécharger ${name}`,
|
|
188
190
|
downloadFailed: "La pièce jointe n’a pas pu être téléchargée.",
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -148,6 +148,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
148
148
|
feedbackContext: {
|
|
149
149
|
user: "Felhasználó",
|
|
150
150
|
attachUrl: "Az aktuális oldal URL-jének csatolása",
|
|
151
|
+
diagnostics: "A bejelentéssel küldött diagnosztika",
|
|
151
152
|
},
|
|
152
153
|
feedbackPage: {
|
|
153
154
|
title: "Beérkezett visszajelzések",
|
|
@@ -180,6 +181,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
180
181
|
cancel: "Mégse",
|
|
181
182
|
url: "URL",
|
|
182
183
|
copyUrl: "URL másolása",
|
|
184
|
+
diagnostics: "Diagnosztika",
|
|
183
185
|
attachment: "Melléklet",
|
|
184
186
|
download: (name) => `${name} letöltése`,
|
|
185
187
|
downloadFailed: "A mellékletet nem sikerült letölteni.",
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -150,6 +150,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
150
150
|
feedbackContext: {
|
|
151
151
|
user: "Utente",
|
|
152
152
|
attachUrl: "Allega l’URL della pagina corrente",
|
|
153
|
+
diagnostics: "Diagnostica inviata con la segnalazione",
|
|
153
154
|
},
|
|
154
155
|
feedbackPage: {
|
|
155
156
|
title: "Feedback ricevuti",
|
|
@@ -182,6 +183,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
182
183
|
cancel: "Annulla",
|
|
183
184
|
url: "URL",
|
|
184
185
|
copyUrl: "Copia URL",
|
|
186
|
+
diagnostics: "Diagnostica",
|
|
185
187
|
attachment: "Allegato",
|
|
186
188
|
download: (name) => `Scarica ${name}`,
|
|
187
189
|
downloadFailed: "Non è stato possibile scaricare l’allegato.",
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -141,6 +141,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
141
141
|
feedbackContext: {
|
|
142
142
|
user: "用户",
|
|
143
143
|
attachUrl: "附加当前页面 URL",
|
|
144
|
+
diagnostics: "随反馈发送的诊断信息",
|
|
144
145
|
},
|
|
145
146
|
feedbackPage: {
|
|
146
147
|
title: "反馈收件箱",
|
|
@@ -173,6 +174,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
173
174
|
cancel: "取消",
|
|
174
175
|
url: "URL",
|
|
175
176
|
copyUrl: "复制 URL",
|
|
177
|
+
diagnostics: "诊断信息",
|
|
176
178
|
attachment: "附件",
|
|
177
179
|
download: (name) => `下载 ${name}`,
|
|
178
180
|
downloadFailed: "无法下载附件。",
|
package/src/index.ts
CHANGED
|
@@ -93,6 +93,10 @@ export {
|
|
|
93
93
|
reviewWrite,
|
|
94
94
|
mergeReviews,
|
|
95
95
|
dropReviews,
|
|
96
|
+
// 0.54: a save or a reset of any size under server-kit's cap of 2000 (kastlan reuse
|
|
97
|
+
// audit 2026-11-02, proposal 5).
|
|
98
|
+
saveReviewsInChunks,
|
|
99
|
+
clearReviewsInChunks,
|
|
96
100
|
translationCorrections,
|
|
97
101
|
// 0.25: grouping and the approvals' Undo (keksdose live #377).
|
|
98
102
|
groupTranslationRows,
|
|
@@ -761,7 +765,9 @@ export type { RegisterFormProps, RegisterLabels, RegisterValues } from "./auth/r
|
|
|
761
765
|
export { CompleteNameDialog, DEFAULT_COMPLETE_NAME_LABELS } from "./auth/complete-name-dialog";
|
|
762
766
|
export type { CompleteNameDialogProps, CompleteNameLabels, CompleteNameValues } from "./auth/complete-name-dialog";
|
|
763
767
|
export { taggedEmail } from "./auth/email-tag";
|
|
764
|
-
|
|
768
|
+
// 0.54 (kastlan reuse audit 2026-11-02, proposal 4): `billingRefusalText`, a billing
|
|
769
|
+
// refusal's words, which every app mapped from the code by hand.
|
|
770
|
+
export { isAuthError, authErrorCode, isBillingError, billingRefusalText } from "./auth/auth-errors";
|
|
765
771
|
export type { AuthErrorCode } from "./auth/auth-errors";
|
|
766
772
|
export { ForgotPasswordForm, DEFAULT_FORGOT_PASSWORD_LABELS } from "./auth/forgot-password-form";
|
|
767
773
|
export type { ForgotPasswordFormProps, ForgotPasswordLabels } from "./auth/forgot-password-form";
|
|
@@ -793,7 +799,13 @@ export type { AuthHeadingLevel } from "./auth/status-parts";
|
|
|
793
799
|
// 0.30.0: a rate limit and its wait, and the codes the account, billing and demo calls
|
|
794
800
|
// answer with.
|
|
795
801
|
export { isRateLimited, retryAfterSeconds } from "./auth/auth-errors";
|
|
796
|
-
export type {
|
|
802
|
+
export type {
|
|
803
|
+
AccountErrorCode,
|
|
804
|
+
BillingErrorCode,
|
|
805
|
+
BillingRefusalTextOptions,
|
|
806
|
+
DemoErrorCode,
|
|
807
|
+
KitErrorCode,
|
|
808
|
+
} from "./auth/auth-errors";
|
|
797
809
|
|
|
798
810
|
// ── account ──────────────────────────────────────────────────────────────────
|
|
799
811
|
// In account/ since 0.34, beside the other account cards (MOD-8, refactor audit 2026-10):
|
package/src/lib/dates.ts
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
* all, so sending it the user's local day is also what it wants.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
// Imports nothing else: `@eifi1/ui-kit/dates` reaches no package (packaging-contract).
|
|
28
|
+
import { dateFormatter } from "./intl-cache";
|
|
29
|
+
|
|
27
30
|
/** Zero-pad a number to two digits ("3" -> "03"). */
|
|
28
31
|
export function pad(n: number): string {
|
|
29
32
|
return String(n).padStart(2, "0");
|
|
@@ -124,14 +127,33 @@ export function parseIsoDate(s: string): Date | null {
|
|
|
124
127
|
return dt;
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
/** The fields that make an option set a DATE for `toLocaleDateString` (ECMA-402's
|
|
131
|
+
* ToDateTimeOptions, required "date"): with none of them it adds year, month and day. */
|
|
132
|
+
const DATE_FIELDS = ["weekday", "year", "month", "day", "dateStyle"] as const;
|
|
133
|
+
const NUMERIC_DATE: Intl.DateTimeFormatOptions = { year: "numeric", month: "numeric", day: "numeric" };
|
|
134
|
+
|
|
135
|
+
/** `options` as `toLocaleDateString` reads them: with a date in them, whatever else they
|
|
136
|
+
* ask for. `{ hour: "numeric" }` alone is a date and an hour there, never the hour alone
|
|
137
|
+
* that `Intl.DateTimeFormat` would print for it. */
|
|
138
|
+
function asDateOptions(options: Intl.DateTimeFormatOptions | undefined): Intl.DateTimeFormatOptions {
|
|
139
|
+
if (!options) return NUMERIC_DATE;
|
|
140
|
+
return DATE_FIELDS.some((field) => options[field] !== undefined) ? options : { ...options, ...NUMERIC_DATE };
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
/** Format a "YYYY-MM-DD" string for display in `locale` (empty string when
|
|
128
144
|
* unparseable). Defaults to the locale's short numeric date; pass `options` to
|
|
129
145
|
* override (e.g. `{ dateStyle: "medium" }`). `locale` may be `undefined` since 0.34 —
|
|
130
146
|
* the runtime's default, as `Intl` reads it and as every other kit formatter takes it
|
|
131
|
-
* (DUP-11: DatePicker re-wrapped this function only to pass none).
|
|
147
|
+
* (DUP-11: DatePicker re-wrapped this function only to pass none).
|
|
148
|
+
*
|
|
149
|
+
* Through the kit's formatter cache since 0.54 (keksdose audit follow-up, 2026-11-01):
|
|
150
|
+
* `toLocaleDateString` builds a formatter per call, and this one runs for the date
|
|
151
|
+
* pickers' trigger on every render and for an app's date column once per row. The output
|
|
152
|
+
* is `toLocaleDateString`'s: the same options, with the date it adds when they name none
|
|
153
|
+
* (`asDateOptions`). */
|
|
132
154
|
export function formatIsoDate(iso: string, locale: string | undefined, options?: Intl.DateTimeFormatOptions): string {
|
|
133
155
|
const d = parseIsoDate(iso);
|
|
134
|
-
return d ?
|
|
156
|
+
return d ? dateFormatter(locale, asDateOptions(options)).format(d) : "";
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
/** A "YYYY-MM-DD" string shifted by `days` (negative shifts back), or the input
|
package/src/lib/format.ts
CHANGED
|
@@ -11,8 +11,8 @@ export type { MinusStyle } from "./minus";
|
|
|
11
11
|
export { formatWithMinus } from "./minus";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Number, money, percentage, date and relative-time formatting — `Intl`,
|
|
15
|
-
* for the figures an app prints OUTSIDE a kit component.
|
|
14
|
+
* Number, money, percentage, duration (0.54), date and relative-time formatting — `Intl`,
|
|
15
|
+
* in one place, for the figures an app prints OUTSIDE a kit component.
|
|
16
16
|
*
|
|
17
17
|
* Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned
|
|
18
18
|
* `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`
|
|
@@ -99,6 +99,19 @@ export interface FormatMoneyOptions extends FormatNumberBaseOptions {
|
|
|
99
99
|
currencyDisplay?: Intl.NumberFormatOptions["currencyDisplay"];
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
export interface FormatDurationOptions extends BaseOptions {
|
|
103
|
+
/**
|
|
104
|
+
* How the units are written — `Intl.NumberFormat`'s `unitDisplay`, in `locale`:
|
|
105
|
+
* - `"narrow"` (default): "3m 12s", "3 Min. 12 Sek." (de-CH), "3min 12s" (fr) —
|
|
106
|
+
* keksdose's scan readout;
|
|
107
|
+
* - `"short"`: "3 min 12 sec", "3 Min. 12 Sek.", "3 min 12 s";
|
|
108
|
+
* - `"long"`: "3 minutes 12 seconds", "3 Minuten 12 Sekunden", "3 minutes 12 secondes".
|
|
109
|
+
*/
|
|
110
|
+
unitDisplay?: "narrow" | "short" | "long";
|
|
111
|
+
/** The minus of a negative duration — {@link FormatNumberBaseOptions.minus}. */
|
|
112
|
+
minus?: MinusStyle;
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
export interface FormatPercentOptions extends FormatNumberBaseOptions {
|
|
103
116
|
/**
|
|
104
117
|
* `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read
|
|
@@ -162,7 +175,7 @@ function devWarnings(): boolean {
|
|
|
162
175
|
const warnedSites = new Set<string>();
|
|
163
176
|
/** This module's own frames, skipped to find the caller ("Object.formatMoney" is a
|
|
164
177
|
* `useKitFormat` wrapper). */
|
|
165
|
-
const OWN_FRAME = /\b(?:formatNumber|formatMoney|formatPercent|toFormattable|warnInvalid|callSite)\b/;
|
|
178
|
+
const OWN_FRAME = /\b(?:formatNumber|formatMoney|formatPercent|formatDuration|toFormattable|warnInvalid|callSite)\b/;
|
|
166
179
|
|
|
167
180
|
/** The first stack frame outside the formatters: where the bad value was passed. */
|
|
168
181
|
function callSite(): string {
|
|
@@ -248,6 +261,53 @@ export function formatPercent(value: FormattableNumber, options: FormatPercentOp
|
|
|
248
261
|
return formatWithMinus(formatter, ratio, options.minus);
|
|
249
262
|
}
|
|
250
263
|
|
|
264
|
+
/** The units a duration is written in, largest first, with their length in seconds. */
|
|
265
|
+
const DURATION_UNITS = [
|
|
266
|
+
["hour", 3600],
|
|
267
|
+
["minute", 60],
|
|
268
|
+
["second", 1],
|
|
269
|
+
] as const;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* A number of SECONDS as an elapsed time in `locale`: "42s", "3m 12s", "1h 0m 5s" — and in
|
|
273
|
+
* German "3 Min. 12 Sek.", in French "3min 12s" (keksdose audit follow-up, 2026-11-01:
|
|
274
|
+
* keksdose's `formatElapsed`, the scan card's readout, moves here). Each unit is
|
|
275
|
+
* `Intl.NumberFormat`'s `style: "unit"`, so the digits and the unit's spelling follow
|
|
276
|
+
* `locale`; `unitDisplay` picks the form, narrow by default.
|
|
277
|
+
*
|
|
278
|
+
* - Seconds alone under a minute ("0s", "59s"); minutes and seconds under an hour ("1m 0s",
|
|
279
|
+
* "59m 59s"); from an hour on, hours, minutes and seconds ("1h 0m 0s"). Hours are the
|
|
280
|
+
* largest unit: a day and a bit is "27h 46m 40s". Every unit below the first is printed,
|
|
281
|
+
* zero too, so a ticking readout keeps its shape from one second to the next.
|
|
282
|
+
* - Whole seconds, cut toward zero: a timer reads "59s" until the minute is full.
|
|
283
|
+
* - The parts are joined by a space, as the readout always was — not `Intl.ListFormat`'s
|
|
284
|
+
* "3 Min. und 12 Sek.", which is a sentence rather than a figure.
|
|
285
|
+
* - Negative: the minus on the first unit, as `Intl.DurationFormat` writes one — "−3m 12s",
|
|
286
|
+
* with the kit's minus (`minus`). An elapsed time from a server's clock can come out a
|
|
287
|
+
* second below zero; clamp it before, as keksdose's card does, when that is not news.
|
|
288
|
+
* - `null` / `undefined` print `empty`; `NaN`, ±`Infinity` and a non-numeric string too,
|
|
289
|
+
* with the formatters' development warning. A numeric string ("185") is a number.
|
|
290
|
+
*/
|
|
291
|
+
export function formatDuration(seconds: FormattableNumber, options: FormatDurationOptions = {}): string {
|
|
292
|
+
const n = toFormattable("formatDuration", seconds);
|
|
293
|
+
if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
294
|
+
const unitDisplay = options.unitDisplay ?? "narrow";
|
|
295
|
+
const negative = n < 0;
|
|
296
|
+
let rest = Math.trunc(Math.abs(n));
|
|
297
|
+
const parts: string[] = [];
|
|
298
|
+
for (const [unit, size] of DURATION_UNITS) {
|
|
299
|
+
const count = Math.floor(rest / size);
|
|
300
|
+
rest -= count * size;
|
|
301
|
+
// Units above the first that has a count are left out; seconds always print.
|
|
302
|
+
if (parts.length === 0 && count === 0 && unit !== "second") continue;
|
|
303
|
+
const formatter = numberFormatter(options.locale, { style: "unit", unit, unitDisplay });
|
|
304
|
+
// `count > 0`: under a second either way is "0s", with no sign on it.
|
|
305
|
+
const signed = negative && parts.length === 0 && count > 0 ? -count : count;
|
|
306
|
+
parts.push(formatWithMinus(formatter, signed, options.minus));
|
|
307
|
+
}
|
|
308
|
+
return parts.join(" ");
|
|
309
|
+
}
|
|
310
|
+
|
|
251
311
|
/* ── Dates ─────────────────────────────────────────────────────────────────── */
|
|
252
312
|
|
|
253
313
|
/**
|
|
@@ -436,6 +496,8 @@ export interface KitFormat {
|
|
|
436
496
|
formatNumber: (value: FormattableNumber, options?: WithoutLocale<FormatNumberOptions>) => string;
|
|
437
497
|
formatMoney: (value: FormattableNumber, currency: string, options?: WithoutLocale<FormatMoneyOptions>) => string;
|
|
438
498
|
formatPercent: (value: FormattableNumber, options?: WithoutLocale<FormatPercentOptions>) => string;
|
|
499
|
+
/** 0.54: {@link formatDuration}, a number of seconds as an elapsed time. */
|
|
500
|
+
formatDuration: (seconds: FormattableNumber, options?: WithoutLocale<FormatDurationOptions>) => string;
|
|
439
501
|
formatDate: (
|
|
440
502
|
value: DateInput,
|
|
441
503
|
style?: FormatDateStyle | Intl.DateTimeFormatOptions,
|
|
@@ -462,6 +524,7 @@ export function useKitFormat(locale?: string): KitFormat {
|
|
|
462
524
|
formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),
|
|
463
525
|
formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),
|
|
464
526
|
formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),
|
|
527
|
+
formatDuration: (seconds, options) => formatDuration(seconds, { locale: kitLocale, ...options }),
|
|
465
528
|
formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, timeZone, ...options }),
|
|
466
529
|
formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, timeZone, ...options }),
|
|
467
530
|
}),
|
package/src/lib/intl-cache.ts
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Constructing the formatter is the expensive part: keksdose measured 392 ms per click on
|
|
5
5
|
* a 270-row register when each cell built its own (dev#632), and kept its own renderer of
|
|
6
6
|
* the date-format preference because the kit's built one per call (refactor audit
|
|
7
|
-
* 2026-10-30, DUP-2). `lib/format` and `lib/date-format` both format through this one
|
|
7
|
+
* 2026-10-30, DUP-2). `lib/format` and `lib/date-format` both format through this one, and
|
|
8
|
+
* since 0.54 `lib/dates`' `formatIsoDate` (keksdose audit follow-up, 2026-11-01).
|
|
8
9
|
*
|
|
9
10
|
* @internal Not part of the barrel — `lib/format` is re-exported whole, so the cache
|
|
10
|
-
* lives apart from it.
|
|
11
|
+
* lives apart from it. It imports nothing, which `@eifi1/ui-kit/dates` (no package at
|
|
12
|
+
* all) relies on.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
const dateFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
@@ -570,6 +570,58 @@ export function dropReviews<T extends TranslationReviewKey>(
|
|
|
570
570
|
return prev.filter((r) => !gone.has(`${r.locale}|${r.key}`));
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
+
/**
|
|
574
|
+
* How many verdicts one request carries. server-kit refuses more than 2000 in one
|
|
575
|
+
* `PUT /translations/reviews` or `POST /translations/reviews/clear` (`BATCH_MAX`, its
|
|
576
|
+
* translation_review/schemas.py); half of it, as each of the three apps sent. A whole area
|
|
577
|
+
* is rarely more than a few hundred, so most saves are one request; "Approve the
|
|
578
|
+
* unreviewed" over an app's every string is not.
|
|
579
|
+
*/
|
|
580
|
+
const REVIEW_CHUNK = 1000;
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* A save of any size under server-kit's cap: `writes` in chunks of 1000, one request
|
|
584
|
+
* after the other, in their order, and every verdict the server answered with, in that
|
|
585
|
+
* order — to patch the page's cached list with {@link mergeReviews} once. `save` is the
|
|
586
|
+
* app's own request (`PUT /translations/reviews`), answering with the verdicts it wrote,
|
|
587
|
+
* in either spelling. No writes, no request: the server refuses an empty batch.
|
|
588
|
+
*
|
|
589
|
+
* A chunk that fails rejects with its error, and the chunks after it are not sent. What
|
|
590
|
+
* the chunks before it wrote stays written — the server has it — but is not handed back,
|
|
591
|
+
* so the page leaves its cache as it was and the panel shows the error; a second try
|
|
592
|
+
* writes those verdicts again, which changes nothing but the time on them.
|
|
593
|
+
*
|
|
594
|
+
* All three apps wrote this loop, with a `chunks` helper and `SAVE_CHUNK = 1000` of
|
|
595
|
+
* their own (kastlan reuse audit 2026-11-02, proposal 5):
|
|
596
|
+
*
|
|
597
|
+
* const written = await saveReviewsInChunks(writes.map(toApiWrite), (part) => translationsApi.save(part));
|
|
598
|
+
* patchCache((reviews) => mergeReviews(reviews, written));
|
|
599
|
+
*/
|
|
600
|
+
export async function saveReviewsInChunks<W extends TranslationReviewKey, R extends TranslationReviewKey>(
|
|
601
|
+
writes: readonly W[],
|
|
602
|
+
save: (chunk: W[]) => Promise<readonly R[]>,
|
|
603
|
+
): Promise<R[]> {
|
|
604
|
+
const written: R[] = [];
|
|
605
|
+
for (let at = 0; at < writes.length; at += REVIEW_CHUNK) {
|
|
606
|
+
written.push(...(await save(writes.slice(at, at + REVIEW_CHUNK))));
|
|
607
|
+
}
|
|
608
|
+
return written;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* {@link saveReviewsInChunks} for a reset (`POST /translations/reviews/clear`): `keys` in
|
|
613
|
+
* chunks of 1000, one request after the other, and the keys themselves once every chunk
|
|
614
|
+
* is through — to drop from the cached list with {@link dropReviews} once. A failing
|
|
615
|
+
* chunk rejects as a save's does, and what the chunks before it cleared stays cleared.
|
|
616
|
+
*/
|
|
617
|
+
export async function clearReviewsInChunks<K extends TranslationReviewKey>(
|
|
618
|
+
keys: readonly K[],
|
|
619
|
+
clear: (chunk: K[]) => Promise<unknown>,
|
|
620
|
+
): Promise<K[]> {
|
|
621
|
+
for (let at = 0; at < keys.length; at += REVIEW_CHUNK) await clear(keys.slice(at, at + REVIEW_CHUNK));
|
|
622
|
+
return [...keys];
|
|
623
|
+
}
|
|
624
|
+
|
|
573
625
|
// ── Swipe bindings ──────────────────────────────────────────────────────────────
|
|
574
626
|
|
|
575
627
|
/**
|