@elytracms/studio 0.0.29 → 0.0.32

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 (61) hide show
  1. package/convex/redirects.ts +3 -0
  2. package/convex/schema.ts +2 -0
  3. package/dist/components/app-views/default-modules.d.ts +3 -1
  4. package/dist/components/app-views/default-modules.js +63 -19
  5. package/dist/components/app-views/default-modules.js.map +1 -1
  6. package/dist/components/app-views/studio-config.d.ts +10 -0
  7. package/dist/components/app-views/studio-config.js.map +1 -1
  8. package/dist/components/builder/property-inspector.js +3 -2
  9. package/dist/components/builder/property-inspector.js.map +1 -1
  10. package/dist/components/builder/reuse-context.js +2 -1
  11. package/dist/components/builder/reuse-context.js.map +1 -1
  12. package/dist/components/cms/cms-index.js +2 -1
  13. package/dist/components/cms/cms-index.js.map +1 -1
  14. package/dist/components/cms/cms-store.d.ts +11 -2
  15. package/dist/components/cms/cms-store.js +17 -4
  16. package/dist/components/cms/cms-store.js.map +1 -1
  17. package/dist/components/cms/collection-table.js +5 -4
  18. package/dist/components/cms/collection-table.js.map +1 -1
  19. package/dist/components/cms/detail-editor.js +5 -4
  20. package/dist/components/cms/detail-editor.js.map +1 -1
  21. package/dist/components/cms/editable-cell.js +3 -2
  22. package/dist/components/cms/editable-cell.js.map +1 -1
  23. package/dist/components/cms/field-control.js +6 -5
  24. package/dist/components/cms/field-control.js.map +1 -1
  25. package/dist/components/cms/scalar-value-editor.js +2 -1
  26. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  27. package/dist/components/components/field-inspector.js +3 -2
  28. package/dist/components/components/field-inspector.js.map +1 -1
  29. package/dist/components/current-user-menu.js +4 -2
  30. package/dist/components/current-user-menu.js.map +1 -1
  31. package/dist/components/redirects/redirects-collection-view-source.d.ts +4 -3
  32. package/dist/components/redirects/redirects-collection-view-source.js +23 -3
  33. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  34. package/dist/components/redirects/redirects-store.js +1 -0
  35. package/dist/components/redirects/redirects-store.js.map +1 -1
  36. package/dist/customization.d.ts +7 -0
  37. package/dist/customization.js +6 -0
  38. package/dist/customization.js.map +1 -1
  39. package/dist/lib/cms-views/view-model.js +2 -2
  40. package/dist/lib/cms-views/view-model.js.map +1 -1
  41. package/dist/lib/i18n/i18n.d.ts +59 -0
  42. package/dist/lib/i18n/i18n.js +131 -0
  43. package/dist/lib/i18n/i18n.js.map +1 -0
  44. package/dist/lib/i18n/index.d.ts +12 -0
  45. package/dist/lib/i18n/index.js +4 -0
  46. package/dist/lib/i18n/index.js.map +1 -0
  47. package/dist/lib/i18n/messages.en.d.ts +15 -0
  48. package/dist/lib/i18n/messages.en.js +39 -0
  49. package/dist/lib/i18n/messages.en.js.map +1 -0
  50. package/dist/lib/i18n/types.d.ts +45 -0
  51. package/dist/lib/i18n/types.js +19 -0
  52. package/dist/lib/i18n/types.js.map +1 -0
  53. package/dist/lib/persistence/convex-adapter.js +2 -0
  54. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  55. package/dist/lib/references/reference-model.js +4 -3
  56. package/dist/lib/references/reference-model.js.map +1 -1
  57. package/dist/lib/workspace/index.d.ts +8 -0
  58. package/dist/lib/workspace/index.js +10 -0
  59. package/dist/lib/workspace/index.js.map +1 -1
  60. package/dist/ui/badge.d.ts +1 -1
  61. package/package.json +6 -2
@@ -0,0 +1,59 @@
1
+ import type { LocalizedString } from '@elytracms/core/cms-core';
2
+ import type { MessageCatalog, SourceCatalog } from './types';
3
+ /**
4
+ * Pick the best available locale for the browser's ordered language preferences
5
+ * (EC-241): an exact tag match wins, else the base language (`de-DE` → `de`), else
6
+ * `defaultLocale`. Pure + deterministic — the browser list is the only input.
7
+ */
8
+ export declare function resolveActiveLocale(available: readonly string[], preferred: readonly string[], defaultLocale: string): string;
9
+ interface ResolvedI18n {
10
+ locale: string;
11
+ defaultLocale: string;
12
+ /** The active-locale catalog (empty when active === default → the English source is used). */
13
+ active: MessageCatalog;
14
+ source: SourceCatalog;
15
+ }
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
+ */
27
+ export declare function resolveStudioI18n(): ResolvedI18n;
28
+ /**
29
+ * Translate a message key for the active UI locale (EC-241): active-locale string
30
+ * ?? English source ?? **the key itself** (a visible fallback + a one-time console
31
+ * signal — "failures are first-class", never a silent blank). Non-hook — reads the
32
+ * per-boot singleton, so config-build and pure code can localize too; {@link useT}
33
+ * is the React-idiomatic wrapper.
34
+ */
35
+ export declare function translate(key: string, vars?: Record<string, string | number>): string;
36
+ /**
37
+ * Resolve a {@link LocalizedString} (field/collection/option label) against the
38
+ * active UI locale (EC-241) — **non-hook**, reading the per-boot singleton, so it is
39
+ * safe to call anywhere: a component body, a nested render helper, or config-build
40
+ * code. Returns `fallback` (default `''`) when the value is absent. Prefer this over
41
+ * `resolveLabel(x, useUiLocale())` at render sites — it localizes without a hook, so
42
+ * it can't trip the rules-of-hooks in a conditional/nested position.
43
+ */
44
+ export declare function localizedLabel(value: LocalizedString | undefined, fallback?: string): string;
45
+ /** The active studio-UI locale (browser-resolved). */
46
+ export declare function useUiLocale(): string;
47
+ /** A `t(key, vars?)` translator bound to the active locale. */
48
+ export declare function useT(): (key: string, vars?: Record<string, string | number>) => string;
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
+ */
54
+ export declare function useLabel(): (value: LocalizedString | undefined, fallback?: string) => string;
55
+ /** Test seam: force the active locale. Never used by app code. */
56
+ export declare function setUiLocaleForTests(locale: string | null): void;
57
+ /** Test seam: reset i18n state (the once-per-boot warnings). Never used by app code. */
58
+ export declare function resetStudioI18nForTests(): void;
59
+ export {};
@@ -0,0 +1,131 @@
1
+ import { useMemo } from 'react';
2
+ import { resolveLabel } from '@elytracms/core/cms-core';
3
+ import { resolveDeploymentI18n } from '@elytracms/studio/lib/workspace';
4
+ import { EN_MESSAGES } from './messages.en';
5
+ /**
6
+ * Studio-UI localization runtime (EC-241). Resolves the active UI locale from the
7
+ * **browser's preferred locale** (no persisted selector in v1 — that rides EC-240's
8
+ * deferred account settings), then translates message keys with an
9
+ * active-locale → English → visible-fallback chain. The studio config is static per
10
+ * boot, so resolution is memoised once (like `getWorkspaceState`); a locale switch
11
+ * needs a reload, which is all v1 offers.
12
+ */
13
+ const BUILTIN_DEFAULT_LOCALE = 'en';
14
+ /**
15
+ * Pick the best available locale for the browser's ordered language preferences
16
+ * (EC-241): an exact tag match wins, else the base language (`de-DE` → `de`), else
17
+ * `defaultLocale`. Pure + deterministic — the browser list is the only input.
18
+ */
19
+ export function resolveActiveLocale(available, preferred, defaultLocale) {
20
+ const lower = available.map((a) => a.toLowerCase());
21
+ for (const pref of preferred) {
22
+ const p = pref.toLowerCase();
23
+ const base = p.split('-')[0] ?? p;
24
+ const at = lower.findIndex((a) => a === p || a === base);
25
+ if (at >= 0)
26
+ return available[at];
27
+ }
28
+ return defaultLocale;
29
+ }
30
+ let localeOverride = null;
31
+ function navigatorLanguages() {
32
+ if (typeof navigator === 'undefined')
33
+ return [];
34
+ if (navigator.languages && navigator.languages.length > 0)
35
+ return [...navigator.languages];
36
+ return navigator.language ? [navigator.language] : [];
37
+ }
38
+ const warned = new Set();
39
+ function warnOnce(message) {
40
+ if (warned.has(message))
41
+ return;
42
+ warned.add(message);
43
+ if (typeof console !== 'undefined')
44
+ console.warn(message);
45
+ }
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
+ */
57
+ export function resolveStudioI18n() {
58
+ const config = resolveDeploymentI18n() ?? {};
59
+ const defaultLocale = config.defaultLocale ?? BUILTIN_DEFAULT_LOCALE;
60
+ const locales = config.locales ?? {};
61
+ const available = [defaultLocale, ...Object.keys(locales).filter((l) => l !== defaultLocale)];
62
+ const locale = localeOverride ?? resolveActiveLocale(available, navigatorLanguages(), defaultLocale);
63
+ const active = locale === defaultLocale ? {} : (locales[locale] ?? {});
64
+ // Coverage lint: warn once when the active locale doesn't cover every English key.
65
+ if (locale !== defaultLocale && locales[locale]) {
66
+ const missing = Object.keys(EN_MESSAGES).filter((k) => !(k in active));
67
+ if (missing.length > 0) {
68
+ warnOnce(`[i18n] locale "${locale}" is missing ${missing.length} message(s); falling back to English for those.`);
69
+ }
70
+ }
71
+ return { locale, defaultLocale, active, source: EN_MESSAGES };
72
+ }
73
+ function interpolate(text, vars) {
74
+ return text.replace(/\{(\w+)\}/g, (whole, key) => (key in vars ? String(vars[key]) : whole));
75
+ }
76
+ /**
77
+ * Translate a message key for the active UI locale (EC-241): active-locale string
78
+ * ?? English source ?? **the key itself** (a visible fallback + a one-time console
79
+ * signal — "failures are first-class", never a silent blank). Non-hook — reads the
80
+ * per-boot singleton, so config-build and pure code can localize too; {@link useT}
81
+ * is the React-idiomatic wrapper.
82
+ */
83
+ export function translate(key, vars) {
84
+ const i18n = resolveStudioI18n();
85
+ const hit = i18n.active[key] ?? i18n.source[key]?.message;
86
+ if (hit === undefined) {
87
+ warnOnce(`[i18n] missing message key: "${key}"`);
88
+ return key;
89
+ }
90
+ return vars ? interpolate(hit, vars) : hit;
91
+ }
92
+ /**
93
+ * Resolve a {@link LocalizedString} (field/collection/option label) against the
94
+ * active UI locale (EC-241) — **non-hook**, reading the per-boot singleton, so it is
95
+ * safe to call anywhere: a component body, a nested render helper, or config-build
96
+ * code. Returns `fallback` (default `''`) when the value is absent. Prefer this over
97
+ * `resolveLabel(x, useUiLocale())` at render sites — it localizes without a hook, so
98
+ * it can't trip the rules-of-hooks in a conditional/nested position.
99
+ */
100
+ export function localizedLabel(value, fallback = '') {
101
+ const { locale, defaultLocale } = resolveStudioI18n();
102
+ return resolveLabel(value, locale, defaultLocale) ?? fallback;
103
+ }
104
+ // --- React hooks ---
105
+ /** The active studio-UI locale (browser-resolved). */
106
+ export function useUiLocale() {
107
+ return resolveStudioI18n().locale;
108
+ }
109
+ /** A `t(key, vars?)` translator bound to the active locale. */
110
+ export function useT() {
111
+ return translate;
112
+ }
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
+ */
118
+ export function useLabel() {
119
+ const { locale, defaultLocale } = resolveStudioI18n();
120
+ return useMemo(() => (value, fallback) => resolveLabel(value, locale, defaultLocale) ?? fallback ?? '', [locale, defaultLocale]);
121
+ }
122
+ /** Test seam: force the active locale. Never used by app code. */
123
+ export function setUiLocaleForTests(locale) {
124
+ localeOverride = locale;
125
+ warned.clear();
126
+ }
127
+ /** Test seam: reset i18n state (the once-per-boot warnings). Never used by app code. */
128
+ export function resetStudioI18nForTests() {
129
+ warned.clear();
130
+ }
131
+ //# sourceMappingURL=i18n.js.map
@@ -0,0 +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 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"]}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@elytracms/studio/lib/i18n` — studio-UI localization (EC-241).
3
+ *
4
+ * - Public config surface (`StudioI18nConfig`, `MessageCatalog`, `msg`) is re-exported
5
+ * from `@elytracms/studio/customization` so a consumer authors catalogs there.
6
+ * - Studio components localize chrome via `translate`/`useT` and schema labels via
7
+ * `useLabel`.
8
+ */
9
+ export type { SourceMessage, SourceCatalog, MessageCatalog, StudioI18nConfig } from './types';
10
+ export { msg } from './types';
11
+ export { EN_MESSAGES } from './messages.en';
12
+ export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, useT, useUiLocale, useLabel, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n';
@@ -0,0 +1,4 @@
1
+ export { msg } from './types';
2
+ export { EN_MESSAGES } from './messages.en';
3
+ export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, useT, useUiLocale, useLabel, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/i18n/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,IAAI,EACJ,WAAW,EACX,QAAQ,EACR,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,QAAQ,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/i18n` — studio-UI localization (EC-241).\n *\n * - Public config surface (`StudioI18nConfig`, `MessageCatalog`, `msg`) is re-exported\n * from `@elytracms/studio/customization` so a consumer authors catalogs there.\n * - Studio components localize chrome via `translate`/`useT` and schema labels via\n * `useLabel`.\n */\nexport type { SourceMessage, SourceCatalog, MessageCatalog, StudioI18nConfig } from './types'\nexport { msg } from './types'\nexport { EN_MESSAGES } from './messages.en'\nexport {\n resolveActiveLocale,\n resolveStudioI18n,\n translate,\n localizedLabel,\n useT,\n useUiLocale,\n useLabel,\n setUiLocaleForTests,\n resetStudioI18nForTests,\n} from './i18n'\n"]}
@@ -0,0 +1,15 @@
1
+ import type { SourceCatalog } from './types';
2
+ /**
3
+ * The engine's built-in **English source catalog** (EC-241) — the source of truth
4
+ * and the ultimate fallback for every studio-chrome string. A consumer adds a
5
+ * language via `defineStudio({ i18n: { locales: { de: deMessages } } })`; a key
6
+ * missing from the active locale falls back to the English `message` here, then to
7
+ * the key itself (a visible fallback + a one-time console signal), never a silent
8
+ * blank.
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.
14
+ */
15
+ export declare const EN_MESSAGES: SourceCatalog;
@@ -0,0 +1,39 @@
1
+ import { msg } from './types';
2
+ /**
3
+ * The engine's built-in **English source catalog** (EC-241) — the source of truth
4
+ * and the ultimate fallback for every studio-chrome string. A consumer adds a
5
+ * language via `defineStudio({ i18n: { locales: { de: deMessages } } })`; a key
6
+ * missing from the active locale falls back to the English `message` here, then to
7
+ * the key itself (a visible fallback + a one-time console signal), never a silent
8
+ * blank.
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.
14
+ */
15
+ export const EN_MESSAGES = {
16
+ // --- Built-in navigation views (the section rail) ---
17
+ 'nav.cms.label': msg('CMS'),
18
+ 'nav.cms.description': msg('Model collections, fields, and documents.'),
19
+ 'nav.components.label': msg('Components'),
20
+ 'nav.components.description': msg('Browse the registry of primitives and project components.'),
21
+ 'nav.redirects.label': msg('Redirects'),
22
+ 'nav.redirects.description': msg('Create and manage URL redirects (301/302) for this site.'),
23
+ 'nav.assets.label': msg('Assets'),
24
+ 'nav.assets.description': msg('Manage uploaded media and metadata.'),
25
+ 'nav.users.label': msg('Users'),
26
+ 'nav.users.description': msg('Manage who has access to this deployment and their role.'),
27
+ 'nav.extensions.label': msg('Extensions'),
28
+ 'nav.extensions.description': msg('Install components, field types, adapters, and templates from the marketplace.'),
29
+ 'nav.settings.label': msg('Settings'),
30
+ 'nav.settings.description': msg('Configure project metadata and locales.'),
31
+ // --- Current-user menu (shell header) ---
32
+ 'userMenu.accountMenu': msg('Account menu', 'aria-label on the avatar button that opens the user menu.'),
33
+ 'userMenu.lightMode': msg('Light mode', 'Menu item: switch the studio to the light colour theme.'),
34
+ 'userMenu.darkMode': msg('Dark mode', 'Menu item: switch the studio to the dark colour theme.'),
35
+ 'userMenu.accountSettings': msg('Account settings'),
36
+ 'userMenu.soon': msg('Soon', 'Badge on a feature that is not available yet.'),
37
+ 'userMenu.signOut': msg('Sign out'),
38
+ };
39
+ //# sourceMappingURL=messages.en.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.en.js","sourceRoot":"","sources":["../../../src/lib/i18n/messages.en.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAG7B;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,WAAW,GAAkB;IACxC,uDAAuD;IACvD,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC;IAC3B,qBAAqB,EAAE,GAAG,CAAC,2CAA2C,CAAC;IACvE,sBAAsB,EAAE,GAAG,CAAC,YAAY,CAAC;IACzC,4BAA4B,EAAE,GAAG,CAAC,2DAA2D,CAAC;IAC9F,qBAAqB,EAAE,GAAG,CAAC,WAAW,CAAC;IACvC,2BAA2B,EAAE,GAAG,CAAC,0DAA0D,CAAC;IAC5F,kBAAkB,EAAE,GAAG,CAAC,QAAQ,CAAC;IACjC,wBAAwB,EAAE,GAAG,CAAC,qCAAqC,CAAC;IACpE,iBAAiB,EAAE,GAAG,CAAC,OAAO,CAAC;IAC/B,uBAAuB,EAAE,GAAG,CAAC,0DAA0D,CAAC;IACxF,sBAAsB,EAAE,GAAG,CAAC,YAAY,CAAC;IACzC,4BAA4B,EAAE,GAAG,CAC/B,gFAAgF,CACjF;IACD,oBAAoB,EAAE,GAAG,CAAC,UAAU,CAAC;IACrC,0BAA0B,EAAE,GAAG,CAAC,yCAAyC,CAAC;IAE1E,2CAA2C;IAC3C,sBAAsB,EAAE,GAAG,CAAC,cAAc,EAAE,2DAA2D,CAAC;IACxG,oBAAoB,EAAE,GAAG,CAAC,YAAY,EAAE,yDAAyD,CAAC;IAClG,mBAAmB,EAAE,GAAG,CAAC,WAAW,EAAE,wDAAwD,CAAC;IAC/F,0BAA0B,EAAE,GAAG,CAAC,kBAAkB,CAAC;IACnD,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,+CAA+C,CAAC;IAC7E,kBAAkB,EAAE,GAAG,CAAC,UAAU,CAAC;CACpC,CAAA","sourcesContent":["import { msg } from './types'\nimport type { SourceCatalog } from './types'\n\n/**\n * The engine's built-in **English source catalog** (EC-241) — the source of truth\n * and the ultimate fallback for every studio-chrome string. A consumer adds a\n * language via `defineStudio({ i18n: { locales: { de: deMessages } } })`; a key\n * missing from the active locale falls back to the English `message` here, then to\n * the key itself (a visible fallback + a one-time console signal), never a silent\n * blank.\n *\n * v1 seeds a representative, high-traffic slice — the section rail + the shell user\n * menu — to prove the pipeline end-to-end. Extracting every remaining studio string\n * into keys is an ongoing mechanical sweep (see EC-241 \"scope honesty\"), not a\n * silent omission.\n */\nexport const EN_MESSAGES: SourceCatalog = {\n // --- Built-in navigation views (the section rail) ---\n 'nav.cms.label': msg('CMS'),\n 'nav.cms.description': msg('Model collections, fields, and documents.'),\n 'nav.components.label': msg('Components'),\n 'nav.components.description': msg('Browse the registry of primitives and project components.'),\n 'nav.redirects.label': msg('Redirects'),\n 'nav.redirects.description': msg('Create and manage URL redirects (301/302) for this site.'),\n 'nav.assets.label': msg('Assets'),\n 'nav.assets.description': msg('Manage uploaded media and metadata.'),\n 'nav.users.label': msg('Users'),\n 'nav.users.description': msg('Manage who has access to this deployment and their role.'),\n 'nav.extensions.label': msg('Extensions'),\n 'nav.extensions.description': msg(\n 'Install components, field types, adapters, and templates from the marketplace.',\n ),\n 'nav.settings.label': msg('Settings'),\n 'nav.settings.description': msg('Configure project metadata and locales.'),\n\n // --- Current-user menu (shell header) ---\n 'userMenu.accountMenu': msg('Account menu', 'aria-label on the avatar button that opens the user menu.'),\n 'userMenu.lightMode': msg('Light mode', 'Menu item: switch the studio to the light colour theme.'),\n 'userMenu.darkMode': msg('Dark mode', 'Menu item: switch the studio to the dark colour theme.'),\n 'userMenu.accountSettings': msg('Account settings'),\n 'userMenu.soon': msg('Soon', 'Badge on a feature that is not available yet.'),\n 'userMenu.signOut': msg('Sign out'),\n}\n"]}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Studio-UI localization types (EC-241) — the leaf module both the message catalog
3
+ * and the resolver import, so nothing here may import studio runtime code (keeps the
4
+ * `defineStudio` config surface free of import cycles).
5
+ *
6
+ * This is the **studio-chrome** axis (engine-owned strings). Schema labels are a
7
+ * separate mechanism — a {@link import('@elytracms/core/cms-core').LocalizedString}
8
+ * on the field/collection def, resolved via `resolveLabel`. Content-*value*
9
+ * localization is a third, unrelated axis (`cms-core/localization.ts`).
10
+ */
11
+ /** A source-catalog entry: the English string + an optional translator hint. */
12
+ export interface SourceMessage {
13
+ /** The English (source-of-truth) string. */
14
+ message: string;
15
+ /**
16
+ * Translator hint/context — the same word can translate differently by context
17
+ * (the standard `msgctxt`/Lingui-comment idea). **Authoring/translation-time only:**
18
+ * it lives on the in-engine source catalog and is NOT shipped in the flat
19
+ * consumer-provided translation catalogs.
20
+ */
21
+ description?: string;
22
+ }
23
+ /** The in-engine English source catalog: message key → {@link SourceMessage}. */
24
+ export type SourceCatalog = Record<string, SourceMessage>;
25
+ /** A consumer-provided translation catalog: flat message key → localized string. */
26
+ export type MessageCatalog = Record<string, string>;
27
+ /** The studio-UI localization config passed to `defineStudio({ i18n })` (EC-241). */
28
+ export interface StudioI18nConfig {
29
+ /**
30
+ * The locale used when the browser's preferred locale matches nothing available.
31
+ * Defaults to `'en'` (the in-engine source language).
32
+ */
33
+ defaultLocale?: string;
34
+ /**
35
+ * Imported translation catalogs keyed by locale code, e.g. `{ de: deMessages }`.
36
+ * English is always available as the built-in source, so it need not be listed.
37
+ */
38
+ locales?: Record<string, MessageCatalog>;
39
+ }
40
+ /**
41
+ * Author a source-catalog entry with an optional translator hint (EC-241). Kept in
42
+ * this leaf module so `messages.en.ts` can build the catalog without importing the
43
+ * resolver (which would import studio runtime code).
44
+ */
45
+ export declare function msg(message: string, description?: string): SourceMessage;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Studio-UI localization types (EC-241) — the leaf module both the message catalog
3
+ * and the resolver import, so nothing here may import studio runtime code (keeps the
4
+ * `defineStudio` config surface free of import cycles).
5
+ *
6
+ * This is the **studio-chrome** axis (engine-owned strings). Schema labels are a
7
+ * separate mechanism — a {@link import('@elytracms/core/cms-core').LocalizedString}
8
+ * on the field/collection def, resolved via `resolveLabel`. Content-*value*
9
+ * localization is a third, unrelated axis (`cms-core/localization.ts`).
10
+ */
11
+ /**
12
+ * Author a source-catalog entry with an optional translator hint (EC-241). Kept in
13
+ * this leaf module so `messages.en.ts` can build the catalog without importing the
14
+ * resolver (which would import studio runtime code).
15
+ */
16
+ export function msg(message, description) {
17
+ return description !== undefined ? { message, description } : { message };
18
+ }
19
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/i18n/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAmCH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,WAAoB;IACvD,OAAO,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAA;AAC3E,CAAC","sourcesContent":["/**\n * Studio-UI localization types (EC-241) — the leaf module both the message catalog\n * and the resolver import, so nothing here may import studio runtime code (keeps the\n * `defineStudio` config surface free of import cycles).\n *\n * This is the **studio-chrome** axis (engine-owned strings). Schema labels are a\n * separate mechanism — a {@link import('@elytracms/core/cms-core').LocalizedString}\n * on the field/collection def, resolved via `resolveLabel`. Content-*value*\n * localization is a third, unrelated axis (`cms-core/localization.ts`).\n */\n\n/** A source-catalog entry: the English string + an optional translator hint. */\nexport interface SourceMessage {\n /** The English (source-of-truth) string. */\n message: string\n /**\n * Translator hint/context — the same word can translate differently by context\n * (the standard `msgctxt`/Lingui-comment idea). **Authoring/translation-time only:**\n * it lives on the in-engine source catalog and is NOT shipped in the flat\n * consumer-provided translation catalogs.\n */\n description?: string\n}\n\n/** The in-engine English source catalog: message key → {@link SourceMessage}. */\nexport type SourceCatalog = Record<string, SourceMessage>\n\n/** A consumer-provided translation catalog: flat message key → localized string. */\nexport type MessageCatalog = Record<string, string>\n\n/** The studio-UI localization config passed to `defineStudio({ i18n })` (EC-241). */\nexport interface StudioI18nConfig {\n /**\n * The locale used when the browser's preferred locale matches nothing available.\n * Defaults to `'en'` (the in-engine source language).\n */\n defaultLocale?: string\n /**\n * Imported translation catalogs keyed by locale code, e.g. `{ de: deMessages }`.\n * English is always available as the built-in source, so it need not be listed.\n */\n locales?: Record<string, MessageCatalog>\n}\n\n/**\n * Author a source-catalog entry with an optional translator hint (EC-241). Kept in\n * this leaf module so `messages.en.ts` can build the catalog without importing the\n * resolver (which would import studio runtime code).\n */\nexport function msg(message: string, description?: string): SourceMessage {\n return description !== undefined ? { message, description } : { message }\n}\n"]}
@@ -587,6 +587,7 @@ function redirectEntryFromDoc(doc, projectId) {
587
587
  to: doc.to,
588
588
  permanent: doc.permanent,
589
589
  enabled: doc.enabled,
590
+ ...(doc.meta !== undefined ? { meta: doc.meta } : {}),
590
591
  createdAt: doc.createdAt,
591
592
  updatedAt: doc.updatedAt,
592
593
  };
@@ -614,6 +615,7 @@ class ConvexRedirectRepository {
614
615
  to: input.to,
615
616
  permanent: input.permanent ?? true,
616
617
  enabled: input.enabled ?? true,
618
+ ...(input.meta !== undefined ? { meta: input.meta } : {}),
617
619
  createdAt: now,
618
620
  updatedAt: now,
619
621
  }));