@colixsystems/widget-sdk 0.37.0 → 0.38.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/README.md CHANGED
@@ -26,7 +26,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
26
26
  | **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill its page-grid tile's reserved height (containers + media fill by default; the author can override per tile). Media-style widgets switch to a `flex: 1` / `height: "100%"` layout; others ignore it. Defaults `false`. |
27
27
  | **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
28
28
  | **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
29
- | **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) then falls back to the raw key. |
29
+ | **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
30
30
  | **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
31
31
  | **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
32
32
  | **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
@@ -47,7 +47,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
47
47
 
48
48
  ## Status
49
49
 
50
- `v0.37.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
50
+ `v0.38.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
51
+
52
+ ### What's new in 0.38.0
53
+
54
+ **Predefined SHARED translation keys (REQ-L10N-SHARED).** The standard strings the default widgets repeat ("Submit", "Cancel", "Save", "Loading…", …) now have a tenant-wide shared namespace `shared.<key>` so an identical string is translated **once** and every widget that uses it inherits the translation.
55
+
56
+ - **New contract field `CONTRACT.sharedTranslationKeys`** — the predefined map (`{ <key>: { en } }`) and the single source the host seeder reads.
57
+ - **New exported helpers** `sharedTranslationPrefix()` / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)` (from `@colixsystems/widget-sdk/contract`).
58
+ - **`useI18n().t(key)` resolution is now three-step**: the per-widget key (`widget.<id>.<key>`) first, then the shared key (`shared.<key>`) when `key` is one of the predefined shared keys, then the raw key / fallback. So a default widget that calls `t("submit")` picks up the shared translation with no manual key entry, and an author who sets `widget.<id>.submit` in the Translations admin still overrides **that instance only**. An author-invented bare key is never silently shared.
59
+ - **The host auto-registers the shared keys** for a tenant at workspace-content seed time and whenever a marketplace or AI-generated widget is added — idempotent and non-destructive (an admin edit is never overwritten). Authors manage / translate them in the Studio Translations screen like any other key.
60
+ - **`CONTRACT.version` → `1.28.0`** (additive: one new contract field + three helper exports + the `useI18n` shared-key step). No existing export changed signature.
51
61
 
52
62
  ### What's new in 0.37.0
53
63
 
package/dist/contract.cjs CHANGED
@@ -1285,6 +1285,65 @@ function widgetTranslationKey(id, key) {
1285
1285
  return `widget.${id}.${key}`;
1286
1286
  }
1287
1287
 
1288
+ // REQ-L10N-SHARED — predefined SHARED translation keys for the standard
1289
+ // strings the default widgets repeat ("Submit", "Cancel", "Loading…", …).
1290
+ // They live in ONE tenant-wide namespace (`shared.<key>`) rather than the
1291
+ // per-widget namespace, so identical strings translate ONCE and every widget
1292
+ // that calls `t("<sharedKey>")` resolves the same dictionary value. An author
1293
+ // still overrides any instance by setting `widget.<id>.<key>` in the
1294
+ // Translations admin — `useI18n` resolves the per-widget key FIRST, then the
1295
+ // shared key, then the raw key, so the override is per-instance.
1296
+ //
1297
+ // This is the analogue of `widget.<id>.` for cross-widget reuse: one prefix,
1298
+ // one definition (the hook reads + the backend seeder writes both call these),
1299
+ // so the wire key and the lookup key can never drift.
1300
+ const SHARED_TRANSLATION_PREFIX = "shared.";
1301
+ function sharedTranslationPrefix() {
1302
+ return SHARED_TRANSLATION_PREFIX;
1303
+ }
1304
+ function sharedTranslationKey(key) {
1305
+ return `${SHARED_TRANSLATION_PREFIX}${key}`;
1306
+ }
1307
+
1308
+ // The predefined set. Map of bare key -> { en } (English default only; the
1309
+ // tenant adds target-language values once in the Translations admin and every
1310
+ // widget inherits them). Adding/removing a key is a CONTRACT change (it grows
1311
+ // the dictionary every tenant gets), so the list lives with the contract and
1312
+ // the seeder reads it from here — never a second copy.
1313
+ const SHARED_TRANSLATION_KEYS = Object.freeze({
1314
+ submit: { en: "Submit" },
1315
+ cancel: { en: "Cancel" },
1316
+ save: { en: "Save" },
1317
+ delete: { en: "Delete" },
1318
+ edit: { en: "Edit" },
1319
+ close: { en: "Close" },
1320
+ confirm: { en: "Confirm" },
1321
+ back: { en: "Back" },
1322
+ next: { en: "Next" },
1323
+ previous: { en: "Previous" },
1324
+ yes: { en: "Yes" },
1325
+ no: { en: "No" },
1326
+ ok: { en: "OK" },
1327
+ loading: { en: "Loading…" },
1328
+ search: { en: "Search" },
1329
+ no_results: { en: "No results" },
1330
+ required: { en: "Required" },
1331
+ retry: { en: "Retry" },
1332
+ add: { en: "Add" },
1333
+ remove: { en: "Remove" },
1334
+ });
1335
+
1336
+ // True when `key` is one of the predefined shared keys — the discriminator
1337
+ // `useI18n` uses to decide whether to try the `shared.<key>` namespace. A key
1338
+ // the author invents is NOT shared (it stays per-widget) so authors can't
1339
+ // accidentally collide with another widget by reusing a bare name.
1340
+ function isSharedTranslationKey(key) {
1341
+ return (
1342
+ typeof key === "string" &&
1343
+ Object.prototype.hasOwnProperty.call(SHARED_TRANSLATION_KEYS, key)
1344
+ );
1345
+ }
1346
+
1288
1347
  const CONTRACT = deepFreeze({
1289
1348
  // REQ-WSDK-PLATFORM bump:
1290
1349
  // - `vettedImports` is a new field (rich allowlist with platforms +
@@ -1486,7 +1545,23 @@ const CONTRACT = deepFreeze({
1486
1545
  // on `react/jsx-runtime`. Adding them converges the linter onto the same
1487
1546
  // contract every other surface already honoured (CLAUDE.md §3). No
1488
1547
  // existing entry changed shape — minor bump on the pre-1.0 channel.
1489
- version: "1.27.0",
1548
+ //
1549
+ // 1.28.0: additive (REQ-L10N-SHARED) — predefined SHARED translation keys.
1550
+ // A new tenant-wide namespace `shared.<key>` carries the standard strings
1551
+ // the default widgets repeat ("Submit", "Cancel", "Loading…", …) so an
1552
+ // identical string is translated ONCE and every widget that calls
1553
+ // `t("<sharedKey>")` inherits it. New contract field
1554
+ // `sharedTranslationKeys` (the predefined map, the single source the
1555
+ // backend seeder reads) + new exported helpers `sharedTranslationPrefix()`
1556
+ // / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)`.
1557
+ // `useI18n().t(key)` now resolves the per-widget key first, THEN the
1558
+ // shared key (when `key` is one of the predefined shared keys), then the
1559
+ // raw key — so a default widget picks up the shared translation with no
1560
+ // manual key entry, and a `widget.<id>.<key>` override still wins per
1561
+ // instance. No existing hook, primitive, manifest field, or token changed
1562
+ // shape — minor bump on the pre-1.0 channel.
1563
+ version: "1.28.0",
1564
+ sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1490
1565
  hooks: HOOKS,
1491
1566
  primitives: PRIMITIVES,
1492
1567
  manifestSchema: MANIFEST_SCHEMA,
@@ -1524,4 +1599,8 @@ module.exports = {
1524
1599
  requiredContextKeys,
1525
1600
  widgetTranslationPrefix,
1526
1601
  widgetTranslationKey,
1602
+ sharedTranslationPrefix,
1603
+ sharedTranslationKey,
1604
+ isSharedTranslationKey,
1605
+ SHARED_TRANSLATION_KEYS,
1527
1606
  };
package/dist/contract.js CHANGED
@@ -1285,6 +1285,65 @@ function widgetTranslationKey(id, key) {
1285
1285
  return `widget.${id}.${key}`;
1286
1286
  }
1287
1287
 
1288
+ // REQ-L10N-SHARED — predefined SHARED translation keys for the standard
1289
+ // strings the default widgets repeat ("Submit", "Cancel", "Loading…", …).
1290
+ // They live in ONE tenant-wide namespace (`shared.<key>`) rather than the
1291
+ // per-widget namespace, so identical strings translate ONCE and every widget
1292
+ // that calls `t("<sharedKey>")` resolves the same dictionary value. An author
1293
+ // still overrides any instance by setting `widget.<id>.<key>` in the
1294
+ // Translations admin — `useI18n` resolves the per-widget key FIRST, then the
1295
+ // shared key, then the raw key, so the override is per-instance.
1296
+ //
1297
+ // This is the analogue of `widget.<id>.` for cross-widget reuse: one prefix,
1298
+ // one definition (the hook reads + the backend seeder writes both call these),
1299
+ // so the wire key and the lookup key can never drift.
1300
+ const SHARED_TRANSLATION_PREFIX = "shared.";
1301
+ function sharedTranslationPrefix() {
1302
+ return SHARED_TRANSLATION_PREFIX;
1303
+ }
1304
+ function sharedTranslationKey(key) {
1305
+ return `${SHARED_TRANSLATION_PREFIX}${key}`;
1306
+ }
1307
+
1308
+ // The predefined set. Map of bare key -> { en } (English default only; the
1309
+ // tenant adds target-language values once in the Translations admin and every
1310
+ // widget inherits them). Adding/removing a key is a CONTRACT change (it grows
1311
+ // the dictionary every tenant gets), so the list lives with the contract and
1312
+ // the seeder reads it from here — never a second copy.
1313
+ const SHARED_TRANSLATION_KEYS = Object.freeze({
1314
+ submit: { en: "Submit" },
1315
+ cancel: { en: "Cancel" },
1316
+ save: { en: "Save" },
1317
+ delete: { en: "Delete" },
1318
+ edit: { en: "Edit" },
1319
+ close: { en: "Close" },
1320
+ confirm: { en: "Confirm" },
1321
+ back: { en: "Back" },
1322
+ next: { en: "Next" },
1323
+ previous: { en: "Previous" },
1324
+ yes: { en: "Yes" },
1325
+ no: { en: "No" },
1326
+ ok: { en: "OK" },
1327
+ loading: { en: "Loading…" },
1328
+ search: { en: "Search" },
1329
+ no_results: { en: "No results" },
1330
+ required: { en: "Required" },
1331
+ retry: { en: "Retry" },
1332
+ add: { en: "Add" },
1333
+ remove: { en: "Remove" },
1334
+ });
1335
+
1336
+ // True when `key` is one of the predefined shared keys — the discriminator
1337
+ // `useI18n` uses to decide whether to try the `shared.<key>` namespace. A key
1338
+ // the author invents is NOT shared (it stays per-widget) so authors can't
1339
+ // accidentally collide with another widget by reusing a bare name.
1340
+ function isSharedTranslationKey(key) {
1341
+ return (
1342
+ typeof key === "string" &&
1343
+ Object.prototype.hasOwnProperty.call(SHARED_TRANSLATION_KEYS, key)
1344
+ );
1345
+ }
1346
+
1288
1347
  const CONTRACT = deepFreeze({
1289
1348
  // REQ-WSDK-PLATFORM bump:
1290
1349
  // - `vettedImports` is a new field (rich allowlist with platforms +
@@ -1486,7 +1545,23 @@ const CONTRACT = deepFreeze({
1486
1545
  // on `react/jsx-runtime`. Adding them converges the linter onto the same
1487
1546
  // contract every other surface already honoured (CLAUDE.md §3). No
1488
1547
  // existing entry changed shape — minor bump on the pre-1.0 channel.
1489
- version: "1.27.0",
1548
+ //
1549
+ // 1.28.0: additive (REQ-L10N-SHARED) — predefined SHARED translation keys.
1550
+ // A new tenant-wide namespace `shared.<key>` carries the standard strings
1551
+ // the default widgets repeat ("Submit", "Cancel", "Loading…", …) so an
1552
+ // identical string is translated ONCE and every widget that calls
1553
+ // `t("<sharedKey>")` inherits it. New contract field
1554
+ // `sharedTranslationKeys` (the predefined map, the single source the
1555
+ // backend seeder reads) + new exported helpers `sharedTranslationPrefix()`
1556
+ // / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)`.
1557
+ // `useI18n().t(key)` now resolves the per-widget key first, THEN the
1558
+ // shared key (when `key` is one of the predefined shared keys), then the
1559
+ // raw key — so a default widget picks up the shared translation with no
1560
+ // manual key entry, and a `widget.<id>.<key>` override still wins per
1561
+ // instance. No existing hook, primitive, manifest field, or token changed
1562
+ // shape — minor bump on the pre-1.0 channel.
1563
+ version: "1.28.0",
1564
+ sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1490
1565
  hooks: HOOKS,
1491
1566
  primitives: PRIMITIVES,
1492
1567
  manifestSchema: MANIFEST_SCHEMA,
@@ -1524,4 +1599,8 @@ export {
1524
1599
  requiredContextKeys,
1525
1600
  widgetTranslationPrefix,
1526
1601
  widgetTranslationKey,
1602
+ sharedTranslationPrefix,
1603
+ sharedTranslationKey,
1604
+ isSharedTranslationKey,
1605
+ SHARED_TRANSLATION_KEYS,
1527
1606
  };
package/dist/hooks.js CHANGED
@@ -26,10 +26,18 @@ import React, {
26
26
  useRef,
27
27
  useState,
28
28
  } from "react";
29
- // REQ-L10N-WIDGET: the per-widget translation key format lives in ONE place
30
- // (contract.js). useI18n (lookup) and the backend seeder (write) both call it
31
- // so the namespaced key can never drift between the two sides.
32
- import { widgetTranslationKey } from "./contract.js";
29
+ // REQ-L10N-WIDGET / REQ-L10N-SHARED: the translation key formats live in ONE
30
+ // place (contract.js). useI18n (lookup) and the backend seeder (write) both
31
+ // call them so the namespaced keys can never drift between the two sides.
32
+ // `widgetTranslationKey` builds the per-widget key (`widget.<id>.<key>`);
33
+ // `sharedTranslationKey` builds the tenant-wide predefined key
34
+ // (`shared.<key>`); `isSharedTranslationKey` tells the hook whether a bare key
35
+ // is one of the predefined shared keys and so should try the shared namespace.
36
+ import {
37
+ widgetTranslationKey,
38
+ sharedTranslationKey,
39
+ isSharedTranslationKey,
40
+ } from "./contract.js";
33
41
 
34
42
  /** @internal — host-injected context value of shape WidgetContext (see index.d.ts). */
35
43
  const HostWidgetContext = createContext(null);
@@ -240,7 +248,27 @@ export function useI18n() {
240
248
  return scoped;
241
249
  }
242
250
  }
243
- // 2) Fall back to the raw key (shared app keys + widgets with no
251
+ // 2) REQ-L10N-SHARED: for a predefined SHARED key, try the tenant-wide
252
+ // `shared.<key>` namespace next. This is what lets identical
253
+ // default-widget strings ("Submit", "Cancel", …) translate ONCE: a
254
+ // widget that has no per-instance `widget.<id>.<key>` override
255
+ // inherits the shared value. The per-widget step above runs FIRST,
256
+ // so an author override of a single instance still wins for that
257
+ // instance. Only the predefined keys are tried here — an
258
+ // author-invented bare key is never silently shared.
259
+ if (isSharedTranslationKey(key)) {
260
+ const shared = sharedTranslationKey(key);
261
+ const scoped = hostT(shared);
262
+ if (
263
+ typeof scoped === "string" &&
264
+ scoped.length > 0 &&
265
+ scoped !== shared &&
266
+ !scoped.startsWith("{{t:")
267
+ ) {
268
+ return scoped;
269
+ }
270
+ }
271
+ // 3) Fall back to the raw key (shared app keys + widgets with no
244
272
  // manifest translations — unchanged from pre-1.10 behaviour).
245
273
  const out = hostT(key, fallback);
246
274
  if (typeof out === "string" && out.length > 0 && out !== key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",