@elytracms/studio 0.0.33 → 0.0.35
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/dist/components/app-views/default-modules.js +13 -6
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +7 -6
- package/dist/components/cms/cms-store.js +30 -10
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +29 -7
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.js +4 -3
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/field-renderer.js +2 -1
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/import-export.js +3 -2
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/issues-pane.js +3 -2
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/redirects/redirects-schema.js +10 -5
- package/dist/components/redirects/redirects-schema.js.map +1 -1
- package/dist/components/users/users-schema.js +7 -4
- package/dist/components/users/users-schema.js.map +1 -1
- package/dist/lib/cms-views/view-model.js +3 -2
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/i18n/i18n.d.ts +29 -2
- package/dist/lib/i18n/i18n.js +93 -9
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +1 -1
- package/dist/lib/i18n/index.js +1 -1
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +38 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/i18n/types.d.ts +13 -0
- package/dist/lib/i18n/types.js.map +1 -1
- package/dist/lib/references/reference-model.js +6 -4
- package/dist/lib/references/reference-model.js.map +1 -1
- package/package.json +2 -2
package/dist/lib/i18n/i18n.js
CHANGED
|
@@ -28,12 +28,50 @@ export function resolveActiveLocale(available, preferred, defaultLocale) {
|
|
|
28
28
|
return defaultLocale;
|
|
29
29
|
}
|
|
30
30
|
let localeOverride = null;
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
let serverLanguagesResolver = null;
|
|
32
|
+
/**
|
|
33
|
+
* Provide the request's preferred languages for **SSR** (EC-241). The server has no
|
|
34
|
+
* `navigator`, so without this the server renders the default locale while the client
|
|
35
|
+
* renders the browser locale — a hydration mismatch (server text ≠ client text). A
|
|
36
|
+
* host (e.g. the TanStack Start root) sets a resolver that reads the request's
|
|
37
|
+
* `Accept-Language` header. It is invoked per render, so when the host reads from
|
|
38
|
+
* request-scoped storage (TanStack's AsyncLocalStorage) it stays concurrency-safe —
|
|
39
|
+
* the engine keeps no request state. A no-op on the client, where `navigator` wins.
|
|
40
|
+
*/
|
|
41
|
+
export function setServerLanguagesResolver(fn) {
|
|
42
|
+
serverLanguagesResolver = fn;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse an HTTP `Accept-Language` header into an ordered list of language tags,
|
|
46
|
+
* highest q-weight first: `de,en-US;q=0.7,en;q=0.3` → `['de', 'en-US', 'en']`. The
|
|
47
|
+
* server equivalent of `navigator.languages`, so SSR resolves the same locale the
|
|
48
|
+
* client will.
|
|
49
|
+
*/
|
|
50
|
+
export function parseAcceptLanguage(header) {
|
|
51
|
+
if (!header)
|
|
33
52
|
return [];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
return header
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((part) => {
|
|
56
|
+
const [tag, ...params] = part.trim().split(';');
|
|
57
|
+
const q = params.map((p) => p.trim()).find((p) => p.startsWith('q='));
|
|
58
|
+
const weight = q ? Number.parseFloat(q.slice(2)) : 1;
|
|
59
|
+
return { tag: (tag ?? '').trim(), weight: Number.isFinite(weight) ? weight : 1 };
|
|
60
|
+
})
|
|
61
|
+
.filter((entry) => entry.tag !== '' && entry.tag !== '*')
|
|
62
|
+
.sort((a, b) => b.weight - a.weight)
|
|
63
|
+
.map((entry) => entry.tag);
|
|
64
|
+
}
|
|
65
|
+
function navigatorLanguages() {
|
|
66
|
+
if (typeof navigator !== 'undefined') {
|
|
67
|
+
if (navigator.languages && navigator.languages.length > 0)
|
|
68
|
+
return [...navigator.languages];
|
|
69
|
+
if (navigator.language)
|
|
70
|
+
return [navigator.language];
|
|
71
|
+
}
|
|
72
|
+
// SSR (no navigator): a host may resolve the request's Accept-Language.
|
|
73
|
+
const server = serverLanguagesResolver?.();
|
|
74
|
+
return server ? [...server] : [];
|
|
37
75
|
}
|
|
38
76
|
const warned = new Set();
|
|
39
77
|
function warnOnce(message) {
|
|
@@ -59,7 +97,18 @@ export function resolveStudioI18n() {
|
|
|
59
97
|
const defaultLocale = config.defaultLocale ?? BUILTIN_DEFAULT_LOCALE;
|
|
60
98
|
const locales = config.locales ?? {};
|
|
61
99
|
const available = [defaultLocale, ...Object.keys(locales).filter((l) => l !== defaultLocale)];
|
|
62
|
-
|
|
100
|
+
// A config-pinned locale (EC-241) takes precedence over browser detection: it is read
|
|
101
|
+
// from config — identical on server and client — so SSR and hydration agree (no
|
|
102
|
+
// navigator-vs-server split). An unavailable pin is ignored with a visible signal
|
|
103
|
+
// ("failures are first-class"), and detection resumes.
|
|
104
|
+
let pinned;
|
|
105
|
+
if (config.locale !== undefined) {
|
|
106
|
+
if (available.includes(config.locale))
|
|
107
|
+
pinned = config.locale;
|
|
108
|
+
else
|
|
109
|
+
warnOnce(`[i18n] pinned locale "${config.locale}" is not available (default "${defaultLocale}" + ${JSON.stringify(Object.keys(locales))}); using browser detection.`);
|
|
110
|
+
}
|
|
111
|
+
const locale = localeOverride ?? pinned ?? resolveActiveLocale(available, navigatorLanguages(), defaultLocale);
|
|
63
112
|
const active = locale === defaultLocale ? {} : (locales[locale] ?? {});
|
|
64
113
|
// Coverage lint: warn once when the active locale doesn't cover every English key.
|
|
65
114
|
if (locale !== defaultLocale && locales[locale]) {
|
|
@@ -89,15 +138,49 @@ export function translate(key, vars) {
|
|
|
89
138
|
}
|
|
90
139
|
return vars ? interpolate(hit, vars) : hit;
|
|
91
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* A **catalog-backed** schema label (EC-241): a label whose text comes from the message
|
|
143
|
+
* catalog by `key`, not a hard-coded string or a per-locale map. This is how the
|
|
144
|
+
* engine's own built-in schemas (the redirects/users rails) localize — their default
|
|
145
|
+
* labels resolve through the *same* consumer catalog as the rest of the chrome, so one
|
|
146
|
+
* German catalog covers them with no per-rail override and no per-language data in
|
|
147
|
+
* engine code. Consumers never author these (their labels are plain strings or
|
|
148
|
+
* `{ en, de }` maps); it is an engine-internal seam.
|
|
149
|
+
*
|
|
150
|
+
* Encoded as a sentinel-prefixed string so it is a valid {@link LocalizedString} (no
|
|
151
|
+
* change to the serializable schema type) and survives zod parse / object spread — it
|
|
152
|
+
* is decoded only at the final render site by {@link resolveMsgLabel}. On the delivery
|
|
153
|
+
* side (no catalog) `resolveLabel` returns it verbatim, which is harmless: the built-in
|
|
154
|
+
* rails never render there.
|
|
155
|
+
*/
|
|
156
|
+
const MSG_LABEL_SENTINEL = '@@msg:';
|
|
157
|
+
export function msgLabel(key) {
|
|
158
|
+
return MSG_LABEL_SENTINEL + key;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Decode a {@link msgLabel} to its translated string for the active locale, or
|
|
162
|
+
* `undefined` when the value is not a catalog-backed label (a plain string / locale
|
|
163
|
+
* map / absent). Every studio label read site checks this first, then falls back to
|
|
164
|
+
* {@link resolveLabel} for ordinary labels.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveMsgLabel(value) {
|
|
167
|
+
return typeof value === 'string' && value.startsWith(MSG_LABEL_SENTINEL)
|
|
168
|
+
? translate(value.slice(MSG_LABEL_SENTINEL.length))
|
|
169
|
+
: undefined;
|
|
170
|
+
}
|
|
92
171
|
/**
|
|
93
172
|
* Resolve a {@link LocalizedString} (field/collection/option label) against the
|
|
94
173
|
* active UI locale (EC-241) — **non-hook**, reading the per-boot singleton, so it is
|
|
95
174
|
* safe to call anywhere: a component body, a nested render helper, or config-build
|
|
96
175
|
* code. Returns `fallback` (default `''`) when the value is absent. Prefer this over
|
|
97
176
|
* `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.
|
|
177
|
+
* it can't trip the rules-of-hooks in a conditional/nested position. A catalog-backed
|
|
178
|
+
* {@link msgLabel} (the built-in rails) resolves through the message catalog.
|
|
99
179
|
*/
|
|
100
180
|
export function localizedLabel(value, fallback = '') {
|
|
181
|
+
const fromCatalog = resolveMsgLabel(value);
|
|
182
|
+
if (fromCatalog !== undefined)
|
|
183
|
+
return fromCatalog;
|
|
101
184
|
const { locale, defaultLocale } = resolveStudioI18n();
|
|
102
185
|
return resolveLabel(value, locale, defaultLocale) ?? fallback;
|
|
103
186
|
}
|
|
@@ -117,15 +200,16 @@ export function useT() {
|
|
|
117
200
|
*/
|
|
118
201
|
export function useLabel() {
|
|
119
202
|
const { locale, defaultLocale } = resolveStudioI18n();
|
|
120
|
-
return useMemo(() => (value, fallback) => resolveLabel(value, locale, defaultLocale) ?? fallback ?? '', [locale, defaultLocale]);
|
|
203
|
+
return useMemo(() => (value, fallback) => resolveMsgLabel(value) ?? resolveLabel(value, locale, defaultLocale) ?? fallback ?? '', [locale, defaultLocale]);
|
|
121
204
|
}
|
|
122
205
|
/** Test seam: force the active locale. Never used by app code. */
|
|
123
206
|
export function setUiLocaleForTests(locale) {
|
|
124
207
|
localeOverride = locale;
|
|
125
208
|
warned.clear();
|
|
126
209
|
}
|
|
127
|
-
/** Test seam: reset i18n state (
|
|
210
|
+
/** Test seam: reset i18n state (once-per-boot warnings + the SSR resolver). Never used by app code. */
|
|
128
211
|
export function resetStudioI18nForTests() {
|
|
129
212
|
warned.clear();
|
|
213
|
+
serverLanguagesResolver = null;
|
|
130
214
|
}
|
|
131
215
|
//# 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 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"]}
|
|
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\ntype LanguagesResolver = () => readonly string[] | null | undefined\nlet serverLanguagesResolver: LanguagesResolver | null = null\n\n/**\n * Provide the request's preferred languages for **SSR** (EC-241). The server has no\n * `navigator`, so without this the server renders the default locale while the client\n * renders the browser locale — a hydration mismatch (server text ≠ client text). A\n * host (e.g. the TanStack Start root) sets a resolver that reads the request's\n * `Accept-Language` header. It is invoked per render, so when the host reads from\n * request-scoped storage (TanStack's AsyncLocalStorage) it stays concurrency-safe —\n * the engine keeps no request state. A no-op on the client, where `navigator` wins.\n */\nexport function setServerLanguagesResolver(fn: LanguagesResolver | null): void {\n serverLanguagesResolver = fn\n}\n\n/**\n * Parse an HTTP `Accept-Language` header into an ordered list of language tags,\n * highest q-weight first: `de,en-US;q=0.7,en;q=0.3` → `['de', 'en-US', 'en']`. The\n * server equivalent of `navigator.languages`, so SSR resolves the same locale the\n * client will.\n */\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return []\n return header\n .split(',')\n .map((part) => {\n const [tag, ...params] = part.trim().split(';')\n const q = params.map((p) => p.trim()).find((p) => p.startsWith('q='))\n const weight = q ? Number.parseFloat(q.slice(2)) : 1\n return { tag: (tag ?? '').trim(), weight: Number.isFinite(weight) ? weight : 1 }\n })\n .filter((entry) => entry.tag !== '' && entry.tag !== '*')\n .sort((a, b) => b.weight - a.weight)\n .map((entry) => entry.tag)\n}\n\nfunction navigatorLanguages(): string[] {\n if (typeof navigator !== 'undefined') {\n if (navigator.languages && navigator.languages.length > 0) return [...navigator.languages]\n if (navigator.language) return [navigator.language]\n }\n // SSR (no navigator): a host may resolve the request's Accept-Language.\n const server = serverLanguagesResolver?.()\n return server ? [...server] : []\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 // A config-pinned locale (EC-241) takes precedence over browser detection: it is read\n // from config — identical on server and client — so SSR and hydration agree (no\n // navigator-vs-server split). An unavailable pin is ignored with a visible signal\n // (\"failures are first-class\"), and detection resumes.\n let pinned: string | undefined\n if (config.locale !== undefined) {\n if (available.includes(config.locale)) pinned = config.locale\n else\n warnOnce(\n `[i18n] pinned locale \"${config.locale}\" is not available (default \"${defaultLocale}\" + ${JSON.stringify(Object.keys(locales))}); using browser detection.`,\n )\n }\n const locale =\n localeOverride ?? pinned ?? 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 * A **catalog-backed** schema label (EC-241): a label whose text comes from the message\n * catalog by `key`, not a hard-coded string or a per-locale map. This is how the\n * engine's own built-in schemas (the redirects/users rails) localize — their default\n * labels resolve through the *same* consumer catalog as the rest of the chrome, so one\n * German catalog covers them with no per-rail override and no per-language data in\n * engine code. Consumers never author these (their labels are plain strings or\n * `{ en, de }` maps); it is an engine-internal seam.\n *\n * Encoded as a sentinel-prefixed string so it is a valid {@link LocalizedString} (no\n * change to the serializable schema type) and survives zod parse / object spread — it\n * is decoded only at the final render site by {@link resolveMsgLabel}. On the delivery\n * side (no catalog) `resolveLabel` returns it verbatim, which is harmless: the built-in\n * rails never render there.\n */\nconst MSG_LABEL_SENTINEL = '@@msg:'\nexport function msgLabel(key: string): LocalizedString {\n return MSG_LABEL_SENTINEL + key\n}\n\n/**\n * Decode a {@link msgLabel} to its translated string for the active locale, or\n * `undefined` when the value is not a catalog-backed label (a plain string / locale\n * map / absent). Every studio label read site checks this first, then falls back to\n * {@link resolveLabel} for ordinary labels.\n */\nexport function resolveMsgLabel(value: unknown): string | undefined {\n return typeof value === 'string' && value.startsWith(MSG_LABEL_SENTINEL)\n ? translate(value.slice(MSG_LABEL_SENTINEL.length))\n : undefined\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. A catalog-backed\n * {@link msgLabel} (the built-in rails) resolves through the message catalog.\n */\nexport function localizedLabel(value: LocalizedString | undefined, fallback = ''): string {\n const fromCatalog = resolveMsgLabel(value)\n if (fromCatalog !== undefined) return fromCatalog\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) =>\n resolveMsgLabel(value) ?? 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 (once-per-boot warnings + the SSR resolver). Never used by app code. */\nexport function resetStudioI18nForTests(): void {\n warned.clear()\n serverLanguagesResolver = null\n}\n"]}
|
package/dist/lib/i18n/index.d.ts
CHANGED
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
export type { SourceMessage, SourceCatalog, MessageCatalog, StudioI18nConfig } from './types';
|
|
10
10
|
export { msg } from './types';
|
|
11
11
|
export { EN_MESSAGES } from './messages.en';
|
|
12
|
-
export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, useT, useUiLocale, useLabel, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n';
|
|
12
|
+
export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, msgLabel, resolveMsgLabel, useT, useUiLocale, useLabel, parseAcceptLanguage, setServerLanguagesResolver, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n';
|
package/dist/lib/i18n/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { msg } from './types';
|
|
2
2
|
export { EN_MESSAGES } from './messages.en';
|
|
3
|
-
export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, useT, useUiLocale, useLabel, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n';
|
|
3
|
+
export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, msgLabel, resolveMsgLabel, useT, useUiLocale, useLabel, parseAcceptLanguage, setServerLanguagesResolver, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +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"]}
|
|
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,QAAQ,EACR,eAAe,EACf,IAAI,EACJ,WAAW,EACX,QAAQ,EACR,mBAAmB,EACnB,0BAA0B,EAC1B,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 msgLabel,\n resolveMsgLabel,\n useT,\n useUiLocale,\n useLabel,\n parseAcceptLanguage,\n setServerLanguagesResolver,\n setUiLocaleForTests,\n resetStudioI18nForTests,\n} from './i18n'\n"]}
|
|
@@ -258,6 +258,29 @@ export const EN_MESSAGES = {
|
|
|
258
258
|
'cms.editor.slugUrlHint': msg('The URL is derived from the slug (and parent) — set a slug to define it.'),
|
|
259
259
|
'cms.editor.embedUnpublishedIssue': msg('Embeds the reusable “{title}”, which isn’t published — it will be empty on the live site until you publish it.'),
|
|
260
260
|
'cms.editor.embedMissingIssue': msg('Embeds a reusable that no longer exists (“{id}”) — fix or remove the embed.'),
|
|
261
|
+
'cms.editor.defaultGroup': msg('Fields', 'Title of the implicit fieldset that holds ungrouped fields.'),
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
263
|
+
// CMS — issues pane (validation)
|
|
264
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
265
|
+
'cms.issues.none': msg('No issues'),
|
|
266
|
+
'cms.issues.satisfied': msg('This document satisfies the collection schema.'),
|
|
267
|
+
'cms.issues.goToField': msg('Go to {field}'),
|
|
268
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
269
|
+
// CMS — details pane (slot + sidebar fields)
|
|
270
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
271
|
+
'cms.details.slot': msg('Slot', 'A named handle the site uses to find a document (e.g. the main menu).'),
|
|
272
|
+
'cms.details.noSlot': msg('— No slot —'),
|
|
273
|
+
'cms.details.slotInUse': msg(' — in use', 'Suffix on a slot option already taken by another document.'),
|
|
274
|
+
'cms.details.slotHint': msg('A stable name the site uses to find this document (e.g. the main menu), instead of which exact entry it is. One document per slot.'),
|
|
275
|
+
'cms.details.empty': msg('No document properties to set.'),
|
|
276
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
277
|
+
// CMS — import dialog
|
|
278
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
279
|
+
'cms.import.title': msg('Import documents'),
|
|
280
|
+
'cms.import.invalidJson': msg('Input is not valid JSON.'),
|
|
281
|
+
'cms.import.imported': msg('{count} imported'),
|
|
282
|
+
'cms.import.rejected': msg('{count} rejected:'),
|
|
283
|
+
'cms.import.confirm': msg('Import', 'Confirm button in the import dialog.'),
|
|
261
284
|
// ─────────────────────────────────────────────────────────────────────────
|
|
262
285
|
// CMS — versions (compare + pane)
|
|
263
286
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -347,6 +370,7 @@ export const EN_MESSAGES = {
|
|
|
347
370
|
'cms.field.addItem': msg('Add item'),
|
|
348
371
|
'cms.field.value': msg('Value', 'Default aria-label for the scalar value editor.'),
|
|
349
372
|
'cms.field.valuePlaceholder': msg('value'),
|
|
373
|
+
'cms.field.colour': msg('Colour', 'aria-label on the colour-swatch field control.'),
|
|
350
374
|
// ─────────────────────────────────────────────────────────────────────────
|
|
351
375
|
// CMS — relation picker
|
|
352
376
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -436,11 +460,25 @@ export const EN_MESSAGES = {
|
|
|
436
460
|
'users.restrictedBody': msg('Managing users and roles requires the admin role on this deployment. Ask an admin to grant you access.'),
|
|
437
461
|
'users.loadFailed': msg('Users failed to load'),
|
|
438
462
|
'users.loading': msg('Loading users…'),
|
|
463
|
+
// Built-in users rail — its schema labels resolve through the catalog (msgLabel),
|
|
464
|
+
// so a consumer's translation catalog localizes the rail with no per-rail override.
|
|
465
|
+
'users.rail.label': msg('Users'),
|
|
466
|
+
'users.rail.labelSingular': msg('user', 'Singular noun for one users-rail row, e.g. in a header.'),
|
|
467
|
+
'users.field.name.label': msg('Name'),
|
|
468
|
+
'users.field.email.label': msg('Email'),
|
|
469
|
+
'users.field.role.label': msg('Role'),
|
|
439
470
|
// ─────────────────────────────────────────────────────────────────────────
|
|
440
471
|
// Redirects (section)
|
|
441
472
|
// ─────────────────────────────────────────────────────────────────────────
|
|
442
473
|
'redirects.loadFailed': msg('Redirects failed to load'),
|
|
443
474
|
'redirects.loading': msg('Loading redirects…'),
|
|
475
|
+
// Built-in redirects rail — schema labels via the catalog (msgLabel), see above.
|
|
476
|
+
'redirects.rail.label': msg('Redirects'),
|
|
477
|
+
'redirects.rail.labelSingular': msg('redirect', 'Singular noun for one redirect row, e.g. in a header.'),
|
|
478
|
+
'redirects.field.from.label': msg('From'),
|
|
479
|
+
'redirects.field.to.label': msg('To'),
|
|
480
|
+
'redirects.field.permanent.label': msg('Permanent (301)'),
|
|
481
|
+
'redirects.field.enabled.label': msg('Enabled'),
|
|
444
482
|
// ─────────────────────────────────────────────────────────────────────────
|
|
445
483
|
// References panel
|
|
446
484
|
// ─────────────────────────────────────────────────────────────────────────
|