@stapel/vocabularies-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.
Files changed (130) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/MODULE.md +92 -0
  4. package/README.md +173 -0
  5. package/dist/api/extensions.d.ts +8 -0
  6. package/dist/api/extensions.d.ts.map +1 -0
  7. package/dist/api/extensions.js +8 -0
  8. package/dist/api/extensions.js.map +1 -0
  9. package/dist/api/generated/schema.d.ts +234 -0
  10. package/dist/api/generated/schema.d.ts.map +1 -0
  11. package/dist/api/generated/schema.js +2 -0
  12. package/dist/api/generated/schema.js.map +1 -0
  13. package/dist/api/types.d.ts +23 -0
  14. package/dist/api/types.d.ts.map +1 -0
  15. package/dist/api/types.js +2 -0
  16. package/dist/api/types.js.map +1 -0
  17. package/dist/api/vocabulariesApi.d.ts +14 -0
  18. package/dist/api/vocabulariesApi.d.ts.map +1 -0
  19. package/dist/api/vocabulariesApi.js +4 -0
  20. package/dist/api/vocabulariesApi.js.map +1 -0
  21. package/dist/client.d.ts +49 -0
  22. package/dist/client.d.ts.map +1 -0
  23. package/dist/client.js +118 -0
  24. package/dist/client.js.map +1 -0
  25. package/dist/default/VocabularyTermSelect.d.ts +24 -0
  26. package/dist/default/VocabularyTermSelect.d.ts.map +1 -0
  27. package/dist/default/VocabularyTermSelect.js +75 -0
  28. package/dist/default/VocabularyTermSelect.js.map +1 -0
  29. package/dist/default/index.d.ts +16 -0
  30. package/dist/default/index.d.ts.map +1 -0
  31. package/dist/default/index.js +14 -0
  32. package/dist/default/index.js.map +1 -0
  33. package/dist/default/types.d.ts +12 -0
  34. package/dist/default/types.d.ts.map +1 -0
  35. package/dist/default/types.js +2 -0
  36. package/dist/default/types.js.map +1 -0
  37. package/dist/flows/errors.d.ts +12 -0
  38. package/dist/flows/errors.d.ts.map +1 -0
  39. package/dist/flows/errors.js +13 -0
  40. package/dist/flows/errors.js.map +1 -0
  41. package/dist/flows/registry.d.ts +22 -0
  42. package/dist/flows/registry.d.ts.map +1 -0
  43. package/dist/flows/registry.js +20 -0
  44. package/dist/flows/registry.js.map +1 -0
  45. package/dist/headless/VocabulariesProvider.d.ts +19 -0
  46. package/dist/headless/VocabulariesProvider.d.ts.map +1 -0
  47. package/dist/headless/VocabulariesProvider.js +15 -0
  48. package/dist/headless/VocabulariesProvider.js.map +1 -0
  49. package/dist/i18n/errorsMap.d.ts +10 -0
  50. package/dist/i18n/errorsMap.d.ts.map +1 -0
  51. package/dist/i18n/errorsMap.js +20 -0
  52. package/dist/i18n/errorsMap.js.map +1 -0
  53. package/dist/i18n/es.d.ts +21 -0
  54. package/dist/i18n/es.d.ts.map +1 -0
  55. package/dist/i18n/es.js +38 -0
  56. package/dist/i18n/es.js.map +1 -0
  57. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  58. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  59. package/dist/i18n/generated/errors.es.gen.js +58 -0
  60. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  61. package/dist/i18n/generated/errors.gen.d.ts +305 -0
  62. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  63. package/dist/i18n/generated/errors.gen.js +156 -0
  64. package/dist/i18n/generated/errors.gen.js.map +1 -0
  65. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  66. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  67. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  68. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  69. package/dist/i18n/keys.d.ts +33 -0
  70. package/dist/i18n/keys.d.ts.map +1 -0
  71. package/dist/i18n/keys.js +44 -0
  72. package/dist/i18n/keys.js.map +1 -0
  73. package/dist/i18n/ru.d.ts +26 -0
  74. package/dist/i18n/ru.d.ts.map +1 -0
  75. package/dist/i18n/ru.js +43 -0
  76. package/dist/i18n/ru.js.map +1 -0
  77. package/dist/index.d.ts +34 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +38 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/model/context.d.ts +11 -0
  82. package/dist/model/context.d.ts.map +1 -0
  83. package/dist/model/context.js +16 -0
  84. package/dist/model/context.js.map +1 -0
  85. package/dist/model/queryKeys.d.ts +5 -0
  86. package/dist/model/queryKeys.d.ts.map +1 -0
  87. package/dist/model/queryKeys.js +28 -0
  88. package/dist/model/queryKeys.js.map +1 -0
  89. package/dist/model/runtime.d.ts +17 -0
  90. package/dist/model/runtime.d.ts.map +1 -0
  91. package/dist/model/runtime.js +6 -0
  92. package/dist/model/runtime.js.map +1 -0
  93. package/dist/model/useTermLabels.d.ts +26 -0
  94. package/dist/model/useTermLabels.d.ts.map +1 -0
  95. package/dist/model/useTermLabels.js +58 -0
  96. package/dist/model/useTermLabels.js.map +1 -0
  97. package/dist/model/useTermSearch.d.ts +24 -0
  98. package/dist/model/useTermSearch.d.ts.map +1 -0
  99. package/dist/model/useTermSearch.js +85 -0
  100. package/dist/model/useTermSearch.js.map +1 -0
  101. package/llms.txt +79 -0
  102. package/manifest.json +466 -0
  103. package/package.json +108 -0
  104. package/src/analytics/generated/events.json +7 -0
  105. package/src/api/extensions.ts +7 -0
  106. package/src/api/generated/schema.ts +236 -0
  107. package/src/api/types.ts +27 -0
  108. package/src/api/vocabulariesApi.ts +17 -0
  109. package/src/client.ts +176 -0
  110. package/src/default/VocabularyTermSelect.tsx +147 -0
  111. package/src/default/index.ts +15 -0
  112. package/src/default/types.ts +11 -0
  113. package/src/flows/errors.ts +16 -0
  114. package/src/flows/registry.ts +32 -0
  115. package/src/headless/VocabulariesProvider.tsx +20 -0
  116. package/src/i18n/errorsMap.ts +31 -0
  117. package/src/i18n/es.ts +49 -0
  118. package/src/i18n/generated/errors.es.gen.ts +64 -0
  119. package/src/i18n/generated/errors.gen.ts +187 -0
  120. package/src/i18n/generated/errors.json +351 -0
  121. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  122. package/src/i18n/keys.ts +53 -0
  123. package/src/i18n/ru.ts +54 -0
  124. package/src/index.ts +92 -0
  125. package/src/model/context.tsx +28 -0
  126. package/src/model/queryKeys.ts +35 -0
  127. package/src/model/runtime.ts +25 -0
  128. package/src/model/useTermLabels.ts +72 -0
  129. package/src/model/useTermSearch.ts +118 -0
  130. package/tsconfig.json +26 -0
@@ -0,0 +1,351 @@
1
+ [
2
+ {
3
+ "code": "error.400.bad_request",
4
+ "status": 400,
5
+ "params": [],
6
+ "remediation": "fix_input",
7
+ "en": "Bad request"
8
+ },
9
+ {
10
+ "code": "error.400.captcha_invalid",
11
+ "status": 400,
12
+ "params": [],
13
+ "remediation": "retry",
14
+ "en": "Captcha verification failed. Please try again."
15
+ },
16
+ {
17
+ "code": "error.400.captcha_required",
18
+ "status": 400,
19
+ "params": [],
20
+ "remediation": "retry",
21
+ "en": "Captcha token is required."
22
+ },
23
+ {
24
+ "code": "error.400.expected_list",
25
+ "status": 400,
26
+ "params": [],
27
+ "remediation": "fix_input",
28
+ "en": "Expected a list of items"
29
+ },
30
+ {
31
+ "code": "error.400.field.blank",
32
+ "status": 400,
33
+ "params": [
34
+ "field"
35
+ ],
36
+ "remediation": "fix_input",
37
+ "en": "{field} may not be blank"
38
+ },
39
+ {
40
+ "code": "error.400.field.does_not_exist",
41
+ "status": 400,
42
+ "params": [
43
+ "field"
44
+ ],
45
+ "remediation": "fix_input",
46
+ "en": "{field} does not exist"
47
+ },
48
+ {
49
+ "code": "error.400.field.invalid",
50
+ "status": 400,
51
+ "params": [
52
+ "field"
53
+ ],
54
+ "remediation": "fix_input",
55
+ "en": "{field} is invalid"
56
+ },
57
+ {
58
+ "code": "error.400.field.invalid_choice",
59
+ "status": 400,
60
+ "params": [
61
+ "field"
62
+ ],
63
+ "remediation": "fix_input",
64
+ "en": "{field} is not a valid choice"
65
+ },
66
+ {
67
+ "code": "error.400.field.max_length",
68
+ "status": 400,
69
+ "params": [
70
+ "field",
71
+ "max_length"
72
+ ],
73
+ "remediation": "fix_input",
74
+ "en": "{field} must be at most {max_length} characters"
75
+ },
76
+ {
77
+ "code": "error.400.field.max_value",
78
+ "status": 400,
79
+ "params": [
80
+ "field",
81
+ "max_value"
82
+ ],
83
+ "remediation": "fix_input",
84
+ "en": "{field} must be at most {max_value}"
85
+ },
86
+ {
87
+ "code": "error.400.field.min_length",
88
+ "status": 400,
89
+ "params": [
90
+ "field",
91
+ "min_length"
92
+ ],
93
+ "remediation": "fix_input",
94
+ "en": "{field} must be at least {min_length} characters"
95
+ },
96
+ {
97
+ "code": "error.400.field.min_value",
98
+ "status": 400,
99
+ "params": [
100
+ "field",
101
+ "min_value"
102
+ ],
103
+ "remediation": "fix_input",
104
+ "en": "{field} must be at least {min_value}"
105
+ },
106
+ {
107
+ "code": "error.400.field.null",
108
+ "status": 400,
109
+ "params": [
110
+ "field"
111
+ ],
112
+ "remediation": "fix_input",
113
+ "en": "{field} may not be null"
114
+ },
115
+ {
116
+ "code": "error.400.field.required",
117
+ "status": 400,
118
+ "params": [
119
+ "field"
120
+ ],
121
+ "remediation": "fix_input",
122
+ "en": "{field} is required"
123
+ },
124
+ {
125
+ "code": "error.400.field.unique",
126
+ "status": 400,
127
+ "params": [
128
+ "field"
129
+ ],
130
+ "remediation": "fix_input",
131
+ "en": "{field} must be unique"
132
+ },
133
+ {
134
+ "code": "error.400.invalid_ad_id",
135
+ "status": 400,
136
+ "params": [],
137
+ "remediation": "fix_input",
138
+ "en": "Invalid advertisement ID"
139
+ },
140
+ {
141
+ "code": "error.400.validation_error",
142
+ "status": 400,
143
+ "params": [],
144
+ "remediation": "fix_input",
145
+ "en": "Validation error"
146
+ },
147
+ {
148
+ "code": "error.400.verification_failed",
149
+ "status": 400,
150
+ "params": [],
151
+ "remediation": "verify",
152
+ "en": "Verification failed"
153
+ },
154
+ {
155
+ "code": "error.400.verification_invalid_factor",
156
+ "status": 400,
157
+ "params": [],
158
+ "remediation": "verify",
159
+ "en": "This verification factor is not available"
160
+ },
161
+ {
162
+ "code": "error.400.vocabularies_bad_parent",
163
+ "status": 400,
164
+ "params": [
165
+ "parent",
166
+ "level"
167
+ ],
168
+ "remediation": "fix_input",
169
+ "en": "No term '{parent}' at the parent level of '{level}'"
170
+ },
171
+ {
172
+ "code": "error.401.unauthorized",
173
+ "status": 401,
174
+ "params": [],
175
+ "remediation": "reauthenticate",
176
+ "en": "Authentication required"
177
+ },
178
+ {
179
+ "code": "error.402.payment_required",
180
+ "status": 402,
181
+ "params": [],
182
+ "remediation": "retry",
183
+ "en": "Payment required"
184
+ },
185
+ {
186
+ "code": "error.403.forbidden",
187
+ "status": 403,
188
+ "params": [],
189
+ "remediation": "retry",
190
+ "en": "You do not have permission to perform this action"
191
+ },
192
+ {
193
+ "code": "error.403.network_blocked",
194
+ "status": 403,
195
+ "params": [],
196
+ "remediation": "contact_support",
197
+ "en": "Requests from this network are not allowed"
198
+ },
199
+ {
200
+ "code": "error.403.verification_enrollment_required",
201
+ "status": 403,
202
+ "params": [],
203
+ "remediation": "verify",
204
+ "en": "Verification factor enrollment required"
205
+ },
206
+ {
207
+ "code": "error.403.verification_required",
208
+ "status": 403,
209
+ "params": [],
210
+ "remediation": "verify",
211
+ "en": "Additional verification required"
212
+ },
213
+ {
214
+ "code": "error.404.ad_not_found",
215
+ "status": 404,
216
+ "params": [],
217
+ "remediation": "retry",
218
+ "en": "Listing not found"
219
+ },
220
+ {
221
+ "code": "error.404.not_found",
222
+ "status": 404,
223
+ "params": [],
224
+ "remediation": "retry",
225
+ "en": "Requested resource not found"
226
+ },
227
+ {
228
+ "code": "error.404.verification_challenge_not_found",
229
+ "status": 404,
230
+ "params": [],
231
+ "remediation": "verify",
232
+ "en": "Verification challenge not found or expired"
233
+ },
234
+ {
235
+ "code": "error.404.vocabularies_level_not_found",
236
+ "status": 404,
237
+ "params": [
238
+ "vocabulary",
239
+ "level"
240
+ ],
241
+ "remediation": "retry",
242
+ "en": "Vocabulary '{vocabulary}' has no level '{level}'"
243
+ },
244
+ {
245
+ "code": "error.404.vocabularies_vocabulary_not_found",
246
+ "status": 404,
247
+ "params": [],
248
+ "remediation": "retry",
249
+ "en": "Vocabulary not found"
250
+ },
251
+ {
252
+ "code": "error.405.method_not_allowed",
253
+ "status": 405,
254
+ "params": [],
255
+ "remediation": "retry",
256
+ "en": "Method not allowed"
257
+ },
258
+ {
259
+ "code": "error.406.not_acceptable",
260
+ "status": 406,
261
+ "params": [],
262
+ "remediation": "retry",
263
+ "en": "Not acceptable"
264
+ },
265
+ {
266
+ "code": "error.408.request_timeout",
267
+ "status": 408,
268
+ "params": [],
269
+ "remediation": "retry",
270
+ "en": "Request timeout"
271
+ },
272
+ {
273
+ "code": "error.409.conflict",
274
+ "status": 409,
275
+ "params": [],
276
+ "remediation": "fix_input",
277
+ "en": "Resource already exists"
278
+ },
279
+ {
280
+ "code": "error.410.gone",
281
+ "status": 410,
282
+ "params": [],
283
+ "remediation": "retry",
284
+ "en": "Resource has been permanently removed"
285
+ },
286
+ {
287
+ "code": "error.413.payload_too_large",
288
+ "status": 413,
289
+ "params": [],
290
+ "remediation": "retry",
291
+ "en": "Request body is too large"
292
+ },
293
+ {
294
+ "code": "error.415.unsupported_media_type",
295
+ "status": 415,
296
+ "params": [],
297
+ "remediation": "retry",
298
+ "en": "Unsupported media type"
299
+ },
300
+ {
301
+ "code": "error.422.unprocessable_entity",
302
+ "status": 422,
303
+ "params": [],
304
+ "remediation": "wait_and_retry",
305
+ "en": "Unprocessable entity"
306
+ },
307
+ {
308
+ "code": "error.423.locked",
309
+ "status": 423,
310
+ "params": [],
311
+ "remediation": "wait_and_retry",
312
+ "en": "Resource is locked"
313
+ },
314
+ {
315
+ "code": "error.423.verification_locked",
316
+ "status": 423,
317
+ "params": [],
318
+ "remediation": "wait_and_retry",
319
+ "en": "Too many failed attempts — verification locked"
320
+ },
321
+ {
322
+ "code": "error.429.rate_limit",
323
+ "status": 429,
324
+ "params": [
325
+ "retry_after_minutes"
326
+ ],
327
+ "remediation": "wait_and_retry",
328
+ "en": "Too many attempts. Try again in {retry_after_minutes} minutes."
329
+ },
330
+ {
331
+ "code": "error.429.too_many_requests",
332
+ "status": 429,
333
+ "params": [],
334
+ "remediation": "wait_and_retry",
335
+ "en": "Too many requests. Please try again later."
336
+ },
337
+ {
338
+ "code": "error.500.internal",
339
+ "status": 500,
340
+ "params": [],
341
+ "remediation": "contact_support",
342
+ "en": "Something went wrong"
343
+ },
344
+ {
345
+ "code": "error.503.mandate_unavailable",
346
+ "status": 503,
347
+ "params": [],
348
+ "remediation": "retry",
349
+ "en": "Cannot verify workspace mandate right now"
350
+ }
351
+ ]
@@ -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.ru.json (locale catalog
5
+ // beside the canonical errors.json — i18n-shipping.md §1).
6
+ import type { VocabulariesErrorCode } from "./errors.gen.js";
7
+
8
+ /**
9
+ * `ru` texts for the backend error codes this catalog carries.
10
+ *
11
+ * PARTIAL, and deliberately typed to say so: 3 key(s) owned by
12
+ * stapel_vocabularies 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 `ru`
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/ru` subpath, NOT the main entry, so
19
+ * the locale stays out of hosts that don't ship it.
20
+ */
21
+ export const vocabulariesErrorBundleRu: Partial<Record<VocabulariesErrorCode, string>> = {
22
+ "error.400.bad_request": "Некорректный запрос",
23
+ "error.400.captcha_invalid": "Проверка капчи не пройдена. Пожалуйста, попробуйте ещё раз.",
24
+ "error.400.captcha_required": "Требуется токен капчи.",
25
+ "error.400.expected_list": "Ожидался список элементов",
26
+ "error.400.field.blank": "Поле «{field}» не может быть пустым",
27
+ "error.400.field.does_not_exist": "«{field}» не существует",
28
+ "error.400.field.invalid": "Поле «{field}» содержит недопустимое значение",
29
+ "error.400.field.invalid_choice": "Недопустимый вариант для поля «{field}»",
30
+ "error.400.field.max_length": "Поле «{field}» должно содержать не более {max_length} символов",
31
+ "error.400.field.max_value": "Значение поля «{field}» должно быть не больше {max_value}",
32
+ "error.400.field.min_length": "Поле «{field}» должно содержать не менее {min_length} символов",
33
+ "error.400.field.min_value": "Значение поля «{field}» должно быть не меньше {min_value}",
34
+ "error.400.field.null": "Поле «{field}» не может быть null",
35
+ "error.400.field.required": "Поле «{field}» обязательно",
36
+ "error.400.field.unique": "Значение поля «{field}» должно быть уникальным",
37
+ "error.400.invalid_ad_id": "Недопустимый идентификатор объявления",
38
+ "error.400.validation_error": "Ошибка валидации",
39
+ "error.400.verification_failed": "Проверка не пройдена",
40
+ "error.400.verification_invalid_factor": "Этот способ подтверждения недоступен",
41
+ "error.401.unauthorized": "Требуется аутентификация",
42
+ "error.402.payment_required": "Требуется оплата",
43
+ "error.403.forbidden": "У вас нет прав для выполнения этого действия",
44
+ "error.403.network_blocked": "Запросы из этой сети не разрешены.",
45
+ "error.403.verification_enrollment_required": "Требуется регистрация фактора подтверждения.",
46
+ "error.403.verification_required": "Требуется дополнительная проверка",
47
+ "error.404.ad_not_found": "Объявление не найдено",
48
+ "error.404.not_found": "Запрошенный ресурс не найден",
49
+ "error.404.verification_challenge_not_found": "Запрос на подтверждение не найден или истёк",
50
+ "error.405.method_not_allowed": "Метод не разрешён",
51
+ "error.406.not_acceptable": "Недопустимый формат ответа",
52
+ "error.408.request_timeout": "Время ожидания запроса истекло",
53
+ "error.409.conflict": "Ресурс уже существует",
54
+ "error.410.gone": "Ресурс был безвозвратно удалён",
55
+ "error.413.payload_too_large": "Тело запроса слишком большое",
56
+ "error.415.unsupported_media_type": "Неподдерживаемый тип данных",
57
+ "error.422.unprocessable_entity": "Невозможно обработать данные запроса",
58
+ "error.423.locked": "Ресурс заблокирован",
59
+ "error.423.verification_locked": "Слишком много неудачных попыток — подтверждение заблокировано",
60
+ "error.429.rate_limit": "Слишком много попыток. Повторите попытку через {retry_after_minutes} мин.",
61
+ "error.429.too_many_requests": "Слишком много запросов. Пожалуйста, повторите попытку позже.",
62
+ "error.500.internal": "Что-то пошло не так",
63
+ "error.503.mandate_unavailable": "Не удалось проверить доступ к рабочему пространству",
64
+ };
@@ -0,0 +1,53 @@
1
+ import type { I18nDictionary, I18nEngine } from "@stapel/core";
2
+ import { vocabulariesErrorBundleEn } from "./generated/errors.gen.js";
3
+
4
+ /**
5
+ * vocabularies-react's own translation KEYS (frontend-standard §4.2): headless
6
+ * components never render literal strings — hosts resolve these via core's i18n
7
+ * engine (`useT`). Backend error codes flow through the SAME contour: a
8
+ * `StapelApiError.code` is already a key, so the default bundle below ships
9
+ * English fallbacks for both the backend error codes (generated) and the
10
+ * pair's own UI keys. Point core's `loadLocale` at stapel-translate to override
11
+ * per locale. UI keys live under the `vocabularies.` namespace.
12
+ */
13
+ export const VOCABULARIES_I18N_KEYS = {
14
+ unknownError: "vocabularies.error.unknown",
15
+ termSelectPlaceholder: "vocabularies.termSelect.placeholder",
16
+ termSelectNoMatches: "vocabularies.termSelect.noMatches",
17
+ termSelectUnavailableTitle: "vocabularies.termSelect.unavailable.title",
18
+ termSelectUnavailable: "vocabularies.termSelect.unavailable",
19
+ } as const;
20
+
21
+ export type VocabulariesI18nKey =
22
+ (typeof VOCABULARIES_I18N_KEYS)[keyof typeof VOCABULARIES_I18N_KEYS];
23
+
24
+ /**
25
+ * English fallback bundle for vocabularies-react UI keys + backend error codes.
26
+ * The generated `vocabulariesErrorBundleEn` (from stapel-vocabularies's error registry,
27
+ * `pnpm gen:errors`) is spread FIRST so every backend `error.*` key has a
28
+ * fallback — a `StapelApiError.code` never renders as a raw key. Hand-polished
29
+ * copy below then OVERRIDES the generated English for the keys users see most.
30
+ */
31
+ export const vocabulariesI18nBundleEn: I18nDictionary = {
32
+ // Backend error codes — generated en fallbacks (coverage by construction).
33
+ ...vocabulariesErrorBundleEn,
34
+
35
+ // vocabularies-react UI
36
+ "vocabularies.error.unknown": "Something went wrong. Please try again.",
37
+
38
+ // the default skin's own copy (see i18n/ru.ts, i18n/es.ts)
39
+ "vocabularies.termSelect.placeholder": "Start typing to search…",
40
+ "vocabularies.termSelect.noMatches": "No term matches that.",
41
+ "vocabularies.termSelect.unavailable.title": "This list cannot be loaded",
42
+ "vocabularies.termSelect.unavailable":
43
+ "The terms for this field come from a vocabulary service that is not configured here, so there is nothing to choose from. Nobody can answer this question until it is wired up — please report it.",
44
+ };
45
+
46
+ /**
47
+ * Register vocabularies-react's key bundle into a core i18n engine (call once at
48
+ * startup). A later `loadLocale` from stapel-translate can layer localized
49
+ * overrides on top.
50
+ */
51
+ export function registerVocabulariesI18n(engine: I18nEngine, locale = "en"): void {
52
+ engine.registerBundle(locale, vocabulariesI18nBundleEn);
53
+ }
package/src/i18n/ru.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { I18nDictionary, I18nEngine } from "@stapel/core";
2
+ import { vocabulariesErrorBundleRu } from "./generated/errors.ru.gen.js";
3
+ import { vocabulariesI18nBundleEn } from "./keys.js";
4
+
5
+ export { vocabulariesErrorBundleRu } from "./generated/errors.ru.gen.js";
6
+
7
+ /**
8
+ * Russian bundle for vocabularies-react — shipped as the
9
+ * `@stapel/vocabularies-react/i18n/ru` subpath (i18n-shipping.md §2) so the locale is
10
+ * opt-in: a host that never registers it never carries these strings (the main
11
+ * entry does not import this module).
12
+ *
13
+ * TWO SOURCES, ON PURPOSE. The generated `vocabulariesErrorBundleRu` covers the
14
+ * 42 cross-cutting codes stapel-core owns and localizes. The 3 codes
15
+ * stapel-vocabularies owns are NOT in it and cannot be: the module ships no
16
+ * `translations/` directory at all, so the generator emits a `Partial` bundle
17
+ * and says so in its own header (`ERRORS_LOCALE_EXEMPT_OWNERS`, the
18
+ * stapel-geo / stapel-reviews precedent). They are authored below, beside the
19
+ * UI copy. When upstream ships `translations/errors.ru.json`, those three
20
+ * lines are deleted and the generated bundle covers them — the keys and the
21
+ * texts do not move.
22
+ */
23
+ export const vocabulariesI18nBundleRu: I18nDictionary = {
24
+ // Backend error codes — generated ru texts for every key core owns.
25
+ ...vocabulariesErrorBundleRu,
26
+
27
+ // Backend error codes stapel-vocabularies owns — authored here (see above).
28
+ "error.400.vocabularies_bad_parent":
29
+ "На родительском уровне «{level}» нет термина «{parent}»",
30
+ "error.404.vocabularies_level_not_found":
31
+ "В справочнике «{vocabulary}» нет уровня «{level}»",
32
+ "error.404.vocabularies_vocabulary_not_found": "Справочник не найден",
33
+
34
+ // vocabularies-react UI
35
+ "vocabularies.error.unknown": "Что-то пошло не так. Попробуйте ещё раз.",
36
+ "vocabularies.termSelect.placeholder": "Начните вводить для поиска…",
37
+ "vocabularies.termSelect.noMatches": "Ничего не найдено.",
38
+ "vocabularies.termSelect.unavailable.title": "Список не удалось загрузить",
39
+ "vocabularies.termSelect.unavailable":
40
+ "Значения для этого поля берутся из справочника, который здесь не подключён, и выбирать не из чего. Ответить на этот вопрос невозможно — сообщите, пожалуйста, об этом.",
41
+ };
42
+
43
+ /**
44
+ * Register the Russian bundle. The en bundle goes UNDER it
45
+ * (merge-priority convention): a key this locale has not translated yet
46
+ * degrades to ENGLISH, never to a raw key.
47
+ */
48
+ export function registerVocabulariesI18nRu(
49
+ engine: I18nEngine,
50
+ locale = "ru"
51
+ ): void {
52
+ engine.registerBundle(locale, vocabulariesI18nBundleEn);
53
+ engine.registerBundle(locale, vocabulariesI18nBundleRu);
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `@stapel/vocabularies-react` — the headless React flow pair for stapel-vocabularies
3
+ * (frontend-standard §2). Business + state only, zero visual opinion. Built on
4
+ * `@stapel/core`'s StapelClient (verification-403 interception, token refresh,
5
+ * i18n, analytics, query layer).
6
+ *
7
+ * Scaffolded by `stapel-new-react-lib`. Layers: api → model → flows → headless
8
+ * → i18n. Generated surfaces (flows registry, error map, manifest, llms.txt)
9
+ * are produced by the monorepo `gen:*` drivers and stand under drift gates.
10
+ */
11
+
12
+ // ── api ──────────────────────────────────────────────────────────────────────
13
+ export { createVocabulariesApi } from "./api/vocabulariesApi.js";
14
+ export type { VocabulariesApi } from "./api/vocabulariesApi.js";
15
+ export type { Schemas, Vocabulary, Level, Term, TermPage } from "./api/types.js";
16
+
17
+ // ── the seam (attributes-v2 §3.4) ────────────────────────────────────────────
18
+ // `createVocabularyClient` returns the two functions
19
+ // `@stapel/attributes-react` declares as `VocabularyClient` and hands to
20
+ // `<VocabularyClientProvider>`. The shape is satisfied STRUCTURALLY: neither
21
+ // package imports the other (test/clientShape.test.ts is the proof), so the
22
+ // two L2 pairs stay independently releasable.
23
+ export {
24
+ createVocabularyClient,
25
+ DEFAULT_TERM_LIMIT,
26
+ RESOLVE_BATCH,
27
+ } from "./client.js";
28
+ export type {
29
+ VocabularyClient,
30
+ VocabularyTerm,
31
+ CreateVocabularyClientOptions,
32
+ } from "./client.js";
33
+
34
+ // ── flows ────────────────────────────────────────────────────────────────────
35
+ // The flow-machine primitive lives in `@stapel/core` (one reviewed copy for
36
+ // every pair — frontend-core-architecture §4b). Re-exported for ergonomics.
37
+ export { createFlowMachine, useFlow, isErrorCode } from "@stapel/core";
38
+ export type {
39
+ FlowMachine,
40
+ FlowMachineOptions,
41
+ FlowStateBase,
42
+ FlowError,
43
+ } from "@stapel/core";
44
+ export { toFlowError } from "./flows/errors.js";
45
+ export { VOCABULARIES_FLOWS, flowEndpoints } from "./flows/registry.js";
46
+ export type {
47
+ VocabulariesFlowId,
48
+ VocabulariesFlowSpec,
49
+ FlowEndpoint,
50
+ } from "./flows/registry.js";
51
+
52
+ // ── model (runtime wiring, query keys, context) ──────────────────────────────
53
+ export { createVocabulariesRuntime } from "./model/runtime.js";
54
+ export type {
55
+ VocabulariesRuntime,
56
+ CreateVocabulariesRuntimeOptions,
57
+ } from "./model/runtime.js";
58
+ export {
59
+ VocabulariesRuntimeContext,
60
+ useVocabulariesRuntime,
61
+ useVocabulariesApi,
62
+ useVocabulariesAnalytics,
63
+ } from "./model/context.js";
64
+ export { vocabulariesQueryKeys } from "./model/queryKeys.js";
65
+ export { useTermSearch, TERM_SEARCH_DEBOUNCE_MS } from "./model/useTermSearch.js";
66
+ export type { TermSearchOptions, TermSearchState } from "./model/useTermSearch.js";
67
+ export { useTermLabels, termLabel } from "./model/useTermLabels.js";
68
+ export type { TermLabels, TermLabelsOptions } from "./model/useTermLabels.js";
69
+
70
+ // ── headless (renderless components) ─────────────────────────────────────────
71
+ export { VocabulariesProvider } from "./headless/VocabulariesProvider.js";
72
+
73
+ // ── i18n ─────────────────────────────────────────────────────────────────────
74
+ export {
75
+ VOCABULARIES_I18N_KEYS,
76
+ vocabulariesI18nBundleEn,
77
+ registerVocabulariesI18n,
78
+ } from "./i18n/keys.js";
79
+ export type { VocabulariesI18nKey } from "./i18n/keys.js";
80
+
81
+ // ── errors map (code → status/params/remediation/en; generated) ──────────────
82
+ export {
83
+ VOCABULARIES_ERRORS,
84
+ VOCABULARIES_ERROR_CODES,
85
+ vocabulariesErrorBundleEn,
86
+ explainVocabulariesError,
87
+ } from "./i18n/errorsMap.js";
88
+ export type {
89
+ VocabulariesErrorCode,
90
+ VocabulariesErrorSpec,
91
+ Remediation,
92
+ } from "./i18n/errorsMap.js";
@@ -0,0 +1,28 @@
1
+ import type { Context } from "react";
2
+ import { createModuleContext } from "@stapel/core";
3
+ import type { Analytics, ModuleContextKit } from "@stapel/core";
4
+ import type { VocabulariesApi } from "../api/vocabulariesApi.js";
5
+ import type { VocabulariesRuntime } from "./runtime.js";
6
+
7
+ /**
8
+ * The wired VocabulariesRuntime shared through React context by
9
+ * `<VocabulariesProvider>`. Hooks in `model/` and `headless/` read the singletons
10
+ * from here. One reviewed copy of this plumbing lives in `@stapel/core`
11
+ * (`createModuleContext`, slim wave §21/S2); this module binds it under the
12
+ * pair's public names.
13
+ */
14
+ const kit: ModuleContextKit<VocabulariesRuntime> =
15
+ createModuleContext<VocabulariesRuntime>("Vocabularies");
16
+
17
+ export const VocabulariesRuntimeContext: Context<VocabulariesRuntime | null> =
18
+ kit.RuntimeContext;
19
+
20
+ export const useVocabulariesRuntime: () => VocabulariesRuntime = kit.useRuntime;
21
+
22
+ export const useVocabulariesApi: () => VocabulariesApi = kit.useApi;
23
+
24
+ export const useVocabulariesAnalytics: () => Analytics | null = kit.useAnalytics;
25
+
26
+ /** @internal Re-exported as `<VocabulariesProvider>` from `headless/`. */
27
+ export const ModuleProvider: ModuleContextKit<VocabulariesRuntime>["Provider"] =
28
+ kit.Provider;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Namespaced TanStack Query keys (frontend-standard §2 — "keys are namespaced").
3
+ * Everything under the `"vocabularies"` root so a host can invalidate the whole
4
+ * module or match a single resource. Explicit tuple return types satisfy
5
+ * `--isolatedDeclarations`.
6
+ *
7
+ * Only ONE read is keyed here, and that is not an oversight: a term SEARCH is
8
+ * not a cacheable resource — it is superseded per keystroke and its answer is
9
+ * scoped to a dropdown that is about to close — so `useTermSearch` owns its own
10
+ * in-flight state and never enters the cache. `useTermLabels` is the opposite:
11
+ * a stable `{code: label}` for codes a page already holds, worth caching across
12
+ * every control that shows the same term, which is why it is a query.
13
+ */
14
+ const ROOT = "vocabularies" as const;
15
+
16
+ export const vocabulariesQueryKeys: {
17
+ readonly all: readonly ["vocabularies"];
18
+ termLabels: (
19
+ vocabulary: string,
20
+ level: string,
21
+ codes: readonly string[]
22
+ ) => readonly ["vocabularies", "termLabels", string, string, string];
23
+ } = {
24
+ all: [ROOT],
25
+ // The codes are sorted and joined into ONE key segment: two controls asking
26
+ // for the same set in a different order are the same read, and an array
27
+ // segment would make them two cache entries and two round trips.
28
+ termLabels: (vocabulary, level, codes) => [
29
+ ROOT,
30
+ "termLabels",
31
+ vocabulary,
32
+ level,
33
+ [...codes].sort().join(","),
34
+ ],
35
+ };