@elytracms/studio 0.0.31 → 0.0.33

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.
Files changed (116) hide show
  1. package/dist/components/app-views/default-modules.js +63 -21
  2. package/dist/components/app-views/default-modules.js.map +1 -1
  3. package/dist/components/assets/asset-library.js +21 -13
  4. package/dist/components/assets/asset-library.js.map +1 -1
  5. package/dist/components/assets/asset-thumb.js +3 -2
  6. package/dist/components/assets/asset-thumb.js.map +1 -1
  7. package/dist/components/backend-gate.js +4 -3
  8. package/dist/components/backend-gate.js.map +1 -1
  9. package/dist/components/builder/blocks-panel.js +11 -2
  10. package/dist/components/builder/blocks-panel.js.map +1 -1
  11. package/dist/components/builder/canvas.js +6 -5
  12. package/dist/components/builder/canvas.js.map +1 -1
  13. package/dist/components/builder/clipboard-actions.js +12 -6
  14. package/dist/components/builder/clipboard-actions.js.map +1 -1
  15. package/dist/components/builder/composition-field-editor.js +7 -4
  16. package/dist/components/builder/composition-field-editor.js.map +1 -1
  17. package/dist/components/builder/fixture-thumbnail.js +2 -1
  18. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  19. package/dist/components/builder/layout-inspector.d.ts +5 -5
  20. package/dist/components/builder/layout-inspector.js +21 -20
  21. package/dist/components/builder/layout-inspector.js.map +1 -1
  22. package/dist/components/builder/page-tree.js +5 -4
  23. package/dist/components/builder/page-tree.js.map +1 -1
  24. package/dist/components/builder/property-inspector.js +10 -10
  25. package/dist/components/builder/property-inspector.js.map +1 -1
  26. package/dist/components/builder/reuse-context.js +9 -9
  27. package/dist/components/builder/reuse-context.js.map +1 -1
  28. package/dist/components/builder/rich-text-toolbar.js +4 -3
  29. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  30. package/dist/components/builder/spacing-handles.js +4 -3
  31. package/dist/components/builder/spacing-handles.js.map +1 -1
  32. package/dist/components/cms/asset-picker.js +8 -7
  33. package/dist/components/cms/asset-picker.js.map +1 -1
  34. package/dist/components/cms/bulk-bar.js +8 -5
  35. package/dist/components/cms/bulk-bar.js.map +1 -1
  36. package/dist/components/cms/cell.js +9 -5
  37. package/dist/components/cms/cell.js.map +1 -1
  38. package/dist/components/cms/cms-store.js +4 -3
  39. package/dist/components/cms/cms-store.js.map +1 -1
  40. package/dist/components/cms/collection-table.js +18 -7
  41. package/dist/components/cms/collection-table.js.map +1 -1
  42. package/dist/components/cms/detail-editor.js +38 -27
  43. package/dist/components/cms/detail-editor.js.map +1 -1
  44. package/dist/components/cms/editable-cell.js +4 -4
  45. package/dist/components/cms/editable-cell.js.map +1 -1
  46. package/dist/components/cms/field-control.js +29 -21
  47. package/dist/components/cms/field-control.js.map +1 -1
  48. package/dist/components/cms/filter-builder.js +9 -8
  49. package/dist/components/cms/filter-builder.js.map +1 -1
  50. package/dist/components/cms/reference-confirm.js +24 -10
  51. package/dist/components/cms/reference-confirm.js.map +1 -1
  52. package/dist/components/cms/relation-picker.js +5 -4
  53. package/dist/components/cms/relation-picker.js.map +1 -1
  54. package/dist/components/cms/scalar-value-editor.js +5 -5
  55. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  56. package/dist/components/cms/version-compare.js +36 -21
  57. package/dist/components/cms/version-compare.js.map +1 -1
  58. package/dist/components/cms/versions-pane.js +8 -5
  59. package/dist/components/cms/versions-pane.js.map +1 -1
  60. package/dist/components/cms/view-bar.js +3 -2
  61. package/dist/components/cms/view-bar.js.map +1 -1
  62. package/dist/components/components/component-detail.js +3 -2
  63. package/dist/components/components/component-detail.js.map +1 -1
  64. package/dist/components/components/component-status.js +8 -13
  65. package/dist/components/components/component-status.js.map +1 -1
  66. package/dist/components/components/components-index.js +5 -1
  67. package/dist/components/components/components-index.js.map +1 -1
  68. package/dist/components/components/field-inspector.js +4 -4
  69. package/dist/components/components/field-inspector.js.map +1 -1
  70. package/dist/components/components/fixture-runner.js +4 -1
  71. package/dist/components/components/fixture-runner.js.map +1 -1
  72. package/dist/components/components/registry-browser.js +4 -3
  73. package/dist/components/components/registry-browser.js.map +1 -1
  74. package/dist/components/delivery-health-banner.js +2 -1
  75. package/dist/components/delivery-health-banner.js.map +1 -1
  76. package/dist/components/dev-login-badge.js +2 -1
  77. package/dist/components/dev-login-badge.js.map +1 -1
  78. package/dist/components/extensions/extensions-view.js +22 -21
  79. package/dist/components/extensions/extensions-view.js.map +1 -1
  80. package/dist/components/guards.js +3 -2
  81. package/dist/components/guards.js.map +1 -1
  82. package/dist/components/operation-error.js +3 -2
  83. package/dist/components/operation-error.js.map +1 -1
  84. package/dist/components/playground-badge.js +2 -1
  85. package/dist/components/playground-badge.js.map +1 -1
  86. package/dist/components/project-list.js +5 -4
  87. package/dist/components/project-list.js.map +1 -1
  88. package/dist/components/project-session.js +3 -2
  89. package/dist/components/project-session.js.map +1 -1
  90. package/dist/components/project-switcher.js +4 -3
  91. package/dist/components/project-switcher.js.map +1 -1
  92. package/dist/components/redirects/redirects-module.js +3 -2
  93. package/dist/components/redirects/redirects-module.js.map +1 -1
  94. package/dist/components/references/references-panel.js +7 -6
  95. package/dist/components/references/references-panel.js.map +1 -1
  96. package/dist/components/section-nav.js +8 -5
  97. package/dist/components/section-nav.js.map +1 -1
  98. package/dist/components/section-state.js +13 -32
  99. package/dist/components/section-state.js.map +1 -1
  100. package/dist/components/settings-form.js +2 -1
  101. package/dist/components/settings-form.js.map +1 -1
  102. package/dist/components/studio-project-screen.js +3 -2
  103. package/dist/components/studio-project-screen.js.map +1 -1
  104. package/dist/components/studio-shell.js +3 -2
  105. package/dist/components/studio-shell.js.map +1 -1
  106. package/dist/components/users/users-module.js +5 -4
  107. package/dist/components/users/users-module.js.map +1 -1
  108. package/dist/components/workspace-gate.js +2 -1
  109. package/dist/components/workspace-gate.js.map +1 -1
  110. package/dist/lib/i18n/i18n.d.ts +18 -4
  111. package/dist/lib/i18n/i18n.js +22 -16
  112. package/dist/lib/i18n/i18n.js.map +1 -1
  113. package/dist/lib/i18n/messages.en.d.ts +11 -4
  114. package/dist/lib/i18n/messages.en.js +636 -6
  115. package/dist/lib/i18n/messages.en.js.map +1 -1
  116. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"users-module.js","sourceRoot":"","sources":["../../../src/components/users/users-module.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, RotateCcw, ShieldAlert } from 'lucide-react'\nimport { Button, Heading, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { useProjectRole } from '../auth-context'\nimport { UsersProvider, useUsers } from './users-store'\nimport { useUsersCollectionViewSource } from './users-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { LoadedCollectionTable } from '../cms/collection-table'\nimport { PortableDetailEditor } from '../cms/detail-editor'\n\n/**\n * The studio Users module (EC-240). An admin-only section for managing who has\n * access to this deployment and their role. AD-19: it is the **reuse proof** for\n * the portable interaction surfaces — it renders the SAME `LoadedCollectionTable`\n * the CMS uses for the roster, and the SAME `DetailEditor` body (via\n * {@link PortableDetailEditor}) for a member's detail, both over a users\n * {@link useUsersCollectionViewSource} instead of a CMS source. No bespoke roster,\n * no bespoke editor: filtering, columns, the count, and the edit form all come\n * from the shared surfaces. The users capability profile (admin-only, no publish/\n * versions/create/import) drives which regions appear.\n *\n * `userId === null` is the roster; a `userId` opens that member's detail (routed\n * via `studioPath.user`). Both render under one `UsersProvider` + one source.\n *\n * Gating is layered: the operations chokepoint AND the Convex handlers enforce\n * admin-only server-side (a non-admin `users.list` is denied). This UI gate is UX\n * politeness — a non-admin sees a clear \"restricted\" state instead of a raw\n * authorization error, and we skip firing a query we know will be denied.\n */\nexport function UsersModule({\n projectId,\n userId = null,\n}: {\n projectId: string\n userId?: string | null\n}) {\n const role = useProjectRole(projectId)\n\n // `useProjectRole` conflates \"loading\" and \"not a member\" as null; in the studio\n // a member always reaches here, so null resolves to the real role momentarily.\n if (role === null) {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n Checking access…\n </Text>\n </Stack>\n )\n }\n if (role !== 'admin') return <RestrictedState />\n\n return (\n <UsersProvider projectId={projectId}>\n <UsersSurface projectId={projectId} userId={userId} />\n </UsersProvider>\n )\n}\n\n/** The non-admin gate: honest about why the surface is empty. */\nfunction RestrictedState() {\n return (\n <Stack gap=\"2\" align=\"center\" justify=\"center\" className=\"h-full p-8 text-center\">\n <ShieldAlert className=\"size-6 text-muted-foreground\" />\n <Heading level={2} size=\"h3\">\n Admins only\n </Heading>\n <Text size=\"sm\" tone=\"muted\" className=\"max-w-sm\">\n Managing users and roles requires the admin role on this deployment. Ask an admin to grant\n you access.\n </Text>\n </Stack>\n )\n}\n\n/** The loaded surface: the portable list (or a member's detail) over the users source. */\nfunction UsersSurface({ projectId, userId }: { projectId: string; userId: string | null }) {\n const store = useUsers()\n const source = useUsersCollectionViewSource(projectId)\n\n if (store.status === 'error') {\n return (\n <Stack gap=\"2\" className=\"p-6\" data-testid=\"users-error\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text weight=\"semibold\">Users failed to load</Text>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? 'The persistence backend is unreachable.'}\n </Text>\n <Row>\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> Retry\n </Button>\n </Row>\n </Stack>\n )\n }\n\n if (store.status === 'loading') {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n Loading users…\n </Text>\n </Stack>\n )\n }\n\n // A member's detail opens in the portable edit shell (AD-19); the list otherwise.\n // The shell mounts its own CollectionViewProvider, so only the list wraps here.\n if (userId !== null) {\n return <PortableDetailEditor source={source} projectId={projectId} documentId={userId} />\n }\n\n return (\n <CollectionViewProvider source={source}>\n <LoadedCollectionTable projectId={projectId} />\n </CollectionViewProvider>\n )\n}\n"]}
1
+ {"version":3,"file":"users-module.js","sourceRoot":"","sources":["../../../src/components/users/users-module.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, RotateCcw, ShieldAlert } from 'lucide-react'\nimport { Button, Heading, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useProjectRole } from '../auth-context'\nimport { UsersProvider, useUsers } from './users-store'\nimport { useUsersCollectionViewSource } from './users-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { LoadedCollectionTable } from '../cms/collection-table'\nimport { PortableDetailEditor } from '../cms/detail-editor'\n\n/**\n * The studio Users module (EC-240). An admin-only section for managing who has\n * access to this deployment and their role. AD-19: it is the **reuse proof** for\n * the portable interaction surfaces — it renders the SAME `LoadedCollectionTable`\n * the CMS uses for the roster, and the SAME `DetailEditor` body (via\n * {@link PortableDetailEditor}) for a member's detail, both over a users\n * {@link useUsersCollectionViewSource} instead of a CMS source. No bespoke roster,\n * no bespoke editor: filtering, columns, the count, and the edit form all come\n * from the shared surfaces. The users capability profile (admin-only, no publish/\n * versions/create/import) drives which regions appear.\n *\n * `userId === null` is the roster; a `userId` opens that member's detail (routed\n * via `studioPath.user`). Both render under one `UsersProvider` + one source.\n *\n * Gating is layered: the operations chokepoint AND the Convex handlers enforce\n * admin-only server-side (a non-admin `users.list` is denied). This UI gate is UX\n * politeness — a non-admin sees a clear \"restricted\" state instead of a raw\n * authorization error, and we skip firing a query we know will be denied.\n */\nexport function UsersModule({\n projectId,\n userId = null,\n}: {\n projectId: string\n userId?: string | null\n}) {\n const role = useProjectRole(projectId)\n\n // `useProjectRole` conflates \"loading\" and \"not a member\" as null; in the studio\n // a member always reaches here, so null resolves to the real role momentarily.\n if (role === null) {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('users.checkingAccess')}\n </Text>\n </Stack>\n )\n }\n if (role !== 'admin') return <RestrictedState />\n\n return (\n <UsersProvider projectId={projectId}>\n <UsersSurface projectId={projectId} userId={userId} />\n </UsersProvider>\n )\n}\n\n/** The non-admin gate: honest about why the surface is empty. */\nfunction RestrictedState() {\n return (\n <Stack gap=\"2\" align=\"center\" justify=\"center\" className=\"h-full p-8 text-center\">\n <ShieldAlert className=\"size-6 text-muted-foreground\" />\n <Heading level={2} size=\"h3\">\n {translate('users.restrictedTitle')}\n </Heading>\n <Text size=\"sm\" tone=\"muted\" className=\"max-w-sm\">\n {translate('users.restrictedBody')}\n </Text>\n </Stack>\n )\n}\n\n/** The loaded surface: the portable list (or a member's detail) over the users source. */\nfunction UsersSurface({ projectId, userId }: { projectId: string; userId: string | null }) {\n const store = useUsers()\n const source = useUsersCollectionViewSource(projectId)\n\n if (store.status === 'error') {\n return (\n <Stack gap=\"2\" className=\"p-6\" data-testid=\"users-error\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text weight=\"semibold\">{translate('users.loadFailed')}</Text>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('common.backendUnreachable')}\n </Text>\n <Row>\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> {translate('common.retry')}\n </Button>\n </Row>\n </Stack>\n )\n }\n\n if (store.status === 'loading') {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('users.loading')}\n </Text>\n </Stack>\n )\n }\n\n // A member's detail opens in the portable edit shell (AD-19); the list otherwise.\n // The shell mounts its own CollectionViewProvider, so only the list wraps here.\n if (userId !== null) {\n return <PortableDetailEditor source={source} projectId={projectId} documentId={userId} />\n }\n\n return (\n <CollectionViewProvider source={source}>\n <LoadedCollectionTable projectId={projectId} />\n </CollectionViewProvider>\n )\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui';
3
3
  import { getWorkspaceState } from '@elytracms/studio/lib/workspace';
4
+ import { translate } from '@elytracms/studio/lib/i18n';
4
5
  /**
5
6
  * Workspace-config boot gate (EC-154) — sibling of the EC-153 `BackendGate`.
6
7
  * The workspace config is static boot input (bundled `elytra.config.ts` or
@@ -16,7 +17,7 @@ export function WorkspaceGate({ children }) {
16
17
  return _jsx(_Fragment, { children: children });
17
18
  }
18
19
  function WorkspaceConfigErrorScreen({ workspace, }) {
19
- return (_jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: _jsxs(Card, { className: "max-w-2xl border-destructive/40", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Workspace config is invalid" }) }), _jsx(CardContent, { children: _jsxs(Stack, { gap: "3", children: [_jsxs(Text, { size: "sm", tone: "muted", children: ["The studio loaded a workspace config from ", _jsx(Code, { children: workspace.source }), ", but it failed validation. Fix the issues below and reload \u2014 the studio never falls back to unconfigured behavior while a config is present."] }), _jsx(Stack, { gap: "2", role: "list", "aria-label": "Workspace config issues", children: workspace.issues.map((issue, index) => (_jsx(WorkspaceIssueLine, { code: issue.code, message: issue.message, location: formatIssuePath(issue.path) }, `${issue.code}-${index}`))) }), _jsxs(Text, { size: "xs", tone: "muted", children: ["Validate locally with ", _jsx(Code, { children: "builder init --project <slug>" }), " \u2014 the CLI runs the same schema and reports the same issues."] })] }) })] }) }));
20
+ return (_jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: _jsxs(Card, { className: "max-w-2xl border-destructive/40", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: translate('shell.workspaceGate.title') }) }), _jsx(CardContent, { children: _jsxs(Stack, { gap: "3", children: [_jsxs(Text, { size: "sm", tone: "muted", children: ["The studio loaded a workspace config from ", _jsx(Code, { children: workspace.source }), ", but it failed validation. Fix the issues below and reload \u2014 the studio never falls back to unconfigured behavior while a config is present."] }), _jsx(Stack, { gap: "2", role: "list", "aria-label": translate('shell.workspaceGate.issuesLabel'), children: workspace.issues.map((issue, index) => (_jsx(WorkspaceIssueLine, { code: issue.code, message: issue.message, location: formatIssuePath(issue.path) }, `${issue.code}-${index}`))) }), _jsxs(Text, { size: "xs", tone: "muted", children: ["Validate locally with ", _jsx(Code, { children: "builder init --project <slug>" }), " \u2014 the CLI runs the same schema and reports the same issues."] })] }) })] }) }));
20
21
  }
21
22
  function WorkspaceIssueLine({ code, message, location, }) {
22
23
  return (_jsxs("div", { role: "listitem", className: "rounded-md border border-destructive/30 bg-destructive/5 p-2", children: [_jsxs(Text, { size: "xs", mono: true, className: "text-destructive", children: ["[", code, "]", location ? ` at ${location}` : ''] }), _jsx(Text, { size: "sm", children: message })] }));
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-gate.js","sourceRoot":"","sources":["../../src/components/workspace-gate.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAClG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AAGnE;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAA2B;IACjE,MAAM,SAAS,GAAG,iBAAiB,EAAE,CAAA;IACrC,IAAI,SAAS,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,KAAC,0BAA0B,IAAC,SAAS,EAAE,SAAS,GAAI,CAAA;IAC7F,OAAO,4BAAG,QAAQ,GAAI,CAAA;AACxB,CAAC;AAED,SAAS,0BAA0B,CAAC,EAClC,SAAS,GAGV;IACC,OAAO,CACL,cAAK,SAAS,EAAC,qDAAqD,YAClE,MAAC,IAAI,IAAC,SAAS,EAAC,iCAAiC,aAC/C,KAAC,UAAU,cACT,KAAC,SAAS,8CAAwC,GACvC,EACb,KAAC,WAAW,cACV,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,2DACgB,KAAC,IAAI,cAAE,SAAS,CAAC,MAAM,GAAQ,0JAGpE,EACP,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,EAAC,MAAM,gBAAY,yBAAyB,YAC5D,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CACtC,KAAC,kBAAkB,IAEjB,IAAI,EAAE,KAAK,CAAC,IAAI,EAChB,OAAO,EAAE,KAAK,CAAC,OAAO,EACtB,QAAQ,EAAE,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,IAHhC,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,EAAE,CAI7B,CACH,CAAC,GACI,EACR,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,uCACJ,KAAC,IAAI,gDAA2C,yEAEjE,IACD,GACI,IACT,GACH,CACP,CAAA;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,EAC1B,IAAI,EACJ,OAAO,EACP,QAAQ,GAKT;IACC,OAAO,CACL,eAAK,IAAI,EAAC,UAAU,EAAC,SAAS,EAAC,8DAA8D,aAC3F,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,QAAC,SAAS,EAAC,kBAAkB,kBAC7C,IAAI,OAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IACrC,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,YAAE,OAAO,GAAQ,IAC5B,CACP,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAoC;IAC3D,OAAO,IAAI,CAAC,MAAM,CAChB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAC7F,EAAE,CACH,CAAA;AACH,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport { Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport { getWorkspaceState } from '@elytracms/studio/lib/workspace'\nimport type { WorkspaceState } from '@elytracms/studio/lib/workspace'\n\n/**\n * Workspace-config boot gate (EC-154) — sibling of the EC-153 `BackendGate`.\n * The workspace config is static boot input (bundled `elytra.config.ts` or\n * env-injected JSON), so an *invalid* config is a deterministic boot-error\n * state rendered before auth and the studio store: structured, file-located\n * issues — never a silent fall-back to unconfigured behavior. No config and a\n * valid config render children untouched.\n */\nexport function WorkspaceGate({ children }: { children: ReactNode }) {\n const workspace = getWorkspaceState()\n if (workspace.kind === 'invalid') return <WorkspaceConfigErrorScreen workspace={workspace} />\n return <>{children}</>\n}\n\nfunction WorkspaceConfigErrorScreen({\n workspace,\n}: {\n workspace: Extract<WorkspaceState, { kind: 'invalid' }>\n}) {\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background p-6\">\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>Workspace config is invalid</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n The studio loaded a workspace config from <Code>{workspace.source}</Code>, but it\n failed validation. Fix the issues below and reload — the studio never falls back to\n unconfigured behavior while a config is present.\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label=\"Workspace config issues\">\n {workspace.issues.map((issue, index) => (\n <WorkspaceIssueLine\n key={`${issue.code}-${index}`}\n code={issue.code}\n message={issue.message}\n location={formatIssuePath(issue.path)}\n />\n ))}\n </Stack>\n <Text size=\"xs\" tone=\"muted\">\n Validate locally with <Code>builder init --project &lt;slug&gt;</Code> — the CLI runs\n the same schema and reports the same issues.\n </Text>\n </Stack>\n </CardContent>\n </Card>\n </div>\n )\n}\n\nfunction WorkspaceIssueLine({\n code,\n message,\n location,\n}: {\n code: string\n message: string\n location: string\n}) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{code}]{location ? ` at ${location}` : ''}\n </Text>\n <Text size=\"sm\">{message}</Text>\n </div>\n )\n}\n\nfunction formatIssuePath(path: ReadonlyArray<string | number>): string {\n return path.reduce<string>(\n (acc, part) => (typeof part === 'number' ? `${acc}[${part}]` : acc ? `${acc}.${part}` : part),\n '',\n )\n}\n"]}
1
+ {"version":3,"file":"workspace-gate.js","sourceRoot":"","sources":["../../src/components/workspace-gate.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAClG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AAEnE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAA2B;IACjE,MAAM,SAAS,GAAG,iBAAiB,EAAE,CAAA;IACrC,IAAI,SAAS,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,KAAC,0BAA0B,IAAC,SAAS,EAAE,SAAS,GAAI,CAAA;IAC7F,OAAO,4BAAG,QAAQ,GAAI,CAAA;AACxB,CAAC;AAED,SAAS,0BAA0B,CAAC,EAClC,SAAS,GAGV;IACC,OAAO,CACL,cAAK,SAAS,EAAC,qDAAqD,YAClE,MAAC,IAAI,IAAC,SAAS,EAAC,iCAAiC,aAC/C,KAAC,UAAU,cACT,KAAC,SAAS,cAAE,SAAS,CAAC,2BAA2B,CAAC,GAAa,GACpD,EACb,KAAC,WAAW,cACV,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,2DACgB,KAAC,IAAI,cAAE,SAAS,CAAC,MAAM,GAAQ,0JAGpE,EACP,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,EAAC,MAAM,gBAAa,SAAS,CAAC,iCAAiC,CAAC,YAChF,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CACtC,KAAC,kBAAkB,IAEjB,IAAI,EAAE,KAAK,CAAC,IAAI,EAChB,OAAO,EAAE,KAAK,CAAC,OAAO,EACtB,QAAQ,EAAE,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,IAHhC,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,EAAE,CAI7B,CACH,CAAC,GACI,EACR,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,uCACJ,KAAC,IAAI,gDAA2C,yEAEjE,IACD,GACI,IACT,GACH,CACP,CAAA;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,EAC1B,IAAI,EACJ,OAAO,EACP,QAAQ,GAKT;IACC,OAAO,CACL,eAAK,IAAI,EAAC,UAAU,EAAC,SAAS,EAAC,8DAA8D,aAC3F,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,QAAC,SAAS,EAAC,kBAAkB,kBAC7C,IAAI,OAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IACrC,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,YAAE,OAAO,GAAQ,IAC5B,CACP,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAoC;IAC3D,OAAO,IAAI,CAAC,MAAM,CAChB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAC7F,EAAE,CACH,CAAA;AACH,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport { Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport { getWorkspaceState } from '@elytracms/studio/lib/workspace'\nimport type { WorkspaceState } from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Workspace-config boot gate (EC-154) — sibling of the EC-153 `BackendGate`.\n * The workspace config is static boot input (bundled `elytra.config.ts` or\n * env-injected JSON), so an *invalid* config is a deterministic boot-error\n * state rendered before auth and the studio store: structured, file-located\n * issues — never a silent fall-back to unconfigured behavior. No config and a\n * valid config render children untouched.\n */\nexport function WorkspaceGate({ children }: { children: ReactNode }) {\n const workspace = getWorkspaceState()\n if (workspace.kind === 'invalid') return <WorkspaceConfigErrorScreen workspace={workspace} />\n return <>{children}</>\n}\n\nfunction WorkspaceConfigErrorScreen({\n workspace,\n}: {\n workspace: Extract<WorkspaceState, { kind: 'invalid' }>\n}) {\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background p-6\">\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.workspaceGate.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n The studio loaded a workspace config from <Code>{workspace.source}</Code>, but it\n failed validation. Fix the issues below and reload — the studio never falls back to\n unconfigured behavior while a config is present.\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.workspaceGate.issuesLabel')}>\n {workspace.issues.map((issue, index) => (\n <WorkspaceIssueLine\n key={`${issue.code}-${index}`}\n code={issue.code}\n message={issue.message}\n location={formatIssuePath(issue.path)}\n />\n ))}\n </Stack>\n <Text size=\"xs\" tone=\"muted\">\n Validate locally with <Code>builder init --project &lt;slug&gt;</Code> — the CLI runs\n the same schema and reports the same issues.\n </Text>\n </Stack>\n </CardContent>\n </Card>\n </div>\n )\n}\n\nfunction WorkspaceIssueLine({\n code,\n message,\n location,\n}: {\n code: string\n message: string\n location: string\n}) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{code}]{location ? ` at ${location}` : ''}\n </Text>\n <Text size=\"sm\">{message}</Text>\n </div>\n )\n}\n\nfunction formatIssuePath(path: ReadonlyArray<string | number>): string {\n return path.reduce<string>(\n (acc, part) => (typeof part === 'number' ? `${acc}[${part}]` : acc ? `${acc}.${part}` : part),\n '',\n )\n}\n"]}
@@ -13,7 +13,17 @@ interface ResolvedI18n {
13
13
  active: MessageCatalog;
14
14
  source: SourceCatalog;
15
15
  }
16
- /** Resolve the studio i18n once per boot (config static; locale from the browser). */
16
+ /**
17
+ * Resolve the studio i18n from the config holder + the browser locale (EC-241).
18
+ *
19
+ * NOT cached: the studio config holder (`createStudio`) is seeded at the mount, which
20
+ * runs AFTER the consumer's `studio.config` module built its sidebar — so an eager
21
+ * `translate` during that sidebar build (e.g. a built-in view label) sees an empty
22
+ * config. Caching the first result would poison every later render-time call with
23
+ * that empty-config English. Re-resolving each call is cheap (a few array ops) and
24
+ * always reflects the config once the mount has run. `translate`/`localizedLabel`
25
+ * are the hot callers; built-in view labels are lazy getters so they re-resolve too.
26
+ */
17
27
  export declare function resolveStudioI18n(): ResolvedI18n;
18
28
  /**
19
29
  * Translate a message key for the active UI locale (EC-241): active-locale string
@@ -36,10 +46,14 @@ export declare function localizedLabel(value: LocalizedString | undefined, fallb
36
46
  export declare function useUiLocale(): string;
37
47
  /** A `t(key, vars?)` translator bound to the active locale. */
38
48
  export declare function useT(): (key: string, vars?: Record<string, string | number>) => string;
39
- /** Resolve a {@link LocalizedString} label (field/collection/option) for the active locale. */
49
+ /**
50
+ * Resolve a {@link LocalizedString} label (field/collection/option) for the active
51
+ * locale. Re-reads the locale each render (keyed into the memo) so it never captures
52
+ * a locale resolved before the mount seeded the i18n config.
53
+ */
40
54
  export declare function useLabel(): (value: LocalizedString | undefined, fallback?: string) => string;
41
- /** Test seam: force the active locale (and clear the per-boot cache). Never used by app code. */
55
+ /** Test seam: force the active locale. Never used by app code. */
42
56
  export declare function setUiLocaleForTests(locale: string | null): void;
43
- /** Test seam: clear the per-boot resolution cache. Never used by app code. */
57
+ /** Test seam: reset i18n state (the once-per-boot warnings). Never used by app code. */
44
58
  export declare function resetStudioI18nForTests(): void;
45
59
  export {};
@@ -27,7 +27,6 @@ export function resolveActiveLocale(available, preferred, defaultLocale) {
27
27
  }
28
28
  return defaultLocale;
29
29
  }
30
- let cached = null;
31
30
  let localeOverride = null;
32
31
  function navigatorLanguages() {
33
32
  if (typeof navigator === 'undefined')
@@ -44,17 +43,24 @@ function warnOnce(message) {
44
43
  if (typeof console !== 'undefined')
45
44
  console.warn(message);
46
45
  }
47
- /** Resolve the studio i18n once per boot (config static; locale from the browser). */
46
+ /**
47
+ * Resolve the studio i18n from the config holder + the browser locale (EC-241).
48
+ *
49
+ * NOT cached: the studio config holder (`createStudio`) is seeded at the mount, which
50
+ * runs AFTER the consumer's `studio.config` module built its sidebar — so an eager
51
+ * `translate` during that sidebar build (e.g. a built-in view label) sees an empty
52
+ * config. Caching the first result would poison every later render-time call with
53
+ * that empty-config English. Re-resolving each call is cheap (a few array ops) and
54
+ * always reflects the config once the mount has run. `translate`/`localizedLabel`
55
+ * are the hot callers; built-in view labels are lazy getters so they re-resolve too.
56
+ */
48
57
  export function resolveStudioI18n() {
49
- if (cached)
50
- return cached;
51
58
  const config = resolveDeploymentI18n() ?? {};
52
59
  const defaultLocale = config.defaultLocale ?? BUILTIN_DEFAULT_LOCALE;
53
60
  const locales = config.locales ?? {};
54
61
  const available = [defaultLocale, ...Object.keys(locales).filter((l) => l !== defaultLocale)];
55
62
  const locale = localeOverride ?? resolveActiveLocale(available, navigatorLanguages(), defaultLocale);
56
63
  const active = locale === defaultLocale ? {} : (locales[locale] ?? {});
57
- cached = { locale, defaultLocale, active, source: EN_MESSAGES };
58
64
  // Coverage lint: warn once when the active locale doesn't cover every English key.
59
65
  if (locale !== defaultLocale && locales[locale]) {
60
66
  const missing = Object.keys(EN_MESSAGES).filter((k) => !(k in active));
@@ -62,7 +68,7 @@ export function resolveStudioI18n() {
62
68
  warnOnce(`[i18n] locale "${locale}" is missing ${missing.length} message(s); falling back to English for those.`);
63
69
  }
64
70
  }
65
- return cached;
71
+ return { locale, defaultLocale, active, source: EN_MESSAGES };
66
72
  }
67
73
  function interpolate(text, vars) {
68
74
  return text.replace(/\{(\w+)\}/g, (whole, key) => (key in vars ? String(vars[key]) : whole));
@@ -95,7 +101,7 @@ export function localizedLabel(value, fallback = '') {
95
101
  const { locale, defaultLocale } = resolveStudioI18n();
96
102
  return resolveLabel(value, locale, defaultLocale) ?? fallback;
97
103
  }
98
- // --- React hooks (config static per boot → stable identities) ---
104
+ // --- React hooks ---
99
105
  /** The active studio-UI locale (browser-resolved). */
100
106
  export function useUiLocale() {
101
107
  return resolveStudioI18n().locale;
@@ -104,22 +110,22 @@ export function useUiLocale() {
104
110
  export function useT() {
105
111
  return translate;
106
112
  }
107
- /** Resolve a {@link LocalizedString} label (field/collection/option) for the active locale. */
113
+ /**
114
+ * Resolve a {@link LocalizedString} label (field/collection/option) for the active
115
+ * locale. Re-reads the locale each render (keyed into the memo) so it never captures
116
+ * a locale resolved before the mount seeded the i18n config.
117
+ */
108
118
  export function useLabel() {
109
- return useMemo(() => {
110
- const { locale, defaultLocale } = resolveStudioI18n();
111
- return (value, fallback) => resolveLabel(value, locale, defaultLocale) ?? fallback ?? '';
112
- }, []);
119
+ const { locale, defaultLocale } = resolveStudioI18n();
120
+ return useMemo(() => (value, fallback) => resolveLabel(value, locale, defaultLocale) ?? fallback ?? '', [locale, defaultLocale]);
113
121
  }
114
- /** Test seam: force the active locale (and clear the per-boot cache). Never used by app code. */
122
+ /** Test seam: force the active locale. Never used by app code. */
115
123
  export function setUiLocaleForTests(locale) {
116
124
  localeOverride = locale;
117
- cached = null;
118
125
  warned.clear();
119
126
  }
120
- /** Test seam: clear the per-boot resolution cache. Never used by app code. */
127
+ /** Test seam: reset i18n state (the once-per-boot warnings). Never used by app code. */
121
128
  export function resetStudioI18nForTests() {
122
- cached = null;
123
129
  warned.clear();
124
130
  }
125
131
  //# sourceMappingURL=i18n.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../../../src/lib/i18n/i18n.ts"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { LocalizedString } from '@elytracms/core/cms-core'\nimport { resolveDeploymentI18n } from '@elytracms/studio/lib/workspace'\nimport { EN_MESSAGES } from './messages.en'\nimport type { MessageCatalog, SourceCatalog } from './types'\n\n/**\n * Studio-UI localization runtime (EC-241). Resolves the active UI locale from the\n * **browser's preferred locale** (no persisted selector in v1 — that rides EC-240's\n * deferred account settings), then translates message keys with an\n * active-locale → English → visible-fallback chain. The studio config is static per\n * boot, so resolution is memoised once (like `getWorkspaceState`); a locale switch\n * needs a reload, which is all v1 offers.\n */\n\nconst BUILTIN_DEFAULT_LOCALE = 'en'\n\n/**\n * Pick the best available locale for the browser's ordered language preferences\n * (EC-241): an exact tag match wins, else the base language (`de-DE` → `de`), else\n * `defaultLocale`. Pure + deterministic — the browser list is the only input.\n */\nexport function resolveActiveLocale(\n available: readonly string[],\n preferred: readonly string[],\n defaultLocale: string,\n): string {\n const lower = available.map((a) => a.toLowerCase())\n for (const pref of preferred) {\n const p = pref.toLowerCase()\n const base = p.split('-')[0] ?? p\n const at = lower.findIndex((a) => a === p || a === base)\n if (at >= 0) return available[at]!\n }\n return defaultLocale\n}\n\ninterface ResolvedI18n {\n locale: string\n defaultLocale: string\n /** The active-locale catalog (empty when active === default → the English source is used). */\n active: MessageCatalog\n source: SourceCatalog\n}\n\nlet cached: ResolvedI18n | null = null\nlet localeOverride: string | null = null\n\nfunction navigatorLanguages(): string[] {\n if (typeof navigator === 'undefined') return []\n if (navigator.languages && navigator.languages.length > 0) return [...navigator.languages]\n return navigator.language ? [navigator.language] : []\n}\n\nconst warned = new Set<string>()\nfunction warnOnce(message: string): void {\n if (warned.has(message)) return\n warned.add(message)\n if (typeof console !== 'undefined') console.warn(message)\n}\n\n/** Resolve the studio i18n once per boot (config static; locale from the browser). */\nexport function resolveStudioI18n(): ResolvedI18n {\n if (cached) return cached\n const config = resolveDeploymentI18n() ?? {}\n const defaultLocale = config.defaultLocale ?? BUILTIN_DEFAULT_LOCALE\n const locales = config.locales ?? {}\n const available = [defaultLocale, ...Object.keys(locales).filter((l) => l !== defaultLocale)]\n const locale = localeOverride ?? resolveActiveLocale(available, navigatorLanguages(), defaultLocale)\n const active = locale === defaultLocale ? {} : (locales[locale] ?? {})\n cached = { locale, defaultLocale, active, source: EN_MESSAGES }\n // Coverage lint: warn once when the active locale doesn't cover every English key.\n if (locale !== defaultLocale && locales[locale]) {\n const missing = Object.keys(EN_MESSAGES).filter((k) => !(k in active))\n if (missing.length > 0) {\n warnOnce(\n `[i18n] locale \"${locale}\" is missing ${missing.length} message(s); falling back to English for those.`,\n )\n }\n }\n return cached\n}\n\nfunction interpolate(text: string, vars: Record<string, string | number>): string {\n return text.replace(/\\{(\\w+)\\}/g, (whole, key: string) => (key in vars ? String(vars[key]) : whole))\n}\n\n/**\n * Translate a message key for the active UI locale (EC-241): active-locale string\n * ?? English source ?? **the key itself** (a visible fallback + a one-time console\n * signal — \"failures are first-class\", never a silent blank). Non-hook — reads the\n * per-boot singleton, so config-build and pure code can localize too; {@link useT}\n * is the React-idiomatic wrapper.\n */\nexport function translate(key: string, vars?: Record<string, string | number>): string {\n const i18n = resolveStudioI18n()\n const hit = i18n.active[key] ?? i18n.source[key]?.message\n if (hit === undefined) {\n warnOnce(`[i18n] missing message key: \"${key}\"`)\n return key\n }\n return vars ? interpolate(hit, vars) : hit\n}\n\n/**\n * Resolve a {@link LocalizedString} (field/collection/option label) against the\n * active UI locale (EC-241) — **non-hook**, reading the per-boot singleton, so it is\n * safe to call anywhere: a component body, a nested render helper, or config-build\n * code. Returns `fallback` (default `''`) when the value is absent. Prefer this over\n * `resolveLabel(x, useUiLocale())` at render sites — it localizes without a hook, so\n * it can't trip the rules-of-hooks in a conditional/nested position.\n */\nexport function localizedLabel(value: LocalizedString | undefined, fallback = ''): string {\n const { locale, defaultLocale } = resolveStudioI18n()\n return resolveLabel(value, locale, defaultLocale) ?? fallback\n}\n\n// --- React hooks (config static per boot → stable identities) ---\n\n/** The active studio-UI locale (browser-resolved). */\nexport function useUiLocale(): string {\n return resolveStudioI18n().locale\n}\n\n/** A `t(key, vars?)` translator bound to the active locale. */\nexport function useT(): (key: string, vars?: Record<string, string | number>) => string {\n return translate\n}\n\n/** Resolve a {@link LocalizedString} label (field/collection/option) for the active locale. */\nexport function useLabel(): (value: LocalizedString | undefined, fallback?: string) => string {\n return useMemo(() => {\n const { locale, defaultLocale } = resolveStudioI18n()\n return (value, fallback) => resolveLabel(value, locale, defaultLocale) ?? fallback ?? ''\n }, [])\n}\n\n/** Test seam: force the active locale (and clear the per-boot cache). Never used by app code. */\nexport function setUiLocaleForTests(locale: string | null): void {\n localeOverride = locale\n cached = null\n warned.clear()\n}\n\n/** Test seam: clear the per-boot resolution cache. Never used by app code. */\nexport function resetStudioI18nForTests(): void {\n cached = null\n warned.clear()\n}\n"]}
1
+ {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../../../src/lib/i18n/i18n.ts"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { LocalizedString } from '@elytracms/core/cms-core'\nimport { resolveDeploymentI18n } from '@elytracms/studio/lib/workspace'\nimport { EN_MESSAGES } from './messages.en'\nimport type { MessageCatalog, SourceCatalog } from './types'\n\n/**\n * Studio-UI localization runtime (EC-241). Resolves the active UI locale from the\n * **browser's preferred locale** (no persisted selector in v1 — that rides EC-240's\n * deferred account settings), then translates message keys with an\n * active-locale → English → visible-fallback chain. The studio config is static per\n * boot, so resolution is memoised once (like `getWorkspaceState`); a locale switch\n * needs a reload, which is all v1 offers.\n */\n\nconst BUILTIN_DEFAULT_LOCALE = 'en'\n\n/**\n * Pick the best available locale for the browser's ordered language preferences\n * (EC-241): an exact tag match wins, else the base language (`de-DE` → `de`), else\n * `defaultLocale`. Pure + deterministic — the browser list is the only input.\n */\nexport function resolveActiveLocale(\n available: readonly string[],\n preferred: readonly string[],\n defaultLocale: string,\n): string {\n const lower = available.map((a) => a.toLowerCase())\n for (const pref of preferred) {\n const p = pref.toLowerCase()\n const base = p.split('-')[0] ?? p\n const at = lower.findIndex((a) => a === p || a === base)\n if (at >= 0) return available[at]!\n }\n return defaultLocale\n}\n\ninterface ResolvedI18n {\n locale: string\n defaultLocale: string\n /** The active-locale catalog (empty when active === default → the English source is used). */\n active: MessageCatalog\n source: SourceCatalog\n}\n\nlet localeOverride: string | null = null\n\nfunction navigatorLanguages(): string[] {\n if (typeof navigator === 'undefined') return []\n if (navigator.languages && navigator.languages.length > 0) return [...navigator.languages]\n return navigator.language ? [navigator.language] : []\n}\n\nconst warned = new Set<string>()\nfunction warnOnce(message: string): void {\n if (warned.has(message)) return\n warned.add(message)\n if (typeof console !== 'undefined') console.warn(message)\n}\n\n/**\n * Resolve the studio i18n from the config holder + the browser locale (EC-241).\n *\n * NOT cached: the studio config holder (`createStudio`) is seeded at the mount, which\n * runs AFTER the consumer's `studio.config` module built its sidebar — so an eager\n * `translate` during that sidebar build (e.g. a built-in view label) sees an empty\n * config. Caching the first result would poison every later render-time call with\n * that empty-config English. Re-resolving each call is cheap (a few array ops) and\n * always reflects the config once the mount has run. `translate`/`localizedLabel`\n * are the hot callers; built-in view labels are lazy getters so they re-resolve too.\n */\nexport function resolveStudioI18n(): ResolvedI18n {\n const config = resolveDeploymentI18n() ?? {}\n const defaultLocale = config.defaultLocale ?? BUILTIN_DEFAULT_LOCALE\n const locales = config.locales ?? {}\n const available = [defaultLocale, ...Object.keys(locales).filter((l) => l !== defaultLocale)]\n const locale = localeOverride ?? resolveActiveLocale(available, navigatorLanguages(), defaultLocale)\n const active = locale === defaultLocale ? {} : (locales[locale] ?? {})\n // Coverage lint: warn once when the active locale doesn't cover every English key.\n if (locale !== defaultLocale && locales[locale]) {\n const missing = Object.keys(EN_MESSAGES).filter((k) => !(k in active))\n if (missing.length > 0) {\n warnOnce(\n `[i18n] locale \"${locale}\" is missing ${missing.length} message(s); falling back to English for those.`,\n )\n }\n }\n return { locale, defaultLocale, active, source: EN_MESSAGES }\n}\n\nfunction interpolate(text: string, vars: Record<string, string | number>): string {\n return text.replace(/\\{(\\w+)\\}/g, (whole, key: string) => (key in vars ? String(vars[key]) : whole))\n}\n\n/**\n * Translate a message key for the active UI locale (EC-241): active-locale string\n * ?? English source ?? **the key itself** (a visible fallback + a one-time console\n * signal — \"failures are first-class\", never a silent blank). Non-hook — reads the\n * per-boot singleton, so config-build and pure code can localize too; {@link useT}\n * is the React-idiomatic wrapper.\n */\nexport function translate(key: string, vars?: Record<string, string | number>): string {\n const i18n = resolveStudioI18n()\n const hit = i18n.active[key] ?? i18n.source[key]?.message\n if (hit === undefined) {\n warnOnce(`[i18n] missing message key: \"${key}\"`)\n return key\n }\n return vars ? interpolate(hit, vars) : hit\n}\n\n/**\n * Resolve a {@link LocalizedString} (field/collection/option label) against the\n * active UI locale (EC-241) — **non-hook**, reading the per-boot singleton, so it is\n * safe to call anywhere: a component body, a nested render helper, or config-build\n * code. Returns `fallback` (default `''`) when the value is absent. Prefer this over\n * `resolveLabel(x, useUiLocale())` at render sites — it localizes without a hook, so\n * it can't trip the rules-of-hooks in a conditional/nested position.\n */\nexport function localizedLabel(value: LocalizedString | undefined, fallback = ''): string {\n const { locale, defaultLocale } = resolveStudioI18n()\n return resolveLabel(value, locale, defaultLocale) ?? fallback\n}\n\n// --- React hooks ---\n\n/** The active studio-UI locale (browser-resolved). */\nexport function useUiLocale(): string {\n return resolveStudioI18n().locale\n}\n\n/** A `t(key, vars?)` translator bound to the active locale. */\nexport function useT(): (key: string, vars?: Record<string, string | number>) => string {\n return translate\n}\n\n/**\n * Resolve a {@link LocalizedString} label (field/collection/option) for the active\n * locale. Re-reads the locale each render (keyed into the memo) so it never captures\n * a locale resolved before the mount seeded the i18n config.\n */\nexport function useLabel(): (value: LocalizedString | undefined, fallback?: string) => string {\n const { locale, defaultLocale } = resolveStudioI18n()\n return useMemo(\n () => (value, fallback) => resolveLabel(value, locale, defaultLocale) ?? fallback ?? '',\n [locale, defaultLocale],\n )\n}\n\n/** Test seam: force the active locale. Never used by app code. */\nexport function setUiLocaleForTests(locale: string | null): void {\n localeOverride = locale\n warned.clear()\n}\n\n/** Test seam: reset i18n state (the once-per-boot warnings). Never used by app code. */\nexport function resetStudioI18nForTests(): void {\n warned.clear()\n}\n"]}
@@ -7,9 +7,16 @@ import type { SourceCatalog } from './types';
7
7
  * the key itself (a visible fallback + a one-time console signal), never a silent
8
8
  * blank.
9
9
  *
10
- * v1 seeds a representative, high-traffic slice — the section rail + the shell user
11
- * menu — to prove the pipeline end-to-end. Extracting every remaining studio string
12
- * into keys is an ongoing mechanical sweep (see EC-241 "scope honesty"), not a
13
- * silent omission.
10
+ * Organised by surface. `common.*` holds generic, reused affordances (buttons,
11
+ * status words); every other namespace maps to one studio surface. Values that come
12
+ * from the project's schema/config (field labels, collection names, component
13
+ * manifests) are NOT here — those are `LocalizedString`s resolved by `resolveLabel`
14
+ * (the separate schema-label axis).
15
+ *
16
+ * Scope honesty (EC-241): this covers the studio's **component chrome**. A handful
17
+ * of `lib/*` label helpers (`persona/labels`, `cms-views` operator labels,
18
+ * `auth-errors`, `clipboard` summaries, core `describeAccept`) and the JSX-chip
19
+ * developer boot hints (backend/workspace gates) are a documented follow-up — they
20
+ * need a message-with-elements splitter beyond the plain-string `translate()`.
14
21
  */
15
22
  export declare const EN_MESSAGES: SourceCatalog;