@cosmicdrift/kumiko-framework 0.255.2 → 0.257.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 +3 -3
- package/src/engine/__tests__/build-app-schema-settings-hub.test.ts +4 -0
- package/src/engine/__tests__/required-surface-keys.test.ts +47 -0
- package/src/engine/boot-validator/__tests__/i18n-keys.test.ts +70 -0
- package/src/engine/i18n-key.ts +12 -0
- package/src/engine/index.ts +1 -0
- package/src/i18n/required-surface-keys.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.257.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>",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-types": "0.257.0",
|
|
202
202
|
"bullmq": "^5.76.7",
|
|
203
203
|
"bun-types": "^1.3.13",
|
|
204
204
|
"hono": "^4.13.1",
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"zod": "^4.4.3"
|
|
215
215
|
},
|
|
216
216
|
"devDependencies": {
|
|
217
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
217
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.257.0",
|
|
218
218
|
"bun-types": "^1.3.13",
|
|
219
219
|
"pino-pretty": "^13.1.3"
|
|
220
220
|
},
|
|
@@ -189,6 +189,10 @@ describe("buildAppSchema — Settings-Hub inline placement", () => {
|
|
|
189
189
|
const ok = defineFeature("ok", (r) => {
|
|
190
190
|
r.config({ keys: { fee: createSystemConfig("number", { mask: { title: "ok.fee" } }) } });
|
|
191
191
|
r.workspace({ id: "w", label: "W", nav: ["config:nav:audience-system"] });
|
|
192
|
+
// mask.title is unconditionally required (fw#2313) — irrelevant to what
|
|
193
|
+
// this test actually asserts (audience-nav QN exemption), but boot
|
|
194
|
+
// still needs it translated.
|
|
195
|
+
r.translations({ keys: { "ok.fee": { en: "Fee" } } });
|
|
192
196
|
});
|
|
193
197
|
expect(() => validateBoot([ok])).not.toThrow();
|
|
194
198
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
requiredKeysFromWorkspace,
|
|
9
9
|
screenTitleKey,
|
|
10
10
|
} from "../../i18n/required-surface-keys";
|
|
11
|
+
import { i18nKey } from "../i18n-key";
|
|
11
12
|
import type {
|
|
12
13
|
ConfigEditScreenDefinition,
|
|
13
14
|
EntityEditScreenDefinition,
|
|
@@ -216,3 +217,49 @@ describe("dot-form labels + treatDotFormAsKey (fw#2260)", () => {
|
|
|
216
217
|
expect(requiredKeysFromNav(nav, { treatDotFormAsKey: true })).toContain("billing.settings");
|
|
217
218
|
});
|
|
218
219
|
});
|
|
220
|
+
|
|
221
|
+
// fw#2313: a hand-written dot-form label ("sessions.list.col.id") is
|
|
222
|
+
// indistinguishable from literal display text ("actions.open") — isI18nKey
|
|
223
|
+
// only recognizes it once the author marks it explicitly via i18nKey().
|
|
224
|
+
describe("dot-form label opt-in (fw#2313)", () => {
|
|
225
|
+
test("unmarked dot-form label is not required (regression guard for literal display text)", () => {
|
|
226
|
+
const screen: EntityListScreenDefinition = {
|
|
227
|
+
id: "fw2313-list",
|
|
228
|
+
type: "entityList",
|
|
229
|
+
entity: "widget",
|
|
230
|
+
columns: ["name"],
|
|
231
|
+
rowActions: [
|
|
232
|
+
{
|
|
233
|
+
id: "open",
|
|
234
|
+
label: "actions.open",
|
|
235
|
+
handler: "fw2313:write:widget:open",
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
const keys = requiredKeysFromScreen("fw2313", screen);
|
|
240
|
+
expect(keys).not.toContain("actions.open");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("i18nKey()-marked dot-form label is required", () => {
|
|
244
|
+
const screen: EntityListScreenDefinition = {
|
|
245
|
+
id: "fw2313-list",
|
|
246
|
+
type: "entityList",
|
|
247
|
+
entity: "widget",
|
|
248
|
+
columns: ["name"],
|
|
249
|
+
rowActions: [
|
|
250
|
+
{
|
|
251
|
+
id: "open",
|
|
252
|
+
label: i18nKey("fw2313.optin.col.alpha"),
|
|
253
|
+
handler: "fw2313:write:widget:open",
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
const keys = requiredKeysFromScreen("fw2313", screen);
|
|
258
|
+
expect(keys).toContain("fw2313.optin.col.alpha");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("i18nKey()-marked nav label is required", () => {
|
|
262
|
+
const nav = { id: "fw2313-nav", label: i18nKey("fw2313.optin.nav.beta") };
|
|
263
|
+
expect(requiredKeysFromNav(nav)).toContain("fw2313.optin.nav.beta");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { buildConfigFeatureSchema } from "../../build-config-feature-schema";
|
|
3
3
|
import { access, createTenantConfig } from "../../config-helpers";
|
|
4
4
|
import { defineFeature } from "../../define-feature";
|
|
5
|
+
import { i18nKey } from "../../i18n-key";
|
|
5
6
|
import { createRegistry } from "../../registry";
|
|
6
7
|
import { isFieldsEditSection } from "../../screen-helpers";
|
|
7
8
|
import type { ConfigEditScreenDefinition } from "../../types";
|
|
@@ -195,3 +196,72 @@ describe("validateI18nSurfaceKeys — gated configEdit section description (PR #
|
|
|
195
196
|
expect(withoutDescription.description).toBeUndefined();
|
|
196
197
|
});
|
|
197
198
|
});
|
|
199
|
+
|
|
200
|
+
// fw#2313: a hand-written dot-form label is indistinguishable from literal
|
|
201
|
+
// display text, so isI18nKey only requires a translation for one explicitly
|
|
202
|
+
// marked via i18nKey() — unmarked dot-form labels must keep failing silently
|
|
203
|
+
// (that's the existing, intentional behavior for literal text like
|
|
204
|
+
// "actions.open"), never crashing boot on their own.
|
|
205
|
+
describe("validateI18nSurfaceKeys — explicit i18nKey() dot-form opt-in (fw#2313)", () => {
|
|
206
|
+
test("i18nKey()-marked dot-form label without a translation fails boot naming the key", () => {
|
|
207
|
+
const feature = defineFeature("fw2313boot", (r) => {
|
|
208
|
+
r.nav({ id: "home", label: i18nKey("fw2313.boot.optin.nav") });
|
|
209
|
+
});
|
|
210
|
+
expect(() => validateBoot([feature])).toThrow(
|
|
211
|
+
/required translation key missing: "fw2313\.boot\.optin\.nav"/,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("i18nKey()-marked dot-form label with a translation passes boot", () => {
|
|
216
|
+
const feature = defineFeature("fw2313boot", (r) => {
|
|
217
|
+
r.nav({ id: "home", label: i18nKey("fw2313.boot.optin.nav.translated") });
|
|
218
|
+
r.translations({ keys: { "fw2313.boot.optin.nav.translated": { en: "Home" } } });
|
|
219
|
+
});
|
|
220
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("unmarked literal dot-form label never fails boot", () => {
|
|
224
|
+
const feature = defineFeature("fw2313boot", (r) => {
|
|
225
|
+
r.nav({ id: "home", label: "fw2313.boot.never-marked.literal" });
|
|
226
|
+
});
|
|
227
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// The mask.title bypass is unconditional (required-surface-keys.ts,
|
|
232
|
+
// requiredKeysFromFeature) — mask.title is always an i18n key by
|
|
233
|
+
// ConfigKeyDefinition contract, never literal display text, so it needs no
|
|
234
|
+
// i18nKey() marker.
|
|
235
|
+
describe("validateI18nSurfaceKeys — mask.title is unconditionally required (fw#2313)", () => {
|
|
236
|
+
function maskedFeature(translationKeys: Record<string, { readonly en: string }>) {
|
|
237
|
+
return defineFeature("fw2313mask", (r) => {
|
|
238
|
+
r.config({
|
|
239
|
+
keys: {
|
|
240
|
+
apiKey: createTenantConfig("text", {
|
|
241
|
+
write: access.roles("TenantAdmin"),
|
|
242
|
+
mask: { title: "fw2313.boot.mask.title" },
|
|
243
|
+
}),
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
if (Object.keys(translationKeys).length > 0) {
|
|
247
|
+
r.translations({ keys: translationKeys });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
test("masked config key without a mask.title translation fails boot", () => {
|
|
253
|
+
const feature = maskedFeature({});
|
|
254
|
+
expect(() => validateBoot([configHub, feature])).toThrow(
|
|
255
|
+
/required translation key missing: "fw2313\.boot\.mask\.title"/,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("masked config key with a mask.title translation passes boot", () => {
|
|
260
|
+
const feature = maskedFeature({
|
|
261
|
+
"fw2313.boot.mask.title": { en: "API Key" },
|
|
262
|
+
"screen:fw2313mask-tenant.title": { en: "Settings" },
|
|
263
|
+
"fw2313mask.settings": { en: "Settings" },
|
|
264
|
+
});
|
|
265
|
+
expect(() => validateBoot([configHub, feature])).not.toThrow();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Dot-form labels are indistinguishable from literal display text ("actions.open"),
|
|
2
|
+
// so authors mark the i18n ones explicitly; the registry is what boot validation reads.
|
|
3
|
+
const explicitDotFormKeys = new Set<string>();
|
|
4
|
+
|
|
5
|
+
export function i18nKey(value: string): string {
|
|
6
|
+
explicitDotFormKeys.add(value);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isExplicitDotFormKey(value: string): boolean {
|
|
11
|
+
return explicitDotFormKeys.has(value);
|
|
12
|
+
}
|
package/src/engine/index.ts
CHANGED
|
@@ -205,6 +205,7 @@ export {
|
|
|
205
205
|
filterReadFields,
|
|
206
206
|
} from "./field-access";
|
|
207
207
|
export { resolveName, withResponseData } from "./handler-helpers";
|
|
208
|
+
export { i18nKey } from "./i18n-key";
|
|
208
209
|
// findForbiddenMembershipRole/isForbiddenMembershipRole/
|
|
209
210
|
// stripForbiddenMembershipRoles/buildSessionRoles are Public API for host
|
|
210
211
|
// apps that build their own membership handlers. FORBIDDEN_MEMBERSHIP_ROLES
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isExplicitDotFormKey } from "../engine/i18n-key";
|
|
1
2
|
import {
|
|
2
3
|
isExtensionEditSection,
|
|
3
4
|
isWriteFormEditSection,
|
|
@@ -63,13 +64,14 @@ export function screenTitleKey(screenId: string): string {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
function isI18nKey(value: string): boolean {
|
|
66
|
-
return value.includes(":");
|
|
67
|
+
return value.includes(":") || isExplicitDotFormKey(value);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
71
|
* `treatAsKey` bypasses the colon-only `isI18nKey` check — used by the Settings-Hub
|
|
71
72
|
* generator's dot-form labels (`${feature}.settings`, mask titles), which are
|
|
72
73
|
* always i18n references by construction, never literal display text (fw#2260).
|
|
74
|
+
* `isI18nKey` itself also accepts dot-form labels explicitly marked via `i18nKey()` (fw#2313).
|
|
73
75
|
*/
|
|
74
76
|
function pushKey(out: Set<string>, value: string | undefined, treatAsKey = false): void {
|
|
75
77
|
if (value !== undefined && (treatAsKey || isI18nKey(value))) out.add(value);
|
|
@@ -348,7 +350,8 @@ export function requiredKeysFromFeature(feature: FeatureDefinition): readonly st
|
|
|
348
350
|
for (const key of requiredKeysFromWorkspace(ws)) out.add(key);
|
|
349
351
|
}
|
|
350
352
|
for (const def of Object.values(feature.configKeys)) {
|
|
351
|
-
|
|
353
|
+
// mask.title is always an i18n key by construction (ConfigKeyDefinition contract), not literal text.
|
|
354
|
+
pushKey(out, def.mask?.title, true);
|
|
352
355
|
}
|
|
353
356
|
collectEntityListFilterKeys(feature, out);
|
|
354
357
|
|