@stapel/chat-react 0.1.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/CHANGELOG.md +1 -0
- package/MODULE.md +118 -0
- package/README.md +126 -0
- package/dist/api/chatApi.d.ts +49 -0
- package/dist/api/chatApi.d.ts.map +1 -0
- package/dist/api/chatApi.js +46 -0
- package/dist/api/chatApi.js.map +1 -0
- package/dist/api/extensions.d.ts +19 -0
- package/dist/api/extensions.d.ts.map +1 -0
- package/dist/api/extensions.js +19 -0
- package/dist/api/extensions.js.map +1 -0
- package/dist/api/generated/schema.d.ts +547 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/types.d.ts +75 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/default/ConversationListPanel.d.ts +22 -0
- package/dist/default/ConversationListPanel.d.ts.map +1 -0
- package/dist/default/ConversationListPanel.js +49 -0
- package/dist/default/ConversationListPanel.js.map +1 -0
- package/dist/default/ConversationThreadPanel.d.ts +21 -0
- package/dist/default/ConversationThreadPanel.d.ts.map +1 -0
- package/dist/default/ConversationThreadPanel.js +51 -0
- package/dist/default/ConversationThreadPanel.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +8 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +26 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/StartChatButton.d.ts +17 -0
- package/dist/default/StartChatButton.d.ts.map +1 -0
- package/dist/default/StartChatButton.js +18 -0
- package/dist/default/StartChatButton.js.map +1 -0
- package/dist/default/index.d.ts +21 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +18 -0
- package/dist/default/index.js.map +1 -0
- package/dist/flows/errors.d.ts +12 -0
- package/dist/flows/errors.d.ts.map +1 -0
- package/dist/flows/errors.js +13 -0
- package/dist/flows/errors.js.map +1 -0
- package/dist/flows/freshness.d.ts +69 -0
- package/dist/flows/freshness.d.ts.map +1 -0
- package/dist/flows/freshness.js +227 -0
- package/dist/flows/freshness.js.map +1 -0
- package/dist/flows/registry.d.ts +26 -0
- package/dist/flows/registry.d.ts.map +1 -0
- package/dist/flows/registry.js +24 -0
- package/dist/flows/registry.js.map +1 -0
- package/dist/headless/ChatProvider.d.ts +22 -0
- package/dist/headless/ChatProvider.d.ts.map +1 -0
- package/dist/headless/ChatProvider.js +18 -0
- package/dist/headless/ChatProvider.js.map +1 -0
- package/dist/headless/ConversationList.d.ts +51 -0
- package/dist/headless/ConversationList.d.ts.map +1 -0
- package/dist/headless/ConversationList.js +47 -0
- package/dist/headless/ConversationList.js.map +1 -0
- package/dist/headless/ConversationThread.d.ts +50 -0
- package/dist/headless/ConversationThread.d.ts.map +1 -0
- package/dist/headless/ConversationThread.js +64 -0
- package/dist/headless/ConversationThread.js.map +1 -0
- package/dist/headless/MessageComposer.d.ts +39 -0
- package/dist/headless/MessageComposer.d.ts.map +1 -0
- package/dist/headless/MessageComposer.js +45 -0
- package/dist/headless/MessageComposer.js.map +1 -0
- package/dist/headless/StartDirectChat.d.ts +40 -0
- package/dist/headless/StartDirectChat.d.ts.map +1 -0
- package/dist/headless/StartDirectChat.js +46 -0
- package/dist/headless/StartDirectChat.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +10 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +20 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +23 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +75 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +58 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +359 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +183 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +58 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +78 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +130 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +28 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +80 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/limits.d.ts +12 -0
- package/dist/model/limits.d.ts.map +1 -0
- package/dist/model/limits.js +12 -0
- package/dist/model/limits.js.map +1 -0
- package/dist/model/mutations.d.ts +66 -0
- package/dist/model/mutations.d.ts.map +1 -0
- package/dist/model/mutations.js +156 -0
- package/dist/model/mutations.js.map +1 -0
- package/dist/model/queries.d.ts +33 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +93 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +8 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +26 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/readMarker.d.ts +25 -0
- package/dist/model/readMarker.d.ts.map +1 -0
- package/dist/model/readMarker.js +31 -0
- package/dist/model/readMarker.js.map +1 -0
- package/dist/model/runtime.d.ts +50 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +27 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/model/threadWindow.d.ts +95 -0
- package/dist/model/threadWindow.d.ts.map +1 -0
- package/dist/model/threadWindow.js +100 -0
- package/dist/model/threadWindow.js.map +1 -0
- package/dist/nav/manifest.d.ts +23 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +15 -0
- package/dist/nav/manifest.js.map +1 -0
- package/dist/realtime/chatSocket.d.ts +93 -0
- package/dist/realtime/chatSocket.d.ts.map +1 -0
- package/dist/realtime/chatSocket.js +199 -0
- package/dist/realtime/chatSocket.js.map +1 -0
- package/dist/realtime/frames.d.ts +123 -0
- package/dist/realtime/frames.d.ts.map +1 -0
- package/dist/realtime/frames.js +123 -0
- package/dist/realtime/frames.js.map +1 -0
- package/dist/realtime/streams.d.ts +49 -0
- package/dist/realtime/streams.d.ts.map +1 -0
- package/dist/realtime/streams.js +52 -0
- package/dist/realtime/streams.js.map +1 -0
- package/llms.txt +89 -0
- package/manifest.json +733 -0
- package/nav-manifest.json +25 -0
- package/package.json +114 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/chatApi.ts +142 -0
- package/src/api/extensions.ts +18 -0
- package/src/api/generated/schema.ts +549 -0
- package/src/api/types.ts +87 -0
- package/src/default/ConversationListPanel.tsx +163 -0
- package/src/default/ConversationThreadPanel.tsx +245 -0
- package/src/default/ErrorAlert.tsx +43 -0
- package/src/default/StartChatButton.tsx +78 -0
- package/src/default/index.ts +20 -0
- package/src/flows/errors.ts +16 -0
- package/src/flows/freshness.ts +315 -0
- package/src/flows/registry.ts +36 -0
- package/src/headless/ChatProvider.tsx +23 -0
- package/src/headless/ConversationList.tsx +92 -0
- package/src/headless/ConversationThread.tsx +107 -0
- package/src/headless/MessageComposer.tsx +86 -0
- package/src/headless/StartDirectChat.tsx +75 -0
- package/src/i18n/errorsMap.ts +31 -0
- package/src/i18n/es.ts +95 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +214 -0
- package/src/i18n/generated/errors.json +408 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +152 -0
- package/src/i18n/ru.ts +98 -0
- package/src/index.ts +189 -0
- package/src/model/context.tsx +25 -0
- package/src/model/limits.ts +11 -0
- package/src/model/mutations.ts +208 -0
- package/src/model/queries.ts +120 -0
- package/src/model/queryKeys.ts +32 -0
- package/src/model/readMarker.ts +32 -0
- package/src/model/runtime.ts +90 -0
- package/src/model/threadWindow.ts +173 -0
- package/src/nav/manifest.ts +36 -0
- package/src/realtime/chatSocket.ts +304 -0
- package/src/realtime/frames.ts +229 -0
- package/src/realtime/streams.ts +85 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { actionAvailable, actionBlocked, firstBlock } from "@stapel/core";
|
|
4
|
+
import type { ActionAvailability } from "@stapel/core";
|
|
5
|
+
import type { Conversation } from "../api/types.js";
|
|
6
|
+
import { useStartDirectChat } from "../model/mutations.js";
|
|
7
|
+
import { CHAT_I18N_KEYS } from "../i18n/keys.js";
|
|
8
|
+
|
|
9
|
+
/** Render-prop bag for {@link StartDirectChat}. */
|
|
10
|
+
export interface StartDirectChatBag {
|
|
11
|
+
/** Blocked reasons: no seller on the listing, or the viewer IS the seller. */
|
|
12
|
+
readonly availability: ActionAvailability;
|
|
13
|
+
readonly isStarting: boolean;
|
|
14
|
+
/** The thread, once opened. */
|
|
15
|
+
readonly conversation: Conversation | undefined;
|
|
16
|
+
readonly error: unknown;
|
|
17
|
+
start(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* "Message the seller" — the buyer's entry into chat, headless.
|
|
22
|
+
*
|
|
23
|
+
* One press, one durable outcome: the direct thread with that person, created
|
|
24
|
+
* if it does not exist and returned if it does. The idempotency is the
|
|
25
|
+
* module's own (`direct_key` over the participant pair, under a unique
|
|
26
|
+
* constraint), so a double click, two tabs, or a second listing by the same
|
|
27
|
+
* seller all land in the same conversation rather than fanning out into
|
|
28
|
+
* near-duplicate threads.
|
|
29
|
+
*
|
|
30
|
+
* WHAT THIS BUTTON CANNOT DO, AND WHY IT SAYS SO HERE. It cannot open a
|
|
31
|
+
* thread ABOUT a listing. The thread is keyed by the pair of people, and
|
|
32
|
+
* `CreateConversationRequest.scope_key` — which looks like the place to put a
|
|
33
|
+
* listing id — is ignored by the server (`api/extensions.ts`). A host that
|
|
34
|
+
* wants the listing named does it in the first message, via
|
|
35
|
+
* `<MessageComposer>`'s initial value.
|
|
36
|
+
*/
|
|
37
|
+
export function StartDirectChat(props: {
|
|
38
|
+
/** The other person. `null`/`undefined` blocks the control with a reason. */
|
|
39
|
+
sellerId: string | null | undefined;
|
|
40
|
+
/** The viewer, when known — writing to yourself is blocked with a reason. */
|
|
41
|
+
viewerId?: string | null;
|
|
42
|
+
/** Navigate to the thread. Called once, with the opened conversation. */
|
|
43
|
+
onOpened?: (conversation: Conversation) => void;
|
|
44
|
+
children: (bag: StartDirectChatBag) => ReactNode;
|
|
45
|
+
}): ReactNode {
|
|
46
|
+
const start = useStartDirectChat();
|
|
47
|
+
const { sellerId, viewerId, onOpened } = props;
|
|
48
|
+
|
|
49
|
+
const availability: ActionAvailability = firstBlock(
|
|
50
|
+
sellerId === null || sellerId === undefined || sellerId.length === 0
|
|
51
|
+
? actionBlocked(CHAT_I18N_KEYS.startBlockedUnknownSeller)
|
|
52
|
+
: actionAvailable(),
|
|
53
|
+
viewerId != null && viewerId === sellerId
|
|
54
|
+
? actionBlocked(CHAT_I18N_KEYS.startBlockedSelf)
|
|
55
|
+
: actionAvailable()
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const { mutate, isPending, data, error } = start;
|
|
59
|
+
const doStart = useCallback((): void => {
|
|
60
|
+
if (!availability.available || isPending) return;
|
|
61
|
+
if (sellerId === null || sellerId === undefined) return;
|
|
62
|
+
mutate(
|
|
63
|
+
{ userId: sellerId },
|
|
64
|
+
{ onSuccess: (conversation) => onOpened?.(conversation) }
|
|
65
|
+
);
|
|
66
|
+
}, [availability.available, isPending, mutate, sellerId, onOpened]);
|
|
67
|
+
|
|
68
|
+
return props.children({
|
|
69
|
+
availability,
|
|
70
|
+
isStarting: isPending,
|
|
71
|
+
conversation: data,
|
|
72
|
+
error,
|
|
73
|
+
start: doStart,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pair's error map (frontend-standard §4 checklist #7, frontend-core §2.5):
|
|
3
|
+
* the generated `code → { status, params, remediation, en }` catalog plus a
|
|
4
|
+
* tiny `explain()` lookup. Backs the manifest `errors` block and gives hosts a
|
|
5
|
+
* mechanical UX branch beside `t(code, params)`. The map itself is generated
|
|
6
|
+
* from the backend registry (`pnpm gen:errors`); this file only adds the
|
|
7
|
+
* lookup helper and re-exports the public surface.
|
|
8
|
+
*/
|
|
9
|
+
import { CHAT_ERRORS } from "./generated/errors.gen.js";
|
|
10
|
+
import type { Remediation } from "./generated/errors.gen.js";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
CHAT_ERRORS,
|
|
14
|
+
CHAT_ERROR_CODES,
|
|
15
|
+
chatErrorBundleEn,
|
|
16
|
+
} from "./generated/errors.gen.js";
|
|
17
|
+
export type {
|
|
18
|
+
ChatErrorCode,
|
|
19
|
+
ChatErrorSpec,
|
|
20
|
+
Remediation,
|
|
21
|
+
} from "./generated/errors.gen.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a backend error code to its remediation hint, or `undefined` for a
|
|
25
|
+
* code this module doesn't know (e.g. a cross-cutting `stapel.http.*`
|
|
26
|
+
* fallback). Zero guessing at runtime — a static lookup over the generated map.
|
|
27
|
+
*/
|
|
28
|
+
export function explainChatError(code: string): Remediation | undefined {
|
|
29
|
+
return (CHAT_ERRORS as Record<string, { remediation: Remediation }>)[code]
|
|
30
|
+
?.remediation;
|
|
31
|
+
}
|
package/src/i18n/es.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { I18nDictionary, I18nEngine } from "@stapel/core";
|
|
2
|
+
import { chatI18nBundleEn } from "./keys.js";
|
|
3
|
+
import { chatErrorBundleEs } from "./generated/errors.es.gen.js";
|
|
4
|
+
|
|
5
|
+
export { chatErrorBundleEs } from "./generated/errors.es.gen.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Spanish bundle for chat-react — the pair's `es` locale, shipped as the
|
|
9
|
+
* `@stapel/chat-react/i18n/es` subpath (i18n-shipping.md §2) so the locale is
|
|
10
|
+
* opt-in.
|
|
11
|
+
*
|
|
12
|
+
* Same two sources as the `ru` bundle: the generated `chatErrorBundleEs`
|
|
13
|
+
* carries the keys stapel-core owns; the 12 keys stapel-chat owns are authored
|
|
14
|
+
* here because the module ships no `translations/` directory (see `ru.ts` for
|
|
15
|
+
* the full note). Unlike notifications-react's es bundle, this one DOES carry
|
|
16
|
+
* hand-written UI copy — a marketplace's buyer-to-seller chat is the surface
|
|
17
|
+
* where a half-translated screen is most obvious.
|
|
18
|
+
*/
|
|
19
|
+
export const chatI18nBundleEs: I18nDictionary = {
|
|
20
|
+
// Backend error codes — generated es texts for every key core owns.
|
|
21
|
+
...chatErrorBundleEs,
|
|
22
|
+
|
|
23
|
+
// Backend error codes stapel-chat owns — authored here (see the note above).
|
|
24
|
+
"error.400.chat_attachments_disabled":
|
|
25
|
+
"Los archivos adjuntos no están habilitados en esta instalación",
|
|
26
|
+
"error.400.chat_body_too_long": "El mensaje supera la longitud máxima permitida",
|
|
27
|
+
"error.400.chat_empty_message":
|
|
28
|
+
"Un mensaje debe llevar texto o al menos un archivo adjunto",
|
|
29
|
+
"error.400.chat_invalid_direct":
|
|
30
|
+
"Una conversación directa necesita exactamente un interlocutor",
|
|
31
|
+
"error.400.chat_invalid_kind": "Tipo de conversación desconocido",
|
|
32
|
+
"error.400.chat_invalid_reply":
|
|
33
|
+
"El mensaje al que respondes no pertenece a esta conversación",
|
|
34
|
+
"error.400.chat_kind_disabled":
|
|
35
|
+
"Este tipo de conversación no está habilitado en esta instalación",
|
|
36
|
+
"error.400.chat_not_support":
|
|
37
|
+
"Esta acción solo se aplica a conversaciones de soporte",
|
|
38
|
+
"error.403.chat_not_operator":
|
|
39
|
+
"Solo un operador de soporte puede realizar esta acción",
|
|
40
|
+
"error.403.chat_not_participant": "No participas en esta conversación",
|
|
41
|
+
"error.404.chat_conversation_not_found": "Conversación no encontrada",
|
|
42
|
+
"error.409.chat_already_assigned": "Esta conversación de soporte ya está asignada",
|
|
43
|
+
|
|
44
|
+
// chat-react UI (hand-written es mirror of the en copy in keys.ts)
|
|
45
|
+
"chat.error.unknown": "Algo ha salido mal. Inténtalo de nuevo.",
|
|
46
|
+
|
|
47
|
+
"chat.list.title": "Mensajes",
|
|
48
|
+
"chat.list.empty": "Todavía no hay conversaciones.",
|
|
49
|
+
"chat.list.loading": "Cargando conversaciones…",
|
|
50
|
+
"chat.list.load_more": "Cargar más",
|
|
51
|
+
"chat.list.end": "Esto es todo.",
|
|
52
|
+
"chat.list.retry": "Reintentar",
|
|
53
|
+
"chat.list.unread": "{count} sin leer",
|
|
54
|
+
"chat.list.open": "Abrir",
|
|
55
|
+
|
|
56
|
+
"chat.kind.direct": "Mensaje directo",
|
|
57
|
+
"chat.kind.group": "Grupo",
|
|
58
|
+
"chat.kind.support": "Soporte",
|
|
59
|
+
|
|
60
|
+
"chat.thread.loading": "Cargando mensajes…",
|
|
61
|
+
"chat.thread.empty": "Todavía no hay mensajes. Saluda.",
|
|
62
|
+
"chat.thread.retry": "Reintentar",
|
|
63
|
+
"chat.thread.load_older": "Ver mensajes anteriores",
|
|
64
|
+
"chat.thread.beginning": "Este es el principio de la conversación.",
|
|
65
|
+
"chat.thread.system": "Sistema",
|
|
66
|
+
|
|
67
|
+
"chat.composer.placeholder": "Escribe un mensaje…",
|
|
68
|
+
"chat.composer.send": "Enviar",
|
|
69
|
+
"chat.composer.sending": "Enviando…",
|
|
70
|
+
"chat.composer.blocked.empty": "Escribe algo primero.",
|
|
71
|
+
"chat.composer.blocked.too_long":
|
|
72
|
+
"Eso supera los {max} caracteres — acórtalo un poco.",
|
|
73
|
+
|
|
74
|
+
"chat.start.button": "Escribir al vendedor",
|
|
75
|
+
"chat.start.starting": "Abriendo…",
|
|
76
|
+
"chat.start.blocked.self": "Este anuncio es tuyo.",
|
|
77
|
+
"chat.start.blocked.unknown_seller": "Este anuncio no tiene vendedor a quien escribir.",
|
|
78
|
+
|
|
79
|
+
"chat.transport.live": "En directo",
|
|
80
|
+
"chat.transport.polling": "Actualizando cada pocos segundos",
|
|
81
|
+
"chat.transport.idle": "En pausa",
|
|
82
|
+
|
|
83
|
+
"chat.nav.conversations": "Mensajes",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Register the pair's `es` locale into a core i18n engine (call once at
|
|
88
|
+
* startup, after {@link registerChatI18n}). The en floor is registered UNDER
|
|
89
|
+
* the es texts inside the `es` locale, so any key the es bundle does not carry
|
|
90
|
+
* degrades to its English text rather than to a raw key.
|
|
91
|
+
*/
|
|
92
|
+
export function registerChatI18nEs(engine: I18nEngine): void {
|
|
93
|
+
engine.registerBundle("es", chatI18nBundleEn);
|
|
94
|
+
engine.registerBundle("es", chatI18nBundleEs);
|
|
95
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/gen-errors.mjs — do not edit by hand.
|
|
2
|
+
// Source: stapel-auth canonical errors.json (backend codegen artifact).
|
|
3
|
+
// Regenerate: pnpm gen:errors · Drift gate: pnpm gen:errors:check
|
|
4
|
+
// Locale source: backend translations/errors.es.json (locale catalog
|
|
5
|
+
// beside the canonical errors.json — i18n-shipping.md §1).
|
|
6
|
+
import type { ChatErrorCode } from "./errors.gen.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `es` texts for the backend error codes this catalog carries.
|
|
10
|
+
*
|
|
11
|
+
* PARTIAL, and deliberately typed to say so: 12 key(s) owned by
|
|
12
|
+
* stapel_chat are absent, because that owner ships no locale catalog
|
|
13
|
+
* (ERRORS_LOCALE_EXEMPT_OWNERS). English for them still comes from the registry
|
|
14
|
+
* artifact via the en bundle; the pair layers its own authored `es`
|
|
15
|
+
* strings over this one until upstream ships translations. `Partial` is what
|
|
16
|
+
* makes the gap visible to TypeScript instead of a silent English fallback.
|
|
17
|
+
*
|
|
18
|
+
* Import from the pair's `./i18n/es` subpath, NOT the main entry, so
|
|
19
|
+
* the locale stays out of hosts that don't ship it.
|
|
20
|
+
*/
|
|
21
|
+
export const chatErrorBundleEs: Partial<Record<ChatErrorCode, string>> = {
|
|
22
|
+
"error.400.bad_request": "Solicitud incorrecta",
|
|
23
|
+
"error.400.captcha_invalid": "La verificación del captcha ha fallado. Inténtalo de nuevo.",
|
|
24
|
+
"error.400.captcha_required": "Se requiere el token del captcha.",
|
|
25
|
+
"error.400.expected_list": "Se esperaba una lista de elementos",
|
|
26
|
+
"error.400.field.blank": "{field} no puede estar vacío",
|
|
27
|
+
"error.400.field.does_not_exist": "{field} no existe",
|
|
28
|
+
"error.400.field.invalid": "{field} no es válido",
|
|
29
|
+
"error.400.field.invalid_choice": "{field} no es una opción válida",
|
|
30
|
+
"error.400.field.max_length": "{field} debe tener como máximo {max_length} caracteres",
|
|
31
|
+
"error.400.field.max_value": "{field} debe ser como máximo {max_value}",
|
|
32
|
+
"error.400.field.min_length": "{field} debe tener al menos {min_length} caracteres",
|
|
33
|
+
"error.400.field.min_value": "{field} debe ser como mínimo {min_value}",
|
|
34
|
+
"error.400.field.null": "{field} no puede ser nulo",
|
|
35
|
+
"error.400.field.required": "{field} es obligatorio",
|
|
36
|
+
"error.400.field.unique": "{field} debe ser único",
|
|
37
|
+
"error.400.invalid_ad_id": "ID de anuncio no válido",
|
|
38
|
+
"error.400.validation_error": "Error de validación",
|
|
39
|
+
"error.400.verification_failed": "La verificación ha fallado",
|
|
40
|
+
"error.400.verification_invalid_factor": "Este factor de verificación no está disponible",
|
|
41
|
+
"error.401.unauthorized": "Se requiere autenticación",
|
|
42
|
+
"error.402.payment_required": "Se requiere pago",
|
|
43
|
+
"error.403.forbidden": "No tienes permiso para realizar esta acción",
|
|
44
|
+
"error.403.network_blocked": "No se permiten solicitudes desde esta red.",
|
|
45
|
+
"error.403.verification_enrollment_required": "Es necesario registrar un factor de verificación.",
|
|
46
|
+
"error.403.verification_required": "Se requiere verificación adicional",
|
|
47
|
+
"error.404.ad_not_found": "Anuncio no encontrado",
|
|
48
|
+
"error.404.not_found": "Recurso solicitado no encontrado",
|
|
49
|
+
"error.404.verification_challenge_not_found": "Desafío de verificación no encontrado o caducado",
|
|
50
|
+
"error.405.method_not_allowed": "Método no permitido",
|
|
51
|
+
"error.406.not_acceptable": "No aceptable",
|
|
52
|
+
"error.408.request_timeout": "Tiempo de espera de la solicitud agotado",
|
|
53
|
+
"error.409.conflict": "El recurso ya existe",
|
|
54
|
+
"error.410.gone": "El recurso se ha eliminado permanentemente",
|
|
55
|
+
"error.413.payload_too_large": "El cuerpo de la solicitud es demasiado grande",
|
|
56
|
+
"error.415.unsupported_media_type": "Tipo de contenido no compatible",
|
|
57
|
+
"error.422.unprocessable_entity": "Entidad no procesable",
|
|
58
|
+
"error.423.locked": "El recurso está bloqueado",
|
|
59
|
+
"error.423.verification_locked": "Demasiados intentos fallidos — verificación bloqueada",
|
|
60
|
+
"error.429.rate_limit": "Demasiados intentos. Inténtalo de nuevo en {retry_after_minutes} minutos.",
|
|
61
|
+
"error.429.too_many_requests": "Demasiadas solicitudes. Inténtalo de nuevo más tarde.",
|
|
62
|
+
"error.500.internal": "Algo salió mal",
|
|
63
|
+
"error.503.mandate_unavailable": "No se puede verificar el mandato del espacio de trabajo",
|
|
64
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/gen-errors.mjs — do not edit by hand.
|
|
2
|
+
// Source: stapel-auth canonical errors.json (backend codegen artifact).
|
|
3
|
+
// Regenerate: pnpm gen:errors · Drift gate: pnpm gen:errors:check
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Remediation vocabulary (frontend-core-architecture §2.5). Declared by the
|
|
7
|
+
* backend on the error registry and consumed verbatim from its `errors.json`
|
|
8
|
+
* artifact. A host maps it to UX — retryable → "try again"; `wait_and_retry` →
|
|
9
|
+
* timer from `params.retry_after_minutes`; `verify` → the step-up seam;
|
|
10
|
+
* `fix_input` → highlight the offending field.
|
|
11
|
+
*/
|
|
12
|
+
export type Remediation =
|
|
13
|
+
| "retry"
|
|
14
|
+
| "wait_and_retry"
|
|
15
|
+
| "reauthenticate"
|
|
16
|
+
| "verify"
|
|
17
|
+
| "fix_input"
|
|
18
|
+
| "contact_support"
|
|
19
|
+
| "bug";
|
|
20
|
+
|
|
21
|
+
export interface ChatErrorSpec {
|
|
22
|
+
/** HTTP status the backend raises this key with. */
|
|
23
|
+
readonly status: number;
|
|
24
|
+
/** `{param}` interpolation slots present in the message. */
|
|
25
|
+
readonly params: readonly string[];
|
|
26
|
+
/** Remediation hint declared by the backend (see {@link Remediation}). */
|
|
27
|
+
readonly remediation: Remediation;
|
|
28
|
+
/** English fallback (source: stapel-auth errors.json). */
|
|
29
|
+
readonly en: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The CHAT_ERRORS map — every `error.*` key stapel-auth can raise, keyed by its
|
|
34
|
+
* canonical code. The single source of truth behind the en fallback bundle, the
|
|
35
|
+
* remediation lookup, and the manifest `errors` block.
|
|
36
|
+
*/
|
|
37
|
+
export const CHAT_ERRORS = {
|
|
38
|
+
"error.400.bad_request": { status: 400, params: [], remediation: "fix_input", en: "Bad request" },
|
|
39
|
+
"error.400.captcha_invalid": { status: 400, params: [], remediation: "retry", en: "Captcha verification failed. Please try again." },
|
|
40
|
+
"error.400.captcha_required": { status: 400, params: [], remediation: "retry", en: "Captcha token is required." },
|
|
41
|
+
"error.400.chat_attachments_disabled": { status: 400, params: [], remediation: "fix_input", en: "Attachments are not enabled in this deployment" },
|
|
42
|
+
"error.400.chat_body_too_long": { status: 400, params: [], remediation: "fix_input", en: "Message body exceeds the maximum allowed length" },
|
|
43
|
+
"error.400.chat_empty_message": { status: 400, params: [], remediation: "fix_input", en: "A message must carry a body or at least one attachment" },
|
|
44
|
+
"error.400.chat_invalid_direct": { status: 400, params: [], remediation: "fix_input", en: "A direct conversation needs exactly one other participant" },
|
|
45
|
+
"error.400.chat_invalid_kind": { status: 400, params: [], remediation: "fix_input", en: "Unknown conversation kind" },
|
|
46
|
+
"error.400.chat_invalid_reply": { status: 400, params: [], remediation: "fix_input", en: "The replied-to message does not belong to this conversation" },
|
|
47
|
+
"error.400.chat_kind_disabled": { status: 400, params: [], remediation: "fix_input", en: "This conversation kind is not enabled in this deployment" },
|
|
48
|
+
"error.400.chat_not_support": { status: 400, params: [], remediation: "fix_input", en: "This operation applies only to support conversations" },
|
|
49
|
+
"error.400.expected_list": { status: 400, params: [], remediation: "fix_input", en: "Expected a list of items" },
|
|
50
|
+
"error.400.field.blank": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} may not be blank" },
|
|
51
|
+
"error.400.field.does_not_exist": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} does not exist" },
|
|
52
|
+
"error.400.field.invalid": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} is invalid" },
|
|
53
|
+
"error.400.field.invalid_choice": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} is not a valid choice" },
|
|
54
|
+
"error.400.field.max_length": { status: 400, params: ["field","max_length"], remediation: "fix_input", en: "{field} must be at most {max_length} characters" },
|
|
55
|
+
"error.400.field.max_value": { status: 400, params: ["field","max_value"], remediation: "fix_input", en: "{field} must be at most {max_value}" },
|
|
56
|
+
"error.400.field.min_length": { status: 400, params: ["field","min_length"], remediation: "fix_input", en: "{field} must be at least {min_length} characters" },
|
|
57
|
+
"error.400.field.min_value": { status: 400, params: ["field","min_value"], remediation: "fix_input", en: "{field} must be at least {min_value}" },
|
|
58
|
+
"error.400.field.null": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} may not be null" },
|
|
59
|
+
"error.400.field.required": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} is required" },
|
|
60
|
+
"error.400.field.unique": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} must be unique" },
|
|
61
|
+
"error.400.invalid_ad_id": { status: 400, params: [], remediation: "fix_input", en: "Invalid advertisement ID" },
|
|
62
|
+
"error.400.validation_error": { status: 400, params: [], remediation: "fix_input", en: "Validation error" },
|
|
63
|
+
"error.400.verification_failed": { status: 400, params: [], remediation: "verify", en: "Verification failed" },
|
|
64
|
+
"error.400.verification_invalid_factor": { status: 400, params: [], remediation: "verify", en: "This verification factor is not available" },
|
|
65
|
+
"error.401.unauthorized": { status: 401, params: [], remediation: "reauthenticate", en: "Authentication required" },
|
|
66
|
+
"error.402.payment_required": { status: 402, params: [], remediation: "retry", en: "Payment required" },
|
|
67
|
+
"error.403.chat_not_operator": { status: 403, params: [], remediation: "retry", en: "Only a support operator may perform this action" },
|
|
68
|
+
"error.403.chat_not_participant": { status: 403, params: [], remediation: "retry", en: "You are not a participant of this conversation" },
|
|
69
|
+
"error.403.forbidden": { status: 403, params: [], remediation: "retry", en: "You do not have permission to perform this action" },
|
|
70
|
+
"error.403.network_blocked": { status: 403, params: [], remediation: "contact_support", en: "Requests from this network are not allowed" },
|
|
71
|
+
"error.403.verification_enrollment_required": { status: 403, params: [], remediation: "verify", en: "Verification factor enrollment required" },
|
|
72
|
+
"error.403.verification_required": { status: 403, params: [], remediation: "verify", en: "Additional verification required" },
|
|
73
|
+
"error.404.ad_not_found": { status: 404, params: [], remediation: "retry", en: "Listing not found" },
|
|
74
|
+
"error.404.chat_conversation_not_found": { status: 404, params: [], remediation: "retry", en: "Conversation not found" },
|
|
75
|
+
"error.404.not_found": { status: 404, params: [], remediation: "retry", en: "Requested resource not found" },
|
|
76
|
+
"error.404.verification_challenge_not_found": { status: 404, params: [], remediation: "verify", en: "Verification challenge not found or expired" },
|
|
77
|
+
"error.405.method_not_allowed": { status: 405, params: [], remediation: "retry", en: "Method not allowed" },
|
|
78
|
+
"error.406.not_acceptable": { status: 406, params: [], remediation: "retry", en: "Not acceptable" },
|
|
79
|
+
"error.408.request_timeout": { status: 408, params: [], remediation: "retry", en: "Request timeout" },
|
|
80
|
+
"error.409.chat_already_assigned": { status: 409, params: [], remediation: "fix_input", en: "This support conversation is already assigned" },
|
|
81
|
+
"error.409.conflict": { status: 409, params: [], remediation: "fix_input", en: "Resource already exists" },
|
|
82
|
+
"error.410.gone": { status: 410, params: [], remediation: "retry", en: "Resource has been permanently removed" },
|
|
83
|
+
"error.413.payload_too_large": { status: 413, params: [], remediation: "retry", en: "Request body is too large" },
|
|
84
|
+
"error.415.unsupported_media_type": { status: 415, params: [], remediation: "retry", en: "Unsupported media type" },
|
|
85
|
+
"error.422.unprocessable_entity": { status: 422, params: [], remediation: "wait_and_retry", en: "Unprocessable entity" },
|
|
86
|
+
"error.423.locked": { status: 423, params: [], remediation: "wait_and_retry", en: "Resource is locked" },
|
|
87
|
+
"error.423.verification_locked": { status: 423, params: [], remediation: "wait_and_retry", en: "Too many failed attempts — verification locked" },
|
|
88
|
+
"error.429.rate_limit": { status: 429, params: ["retry_after_minutes"], remediation: "wait_and_retry", en: "Too many attempts. Try again in {retry_after_minutes} minutes." },
|
|
89
|
+
"error.429.too_many_requests": { status: 429, params: [], remediation: "wait_and_retry", en: "Too many requests. Please try again later." },
|
|
90
|
+
"error.500.internal": { status: 500, params: [], remediation: "contact_support", en: "Something went wrong" },
|
|
91
|
+
"error.503.mandate_unavailable": { status: 503, params: [], remediation: "retry", en: "Cannot verify workspace mandate right now" },
|
|
92
|
+
} as const;
|
|
93
|
+
|
|
94
|
+
export type ChatErrorCode = keyof typeof CHAT_ERRORS;
|
|
95
|
+
|
|
96
|
+
/** Every backend error code this module can surface, sorted. */
|
|
97
|
+
export const CHAT_ERROR_CODES: readonly ChatErrorCode[] = [
|
|
98
|
+
"error.400.bad_request",
|
|
99
|
+
"error.400.captcha_invalid",
|
|
100
|
+
"error.400.captcha_required",
|
|
101
|
+
"error.400.chat_attachments_disabled",
|
|
102
|
+
"error.400.chat_body_too_long",
|
|
103
|
+
"error.400.chat_empty_message",
|
|
104
|
+
"error.400.chat_invalid_direct",
|
|
105
|
+
"error.400.chat_invalid_kind",
|
|
106
|
+
"error.400.chat_invalid_reply",
|
|
107
|
+
"error.400.chat_kind_disabled",
|
|
108
|
+
"error.400.chat_not_support",
|
|
109
|
+
"error.400.expected_list",
|
|
110
|
+
"error.400.field.blank",
|
|
111
|
+
"error.400.field.does_not_exist",
|
|
112
|
+
"error.400.field.invalid",
|
|
113
|
+
"error.400.field.invalid_choice",
|
|
114
|
+
"error.400.field.max_length",
|
|
115
|
+
"error.400.field.max_value",
|
|
116
|
+
"error.400.field.min_length",
|
|
117
|
+
"error.400.field.min_value",
|
|
118
|
+
"error.400.field.null",
|
|
119
|
+
"error.400.field.required",
|
|
120
|
+
"error.400.field.unique",
|
|
121
|
+
"error.400.invalid_ad_id",
|
|
122
|
+
"error.400.validation_error",
|
|
123
|
+
"error.400.verification_failed",
|
|
124
|
+
"error.400.verification_invalid_factor",
|
|
125
|
+
"error.401.unauthorized",
|
|
126
|
+
"error.402.payment_required",
|
|
127
|
+
"error.403.chat_not_operator",
|
|
128
|
+
"error.403.chat_not_participant",
|
|
129
|
+
"error.403.forbidden",
|
|
130
|
+
"error.403.network_blocked",
|
|
131
|
+
"error.403.verification_enrollment_required",
|
|
132
|
+
"error.403.verification_required",
|
|
133
|
+
"error.404.ad_not_found",
|
|
134
|
+
"error.404.chat_conversation_not_found",
|
|
135
|
+
"error.404.not_found",
|
|
136
|
+
"error.404.verification_challenge_not_found",
|
|
137
|
+
"error.405.method_not_allowed",
|
|
138
|
+
"error.406.not_acceptable",
|
|
139
|
+
"error.408.request_timeout",
|
|
140
|
+
"error.409.chat_already_assigned",
|
|
141
|
+
"error.409.conflict",
|
|
142
|
+
"error.410.gone",
|
|
143
|
+
"error.413.payload_too_large",
|
|
144
|
+
"error.415.unsupported_media_type",
|
|
145
|
+
"error.422.unprocessable_entity",
|
|
146
|
+
"error.423.locked",
|
|
147
|
+
"error.423.verification_locked",
|
|
148
|
+
"error.429.rate_limit",
|
|
149
|
+
"error.429.too_many_requests",
|
|
150
|
+
"error.500.internal",
|
|
151
|
+
"error.503.mandate_unavailable",
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* English fallback bundle — one string per backend key, so a `StapelApiError.code`
|
|
156
|
+
* never renders as a raw key. Merged UNDER the hand-polished UI copy in
|
|
157
|
+
* {@link authI18nBundleEn} (polish wins; this guarantees coverage).
|
|
158
|
+
*/
|
|
159
|
+
export const chatErrorBundleEn: Record<ChatErrorCode, string> = {
|
|
160
|
+
"error.400.bad_request": "Bad request",
|
|
161
|
+
"error.400.captcha_invalid": "Captcha verification failed. Please try again.",
|
|
162
|
+
"error.400.captcha_required": "Captcha token is required.",
|
|
163
|
+
"error.400.chat_attachments_disabled": "Attachments are not enabled in this deployment",
|
|
164
|
+
"error.400.chat_body_too_long": "Message body exceeds the maximum allowed length",
|
|
165
|
+
"error.400.chat_empty_message": "A message must carry a body or at least one attachment",
|
|
166
|
+
"error.400.chat_invalid_direct": "A direct conversation needs exactly one other participant",
|
|
167
|
+
"error.400.chat_invalid_kind": "Unknown conversation kind",
|
|
168
|
+
"error.400.chat_invalid_reply": "The replied-to message does not belong to this conversation",
|
|
169
|
+
"error.400.chat_kind_disabled": "This conversation kind is not enabled in this deployment",
|
|
170
|
+
"error.400.chat_not_support": "This operation applies only to support conversations",
|
|
171
|
+
"error.400.expected_list": "Expected a list of items",
|
|
172
|
+
"error.400.field.blank": "{field} may not be blank",
|
|
173
|
+
"error.400.field.does_not_exist": "{field} does not exist",
|
|
174
|
+
"error.400.field.invalid": "{field} is invalid",
|
|
175
|
+
"error.400.field.invalid_choice": "{field} is not a valid choice",
|
|
176
|
+
"error.400.field.max_length": "{field} must be at most {max_length} characters",
|
|
177
|
+
"error.400.field.max_value": "{field} must be at most {max_value}",
|
|
178
|
+
"error.400.field.min_length": "{field} must be at least {min_length} characters",
|
|
179
|
+
"error.400.field.min_value": "{field} must be at least {min_value}",
|
|
180
|
+
"error.400.field.null": "{field} may not be null",
|
|
181
|
+
"error.400.field.required": "{field} is required",
|
|
182
|
+
"error.400.field.unique": "{field} must be unique",
|
|
183
|
+
"error.400.invalid_ad_id": "Invalid advertisement ID",
|
|
184
|
+
"error.400.validation_error": "Validation error",
|
|
185
|
+
"error.400.verification_failed": "Verification failed",
|
|
186
|
+
"error.400.verification_invalid_factor": "This verification factor is not available",
|
|
187
|
+
"error.401.unauthorized": "Authentication required",
|
|
188
|
+
"error.402.payment_required": "Payment required",
|
|
189
|
+
"error.403.chat_not_operator": "Only a support operator may perform this action",
|
|
190
|
+
"error.403.chat_not_participant": "You are not a participant of this conversation",
|
|
191
|
+
"error.403.forbidden": "You do not have permission to perform this action",
|
|
192
|
+
"error.403.network_blocked": "Requests from this network are not allowed",
|
|
193
|
+
"error.403.verification_enrollment_required": "Verification factor enrollment required",
|
|
194
|
+
"error.403.verification_required": "Additional verification required",
|
|
195
|
+
"error.404.ad_not_found": "Listing not found",
|
|
196
|
+
"error.404.chat_conversation_not_found": "Conversation not found",
|
|
197
|
+
"error.404.not_found": "Requested resource not found",
|
|
198
|
+
"error.404.verification_challenge_not_found": "Verification challenge not found or expired",
|
|
199
|
+
"error.405.method_not_allowed": "Method not allowed",
|
|
200
|
+
"error.406.not_acceptable": "Not acceptable",
|
|
201
|
+
"error.408.request_timeout": "Request timeout",
|
|
202
|
+
"error.409.chat_already_assigned": "This support conversation is already assigned",
|
|
203
|
+
"error.409.conflict": "Resource already exists",
|
|
204
|
+
"error.410.gone": "Resource has been permanently removed",
|
|
205
|
+
"error.413.payload_too_large": "Request body is too large",
|
|
206
|
+
"error.415.unsupported_media_type": "Unsupported media type",
|
|
207
|
+
"error.422.unprocessable_entity": "Unprocessable entity",
|
|
208
|
+
"error.423.locked": "Resource is locked",
|
|
209
|
+
"error.423.verification_locked": "Too many failed attempts — verification locked",
|
|
210
|
+
"error.429.rate_limit": "Too many attempts. Try again in {retry_after_minutes} minutes.",
|
|
211
|
+
"error.429.too_many_requests": "Too many requests. Please try again later.",
|
|
212
|
+
"error.500.internal": "Something went wrong",
|
|
213
|
+
"error.503.mandate_unavailable": "Cannot verify workspace mandate right now",
|
|
214
|
+
};
|