@pramen/cms 0.0.50 → 0.0.51

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
@@ -467,6 +467,28 @@ index is what turns a concurrent duplicate into a visible failure instead of a s
467
467
  ambiguous history. For the same reason the delete-and-purge pair is atomic on the DO but not
468
468
  on D1.
469
469
 
470
+ ### Locales
471
+
472
+ A deployment declares the locales it publishes in, and the editor renders its i18n surface
473
+ off that — the Translations panel, the Locale field, the per-row locale column:
474
+
475
+ ```ts
476
+ const handlers = { ...createCmsHandlers({ locales: ["cs", "en"] }), ... };
477
+ ```
478
+
479
+ One locale (or none declared) means monolingual: no i18n chrome anywhere, and the editor
480
+ stops sending `locale` on a page save at all, so nothing can blind-overwrite a field it no
481
+ longer shows. The FIRST entry is the locale a page is stamped with when created without
482
+ one — derived, not a second `defaultLocale` option, because two settings that can disagree
483
+ about the same fact is how a Czech-only site ends up publishing `en`.
484
+
485
+ Declared, not inferred from the data: the only way to create a second locale is
486
+ `createTranslation`, which the editor exposes from inside the Translations panel — so a rule
487
+ like "show i18n once a second locale exists" would mean a monolingual site could never
488
+ become multilingual. `listCmsCapabilities` is what the editor reads; `listLocales` remains a
489
+ DATA query (which locales are actually authored), and the two differ while a locale is
490
+ declared but not yet used.
491
+
470
492
  ## Limitations
471
493
 
472
494
  - **Block `fields` are opaque JSON**, so pramen's row/cell-level ACL and relational queries
package/dist/index.d.ts CHANGED
@@ -791,8 +791,19 @@ export interface CmsHandlerOpts {
791
791
  editorRoles?: readonly string[];
792
792
  /** Max accepted media upload size in bytes (enforced at the Worker). Default 25 MB. */
793
793
  mediaMaxSize?: number;
794
- /** Default locale used when `getPage`/`createPage` omit one. Default `"en"`. */
795
- defaultLocale?: string;
794
+ /** The locales this deployment publishes in, most-preferred first. Default `["en"]`.
795
+ *
796
+ * DECLARED, not inferred. The editor renders its i18n surface — the Translations panel,
797
+ * the Locale field, the per-row locale column — only when there is more than one, and
798
+ * `listCmsCapabilities` is how it finds out. Inferring "is this site multilingual?" from
799
+ * the locales PRESENT IN DATA cannot work: the only way to create a second locale is
800
+ * `createTranslation`, which the editor exposes from inside the very panel that would
801
+ * stay hidden, so a monolingual site could never become multilingual.
802
+ *
803
+ * The first entry is the default stamped on a page created without one, which is why
804
+ * `defaultLocale` is derived from this rather than configured beside it — two options
805
+ * that can disagree about the same fact is how a Czech-only site ends up stamping "en". */
806
+ locales?: readonly string[];
796
807
  /** Roles permitted to approve/reject a page in review and publish (the editorial gate).
797
808
  * Default `["reviewer", "admin"]`. */
798
809
  reviewerRoles?: readonly string[];
@@ -960,7 +971,21 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
960
971
  title: string;
961
972
  status: string;
962
973
  }[]>;
963
- /** Distinct locales present across all pages. */
974
+ /** What this deployment supports, for an editor to render against — the pages-side
975
+ * counterpart to `listCollections`' `supports: [...]`.
976
+ *
977
+ * The editor asks the SERVER what exists rather than being told by its own /config.js:
978
+ * a client flag can hide a control but cannot make the data right, and the two drift
979
+ * the moment someone adds a locale. `multilingual` is the derived answer to the only
980
+ * question the UI actually asks, so each surface doesn't re-derive it from the list. */
981
+ listCmsCapabilities: import("@pramen/server").Handler<unknown, {
982
+ locales: string[];
983
+ defaultLocale: string;
984
+ multilingual: boolean;
985
+ }>;
986
+ /** Distinct locales present across all pages. NOTE: a DATA query — what is in the
987
+ * store — not configuration. `listCmsCapabilities().locales` is what the deployment
988
+ * declares; these two differ while a locale is declared but not yet authored. */
964
989
  listLocales: import("@pramen/server").Handler<unknown, string[]>;
965
990
  /** Create a block instance and place it into a page region in one call (the common
966
991
  * editor action). Validates the fields against the block type's schema and the region
@@ -1300,7 +1325,21 @@ export declare const cmsHandlers: {
1300
1325
  title: string;
1301
1326
  status: string;
1302
1327
  }[]>;
1303
- /** Distinct locales present across all pages. */
1328
+ /** What this deployment supports, for an editor to render against — the pages-side
1329
+ * counterpart to `listCollections`' `supports: [...]`.
1330
+ *
1331
+ * The editor asks the SERVER what exists rather than being told by its own /config.js:
1332
+ * a client flag can hide a control but cannot make the data right, and the two drift
1333
+ * the moment someone adds a locale. `multilingual` is the derived answer to the only
1334
+ * question the UI actually asks, so each surface doesn't re-derive it from the list. */
1335
+ listCmsCapabilities: import("@pramen/server").Handler<unknown, {
1336
+ locales: string[];
1337
+ defaultLocale: string;
1338
+ multilingual: boolean;
1339
+ }>;
1340
+ /** Distinct locales present across all pages. NOTE: a DATA query — what is in the
1341
+ * store — not configuration. `listCmsCapabilities().locales` is what the deployment
1342
+ * declares; these two differ while a locale is declared but not yet authored. */
1304
1343
  listLocales: import("@pramen/server").Handler<unknown, string[]>;
1305
1344
  /** Create a block instance and place it into a page region in one call (the common
1306
1345
  * editor action). Validates the fields against the block type's schema and the region
package/dist/index.js CHANGED
@@ -959,7 +959,8 @@ export function createCmsHandlers(opts = {}) {
959
959
  const editorRoles = opts.editorRoles ?? ["editor", "admin"];
960
960
  const editor = { auth: editorRoles };
961
961
  const mediaMaxSize = opts.mediaMaxSize ?? 25_000_000;
962
- const defaultLocale = opts.defaultLocale ?? "en";
962
+ const locales = opts.locales && opts.locales.length > 0 ? [...opts.locales] : ["en"];
963
+ const defaultLocale = locales[0];
963
964
  const reviewerRoles = opts.reviewerRoles ?? ["reviewer", "admin"];
964
965
  const reviewer = { auth: reviewerRoles };
965
966
  const previewTtl = opts.previewTtlSeconds ?? DEFAULT_PREVIEW_TTL_SECONDS;
@@ -1499,7 +1500,17 @@ export function createCmsHandlers(opts = {}) {
1499
1500
  return o;
1500
1501
  },
1501
1502
  }),
1502
- /** Distinct locales present across all pages. */
1503
+ /** What this deployment supports, for an editor to render against — the pages-side
1504
+ * counterpart to `listCollections`' `supports: [...]`.
1505
+ *
1506
+ * The editor asks the SERVER what exists rather than being told by its own /config.js:
1507
+ * a client flag can hide a control but cannot make the data right, and the two drift
1508
+ * the moment someone adds a locale. `multilingual` is the derived answer to the only
1509
+ * question the UI actually asks, so each surface doesn't re-derive it from the list. */
1510
+ listCmsCapabilities: query(() => ({ locales, defaultLocale, multilingual: locales.length > 1 }), viewer),
1511
+ /** Distinct locales present across all pages. NOTE: a DATA query — what is in the
1512
+ * store — not configuration. `listCmsCapabilities().locales` is what the deployment
1513
+ * declares; these two differ while a locale is declared but not yet authored. */
1503
1514
  listLocales: query(async (ctx) => {
1504
1515
  // Raw exec bypasses the ACL, so the trash filter has to be written out by hand —
1505
1516
  // otherwise the editor's locale switcher offers a locale with zero live pages.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "description": "Optional block/page builder for pramen — Drupal-Paragraphs-style typed blocks in named regions, reusable blocks, scheduled publishing, built entirely from pramen primitives.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@pramen/server": "0.0.50"
44
+ "@pramen/server": "0.0.51"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "react": ">=18"
package/src/index.ts CHANGED
@@ -1379,8 +1379,19 @@ export interface CmsHandlerOpts {
1379
1379
  editorRoles?: readonly string[];
1380
1380
  /** Max accepted media upload size in bytes (enforced at the Worker). Default 25 MB. */
1381
1381
  mediaMaxSize?: number;
1382
- /** Default locale used when `getPage`/`createPage` omit one. Default `"en"`. */
1383
- defaultLocale?: string;
1382
+ /** The locales this deployment publishes in, most-preferred first. Default `["en"]`.
1383
+ *
1384
+ * DECLARED, not inferred. The editor renders its i18n surface — the Translations panel,
1385
+ * the Locale field, the per-row locale column — only when there is more than one, and
1386
+ * `listCmsCapabilities` is how it finds out. Inferring "is this site multilingual?" from
1387
+ * the locales PRESENT IN DATA cannot work: the only way to create a second locale is
1388
+ * `createTranslation`, which the editor exposes from inside the very panel that would
1389
+ * stay hidden, so a monolingual site could never become multilingual.
1390
+ *
1391
+ * The first entry is the default stamped on a page created without one, which is why
1392
+ * `defaultLocale` is derived from this rather than configured beside it — two options
1393
+ * that can disagree about the same fact is how a Czech-only site ends up stamping "en". */
1394
+ locales?: readonly string[];
1384
1395
  /** Roles permitted to approve/reject a page in review and publish (the editorial gate).
1385
1396
  * Default `["reviewer", "admin"]`. */
1386
1397
  reviewerRoles?: readonly string[];
@@ -1398,7 +1409,8 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1398
1409
  const editorRoles = opts.editorRoles ?? ["editor", "admin"];
1399
1410
  const editor = { auth: editorRoles };
1400
1411
  const mediaMaxSize = opts.mediaMaxSize ?? 25_000_000;
1401
- const defaultLocale = opts.defaultLocale ?? "en";
1412
+ const locales = opts.locales && opts.locales.length > 0 ? [...opts.locales] : ["en"];
1413
+ const defaultLocale = locales[0]!;
1402
1414
  const reviewerRoles = opts.reviewerRoles ?? ["reviewer", "admin"];
1403
1415
  const reviewer = { auth: reviewerRoles };
1404
1416
  const previewTtl = opts.previewTtlSeconds ?? DEFAULT_PREVIEW_TTL_SECONDS;
@@ -1932,7 +1944,18 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1932
1944
  },
1933
1945
  }),
1934
1946
 
1935
- /** Distinct locales present across all pages. */
1947
+ /** What this deployment supports, for an editor to render against — the pages-side
1948
+ * counterpart to `listCollections`' `supports: [...]`.
1949
+ *
1950
+ * The editor asks the SERVER what exists rather than being told by its own /config.js:
1951
+ * a client flag can hide a control but cannot make the data right, and the two drift
1952
+ * the moment someone adds a locale. `multilingual` is the derived answer to the only
1953
+ * question the UI actually asks, so each surface doesn't re-derive it from the list. */
1954
+ listCmsCapabilities: query(() => ({ locales, defaultLocale, multilingual: locales.length > 1 }), viewer),
1955
+
1956
+ /** Distinct locales present across all pages. NOTE: a DATA query — what is in the
1957
+ * store — not configuration. `listCmsCapabilities().locales` is what the deployment
1958
+ * declares; these two differ while a locale is declared but not yet authored. */
1936
1959
  listLocales: query(async (ctx) => {
1937
1960
  // Raw exec bypasses the ACL, so the trash filter has to be written out by hand —
1938
1961
  // otherwise the editor's locale switcher offers a locale with zero live pages.