@cosmicdrift/kumiko-framework 0.147.2 → 0.148.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-framework",
3
- "version": "0.147.2",
3
+ "version": "0.148.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -193,7 +193,7 @@
193
193
  "zod": "^4.4.3"
194
194
  },
195
195
  "devDependencies": {
196
- "@cosmicdrift/kumiko-dispatcher-live": "0.147.2",
196
+ "@cosmicdrift/kumiko-dispatcher-live": "0.148.0",
197
197
  "bun-types": "^1.3.13",
198
198
  "pino-pretty": "^13.1.3"
199
199
  },
@@ -47,6 +47,38 @@ describe("buildAppSchema", () => {
47
47
  expect(fleet?.entities["vehicle"]).toBeDefined();
48
48
  });
49
49
 
50
+ // #1059: r.translations({keys}) must flow into FeatureSchema.translations
51
+ // BYTE-IDENTICAL — nav/screen labels resolve these keys verbatim via
52
+ // t(label) client-side, so any re-prefixing (like the registry's internal
53
+ // mergedTranslations does for features whose keys already carry a colon)
54
+ // would break the lookup. Mirrors cap-counter's real shape exactly: nav
55
+ // label references its own already-qualified translation key.
56
+ test("r.translations landet verbatim in FeatureSchema.translations, ohne Re-Prefixing", () => {
57
+ const capCounterLikeFeature = defineFeature("cap-counter", (r) => {
58
+ r.nav({ id: "cap-list", label: "cap-counter:nav.cap-list" });
59
+ r.translations({
60
+ keys: {
61
+ "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
62
+ },
63
+ });
64
+ });
65
+
66
+ const app = buildAppSchema(createRegistry([capCounterLikeFeature]));
67
+ const feature = app.features.find((f) => f.featureName === "cap-counter");
68
+
69
+ expect(feature?.translations).toEqual({
70
+ "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
71
+ });
72
+ });
73
+
74
+ test("Feature ohne r.translations lässt das Feld weg (omit-undefined-Pattern)", () => {
75
+ const f = defineFeature("bare", (r) => {
76
+ r.nav({ id: "x", label: "X" });
77
+ });
78
+ const app = buildAppSchema(createRegistry([f]));
79
+ expect(app.features[0]?.translations).toBeUndefined();
80
+ });
81
+
50
82
  test("Workspaces — definition + aufgelöste navMembers landen auf AppSchema-Ebene", () => {
51
83
  const ordersFeature = defineFeature("orders", (r) => {
52
84
  r.nav({ id: "list", label: "List" });
@@ -17,6 +17,8 @@
17
17
  // sind plain literals)
18
18
  // Nav: verbatim (NavDefinition ist nur strings + literals)
19
19
  // Workspace: verbatim definition + getWorkspaceNavs() von der Registry
20
+ // Translations: verbatim feature.translations (raw r.translations keys,
21
+ // unfiltered — see FeatureSchema.translations doc)
20
22
  //
21
23
  // Feature-Toggles: BISHER NICHT GEFILTERT. Wenn ein Feature über die
22
24
  // feature-toggles-bundled-feature global deaktiviert ist, erscheint es
@@ -49,6 +51,12 @@ export function buildAppSchema(registry: Registry, options: BuildAppSchemaOption
49
51
  entities: projectEntities(feature.entities ?? {}),
50
52
  screens: Object.values(feature.screens),
51
53
  ...(navs.length > 0 && { navs }),
54
+ // #1059: verbatim r.translations({keys}) — see FeatureSchema.translations
55
+ // doc for why this must NOT go through registry.getAllTranslations()
56
+ // (double-prefixes features that already qualify their own keys).
57
+ ...(Object.keys(feature.translations ?? {}).length > 0 && {
58
+ translations: feature.translations,
59
+ }),
52
60
  };
53
61
  features.push(featureSchema);
54
62
  }
@@ -7,6 +7,7 @@
7
7
  // web), Runtime-Helpers (toAppSchema, isAppSchema) bleiben renderer-side
8
8
  // weil das die Layer ist die mit den AppSchemas zur Laufzeit arbeitet.
9
9
 
10
+ import type { TranslationKeys } from "../engine/types/config";
10
11
  import type { EntityDefinition } from "../engine/types/fields";
11
12
  import type { NavDefinition } from "../engine/types/nav";
12
13
  import type { ScreenDefinition } from "../engine/types/screen";
@@ -19,6 +20,17 @@ export type FeatureSchema = {
19
20
  // Flat list; resolveNavigation builds the tree at render-time from
20
21
  // the registry's indexes. Omitted when the app has no top-level nav.
21
22
  readonly navs?: readonly NavDefinition[];
23
+ // Server-authored `r.translations({ keys })`, projected verbatim — byte-
24
+ // identical keys, NOT re-prefixed with featureName (unlike the registry's
25
+ // internal mergedTranslations, which double-prefixes features that
26
+ // already qualify their own keys — see registry-ingest.ts populate
27
+ // Translations). Nav/screen labels resolve these keys directly via
28
+ // `t(label)`, so shipping anything other than the raw authored string
29
+ // would break the lookup. Omitted when a feature declares none. #1059:
30
+ // without this, nav labels only resolved when an app ALSO duplicated the
31
+ // key in `web/i18n.ts` — most bundled features never did, so labels
32
+ // rendered as raw i18n keys in the shell.
33
+ readonly translations?: TranslationKeys;
22
34
  // Workspaces — Legacy-Slot für single-feature-Apps. Bevorzugt liegt
23
35
  // workspaces auf der AppSchema-Ebene weil ihre navMembers regelmäßig
24
36
  // Cross-Feature-Navs referenzieren (siehe AppSchema-Doc). Hier als