@eifi1/ui-kit 0.18.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 (119) hide show
  1. package/README.md +60 -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 +243 -0
  52. package/dist/i18n/languages.js +70 -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/review.d.ts +102 -0
  74. package/dist/i18n/review.js +158 -0
  75. package/dist/i18n/review.js.map +1 -0
  76. package/dist/index.d.ts +9 -1
  77. package/dist/index.js +64 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/{kit-labels-CPbsjlXr.d.ts → kit-labels-D1J2_jyD.d.ts} +7 -0
  80. package/dist/lib/floating-stack.d.ts +31 -0
  81. package/dist/lib/floating-stack.js +42 -0
  82. package/dist/lib/floating-stack.js.map +1 -0
  83. package/dist/lib/server-wake.d.ts +24 -8
  84. package/dist/lib/server-wake.js +6 -5
  85. package/dist/lib/server-wake.js.map +1 -1
  86. package/dist/lib/translation-review.d.ts +232 -0
  87. package/dist/lib/translation-review.js +194 -0
  88. package/dist/lib/translation-review.js.map +1 -0
  89. package/dist/rhf/fields.d.ts +4 -1
  90. package/dist/rhf/form.d.ts +4 -1
  91. package/dist/rhf.d.ts +4 -1
  92. package/dist/shell/app-shell.d.ts +4 -1
  93. package/dist/shell/top-bar-brand.d.ts +4 -1
  94. package/dist/shell.d.ts +4 -1
  95. package/dist/wizard/stepper-nav.d.ts +4 -1
  96. package/dist/wizard.d.ts +4 -1
  97. package/package.json +1 -1
  98. package/src/components/floating-panel.tsx +19 -1
  99. package/src/components/legal.tsx +159 -0
  100. package/src/components/server-wake.tsx +37 -5
  101. package/src/components/translation-review-editor.tsx +261 -0
  102. package/src/components/translation-review-labels.ts +152 -0
  103. package/src/components/translation-review-parts.tsx +269 -0
  104. package/src/components/translation-review.tsx +502 -0
  105. package/src/i18n/defaults.ts +4 -0
  106. package/src/i18n/german.ts +52 -0
  107. package/src/i18n/kit-labels.tsx +7 -0
  108. package/src/i18n/languages.ts +285 -0
  109. package/src/i18n/locales/en.ts +8 -0
  110. package/src/i18n/locales/es.ts +84 -33
  111. package/src/i18n/locales/fr.ts +51 -0
  112. package/src/i18n/locales/hu.ts +52 -0
  113. package/src/i18n/locales/it.ts +91 -40
  114. package/src/i18n/locales/zh.ts +52 -0
  115. package/src/i18n/review.ts +193 -0
  116. package/src/index.ts +63 -0
  117. package/src/lib/floating-stack.ts +66 -0
  118. package/src/lib/server-wake.ts +34 -13
  119. package/src/lib/translation-review.ts +465 -0
@@ -0,0 +1,285 @@
1
+ import type { UiKitLabels } from "./kit-labels";
2
+ import type { LanguageOption } from "../shell/topbar-controls";
3
+
4
+ /**
5
+ * "Which language is this reader in, and what does the kit say in it?" — the one
6
+ * answer the three apps each wrote for themselves (docs/i18n-harmonization.md, H2).
7
+ *
8
+ * kastlan, keksdose and Kurvenschmiede all ship a subset of the same seven languages,
9
+ * and every one of them hand-rolled the same three things: a list of languages with
10
+ * their names and flags, a rule for turning what a browser asks for (`de-AT`, `zh-TW`,
11
+ * `en-AU`) into a code the app ships, and a bridge from that code to the kit's label
12
+ * catalogue. The three disagreed where nobody was looking: a reader asking for `zh-TW`
13
+ * got German in kastlan and Chinese in Kurvenschmiede; keksdose's English got the raw
14
+ * defaults, so its counts read "12345" where the other two wrote "12,345"; kastlan
15
+ * formatted French as `fr-CH` and Kurvenschmiede as `fr-FR`. None of it was a decision.
16
+ * This module makes it one, in one place, tested once.
17
+ *
18
+ * WHAT IT IS NOT: an i18n library. It resolves no app strings, carries no detector and
19
+ * depends on nothing — the app keeps i18next (or whatever it uses) for its own words and
20
+ * asks this module only the questions the kit can answer for all three apps alike:
21
+ * - {@link KIT_LANGUAGES}: the seven codes, each with its own name, its English name,
22
+ * the flag it flies and the locale it formats in;
23
+ * - {@link resolveLanguage}: which of the codes an app offers answers a reader;
24
+ * - {@link formatLocaleOf}: the `Intl` tag for a code (the provider's `locale`,
25
+ * `<html lang>`, every `toLocaleString` in the app);
26
+ * - {@link loadUiKitLabels}: the kit's catalogue for a code, fetched on demand;
27
+ * - {@link languageOptions}: the rows `LanguageMenu` and `LanguageSetting` take.
28
+ *
29
+ * THE CODE AND THE FORMATTING LOCALE ARE TWO THINGS (Kurvenschmiede's insight, made
30
+ * the rule). The code names a catalogue — what the account stores, what i18next
31
+ * resolves, what a mail is chosen by — and a bare `en` says nothing about how a date is
32
+ * written. Each language formats like its home (Marcel, 2026-10-02, H6): `de-CH`,
33
+ * `en-GB` (British, like the spelling), `es-ES`, `fr-FR`, `it-IT`, `hu-HU`, `zh-CN`. An
34
+ * app that must format otherwise — kastlan's QR bill follows the SIX spec, not the
35
+ * reader — passes its own locale where it needs it; the registry is the default.
36
+ *
37
+ * THERE IS ONE GERMAN, `de-CH`: Swiss Standard German, formal (0.18). Every other
38
+ * German a browser or an old localStorage can hand over — `de`, `de-DE`, `de-AT`,
39
+ * `de-LI`, the briefly offered `de-informal` / `de-CH-informal` — is read as it.
40
+ * Its name is plain "Deutsch": it is the only German, so there is nothing to tell it
41
+ * apart from, and the Swiss flag beside it is what says where the spelling comes from.
42
+ *
43
+ * AND ONE CHINESE, `zh`, WHICH IS SIMPLIFIED. `zh-TW`, `zh-HK` and `zh-Hant` resolve to
44
+ * it: there is no Traditional catalogue, and a reader of Traditional characters reads
45
+ * Simplified far more readily than whichever second language a fallback would guess
46
+ * at (Kurvenschmiede's reasoning, now everyone's). Its name says "简体中文" so that
47
+ * reader sees what they are getting before they pick it.
48
+ */
49
+
50
+ /** The seven languages the kit ships a catalogue for. An app offers a subset. */
51
+ export type KitLanguageCode = "de-CH" | "en" | "es" | "fr" | "it" | "hu" | "zh";
52
+
53
+ /** One language the kit ships: what it is called, which flag it flies and which
54
+ * BCP 47 tag it formats in. */
55
+ export interface KitLanguage {
56
+ /** The catalogue code — what an app stores, sends and hands i18next. */
57
+ readonly code: KitLanguageCode;
58
+ /** The language's name in itself, for a switcher: the reader looking for theirs may
59
+ * not read the language the page is currently in. */
60
+ readonly nativeName: string;
61
+ /** The name in English, for places that list languages to a team rather than to a
62
+ * reader (a translation review's locale column, an admin's reviewer grant). */
63
+ readonly englishName: string;
64
+ /** The BCP 47 tag the language formats numbers and dates in, and declares itself in
65
+ * on `<html lang>`. See {@link formatLocaleOf}. */
66
+ readonly formatLocale: string;
67
+ /** ISO 3166-1 alpha-2 country (lowercase) for flag-icons' `fi fi-<flag>` class —
68
+ * NOT the language code: English flies the UK flag, German the Swiss one, Chinese
69
+ * the PRC's (the catalogue is Simplified). The kit renders the class name only; the
70
+ * app supplies the flag stylesheet (README, "What you install alongside it"). */
71
+ readonly flag: string;
72
+ }
73
+
74
+ /**
75
+ * Every language the kit ships, in the order a switcher lists them: alphabetical by
76
+ * native name (Deutsch, English, Español … 简体中文), which is what a reader scanning for
77
+ * their own language's name expects.
78
+ *
79
+ * An app offering four of them passes those four codes to {@link resolveLanguage} and
80
+ * {@link languageOptions}; it does not copy this list. A language added here is one the
81
+ * kit has a catalogue for (the loader map below is typed by {@link KitLanguageCode}, so
82
+ * a code without one does not compile).
83
+ */
84
+ export const KIT_LANGUAGES: readonly KitLanguage[] = [
85
+ { code: "de-CH", nativeName: "Deutsch", englishName: "German", formatLocale: "de-CH", flag: "ch" },
86
+ { code: "en", nativeName: "English", englishName: "English", formatLocale: "en-GB", flag: "gb" },
87
+ { code: "es", nativeName: "Español", englishName: "Spanish", formatLocale: "es-ES", flag: "es" },
88
+ { code: "fr", nativeName: "Français", englishName: "French", formatLocale: "fr-FR", flag: "fr" },
89
+ { code: "it", nativeName: "Italiano", englishName: "Italian", formatLocale: "it-IT", flag: "it" },
90
+ { code: "hu", nativeName: "Magyar", englishName: "Hungarian", formatLocale: "hu-HU", flag: "hu" },
91
+ { code: "zh", nativeName: "简体中文", englishName: "Chinese (Simplified)", formatLocale: "zh-CN", flag: "cn" },
92
+ ];
93
+
94
+ const ALL_CODES: readonly KitLanguageCode[] = KIT_LANGUAGES.map((language) => language.code);
95
+
96
+ /** The kit's home language: what a reader nothing else matches gets, when it is offered. */
97
+ const HOME: KitLanguageCode = "de-CH";
98
+
99
+ /** The language part of a tag, lower-cased: `de-CH-informal` → `de`, `zh_Hant` → `zh`. */
100
+ const languagePart = (tag: string) => tag.split("-")[0].toLowerCase();
101
+
102
+ /**
103
+ * The offered code that answers ONE requested tag, or `undefined`.
104
+ *
105
+ * Exact first, ignoring case (`DE-ch` → `de-CH`), then by language part: since the kit
106
+ * has exactly one code per language, "the offered code whose language is the tag's
107
+ * language" is the whole of the rule, and it is what takes every German to `de-CH`
108
+ * (`de-AT`, `de-informal`), every Chinese to `zh` (`zh-TW`, `zh-Hant-HK`) and every
109
+ * regional form to its base (`fr-CA` → `fr`, `en-AU` → `en`). A language the app does
110
+ * not offer answers nothing, so the caller moves on to the reader's next preference
111
+ * rather than landing on a guess. Underscores are read as hyphens (`de_CH`), the way a
112
+ * Java or Python backend may have stored the account's locale.
113
+ */
114
+ function answer(
115
+ candidate: unknown,
116
+ offered: readonly KitLanguageCode[],
117
+ ): KitLanguageCode | undefined {
118
+ if (typeof candidate !== "string") return undefined;
119
+ const tag = candidate.trim().replace(/_/g, "-");
120
+ if (!tag) return undefined;
121
+ const lower = tag.toLowerCase();
122
+ const exact = offered.find((code) => code.toLowerCase() === lower);
123
+ if (exact) return exact;
124
+ const language = languagePart(tag);
125
+ return offered.find((code) => languagePart(code) === language);
126
+ }
127
+
128
+ /**
129
+ * Which of the codes an app offers answers this reader.
130
+ *
131
+ * `candidates` are the reader's preferences **in precedence order**, and the first
132
+ * one that matches wins — so the order is the policy. The one the harmonisation plan
133
+ * gives every app (H5):
134
+ *
135
+ * resolveLanguage(
136
+ * [localStorage.getItem("<app>-lang"), user?.locale, ...navigator.languages],
137
+ * OFFERED,
138
+ * );
139
+ *
140
+ * the choice stored on this device (the reader picked it, here) → the account's
141
+ * language (read back on sign-in, so a new device starts in it) → the browser's list,
142
+ * in its own order → the fallback. Missing sources are simply `null` / `undefined`;
143
+ * they are skipped, as is an empty string.
144
+ *
145
+ * Each candidate is matched exactly (case-insensitively), then by language — every
146
+ * German variant to `de-CH`, every Chinese to `zh` (Simplified), any other regional
147
+ * form to its base language (`fr-CA` → `fr`, `en-AU` → `en`) — and only within
148
+ * `offered`: a reader whose browser says `es-MX` first and `en` second, in an app that
149
+ * offers no Spanish, gets English rather than the fallback. That is the difference
150
+ * from a single-tag lookup, and why the browser's whole list is passed, not its first
151
+ * entry.
152
+ *
153
+ * `fallback` is what a reader nothing matches gets: by default `de-CH` when the app
154
+ * offers it — the kit's and the three apps' home language — else the app's first
155
+ * offered code. It is returned as given, offered or not; the app chose it.
156
+ *
157
+ * Why not i18next's own matcher (`getBestMatchFromCodes`, which Kurvenschmiede calls):
158
+ * it needs an i18next instance, it reduces `de-AT` to a `de` none of the apps ships
159
+ * unless every caller remembers a converter first, and its answer to a list depends on
160
+ * `supportedLngs` and `nonExplicitSupportedLngs` being set exactly so. This is a pure
161
+ * function of its arguments, so the switcher, the account form, a server-rendered
162
+ * guess and a test all get the same answer without one.
163
+ */
164
+ export function resolveLanguage(
165
+ candidates: readonly (string | null | undefined)[],
166
+ offered: readonly KitLanguageCode[],
167
+ fallback?: KitLanguageCode,
168
+ ): KitLanguageCode {
169
+ for (const candidate of candidates) {
170
+ const found = answer(candidate, offered);
171
+ if (found) return found;
172
+ }
173
+ return fallback ?? (offered.includes(HOME) ? HOME : (offered[0] ?? HOME));
174
+ }
175
+
176
+ /**
177
+ * The BCP 47 tag a language formats in — `formatLocaleOf("en")` is `"en-GB"`.
178
+ *
179
+ * For `<UiKitProvider locale>`, for `<html lang>` (a screen reader pronounces by it,
180
+ * and `en` alone does not say which English) and for the app's own `Intl` calls, so
181
+ * the kit's counts and the app's amounts group their digits the same way.
182
+ *
183
+ * Takes any tag, not only a {@link KitLanguageCode}, and resolves it the way
184
+ * {@link resolveLanguage} does against all seven: `de-AT` formats as `de-CH`, `en-AU`
185
+ * as `en-GB` (the language's home, by decision — not the reader's region), and a
186
+ * language the kit does not ship as the home language, `de-CH`. So an app can hand it
187
+ * i18next's `resolvedLanguage` as it comes.
188
+ */
189
+ export function formatLocaleOf(code: string): string {
190
+ const resolved = resolveLanguage([code], ALL_CODES);
191
+ return KIT_LANGUAGES.find((language) => language.code === resolved)!.formatLocale;
192
+ }
193
+
194
+ /**
195
+ * One loader per catalogue, each a LITERAL relative `import()`.
196
+ *
197
+ * The literal is load-bearing twice. The library is built one file per module
198
+ * (`bundle: false`, tsup.config.ts), so `./locales/fr` stays a module of its own in
199
+ * dist/ and `scripts/fix-esm-extensions.mjs` gives the specifier its `.js`. And an
200
+ * app's bundler can only split what it can read: a path assembled from a code is a
201
+ * chunk rollup cannot make, and the failure is at runtime. With literals, each
202
+ * language is a chunk of its own and a reader downloads exactly theirs — about 25 kB of
203
+ * JavaScript a catalogue, so an entry chunk holding all of them would carry six that
204
+ * the reader never uses.
205
+ *
206
+ * Relative, not `@eifi1/ui-kit/i18n/fr`: a package importing itself by name resolves
207
+ * through the consumer's node_modules, which a linked checkout or a test of this repo
208
+ * does not have.
209
+ */
210
+ const LOADERS: Record<KitLanguageCode, (formatLocale: string) => Promise<UiKitLabels>> = {
211
+ "de-CH": async (locale) => (await import("./locales/de-CH")).uiKitLabelsDeCh(locale),
212
+ en: async (locale) => (await import("./locales/en")).uiKitLabelsEn(locale),
213
+ es: async (locale) => (await import("./locales/es")).uiKitLabelsEs(locale),
214
+ fr: async (locale) => (await import("./locales/fr")).uiKitLabelsFr(locale),
215
+ it: async (locale) => (await import("./locales/it")).uiKitLabelsIt(locale),
216
+ hu: async (locale) => (await import("./locales/hu")).uiKitLabelsHu(locale),
217
+ zh: async (locale) => (await import("./locales/zh")).uiKitLabelsZh(locale),
218
+ };
219
+
220
+ /** One promise per (language, format locale), so asking twice is one fetch and one
221
+ * object: the provider's context value keeps its identity, and every kit component
222
+ * reading it is spared a re-render on a switch back. */
223
+ const loaded = new Map<string, Promise<UiKitLabels>>();
224
+
225
+ /**
226
+ * The kit's catalogue for a language, fetched on demand and formatted in
227
+ * `formatLocale` — for `<UiKitProvider labels={…}>`.
228
+ *
229
+ * Every language goes through its catalogue's factory, English included:
230
+ * `uiKitLabelsEn("en-GB")` writes "12,345 rows" where the bare
231
+ * `DEFAULT_UI_KIT_LABELS` print "12345" (and keksdose's English did, by passing no
232
+ * labels at all). `formatLocale` defaults to the language's own
233
+ * ({@link formatLocaleOf}); pass another only for a reason, as the factories' own
234
+ * docs describe (`"de-DE"` keeps the Swiss words and writes German digits).
235
+ *
236
+ * `code` may be any tag and is resolved like {@link formatLocaleOf}'s, so `de` loads
237
+ * the one German and a language the kit does not ship loads `de-CH` — the same answer
238
+ * the app's own fallback gives, rather than English beside the app's German.
239
+ *
240
+ * Asynchronous on purpose, and there is no synchronous twin: one would have to import
241
+ * all seven catalogues statically, which puts every language in every app's entry
242
+ * chunk — exactly what one subpath per language exists to prevent. Await it before the
243
+ * first render, beside the app's own catalogue (the apps already await i18next's init
244
+ * for the same reason: no frame in the wrong language), and keep the result in state.
245
+ * To change a few words, spread over what it returns rather than copying a catalogue:
246
+ * a copy misses every key a later release adds.
247
+ *
248
+ * A rejected fetch (a deploy replaced the chunk, the device went offline) is not
249
+ * remembered, so the next call tries again.
250
+ */
251
+ export function loadUiKitLabels(
252
+ code: string,
253
+ formatLocale: string = formatLocaleOf(code),
254
+ ): Promise<UiKitLabels> {
255
+ const language = resolveLanguage([code], ALL_CODES);
256
+ const key = `${language}\u0000${formatLocale}`;
257
+ let labels = loaded.get(key);
258
+ if (!labels) {
259
+ labels = LOADERS[language](formatLocale);
260
+ loaded.set(key, labels);
261
+ labels.catch(() => loaded.delete(key));
262
+ }
263
+ return labels;
264
+ }
265
+
266
+ /**
267
+ * The rows `LanguageMenu` and `LanguageSetting` take, for the codes an app offers, in
268
+ * the order given: `{ code, label: nativeName, country: flag }`.
269
+ *
270
+ * `LanguageMenu` draws `country` as a flag-icons class (`fi fi-ch`) and nothing more —
271
+ * the kit ships no flag artwork and no flag CSS, the app loads the stylesheet (or just
272
+ * the rules for its flags, as Kurvenschmiede and keksdose do). `LanguageSetting` is a
273
+ * native `<select>`, whose options can hold text only, so it shows the names and
274
+ * ignores `country`; the same array serves both.
275
+ *
276
+ * A label of the app's own (keksdose's "Deutsch (Schweiz)") is a `.map` over the
277
+ * result.
278
+ */
279
+ export function languageOptions(codes: readonly KitLanguageCode[] = ALL_CODES): LanguageOption[] {
280
+ return codes.flatMap((code) => {
281
+ const language = KIT_LANGUAGES.find((entry) => entry.code === code);
282
+ if (!language) return [];
283
+ return [{ code: language.code, label: language.nativeName, country: language.flag }];
284
+ });
285
+ }
@@ -112,5 +112,13 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
112
112
  counter: (index, count) => `${n(index)} / ${n(count)}`,
113
113
  position: (index, count) => `Image ${n(index)} of ${n(count)}`,
114
114
  },
115
+ translationReview: {
116
+ ...d.translationReview,
117
+ filterCount: (label, count) => `${label} · ${n(count)}`,
118
+ placeholdersOnly: (count) => `Only placeholder problems (${n(count)})`,
119
+ progress: (approved, total) => `${n(approved)} of ${n(total)} approved`,
120
+ localeProgress: (approved, total) => `${n(approved)}/${n(total)}`,
121
+ exportCorrections: (count) => `Export corrections (${n(count)})`,
122
+ },
115
123
  };
116
124
  }
@@ -19,12 +19,12 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
19
19
  feedbackAttachment: {
20
20
  attachmentAdd: "Adjuntar imagen",
21
21
  attachmentCapture: "Capturar pantalla",
22
- attachmentPaste: "…o pega una captura de pantalla desde el portapapeles.",
22
+ attachmentPaste: "…o pegue una captura de pantalla desde el portapapeles.",
23
23
  attachmentRemove: "Quitar adjunto",
24
24
  attachmentList: "Adjuntos",
25
25
  attachmentScreenshot: "Captura de pantalla",
26
26
  attachmentRemoveFile: (name) => `Quitar ${name}`,
27
- attachmentLimit: (max) => `Máximo ${n(max)} ${max === 1 ? "adjunto" : "adjuntos"}: quita uno para añadir otro.`,
27
+ attachmentLimit: (max) => `Máximo ${n(max)} ${max === 1 ? "adjunto" : "adjuntos"}: quite uno para añadir otro.`,
28
28
  },
29
29
  feedbackDialog: {
30
30
  title: "Enviar comentarios",
@@ -41,13 +41,13 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
41
41
  thread: "Comentarios",
42
42
  empty: "Aún no hay comentarios",
43
43
  loading: "Cargando comentarios…",
44
- you: "Tú",
44
+ you: "Usted",
45
45
  staff: "Equipo",
46
46
  attachments: "Adjuntos",
47
47
  },
48
48
  feedbackComposer: {
49
- field: "Escribe un comentario",
50
- placeholder: "Escribe un comentario…",
49
+ field: "Escriba un comentario",
50
+ placeholder: "Escriba un comentario…",
51
51
  send: "Enviar",
52
52
  sendHint: (modifier) => `${modifier} + Intro para enviar`,
53
53
  sendHintEnter: "Intro para enviar, Mayús + Intro para una nueva línea",
@@ -75,23 +75,23 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
75
75
  enabledText: "Activada",
76
76
  disabledText: "Desactivada",
77
77
  enable: "Configurar la verificación en dos pasos",
78
- scanHint: "Escanea este código con tu app de autenticación e introduce el código que muestra.",
78
+ scanHint: "Escanee este código con su app de autenticación e introduzca el código que muestra.",
79
79
  codeLabel: "Código de verificación",
80
80
  verify: "Verificar y activar",
81
81
  disableSection: "Desactivar la verificación en dos pasos",
82
82
  password: "Contraseña actual",
83
83
  disable: "Desactivar",
84
- qrAlt: "Código QR para tu app de autenticación",
85
- secretHint: "¿No puedes escanearlo? Introduce esta clave en la app:",
84
+ qrAlt: "Código QR para su app de autenticación",
85
+ secretHint: "¿No puede escanearlo? Introduzca esta clave en la app:",
86
86
  copySecret: "Copiar clave",
87
87
  },
88
88
  passkeys: {
89
89
  title: "Llaves de acceso",
90
- description: "Inicia sesión con tu huella, tu cara o el PIN del dispositivo en lugar de una contraseña.",
91
- descriptionAlongside: "Inicia sesión con tu huella, tu cara o el PIN del dispositivo. Tu contraseña sigue funcionando.",
90
+ description: "Inicie sesión con su huella, su cara o el PIN del dispositivo en lugar de una contraseña.",
91
+ descriptionAlongside: "Inicie sesión con su huella, su cara o el PIN del dispositivo. Su contraseña sigue funcionando.",
92
92
  empty: "Aún no hay llaves de acceso",
93
93
  loading: "Cargando llaves de acceso…",
94
- list: "Tus llaves de acceso",
94
+ list: "Sus llaves de acceso",
95
95
  nameLabel: "Nombre (opcional)",
96
96
  namePlaceholder: "p. ej. Portátil, Móvil",
97
97
  add: "Añadir llave de acceso",
@@ -103,7 +103,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
103
103
  cancel: "Cancelar",
104
104
  delete: "Eliminar",
105
105
  deleteItem: (name) => `Eliminar ${name}`,
106
- deleteConfirm: (name) => `¿Eliminar «${name}»? Ya no podrás iniciar sesión con ella.`,
106
+ deleteConfirm: (name) => `¿Eliminar «${name}»? Ya no podrá iniciar sesión con ella.`,
107
107
  confirmDelete: "Eliminar llave de acceso",
108
108
  created: (date) => `Añadida el ${date}`,
109
109
  lastUsed: (date) => `Último uso: ${date}`,
@@ -117,12 +117,12 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
117
117
  addRow: "Añadir fila",
118
118
  removeRow: (row) => `Quitar la fila ${row}`,
119
119
  clear: "Vaciar tabla",
120
- pasteHint: "Pega un bloque de una hoja de cálculo en cualquier celda",
120
+ pasteHint: "Pegue un bloque de una hoja de cálculo en cualquier celda",
121
121
  cell: (column, row) => `${column}, fila ${row}`,
122
122
  rowNumber: "Fila",
123
123
  rowActions: "Acciones de fila",
124
124
  keyboardHint:
125
- "Las flechas mueven entre celdas. Escribe para reemplazar una celda, F2 para editarla, Escape para deshacer la edición. Intro baja y añade una fila al final.",
125
+ "Las flechas mueven entre celdas. Escriba para reemplazar una celda, F2 para editarla, Escape para deshacer la edición. Intro baja y añade una fila al final.",
126
126
  lineError: (line) => `No se pudo leer la línea ${line}`,
127
127
  points: (count) => (count === 1 ? "1 punto" : `${count} puntos`),
128
128
  problems: (count) =>
@@ -148,7 +148,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
148
148
  dataTable: {
149
149
  columns: "Columnas",
150
150
  selectAllRows: "Seleccionar todas las filas",
151
- sortHint: "Haz clic para ordenar · Mayús+clic para añadir un criterio",
151
+ sortHint: "Haga clic para ordenar · Mayús+clic para añadir un criterio",
152
152
  filter: "Filtrar",
153
153
  close: "Cerrar",
154
154
  selectRow: "Seleccionar fila",
@@ -207,13 +207,13 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
207
207
  nextMonth: "Mes siguiente",
208
208
  // Already formatted in the provider's locale ("lunes, 14 de septiembre de 2026").
209
209
  day: (date) => date,
210
- chooseStart: "Elige una fecha de inicio",
211
- chooseEnd: "Elige una fecha de fin",
210
+ chooseStart: "Elija una fecha de inicio",
211
+ chooseEnd: "Elija una fecha de fin",
212
212
  // "Fecha" is feminine; the date follows a label rather than being the subject of
213
213
  // a participle it cannot agree with.
214
- startSelected: (date) => `Fecha de inicio: ${date}. Elige una fecha de fin.`,
214
+ startSelected: (date) => `Fecha de inicio: ${date}. Elija una fecha de fin.`,
215
215
  rangeSelected: (from, to) =>
216
- `Periodo seleccionado: del ${from} al ${to}. Elige una fecha de inicio para empezar de nuevo.`,
216
+ `Periodo seleccionado: del ${from} al ${to}. Elija una fecha de inicio para empezar de nuevo.`,
217
217
  },
218
218
  monthPicker: {
219
219
  previousYear: "Año anterior",
@@ -256,7 +256,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
256
256
  loadError: "No se han podido cargar los resultados",
257
257
  resultCount: (count) => `${n(count)} ${plural(count, "resultado", "resultados")}`,
258
258
  minChars: (count) =>
259
- `Escribe al menos ${n(count)} ${plural(count, "carácter", "caracteres")}`,
259
+ `Escriba al menos ${n(count)} ${plural(count, "carácter", "caracteres")}`,
260
260
  },
261
261
  multiSelect: {
262
262
  search: "Buscar",
@@ -319,7 +319,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
319
319
  cancel: "Cancelar",
320
320
  prompt: "Esta acción no se puede deshacer.",
321
321
  password: "Contraseña",
322
- phrase: (phrase) => `Escribe «${phrase}» para confirmar`,
322
+ phrase: (phrase) => `Escriba «${phrase}» para confirmar`,
323
323
  },
324
324
  tabs: {
325
325
  add: "Añadir pestaña",
@@ -395,7 +395,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
395
395
  cancelDismissLabel: "Seguir editando",
396
396
  reviewTitle: "Revisión",
397
397
  edit: "Editar",
398
- missingRequired: "Rellena todos los campos obligatorios.",
398
+ missingRequired: "Rellene todos los campos obligatorios.",
399
399
  genericError: "Se ha producido un error",
400
400
  },
401
401
  tour: {
@@ -403,7 +403,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
403
403
  back: "Atrás",
404
404
  skip: "Omitir",
405
405
  done: "Listo",
406
- awaitClickHint: "Haz clic en el elemento resaltado para continuar",
406
+ awaitClickHint: "Haga clic en el elemento resaltado para continuar",
407
407
  step: (current, total) => `${n(current)} / ${n(total)}`,
408
408
  },
409
409
  commandPalette: {
@@ -442,8 +442,8 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
442
442
  },
443
443
  signaturePad: {
444
444
  label: "Firma",
445
- instructions: "Firma en el recuadro con el ratón, el dedo o un lápiz.",
446
- typedFallbackHint: "Si no puedes dibujar, escribe tu nombre.",
445
+ instructions: "Firme en el recuadro con el ratón, el dedo o un lápiz.",
446
+ typedFallbackHint: "Si no puede dibujar, escriba su nombre.",
447
447
  empty: "Aún no hay nada dibujado",
448
448
  signed: "Firma dibujada",
449
449
  undo: "Deshacer el último trazo",
@@ -481,7 +481,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
481
481
  seriesChart: {
482
482
  resetZoom: "Restablecer zoom",
483
483
  zoomHint:
484
- "Arrastra para ampliar: una selección más o menos cuadrada amplía ambos ejes; una larga y estrecha, solo el suyo. Haz doble clic para restablecer.",
484
+ "Arrastre para ampliar: una selección más o menos cuadrada amplía ambos ejes; una larga y estrecha, solo el suyo. Haga doble clic para restablecer.",
485
485
  empty: "Sin datos",
486
486
  legend: "Series",
487
487
  points: "Valores del gráfico",
@@ -496,7 +496,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
496
496
  confirmDialog: {
497
497
  confirm: "Confirmar",
498
498
  cancel: "Cancelar",
499
- typed: (text) => `Escribe «${text}» para confirmar`,
499
+ typed: (text) => `Escriba «${text}» para confirmar`,
500
500
  },
501
501
  floatingPanel: {
502
502
  close: "Cerrar",
@@ -533,7 +533,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
533
533
  lineItems: {
534
534
  add: "Añadir fila",
535
535
  remove: (row) => `Eliminar fila ${row}`,
536
- confirmRemove: (row) => `¿Eliminar fila ${row}? Pulsa de nuevo para confirmar`,
536
+ confirmRemove: (row) => `¿Eliminar fila ${row}? Pulse de nuevo para confirmar`,
537
537
  row: (row) => `Fila ${row}`,
538
538
  cell: (column, row) => `${column}, fila ${row}`,
539
539
  totals: "Total",
@@ -583,7 +583,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
583
583
  openInNewTab: "Abrir en una pestaña nueva",
584
584
  },
585
585
  writeLock: {
586
- reason: "Puedes verlo, pero no modificarlo.",
586
+ reason: "Puede verlo, pero no modificarlo.",
587
587
  },
588
588
  accountState: {
589
589
  // Agreeing with "cuenta" (feminine): the chip is the state of an account.
@@ -601,7 +601,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
601
601
  emailOptional: "Correo electrónico (opcional)",
602
602
  // example.com is reserved for examples (RFC 2606); a localised domain is real.
603
603
  emailPlaceholder: "nombre@example.com",
604
- invalidEmail: "Introduce una dirección de correo completa.",
604
+ invalidEmail: "Introduzca una dirección de correo completa.",
605
605
  role: "Rol",
606
606
  roleOf: (name) => `Rol de ${name}`,
607
607
  add: "Compartir",
@@ -616,12 +616,12 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
616
616
  removeConfirm: (name) => `¿Quitar el acceso a ${name}?`,
617
617
  revokePending: "Retirar invitación",
618
618
  revokePendingConfirm: (name) => `¿Retirar la invitación para ${name}?`,
619
- failed: "No ha funcionado. Inténtalo de nuevo.",
619
+ failed: "No ha funcionado. Inténtelo de nuevo.",
620
620
  loading: "Cargando…",
621
621
  },
622
622
  reauthDialog: {
623
- title: "Confirma que eres tú",
624
- description: "Introduce tu contraseña actual para continuar.",
623
+ title: "Confirme que es usted",
624
+ description: "Introduzca su contraseña actual para continuar.",
625
625
  password: "Contraseña actual",
626
626
  submit: "Continuar",
627
627
  cancel: "Cancelar",
@@ -631,6 +631,57 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
631
631
  waking: (appName) =>
632
632
  `El servidor se duerme cuando nadie usa ${appName ?? "la aplicación"}, así que la primera solicitud tras una pausa tiene que volver a arrancarlo. Puede tardar un momento; no se pierde nada y la página se completará sola.`,
633
633
  },
634
+ legal: {
635
+ navLabel: "Información legal",
636
+ },
637
+ translationReview: {
638
+ statusMissing: "Falta",
639
+ statusUnreviewed: "Sin revisar",
640
+ statusChanged: "Cambiado desde la revisión",
641
+ statusNeedsChange: "Requiere cambios",
642
+ statusApproved: "Aprobado",
643
+ statusFilter: "Estado",
644
+ all: "Todos",
645
+ filterCount: (label, count) => `${label} · ${n(count)}`,
646
+ source: "Textos",
647
+ allSources: "Todos los textos",
648
+ namespace: "Área",
649
+ allNamespaces: "Todas las áreas",
650
+ search: "Buscar en claves y textos",
651
+ placeholdersOnly: (count) => `Solo problemas de marcadores (${n(count)})`,
652
+ placeholderChip: "Marcadores",
653
+ key: "Clave",
654
+ statusColumn: "Estado",
655
+ empty: "Ningún texto coincide con estos filtros.",
656
+ missingText: "Falta en este idioma",
657
+ progress: (approved, total) => `${n(approved)} de ${n(total)} ${plural(approved, "aprobado", "aprobados")}`,
658
+ localeProgress: (approved, total) => `${n(approved)}/${n(total)}`,
659
+ locales: "Idiomas",
660
+ approve: "Aprobar",
661
+ flag: "Requiere cambios",
662
+ suggest: "Proponer traducción",
663
+ reset: "Marcar como sin revisar",
664
+ cancel: "Cancelar",
665
+ approveSelected: "Aprobar la selección",
666
+ resetSelected: "Marcar la selección como sin revisar",
667
+ selectShown: "Seleccionar todo lo mostrado",
668
+ changedSince: "Este texto cambió después de revisarse. Vuelva a leerlo, por favor.",
669
+ reviewedWording: "Redacción al revisarse",
670
+ reviewedReference: "Referencia al revisarse",
671
+ placeholderMismatch: (reference, text) =>
672
+ `Los marcadores no coinciden con la referencia — referencia: ${reference}; este texto: ${text}. La aplicación los rellena, así que deben quedar exactamente como están.`,
673
+ lastApproved: (name, date) => `Aprobado por ${name} el ${date}`,
674
+ lastFlagged: (name, date) => `Marcado para cambiar por ${name} el ${date}`,
675
+ erasedReviewer: "una cuenta eliminada",
676
+ suggestion: "Mejor redacción",
677
+ translation: "Traducción",
678
+ note: "Nota (opcional)",
679
+ notePlaceholder: "Qué está mal o qué hay que tener en cuenta",
680
+ readOnly: "Puede leer este idioma, pero no revisarlo.",
681
+ scope: (areas) => `Su revisión se limita a: ${areas}.`,
682
+ exportCorrections: (count) => `Exportar correcciones (${n(count)})`,
683
+ failed: "No ha funcionado. Inténtelo de nuevo.",
684
+ },
634
685
  };
635
686
  }
636
687
 
@@ -648,6 +648,57 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
648
648
  waking: (appName) =>
649
649
  `Le serveur se met en veille quand personne n’utilise ${appName ?? "l’application"}, donc la première requête après une pause doit le redémarrer. Cela peut prendre un moment — rien n’est perdu, la page se remplira d’elle-même.`,
650
650
  },
651
+ legal: {
652
+ navLabel: "Informations légales",
653
+ },
654
+ translationReview: {
655
+ statusMissing: "Manquant",
656
+ statusUnreviewed: "Non relu",
657
+ statusChanged: "Modifié depuis la relecture",
658
+ statusNeedsChange: "À modifier",
659
+ statusApproved: "Approuvé",
660
+ statusFilter: "Statut",
661
+ all: "Tous",
662
+ filterCount: (label, count) => `${label} · ${n(count)}`,
663
+ source: "Textes",
664
+ allSources: "Tous les textes",
665
+ namespace: "Domaine",
666
+ allNamespaces: "Tous les domaines",
667
+ search: "Rechercher dans les clés et les textes",
668
+ placeholdersOnly: (count) => `Seulement les problèmes d’espaces réservés (${n(count)})`,
669
+ placeholderChip: "Espaces réservés",
670
+ key: "Clé",
671
+ statusColumn: "Statut",
672
+ empty: "Aucun texte ne correspond à ces filtres.",
673
+ missingText: "Manquant dans cette langue",
674
+ progress: (approved, total) => `${n(approved)} sur ${n(total)} ${plural(approved, "approuvé", "approuvés")}`,
675
+ localeProgress: (approved, total) => `${n(approved)}/${n(total)}`,
676
+ locales: "Langues",
677
+ approve: "Approuver",
678
+ flag: "À modifier",
679
+ suggest: "Proposer une traduction",
680
+ reset: "Marquer comme non relu",
681
+ cancel: "Annuler",
682
+ approveSelected: "Approuver la sélection",
683
+ resetSelected: "Marquer la sélection comme non relue",
684
+ selectShown: "Sélectionner tout ce qui est affiché",
685
+ changedSince: "Ce texte a été modifié après la relecture. Veuillez le relire.",
686
+ reviewedWording: "Formulation lors de la relecture",
687
+ reviewedReference: "Référence lors de la relecture",
688
+ placeholderMismatch: (reference, text) =>
689
+ `Les espaces réservés diffèrent de la référence — référence\u00a0: ${reference}\u202f; ce texte\u00a0: ${text}. L’application y insère des valeurs, ils doivent donc rester exactement tels quels.`,
690
+ lastApproved: (name, date) => `Approuvé par ${name} le ${date}`,
691
+ lastFlagged: (name, date) => `Signalé à modifier par ${name} le ${date}`,
692
+ erasedReviewer: "un compte supprimé",
693
+ suggestion: "Meilleure formulation",
694
+ translation: "Traduction",
695
+ note: "Remarque (facultatif)",
696
+ notePlaceholder: "Ce qui ne va pas, ou ce qu’il faut garder en tête",
697
+ readOnly: "Vous pouvez consulter cette langue, mais pas la relire.",
698
+ scope: (areas) => `Votre relecture se limite à\u00a0: ${areas}.`,
699
+ exportCorrections: (count) => `Exporter les corrections (${n(count)})`,
700
+ failed: "Cela n’a pas fonctionné. Veuillez réessayer.",
701
+ },
651
702
  };
652
703
  }
653
704