@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.1

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 (259) hide show
  1. package/dist/app/action-form-shim.d.ts +14 -0
  2. package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
  3. package/dist/app/app-features-context.d.ts +8 -0
  4. package/dist/app/app-features-context.js +16 -0
  5. package/dist/app/column-renderers.d.ts +30 -0
  6. package/dist/app/column-renderers.js +34 -0
  7. package/dist/app/config-edit-shim.d.ts +11 -0
  8. package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
  9. package/dist/app/content-editors.d.ts +38 -0
  10. package/dist/app/content-editors.js +26 -0
  11. package/dist/app/content-preview.d.ts +11 -0
  12. package/dist/app/content-preview.js +30 -0
  13. package/dist/app/custom-screens.d.ts +11 -0
  14. package/dist/app/custom-screens.js +20 -0
  15. package/dist/app/dashboard-body.d.ts +16 -0
  16. package/dist/app/dashboard-body.js +11 -0
  17. package/dist/app/extension-form-submit.d.ts +32 -0
  18. package/dist/app/extension-form-submit.js +73 -0
  19. package/dist/app/extension-sections.d.ts +72 -0
  20. package/dist/app/extension-sections.js +34 -0
  21. package/dist/app/feature-schema.d.ts +4 -0
  22. package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
  23. package/dist/app/form-schema.d.ts +4 -0
  24. package/dist/app/form-schema.js +85 -0
  25. package/dist/app/kumiko-screen.d.ts +43 -0
  26. package/dist/app/kumiko-screen.js +2206 -0
  27. package/dist/app/layout-fields.d.ts +3 -0
  28. package/dist/app/layout-fields.js +20 -0
  29. package/dist/app/list-facets.d.ts +48 -0
  30. package/dist/app/list-facets.js +185 -0
  31. package/dist/app/nav.d.ts +61 -0
  32. package/dist/app/nav.js +139 -0
  33. package/dist/app/projection-detail-shim.d.ts +8 -0
  34. package/dist/app/projection-detail-shim.js +71 -0
  35. package/dist/app/projection-list-shim.d.ts +13 -0
  36. package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
  37. package/dist/app/qn.d.ts +7 -0
  38. package/{src/app/qn.ts → dist/app/qn.js} +9 -11
  39. package/dist/app/qualify-screen-id.d.ts +2 -0
  40. package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
  41. package/dist/app/reference-facet-bridge.d.ts +10 -0
  42. package/dist/app/reference-facet-bridge.js +22 -0
  43. package/dist/app/return-to.d.ts +31 -0
  44. package/dist/app/return-to.js +188 -0
  45. package/dist/app/row-actions.d.ts +77 -0
  46. package/dist/app/row-actions.js +416 -0
  47. package/dist/app/screen-access.d.ts +8 -0
  48. package/dist/app/screen-access.js +48 -0
  49. package/dist/app/secret-mint-body.d.ts +10 -0
  50. package/dist/app/secret-mint-body.js +140 -0
  51. package/dist/app/secrets-edit-body.d.ts +8 -0
  52. package/dist/app/secrets-edit-body.js +76 -0
  53. package/dist/app/use-embedded-screen.d.ts +6 -0
  54. package/dist/app/use-embedded-screen.js +19 -0
  55. package/dist/app/variable-chips.d.ts +7 -0
  56. package/dist/app/variable-chips.js +16 -0
  57. package/dist/app/write-failed-error.d.ts +6 -0
  58. package/dist/app/write-failed-error.js +21 -0
  59. package/dist/components/embedded-list-field.d.ts +10 -0
  60. package/dist/components/embedded-list-field.js +259 -0
  61. package/dist/components/grid-cell-for-field.d.ts +25 -0
  62. package/dist/components/grid-cell-for-field.js +31 -0
  63. package/dist/components/multi-select-checkboxes.d.ts +14 -0
  64. package/dist/components/multi-select-checkboxes.js +36 -0
  65. package/dist/components/reference-create-dialog.d.ts +13 -0
  66. package/dist/components/reference-create-dialog.js +46 -0
  67. package/dist/components/related-list-section.d.ts +25 -0
  68. package/dist/components/related-list-section.js +273 -0
  69. package/dist/components/render-edit-action-button.d.ts +22 -0
  70. package/dist/components/render-edit-action-button.js +43 -0
  71. package/dist/components/render-edit-logic.d.ts +6 -0
  72. package/dist/components/render-edit-logic.js +63 -0
  73. package/dist/components/render-edit-types.d.ts +209 -0
  74. package/dist/components/render-edit-types.js +1 -0
  75. package/dist/components/render-edit.d.ts +6 -0
  76. package/dist/components/render-edit.js +1169 -0
  77. package/dist/components/render-field.d.ts +38 -0
  78. package/dist/components/render-field.js +544 -0
  79. package/dist/components/render-list.d.ts +117 -0
  80. package/dist/components/render-list.js +232 -0
  81. package/dist/components/write-form-section.d.ts +17 -0
  82. package/dist/components/write-form-section.js +104 -0
  83. package/dist/context/dispatcher-context.d.ts +10 -0
  84. package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
  85. package/dist/context/draft-storage-context.d.ts +12 -0
  86. package/dist/context/draft-storage-context.js +18 -0
  87. package/dist/context/user-roles-context.d.ts +7 -0
  88. package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
  89. package/dist/format-when.d.ts +1 -0
  90. package/{src/format-when.ts → dist/format-when.js} +7 -7
  91. package/dist/hooks/reference-limits.d.ts +3 -0
  92. package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
  93. package/dist/hooks/use-ai-text.d.ts +52 -0
  94. package/dist/hooks/use-ai-text.js +90 -0
  95. package/dist/hooks/use-disclosure.d.ts +7 -0
  96. package/dist/hooks/use-disclosure.js +8 -0
  97. package/dist/hooks/use-form.d.ts +7 -0
  98. package/dist/hooks/use-form.js +36 -0
  99. package/dist/hooks/use-list-url-state.d.ts +43 -0
  100. package/dist/hooks/use-list-url-state.js +131 -0
  101. package/dist/hooks/use-mutation.d.ts +9 -0
  102. package/dist/hooks/use-mutation.js +37 -0
  103. package/dist/hooks/use-query.d.ts +14 -0
  104. package/dist/hooks/use-query.js +118 -0
  105. package/dist/hooks/use-reference-lookup.d.ts +14 -0
  106. package/dist/hooks/use-reference-lookup.js +63 -0
  107. package/dist/hooks/use-store.d.ts +3 -0
  108. package/dist/hooks/use-store.js +38 -0
  109. package/dist/hooks/use-stream-handler.d.ts +14 -0
  110. package/dist/hooks/use-stream-handler.js +93 -0
  111. package/dist/i18n-defaults.d.ts +2 -0
  112. package/dist/i18n-defaults.js +201 -0
  113. package/dist/i18n.d.ts +64 -0
  114. package/dist/i18n.js +166 -0
  115. package/dist/index.d.ts +77 -0
  116. package/dist/index.js +55 -0
  117. package/dist/inside-drawer.d.ts +2 -0
  118. package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
  121. package/dist/primitives.d.ts +1351 -0
  122. package/dist/primitives.js +73 -0
  123. package/dist/screen-fills-height.d.ts +3 -0
  124. package/dist/screen-fills-height.js +3 -0
  125. package/dist/share-links.d.ts +4 -0
  126. package/dist/share-links.js +11 -0
  127. package/dist/sort-by-accessor.d.ts +5 -0
  128. package/dist/sort-by-accessor.js +16 -0
  129. package/dist/sse/live-events.d.ts +23 -0
  130. package/dist/sse/live-events.js +28 -0
  131. package/dist/tokens.d.ts +83 -0
  132. package/dist/tokens.js +80 -0
  133. package/package.json +16 -9
  134. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  135. package/src/__tests__/format-when.test.ts +0 -12
  136. package/src/__tests__/i18n.test.tsx +0 -283
  137. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  138. package/src/__tests__/qn.test.ts +0 -83
  139. package/src/__tests__/share-links.test.ts +0 -26
  140. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  141. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  142. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  143. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  144. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  145. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  146. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  147. package/src/app/__tests__/content-editors.test.tsx +0 -128
  148. package/src/app/__tests__/content-preview.test.tsx +0 -184
  149. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  150. package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
  151. package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
  152. package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
  153. package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
  154. package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
  155. package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
  156. package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
  157. package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
  158. package/src/app/__tests__/feature-schema.test.ts +0 -40
  159. package/src/app/__tests__/form-schema.test.ts +0 -247
  160. package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
  161. package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
  162. package/src/app/__tests__/nav.test.ts +0 -198
  163. package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
  164. package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
  165. package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
  166. package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
  167. package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
  168. package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
  169. package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
  170. package/src/app/__tests__/qualified-names.test.ts +0 -37
  171. package/src/app/__tests__/return-to.test.ts +0 -396
  172. package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
  173. package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
  174. package/src/app/__tests__/screen-access-allows.test.ts +0 -34
  175. package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
  176. package/src/app/__tests__/variable-chips.test.tsx +0 -70
  177. package/src/app/app-features-context.tsx +0 -25
  178. package/src/app/column-renderers.tsx +0 -68
  179. package/src/app/content-editors.tsx +0 -85
  180. package/src/app/content-preview.tsx +0 -67
  181. package/src/app/custom-screens.tsx +0 -29
  182. package/src/app/dashboard-body.tsx +0 -33
  183. package/src/app/extension-form-submit.tsx +0 -123
  184. package/src/app/extension-sections.tsx +0 -111
  185. package/src/app/form-schema.ts +0 -91
  186. package/src/app/kumiko-screen.tsx +0 -3801
  187. package/src/app/layout-fields.ts +0 -31
  188. package/src/app/list-facets.ts +0 -259
  189. package/src/app/nav.tsx +0 -258
  190. package/src/app/projection-detail-shim.ts +0 -86
  191. package/src/app/reference-facet-bridge.tsx +0 -58
  192. package/src/app/return-to.tsx +0 -234
  193. package/src/app/row-actions.ts +0 -640
  194. package/src/app/screen-access.ts +0 -63
  195. package/src/app/secret-mint-body.tsx +0 -259
  196. package/src/app/secrets-edit-body.tsx +0 -194
  197. package/src/app/use-embedded-screen.ts +0 -27
  198. package/src/app/variable-chips.tsx +0 -39
  199. package/src/app/write-failed-error.ts +0 -27
  200. package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
  201. package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
  202. package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
  203. package/src/components/__tests__/related-list-section.test.tsx +0 -1733
  204. package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
  205. package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
  206. package/src/components/__tests__/render-edit-logic.test.ts +0 -314
  207. package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
  208. package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
  209. package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
  210. package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
  211. package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
  212. package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
  213. package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
  214. package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
  215. package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
  216. package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
  217. package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
  218. package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
  219. package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
  220. package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
  221. package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
  222. package/src/components/__tests__/write-form-section.test.tsx +0 -375
  223. package/src/components/embedded-list-field.tsx +0 -354
  224. package/src/components/grid-cell-for-field.tsx +0 -88
  225. package/src/components/multi-select-checkboxes.tsx +0 -93
  226. package/src/components/reference-create-dialog.tsx +0 -91
  227. package/src/components/related-list-section.tsx +0 -491
  228. package/src/components/render-edit-action-button.tsx +0 -109
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -226
  231. package/src/components/render-edit.tsx +0 -1871
  232. package/src/components/render-field.tsx +0 -951
  233. package/src/components/render-list.tsx +0 -593
  234. package/src/components/write-form-section.tsx +0 -198
  235. package/src/context/draft-storage-context.tsx +0 -39
  236. package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
  237. package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
  238. package/src/hooks/__tests__/use-form.test.tsx +0 -65
  239. package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
  240. package/src/hooks/__tests__/use-query.test.tsx +0 -149
  241. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  242. package/src/hooks/use-ai-text.ts +0 -172
  243. package/src/hooks/use-disclosure.ts +0 -20
  244. package/src/hooks/use-form.ts +0 -78
  245. package/src/hooks/use-list-url-state.ts +0 -207
  246. package/src/hooks/use-mutation.ts +0 -61
  247. package/src/hooks/use-query.ts +0 -166
  248. package/src/hooks/use-reference-lookup.ts +0 -76
  249. package/src/hooks/use-store.ts +0 -48
  250. package/src/hooks/use-stream-handler.ts +0 -134
  251. package/src/i18n-defaults.ts +0 -240
  252. package/src/i18n.tsx +0 -286
  253. package/src/index.ts +0 -279
  254. package/src/primitives.tsx +0 -1567
  255. package/src/screen-fills-height.ts +0 -3
  256. package/src/share-links.ts +0 -12
  257. package/src/sort-by-accessor.ts +0 -20
  258. package/src/sse/live-events.tsx +0 -56
  259. package/src/tokens.tsx +0 -170
@@ -0,0 +1,63 @@
1
+ // Tier 2.7e-4: Renderer-Side Eagerload für Reference-Felder.
2
+ //
3
+ // Pro Reference-Spalte einer Liste wird einmal `<feature>:query:
4
+ // <refEntity>:list` (limit:200) gerufen, eine Map<uuid, displayValue>
5
+ // gebaut, und der Renderer nutzt sie als Cell-Display-Renderer.
6
+ //
7
+ // Strategy-Trade-offs:
8
+ // - Server-side Drizzle-Joins wären effizienter (eine Query statt
9
+ // N+1), würden aber die executor-API um lookupTables erweitern
10
+ // müssen. Für das MVP geht der Renderer-Side-Pfad vor.
11
+ // - Limit:200 ist eine harte UX-Grenze: bei mehr Entries in der
12
+ // referenced Entity zeigen die letzten Rows nur noch UUIDs.
13
+ // Searchable-Combobox (Tier 2.1c) + Server-Eagerload mit
14
+ // ID-Whitelist heben das später auf.
15
+ //
16
+ // Dieser Hook ist call-stable: Lookup-Map wird durch useQuery's
17
+ // internen Cache shared zwischen List + Edit-Form für die gleiche
18
+ // Entity, Live-Updates kommen via SSE (use-query-live).
19
+ import { REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
20
+ import { useMemo } from "react";
21
+ import { toKebab } from "../app/qn.js";
22
+ import { useTranslation } from "../i18n.js";
23
+ import { REFERENCE_LIST_LOOKUP_LIMIT } from "./reference-limits.js";
24
+ import { useQuery } from "./use-query.js";
25
+ /** Where a reference target's label rows come from: the central override for
26
+ * that `feature:entity` if one exists, else the entity-convention list
27
+ * handler plus the caller's own label field. */
28
+ export function referenceLookupSource(refFeature, refEntity, labelField) {
29
+ const override = REFERENCE_LOOKUP_SOURCES[`${refFeature}:${refEntity}`];
30
+ if (override !== undefined)
31
+ return override;
32
+ return {
33
+ queryQn: `${toKebab(refFeature)}:query:${toKebab(refEntity)}:list`,
34
+ labelKey: labelField,
35
+ };
36
+ }
37
+ /** Empty while the query loads (or fails), so callers fall back to the raw id.
38
+ * `featureName` is the reference's target feature, not the screen's. */
39
+ export function useReferenceLookup(featureName, refEntity, labelField) {
40
+ const { queryQn, labelKey } = referenceLookupSource(featureName, refEntity, labelField);
41
+ const result = useQuery(queryQn, {
42
+ limit: REFERENCE_LIST_LOOKUP_LIMIT,
43
+ });
44
+ const translate = useTranslation();
45
+ const systemLabel = SYSTEM_REFERENCE_LABELS[`${featureName}:${refEntity}`];
46
+ const map = useMemo(() => {
47
+ const out = new Map();
48
+ for (const row of result.data?.rows ?? []) {
49
+ const id = row["id"];
50
+ if (id === undefined || id === null)
51
+ continue;
52
+ const idStr = String(id);
53
+ const label = row[labelKey] ?? id;
54
+ out.set(idStr, String(label));
55
+ }
56
+ // System-scope ids (e.g. SYSTEM_TENANT_ID) never have a backing row, so
57
+ // the bulk lookup above never covers them — inject the label directly.
58
+ if (systemLabel !== undefined)
59
+ out.set(systemLabel.id, translate(systemLabel.labelKey));
60
+ return out;
61
+ }, [result.data, labelKey, systemLabel, translate]);
62
+ return { map, loading: result.loading };
63
+ }
@@ -0,0 +1,3 @@
1
+ import type { Store } from "@cosmicdrift/kumiko-headless";
2
+ export declare function useStore<T>(store: Store<T>): T;
3
+ export declare function useStoreSelector<T, S>(store: Store<T>, select: (snapshot: T) => S, equals?: (a: S, b: S) => boolean): S;
@@ -0,0 +1,38 @@
1
+ import { useRef, useSyncExternalStore } from "react";
2
+ // React-Bindings für Kumikos Store-Primitive (`createStore` aus
3
+ // @cosmicdrift/kumiko-headless). Beide Hooks sind dünne Wrapper um React's
4
+ // useSyncExternalStore — die ganze Subscribe/Notify-Mechanik lebt im
5
+ // Store selbst, hier wird nur React verdrahtet.
6
+ //
7
+ // useStore: gibt den ganzen Snapshot zurück. Re-rendert wenn der
8
+ // Store seine Identität wechselt. Object.is-Gate im Store selbst
9
+ // verhindert no-op-Updates.
10
+ //
11
+ // useStoreSelector: gibt eine abgeleitete Sicht zurück. Selectors die
12
+ // Object-/Array-Literale zurückgeben (z.B. `s => ({ a, b })`) erzeugen
13
+ // pro Render eine neue Identität — useSyncExternalStore würde das als
14
+ // "Change" lesen und in eine Re-Render-Schleife laufen. Der dritte
15
+ // Arg `equals` dient genau dafür: gleiche Auswahl → cached
16
+ // Identität zurück, kein Re-Render.
17
+ // @wrapper-known semantic-alias
18
+ export function useStore(store) {
19
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
20
+ }
21
+ export function useStoreSelector(store, select, equals = Object.is) {
22
+ // Cache der letzten Auswahl. Beim nächsten getSnapshot-Call vergleicht
23
+ // der Wrapper die neu berechnete Auswahl mit dem Cache und gibt — falls
24
+ // gleich — die cached Identität zurück. Das ist der Trick, der
25
+ // Selector-Returns wie `s => ({ a, b })` stabil hält.
26
+ const lastSelected = useRef(undefined);
27
+ const initialized = useRef(false);
28
+ const getSelectedSnapshot = () => {
29
+ const next = select(store.getSnapshot());
30
+ if (initialized.current && equals(lastSelected.current, next)) {
31
+ return lastSelected.current;
32
+ }
33
+ lastSelected.current = next;
34
+ initialized.current = true;
35
+ return next;
36
+ };
37
+ return useSyncExternalStore(store.subscribe, getSelectedSnapshot, getSelectedSnapshot);
38
+ }
@@ -0,0 +1,14 @@
1
+ import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
+ export type StreamStatus = "idle" | "streaming" | "done" | "error";
3
+ export type UseStreamHandlerResult<TChunk> = {
4
+ readonly chunks: readonly TChunk[];
5
+ readonly status: StreamStatus;
6
+ readonly error: DispatcherError | null;
7
+ readonly start: (payload?: unknown) => Promise<void>;
8
+ readonly abort: () => void;
9
+ readonly reset: () => void;
10
+ };
11
+ export type UseStreamHandlerOptions = {
12
+ readonly autoStart?: boolean;
13
+ };
14
+ export declare function useStreamHandler<TChunk = unknown>(type: string, payload?: unknown, options?: UseStreamHandlerOptions): UseStreamHandlerResult<TChunk>;
@@ -0,0 +1,93 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useDispatcher } from "../context/dispatcher-context.js";
3
+ export function useStreamHandler(type, payload = {}, options = {}) {
4
+ const dispatcher = useDispatcher();
5
+ const { autoStart = false } = options;
6
+ const [chunks, setChunks] = useState([]);
7
+ const [status, setStatus] = useState("idle");
8
+ const [error, setError] = useState(null);
9
+ const activeCtrl = useRef(null);
10
+ const payloadRef = useRef(payload);
11
+ payloadRef.current = payload;
12
+ const abort = useCallback(() => {
13
+ activeCtrl.current?.abort();
14
+ activeCtrl.current = null;
15
+ // User cancel — exit streaming so UI can re-enable the start button.
16
+ setStatus((s) => (s === "streaming" ? "idle" : s));
17
+ }, []);
18
+ const reset = useCallback(() => {
19
+ abort();
20
+ setChunks([]);
21
+ setStatus("idle");
22
+ setError(null);
23
+ }, [abort]);
24
+ // Shared by every abort-detection branch below: only touch status if this
25
+ // run is still the active one (or activeCtrl was already cleared by abort()).
26
+ const settleAborted = useCallback((ctrl) => {
27
+ if (activeCtrl.current === null || activeCtrl.current === ctrl)
28
+ setStatus("idle");
29
+ }, []);
30
+ const start = useCallback(async (overridePayload) => {
31
+ activeCtrl.current?.abort();
32
+ const ctrl = new AbortController();
33
+ activeCtrl.current = ctrl;
34
+ setChunks([]);
35
+ setError(null);
36
+ setStatus("streaming");
37
+ const body = overridePayload !== undefined ? overridePayload : payloadRef.current;
38
+ try {
39
+ for await (const chunk of dispatcher.stream(type, body, { signal: ctrl.signal })) {
40
+ // skip: a newer start() already superseded this run
41
+ if (ctrl.signal.aborted) {
42
+ settleAborted(ctrl);
43
+ return;
44
+ }
45
+ setChunks((prev) => [...prev, chunk]);
46
+ }
47
+ // skip: aborted after last chunk, don't mark done
48
+ if (ctrl.signal.aborted) {
49
+ settleAborted(ctrl);
50
+ return;
51
+ }
52
+ setStatus("done");
53
+ }
54
+ catch (e) {
55
+ // skip: abort is not an error toast
56
+ if (ctrl.signal.aborted) {
57
+ settleAborted(ctrl);
58
+ return;
59
+ }
60
+ const mapped = asDispatcherError(e);
61
+ // skip: dispatcher-mapped abort (fetch cancelled)
62
+ if (mapped.code === "aborted") {
63
+ settleAborted(ctrl);
64
+ return;
65
+ }
66
+ setError(mapped);
67
+ setStatus("error");
68
+ }
69
+ }, [dispatcher, type, settleAborted]);
70
+ useEffect(() => {
71
+ // skip: autoStart off — streams are usually user-triggered
72
+ if (!autoStart)
73
+ return;
74
+ void start();
75
+ }, [autoStart, start]);
76
+ useEffect(() => {
77
+ return () => {
78
+ activeCtrl.current?.abort();
79
+ };
80
+ }, []);
81
+ return { chunks, status, error, start, abort, reset };
82
+ }
83
+ function asDispatcherError(e) {
84
+ if (e && typeof e === "object" && "code" in e && "message" in e && "i18nKey" in e) {
85
+ return e;
86
+ }
87
+ return {
88
+ code: "stream_error",
89
+ httpStatus: 0,
90
+ i18nKey: "errors.unknown",
91
+ message: e instanceof Error ? e.message : String(e),
92
+ };
93
+ }
@@ -0,0 +1,2 @@
1
+ import type { TranslationsByLocale } from "./i18n.js";
2
+ export declare const kumikoDefaultTranslations: TranslationsByLocale;
@@ -0,0 +1,201 @@
1
+ // Framework-Default-Bundle. Strings die die Renderer-Components hart
2
+ // brauchen (Save/Cancel/Delete-Buttons, Empty-States, Search-Placeholder,
3
+ // Nav-Toggle-aria-Labels, Validation-Reasons). createKumikoApp hängt das
4
+ // als ALLERLETZTEN Fallback in den LocaleProvider — Apps können
5
+ // einzelne Keys via clientFeatures.translations überschreiben.
6
+ //
7
+ // Convention: alle Keys mit `kumiko.`-Prefix damit sie nicht mit
8
+ // App-Keys kollidieren. Sub-Pfade gruppieren nach Bereich (actions /
9
+ // list / nav / form / validation).
10
+ export const kumikoDefaultTranslations = {
11
+ en: {
12
+ "kumiko.actions.save": "Save",
13
+ "kumiko.actions.cancel": "Cancel",
14
+ "kumiko.actions.delete": "Delete",
15
+ "kumiko.actions.delete-confirm": "Confirm delete?",
16
+ "kumiko.actions.reload": "Reload",
17
+ "kumiko.actions.create": "New",
18
+ "kumiko.actions.edit": "Edit",
19
+ "kumiko.actions.view": "View",
20
+ "kumiko.actions.copyLink": "Copy link",
21
+ "kumiko.actions.copyLinkCopied": "Copied!",
22
+ "kumiko.actions.next": "Next",
23
+ "kumiko.actions.back": "Back",
24
+ "kumiko.actions.finish": "Finish",
25
+ "kumiko.wizard.step": "Step {current} of {total}",
26
+ "kumiko.wizard.step-with-title": "Step {current} of {total} · {title}",
27
+ "kumiko.version.update-available": "A new version is available.",
28
+ "kumiko.toast.learn-more": "Learn more",
29
+ "kumiko.field.open-calendar": "Open calendar",
30
+ "kumiko.field.dateField.placeholderYear": "Y",
31
+ "kumiko.field.dateField.placeholderMonth": "M",
32
+ "kumiko.field.dateField.placeholderDay": "D",
33
+ "kumiko.field.time": "Time",
34
+ "kumiko.field.timezone": "Time zone",
35
+ "kumiko.field.locatedTzHint": "Time local to the given location",
36
+ "kumiko.field.reference-created-no-id": "Record was created but could not be selected automatically. Please select it manually.",
37
+ "kumiko.field.unsupported": "This field type can't be edited here yet.",
38
+ "kumiko.field.embedded-list.add-row": "Add row",
39
+ "kumiko.field.embedded-list.remove-row": "Remove row",
40
+ "kumiko.field.embedded-list.duplicate-row": "Duplicate row",
41
+ "kumiko.field.embedded-list.move-up": "Move up",
42
+ "kumiko.field.embedded-list.move-down": "Move down",
43
+ "kumiko.field.embedded-list.empty": "No rows yet.",
44
+ "kumiko.field.embedded-list.empty-cta": "Add first row",
45
+ "kumiko.field.embedded-list.paste-rows-truncated": "{count} pasted row(s) were dropped (max row count reached).",
46
+ "kumiko.field.embedded-list.paste-cells-unmatched": "{count} cell(s) had no matching option and were left unchanged.",
47
+ "kumiko.field.multiSelect.select-all": "Select all",
48
+ "kumiko.field.multiSelect.deselect-all": "Deselect all",
49
+ "kumiko.list.search-placeholder": "Search…",
50
+ "kumiko.list.empty.title": "No entries yet.",
51
+ "kumiko.list.empty.hint": "Create the first one to get started.",
52
+ "kumiko.list.no-entries": "No entries.",
53
+ "kumiko.list.end-of-list": "— End of list —",
54
+ "kumiko.list.sort.label": "Sort",
55
+ "kumiko.list.sort.unsorted": "Unsorted",
56
+ "kumiko.list.filter.reset": "Reset",
57
+ "kumiko.list.filter.toggle": "Filters",
58
+ "kumiko.list.related-list-truncated": "Showing the first {count} entries. More are available but not loaded — this list does not paginate.",
59
+ "kumiko.reference.system-tenant": "System",
60
+ "kumiko.reference.system-user": "System",
61
+ "kumiko.pager.status": "{from}–{to} of {total}",
62
+ "kumiko.pager.pageOf": "Page {page} of {pages}",
63
+ "kumiko.form.all-required": "All fields are required.",
64
+ "kumiko.pager.status.noun": "{from}–{to} of {total} {noun}",
65
+ "kumiko.pager.pageSize": "{size} per page",
66
+ "kumiko.pager.pageSizeLabel": "Entries per page",
67
+ "kumiko.list.count.noun": "{count} {noun}",
68
+ "kumiko.list.count.one": "1 entry",
69
+ "kumiko.list.count.other": "{count} entries",
70
+ "kumiko.list.row-actions.more": "More actions",
71
+ "kumiko.list.sort.by": "Sorted by {column}",
72
+ "kumiko.form.changed": "changed",
73
+ "kumiko.form.on-this-page": "On this page",
74
+ "kumiko.form.unsaved.one": "1 unsaved change",
75
+ "kumiko.form.unsaved.other": "{count} unsaved changes",
76
+ "kumiko.wizard.next-with-title": "Next: {title}",
77
+ "kumiko.wizard.save-and-close": "Save and close",
78
+ "kumiko.pager.previousPage": "Previous page",
79
+ "kumiko.pager.nextPage": "Next page",
80
+ "kumiko.combobox.search-placeholder": "Search…",
81
+ "kumiko.combobox.empty": "No matches.",
82
+ "kumiko.combobox.loading": "Loading…",
83
+ "kumiko.combobox.placeholder": "—",
84
+ "kumiko.dashboard.filter.all": "All",
85
+ "kumiko.app-boot.loading": "Loading…",
86
+ "kumiko.app-boot.unauthorized": "You need to sign in to see this.",
87
+ "kumiko.app-boot.failed": "Couldn't load the app. Check your connection and try again.",
88
+ "kumiko.app-boot.no-open-screen": "This app has no screen that's accessible without a role restriction.",
89
+ "kumiko.widget.loading": "Loading…",
90
+ "kumiko.widget.error.title": "Couldn't load.",
91
+ "kumiko.widget.upload.uploading": "Uploading…",
92
+ "kumiko.widget.upload.done": "Uploaded",
93
+ "kumiko.widget.upload.error": "Failed",
94
+ "kumiko.widget.upload.rejected-type": "File type not allowed",
95
+ "kumiko.widget.step-bar.done": "Done",
96
+ "kumiko.widget.drawer.restore": "Restore drawer width",
97
+ "kumiko.widget.drawer.maximize": "Maximize drawer width",
98
+ "kumiko.widget.drawer.resize": "Resize drawer",
99
+ "kumiko.widget.floatingPanel.move": "Move panel",
100
+ "kumiko.widget.floatingPanel.resizeTop": "Resize panel from the top edge",
101
+ "kumiko.widget.floatingPanel.resizeRight": "Resize panel from the right edge",
102
+ "kumiko.widget.floatingPanel.resizeBottom": "Resize panel from the bottom edge",
103
+ "kumiko.widget.floatingPanel.resizeLeft": "Resize panel from the left edge",
104
+ "kumiko.planCard.current": "Current plan",
105
+ "kumiko.planCard.priceUnavailable": "Price not available",
106
+ "kumiko.nav.expand": "Expand",
107
+ "kumiko.nav.collapse": "Collapse",
108
+ "kumiko.nav.search": "Search navigation…",
109
+ "kumiko.nav.language": "Language",
110
+ "kumiko.workspace.switch": "Switch workspace",
111
+ // Workspace-Switcher fallback label when activeId points at no visible workspace.
112
+ "kumiko.workspace.select": "Select workspace",
113
+ "kumiko.dialog.confirm": "Confirm",
114
+ "kumiko.dialog.cancel": "Cancel",
115
+ "kumiko.dialog.close": "Close",
116
+ "kumiko.drawer.discard.title": "Discard changes?",
117
+ "kumiko.drawer.discard.body": "Your input in this form will be lost.",
118
+ "kumiko.drawer.discard.confirm": "Discard",
119
+ "kumiko.drawer.discard.cancel": "Keep editing",
120
+ "kumiko.aiText.acceptHint": "Tab = accept, Esc = discard",
121
+ "kumiko.aiText.correct": "Correct",
122
+ "kumiko.aiText.translate": "Translate",
123
+ "kumiko.aiText.rewrite": "Rewrite",
124
+ "kumiko.aiText.diff.before": "Before",
125
+ "kumiko.aiText.diff.after": "After",
126
+ "kumiko.aiText.diff.generating": "Generating…",
127
+ "kumiko.aiText.style.formal": "Formal",
128
+ "kumiko.aiText.style.casual": "Casual",
129
+ "kumiko.aiText.style.concise": "Concise",
130
+ "kumiko.aiText.style.expand": "Expand",
131
+ "kumiko.aiText.capExceeded": "Monthly AI limit reached.",
132
+ "kumiko.rowAction.failed": "Action failed",
133
+ "kumiko.contentEditor.insertVariable": "Insert {name}",
134
+ "kumiko.contentEditor.preview": "Preview",
135
+ "kumiko.contentEditor.editMode": "Edit",
136
+ "kumiko.contentEditor.bold": "Bold",
137
+ "kumiko.contentEditor.italic": "Italic",
138
+ "kumiko.contentEditor.heading1": "Heading 1",
139
+ "kumiko.contentEditor.heading2": "Heading 2",
140
+ "kumiko.contentEditor.bulletList": "Bullet list",
141
+ "kumiko.contentEditor.orderedList": "Numbered list",
142
+ "kumiko.config.source.user": "My value",
143
+ "kumiko.config.source.tenant": "Tenant",
144
+ "kumiko.config.source.system": "System",
145
+ "kumiko.config.source.appOverride": "App override",
146
+ "kumiko.config.source.computed": "Computed",
147
+ "kumiko.config.source.default": "Default",
148
+ "kumiko.config.source.missing": "Missing",
149
+ "kumiko.config.cascade.noValue": "No value set",
150
+ "kumiko.config.cascade.activeMarker": "active",
151
+ "kumiko.config.cascade.resetTo": "Reset override ({scope})",
152
+ "kumiko.form.error.generic": "Something went wrong.",
153
+ "kumiko.form.error.version-conflict": "Record was modified in the meantime. Reload and try again.",
154
+ "kumiko.form.extension.save-failed": "A custom field could not be saved.",
155
+ "kumiko.form.draft.resume-multiple": "Found multiple open drafts for this form. Which one do you want to resume?",
156
+ "kumiko.form.draft.resume-single": "Found an open draft for this form. Do you want to resume it?",
157
+ "kumiko.form.draft.start-new": "Start new",
158
+ "kumiko.secretMint.title": "Your new secret",
159
+ "kumiko.secretMint.warning": "Copy this now — it is shown only this once and cannot be retrieved later.",
160
+ "kumiko.secretMint.confirm": "I have copied it",
161
+ "kumiko.secretMint.copy": "Copy",
162
+ "kumiko.secretMint.copied": "Copied",
163
+ "kumiko.secretMint.done": "Done.",
164
+ "kumiko.validation.required": "Required.",
165
+ "kumiko.validation.invalid": "Invalid value.",
166
+ "kumiko.validation.too-short": "Too short (at least {min} characters).",
167
+ "kumiko.validation.too-long": "Too long (at most {max} characters).",
168
+ "kumiko.validation.out-of-range": "Value out of allowed range.",
169
+ "errors.validation.invalid_type": "Invalid value.",
170
+ "errors.validation.too_small": "Too small or too short (minimum: {minimum}).",
171
+ "errors.validation.too_big": "Too big or too long (maximum: {maximum}).",
172
+ "errors.validation.invalid_format": "Invalid format.",
173
+ "errors.validation.not_multiple_of": "Must be a multiple of {divisor}.",
174
+ "errors.validation.unrecognized_keys": "Unknown fields.",
175
+ "errors.validation.invalid_union": "Invalid value.",
176
+ "errors.validation.invalid_key": "Invalid key.",
177
+ "errors.validation.invalid_element": "Invalid entry.",
178
+ "errors.validation.invalid_value": "Invalid choice.",
179
+ "errors.validation.custom": "Invalid value.",
180
+ "errors.validation.unexpected_field": "Unknown field.",
181
+ "errors.validation.out_of_bounds": "Value out of allowed range.",
182
+ "errors.validation.invalid_option": "Invalid choice.",
183
+ "errors.validation.failed": "Validation failed.",
184
+ "errors.feature.disabled": "This feature is currently unavailable.",
185
+ "errors.access.denied": "You don't have permission to do this.",
186
+ "errors.notFound": "Not found.",
187
+ "errors.conflict": "Conflict — the operation could not be completed.",
188
+ "errors.versionConflict": "The record was modified in the meantime. Reload and try again.",
189
+ "errors.preconditionFailed": "The record changed in the meantime. Reload and try again.",
190
+ "errors.uniqueViolation": "This entry already exists.",
191
+ "errors.unprocessable": "The request could not be processed.",
192
+ "errors.unconfigured": "This feature isn't configured yet.",
193
+ "errors.internal": "Something went wrong. Please try again later.",
194
+ "errors.rate_limited": "Too many requests. Please try again shortly.",
195
+ "errors.cap.exceeded": "Limit reached. Upgrade your plan or wait for the next period.",
196
+ "errors.download.urlMissing": "Download unavailable — please try again.",
197
+ "auth.errors.originNotAllowed": "Requests from this origin are not allowed.",
198
+ "dispatcher.errors.network": "Network error. Please check your connection and try again.",
199
+ "dispatcher.errors.aborted": "Request was cancelled.",
200
+ },
201
+ };
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { type Formality, type TranslationValue } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
3
+ import { type ReactNode } from "react";
4
+ /** Map of i18n key → TranslationValue. Strings may contain `{name}`
5
+ * placeholders (same semantics as i18next t); plural entries resolve via
6
+ * `resolveTranslationValue`. */
7
+ export type TranslationBundle = Readonly<Record<string, TranslationValue>>;
8
+ /** Map von Locale-Code (BCP-47, z.B. `"de"`, `"en-US"`) → Bundle. */
9
+ export type TranslationsByLocale = Readonly<Record<string, TranslationBundle>>;
10
+ /** Key-first shape for `r.translations({ keys })` — each key maps locale → TranslationValue. */
11
+ export type TranslationsByKey = Readonly<Record<string, Readonly<Record<string, TranslationValue>>>>;
12
+ /** Pivot key-first server translations to locale-first client bundles. */
13
+ export declare function translationsByLocaleFromKeys(source: TranslationsByKey): TranslationsByLocale;
14
+ /** Merged zwei TranslationsByLocale-Maps — der override gewinnt pro Key,
15
+ * die Locales werden zusammengeführt. Standard-Baustein für Client-
16
+ * Plugins, die App-Overrides über ihre Default-Bundles legen. */
17
+ export declare function mergeTranslations(base: TranslationsByLocale, override: TranslationsByLocale): TranslationsByLocale;
18
+ export type LocaleProviderProps = {
19
+ readonly resolver: LocaleResolver;
20
+ /** Default bundles supplied by feature plugins. Lookup order per key:
21
+ * (1) app resolver, (2) these bundles in array order, (3) key as-is.
22
+ * Apps can thus override individual keys without swapping out whole
23
+ * feature bundles. */
24
+ readonly fallbackBundles?: readonly TranslationsByLocale[];
25
+ /** Falls back to fallbackLocale when neither the current-locale nor the
26
+ * key lookup hits in a plugin bundle. Default: `"en"`. */
27
+ readonly fallbackLocale?: string;
28
+ readonly children: ReactNode;
29
+ };
30
+ export declare function LocaleProvider({ resolver, fallbackBundles, fallbackLocale, children, }: LocaleProviderProps): ReactNode;
31
+ /** Liefert den aktuellen LocaleResolver und abonniert automatisch
32
+ * Locale-Änderungen — der aufrufende Component re-rendert sobald die
33
+ * Sprache gewechselt wird. Wirft wenn kein Provider im Baum ist. */
34
+ export declare function useLocale(): LocaleResolver;
35
+ /** `t("key", params)` resolves in this order:
36
+ * 1. App resolver (e.g. i18next)
37
+ * 2. Plugin fallback bundles for the current locale (under a "formal"
38
+ * FormalityProvider, `<locale>-x-formal` across all bundles first)
39
+ * 3. Plugin fallback bundles for fallbackLocale
40
+ * 4. The key as-is
41
+ * `{name}` interpolation applies to every source, fallback strings included. */
42
+ export declare function useTranslation(): (key: string, params?: Readonly<Record<string, unknown>>) => string;
43
+ /** Wording for the subtree: `formal` makes `t` prefer the `<locale>-x-formal`
44
+ * bundle entries (e.g. "Sie" on public pages) over plain `<locale>`. */
45
+ export declare function FormalityProvider({ formality, children, }: {
46
+ readonly formality: Formality;
47
+ readonly children: ReactNode;
48
+ }): ReactNode;
49
+ export declare function useFormality(): Formality;
50
+ /** Non-throwing LocaleContext read — undefined outside LocaleProvider.
51
+ * Used by DataTable FormatCell so plain tables without a provider do not crash. */
52
+ export declare function useOptionalLocale(): string | undefined;
53
+ /** Non-throwing time-zone read — "UTC" outside LocaleProvider, matching
54
+ * createStaticLocaleResolver's own default. */
55
+ export declare function useOptionalTimeZone(): string;
56
+ /** Non-throwing translate — undefined outside LocaleProvider. */
57
+ export declare function useOptionalTranslation(): ((key: string, params?: Readonly<Record<string, unknown>>) => string) | undefined;
58
+ /** Default-Resolver für Apps ohne eigene i18n-Schicht. Gibt jeden Key
59
+ * unverändert zurück — die Plugin-Fallback-Bundles erledigen dann die
60
+ * echte Übersetzung. Nützlich auch als Basis für Tests. */
61
+ export declare function createStaticLocaleResolver(options?: {
62
+ readonly locale?: string;
63
+ readonly timeZone?: string;
64
+ }): LocaleResolver;
package/dist/i18n.js ADDED
@@ -0,0 +1,166 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Locale handling for React consumers of the Kumiko renderer. A thin layer
3
+ // around the platform-agnostic `LocaleResolver` contract from
4
+ // @cosmicdrift/kumiko-headless: provider, hooks, a default no-op resolver,
5
+ // and a fallback-bundle merge for feature-supplied translations.
6
+ //
7
+ // Architecture:
8
+ // 1. The app supplies exactly one `LocaleResolver` via `<LocaleProvider>`
9
+ // (or none at all → the default resolver returns keys as-is).
10
+ // 2. Feature plugins may bring fallback bundles: when the app resolver
11
+ // can't resolve a key, `useTranslation` tries the plugin bundles.
12
+ // This keeps feature UI independent of the app's own i18next instance
13
+ // and works out of the box, while staying fully overridable.
14
+ // 3. Re-render on locale change via `useSyncExternalStore` on the
15
+ // resolver's `subscribe()` — app code can switch language mid-session
16
+ // without a reload.
17
+ import { formalLocaleTag, resolveTranslationValue, } from "@cosmicdrift/kumiko-framework/ui-types";
18
+ import { createContext, useCallback, useContext, useMemo, useSyncExternalStore, } from "react";
19
+ /** Pivot key-first server translations to locale-first client bundles. */
20
+ export function translationsByLocaleFromKeys(source) {
21
+ const out = {};
22
+ for (const [key, byLocale] of Object.entries(source)) {
23
+ for (const [locale, value] of Object.entries(byLocale)) {
24
+ out[locale] ??= {};
25
+ out[locale][key] = value;
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+ /** Merged zwei TranslationsByLocale-Maps — der override gewinnt pro Key,
31
+ * die Locales werden zusammengeführt. Standard-Baustein für Client-
32
+ * Plugins, die App-Overrides über ihre Default-Bundles legen. */
33
+ export function mergeTranslations(base, override) {
34
+ const locales = new Set([...Object.keys(base), ...Object.keys(override)]);
35
+ const merged = {};
36
+ for (const locale of locales) {
37
+ merged[locale] = { ...(base[locale] ?? {}), ...(override[locale] ?? {}) };
38
+ }
39
+ return merged;
40
+ }
41
+ const LocaleContext = createContext(undefined);
42
+ const FormalityContext = createContext("informal");
43
+ // Stabile Referenz statt `fallbackBundles = []` als Default-Parameter:
44
+ // ein Literal-Default wird bei JEDEM Aufruf neu allokiert und würde die
45
+ // useMemo-Referenzprüfung im Provider unten aushebeln, sobald der
46
+ // Aufrufer fallbackBundles weglässt.
47
+ const EMPTY_FALLBACK_BUNDLES = [];
48
+ export function LocaleProvider({ resolver, fallbackBundles = EMPTY_FALLBACK_BUNDLES, fallbackLocale = "en", children, }) {
49
+ // Without memoization every re-render of the provider (e.g. because an
50
+ // ancestor component re-renders) builds a new context-value object —
51
+ // every consumer of useTranslation()/useLocale() then sees a new `ctx`
52
+ // reference and, even with useCallback memoization, a new `t`.
53
+ // Consequence: `t` in a useEffect dependency array triggers an infinite
54
+ // loop (see admin-shell Overview screens, prod incident).
55
+ const value = useMemo(() => ({ resolver, fallbackBundles, fallbackLocale }), [resolver, fallbackBundles, fallbackLocale]);
56
+ return _jsx(LocaleContext.Provider, { value: value, children: children });
57
+ }
58
+ /** Liefert den aktuellen LocaleResolver und abonniert automatisch
59
+ * Locale-Änderungen — der aufrufende Component re-rendert sobald die
60
+ * Sprache gewechselt wird. Wirft wenn kein Provider im Baum ist. */
61
+ export function useLocale() {
62
+ const ctx = useContext(LocaleContext);
63
+ if (ctx === undefined) {
64
+ throw new Error("useLocale must be used inside <LocaleProvider>");
65
+ }
66
+ // Subscribe + current locale-snapshot. Wir brauchen den Rückgabewert
67
+ // selbst nicht — wichtig ist nur der re-render-trigger.
68
+ useSyncExternalStore(ctx.resolver.subscribe, () => ctx.resolver.locale(), () => ctx.resolver.locale());
69
+ return ctx.resolver;
70
+ }
71
+ /** `t("key", params)` resolves in this order:
72
+ * 1. App resolver (e.g. i18next)
73
+ * 2. Plugin fallback bundles for the current locale (under a "formal"
74
+ * FormalityProvider, `<locale>-x-formal` across all bundles first)
75
+ * 3. Plugin fallback bundles for fallbackLocale
76
+ * 4. The key as-is
77
+ * `{name}` interpolation applies to every source, fallback strings included. */
78
+ export function useTranslation() {
79
+ const ctx = useContext(LocaleContext);
80
+ if (ctx === undefined) {
81
+ throw new Error("useTranslation must be used inside <LocaleProvider>");
82
+ }
83
+ // Re-Render bei Sprach-Wechsel. `ctx.resolver.subscribe` ist bereits
84
+ // eine stable-reference aus dem Resolver, daher hier keine eigene
85
+ // Memoization der Subscribe-Callback nötig.
86
+ const locale = useSyncExternalStore(ctx.resolver.subscribe, () => ctx.resolver.locale(), () => ctx.resolver.locale());
87
+ // `t` MUSS referenz-stabil sein solange sich Resolver/Bundles/Locale
88
+ // nicht ändern — Consumer nutzen `t` regelmäßig in useEffect-Deps
89
+ // (z.B. um Queries neu zu laden wenn sich die Sprache ändert). Ein neu
90
+ // erzeugtes `t` pro Render führt sonst zu einem Render/Effect-Endlos-
91
+ // Loop (siehe admin-shell Overview-Screens, Prod-Incident 2026-07-07).
92
+ const formality = useContext(FormalityContext);
93
+ return useCallback((key, params) => translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
94
+ }
95
+ /** Wording for the subtree: `formal` makes `t` prefer the `<locale>-x-formal`
96
+ * bundle entries (e.g. "Sie" on public pages) over plain `<locale>`. */
97
+ export function FormalityProvider({ formality, children, }) {
98
+ return _jsx(FormalityContext.Provider, { value: formality, children: children });
99
+ }
100
+ export function useFormality() {
101
+ return useContext(FormalityContext);
102
+ }
103
+ // Lookup order: (1) app resolver — it returns the key itself when it has no
104
+ // entry, which is the invitation to try the plugin bundles; (2) the bundles in
105
+ // array order per locale tier; (3) the key as-is. Under "formal" the formal
106
+ // tier is searched across ALL bundles before any plain tier, so a plain "de"
107
+ // app override never puts "du" on a formal page when locale-de has a formal
108
+ // variant; an app rewording such a key must also ship its "de-x-formal" text.
109
+ function translateWithFallbacks(ctx, locale, formality, key, params) {
110
+ const resolved = ctx.resolver.translate(key, params);
111
+ if (resolved !== key)
112
+ return resolved;
113
+ for (const localesToTry of bundleLocaleTiers(locale, ctx.fallbackLocale, formality)) {
114
+ for (const bundle of ctx.fallbackBundles) {
115
+ for (const localeToTry of localesToTry) {
116
+ const value = bundle[localeToTry]?.[key];
117
+ if (value !== undefined)
118
+ return resolveTranslationValue(value, localeToTry, params);
119
+ }
120
+ }
121
+ }
122
+ return key;
123
+ }
124
+ function bundleLocaleTiers(locale, fallbackLocale, formality) {
125
+ const languageRoot = locale.split("-")[0] ?? locale;
126
+ const plainTier = [locale, languageRoot, fallbackLocale];
127
+ if (formality === "informal")
128
+ return [plainTier];
129
+ return [[formalLocaleTag(locale), formalLocaleTag(languageRoot)], plainTier];
130
+ }
131
+ /** Non-throwing LocaleContext read — undefined outside LocaleProvider.
132
+ * Used by DataTable FormatCell so plain tables without a provider do not crash. */
133
+ export function useOptionalLocale() {
134
+ const ctx = useContext(LocaleContext);
135
+ useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), () => (ctx ? ctx.resolver.locale() : "en"), () => (ctx ? ctx.resolver.locale() : "en"));
136
+ return ctx === undefined ? undefined : ctx.resolver.locale();
137
+ }
138
+ /** Non-throwing time-zone read — "UTC" outside LocaleProvider, matching
139
+ * createStaticLocaleResolver's own default. */
140
+ export function useOptionalTimeZone() {
141
+ const ctx = useContext(LocaleContext);
142
+ return useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), () => (ctx ? ctx.resolver.timeZone() : "UTC"), () => (ctx ? ctx.resolver.timeZone() : "UTC"));
143
+ }
144
+ /** Non-throwing translate — undefined outside LocaleProvider. */
145
+ export function useOptionalTranslation() {
146
+ const ctx = useContext(LocaleContext);
147
+ const locale = useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), () => (ctx ? ctx.resolver.locale() : "en"), () => (ctx ? ctx.resolver.locale() : "en"));
148
+ const formality = useContext(FormalityContext);
149
+ const t = useCallback((key, params) => ctx === undefined ? key : translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
150
+ return ctx === undefined ? undefined : t;
151
+ }
152
+ /** Default-Resolver für Apps ohne eigene i18n-Schicht. Gibt jeden Key
153
+ * unverändert zurück — die Plugin-Fallback-Bundles erledigen dann die
154
+ * echte Übersetzung. Nützlich auch als Basis für Tests. */
155
+ export function createStaticLocaleResolver(options = {}) {
156
+ const locale = options.locale ?? "en";
157
+ const timeZone = options.timeZone ?? "UTC";
158
+ return {
159
+ translate: (key) => key,
160
+ locale: () => locale,
161
+ timeZone: () => timeZone,
162
+ // No-op subscribe: unsere Locale ist statisch, es gibt nie ein
163
+ // Change-Event. Unsubscribe ist ebenfalls no-op.
164
+ subscribe: () => () => { },
165
+ };
166
+ }