@cosmicdrift/kumiko-renderer 0.327.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 (257) 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 -44
  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 -38
  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/dist/inside-drawer.js +9 -0
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/dist/page-header-slot.js +9 -0
  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/changes.json +52 -0
  135. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  136. package/src/__tests__/format-when.test.ts +0 -12
  137. package/src/__tests__/i18n.test.tsx +0 -283
  138. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  139. package/src/__tests__/qn.test.ts +0 -83
  140. package/src/__tests__/share-links.test.ts +0 -26
  141. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  142. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  143. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  144. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  145. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  146. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  147. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  148. package/src/app/__tests__/content-editors.test.tsx +0 -128
  149. package/src/app/__tests__/content-preview.test.tsx +0 -184
  150. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  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 -111
  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 -3614
  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 -85
  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 -639
  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 -1669
  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 -60
  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 -467
  228. package/src/components/render-edit-action-button.tsx +0 -106
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -216
  231. package/src/components/render-edit.tsx +0 -1700
  232. package/src/components/render-field.tsx +0 -942
  233. package/src/components/render-list.tsx +0 -561
  234. package/src/components/write-form-section.tsx +0 -199
  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-stream-handler.test.tsx +0 -107
  241. package/src/hooks/use-ai-text.ts +0 -172
  242. package/src/hooks/use-disclosure.ts +0 -20
  243. package/src/hooks/use-form.ts +0 -78
  244. package/src/hooks/use-list-url-state.ts +0 -171
  245. package/src/hooks/use-mutation.ts +0 -61
  246. package/src/hooks/use-query.ts +0 -132
  247. package/src/hooks/use-reference-lookup.ts +0 -76
  248. package/src/hooks/use-store.ts +0 -48
  249. package/src/hooks/use-stream-handler.ts +0 -134
  250. package/src/i18n-defaults.ts +0 -220
  251. package/src/i18n.tsx +0 -286
  252. package/src/index.ts +0 -272
  253. package/src/primitives.tsx +0 -1446
  254. package/src/share-links.ts +0 -12
  255. package/src/sort-by-accessor.ts +0 -20
  256. package/src/sse/live-events.tsx +0 -56
  257. package/src/tokens.tsx +0 -170
@@ -1,283 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
3
- import { act, render, renderHook } from "@testing-library/react";
4
- import type { ReactNode } from "react";
5
- import { renderToStaticMarkup } from "react-dom/server";
6
- import {
7
- createStaticLocaleResolver,
8
- FormalityProvider,
9
- LocaleProvider,
10
- type TranslationsByLocale,
11
- translationsByLocaleFromKeys,
12
- useLocale,
13
- useOptionalTranslation,
14
- useTranslation,
15
- } from "../i18n";
16
-
17
- // Stateful resolver fixture: we drive locale changes with setState
18
- // and the test asserts re-render via subscribe.
19
- function makeStatefulResolver(initial: string): LocaleResolver {
20
- let current = initial;
21
- const listeners = new Set<() => void>();
22
- return {
23
- translate: (key: string) => key,
24
- locale: () => current,
25
- timeZone: () => "UTC",
26
- subscribe: (l: () => void) => {
27
- listeners.add(l);
28
- return () => {
29
- listeners.delete(l);
30
- };
31
- },
32
- setLocale: (next: string) => {
33
- current = next;
34
- for (const l of listeners) l();
35
- },
36
- };
37
- }
38
- const wrap =
39
- (resolver: LocaleResolver, fallbackBundles?: TranslationsByLocale[]) =>
40
- ({ children }: { readonly children: ReactNode }): ReactNode => (
41
- <LocaleProvider resolver={resolver} {...(fallbackBundles !== undefined && { fallbackBundles })}>
42
- {children}
43
- </LocaleProvider>
44
- );
45
- describe("useTranslation — lookup order", () => {
46
- test("App-Resolver wins when it returns a non-key value", () => {
47
- const resolver: LocaleResolver = {
48
- ...createStaticLocaleResolver({ locale: "de" }),
49
- translate: (key: string) => (key === "hello" ? "Resolved by app" : key),
50
- };
51
- const { result } = renderHook(() => useTranslation(), { wrapper: wrap(resolver) });
52
- expect(result.current("hello")).toBe("Resolved by app");
53
- });
54
- test("falls back to plugin-bundle for current locale", () => {
55
- const resolver = createStaticLocaleResolver({ locale: "de" });
56
- const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" }, en: { greet: "Hello" } }];
57
- const { result } = renderHook(() => useTranslation(), {
58
- wrapper: wrap(resolver, bundles),
59
- });
60
- expect(result.current("greet")).toBe("Hallo");
61
- });
62
- test("strips region for bundle lookup (de-AT → de)", () => {
63
- const resolver = createStaticLocaleResolver({ locale: "de-AT" });
64
- const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" } }];
65
- const { result } = renderHook(() => useTranslation(), {
66
- wrapper: wrap(resolver, bundles),
67
- });
68
- expect(result.current("greet")).toBe("Hallo");
69
- });
70
- test("falls back to fallbackLocale (en) when current locale missing", () => {
71
- const resolver = createStaticLocaleResolver({ locale: "fr" });
72
- const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" }, en: { greet: "Hello" } }];
73
- const { result } = renderHook(() => useTranslation(), {
74
- wrapper: wrap(resolver, bundles),
75
- });
76
- expect(result.current("greet")).toBe("Hello");
77
- });
78
- test("returns key as-is when nothing resolves", () => {
79
- const resolver = createStaticLocaleResolver({ locale: "de" });
80
- const { result } = renderHook(() => useTranslation(), { wrapper: wrap(resolver) });
81
- expect(result.current("missing.key")).toBe("missing.key");
82
- });
83
- test("interpolates {param}-placeholders from params arg", () => {
84
- const resolver = createStaticLocaleResolver({ locale: "de" });
85
- const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo {name}!" } }];
86
- const { result } = renderHook(() => useTranslation(), {
87
- wrapper: wrap(resolver, bundles),
88
- });
89
- expect(result.current("greet", { name: "Marc" })).toBe("Hallo Marc!");
90
- });
91
- test("resolves a plural entry's CLDR category from a fallback bundle — matches the server's output", () => {
92
- const bundles: TranslationsByLocale[] = [
93
- {
94
- de: {
95
- "notifications.unread": {
96
- one: "{count} ungelesene Nachricht",
97
- other: "{count} ungelesene Nachrichten",
98
- },
99
- },
100
- en: {
101
- "notifications.unread": {
102
- one: "{count} unread message",
103
- other: "{count} unread messages",
104
- },
105
- },
106
- },
107
- ];
108
- const { result: en } = renderHook(() => useTranslation(), {
109
- wrapper: wrap(createStaticLocaleResolver({ locale: "en" }), bundles),
110
- });
111
- expect(en.current("notifications.unread", { count: 1 })).toBe("1 unread message");
112
- expect(en.current("notifications.unread", { count: 5 })).toBe("5 unread messages");
113
-
114
- const { result: de } = renderHook(() => useTranslation(), {
115
- wrapper: wrap(createStaticLocaleResolver({ locale: "de" }), bundles),
116
- });
117
- expect(de.current("notifications.unread", { count: 1 })).toBe("1 ungelesene Nachricht");
118
- });
119
- });
120
- describe("useTranslation — formality", () => {
121
- const bundles: TranslationsByLocale[] = [
122
- { de: { greet: "Hallo, wie geht es dir?", save: "Speichern" } },
123
- { "de-x-formal": { greet: "Guten Tag, wie geht es Ihnen?" } },
124
- ];
125
- const wrapFormal =
126
- (formality: "formal" | "informal", locale = "de") =>
127
- ({ children }: { readonly children: ReactNode }): ReactNode => (
128
- <LocaleProvider resolver={createStaticLocaleResolver({ locale })} fallbackBundles={bundles}>
129
- <FormalityProvider formality={formality}>{children}</FormalityProvider>
130
- </LocaleProvider>
131
- );
132
-
133
- test("without a FormalityProvider the plain locale wording is used", () => {
134
- const { result } = renderHook(() => useTranslation(), {
135
- wrapper: wrap(createStaticLocaleResolver({ locale: "de" }), bundles),
136
- });
137
- expect(result.current("greet")).toBe("Hallo, wie geht es dir?");
138
- });
139
-
140
- test("formal prefers a -x-formal entry from any bundle over a plain one, also for de-AT", () => {
141
- for (const locale of ["de", "de-AT"]) {
142
- const { result } = renderHook(() => useTranslation(), {
143
- wrapper: wrapFormal("formal", locale),
144
- });
145
- expect(result.current("greet")).toBe("Guten Tag, wie geht es Ihnen?");
146
- }
147
- });
148
-
149
- test("formal falls back to the plain wording for keys without a formal variant", () => {
150
- const { result } = renderHook(() => useTranslation(), { wrapper: wrapFormal("formal") });
151
- expect(result.current("save")).toBe("Speichern");
152
- });
153
-
154
- test("useOptionalTranslation honors formality too", () => {
155
- const { result } = renderHook(() => useOptionalTranslation(), {
156
- wrapper: wrapFormal("formal"),
157
- });
158
- expect(result.current?.("greet")).toBe("Guten Tag, wie geht es Ihnen?");
159
- });
160
- });
161
-
162
- describe("useTranslation — re-render on locale change", () => {
163
- test("subscribe fires when setLocale runs, hook returns new value", () => {
164
- const resolver = makeStatefulResolver("de");
165
- const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" }, en: { greet: "Hello" } }];
166
- function Probe(): ReactNode {
167
- const t = useTranslation();
168
- return <span data-testid="msg">{t("greet")}</span>;
169
- }
170
- const { getByTestId } = render(
171
- <LocaleProvider resolver={resolver} fallbackBundles={bundles}>
172
- <Probe />
173
- </LocaleProvider>,
174
- );
175
- expect(getByTestId("msg").textContent).toBe("Hallo");
176
- act(() => {
177
- resolver.setLocale?.("en");
178
- });
179
- expect(getByTestId("msg").textContent).toBe("Hello");
180
- });
181
- });
182
- describe("useTranslation — referential stability", () => {
183
- // Prod-Incident 2026-07-07: admin-shell Overview-Screens hatten `t` in
184
- // einem useEffect-Dependency-Array. Ein neues `t` pro Render triggerte
185
- // einen Render/Effect-Endlos-Loop (~600 Queries/Sekunde). `t` (und der
186
- // gesamte Context-Value) MUSS über Re-Renders hinweg stabil bleiben,
187
- // solange sich Resolver/Bundles/Locale nicht ändern.
188
- test("t keeps the same reference across re-renders when nothing changed", () => {
189
- const resolver = createStaticLocaleResolver({ locale: "de" });
190
- const { result, rerender } = renderHook(() => useTranslation(), {
191
- wrapper: wrap(resolver),
192
- });
193
- const firstT = result.current;
194
- rerender();
195
- expect(result.current).toBe(firstT);
196
- });
197
- test("t stays stable across parent re-renders even with a fresh fallbackBundles literal per parent-render", () => {
198
- // Realistischer Fall: eine App übergibt `fallbackBundles={[...]}` als
199
- // Inline-Literal. Ohne Provider-seitige Memoization würde jeder
200
- // Ahnen-Re-Render den Context-Value neu bauen. Hier prüfen wir nur
201
- // den Provider-internen Memoization-Pfad bei stabilen Props.
202
- const resolver = createStaticLocaleResolver({ locale: "de" });
203
- const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" } }];
204
- function Probe(): ReactNode {
205
- const t = useTranslation();
206
- (Probe as unknown as { lastT?: unknown }).lastT = t;
207
- return null;
208
- }
209
- const { rerender } = render(
210
- <LocaleProvider resolver={resolver} fallbackBundles={bundles}>
211
- <Probe />
212
- </LocaleProvider>,
213
- );
214
- const firstT = (Probe as unknown as { lastT?: unknown }).lastT;
215
- rerender(
216
- <LocaleProvider resolver={resolver} fallbackBundles={bundles}>
217
- <Probe />
218
- </LocaleProvider>,
219
- );
220
- expect((Probe as unknown as { lastT?: unknown }).lastT).toBe(firstT);
221
- });
222
- });
223
- describe("translationsByLocaleFromKeys", () => {
224
- test("pivots key-first source to locale-first bundles losslessly", () => {
225
- const source = {
226
- "app:nav.home": { de: "Start", en: "Home" },
227
- "app:nav.settings": { de: "Einstellungen", en: "Settings" },
228
- };
229
- const byLocale = translationsByLocaleFromKeys(source);
230
- expect(byLocale["de"]).toEqual({
231
- "app:nav.home": "Start",
232
- "app:nav.settings": "Einstellungen",
233
- });
234
- expect(byLocale["en"]).toEqual({
235
- "app:nav.home": "Home",
236
- "app:nav.settings": "Settings",
237
- });
238
- });
239
- });
240
- describe("useLocale", () => {
241
- test("returns the resolver", () => {
242
- const resolver = createStaticLocaleResolver({ locale: "de" });
243
- const { result } = renderHook(() => useLocale(), { wrapper: wrap(resolver) });
244
- expect(result.current.locale()).toBe("de");
245
- });
246
- });
247
- describe("useTranslation — SSR", () => {
248
- // kumiko-framework#2595: getServerSnapshot hardcoded "en", so any
249
- // server render came out English regardless of the resolver's actual
250
- // locale — untestable via renderToStaticMarkup until getServerSnapshot
251
- // read the resolver instead of a constant.
252
- test(`renderToStaticMarkup with a non-English resolver renders the resolver's locale, not "en"`, () => {
253
- const resolver = createStaticLocaleResolver({ locale: "de" });
254
- const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" }, en: { greet: "Hello" } }];
255
- function Probe(): ReactNode {
256
- const t = useTranslation();
257
- return <span>{t("greet")}</span>;
258
- }
259
- const html = renderToStaticMarkup(
260
- <LocaleProvider resolver={resolver} fallbackBundles={bundles}>
261
- <Probe />
262
- </LocaleProvider>,
263
- );
264
- expect(html).toContain("Hallo");
265
- expect(html).not.toContain("Hello");
266
- });
267
-
268
- test(`renderToStaticMarkup via useOptionalTranslation resolves the resolver's locale, not "en"`, () => {
269
- const resolver = createStaticLocaleResolver({ locale: "de" });
270
- const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" }, en: { greet: "Hello" } }];
271
- function Probe(): ReactNode {
272
- const t = useOptionalTranslation();
273
- return <span>{t?.("greet")}</span>;
274
- }
275
- const html = renderToStaticMarkup(
276
- <LocaleProvider resolver={resolver} fallbackBundles={bundles}>
277
- <Probe />
278
- </LocaleProvider>,
279
- );
280
- expect(html).toContain("Hallo");
281
- expect(html).not.toContain("Hello");
282
- });
283
- });