@cosmicdrift/kumiko-renderer-web 0.147.3 → 0.149.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.147.3",
3
+ "version": "0.149.0",
4
4
  "description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.147.3",
20
- "@cosmicdrift/kumiko-headless": "0.147.3",
21
- "@cosmicdrift/kumiko-renderer": "0.147.3",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.149.0",
20
+ "@cosmicdrift/kumiko-headless": "0.149.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.149.0",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -1,11 +1,13 @@
1
1
  import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test";
2
2
  import type {
3
+ DashboardScreenDefinition,
3
4
  EntityDefinition,
4
5
  EntityEditScreenDefinition,
5
6
  EntityListScreenDefinition,
6
7
  } from "@cosmicdrift/kumiko-framework/ui-types";
7
8
  import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
8
9
  import type { ColumnRendererProps, FeatureSchema, NavApi } from "@cosmicdrift/kumiko-renderer";
10
+ import { createStaticLocaleResolver } from "@cosmicdrift/kumiko-renderer";
9
11
  import { act, screen, waitFor } from "@testing-library/react";
10
12
  import type { ReactNode } from "react";
11
13
  import type { ClientFeatureDefinition } from "../app/client-plugin";
@@ -252,6 +254,99 @@ describe("createKumikoApp", () => {
252
254
  expect(screen.getByTestId("ca-swatch-field").textContent).toBe("color");
253
255
  });
254
256
 
257
+ test("schema.translations (r.translations, #1059) resolves nav/dashboard labels without any clientFeatures duplication", async () => {
258
+ // Mirrors cap-counter's real shape: a feature that declares r.translations
259
+ // for its own nav label but ships NO web/i18n.ts / clientFeatures entry at
260
+ // all. Before #1059 this label rendered as the raw i18n key forever.
261
+ const dashboardScreen: DashboardScreenDefinition = {
262
+ id: "overview",
263
+ type: "dashboard",
264
+ panels: [
265
+ {
266
+ kind: "stat",
267
+ id: "cap-list",
268
+ label: "cap-counter:nav.cap-list",
269
+ query: "cap-counter:query:stat",
270
+ valueField: "value",
271
+ },
272
+ ],
273
+ };
274
+ const schema: FeatureSchema = {
275
+ featureName: "cap-counter",
276
+ entities: {},
277
+ screens: [dashboardScreen],
278
+ translations: {
279
+ "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
280
+ },
281
+ };
282
+ const dispatcher = createMockDispatcher({
283
+ query: (async () => ({
284
+ isSuccess: true,
285
+ data: { value: "3" },
286
+ })) as unknown as Dispatcher["query"],
287
+ });
288
+
289
+ mountRoot();
290
+ await mountApp({
291
+ schema,
292
+ dispatcher,
293
+ locale: createStaticLocaleResolver({ locale: "en" }),
294
+ });
295
+
296
+ await waitFor(() => expect(screen.getByText("Caps")).toBeTruthy());
297
+ expect(screen.queryByText("cap-counter:nav.cap-list")).toBeNull();
298
+ });
299
+
300
+ test("clientFeatures.translations still overrides schema.translations for the same key (precedence)", async () => {
301
+ // App-Override muss auch gegen ein framework-eigenes Label gewinnen —
302
+ // sonst kann eine App ein bundled-feature-Label nicht mehr anpassen,
303
+ // sobald #1059 dessen r.translations automatisch mitliefert.
304
+ const dashboardScreen: DashboardScreenDefinition = {
305
+ id: "overview",
306
+ type: "dashboard",
307
+ panels: [
308
+ {
309
+ kind: "stat",
310
+ id: "cap-list",
311
+ label: "cap-counter:nav.cap-list",
312
+ query: "cap-counter:query:stat",
313
+ valueField: "value",
314
+ },
315
+ ],
316
+ };
317
+ const schema: FeatureSchema = {
318
+ featureName: "cap-counter",
319
+ entities: {},
320
+ screens: [dashboardScreen],
321
+ translations: {
322
+ "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
323
+ },
324
+ };
325
+ const dispatcher = createMockDispatcher({
326
+ query: (async () => ({
327
+ isSuccess: true,
328
+ data: { value: "3" },
329
+ })) as unknown as Dispatcher["query"],
330
+ });
331
+ const override: ClientFeatureDefinition = {
332
+ name: "cap-counter-app-override",
333
+ translations: {
334
+ en: { "cap-counter:nav.cap-list": "Quota" },
335
+ },
336
+ };
337
+
338
+ mountRoot();
339
+ await mountApp({
340
+ schema,
341
+ dispatcher,
342
+ clientFeatures: [override],
343
+ locale: createStaticLocaleResolver({ locale: "en" }),
344
+ });
345
+
346
+ await waitFor(() => expect(screen.getByText("Quota")).toBeTruthy());
347
+ expect(screen.queryByText("Caps")).toBeNull();
348
+ });
349
+
255
350
  test("navAdapter override: eigener Router steuert den aktiven Screen", async () => {
256
351
  // Beweist den Nav-Seam: der Default-Adapter liest location.pathname,
257
352
  // dieser Memory-Adapter hardcoded die Route. Wenn swap funktioniert,
@@ -20,13 +20,16 @@ import {
20
20
  kumikoDefaultTranslations,
21
21
  LiveEventsProvider,
22
22
  LocaleProvider,
23
+ mergeTranslations,
23
24
  type NavApi,
24
25
  NavProvider,
25
26
  PrimitivesProvider,
26
27
  type PrimitivesRegistry,
27
28
  qualifyScreenId,
28
29
  TokensProvider,
30
+ type TranslationsByLocale,
29
31
  toAppSchema,
32
+ translationsByLocaleFromKeys,
30
33
  useNav,
31
34
  } from "@cosmicdrift/kumiko-renderer";
32
35
  import { type ComponentType, type ReactNode, useMemo } from "react";
@@ -177,12 +180,24 @@ export function createKumikoApp(options: CreateKumikoAppOptions = {}): { readonl
177
180
  const clientFeatures = options.clientFeatures ?? [];
178
181
  const providers = clientFeatures.flatMap((f) => f.providers ?? []);
179
182
  const gates = clientFeatures.flatMap((f) => f.gates ?? []);
180
- // Framework-Default-Bundle als ALLERLETZTER Fallback App-Resolver +
181
- // clientFeatures.translations haben Vorrang. Apps können einzelne
182
- // kumiko.*-Keys via clientFeatures.translations überschreiben (z.B.
183
- // "kumiko.actions.save" "Sichern" für ein bestimmtes Feature).
183
+ // Precedence in fallbackBundles (Array-Order = Priorität, höchste zuerst):
184
+ // 1. clientFeatures.translations App-Overrides gewinnen immer, auch
185
+ // gegen framework-eigene Labels.
186
+ // 2. schemaTranslations server-authored r.translations, von
187
+ // buildAppSchema verbatim projiziert (#1059). Ohne dieses Bundle
188
+ // resolven Nav-/Screen-Labels nur, wenn eine App sie ZUSÄTZLICH in
189
+ // web/i18n.ts dupliziert — die meisten bundled-features taten das
190
+ // nie, Labels rendern dann als rohe i18n-Keys.
191
+ // 3. kumikoDefaultTranslations — Framework-Defaults, ALLERLETZTER
192
+ // Fallback.
193
+ const schemaTranslations = app.features.reduce<TranslationsByLocale>(
194
+ (acc, f) =>
195
+ f.translations ? mergeTranslations(acc, translationsByLocaleFromKeys(f.translations)) : acc,
196
+ {},
197
+ );
184
198
  const fallbackBundles = [
185
199
  ...clientFeatures.flatMap((f) => (f.translations !== undefined ? [f.translations] : [])),
200
+ schemaTranslations,
186
201
  kumikoDefaultTranslations,
187
202
  ];
188
203
  // Custom-Screen-Components-Map mergen: spätere Features überschreiben