@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
@@ -1,134 +0,0 @@
1
- import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { useDispatcher } from "../context/dispatcher-context";
4
-
5
- // React wrapper around dispatcher.stream (#1382). Accumulates yielded
6
- // chunks into `chunks` (unlike useQuery({live:true}), which only
7
- // invalidates and re-fetches). Each start() owns an AbortController;
8
- // a newer start() / unmount aborts the previous run so late chunks
9
- // cannot clobber fresher state.
10
-
11
- export type StreamStatus = "idle" | "streaming" | "done" | "error";
12
-
13
- export type UseStreamHandlerResult<TChunk> = {
14
- readonly chunks: readonly TChunk[];
15
- readonly status: StreamStatus;
16
- readonly error: DispatcherError | null;
17
- readonly start: (payload?: unknown) => Promise<void>;
18
- readonly abort: () => void;
19
- readonly reset: () => void;
20
- };
21
-
22
- export type UseStreamHandlerOptions = {
23
- // When true, start() runs once on mount with the initial payload.
24
- // Default false — streams are usually user-triggered (unlike queries).
25
- readonly autoStart?: boolean;
26
- };
27
-
28
- export function useStreamHandler<TChunk = unknown>(
29
- type: string,
30
- payload: unknown = {},
31
- options: UseStreamHandlerOptions = {},
32
- ): UseStreamHandlerResult<TChunk> {
33
- const dispatcher = useDispatcher();
34
- const { autoStart = false } = options;
35
-
36
- const [chunks, setChunks] = useState<readonly TChunk[]>([]);
37
- const [status, setStatus] = useState<StreamStatus>("idle");
38
- const [error, setError] = useState<DispatcherError | null>(null);
39
-
40
- const activeCtrl = useRef<AbortController | null>(null);
41
- const payloadRef = useRef(payload);
42
- payloadRef.current = payload;
43
-
44
- const abort = useCallback((): void => {
45
- activeCtrl.current?.abort();
46
- activeCtrl.current = null;
47
- // User cancel — exit streaming so UI can re-enable the start button.
48
- setStatus((s) => (s === "streaming" ? "idle" : s));
49
- }, []);
50
-
51
- const reset = useCallback((): void => {
52
- abort();
53
- setChunks([]);
54
- setStatus("idle");
55
- setError(null);
56
- }, [abort]);
57
-
58
- // Shared by every abort-detection branch below: only touch status if this
59
- // run is still the active one (or activeCtrl was already cleared by abort()).
60
- const settleAborted = useCallback((ctrl: AbortController): void => {
61
- if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
62
- }, []);
63
-
64
- const start = useCallback(
65
- async (overridePayload?: unknown): Promise<void> => {
66
- activeCtrl.current?.abort();
67
- const ctrl = new AbortController();
68
- activeCtrl.current = ctrl;
69
-
70
- setChunks([]);
71
- setError(null);
72
- setStatus("streaming");
73
-
74
- const body = overridePayload !== undefined ? overridePayload : payloadRef.current;
75
- try {
76
- for await (const chunk of dispatcher.stream<TChunk>(type, body, { signal: ctrl.signal })) {
77
- // skip: a newer start() already superseded this run
78
- if (ctrl.signal.aborted) {
79
- settleAborted(ctrl);
80
- return;
81
- }
82
- setChunks((prev) => [...prev, chunk]);
83
- }
84
- // skip: aborted after last chunk, don't mark done
85
- if (ctrl.signal.aborted) {
86
- settleAborted(ctrl);
87
- return;
88
- }
89
- setStatus("done");
90
- } catch (e) {
91
- // skip: abort is not an error toast
92
- if (ctrl.signal.aborted) {
93
- settleAborted(ctrl);
94
- return;
95
- }
96
- const mapped = asDispatcherError(e);
97
- // skip: dispatcher-mapped abort (fetch cancelled)
98
- if (mapped.code === "aborted") {
99
- settleAborted(ctrl);
100
- return;
101
- }
102
- setError(mapped);
103
- setStatus("error");
104
- }
105
- },
106
- [dispatcher, type, settleAborted],
107
- );
108
-
109
- useEffect(() => {
110
- // skip: autoStart off — streams are usually user-triggered
111
- if (!autoStart) return;
112
- void start();
113
- }, [autoStart, start]);
114
-
115
- useEffect(() => {
116
- return () => {
117
- activeCtrl.current?.abort();
118
- };
119
- }, []);
120
-
121
- return { chunks, status, error, start, abort, reset };
122
- }
123
-
124
- function asDispatcherError(e: unknown): DispatcherError {
125
- if (e && typeof e === "object" && "code" in e && "message" in e && "i18nKey" in e) {
126
- return e as DispatcherError;
127
- }
128
- return {
129
- code: "stream_error",
130
- httpStatus: 0,
131
- i18nKey: "errors.unknown",
132
- message: e instanceof Error ? e.message : String(e),
133
- };
134
- }
@@ -1,240 +0,0 @@
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
-
11
- import type { TranslationsByLocale } from "./i18n";
12
-
13
- export const kumikoDefaultTranslations: TranslationsByLocale = {
14
- en: {
15
- "kumiko.actions.save": "Save",
16
- "kumiko.actions.cancel": "Cancel",
17
- "kumiko.actions.delete": "Delete",
18
- "kumiko.actions.delete-confirm": "Confirm delete?",
19
- "kumiko.actions.reload": "Reload",
20
- "kumiko.actions.create": "New",
21
- "kumiko.actions.edit": "Edit",
22
- "kumiko.actions.view": "View",
23
- "kumiko.actions.copyLink": "Copy link",
24
- "kumiko.actions.copyLinkCopied": "Copied!",
25
- "kumiko.actions.next": "Next",
26
- "kumiko.actions.back": "Back",
27
- "kumiko.actions.finish": "Finish",
28
-
29
- "kumiko.wizard.step": "Step {current} of {total}",
30
- "kumiko.wizard.step-with-title": "Step {current} of {total} · {title}",
31
-
32
- "kumiko.version.update-available": "A new version is available.",
33
-
34
- "kumiko.toast.learn-more": "Learn more",
35
-
36
- "kumiko.field.open-calendar": "Open calendar",
37
- "kumiko.field.dateField.placeholderYear": "Y",
38
- "kumiko.field.dateField.placeholderMonth": "M",
39
- "kumiko.field.dateField.placeholderDay": "D",
40
- "kumiko.field.time": "Time",
41
- "kumiko.field.timezone": "Time zone",
42
- "kumiko.field.locatedTzHint": "Time local to the given location",
43
- "kumiko.field.reference-created-no-id":
44
- "Record was created but could not be selected automatically. Please select it manually.",
45
- "kumiko.field.unsupported": "This field type can't be edited here yet.",
46
- "kumiko.field.embedded-list.add-row": "Add row",
47
- "kumiko.field.embedded-list.remove-row": "Remove row",
48
- "kumiko.field.embedded-list.duplicate-row": "Duplicate row",
49
- "kumiko.field.embedded-list.move-up": "Move up",
50
- "kumiko.field.embedded-list.move-down": "Move down",
51
- "kumiko.field.embedded-list.empty": "No rows yet.",
52
- "kumiko.field.embedded-list.empty-cta": "Add first row",
53
- "kumiko.field.embedded-list.paste-rows-truncated":
54
- "{count} pasted row(s) were dropped (max row count reached).",
55
- "kumiko.field.embedded-list.paste-cells-unmatched":
56
- "{count} cell(s) had no matching option and were left unchanged.",
57
- "kumiko.field.multiSelect.select-all": "Select all",
58
- "kumiko.field.multiSelect.deselect-all": "Deselect all",
59
-
60
- "kumiko.list.search-placeholder": "Search…",
61
- "kumiko.list.empty.title": "No entries yet.",
62
- "kumiko.list.empty.hint": "Create the first one to get started.",
63
- "kumiko.list.no-entries": "No entries.",
64
- "kumiko.list.end-of-list": "— End of list —",
65
- "kumiko.list.sort.label": "Sort",
66
- "kumiko.list.sort.unsorted": "Unsorted",
67
- "kumiko.list.filter.reset": "Reset",
68
- "kumiko.list.filter.toggle": "Filters",
69
- "kumiko.list.related-list-truncated":
70
- "Showing the first {count} entries. More are available but not loaded — this list does not paginate.",
71
- "kumiko.reference.system-tenant": "System",
72
- "kumiko.reference.system-user": "System",
73
-
74
- "kumiko.pager.status": "{from}–{to} of {total}",
75
- "kumiko.pager.pageOf": "Page {page} of {pages}",
76
- "kumiko.form.all-required": "All fields are required.",
77
- "kumiko.pager.status.noun": "{from}–{to} of {total} {noun}",
78
- "kumiko.pager.pageSize": "{size} per page",
79
- "kumiko.pager.pageSizeLabel": "Entries per page",
80
- "kumiko.list.count.noun": "{count} {noun}",
81
- "kumiko.list.count.one": "1 entry",
82
- "kumiko.list.count.other": "{count} entries",
83
- "kumiko.list.row-actions.more": "More actions",
84
- "kumiko.list.sort.by": "Sorted by {column}",
85
- "kumiko.form.changed": "changed",
86
- "kumiko.form.on-this-page": "On this page",
87
- "kumiko.form.unsaved.one": "1 unsaved change",
88
- "kumiko.form.unsaved.other": "{count} unsaved changes",
89
- "kumiko.wizard.next-with-title": "Next: {title}",
90
- "kumiko.wizard.save-and-close": "Save and close",
91
- "kumiko.pager.previousPage": "Previous page",
92
- "kumiko.pager.nextPage": "Next page",
93
-
94
- "kumiko.combobox.search-placeholder": "Search…",
95
- "kumiko.combobox.empty": "No matches.",
96
- "kumiko.combobox.loading": "Loading…",
97
- "kumiko.combobox.placeholder": "—",
98
-
99
- "kumiko.dashboard.filter.all": "All",
100
-
101
- "kumiko.app-boot.loading": "Loading…",
102
- "kumiko.app-boot.unauthorized": "You need to sign in to see this.",
103
- "kumiko.app-boot.failed": "Couldn't load the app. Check your connection and try again.",
104
- "kumiko.app-boot.no-open-screen":
105
- "This app has no screen that's accessible without a role restriction.",
106
-
107
- "kumiko.widget.loading": "Loading…",
108
- "kumiko.widget.error.title": "Couldn't load.",
109
-
110
- "kumiko.widget.upload.uploading": "Uploading…",
111
- "kumiko.widget.upload.done": "Uploaded",
112
- "kumiko.widget.upload.error": "Failed",
113
- "kumiko.widget.upload.rejected-type": "File type not allowed",
114
-
115
- "kumiko.widget.step-bar.done": "Done",
116
-
117
- "kumiko.widget.drawer.restore": "Restore drawer width",
118
- "kumiko.widget.drawer.maximize": "Maximize drawer width",
119
- "kumiko.widget.drawer.resize": "Resize drawer",
120
-
121
- "kumiko.widget.floatingPanel.move": "Move panel",
122
- "kumiko.widget.floatingPanel.resizeTop": "Resize panel from the top edge",
123
- "kumiko.widget.floatingPanel.resizeRight": "Resize panel from the right edge",
124
- "kumiko.widget.floatingPanel.resizeBottom": "Resize panel from the bottom edge",
125
- "kumiko.widget.floatingPanel.resizeLeft": "Resize panel from the left edge",
126
-
127
- "kumiko.planCard.current": "Current plan",
128
- "kumiko.planCard.priceUnavailable": "Price not available",
129
-
130
- "kumiko.nav.expand": "Expand",
131
- "kumiko.nav.collapse": "Collapse",
132
- "kumiko.nav.search": "Search navigation…",
133
- "kumiko.nav.language": "Language",
134
-
135
- "kumiko.workspace.switch": "Switch workspace",
136
- // Workspace-Switcher fallback label when activeId points at no visible workspace.
137
- "kumiko.workspace.select": "Select workspace",
138
-
139
- "kumiko.dialog.confirm": "Confirm",
140
- "kumiko.dialog.cancel": "Cancel",
141
- "kumiko.dialog.close": "Close",
142
- "kumiko.drawer.discard.title": "Discard changes?",
143
- "kumiko.drawer.discard.body": "Your input in this form will be lost.",
144
- "kumiko.drawer.discard.confirm": "Discard",
145
- "kumiko.drawer.discard.cancel": "Keep editing",
146
-
147
- "kumiko.aiText.acceptHint": "Tab = accept, Esc = discard",
148
- "kumiko.aiText.correct": "Correct",
149
- "kumiko.aiText.translate": "Translate",
150
- "kumiko.aiText.rewrite": "Rewrite",
151
- "kumiko.aiText.diff.before": "Before",
152
- "kumiko.aiText.diff.after": "After",
153
- "kumiko.aiText.diff.generating": "Generating…",
154
- "kumiko.aiText.style.formal": "Formal",
155
- "kumiko.aiText.style.casual": "Casual",
156
- "kumiko.aiText.style.concise": "Concise",
157
- "kumiko.aiText.style.expand": "Expand",
158
- "kumiko.aiText.capExceeded": "Monthly AI limit reached.",
159
-
160
- "kumiko.rowAction.failed": "Action failed",
161
-
162
- "kumiko.contentEditor.insertVariable": "Insert {name}",
163
- "kumiko.contentEditor.preview": "Preview",
164
- "kumiko.contentEditor.editMode": "Edit",
165
- "kumiko.contentEditor.bold": "Bold",
166
- "kumiko.contentEditor.italic": "Italic",
167
- "kumiko.contentEditor.heading1": "Heading 1",
168
- "kumiko.contentEditor.heading2": "Heading 2",
169
- "kumiko.contentEditor.bulletList": "Bullet list",
170
- "kumiko.contentEditor.orderedList": "Numbered list",
171
-
172
- "kumiko.config.source.user": "My value",
173
- "kumiko.config.source.tenant": "Tenant",
174
- "kumiko.config.source.system": "System",
175
- "kumiko.config.source.appOverride": "App override",
176
- "kumiko.config.source.computed": "Computed",
177
- "kumiko.config.source.default": "Default",
178
- "kumiko.config.source.missing": "Missing",
179
- "kumiko.config.cascade.noValue": "No value set",
180
- "kumiko.config.cascade.activeMarker": "active",
181
- "kumiko.config.cascade.resetTo": "Reset override ({scope})",
182
-
183
- "kumiko.form.error.generic": "Something went wrong.",
184
- "kumiko.form.error.version-conflict":
185
- "Record was modified in the meantime. Reload and try again.",
186
- "kumiko.form.extension.save-failed": "A custom field could not be saved.",
187
- "kumiko.form.draft.resume-multiple":
188
- "Found multiple open drafts for this form. Which one do you want to resume?",
189
- "kumiko.form.draft.resume-single":
190
- "Found an open draft for this form. Do you want to resume it?",
191
- "kumiko.form.draft.start-new": "Start new",
192
-
193
- "kumiko.secretMint.title": "Your new secret",
194
- "kumiko.secretMint.warning":
195
- "Copy this now — it is shown only this once and cannot be retrieved later.",
196
- "kumiko.secretMint.confirm": "I have copied it",
197
- "kumiko.secretMint.copy": "Copy",
198
- "kumiko.secretMint.copied": "Copied",
199
- "kumiko.secretMint.done": "Done.",
200
-
201
- "kumiko.validation.required": "Required.",
202
- "kumiko.validation.invalid": "Invalid value.",
203
- "kumiko.validation.too-short": "Too short (at least {min} characters).",
204
- "kumiko.validation.too-long": "Too long (at most {max} characters).",
205
- "kumiko.validation.out-of-range": "Value out of allowed range.",
206
-
207
- "errors.validation.invalid_type": "Invalid value.",
208
- "errors.validation.too_small": "Too small or too short (minimum: {minimum}).",
209
- "errors.validation.too_big": "Too big or too long (maximum: {maximum}).",
210
- "errors.validation.invalid_format": "Invalid format.",
211
- "errors.validation.not_multiple_of": "Must be a multiple of {divisor}.",
212
- "errors.validation.unrecognized_keys": "Unknown fields.",
213
- "errors.validation.invalid_union": "Invalid value.",
214
- "errors.validation.invalid_key": "Invalid key.",
215
- "errors.validation.invalid_element": "Invalid entry.",
216
- "errors.validation.invalid_value": "Invalid choice.",
217
- "errors.validation.custom": "Invalid value.",
218
- "errors.validation.unexpected_field": "Unknown field.",
219
- "errors.validation.out_of_bounds": "Value out of allowed range.",
220
- "errors.validation.invalid_option": "Invalid choice.",
221
- "errors.validation.failed": "Validation failed.",
222
-
223
- "errors.feature.disabled": "This feature is currently unavailable.",
224
- "errors.access.denied": "You don't have permission to do this.",
225
- "errors.notFound": "Not found.",
226
- "errors.conflict": "Conflict — the operation could not be completed.",
227
- "errors.versionConflict": "The record was modified in the meantime. Reload and try again.",
228
- "errors.preconditionFailed": "The record changed in the meantime. Reload and try again.",
229
- "errors.uniqueViolation": "This entry already exists.",
230
- "errors.unprocessable": "The request could not be processed.",
231
- "errors.unconfigured": "This feature isn't configured yet.",
232
- "errors.internal": "Something went wrong. Please try again later.",
233
- "errors.rate_limited": "Too many requests. Please try again shortly.",
234
- "errors.cap.exceeded": "Limit reached. Upgrade your plan or wait for the next period.",
235
- "errors.download.urlMissing": "Download unavailable — please try again.",
236
- "auth.errors.originNotAllowed": "Requests from this origin are not allowed.",
237
- "dispatcher.errors.network": "Network error. Please check your connection and try again.",
238
- "dispatcher.errors.aborted": "Request was cancelled.",
239
- },
240
- };
package/src/i18n.tsx DELETED
@@ -1,286 +0,0 @@
1
- // Locale handling for React consumers of the Kumiko renderer. A thin layer
2
- // around the platform-agnostic `LocaleResolver` contract from
3
- // @cosmicdrift/kumiko-headless: provider, hooks, a default no-op resolver,
4
- // and a fallback-bundle merge for feature-supplied translations.
5
- //
6
- // Architecture:
7
- // 1. The app supplies exactly one `LocaleResolver` via `<LocaleProvider>`
8
- // (or none at all → the default resolver returns keys as-is).
9
- // 2. Feature plugins may bring fallback bundles: when the app resolver
10
- // can't resolve a key, `useTranslation` tries the plugin bundles.
11
- // This keeps feature UI independent of the app's own i18next instance
12
- // and works out of the box, while staying fully overridable.
13
- // 3. Re-render on locale change via `useSyncExternalStore` on the
14
- // resolver's `subscribe()` — app code can switch language mid-session
15
- // without a reload.
16
-
17
- import {
18
- type Formality,
19
- formalLocaleTag,
20
- resolveTranslationValue,
21
- type TranslationValue,
22
- } from "@cosmicdrift/kumiko-framework/ui-types";
23
- import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
24
- import {
25
- createContext,
26
- type ReactNode,
27
- useCallback,
28
- useContext,
29
- useMemo,
30
- useSyncExternalStore,
31
- } from "react";
32
-
33
- /** Map of i18n key → TranslationValue. Strings may contain `{name}`
34
- * placeholders (same semantics as i18next t); plural entries resolve via
35
- * `resolveTranslationValue`. */
36
- export type TranslationBundle = Readonly<Record<string, TranslationValue>>;
37
-
38
- /** Map von Locale-Code (BCP-47, z.B. `"de"`, `"en-US"`) → Bundle. */
39
- export type TranslationsByLocale = Readonly<Record<string, TranslationBundle>>;
40
-
41
- /** Key-first shape for `r.translations({ keys })` — each key maps locale → TranslationValue. */
42
- export type TranslationsByKey = Readonly<
43
- Record<string, Readonly<Record<string, TranslationValue>>>
44
- >;
45
-
46
- /** Pivot key-first server translations to locale-first client bundles. */
47
- export function translationsByLocaleFromKeys(source: TranslationsByKey): TranslationsByLocale {
48
- const out: Record<string, Record<string, TranslationValue>> = {};
49
- for (const [key, byLocale] of Object.entries(source)) {
50
- for (const [locale, value] of Object.entries(byLocale)) {
51
- out[locale] ??= {};
52
- out[locale][key] = value;
53
- }
54
- }
55
- return out;
56
- }
57
-
58
- /** Merged zwei TranslationsByLocale-Maps — der override gewinnt pro Key,
59
- * die Locales werden zusammengeführt. Standard-Baustein für Client-
60
- * Plugins, die App-Overrides über ihre Default-Bundles legen. */
61
- export function mergeTranslations(
62
- base: TranslationsByLocale,
63
- override: TranslationsByLocale,
64
- ): TranslationsByLocale {
65
- const locales = new Set([...Object.keys(base), ...Object.keys(override)]);
66
- const merged: Record<string, Record<string, TranslationValue>> = {};
67
- for (const locale of locales) {
68
- merged[locale] = { ...(base[locale] ?? {}), ...(override[locale] ?? {}) };
69
- }
70
- return merged;
71
- }
72
-
73
- type LocaleContextValue = {
74
- readonly resolver: LocaleResolver;
75
- readonly fallbackBundles: readonly TranslationsByLocale[];
76
- readonly fallbackLocale: string;
77
- };
78
-
79
- const LocaleContext = createContext<LocaleContextValue | undefined>(undefined);
80
- const FormalityContext = createContext<Formality>("informal");
81
-
82
- // Stabile Referenz statt `fallbackBundles = []` als Default-Parameter:
83
- // ein Literal-Default wird bei JEDEM Aufruf neu allokiert und würde die
84
- // useMemo-Referenzprüfung im Provider unten aushebeln, sobald der
85
- // Aufrufer fallbackBundles weglässt.
86
- const EMPTY_FALLBACK_BUNDLES: readonly TranslationsByLocale[] = [];
87
-
88
- export type LocaleProviderProps = {
89
- readonly resolver: LocaleResolver;
90
- /** Default bundles supplied by feature plugins. Lookup order per key:
91
- * (1) app resolver, (2) these bundles in array order, (3) key as-is.
92
- * Apps can thus override individual keys without swapping out whole
93
- * feature bundles. */
94
- readonly fallbackBundles?: readonly TranslationsByLocale[];
95
- /** Falls back to fallbackLocale when neither the current-locale nor the
96
- * key lookup hits in a plugin bundle. Default: `"en"`. */
97
- readonly fallbackLocale?: string;
98
- readonly children: ReactNode;
99
- };
100
-
101
- export function LocaleProvider({
102
- resolver,
103
- fallbackBundles = EMPTY_FALLBACK_BUNDLES,
104
- fallbackLocale = "en",
105
- children,
106
- }: LocaleProviderProps): ReactNode {
107
- // Without memoization every re-render of the provider (e.g. because an
108
- // ancestor component re-renders) builds a new context-value object —
109
- // every consumer of useTranslation()/useLocale() then sees a new `ctx`
110
- // reference and, even with useCallback memoization, a new `t`.
111
- // Consequence: `t` in a useEffect dependency array triggers an infinite
112
- // loop (see admin-shell Overview screens, prod incident).
113
- const value = useMemo(
114
- () => ({ resolver, fallbackBundles, fallbackLocale }),
115
- [resolver, fallbackBundles, fallbackLocale],
116
- );
117
- return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;
118
- }
119
-
120
- /** Liefert den aktuellen LocaleResolver und abonniert automatisch
121
- * Locale-Änderungen — der aufrufende Component re-rendert sobald die
122
- * Sprache gewechselt wird. Wirft wenn kein Provider im Baum ist. */
123
- export function useLocale(): LocaleResolver {
124
- const ctx = useContext(LocaleContext);
125
- if (ctx === undefined) {
126
- throw new Error("useLocale must be used inside <LocaleProvider>");
127
- }
128
- // Subscribe + current locale-snapshot. Wir brauchen den Rückgabewert
129
- // selbst nicht — wichtig ist nur der re-render-trigger.
130
- useSyncExternalStore(
131
- ctx.resolver.subscribe,
132
- () => ctx.resolver.locale(),
133
- () => ctx.resolver.locale(),
134
- );
135
- return ctx.resolver;
136
- }
137
-
138
- /** `t("key", params)` resolves in this order:
139
- * 1. App resolver (e.g. i18next)
140
- * 2. Plugin fallback bundles for the current locale (under a "formal"
141
- * FormalityProvider, `<locale>-x-formal` across all bundles first)
142
- * 3. Plugin fallback bundles for fallbackLocale
143
- * 4. The key as-is
144
- * `{name}` interpolation applies to every source, fallback strings included. */
145
- export function useTranslation(): (
146
- key: string,
147
- params?: Readonly<Record<string, unknown>>,
148
- ) => string {
149
- const ctx = useContext(LocaleContext);
150
- if (ctx === undefined) {
151
- throw new Error("useTranslation must be used inside <LocaleProvider>");
152
- }
153
- // Re-Render bei Sprach-Wechsel. `ctx.resolver.subscribe` ist bereits
154
- // eine stable-reference aus dem Resolver, daher hier keine eigene
155
- // Memoization der Subscribe-Callback nötig.
156
- const locale = useSyncExternalStore(
157
- ctx.resolver.subscribe,
158
- () => ctx.resolver.locale(),
159
- () => ctx.resolver.locale(),
160
- );
161
-
162
- // `t` MUSS referenz-stabil sein solange sich Resolver/Bundles/Locale
163
- // nicht ändern — Consumer nutzen `t` regelmäßig in useEffect-Deps
164
- // (z.B. um Queries neu zu laden wenn sich die Sprache ändert). Ein neu
165
- // erzeugtes `t` pro Render führt sonst zu einem Render/Effect-Endlos-
166
- // Loop (siehe admin-shell Overview-Screens, Prod-Incident 2026-07-07).
167
- const formality = useContext(FormalityContext);
168
- return useCallback(
169
- (key: string, params?: Readonly<Record<string, unknown>>): string =>
170
- translateWithFallbacks(ctx, locale, formality, key, params),
171
- [ctx, locale, formality],
172
- );
173
- }
174
-
175
- /** Wording for the subtree: `formal` makes `t` prefer the `<locale>-x-formal`
176
- * bundle entries (e.g. "Sie" on public pages) over plain `<locale>`. */
177
- export function FormalityProvider({
178
- formality,
179
- children,
180
- }: {
181
- readonly formality: Formality;
182
- readonly children: ReactNode;
183
- }): ReactNode {
184
- return <FormalityContext.Provider value={formality}>{children}</FormalityContext.Provider>;
185
- }
186
-
187
- export function useFormality(): Formality {
188
- return useContext(FormalityContext);
189
- }
190
-
191
- // Lookup order: (1) app resolver — it returns the key itself when it has no
192
- // entry, which is the invitation to try the plugin bundles; (2) the bundles in
193
- // array order per locale tier; (3) the key as-is. Under "formal" the formal
194
- // tier is searched across ALL bundles before any plain tier, so a plain "de"
195
- // app override never puts "du" on a formal page when locale-de has a formal
196
- // variant; an app rewording such a key must also ship its "de-x-formal" text.
197
- function translateWithFallbacks(
198
- ctx: LocaleContextValue,
199
- locale: string,
200
- formality: Formality,
201
- key: string,
202
- params?: Readonly<Record<string, unknown>>,
203
- ): string {
204
- const resolved = ctx.resolver.translate(key, params);
205
- if (resolved !== key) return resolved;
206
- for (const localesToTry of bundleLocaleTiers(locale, ctx.fallbackLocale, formality)) {
207
- for (const bundle of ctx.fallbackBundles) {
208
- for (const localeToTry of localesToTry) {
209
- const value = bundle[localeToTry]?.[key];
210
- if (value !== undefined) return resolveTranslationValue(value, localeToTry, params);
211
- }
212
- }
213
- }
214
- return key;
215
- }
216
-
217
- function bundleLocaleTiers(
218
- locale: string,
219
- fallbackLocale: string,
220
- formality: Formality,
221
- ): readonly (readonly string[])[] {
222
- const languageRoot = locale.split("-")[0] ?? locale;
223
- const plainTier = [locale, languageRoot, fallbackLocale];
224
- if (formality === "informal") return [plainTier];
225
- return [[formalLocaleTag(locale), formalLocaleTag(languageRoot)], plainTier];
226
- }
227
-
228
- /** Non-throwing LocaleContext read — undefined outside LocaleProvider.
229
- * Used by DataTable FormatCell so plain tables without a provider do not crash. */
230
- export function useOptionalLocale(): string | undefined {
231
- const ctx = useContext(LocaleContext);
232
- useSyncExternalStore(
233
- (onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => {}),
234
- () => (ctx ? ctx.resolver.locale() : "en"),
235
- () => (ctx ? ctx.resolver.locale() : "en"),
236
- );
237
- return ctx === undefined ? undefined : ctx.resolver.locale();
238
- }
239
-
240
- /** Non-throwing time-zone read — "UTC" outside LocaleProvider, matching
241
- * createStaticLocaleResolver's own default. */
242
- export function useOptionalTimeZone(): string {
243
- const ctx = useContext(LocaleContext);
244
- return useSyncExternalStore(
245
- (onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => {}),
246
- () => (ctx ? ctx.resolver.timeZone() : "UTC"),
247
- () => (ctx ? ctx.resolver.timeZone() : "UTC"),
248
- );
249
- }
250
-
251
- /** Non-throwing translate — undefined outside LocaleProvider. */
252
- export function useOptionalTranslation():
253
- | ((key: string, params?: Readonly<Record<string, unknown>>) => string)
254
- | undefined {
255
- const ctx = useContext(LocaleContext);
256
- const locale = useSyncExternalStore(
257
- (onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => {}),
258
- () => (ctx ? ctx.resolver.locale() : "en"),
259
- () => (ctx ? ctx.resolver.locale() : "en"),
260
- );
261
- const formality = useContext(FormalityContext);
262
- const t = useCallback(
263
- (key: string, params?: Readonly<Record<string, unknown>>): string =>
264
- ctx === undefined ? key : translateWithFallbacks(ctx, locale, formality, key, params),
265
- [ctx, locale, formality],
266
- );
267
- return ctx === undefined ? undefined : t;
268
- }
269
-
270
- /** Default-Resolver für Apps ohne eigene i18n-Schicht. Gibt jeden Key
271
- * unverändert zurück — die Plugin-Fallback-Bundles erledigen dann die
272
- * echte Übersetzung. Nützlich auch als Basis für Tests. */
273
- export function createStaticLocaleResolver(
274
- options: { readonly locale?: string; readonly timeZone?: string } = {},
275
- ): LocaleResolver {
276
- const locale = options.locale ?? "en";
277
- const timeZone = options.timeZone ?? "UTC";
278
- return {
279
- translate: (key: string) => key,
280
- locale: () => locale,
281
- timeZone: () => timeZone,
282
- // No-op subscribe: unsere Locale ist statisch, es gibt nie ein
283
- // Change-Event. Unsubscribe ist ebenfalls no-op.
284
- subscribe: () => () => {},
285
- };
286
- }