@elytracms/studio 0.0.80 → 0.0.82
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/convex/contract-parity.test.ts +1 -1
- package/convex/functions.ts +13 -3
- package/convex/http-routes.test.ts +143 -0
- package/convex/http.ts +19 -9
- package/convex/redirect-revalidate.test.ts +130 -0
- package/convex/revalidate.ts +9 -7
- package/convex/schema.ts +0 -19
- package/convex/triggers.ts +33 -3
- package/convex/users.ts +1 -1
- package/dist/components/ai/ai-chat-panel.js +26 -4
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +27 -0
- package/dist/components/ai/proposal-card.js +31 -3
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.d.ts +55 -0
- package/dist/components/ai/proposal-issues.js +69 -0
- package/dist/components/ai/proposal-issues.js.map +1 -0
- package/dist/components/ai/translate-document-dialog.js +38 -3
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +141 -44
- package/dist/components/app-views/default-modules.js +229 -65
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +38 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +5 -2
- package/dist/components/app-views/sidebar.js +16 -5
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +28 -1
- package/dist/components/app-views/studio-config.js +1 -1
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +10 -2
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/reuse-context.js +7 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +94 -16
- package/dist/components/builder/reuse.js +201 -19
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +10 -5
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +18 -3
- package/dist/components/cms/cell-edit.js +29 -7
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +54 -14
- package/dist/components/cms/cms-store.js +142 -45
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +2 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +15 -14
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +12 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/field-control.js +22 -8
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.js +8 -1
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +36 -0
- package/dist/components/cms/gate-validation.js +17 -0
- package/dist/components/cms/gate-validation.js.map +1 -0
- package/dist/components/cms/import-export.js +2 -1
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/inline-field-errors.d.ts +8 -0
- package/dist/components/cms/inline-field-errors.js +52 -0
- package/dist/components/cms/inline-field-errors.js.map +1 -0
- package/dist/components/cms/issues-pane.d.ts +4 -2
- package/dist/components/cms/issues-pane.js +13 -7
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/slug-collision-report.d.ts +41 -0
- package/dist/components/cms/slug-collision-report.js +52 -0
- package/dist/components/cms/slug-collision-report.js.map +1 -0
- package/dist/components/components/transfer-dialog.js +10 -12
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +24 -0
- package/dist/components/deployment-setup-gate.js +29 -4
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/issue-severity.d.ts +73 -0
- package/dist/components/issue-severity.js +56 -0
- package/dist/components/issue-severity.js.map +1 -0
- package/dist/components/nav-issues.d.ts +68 -0
- package/dist/components/nav-issues.js +78 -0
- package/dist/components/nav-issues.js.map +1 -0
- package/dist/components/operation-error.js +3 -3
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/project-list.d.ts +9 -3
- package/dist/components/project-list.js +11 -5
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.d.ts +3 -1
- package/dist/components/project-switcher.js +4 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/redirects/redirects-store.d.ts +21 -0
- package/dist/components/redirects/redirects-store.js +17 -4
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/section-nav.js +75 -10
- package/dist/components/section-nav.js.map +1 -1
- package/dist/customization.d.ts +7 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/ai/apply-content.js +6 -7
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +32 -26
- package/dist/lib/ai/chat-context.js +81 -9
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +42 -0
- package/dist/lib/ai/constraints.js +259 -0
- package/dist/lib/ai/constraints.js.map +1 -0
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/proposal-validation.d.ts +26 -0
- package/dist/lib/ai/proposal-validation.js +17 -0
- package/dist/lib/ai/proposal-validation.js.map +1 -0
- package/dist/lib/ai/shape.js +60 -13
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -4
- package/dist/lib/ai/tools.js +43 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +35 -1
- package/dist/lib/ai/translate-document.js +16 -1
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +11 -6
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +8 -6
- package/dist/lib/ai/update-fields.js +9 -22
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +5 -0
- package/dist/lib/clipboard/paste-issues.js +16 -6
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/cms/document-gate.d.ts +37 -0
- package/dist/lib/cms/document-gate.js +40 -0
- package/dist/lib/cms/document-gate.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +5 -0
- package/dist/lib/cms-views/view-model.js +10 -4
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +6 -1
- package/dist/lib/data/collection-content.js +51 -15
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/i18n.d.ts +51 -16
- package/dist/lib/i18n/i18n.js +119 -43
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +8 -1
- package/dist/lib/i18n/index.js +1 -1
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +200 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +162 -0
- package/dist/lib/i18n/messages.en.js +226 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/i18n/types.d.ts +15 -9
- package/dist/lib/i18n/types.js.map +1 -1
- package/dist/lib/locale/binding-fallback.d.ts +8 -1
- package/dist/lib/locale/binding-fallback.js +8 -1
- package/dist/lib/locale/binding-fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +41 -9
- package/dist/lib/locale/locale-store.js +34 -15
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/locale/locale-switcher.d.ts +3 -1
- package/dist/lib/locale/locale-switcher.js +4 -2
- package/dist/lib/locale/locale-switcher.js.map +1 -1
- package/dist/lib/locale/use-studio-locale.d.ts +9 -4
- package/dist/lib/locale/use-studio-locale.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +19 -2
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +1 -3
- package/dist/lib/persistence/convex-adapter.js +0 -55
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.js +10 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +85 -9
- package/dist/lib/persona/labels.js +198 -58
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +9 -0
- package/dist/lib/routes/route-issues.js +117 -0
- package/dist/lib/routes/route-issues.js.map +1 -0
- package/dist/lib/webhooks/dispatcher.d.ts +5 -2
- package/dist/lib/webhooks/dispatcher.js.map +1 -1
- package/dist/lib/webhooks/tags.js +150 -17
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/config-issues.d.ts +56 -0
- package/dist/lib/workspace/config-issues.js +72 -0
- package/dist/lib/workspace/config-issues.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +24 -8
- package/dist/lib/workspace/index.js +28 -18
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +20 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +27 -0
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +13 -6
- package/dist/previews/manifest-format.js +9 -2
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/root.d.ts +36 -1
- package/dist/router/root.js +54 -7
- package/dist/router/root.js.map +1 -1
- package/package.json +2 -2
- package/convex/cliTokens.ts +0 -63
- package/convex/sync.ts +0 -297
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createContentClient, createContentContext, queryError, validateLimit, validateSort, validateWhere, } from '@elytracms/core/content';
|
|
2
|
-
import { documentKey, resolveDocumentTitle
|
|
3
|
-
import { getStudioLocale } from '../locale/locale-store.js';
|
|
2
|
+
import { documentKey, resolveDocumentTitle } from '@elytracms/core/cms-core';
|
|
3
|
+
import { getStudioLocale, getStudioLocaleConfig } from '../locale/locale-store.js';
|
|
4
4
|
/**
|
|
5
5
|
* Default (no-session) collection schema: EMPTY. The studio ships no demo content
|
|
6
6
|
* (Oli, 2026-06-19) — with no backend/content session configured, there is nothing
|
|
@@ -12,7 +12,8 @@ import { getStudioLocale } from '../locale/locale-store.js';
|
|
|
12
12
|
export const playgroundCollections = [];
|
|
13
13
|
let contentSession = null;
|
|
14
14
|
let lookup = createLookup(playgroundCollections, [], []);
|
|
15
|
-
// Keyed by
|
|
15
|
+
// Keyed by locale + perspective (EC-300) + the resolved locale CONFIG (EC-479) —
|
|
16
|
+
// one deterministic client per distinct resolution context. See `clientFor`.
|
|
16
17
|
const clients = new Map();
|
|
17
18
|
// EC-460: a monotonically increasing session epoch + listener set, so a REACT
|
|
18
19
|
// surface (the canvas render context) can re-render when the session is
|
|
@@ -57,8 +58,40 @@ export function hasContentSession() {
|
|
|
57
58
|
function activeCollections() {
|
|
58
59
|
return contentSession?.collections ?? playgroundCollections;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The sentinel {@link previewLocale} yields when NOTHING has declared a locale —
|
|
63
|
+
* no ambient studio config, no content session. It is not a locale code and
|
|
64
|
+
* matches no stored key, which is the point: with no session the lookup is empty
|
|
65
|
+
* (`playgroundCollections` is `[]`), so every query already resolves to nothing
|
|
66
|
+
* and this only fills `CacheTags.locale`, which no studio surface reads. EC-479:
|
|
67
|
+
* the engine used to answer this question with its `sampleLocaleConfig` fixture's
|
|
68
|
+
* `en` — a locale nobody chose, standing in for the project's own.
|
|
69
|
+
*/
|
|
70
|
+
const NO_LOCALE = '';
|
|
71
|
+
/**
|
|
72
|
+
* The locale a canvas preview resolves in, when the caller names none: the
|
|
73
|
+
* ambient studio locale (installed from the PROJECT's config by `CmsProvider`),
|
|
74
|
+
* else the active content session's own declared default (surfaces that render
|
|
75
|
+
* outside a CMS session — the Components view's previews — still resolve in the
|
|
76
|
+
* project's locale), else {@link NO_LOCALE}. Never an engine fixture.
|
|
77
|
+
*/
|
|
78
|
+
function previewLocale() {
|
|
79
|
+
return getStudioLocale() ?? contentSession?.localeConfig?.default ?? NO_LOCALE;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The locale set a preview resolves fallbacks against (EC-018): the session's own
|
|
83
|
+
* declaration, else the ambient studio config (the project's, via `elytra.config.ts`),
|
|
84
|
+
* else the single locale actually being asked for.
|
|
85
|
+
*
|
|
86
|
+
* That last step is a DERIVATION, not an invention — it says "the only locale we
|
|
87
|
+
* know of is the one you named", so `documentDefaultLocale` falls back to the
|
|
88
|
+
* document's own `defaultLocale` and then to `doc.values`. Before EC-479 this line
|
|
89
|
+
* read `?? sampleLocaleConfig`, which silently declared every locale-less studio
|
|
90
|
+
* bilingual `en/de` and resolved fallbacks through an `en` no project had chosen.
|
|
91
|
+
*/
|
|
92
|
+
function activeLocaleConfig(locale) {
|
|
93
|
+
return (contentSession?.localeConfig ??
|
|
94
|
+
getStudioLocaleConfig() ?? { default: locale, locales: [locale] });
|
|
62
95
|
}
|
|
63
96
|
/** Resolve a collection in the active content session (fixtures in playground). */
|
|
64
97
|
export function playgroundCollection(id) {
|
|
@@ -155,7 +188,12 @@ function createLookup(collectionDefs, documents, publishedKeys, assetRecords) {
|
|
|
155
188
|
* session changes.
|
|
156
189
|
*/
|
|
157
190
|
function clientFor(locale, perspective = 'published') {
|
|
158
|
-
const
|
|
191
|
+
const locales = activeLocaleConfig(locale);
|
|
192
|
+
// EC-479: the locale CONFIG is part of the client's identity now — it may come
|
|
193
|
+
// from the ambient studio store, which changes independently of the session the
|
|
194
|
+
// cache is cleared on, so keying by locale alone could serve a client built
|
|
195
|
+
// against a superseded locale set.
|
|
196
|
+
const key = `${locale}:${perspective}:${locales.default}|${locales.locales.join(',')}`;
|
|
159
197
|
const existing = clients.get(key);
|
|
160
198
|
if (existing)
|
|
161
199
|
return existing;
|
|
@@ -164,7 +202,7 @@ function clientFor(locale, perspective = 'published') {
|
|
|
164
202
|
context: createContentContext({
|
|
165
203
|
locale,
|
|
166
204
|
perspective,
|
|
167
|
-
locales
|
|
205
|
+
locales,
|
|
168
206
|
}),
|
|
169
207
|
// EC-291: the routes a rich-text link resolves against (config-sourced) — the
|
|
170
208
|
// studio-live half of the host link bake.
|
|
@@ -179,11 +217,11 @@ function clientFor(locale, perspective = 'published') {
|
|
|
179
217
|
* queries come back as structured errors — never thrown, never silently
|
|
180
218
|
* widened. Resolves in the studio's active locale unless one is passed.
|
|
181
219
|
*/
|
|
182
|
-
export function runCollectionListQuery(collectionId, query = {}, locale =
|
|
220
|
+
export function runCollectionListQuery(collectionId, query = {}, locale = previewLocale()) {
|
|
183
221
|
return clientFor(locale).listDocuments(collectionId, query);
|
|
184
222
|
}
|
|
185
223
|
/** Resolve one document (by id or slug) into delivery shape. */
|
|
186
|
-
export function getCollectionDocument(collectionId, idOrSlug, locale =
|
|
224
|
+
export function getCollectionDocument(collectionId, idOrSlug, locale = previewLocale()) {
|
|
187
225
|
return clientFor(locale).getDocument(collectionId, idOrSlug);
|
|
188
226
|
}
|
|
189
227
|
/**
|
|
@@ -194,7 +232,7 @@ export function getCollectionDocument(collectionId, idOrSlug, locale = getStudio
|
|
|
194
232
|
* Reusable's element) exactly as delivery renders them. `null` when the target is
|
|
195
233
|
* missing or not visible → the block degrades to its own empty state.
|
|
196
234
|
*/
|
|
197
|
-
export function resolveCanvasRelation(ref, locale =
|
|
235
|
+
export function resolveCanvasRelation(ref, locale = previewLocale(), perspective = 'published') {
|
|
198
236
|
return clientFor(locale, perspective).resolveRelation(ref);
|
|
199
237
|
}
|
|
200
238
|
/**
|
|
@@ -206,7 +244,7 @@ export function resolveCanvasRelation(ref, locale = getStudioLocale(), perspecti
|
|
|
206
244
|
* (structured, never thrown) or a non-list lookup yields `[]` → the block's empty
|
|
207
245
|
* state. Reuses the same `listDocuments` path as `runCollectionListQuery`.
|
|
208
246
|
*/
|
|
209
|
-
export function resolveCanvasListing(query, locale =
|
|
247
|
+
export function resolveCanvasListing(query, locale = previewLocale(), perspective = 'published') {
|
|
210
248
|
const result = clientFor(locale, perspective).listDocuments(query.collection, {
|
|
211
249
|
where: query.where,
|
|
212
250
|
...(query.sort ? { sort: query.sort } : {}),
|
|
@@ -230,9 +268,7 @@ export function validateCanvasListingQuery(query) {
|
|
|
230
268
|
const collection = playgroundCollection(query.collection);
|
|
231
269
|
if (!collection) {
|
|
232
270
|
return [
|
|
233
|
-
queryError('unknown-collection', `Collection "${query.collection}" is not defined.`, [
|
|
234
|
-
'collection',
|
|
235
|
-
]),
|
|
271
|
+
queryError('unknown-collection', `Collection "${query.collection}" is not defined.`, ['collection'], undefined, { key: 'content.queryError.unknownCollection', vars: { collection: query.collection } }),
|
|
236
272
|
];
|
|
237
273
|
}
|
|
238
274
|
return [
|
|
@@ -250,7 +286,7 @@ export function validateCanvasListingQuery(query) {
|
|
|
250
286
|
* no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses
|
|
251
287
|
* the same `resolveLink` path as the host bake, so both agree on the derived URL.
|
|
252
288
|
*/
|
|
253
|
-
export function resolveCanvasLink(ref, locale =
|
|
289
|
+
export function resolveCanvasLink(ref, locale = previewLocale(), perspective = 'published') {
|
|
254
290
|
return clientFor(locale, perspective).resolveLink(ref);
|
|
255
291
|
}
|
|
256
292
|
/** Published entries of a collection, labeled by the schema's title field. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-content.js","sourceRoot":"","sources":["../../../src/lib/data/collection-content.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createContentClient,\n createContentContext,\n queryError,\n validateLimit,\n validateSort,\n validateWhere,\n} from '@elytracms/core/content'\nimport type {\n ContentClient,\n ContentDocumentSource,\n ContentLookup,\n ContentQueryError,\n GetDocumentResult,\n ListDocumentsQuery,\n ListDocumentsResult,\n ResolvedDocument,\n} from '@elytracms/core/content'\nimport { documentKey, resolveDocumentTitle, sampleLocaleConfig } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { ListingQuery } from '@elytracms/core/runtime-renderer'\nimport { getStudioLocale } from '../locale/locale-store'\n\n/**\n * The studio's content delivery engine for canvas previews (EC-166).\n *\n * Repeating sections and template pages preview *delivery-shaped* documents —\n * the exact shape `@elytracms/core/content` serves in production — so the canvas\n * shows what publishing will show. This module wires the canonical content\n * client (closed AD-3 query vocabulary, published perspective, EC-018 locale\n * fallback, depth-1 population, resolved assets) over a **content session**:\n *\n * - Convex mode: the project-session gate (`components/studio/project-session`)\n * configures the session with the project's HYDRATED schema + documents, so\n * \"Preview with\" lists and section previews show the live content — never\n * fixture titles (EC-177 gap 1).\n * - Playground mode (EC-361): the SAME hydration loader runs over the seeded\n * in-memory adapter (through operations — AD-17), so canvas listing/relation\n * population resolves from the in-memory session documents with identical\n * semantics. With no session configured (tests, boot instants, no persisted\n * content) the engine serves nothing.\n *\n * Nothing here invents query semantics; both modes go through the same\n * canonical client.\n */\n\n/** The schema + documents the preview engine serves (one project session). */\nexport interface ContentSessionState {\n collections: readonly CollectionDef[]\n documents: readonly CmsDocument[]\n /**\n * Published document keys via the version POINTER (EC-224). When present, the\n * preview's published perspective resolves by the pin — never the flat\n * `state` enum — so it matches what delivery serves. Absent (playground\n * fixtures / tests) → the rows' own `state` gates the perspective, unchanged.\n */\n publishedKeys?: readonly string[]\n /** Locale config of the session content; defaults to the sample config. */\n localeConfig?: LocaleConfig\n /**\n * The project's config-sourced routes (EC-207). Wired into the content client so\n * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from\n * the SAME routes delivery serves — the studio-live half of the host link bake.\n * Absent (playground/tests) → no routes, so `resolveLink` returns null and a\n * reference link degrades to the visible `unresolved-ref` issue.\n */\n routes?: readonly RouteRecord[]\n /**\n * The project's asset records (assets table). Wired into the lookup so the\n * content client resolves `asset`-typed FIELDS on listing/relation results\n * (e.g. a ProductGrid product's `gallery`) to delivery shape — the same assets\n * the renderer's `resolveAsset` reads for static props (EC-262). Absent\n * (playground/tests) → assets come only from `asset`-collection fixture docs.\n */\n assets?: readonly AssetRecord[]\n}\n\n/**\n * Default (no-session) collection schema: EMPTY. The studio ships no demo content\n * (Oli, 2026-06-19) — with no backend/content session configured, there is nothing\n * to preview, and the studio says so rather than serving fixtures. A real project's\n * collections + documents arrive via {@link configureContentSession} in BOTH modes\n * (EC-361): Convex hydration, or the playground's seeded in-memory adapter through\n * the same loader.\n */\nexport const playgroundCollections: readonly CollectionDef[] = []\n\n/**\n * The perspective a canvas preview resolves references against (EC-300/AD-16). The\n * canvas is draft-coherent: the editable Draft view resolves references to their\n * `'draft'` targets (so a just-made reusable shows immediately), the read-only\n * Published view resolves `'published'` (an honest preview of what delivery serves;\n * an unpublished hard ref shows the in-canvas placeholder). Delivery stays\n * published-only — this does not weaken `live-is-published-only`.\n */\nexport type CanvasPerspective = 'draft' | 'published'\n\nlet contentSession: ContentSessionState | null = null\nlet lookup: ContentLookup = createLookup(playgroundCollections, [], [])\n// Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.\nconst clients = new Map<string, ContentClient>()\n\n// EC-460: a monotonically increasing session epoch + listener set, so a REACT\n// surface (the canvas render context) can re-render when the session is\n// re-pointed at fresher content. The session itself stays a module global (the\n// EC-361 design); this only makes \"it changed\" observable — `configureContentSession`\n// remains the single setter.\nlet sessionEpoch = 0\nconst sessionListeners = new Set<() => void>()\n\n/** The current content-session epoch — bumps on every `configureContentSession`. */\nexport function contentSessionEpoch(): number {\n return sessionEpoch\n}\n\n/**\n * Subscribe to content-session swaps (EC-460). The listener fires after every\n * `configureContentSession`; returns the unsubscribe. Shaped for\n * `useSyncExternalStore(subscribeContentSession, contentSessionEpoch)`.\n */\nexport function subscribeContentSession(listener: () => void): () => void {\n sessionListeners.add(listener)\n return () => sessionListeners.delete(listener)\n}\n\n/**\n * Point the preview engine at a project's hydrated content — Convex hydration\n * or the playground's in-memory persisted state (EC-361), both through the same\n * `loadHydratedCms` loader — or back at the empty no-session default (`null`).\n */\nexport function configureContentSession(next: ContentSessionState | null): void {\n contentSession = next\n lookup = next\n ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)\n : createLookup(playgroundCollections, [], [])\n clients.clear()\n sessionEpoch += 1\n for (const listener of [...sessionListeners]) listener()\n}\n\n/** Whether a hydrated content session is configured (Convex mode). */\nexport function hasContentSession(): boolean {\n return contentSession !== null\n}\n\n/** The collections the preview engine currently serves (session or fixtures). */\nfunction activeCollections(): readonly CollectionDef[] {\n return contentSession?.collections ?? playgroundCollections\n}\n\nfunction activeLocaleConfig(): LocaleConfig {\n return contentSession?.localeConfig ?? sampleLocaleConfig\n}\n\n/** Resolve a collection in the active content session (fixtures in playground). */\nexport function playgroundCollection(id: string): CollectionDef | undefined {\n return activeCollections().find((collection) => collection.id === id)\n}\n\n/**\n * Overlay the active session's canonical `filterable` declarations onto a\n * schema that doesn't carry any of its own. The inspector hands the Content\n * tab fixture collection defs; the filter/sort pickers must offer exactly the\n * EC-143 declaration set the delivery engine enforces, so both ends agree. A\n * collection that declares its own filterable fields is left untouched.\n */\nexport function withPlaygroundFilterable(\n collections: readonly CollectionDef[],\n): readonly CollectionDef[] {\n return collections.map((collection) => {\n if (collection.fields.some((field) => field.filterable === true)) return collection\n const counterpart = playgroundCollection(collection.id)\n if (counterpart === undefined) return collection\n const flagged = new Set(\n counterpart.fields.filter((field) => field.filterable === true).map((field) => field.name),\n )\n if (flagged.size === 0) return collection\n return {\n ...collection,\n fields: collection.fields.map((field) =>\n flagged.has(field.name) ? { ...field, filterable: true } : field,\n ),\n }\n })\n}\n\n/** Synthesize the persistence-shaped asset record the asset resolver reads. */\nfunction assetRecordOf(doc: CmsDocument): AssetRecord {\n const values = doc.values\n const alt = doc.localized?.en?.alt\n return {\n id: doc.id,\n projectId: 'playground',\n filename: typeof values.filename === 'string' ? values.filename : doc.id,\n contentType: 'image/png',\n byteSize: 0,\n alt: typeof alt === 'string' ? alt : undefined,\n storageKey: typeof values.url === 'string' ? values.url : doc.id,\n createdAt: '2026-01-01T00:00:00.000Z',\n updatedAt: '2026-01-01T00:00:00.000Z',\n }\n}\n\nfunction createLookup(\n collectionDefs: readonly CollectionDef[],\n documents: readonly CmsDocument[],\n publishedKeys?: readonly string[],\n assetRecords?: readonly AssetRecord[],\n): ContentLookup {\n const collections = new Map(collectionDefs.map((c) => [c.id, c]))\n // EC-224: published-ness is the version POINTER, not the flat `state` enum.\n // A hydrated session hands `publishedKeys` (the pinned set) → wrap each row as\n // a `DocumentHistory` so the published perspective resolves by the pin (the\n // live row is always a draft now) with ZERO dependence on `state`. Without a\n // pointer set (playground fixtures / tests) we keep bare rows, whose own\n // `state` gates the published perspective — byte-identical to before.\n const published = publishedKeys ? new Set(publishedKeys) : null\n const toSource = (doc: CmsDocument): ContentDocumentSource => {\n if (!published) return doc\n return published.has(documentKey({ collection: doc.collection, id: doc.id }))\n ? { draft: doc, published: doc, versions: [] }\n : { draft: doc, published: undefined, versions: [] }\n }\n const byKey = new Map(\n documents.map((doc) => [`${doc.collection}:${doc.id}`, toSource(doc)] as const),\n )\n // Asset resolution source for `asset`-typed FIELDS on resolved documents\n // (listing/relation results, e.g. a product's `gallery`). Two sources, real\n // records last so they win: (1) `asset`-collection fixture documents — how\n // playground/tests model assets; (2) the project's real asset-table records\n // (EC-262) — a Convex session has its assets here, NOT as `asset` documents,\n // so without these the content client resolves no nested image.\n const assets = new Map<string, AssetRecord>()\n for (const doc of documents) {\n if (doc.collection === 'asset') assets.set(doc.id, assetRecordOf(doc))\n }\n for (const record of assetRecords ?? []) assets.set(record.id, record)\n return {\n collection: (id) => collections.get(id),\n document: (ref) => byKey.get(`${ref.collection}:${ref.id}`),\n documents: (collectionId) =>\n documents.filter((doc) => doc.collection === collectionId).map(toSource),\n asset: (id) => assets.get(id),\n // Prefer the record's resolved servable `url` — the one the renderer's\n // resolveAsset reads (EC-262). Real asset-table records carry a Convex blob\n // locator in `storageKey` and the servable URL in `url`; fixture/`asset`-document\n // records (assetRecordOf) leave `url` unset and store the servable url in\n // `storageKey`. Falling back keeps both correct.\n assetUrl: (asset) => asset.url ?? asset.storageKey,\n }\n}\n\n/**\n * One deterministic client per (locale, perspective): EC-018 locale fallback,\n * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's\n * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the\n * canvas previews follow the locale switcher exactly like delivery would; the canvas's\n * Draft/Published view picks the perspective. The cache resets whenever the content\n * session changes.\n */\nfunction clientFor(locale: Locale, perspective: CanvasPerspective = 'published'): ContentClient {\n const key = `${locale}:${perspective}`\n const existing = clients.get(key)\n if (existing) return existing\n const client = createContentClient({\n lookup,\n context: createContentContext({\n locale,\n perspective,\n locales: activeLocaleConfig(),\n }),\n // EC-291: the routes a rich-text link resolves against (config-sourced) — the\n // studio-live half of the host link bake.\n routes: contentSession?.routes ?? [],\n })\n clients.set(key, client)\n return client\n}\n\n/**\n * Run a closed-vocabulary list query (AD-3: eq/in/lt/lte/gt/gte, AND-only,\n * filterable fields only) and return the delivery-shaped result. Invalid\n * queries come back as structured errors — never thrown, never silently\n * widened. Resolves in the studio's active locale unless one is passed.\n */\nexport function runCollectionListQuery(\n collectionId: string,\n query: ListDocumentsQuery = {},\n locale: Locale = getStudioLocale(),\n): ListDocumentsResult {\n return clientFor(locale).listDocuments(collectionId, query)\n}\n\n/** Resolve one document (by id or slug) into delivery shape. */\nexport function getCollectionDocument(\n collectionId: string,\n idOrSlug: string,\n locale: Locale = getStudioLocale(),\n): GetDocumentResult {\n return clientFor(locale).getDocument(collectionId, idOrSlug)\n}\n\n/**\n * Populate a composition-node `relation` prop to delivery shape for the canvas\n * (EC-254) — the studio analog of the host's baked relations map. Resolves the\n * ref through the active session's client (published perspective, active locale),\n * so the canvas previews a block's picked documents (a ProductGrid's products, a\n * Reusable's element) exactly as delivery renders them. `null` when the target is\n * missing or not visible → the block degrades to its own empty state.\n */\nexport function resolveCanvasRelation(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument | null {\n return clientFor(locale, perspective).resolveRelation(ref)\n}\n\n/**\n * Resolve a listing block's query to its documents for the canvas (EC-255) — the\n * studio analog of the host's baked listings map. Runs the query (collection +\n * AND-only filter, optional sort/limit) through the active session's client\n * (published perspective, active locale), so the canvas previews a dynamic grid (a\n * ProductGrid keyed to a category) exactly as delivery renders it. An invalid query\n * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty\n * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.\n */\nexport function resolveCanvasListing(\n query: ListingQuery,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument[] {\n const result = clientFor(locale, perspective).listDocuments(query.collection, {\n where: query.where,\n ...(query.sort ? { sort: query.sort } : {}),\n ...(query.limit !== undefined ? { limit: query.limit } : {}),\n })\n return result.ok ? result.documents : []\n}\n\n/**\n * Statically validate a listing block's query against the ACTIVE session schema\n * (EC-460, the never-silent rule): the exact `validateWhere`/`validateSort`/\n * `validateLimit` checks `listDocuments` runs, WITHOUT executing the query — so\n * the canvas can surface a config-invalid listing (a filter/sort field the\n * collection doesn't declare `filterable`, an unknown collection) as a visible\n * notice instead of the block's indistinguishable empty state. With no content\n * session configured (boot instants, fixtures) there is no schema to judge\n * against, so it reports nothing rather than false-flagging every listing.\n */\nexport function validateCanvasListingQuery(query: ListingQuery): ContentQueryError[] {\n if (contentSession === null) return []\n const collection = playgroundCollection(query.collection)\n if (!collection) {\n return [\n queryError('unknown-collection', `Collection \"${query.collection}\" is not defined.`, [\n 'collection',\n ]),\n ]\n }\n return [\n ...validateWhere(collection, query.where),\n ...(query.sort ? validateSort(collection, query.sort) : []),\n ...(query.limit !== undefined ? validateLimit(query.limit) : []),\n ]\n}\n\n/**\n * Resolve a rich-text internal link's target document to its URL path for the canvas\n * (EC-291) — the studio analog of the host's baked links map. Derives the URL through\n * the active session's client (published perspective, active locale, config-sourced\n * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as\n * delivery serves it. `null` when the target is unpublished, missing, or addressed by\n * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses\n * the same `resolveLink` path as the host bake, so both agree on the derived URL.\n */\nexport function resolveCanvasLink(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): string | null {\n return clientFor(locale, perspective).resolveLink(ref)\n}\n\n/** A pickable entry of a collection (the template-page preview switcher). */\nexport interface CollectionEntrySummary {\n id: string\n label: string\n}\n\n/** Published entries of a collection, labeled by the schema's title field. */\nexport function listCollectionEntries(collectionId: string): CollectionEntrySummary[] {\n const collection = playgroundCollection(collectionId)\n const result = runCollectionListQuery(collectionId)\n if (!result.ok) return []\n // EC-242: label via the shared identity resolver (string `titleField` or a\n // function pattern), over the delivery-shape values; falls back to the id.\n return result.documents.map((doc) => ({\n id: doc.id,\n label: collection ? resolveDocumentTitle(collection, doc, doc.id) : doc.id,\n }))\n}\n"]}
|
|
1
|
+
{"version":3,"file":"collection-content.js","sourceRoot":"","sources":["../../../src/lib/data/collection-content.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createContentClient,\n createContentContext,\n queryError,\n validateLimit,\n validateSort,\n validateWhere,\n} from '@elytracms/core/content'\nimport type {\n ContentClient,\n ContentDocumentSource,\n ContentLookup,\n ContentQueryError,\n GetDocumentResult,\n ListDocumentsQuery,\n ListDocumentsResult,\n ResolvedDocument,\n} from '@elytracms/core/content'\nimport { documentKey, resolveDocumentTitle } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { ListingQuery } from '@elytracms/core/runtime-renderer'\nimport { getStudioLocale, getStudioLocaleConfig } from '../locale/locale-store'\n\n/**\n * The studio's content delivery engine for canvas previews (EC-166).\n *\n * Repeating sections and template pages preview *delivery-shaped* documents —\n * the exact shape `@elytracms/core/content` serves in production — so the canvas\n * shows what publishing will show. This module wires the canonical content\n * client (closed AD-3 query vocabulary, published perspective, EC-018 locale\n * fallback, depth-1 population, resolved assets) over a **content session**:\n *\n * - Convex mode: the project-session gate (`components/studio/project-session`)\n * configures the session with the project's HYDRATED schema + documents, so\n * \"Preview with\" lists and section previews show the live content — never\n * fixture titles (EC-177 gap 1).\n * - Playground mode (EC-361): the SAME hydration loader runs over the seeded\n * in-memory adapter (through operations — AD-17), so canvas listing/relation\n * population resolves from the in-memory session documents with identical\n * semantics. With no session configured (tests, boot instants, no persisted\n * content) the engine serves nothing.\n *\n * Nothing here invents query semantics; both modes go through the same\n * canonical client.\n */\n\n/** The schema + documents the preview engine serves (one project session). */\nexport interface ContentSessionState {\n collections: readonly CollectionDef[]\n documents: readonly CmsDocument[]\n /**\n * Published document keys via the version POINTER (EC-224). When present, the\n * preview's published perspective resolves by the pin — never the flat\n * `state` enum — so it matches what delivery serves. Absent (playground\n * fixtures / tests) → the rows' own `state` gates the perspective, unchanged.\n */\n publishedKeys?: readonly string[]\n /**\n * Locale config of the session content — the project's own, composed by\n * `contentSessionOf` from `elytra.config.ts`. Absent (a kit session, tests) →\n * the ambient studio config, then the single locale being asked for; EC-479\n * retired the engine's `sampleLocaleConfig` fallback here.\n */\n localeConfig?: LocaleConfig\n /**\n * The project's config-sourced routes (EC-207). Wired into the content client so\n * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from\n * the SAME routes delivery serves — the studio-live half of the host link bake.\n * Absent (playground/tests) → no routes, so `resolveLink` returns null and a\n * reference link degrades to the visible `unresolved-ref` issue.\n */\n routes?: readonly RouteRecord[]\n /**\n * The project's asset records (assets table). Wired into the lookup so the\n * content client resolves `asset`-typed FIELDS on listing/relation results\n * (e.g. a ProductGrid product's `gallery`) to delivery shape — the same assets\n * the renderer's `resolveAsset` reads for static props (EC-262). Absent\n * (playground/tests) → assets come only from `asset`-collection fixture docs.\n */\n assets?: readonly AssetRecord[]\n}\n\n/**\n * Default (no-session) collection schema: EMPTY. The studio ships no demo content\n * (Oli, 2026-06-19) — with no backend/content session configured, there is nothing\n * to preview, and the studio says so rather than serving fixtures. A real project's\n * collections + documents arrive via {@link configureContentSession} in BOTH modes\n * (EC-361): Convex hydration, or the playground's seeded in-memory adapter through\n * the same loader.\n */\nexport const playgroundCollections: readonly CollectionDef[] = []\n\n/**\n * The perspective a canvas preview resolves references against (EC-300/AD-16). The\n * canvas is draft-coherent: the editable Draft view resolves references to their\n * `'draft'` targets (so a just-made reusable shows immediately), the read-only\n * Published view resolves `'published'` (an honest preview of what delivery serves;\n * an unpublished hard ref shows the in-canvas placeholder). Delivery stays\n * published-only — this does not weaken `live-is-published-only`.\n */\nexport type CanvasPerspective = 'draft' | 'published'\n\nlet contentSession: ContentSessionState | null = null\nlet lookup: ContentLookup = createLookup(playgroundCollections, [], [])\n// Keyed by locale + perspective (EC-300) + the resolved locale CONFIG (EC-479) —\n// one deterministic client per distinct resolution context. See `clientFor`.\nconst clients = new Map<string, ContentClient>()\n\n// EC-460: a monotonically increasing session epoch + listener set, so a REACT\n// surface (the canvas render context) can re-render when the session is\n// re-pointed at fresher content. The session itself stays a module global (the\n// EC-361 design); this only makes \"it changed\" observable — `configureContentSession`\n// remains the single setter.\nlet sessionEpoch = 0\nconst sessionListeners = new Set<() => void>()\n\n/** The current content-session epoch — bumps on every `configureContentSession`. */\nexport function contentSessionEpoch(): number {\n return sessionEpoch\n}\n\n/**\n * Subscribe to content-session swaps (EC-460). The listener fires after every\n * `configureContentSession`; returns the unsubscribe. Shaped for\n * `useSyncExternalStore(subscribeContentSession, contentSessionEpoch)`.\n */\nexport function subscribeContentSession(listener: () => void): () => void {\n sessionListeners.add(listener)\n return () => sessionListeners.delete(listener)\n}\n\n/**\n * Point the preview engine at a project's hydrated content — Convex hydration\n * or the playground's in-memory persisted state (EC-361), both through the same\n * `loadHydratedCms` loader — or back at the empty no-session default (`null`).\n */\nexport function configureContentSession(next: ContentSessionState | null): void {\n contentSession = next\n lookup = next\n ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)\n : createLookup(playgroundCollections, [], [])\n clients.clear()\n sessionEpoch += 1\n for (const listener of [...sessionListeners]) listener()\n}\n\n/** Whether a hydrated content session is configured (Convex mode). */\nexport function hasContentSession(): boolean {\n return contentSession !== null\n}\n\n/** The collections the preview engine currently serves (session or fixtures). */\nfunction activeCollections(): readonly CollectionDef[] {\n return contentSession?.collections ?? playgroundCollections\n}\n\n/**\n * The sentinel {@link previewLocale} yields when NOTHING has declared a locale —\n * no ambient studio config, no content session. It is not a locale code and\n * matches no stored key, which is the point: with no session the lookup is empty\n * (`playgroundCollections` is `[]`), so every query already resolves to nothing\n * and this only fills `CacheTags.locale`, which no studio surface reads. EC-479:\n * the engine used to answer this question with its `sampleLocaleConfig` fixture's\n * `en` — a locale nobody chose, standing in for the project's own.\n */\nconst NO_LOCALE = ''\n\n/**\n * The locale a canvas preview resolves in, when the caller names none: the\n * ambient studio locale (installed from the PROJECT's config by `CmsProvider`),\n * else the active content session's own declared default (surfaces that render\n * outside a CMS session — the Components view's previews — still resolve in the\n * project's locale), else {@link NO_LOCALE}. Never an engine fixture.\n */\nfunction previewLocale(): Locale {\n return getStudioLocale() ?? contentSession?.localeConfig?.default ?? NO_LOCALE\n}\n\n/**\n * The locale set a preview resolves fallbacks against (EC-018): the session's own\n * declaration, else the ambient studio config (the project's, via `elytra.config.ts`),\n * else the single locale actually being asked for.\n *\n * That last step is a DERIVATION, not an invention — it says \"the only locale we\n * know of is the one you named\", so `documentDefaultLocale` falls back to the\n * document's own `defaultLocale` and then to `doc.values`. Before EC-479 this line\n * read `?? sampleLocaleConfig`, which silently declared every locale-less studio\n * bilingual `en/de` and resolved fallbacks through an `en` no project had chosen.\n */\nfunction activeLocaleConfig(locale: Locale): LocaleConfig {\n return (\n contentSession?.localeConfig ??\n getStudioLocaleConfig() ?? { default: locale, locales: [locale] }\n )\n}\n\n/** Resolve a collection in the active content session (fixtures in playground). */\nexport function playgroundCollection(id: string): CollectionDef | undefined {\n return activeCollections().find((collection) => collection.id === id)\n}\n\n/**\n * Overlay the active session's canonical `filterable` declarations onto a\n * schema that doesn't carry any of its own. The inspector hands the Content\n * tab fixture collection defs; the filter/sort pickers must offer exactly the\n * EC-143 declaration set the delivery engine enforces, so both ends agree. A\n * collection that declares its own filterable fields is left untouched.\n */\nexport function withPlaygroundFilterable(\n collections: readonly CollectionDef[],\n): readonly CollectionDef[] {\n return collections.map((collection) => {\n if (collection.fields.some((field) => field.filterable === true)) return collection\n const counterpart = playgroundCollection(collection.id)\n if (counterpart === undefined) return collection\n const flagged = new Set(\n counterpart.fields.filter((field) => field.filterable === true).map((field) => field.name),\n )\n if (flagged.size === 0) return collection\n return {\n ...collection,\n fields: collection.fields.map((field) =>\n flagged.has(field.name) ? { ...field, filterable: true } : field,\n ),\n }\n })\n}\n\n/** Synthesize the persistence-shaped asset record the asset resolver reads. */\nfunction assetRecordOf(doc: CmsDocument): AssetRecord {\n const values = doc.values\n const alt = doc.localized?.en?.alt\n return {\n id: doc.id,\n projectId: 'playground',\n filename: typeof values.filename === 'string' ? values.filename : doc.id,\n contentType: 'image/png',\n byteSize: 0,\n alt: typeof alt === 'string' ? alt : undefined,\n storageKey: typeof values.url === 'string' ? values.url : doc.id,\n createdAt: '2026-01-01T00:00:00.000Z',\n updatedAt: '2026-01-01T00:00:00.000Z',\n }\n}\n\nfunction createLookup(\n collectionDefs: readonly CollectionDef[],\n documents: readonly CmsDocument[],\n publishedKeys?: readonly string[],\n assetRecords?: readonly AssetRecord[],\n): ContentLookup {\n const collections = new Map(collectionDefs.map((c) => [c.id, c]))\n // EC-224: published-ness is the version POINTER, not the flat `state` enum.\n // A hydrated session hands `publishedKeys` (the pinned set) → wrap each row as\n // a `DocumentHistory` so the published perspective resolves by the pin (the\n // live row is always a draft now) with ZERO dependence on `state`. Without a\n // pointer set (playground fixtures / tests) we keep bare rows, whose own\n // `state` gates the published perspective — byte-identical to before.\n const published = publishedKeys ? new Set(publishedKeys) : null\n const toSource = (doc: CmsDocument): ContentDocumentSource => {\n if (!published) return doc\n return published.has(documentKey({ collection: doc.collection, id: doc.id }))\n ? { draft: doc, published: doc, versions: [] }\n : { draft: doc, published: undefined, versions: [] }\n }\n const byKey = new Map(\n documents.map((doc) => [`${doc.collection}:${doc.id}`, toSource(doc)] as const),\n )\n // Asset resolution source for `asset`-typed FIELDS on resolved documents\n // (listing/relation results, e.g. a product's `gallery`). Two sources, real\n // records last so they win: (1) `asset`-collection fixture documents — how\n // playground/tests model assets; (2) the project's real asset-table records\n // (EC-262) — a Convex session has its assets here, NOT as `asset` documents,\n // so without these the content client resolves no nested image.\n const assets = new Map<string, AssetRecord>()\n for (const doc of documents) {\n if (doc.collection === 'asset') assets.set(doc.id, assetRecordOf(doc))\n }\n for (const record of assetRecords ?? []) assets.set(record.id, record)\n return {\n collection: (id) => collections.get(id),\n document: (ref) => byKey.get(`${ref.collection}:${ref.id}`),\n documents: (collectionId) =>\n documents.filter((doc) => doc.collection === collectionId).map(toSource),\n asset: (id) => assets.get(id),\n // Prefer the record's resolved servable `url` — the one the renderer's\n // resolveAsset reads (EC-262). Real asset-table records carry a Convex blob\n // locator in `storageKey` and the servable URL in `url`; fixture/`asset`-document\n // records (assetRecordOf) leave `url` unset and store the servable url in\n // `storageKey`. Falling back keeps both correct.\n assetUrl: (asset) => asset.url ?? asset.storageKey,\n }\n}\n\n/**\n * One deterministic client per (locale, perspective): EC-018 locale fallback,\n * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's\n * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the\n * canvas previews follow the locale switcher exactly like delivery would; the canvas's\n * Draft/Published view picks the perspective. The cache resets whenever the content\n * session changes.\n */\nfunction clientFor(locale: Locale, perspective: CanvasPerspective = 'published'): ContentClient {\n const locales = activeLocaleConfig(locale)\n // EC-479: the locale CONFIG is part of the client's identity now — it may come\n // from the ambient studio store, which changes independently of the session the\n // cache is cleared on, so keying by locale alone could serve a client built\n // against a superseded locale set.\n const key = `${locale}:${perspective}:${locales.default}|${locales.locales.join(',')}`\n const existing = clients.get(key)\n if (existing) return existing\n const client = createContentClient({\n lookup,\n context: createContentContext({\n locale,\n perspective,\n locales,\n }),\n // EC-291: the routes a rich-text link resolves against (config-sourced) — the\n // studio-live half of the host link bake.\n routes: contentSession?.routes ?? [],\n })\n clients.set(key, client)\n return client\n}\n\n/**\n * Run a closed-vocabulary list query (AD-3: eq/in/lt/lte/gt/gte, AND-only,\n * filterable fields only) and return the delivery-shaped result. Invalid\n * queries come back as structured errors — never thrown, never silently\n * widened. Resolves in the studio's active locale unless one is passed.\n */\nexport function runCollectionListQuery(\n collectionId: string,\n query: ListDocumentsQuery = {},\n locale: Locale = previewLocale(),\n): ListDocumentsResult {\n return clientFor(locale).listDocuments(collectionId, query)\n}\n\n/** Resolve one document (by id or slug) into delivery shape. */\nexport function getCollectionDocument(\n collectionId: string,\n idOrSlug: string,\n locale: Locale = previewLocale(),\n): GetDocumentResult {\n return clientFor(locale).getDocument(collectionId, idOrSlug)\n}\n\n/**\n * Populate a composition-node `relation` prop to delivery shape for the canvas\n * (EC-254) — the studio analog of the host's baked relations map. Resolves the\n * ref through the active session's client (published perspective, active locale),\n * so the canvas previews a block's picked documents (a ProductGrid's products, a\n * Reusable's element) exactly as delivery renders them. `null` when the target is\n * missing or not visible → the block degrades to its own empty state.\n */\nexport function resolveCanvasRelation(\n ref: DocumentRef,\n locale: Locale = previewLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument | null {\n return clientFor(locale, perspective).resolveRelation(ref)\n}\n\n/**\n * Resolve a listing block's query to its documents for the canvas (EC-255) — the\n * studio analog of the host's baked listings map. Runs the query (collection +\n * AND-only filter, optional sort/limit) through the active session's client\n * (published perspective, active locale), so the canvas previews a dynamic grid (a\n * ProductGrid keyed to a category) exactly as delivery renders it. An invalid query\n * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty\n * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.\n */\nexport function resolveCanvasListing(\n query: ListingQuery,\n locale: Locale = previewLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument[] {\n const result = clientFor(locale, perspective).listDocuments(query.collection, {\n where: query.where,\n ...(query.sort ? { sort: query.sort } : {}),\n ...(query.limit !== undefined ? { limit: query.limit } : {}),\n })\n return result.ok ? result.documents : []\n}\n\n/**\n * Statically validate a listing block's query against the ACTIVE session schema\n * (EC-460, the never-silent rule): the exact `validateWhere`/`validateSort`/\n * `validateLimit` checks `listDocuments` runs, WITHOUT executing the query — so\n * the canvas can surface a config-invalid listing (a filter/sort field the\n * collection doesn't declare `filterable`, an unknown collection) as a visible\n * notice instead of the block's indistinguishable empty state. With no content\n * session configured (boot instants, fixtures) there is no schema to judge\n * against, so it reports nothing rather than false-flagging every listing.\n */\nexport function validateCanvasListingQuery(query: ListingQuery): ContentQueryError[] {\n if (contentSession === null) return []\n const collection = playgroundCollection(query.collection)\n if (!collection) {\n return [\n queryError(\n 'unknown-collection',\n `Collection \"${query.collection}\" is not defined.`,\n ['collection'],\n undefined,\n { key: 'content.queryError.unknownCollection', vars: { collection: query.collection } },\n ),\n ]\n }\n return [\n ...validateWhere(collection, query.where),\n ...(query.sort ? validateSort(collection, query.sort) : []),\n ...(query.limit !== undefined ? validateLimit(query.limit) : []),\n ]\n}\n\n/**\n * Resolve a rich-text internal link's target document to its URL path for the canvas\n * (EC-291) — the studio analog of the host's baked links map. Derives the URL through\n * the active session's client (published perspective, active locale, config-sourced\n * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as\n * delivery serves it. `null` when the target is unpublished, missing, or addressed by\n * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses\n * the same `resolveLink` path as the host bake, so both agree on the derived URL.\n */\nexport function resolveCanvasLink(\n ref: DocumentRef,\n locale: Locale = previewLocale(),\n perspective: CanvasPerspective = 'published',\n): string | null {\n return clientFor(locale, perspective).resolveLink(ref)\n}\n\n/** A pickable entry of a collection (the template-page preview switcher). */\nexport interface CollectionEntrySummary {\n id: string\n label: string\n}\n\n/** Published entries of a collection, labeled by the schema's title field. */\nexport function listCollectionEntries(collectionId: string): CollectionEntrySummary[] {\n const collection = playgroundCollection(collectionId)\n const result = runCollectionListQuery(collectionId)\n if (!result.ok) return []\n // EC-242: label via the shared identity resolver (string `titleField` or a\n // function pattern), over the delivery-shape values; falls back to the id.\n return result.documents.map((doc) => ({\n id: doc.id,\n label: collection ? resolveDocumentTitle(collection, doc, doc.id) : doc.id,\n }))\n}\n"]}
|
package/dist/lib/i18n/i18n.d.ts
CHANGED
|
@@ -13,24 +13,49 @@ interface ResolvedI18n {
|
|
|
13
13
|
active: MessageCatalog;
|
|
14
14
|
source: SourceCatalog;
|
|
15
15
|
}
|
|
16
|
-
type LanguagesResolver = () => readonly string[] | null | undefined;
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the
|
|
17
|
+
* Subscribe to the resolved UI locale, and — on the first subscription — switch browser
|
|
18
|
+
* detection on (EC-513).
|
|
19
|
+
*
|
|
20
|
+
* This is the whole post-hydration handover. `useSyncExternalStore` subscribes in a
|
|
21
|
+
* commit effect, i.e. **after** React has hydrated the server markup, so every render up
|
|
22
|
+
* to that point resolves the same default/pinned locale the server did. Flipping the flag
|
|
23
|
+
* changes the snapshot, React re-renders, and from then on `navigator.languages` decides.
|
|
24
|
+
*
|
|
25
|
+
* The flip is one-way and global rather than per-subscriber because the runtime is a
|
|
26
|
+
* per-boot singleton: 948 of the studio's ~956 localized render sites call the non-hook
|
|
27
|
+
* {@link translate} (measured 2026-08-01), so a per-hook flag would leave almost the whole
|
|
28
|
+
* chrome reading a locale nobody re-rendered for. The studio root subscribes once
|
|
29
|
+
* (`router/root.tsx`), which re-renders the tree exactly once when the handover happens.
|
|
30
|
+
*
|
|
31
|
+
* Exported (module-level only — deliberately not on the `lib/i18n` barrel) so the
|
|
32
|
+
* handover is testable without a React renderer, per the house test pattern.
|
|
25
33
|
*/
|
|
26
|
-
export declare function
|
|
34
|
+
export declare function subscribeToUiLocale(listener: () => void): () => void;
|
|
27
35
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
36
|
+
* Has React finished hydrating? `false` for every server render **and** for the
|
|
37
|
+
* hydration render itself; `true` from the first committed client render onwards.
|
|
38
|
+
*
|
|
39
|
+
* **Gate any localized text that can reach the prerendered shell on this** (EC-523).
|
|
40
|
+
* The studio builds as a static SPA, so `dist/client/_shell.html` is rendered at BUILD
|
|
41
|
+
* time: no request, no `Accept-Language`, no session. Every localized string baked into
|
|
42
|
+
* that file is therefore guaranteed wrong for some visitor — and that includes
|
|
43
|
+
* `aria-label` / `sr-only` text, because an accessible name is still a string and a
|
|
44
|
+
* screen-reader user cannot even notice it flip. The honest shape is to render no words
|
|
45
|
+
* at all until hydration, then announce in the visitor's real locale.
|
|
46
|
+
*
|
|
47
|
+
* Mechanism, and why it cannot itself cause a mismatch: `useSyncExternalStore` reads
|
|
48
|
+
* `getServerSnapshot` for the prerender *and* for the hydration render, so the two agree
|
|
49
|
+
* (a text mismatch makes React discard the whole tree — EC-513), then switches to
|
|
50
|
+
* `getSnapshot` in the commit that follows. The store never changes again, so `subscribe`
|
|
51
|
+
* is an inert module-level constant; a fresh closure per render would resubscribe on
|
|
52
|
+
* every render.
|
|
53
|
+
*
|
|
54
|
+
* Lives beside the EC-513 locale handover deliberately: both answer the same question —
|
|
55
|
+
* *is it safe to render locale-dependent output yet?* — and a contributor chasing "why is
|
|
56
|
+
* my string baked into the shell?" lands here.
|
|
32
57
|
*/
|
|
33
|
-
export declare function
|
|
58
|
+
export declare function useHydrated(): boolean;
|
|
34
59
|
/**
|
|
35
60
|
* Resolve the studio i18n from the config holder + the browser locale (EC-241).
|
|
36
61
|
*
|
|
@@ -69,7 +94,14 @@ export declare function resolveMsgLabel(value: unknown): string | undefined;
|
|
|
69
94
|
* {@link msgLabel} (the built-in rails) resolves through the message catalog.
|
|
70
95
|
*/
|
|
71
96
|
export declare function localizedLabel(value: LocalizedString | undefined, fallback?: string): string;
|
|
72
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* The active studio-UI locale — config-pinned, else browser-resolved.
|
|
99
|
+
*
|
|
100
|
+
* Subscribing (EC-513) is what makes the post-hydration handover visible: the hydration
|
|
101
|
+
* render gets the server's locale, and the component re-renders once when browser
|
|
102
|
+
* detection takes over. `router/root.tsx` calls this at the root so the whole chrome —
|
|
103
|
+
* including the ~948 non-hook {@link translate} sites — re-renders with it.
|
|
104
|
+
*/
|
|
73
105
|
export declare function useUiLocale(): string;
|
|
74
106
|
/** A `t(key, vars?)` translator bound to the active locale. */
|
|
75
107
|
export declare function useT(): (key: string, vars?: Record<string, string | number>) => string;
|
|
@@ -81,6 +113,9 @@ export declare function useT(): (key: string, vars?: Record<string, string | num
|
|
|
81
113
|
export declare function useLabel(): (value: LocalizedString | undefined, fallback?: string) => string;
|
|
82
114
|
/** Test seam: force the active locale. Never used by app code. */
|
|
83
115
|
export declare function setUiLocaleForTests(locale: string | null): void;
|
|
84
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Test seam: reset i18n state — once-per-boot warnings + the EC-513 browser-detection
|
|
118
|
+
* handover flag and its subscribers. Never used by app code.
|
|
119
|
+
*/
|
|
85
120
|
export declare function resetStudioI18nForTests(): void;
|
|
86
121
|
export {};
|
package/dist/lib/i18n/i18n.js
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
2
2
|
import { resolveLabel } from '@elytracms/core/cms-core';
|
|
3
3
|
import { resolveDeploymentI18n } from '@elytracms/studio/lib/workspace';
|
|
4
4
|
import { EN_MESSAGES } from './messages.en.js';
|
|
5
5
|
/**
|
|
6
|
-
* Studio-UI localization runtime (EC-241). Resolves the active UI locale
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* Studio-UI localization runtime (EC-241). Resolves the active UI locale, then
|
|
7
|
+
* translates message keys with an active-locale → English → visible-fallback chain.
|
|
8
|
+
*
|
|
9
|
+
* **Locale resolution is client-side** (EC-513, owner-ratified 2026-08-01: "the studio
|
|
10
|
+
* is primarily client-side and locales should be too"). Two mechanisms, in precedence
|
|
11
|
+
* order:
|
|
12
|
+
*
|
|
13
|
+
* 1. **`defineStudio({ i18n: { locale } })` — the supported way to fix the UI language.**
|
|
14
|
+
* A pin is read from *config*, which is imported identically on server and client, so
|
|
15
|
+
* SSR and the browser render the same locale with no header plumbing and no flash.
|
|
16
|
+
* Both production consumers pin `de`.
|
|
17
|
+
* 2. **Browser detection** (`navigator.languages`) when nothing is pinned. It is
|
|
18
|
+
* **deferred until after hydration** — see {@link subscribeToUiLocale}. The server
|
|
19
|
+
* has no `navigator`, so it renders the default/pinned locale; the first client
|
|
20
|
+
* render must produce byte-identical output or React discards the whole tree with a
|
|
21
|
+
* hydration error (measured EC-513 under `de-DE`: the auth gate's "Checking session…").
|
|
22
|
+
*
|
|
23
|
+
* There is deliberately **no server-side resolver seam**. `setServerLanguagesResolver` +
|
|
24
|
+
* `parseAcceptLanguage` shipped exported and unit-tested from EC-241 but were never
|
|
25
|
+
* called by anything in production — not the demo, not the scaffold template, neither
|
|
26
|
+
* consumer — and EC-241 itself had already measured that TanStack Start's request-header
|
|
27
|
+
* helper is unreachable from a normal SSR route. EC-513 deleted both.
|
|
28
|
+
*
|
|
29
|
+
* No persisted locale selector in v1 (that rides EC-240's deferred account settings).
|
|
12
30
|
*/
|
|
13
31
|
const BUILTIN_DEFAULT_LOCALE = 'en';
|
|
14
32
|
/**
|
|
@@ -28,50 +46,95 @@ export function resolveActiveLocale(available, preferred, defaultLocale) {
|
|
|
28
46
|
return defaultLocale;
|
|
29
47
|
}
|
|
30
48
|
let localeOverride = null;
|
|
31
|
-
|
|
49
|
+
// --- post-hydration browser-locale detection (EC-513) ---
|
|
50
|
+
let browserDetectionEnabled = false;
|
|
51
|
+
const localeListeners = new Set();
|
|
32
52
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
53
|
+
* Is `navigator` allowed to influence the resolved locale yet?
|
|
54
|
+
*
|
|
55
|
+
* In a **browser** it is not, until the first committed render of a locale consumer has
|
|
56
|
+
* run — the hydration render has to reproduce the server's markup exactly, and the
|
|
57
|
+
* server never sees `navigator`. Outside a browser (SSR, node tests) there is nothing
|
|
58
|
+
* to defer: `navigatorLanguages()` there is either genuinely absent (server → `[]`, the
|
|
59
|
+
* default locale, which is precisely what must be reproduced) or deliberately stubbed
|
|
60
|
+
* by a test.
|
|
40
61
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
function browserDetectionReady() {
|
|
63
|
+
return browserDetectionEnabled || typeof window === 'undefined';
|
|
43
64
|
}
|
|
44
65
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
66
|
+
* Subscribe to the resolved UI locale, and — on the first subscription — switch browser
|
|
67
|
+
* detection on (EC-513).
|
|
68
|
+
*
|
|
69
|
+
* This is the whole post-hydration handover. `useSyncExternalStore` subscribes in a
|
|
70
|
+
* commit effect, i.e. **after** React has hydrated the server markup, so every render up
|
|
71
|
+
* to that point resolves the same default/pinned locale the server did. Flipping the flag
|
|
72
|
+
* changes the snapshot, React re-renders, and from then on `navigator.languages` decides.
|
|
73
|
+
*
|
|
74
|
+
* The flip is one-way and global rather than per-subscriber because the runtime is a
|
|
75
|
+
* per-boot singleton: 948 of the studio's ~956 localized render sites call the non-hook
|
|
76
|
+
* {@link translate} (measured 2026-08-01), so a per-hook flag would leave almost the whole
|
|
77
|
+
* chrome reading a locale nobody re-rendered for. The studio root subscribes once
|
|
78
|
+
* (`router/root.tsx`), which re-renders the tree exactly once when the handover happens.
|
|
79
|
+
*
|
|
80
|
+
* Exported (module-level only — deliberately not on the `lib/i18n` barrel) so the
|
|
81
|
+
* handover is testable without a React renderer, per the house test pattern.
|
|
49
82
|
*/
|
|
50
|
-
export function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
export function subscribeToUiLocale(listener) {
|
|
84
|
+
localeListeners.add(listener);
|
|
85
|
+
if (!browserDetectionEnabled) {
|
|
86
|
+
browserDetectionEnabled = true;
|
|
87
|
+
for (const l of [...localeListeners])
|
|
88
|
+
l();
|
|
89
|
+
}
|
|
90
|
+
return () => {
|
|
91
|
+
localeListeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function uiLocaleSnapshot() {
|
|
95
|
+
return resolveStudioI18n().locale;
|
|
96
|
+
}
|
|
97
|
+
// --- post-hydration rendering gate (EC-523) ---
|
|
98
|
+
const noopUnsubscribe = () => { };
|
|
99
|
+
const subscribeNever = () => noopUnsubscribe;
|
|
100
|
+
const hydratedSnapshot = () => true;
|
|
101
|
+
const preHydrationSnapshot = () => false;
|
|
102
|
+
/**
|
|
103
|
+
* Has React finished hydrating? `false` for every server render **and** for the
|
|
104
|
+
* hydration render itself; `true` from the first committed client render onwards.
|
|
105
|
+
*
|
|
106
|
+
* **Gate any localized text that can reach the prerendered shell on this** (EC-523).
|
|
107
|
+
* The studio builds as a static SPA, so `dist/client/_shell.html` is rendered at BUILD
|
|
108
|
+
* time: no request, no `Accept-Language`, no session. Every localized string baked into
|
|
109
|
+
* that file is therefore guaranteed wrong for some visitor — and that includes
|
|
110
|
+
* `aria-label` / `sr-only` text, because an accessible name is still a string and a
|
|
111
|
+
* screen-reader user cannot even notice it flip. The honest shape is to render no words
|
|
112
|
+
* at all until hydration, then announce in the visitor's real locale.
|
|
113
|
+
*
|
|
114
|
+
* Mechanism, and why it cannot itself cause a mismatch: `useSyncExternalStore` reads
|
|
115
|
+
* `getServerSnapshot` for the prerender *and* for the hydration render, so the two agree
|
|
116
|
+
* (a text mismatch makes React discard the whole tree — EC-513), then switches to
|
|
117
|
+
* `getSnapshot` in the commit that follows. The store never changes again, so `subscribe`
|
|
118
|
+
* is an inert module-level constant; a fresh closure per render would resubscribe on
|
|
119
|
+
* every render.
|
|
120
|
+
*
|
|
121
|
+
* Lives beside the EC-513 locale handover deliberately: both answer the same question —
|
|
122
|
+
* *is it safe to render locale-dependent output yet?* — and a contributor chasing "why is
|
|
123
|
+
* my string baked into the shell?" lands here.
|
|
124
|
+
*/
|
|
125
|
+
export function useHydrated() {
|
|
126
|
+
return useSyncExternalStore(subscribeNever, hydratedSnapshot, preHydrationSnapshot);
|
|
64
127
|
}
|
|
65
128
|
function navigatorLanguages() {
|
|
129
|
+
if (!browserDetectionReady())
|
|
130
|
+
return [];
|
|
66
131
|
if (typeof navigator !== 'undefined') {
|
|
67
132
|
if (navigator.languages && navigator.languages.length > 0)
|
|
68
133
|
return [...navigator.languages];
|
|
69
134
|
if (navigator.language)
|
|
70
135
|
return [navigator.language];
|
|
71
136
|
}
|
|
72
|
-
|
|
73
|
-
const server = serverLanguagesResolver?.();
|
|
74
|
-
return server ? [...server] : [];
|
|
137
|
+
return [];
|
|
75
138
|
}
|
|
76
139
|
const warned = new Set();
|
|
77
140
|
function warnOnce(message) {
|
|
@@ -185,12 +248,20 @@ export function localizedLabel(value, fallback = '') {
|
|
|
185
248
|
return resolveLabel(value, locale, defaultLocale) ?? fallback;
|
|
186
249
|
}
|
|
187
250
|
// --- React hooks ---
|
|
188
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* The active studio-UI locale — config-pinned, else browser-resolved.
|
|
253
|
+
*
|
|
254
|
+
* Subscribing (EC-513) is what makes the post-hydration handover visible: the hydration
|
|
255
|
+
* render gets the server's locale, and the component re-renders once when browser
|
|
256
|
+
* detection takes over. `router/root.tsx` calls this at the root so the whole chrome —
|
|
257
|
+
* including the ~948 non-hook {@link translate} sites — re-renders with it.
|
|
258
|
+
*/
|
|
189
259
|
export function useUiLocale() {
|
|
190
|
-
return
|
|
260
|
+
return useSyncExternalStore(subscribeToUiLocale, uiLocaleSnapshot, uiLocaleSnapshot);
|
|
191
261
|
}
|
|
192
262
|
/** A `t(key, vars?)` translator bound to the active locale. */
|
|
193
263
|
export function useT() {
|
|
264
|
+
useUiLocale(); // re-render this component when the locale handover happens (EC-513)
|
|
194
265
|
return translate;
|
|
195
266
|
}
|
|
196
267
|
/**
|
|
@@ -199,7 +270,8 @@ export function useT() {
|
|
|
199
270
|
* a locale resolved before the mount seeded the i18n config.
|
|
200
271
|
*/
|
|
201
272
|
export function useLabel() {
|
|
202
|
-
const
|
|
273
|
+
const locale = useUiLocale(); // subscribes to the EC-513 post-hydration handover
|
|
274
|
+
const { defaultLocale } = resolveStudioI18n();
|
|
203
275
|
return useMemo(() => (value, fallback) => resolveMsgLabel(value) ?? resolveLabel(value, locale, defaultLocale) ?? fallback ?? '', [locale, defaultLocale]);
|
|
204
276
|
}
|
|
205
277
|
/** Test seam: force the active locale. Never used by app code. */
|
|
@@ -207,9 +279,13 @@ export function setUiLocaleForTests(locale) {
|
|
|
207
279
|
localeOverride = locale;
|
|
208
280
|
warned.clear();
|
|
209
281
|
}
|
|
210
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* Test seam: reset i18n state — once-per-boot warnings + the EC-513 browser-detection
|
|
284
|
+
* handover flag and its subscribers. Never used by app code.
|
|
285
|
+
*/
|
|
211
286
|
export function resetStudioI18nForTests() {
|
|
212
287
|
warned.clear();
|
|
213
|
-
|
|
288
|
+
browserDetectionEnabled = false;
|
|
289
|
+
localeListeners.clear();
|
|
214
290
|
}
|
|
215
291
|
//# sourceMappingURL=i18n.js.map
|