@gusnips/react 0.9.15 → 0.9.16

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 CHANGED
@@ -284,8 +284,10 @@ operator arriving while `/auth/me` is down gets told the page does not exist: th
284
284
  retry, and no request id to quote. A guard has three answers — wait, fail, refuse — and only a
285
285
  real `false` reaches the refusal.
286
286
 
287
- `createRequireAuth` sends an anonymous visitor to `?next=`, carrying path, query and hash. The URL
288
- survives reloads, OAuth and email links; router state does not. Read it through the main entry's
287
+ `createRequireAuth` sends an anonymous visitor to `?next=`, carrying path, query and hash. A hash
288
+ that carries a sign-in token (`#access_token=…`, as an OAuth callback does) is left off, so the
289
+ token never lands in the address, the logs or the history. The URL survives reloads, OAuth and
290
+ email links; router state does not. Read it through the main entry's
289
291
  validator before navigating yourself:
290
292
 
291
293
  ```ts
@@ -334,6 +336,26 @@ const { cause, fix, recover, reference } = describeError(error);
334
336
  // recover: "retry" | "signin" | "wait" | "none"
335
337
  ```
336
338
 
339
+ Build it once. An app in one language writes its four sentences out:
340
+
341
+ ```ts
342
+ const inTime = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
343
+
344
+ export const describeError = createErrorDescriber({
345
+ copy: {
346
+ network: "We could not reach the server.",
347
+ networkHint: "Check your connection, then try again.",
348
+ unexpected: "Something went wrong on our side.",
349
+ retrySoon: "Try again in a moment.",
350
+ },
351
+ formatWait: (secs) => inTime.format(secs, "second"),
352
+ });
353
+ ```
354
+
355
+ An app that translates passes `t`, `copyPrefix`, `messageKeyPrefix` and `knownMessageKeys`
356
+ instead. Its catalog holds those four under `copyPrefix`, and when the server names a sentence
357
+ the catalog has, the reader gets it in their own language.
358
+
337
359
  The field is `fix`, the same word as the prop above. It was `hint` until 0.6.0, which left every
338
360
  adopter writing `fix={hint}` at every error surface.
339
361
 
@@ -342,7 +364,7 @@ reader sees and the retry that actually happens cannot disagree. Give it the sam
342
364
  `durableLimitCodes` list you give `queryDefaults`, and a spent quota offers no button instead of
343
365
  one that cannot work.
344
366
 
345
- You also say how a stated wait becomes words:
367
+ `formatWait` says how a stated wait becomes words:
346
368
 
347
369
  ```ts
348
370
  formatWait: (secs) => humanizeWait(t, secs, "errors."); // three ICU plural keys
@@ -123,10 +123,8 @@ export interface ErrorContext {
123
123
  */
124
124
  recover: RecoveryKind;
125
125
  }
126
- export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix extends string, ServerName extends string> {
127
- t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName | PluralBase<ServerName>}`>;
128
- /** Catalog namespace for this module's own copy, e.g. `"errors."`. */
129
- copyPrefix: Prefix;
126
+ /** What both forms share: everything but where the words come from. */
127
+ interface DescriberRules {
130
128
  /**
131
129
  * How a stated wait becomes words, for the `wait` an arm reads off its context.
132
130
  *
@@ -137,6 +135,44 @@ export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix exten
137
135
  * `(secs) => formatIn(secs, locale)` — and the catalog you need is the one you can see.
138
136
  */
139
137
  formatWait: (secs: number) => string;
138
+ /**
139
+ * Per-code copy. Everything not listed falls through to the default arm.
140
+ *
141
+ * The arms look up the app's own keys, so they close over the app's `t` rather than being
142
+ * handed one — this module has no names for that copy and no business typing it.
143
+ *
144
+ * Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
145
+ * code that does not exist is a compile error at the arm rather than a branch that never runs.
146
+ */
147
+ codes?: Partial<Record<string, ErrorArm>>;
148
+ /**
149
+ * What to say when nothing in `codes` matches: a code this build has no arm for, or an answer
150
+ * with no envelope at all.
151
+ *
152
+ * The built-in arm hands the reader the server's own `message`, and for one API in the fleet
153
+ * that is right — its message IS the sentence a person should read. For another it is exactly
154
+ * wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
155
+ * ships three languages, so the built-in would put developer English in front of a reader who
156
+ * does not speak it. One default cannot be right for both, and the one baked in here was
157
+ * simply the first adopter's convention, mistaken for a fact.
158
+ *
159
+ * An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
160
+ * with no arm of its own could state its expiry and have that go unsaid.
161
+ */
162
+ fallback?: ErrorArm;
163
+ /**
164
+ * The codes that mean "this limit does not clear by waiting" — **the same list you hand
165
+ * `queryDefaults`**, because it answers the same question and a second copy is a second
166
+ * chance to disagree with the retry rule. Declare it once in the app and pass it twice.
167
+ */
168
+ durableLimitCodes?: readonly string[];
169
+ /** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
170
+ maxRetryWaitSecs?: number;
171
+ }
172
+ export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix extends string, ServerName extends string> extends DescriberRules {
173
+ t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName | PluralBase<ServerName>}`>;
174
+ /** Catalog namespace for this module's own copy, e.g. `"errors."`. */
175
+ copyPrefix: Prefix;
140
176
  /**
141
177
  * The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
142
178
  * ignored — the server names a sentence, it does not get to name any key in the app.
@@ -176,42 +212,22 @@ export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix exten
176
212
  * deliberately takes only the params — an arm that needs the error has `ctx` for that.
177
213
  */
178
214
  localizeParams?: (params: Record<string, string | number> | undefined) => Record<string, unknown> | undefined;
179
- /**
180
- * Per-code copy. Everything not listed falls through to the default arm.
181
- *
182
- * The arms look up the app's own keys, so they close over the app's `t` rather than being
183
- * handed one — this module has no names for that copy and no business typing it.
184
- *
185
- * Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
186
- * code that does not exist is a compile error at the arm rather than a branch that never runs.
187
- */
188
- codes?: Partial<Record<string, ErrorArm>>;
189
- /**
190
- * What to say when nothing in `codes` matches: a code this build has no arm for, or an answer
191
- * with no envelope at all.
192
- *
193
- * The built-in arm hands the reader the server's own `message`, and for one API in the fleet
194
- * that is right — its message IS the sentence a person should read. For another it is exactly
195
- * wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
196
- * ships three languages, so the built-in would put developer English in front of a reader who
197
- * does not speak it. One default cannot be right for both, and the one baked in here was
198
- * simply the first adopter's convention, mistaken for a fact.
199
- *
200
- * An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
201
- * with no arm of its own could state its expiry and have that go unsaid.
202
- */
203
- fallback?: ErrorArm;
204
- /**
205
- * The codes that mean "this limit does not clear by waiting" — **the same list you hand
206
- * `queryDefaults`**, because it answers the same question and a second copy is a second
207
- * chance to disagree with the retry rule. Declare it once in the app and pass it twice.
208
- */
209
- durableLimitCodes?: readonly string[];
210
- /** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
211
- maxRetryWaitSecs?: number;
215
+ }
216
+ /**
217
+ * An app in one language writes its four sentences out, and needs no `t`, no prefixes and no
218
+ * catalog.
219
+ *
220
+ * With no catalog, the server cannot name a sentence the app carries, so a refusal with no arm of
221
+ * its own shows the server's `message`. In a one-language product that is the right sentence: the
222
+ * server writes in the same language the app does.
223
+ */
224
+ export interface OneLanguageDescriberOptions extends DescriberRules {
225
+ copy: Record<CopyName, string>;
212
226
  }
213
227
  export type ErrorArm = (ctx: ErrorContext) => ErrorCopy;
214
- /** Build the describer. */
215
- export declare function createErrorDescriber<Prefix extends string, ServerPrefix extends string, ServerName extends string>({ t, copyPrefix, formatWait, messageKeyPrefix, knownMessageKeys, localizeParams, codes, fallback, durableLimitCodes, maxRetryWaitSecs, }: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError;
228
+ /** Build the describer for an app in one language. */
229
+ export declare function createErrorDescriber(options: OneLanguageDescriberOptions): (error: unknown) => DescribedError;
230
+ /** Build the describer for an app that translates. */
231
+ export declare function createErrorDescriber<Prefix extends string, ServerPrefix extends string, ServerName extends string>(options: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError;
216
232
  export {};
217
233
  //# sourceMappingURL=describe-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
@@ -24,8 +24,7 @@ export function humanizeWait(t, secs, prefix) {
24
24
  return t(`${prefix}waitMinutes`, { count: Math.ceil(secs / 60) });
25
25
  return t(`${prefix}waitHours`, { count: Math.ceil(secs / 3600) });
26
26
  }
27
- /** Build the describer. */
28
- export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPrefix, knownMessageKeys, localizeParams = (params) => params, codes = {}, fallback, durableLimitCodes = [], maxRetryWaitSecs, }) {
27
+ function translatedWords({ t, copyPrefix, messageKeyPrefix, knownMessageKeys, localizeParams = (params) => params, }) {
29
28
  // Both spellings of every name: the one the catalog stores, and — for a plural — the one the
30
29
  // server sends. `Object.keys` and not `in`, which would have accepted "toString".
31
30
  const known = new Set();
@@ -36,19 +35,28 @@ export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPref
36
35
  // Still sound, and still the reason nothing here needs a cast: the set holds exactly the raw
37
36
  // keys and their plural bases, which is exactly what the predicate claims.
38
37
  const has = (name) => known.has(name);
39
- function serverSentence(error) {
40
- const lookup = (named) => {
41
- if (named === undefined || !named.startsWith(messageKeyPrefix))
42
- return null;
43
- const name = named.slice(messageKeyPrefix.length);
44
- return has(name) ? t(`${messageKeyPrefix}${name}`, localizeParams(error.params)) : null;
45
- };
46
- // `messageKey` first: an API that sends both means the key, and the code is what the UI
47
- // switches on. The code is only consulted when there is no key to prefer, and it is spelled
48
- // under the same prefix so the gate is identical.
49
- return (lookup(error.messageKey) ??
50
- (error.code === undefined ? null : lookup(`${messageKeyPrefix}${error.code}`)));
51
- }
38
+ return {
39
+ copyOf: (name) => t(`${copyPrefix}${name}`),
40
+ serverSentence(error) {
41
+ const lookup = (named) => {
42
+ if (named === undefined || !named.startsWith(messageKeyPrefix))
43
+ return null;
44
+ const name = named.slice(messageKeyPrefix.length);
45
+ return has(name) ? t(`${messageKeyPrefix}${name}`, localizeParams(error.params)) : null;
46
+ };
47
+ // `messageKey` first: an API that sends both means the key, and the code is what the UI
48
+ // switches on. The code is only consulted when there is no key to prefer, and it is spelled
49
+ // under the same prefix so the gate is identical.
50
+ return (lookup(error.messageKey) ??
51
+ (error.code === undefined ? null : lookup(`${messageKeyPrefix}${error.code}`)));
52
+ },
53
+ };
54
+ }
55
+ export function createErrorDescriber(options) {
56
+ const { formatWait, codes = {}, fallback, durableLimitCodes = [], maxRetryWaitSecs } = options;
57
+ const { copyOf, serverSentence } = "copy" in options
58
+ ? { copyOf: (name) => options.copy[name], serverSentence: () => null }
59
+ : translatedWords(options);
52
60
  /**
53
61
  * Which control to offer, decided once and never switched on separately.
54
62
  *
@@ -86,8 +94,8 @@ export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPref
86
94
  */
87
95
  const unmapped = fallback ??
88
96
  (({ error, says }) => ({
89
- cause: says ?? ((error.code === undefined ? "" : error.message) || t(`${copyPrefix}unexpected`)),
90
- fix: error.status >= 500 ? t(`${copyPrefix}retrySoon`) : undefined,
97
+ cause: says ?? ((error.code === undefined ? "" : error.message) || copyOf("unexpected")),
98
+ fix: error.status >= 500 ? copyOf("retrySoon") : undefined,
91
99
  }));
92
100
  return function describeError(error) {
93
101
  if (!(error instanceof ApiError)) {
@@ -95,10 +103,10 @@ export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPref
95
103
  // never worth showing a stack trace for. `shouldRetry` answers a non-response the same way,
96
104
  // so these two agree here without either being told about the other.
97
105
  return {
98
- cause: t(`${copyPrefix}network`),
106
+ cause: copyOf("network"),
99
107
  // The catalog KEY keeps its name while the field becomes `fix`: renaming it would cost
100
108
  // every adopter a JSON edit in every language, for a string no caller ever types.
101
- fix: t(`${copyPrefix}networkHint`),
109
+ fix: copyOf("networkHint"),
102
110
  recover: "retry",
103
111
  };
104
112
  }
@@ -1 +1 @@
1
- {"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA2GzC,MAAM,aAAa,GAAG,iCAAiC,CAAC;AA2BxD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAC1B,CAAoC,EACpC,IAAY,EACZ,MAAc;IAEd,+FAA+F;IAC/F,+BAA+B;IAC/B,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC,GAAG,MAAM,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IACzF,IAAI,IAAI,GAAG,EAAE,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC,GAAG,MAAM,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACtF,OAAO,CAAC,CAAC,GAAG,MAAM,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;AACpE,CAAC;AAwID,SAAS,eAAe,CAItB,EACA,CAAC,EACD,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,GACqB;IACxD,6FAA6F;IAC7F,kFAAkF;IAClF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACjD,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChB,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,6FAA6F;IAC7F,2EAA2E;IAC3E,MAAM,GAAG,GAAG,CAAC,IAAY,EAA+C,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAE3F,OAAO;QACL,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,UAAU,GAAG,IAAI,EAAE,CAAC;QAC3C,cAAc,CAAC,KAAK;YAClB,MAAM,MAAM,GAAG,CAAC,KAAyB,EAAiB,EAAE;gBAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC;oBAAE,OAAO,IAAI,CAAC;gBAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;gBAClD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,gBAAgB,GAAG,IAAI,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC1F,CAAC,CAAC;YACF,wFAAwF;YACxF,4FAA4F;YAC5F,kDAAkD;YAClD,OAAO,CACL,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;gBACxB,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAC/E,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAcD,MAAM,UAAU,oBAAoB,CAKlC,OAA8F;IAE9F,MAAM,EAAE,UAAU,EAAE,KAAK,GAAG,EAAE,EAAE,QAAQ,EAAE,iBAAiB,GAAG,EAAE,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC;IAC/F,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAC9B,MAAM,IAAI,OAAO;QACf,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE;QACtE,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IAE/B;;;;;OAKG;IACH,SAAS,WAAW,CAAC,KAAe,EAAE,QAAuB;QAC3D,6FAA6F;QAC7F,0FAA0F;QAC1F,kFAAkF;QAClF,IAAI,KAAK,CAAC,QAAQ;YAAE,OAAO,MAAM,CAAC;QAClC,6FAA6F;QAC7F,0FAA0F;QAC1F,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,QAAQ,CAAC;QAC1C,6FAA6F;QAC7F,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,MAAM,CAAC;QACrC,OAAO,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,MAAM,QAAQ,GACZ,QAAQ;QACR,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YACrB,KAAK,EAAE,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC;YACxF,GAAG,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS;SAC3D,CAAC,CAAC,CAAC;IAEN,OAAO,SAAS,aAAa,CAAC,KAAc;QAC1C,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC,EAAE,CAAC;YACjC,0FAA0F;YAC1F,4FAA4F;YAC5F,qEAAqE;YACrE,OAAO;gBACL,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC;gBACxB,uFAAuF;gBACvF,kFAAkF;gBAClF,GAAG,EAAE,MAAM,CAAC,aAAa,CAAC;gBAC1B,OAAO,EAAE,OAAO;aACjB,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACvC,MAAM,GAAG,GAAiB;YACxB,KAAK;YACL,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC;YAC3B,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC;YACrD,QAAQ;YACR,OAAO,EAAE,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC;SACtC,CAAC;QAEF,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,QAAQ,CAAC;QACnF,MAAM,IAAI,GAAc,GAAG,CAAC,GAAG,CAAC,CAAC;QAEjC,2FAA2F;QAC3F,sFAAsF;QACtF,0DAA0D;QAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;QACpD,OAAO;YACL,GAAG,IAAI;YACP,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO;YACpC,GAAG,CAAC,SAAS,KAAK,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC;SAC9C,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { createApiClient, type ApiClient, type ApiClientOptions, type RefreshRes
7
7
  export { createSseParser, readSseStream, type SseFrame } from "./sse.ts";
8
8
  export { queryDefaults, queryView, retryDelayMs, shouldRetry, type QueryDefaultsOptions, type QueryView, } from "./query.ts";
9
9
  export { ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps, } from "./error-boundary.tsx";
10
- export { createErrorDescriber, humanizeWait, type DescribedError, type ErrorArm, type ErrorContext, type ErrorCopy, type ErrorDescriberOptions, type RecoveryKind, type Translate, } from "./describe-error.ts";
10
+ export { createErrorDescriber, humanizeWait, type DescribedError, type ErrorArm, type ErrorContext, type ErrorCopy, type ErrorDescriberOptions, type OneLanguageDescriberOptions, type RecoveryKind, type Translate, } from "./describe-error.ts";
11
11
  export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, isStaleBuild, RELOAD_GUARD_KEY, reloadOnce, } from "./deploy-recovery.ts";
12
12
  export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
13
13
  export { applyBrandVars, i18nInitOptions, type I18nInitOptions } from "./i18n.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,EACX,KAAK,oBAAoB,EACzB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,EACX,KAAK,oBAAoB,EACzB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,qEAAqE;AACrE,8DAA8D;AAC9D,uEAAuE;AACvE,gEAAgE;AAChE,4EAA4E;AAC5E,4FAA4F;AAC5F,8FAA8F;AAC9F,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAoC,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAkB,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,GAGZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAQb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,qEAAqE;AACrE,8DAA8D;AAC9D,uEAAuE;AACvE,gEAAgE;AAChE,4EAA4E;AAC5E,4FAA4F;AAC5F,8FAA8F;AAC9F,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAoC,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAkB,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,GAGZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GASb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
package/dist/states.d.ts CHANGED
@@ -33,6 +33,11 @@ export interface EmptyStateProps {
33
33
  *
34
34
  * Only one donor had this, and it is the difference between a screen reader announcing a
35
35
  * page and announcing nothing.
36
+ *
37
+ * **The type cannot make your panel read it.** A panel that always renders an `<h3>` accepts
38
+ * `headingLevel={1}` and ignores it, and the page ships with no `h1`. The guide's own example
39
+ * app did exactly that on its 404 until a browser check found it. Render the heading from the
40
+ * prop: `` const Heading = `h${headingLevel}` as const ``.
36
41
  */
37
42
  headingLevel?: 1 | 2 | 3;
38
43
  className?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"states.d.ts","sourceRoot":"","sources":["../src/states.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,WAAW,EAAE,SAAS,CAAC;IACvB,yDAAyD;IACzD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,iCAAiC;IACjC,GAAG,EAAE,SAAS,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,SAAS,CAAC;IAClB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,gDAAgD;IAChD,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"states.d.ts","sourceRoot":"","sources":["../src/states.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,WAAW,EAAE,SAAS,CAAC;IACvB,yDAAyD;IACzD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,iCAAiC;IACjC,GAAG,EAAE,SAAS,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,SAAS,CAAC;IAClB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,gDAAgD;IAChD,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/react",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "The headless runtime under a Vite + React SPA: typed fetch client with single-flight refresh, auth store, query defaults, route guards, error boundary, stale-tab recovery, i18n bootstrap.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -386,3 +386,68 @@ describe("createErrorDescriber, when the code names the sentence", () => {
386
386
  expect(describeError(error).cause).toBe("errors.network");
387
387
  });
388
388
  });
389
+
390
+ /**
391
+ * The form for an app in one language. It had to pass `copyPrefix`, `messageKeyPrefix` and an
392
+ * empty `knownMessageKeys` to say "I have no catalog", and typing `t`'s argument by hand broke the
393
+ * inference. It writes its four sentences out instead.
394
+ */
395
+ describe("one language", () => {
396
+ const COPY = {
397
+ network: "We could not reach the server.",
398
+ networkHint: "Check your connection, then try again.",
399
+ unexpected: "Something went wrong on our side.",
400
+ retrySoon: "Try again in a moment.",
401
+ };
402
+ const describeError = createErrorDescriber({
403
+ copy: COPY,
404
+ formatWait: (secs) => `in ${secs}s`,
405
+ codes: {
406
+ RATE_LIMIT_EXCEEDED: ({ wait }) => ({ cause: "Too many at once.", fix: `Try ${wait}.` }),
407
+ },
408
+ });
409
+
410
+ it("takes its own sentences from `copy`", () => {
411
+ expect(describeError(new TypeError("Failed to fetch"))).toEqual({
412
+ cause: COPY.network,
413
+ fix: COPY.networkHint,
414
+ recover: "retry",
415
+ });
416
+ expect(describeError(new ApiError(502, null))).toMatchObject({
417
+ cause: COPY.unexpected,
418
+ fix: COPY.retrySoon,
419
+ });
420
+ });
421
+
422
+ // No catalog, so nothing the server names can reach the screen as a key. The server writes in
423
+ // the app's one language, so its own sentence is what an unmapped refusal shows.
424
+ it("shows the server's sentence and never a key the server named", () => {
425
+ const error = new ApiError(409, {
426
+ code: "SLUG_TAKEN",
427
+ message: "That address is taken.",
428
+ messageKey: "serverErrors.slugTaken",
429
+ });
430
+ expect(describeError(error).cause).toBe("That address is taken.");
431
+ });
432
+
433
+ it("still runs the arms and the stated wait", () => {
434
+ const error = new ApiError(
435
+ 429,
436
+ { code: "RATE_LIMIT_EXCEEDED", message: "slow down" },
437
+ { retryAfterSecs: 30 },
438
+ );
439
+ expect(describeError(error)).toMatchObject({
440
+ cause: "Too many at once.",
441
+ fix: "Try in 30s.",
442
+ recover: "wait",
443
+ });
444
+ });
445
+
446
+ it("refuses `copy` with a sentence missing", () => {
447
+ // @ts-expect-error -- `retrySoon` is missing, and a 5xx would show `undefined` without it.
448
+ createErrorDescriber({
449
+ copy: { network: "", networkHint: "", unexpected: "" },
450
+ formatWait: String,
451
+ });
452
+ });
453
+ });
@@ -175,14 +175,8 @@ export interface ErrorContext {
175
175
  recover: RecoveryKind;
176
176
  }
177
177
 
178
- export interface ErrorDescriberOptions<
179
- Prefix extends string,
180
- ServerPrefix extends string,
181
- ServerName extends string,
182
- > {
183
- t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName | PluralBase<ServerName>}`>;
184
- /** Catalog namespace for this module's own copy, e.g. `"errors."`. */
185
- copyPrefix: Prefix;
178
+ /** What both forms share: everything but where the words come from. */
179
+ interface DescriberRules {
186
180
  /**
187
181
  * How a stated wait becomes words, for the `wait` an arm reads off its context.
188
182
  *
@@ -193,6 +187,49 @@ export interface ErrorDescriberOptions<
193
187
  * `(secs) => formatIn(secs, locale)` — and the catalog you need is the one you can see.
194
188
  */
195
189
  formatWait: (secs: number) => string;
190
+ /**
191
+ * Per-code copy. Everything not listed falls through to the default arm.
192
+ *
193
+ * The arms look up the app's own keys, so they close over the app's `t` rather than being
194
+ * handed one — this module has no names for that copy and no business typing it.
195
+ *
196
+ * Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
197
+ * code that does not exist is a compile error at the arm rather than a branch that never runs.
198
+ */
199
+ codes?: Partial<Record<string, ErrorArm>>;
200
+ /**
201
+ * What to say when nothing in `codes` matches: a code this build has no arm for, or an answer
202
+ * with no envelope at all.
203
+ *
204
+ * The built-in arm hands the reader the server's own `message`, and for one API in the fleet
205
+ * that is right — its message IS the sentence a person should read. For another it is exactly
206
+ * wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
207
+ * ships three languages, so the built-in would put developer English in front of a reader who
208
+ * does not speak it. One default cannot be right for both, and the one baked in here was
209
+ * simply the first adopter's convention, mistaken for a fact.
210
+ *
211
+ * An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
212
+ * with no arm of its own could state its expiry and have that go unsaid.
213
+ */
214
+ fallback?: ErrorArm;
215
+ /**
216
+ * The codes that mean "this limit does not clear by waiting" — **the same list you hand
217
+ * `queryDefaults`**, because it answers the same question and a second copy is a second
218
+ * chance to disagree with the retry rule. Declare it once in the app and pass it twice.
219
+ */
220
+ durableLimitCodes?: readonly string[];
221
+ /** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
222
+ maxRetryWaitSecs?: number;
223
+ }
224
+
225
+ export interface ErrorDescriberOptions<
226
+ Prefix extends string,
227
+ ServerPrefix extends string,
228
+ ServerName extends string,
229
+ > extends DescriberRules {
230
+ t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName | PluralBase<ServerName>}`>;
231
+ /** Catalog namespace for this module's own copy, e.g. `"errors."`. */
232
+ copyPrefix: Prefix;
196
233
  /**
197
234
  * The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
198
235
  * ignored — the server names a sentence, it does not get to name any key in the app.
@@ -234,60 +271,39 @@ export interface ErrorDescriberOptions<
234
271
  localizeParams?: (
235
272
  params: Record<string, string | number> | undefined,
236
273
  ) => Record<string, unknown> | undefined;
237
- /**
238
- * Per-code copy. Everything not listed falls through to the default arm.
239
- *
240
- * The arms look up the app's own keys, so they close over the app's `t` rather than being
241
- * handed one — this module has no names for that copy and no business typing it.
242
- *
243
- * Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
244
- * code that does not exist is a compile error at the arm rather than a branch that never runs.
245
- */
246
- codes?: Partial<Record<string, ErrorArm>>;
247
- /**
248
- * What to say when nothing in `codes` matches: a code this build has no arm for, or an answer
249
- * with no envelope at all.
250
- *
251
- * The built-in arm hands the reader the server's own `message`, and for one API in the fleet
252
- * that is right — its message IS the sentence a person should read. For another it is exactly
253
- * wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
254
- * ships three languages, so the built-in would put developer English in front of a reader who
255
- * does not speak it. One default cannot be right for both, and the one baked in here was
256
- * simply the first adopter's convention, mistaken for a fact.
257
- *
258
- * An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
259
- * with no arm of its own could state its expiry and have that go unsaid.
260
- */
261
- fallback?: ErrorArm;
262
- /**
263
- * The codes that mean "this limit does not clear by waiting" — **the same list you hand
264
- * `queryDefaults`**, because it answers the same question and a second copy is a second
265
- * chance to disagree with the retry rule. Declare it once in the app and pass it twice.
266
- */
267
- durableLimitCodes?: readonly string[];
268
- /** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
269
- maxRetryWaitSecs?: number;
274
+ }
275
+
276
+ /**
277
+ * An app in one language writes its four sentences out, and needs no `t`, no prefixes and no
278
+ * catalog.
279
+ *
280
+ * With no catalog, the server cannot name a sentence the app carries, so a refusal with no arm of
281
+ * its own shows the server's `message`. In a one-language product that is the right sentence: the
282
+ * server writes in the same language the app does.
283
+ */
284
+ export interface OneLanguageDescriberOptions extends DescriberRules {
285
+ copy: Record<CopyName, string>;
270
286
  }
271
287
 
272
288
  export type ErrorArm = (ctx: ErrorContext) => ErrorCopy;
273
289
 
274
- /** Build the describer. */
275
- export function createErrorDescriber<
290
+ /** Where the describer's words come from: its own four, and the server's when it named one. */
291
+ interface Words {
292
+ copyOf: (name: CopyName) => string;
293
+ serverSentence: (error: ApiError) => string | null;
294
+ }
295
+
296
+ function translatedWords<
276
297
  Prefix extends string,
277
298
  ServerPrefix extends string,
278
299
  ServerName extends string,
279
300
  >({
280
301
  t,
281
302
  copyPrefix,
282
- formatWait,
283
303
  messageKeyPrefix,
284
304
  knownMessageKeys,
285
305
  localizeParams = (params) => params,
286
- codes = {},
287
- fallback,
288
- durableLimitCodes = [],
289
- maxRetryWaitSecs,
290
- }: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError {
306
+ }: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): Words {
291
307
  // Both spellings of every name: the one the catalog stores, and — for a plural — the one the
292
308
  // server sends. `Object.keys` and not `in`, which would have accepted "toString".
293
309
  const known = new Set<string>();
@@ -300,20 +316,49 @@ export function createErrorDescriber<
300
316
  // keys and their plural bases, which is exactly what the predicate claims.
301
317
  const has = (name: string): name is ServerName | PluralBase<ServerName> => known.has(name);
302
318
 
303
- function serverSentence(error: ApiError): string | null {
304
- const lookup = (named: string | undefined): string | null => {
305
- if (named === undefined || !named.startsWith(messageKeyPrefix)) return null;
306
- const name = named.slice(messageKeyPrefix.length);
307
- return has(name) ? t(`${messageKeyPrefix}${name}`, localizeParams(error.params)) : null;
308
- };
309
- // `messageKey` first: an API that sends both means the key, and the code is what the UI
310
- // switches on. The code is only consulted when there is no key to prefer, and it is spelled
311
- // under the same prefix so the gate is identical.
312
- return (
313
- lookup(error.messageKey) ??
314
- (error.code === undefined ? null : lookup(`${messageKeyPrefix}${error.code}`))
315
- );
316
- }
319
+ return {
320
+ copyOf: (name) => t(`${copyPrefix}${name}`),
321
+ serverSentence(error) {
322
+ const lookup = (named: string | undefined): string | null => {
323
+ if (named === undefined || !named.startsWith(messageKeyPrefix)) return null;
324
+ const name = named.slice(messageKeyPrefix.length);
325
+ return has(name) ? t(`${messageKeyPrefix}${name}`, localizeParams(error.params)) : null;
326
+ };
327
+ // `messageKey` first: an API that sends both means the key, and the code is what the UI
328
+ // switches on. The code is only consulted when there is no key to prefer, and it is spelled
329
+ // under the same prefix so the gate is identical.
330
+ return (
331
+ lookup(error.messageKey) ??
332
+ (error.code === undefined ? null : lookup(`${messageKeyPrefix}${error.code}`))
333
+ );
334
+ },
335
+ };
336
+ }
337
+
338
+ /** Build the describer for an app in one language. */
339
+ export function createErrorDescriber(
340
+ options: OneLanguageDescriberOptions,
341
+ ): (error: unknown) => DescribedError;
342
+ /** Build the describer for an app that translates. */
343
+ export function createErrorDescriber<
344
+ Prefix extends string,
345
+ ServerPrefix extends string,
346
+ ServerName extends string,
347
+ >(
348
+ options: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>,
349
+ ): (error: unknown) => DescribedError;
350
+ export function createErrorDescriber<
351
+ Prefix extends string,
352
+ ServerPrefix extends string,
353
+ ServerName extends string,
354
+ >(
355
+ options: OneLanguageDescriberOptions | ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>,
356
+ ): (error: unknown) => DescribedError {
357
+ const { formatWait, codes = {}, fallback, durableLimitCodes = [], maxRetryWaitSecs } = options;
358
+ const { copyOf, serverSentence }: Words =
359
+ "copy" in options
360
+ ? { copyOf: (name) => options.copy[name], serverSentence: () => null }
361
+ : translatedWords(options);
317
362
 
318
363
  /**
319
364
  * Which control to offer, decided once and never switched on separately.
@@ -351,9 +396,8 @@ export function createErrorDescriber<
351
396
  const unmapped: ErrorArm =
352
397
  fallback ??
353
398
  (({ error, says }) => ({
354
- cause:
355
- says ?? ((error.code === undefined ? "" : error.message) || t(`${copyPrefix}unexpected`)),
356
- fix: error.status >= 500 ? t(`${copyPrefix}retrySoon`) : undefined,
399
+ cause: says ?? ((error.code === undefined ? "" : error.message) || copyOf("unexpected")),
400
+ fix: error.status >= 500 ? copyOf("retrySoon") : undefined,
357
401
  }));
358
402
 
359
403
  return function describeError(error: unknown): DescribedError {
@@ -362,10 +406,10 @@ export function createErrorDescriber<
362
406
  // never worth showing a stack trace for. `shouldRetry` answers a non-response the same way,
363
407
  // so these two agree here without either being told about the other.
364
408
  return {
365
- cause: t(`${copyPrefix}network`),
409
+ cause: copyOf("network"),
366
410
  // The catalog KEY keeps its name while the field becomes `fix`: renaming it would cost
367
411
  // every adopter a JSON edit in every language, for a string no caller ever types.
368
- fix: t(`${copyPrefix}networkHint`),
412
+ fix: copyOf("networkHint"),
369
413
  recover: "retry",
370
414
  };
371
415
  }
package/src/index.ts CHANGED
@@ -68,6 +68,7 @@ export {
68
68
  type ErrorContext,
69
69
  type ErrorCopy,
70
70
  type ErrorDescriberOptions,
71
+ type OneLanguageDescriberOptions,
71
72
  type RecoveryKind,
72
73
  type Translate,
73
74
  } from "./describe-error.ts";
package/src/states.ts CHANGED
@@ -35,6 +35,11 @@ export interface EmptyStateProps {
35
35
  *
36
36
  * Only one donor had this, and it is the difference between a screen reader announcing a
37
37
  * page and announcing nothing.
38
+ *
39
+ * **The type cannot make your panel read it.** A panel that always renders an `<h3>` accepts
40
+ * `headingLevel={1}` and ignores it, and the page ships with no `h1`. The guide's own example
41
+ * app did exactly that on its 404 until a browser check found it. Render the heading from the
42
+ * prop: `` const Heading = `h${headingLevel}` as const ``.
38
43
  */
39
44
  headingLevel?: 1 | 2 | 3;
40
45
  className?: string;