@elytracms/studio 0.0.34 → 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/cms/cms-store.js +13 -1
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +23 -6
- package/dist/components/cms/detail-editor.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/i18n/i18n.d.ts +10 -1
- package/dist/lib/i18n/i18n.js +48 -3
- 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 +14 -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/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { collectionDefSchema } from '@elytracms/core/cms-core';
|
|
2
2
|
import { roleOptions } from '@elytracms/core/persistence';
|
|
3
|
+
import { msgLabel } from '@elytracms/studio/lib/i18n';
|
|
3
4
|
/**
|
|
4
5
|
* The built-in **users** field lens (EC-240, AD-19). Users are a separate rail —
|
|
5
6
|
* NOT a `cmsDocuments` collection (`collectionKindSchema` is `document|asset`) —
|
|
@@ -22,16 +23,18 @@ export const usersCollectionDef = collectionDefSchema.parse({
|
|
|
22
23
|
// source's capabilities (adminOnly, no publish/versions/create) carry the semantics.
|
|
23
24
|
kind: 'document',
|
|
24
25
|
titleField: 'name',
|
|
25
|
-
|
|
26
|
+
// EC-241: built-in rail labels resolve through the message catalog (msgLabel) — a
|
|
27
|
+
// consumer's translation catalog localizes them without a per-rail override.
|
|
28
|
+
form: { label: msgLabel('users.rail.label'), labelSingular: msgLabel('users.rail.labelSingular') },
|
|
26
29
|
fields: [
|
|
27
|
-
{ name: 'name', type: 'text', form: { label: '
|
|
28
|
-
{ name: 'email', type: 'text', form: { label: '
|
|
30
|
+
{ name: 'name', type: 'text', form: { label: msgLabel('users.field.name.label'), readOnly: true }, filterable: true },
|
|
31
|
+
{ name: 'email', type: 'text', form: { label: msgLabel('users.field.email.label'), readOnly: true }, filterable: true },
|
|
29
32
|
{
|
|
30
33
|
name: 'role',
|
|
31
34
|
type: 'select',
|
|
32
35
|
options: [...USER_ROLE_OPTIONS],
|
|
33
36
|
filterable: true,
|
|
34
|
-
form: { label: '
|
|
37
|
+
form: { label: msgLabel('users.field.role.label') },
|
|
35
38
|
},
|
|
36
39
|
],
|
|
37
40
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users-schema.js","sourceRoot":"","sources":["../../../src/components/users/users-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"users-schema.js","sourceRoot":"","sources":["../../../src/components/users/users-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAErD;;;;;;;;;;;;GAYG;AACH,kFAAkF;AAClF,oFAAoF;AACpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,EAAE,CAAA;AAE9C,MAAM,CAAC,MAAM,kBAAkB,GAAkB,mBAAmB,CAAC,KAAK,CAAC;IACzE,EAAE,EAAE,MAAM;IACV,4EAA4E;IAC5E,qFAAqF;IACrF,IAAI,EAAE,UAAU;IAChB,UAAU,EAAE,MAAM;IAClB,kFAAkF;IAClF,6EAA6E;IAC7E,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,0BAA0B,CAAC,EAAE;IAClG,MAAM,EAAE;QACN,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;QACrH,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,yBAAyB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;QACvH;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,CAAC,GAAG,iBAAiB,CAAC;YAC/B,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAAE;SACpD;KACF;CACF,CAAC,CAAA","sourcesContent":["import { collectionDefSchema } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { roleOptions } from '@elytracms/core/persistence'\nimport { msgLabel } from '@elytracms/studio/lib/i18n'\n\n/**\n * The built-in **users** field lens (EC-240, AD-19). Users are a separate rail —\n * NOT a `cmsDocuments` collection (`collectionKindSchema` is `document|asset`) —\n * so this `CollectionDef` exists only to drive the portable list/edit surfaces\n * over the users {@link CollectionViewSource}. The rail's *user-ness* comes from\n * the source's capabilities (admin-only, no publish/versions/create), never the\n * schema `kind`.\n *\n * Identity fields (`name`, `email`) are **auth-owned** and read-only here — an\n * admin manages a member's *role* (and, later, project-declared custom fields\n * over the `meta` bag, mirroring assets); the person edits their own name/email\n * via account settings. A v1 default; a project may extend the field set later.\n */\n// Derived from the ONE role policy (EC-240c) — no hardcoded duplicate of the role\n// set. Reflects the shipped default today; a project's custom policy flows through.\nexport const USER_ROLE_OPTIONS = roleOptions()\n\nexport const usersCollectionDef: CollectionDef = collectionDefSchema.parse({\n id: 'user',\n // Users aren't a CMS `kind`; 'document' just satisfies the lens schema. The\n // source's capabilities (adminOnly, no publish/versions/create) carry the semantics.\n kind: 'document',\n titleField: 'name',\n // EC-241: built-in rail labels resolve through the message catalog (msgLabel) — a\n // consumer's translation catalog localizes them without a per-rail override.\n form: { label: msgLabel('users.rail.label'), labelSingular: msgLabel('users.rail.labelSingular') },\n fields: [\n { name: 'name', type: 'text', form: { label: msgLabel('users.field.name.label'), readOnly: true }, filterable: true },\n { name: 'email', type: 'text', form: { label: msgLabel('users.field.email.label'), readOnly: true }, filterable: true },\n {\n name: 'role',\n type: 'select',\n options: [...USER_ROLE_OPTIONS],\n filterable: true,\n form: { label: msgLabel('users.field.role.label') },\n },\n ],\n})\n"]}
|
package/dist/lib/i18n/i18n.d.ts
CHANGED
|
@@ -51,13 +51,22 @@ export declare function resolveStudioI18n(): ResolvedI18n;
|
|
|
51
51
|
* is the React-idiomatic wrapper.
|
|
52
52
|
*/
|
|
53
53
|
export declare function translate(key: string, vars?: Record<string, string | number>): string;
|
|
54
|
+
export declare function msgLabel(key: string): LocalizedString;
|
|
55
|
+
/**
|
|
56
|
+
* Decode a {@link msgLabel} to its translated string for the active locale, or
|
|
57
|
+
* `undefined` when the value is not a catalog-backed label (a plain string / locale
|
|
58
|
+
* map / absent). Every studio label read site checks this first, then falls back to
|
|
59
|
+
* {@link resolveLabel} for ordinary labels.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveMsgLabel(value: unknown): string | undefined;
|
|
54
62
|
/**
|
|
55
63
|
* Resolve a {@link LocalizedString} (field/collection/option label) against the
|
|
56
64
|
* active UI locale (EC-241) — **non-hook**, reading the per-boot singleton, so it is
|
|
57
65
|
* safe to call anywhere: a component body, a nested render helper, or config-build
|
|
58
66
|
* code. Returns `fallback` (default `''`) when the value is absent. Prefer this over
|
|
59
67
|
* `resolveLabel(x, useUiLocale())` at render sites — it localizes without a hook, so
|
|
60
|
-
* it can't trip the rules-of-hooks in a conditional/nested position.
|
|
68
|
+
* it can't trip the rules-of-hooks in a conditional/nested position. A catalog-backed
|
|
69
|
+
* {@link msgLabel} (the built-in rails) resolves through the message catalog.
|
|
61
70
|
*/
|
|
62
71
|
export declare function localizedLabel(value: LocalizedString | undefined, fallback?: string): string;
|
|
63
72
|
/** The active studio-UI locale (browser-resolved). */
|
package/dist/lib/i18n/i18n.js
CHANGED
|
@@ -97,7 +97,18 @@ export function resolveStudioI18n() {
|
|
|
97
97
|
const defaultLocale = config.defaultLocale ?? BUILTIN_DEFAULT_LOCALE;
|
|
98
98
|
const locales = config.locales ?? {};
|
|
99
99
|
const available = [defaultLocale, ...Object.keys(locales).filter((l) => l !== defaultLocale)];
|
|
100
|
-
|
|
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);
|
|
101
112
|
const active = locale === defaultLocale ? {} : (locales[locale] ?? {});
|
|
102
113
|
// Coverage lint: warn once when the active locale doesn't cover every English key.
|
|
103
114
|
if (locale !== defaultLocale && locales[locale]) {
|
|
@@ -127,15 +138,49 @@ export function translate(key, vars) {
|
|
|
127
138
|
}
|
|
128
139
|
return vars ? interpolate(hit, vars) : hit;
|
|
129
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
|
+
}
|
|
130
171
|
/**
|
|
131
172
|
* Resolve a {@link LocalizedString} (field/collection/option label) against the
|
|
132
173
|
* active UI locale (EC-241) — **non-hook**, reading the per-boot singleton, so it is
|
|
133
174
|
* safe to call anywhere: a component body, a nested render helper, or config-build
|
|
134
175
|
* code. Returns `fallback` (default `''`) when the value is absent. Prefer this over
|
|
135
176
|
* `resolveLabel(x, useUiLocale())` at render sites — it localizes without a hook, so
|
|
136
|
-
* 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.
|
|
137
179
|
*/
|
|
138
180
|
export function localizedLabel(value, fallback = '') {
|
|
181
|
+
const fromCatalog = resolveMsgLabel(value);
|
|
182
|
+
if (fromCatalog !== undefined)
|
|
183
|
+
return fromCatalog;
|
|
139
184
|
const { locale, defaultLocale } = resolveStudioI18n();
|
|
140
185
|
return resolveLabel(value, locale, defaultLocale) ?? fallback;
|
|
141
186
|
}
|
|
@@ -155,7 +200,7 @@ export function useT() {
|
|
|
155
200
|
*/
|
|
156
201
|
export function useLabel() {
|
|
157
202
|
const { locale, defaultLocale } = resolveStudioI18n();
|
|
158
|
-
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]);
|
|
159
204
|
}
|
|
160
205
|
/** Test seam: force the active locale. Never used by app code. */
|
|
161
206
|
export function setUiLocaleForTests(locale) {
|
|
@@ -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\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 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 (once-per-boot warnings + the SSR resolver). Never used by app code. */\nexport function resetStudioI18nForTests(): void {\n warned.clear()\n serverLanguagesResolver = null\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, parseAcceptLanguage, setServerLanguagesResolver, 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, parseAcceptLanguage, setServerLanguagesResolver, 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,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 useT,\n useUiLocale,\n useLabel,\n parseAcceptLanguage,\n setServerLanguagesResolver,\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"]}
|
|
@@ -460,11 +460,25 @@ export const EN_MESSAGES = {
|
|
|
460
460
|
'users.restrictedBody': msg('Managing users and roles requires the admin role on this deployment. Ask an admin to grant you access.'),
|
|
461
461
|
'users.loadFailed': msg('Users failed to load'),
|
|
462
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'),
|
|
463
470
|
// ─────────────────────────────────────────────────────────────────────────
|
|
464
471
|
// Redirects (section)
|
|
465
472
|
// ─────────────────────────────────────────────────────────────────────────
|
|
466
473
|
'redirects.loadFailed': msg('Redirects failed to load'),
|
|
467
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'),
|
|
468
482
|
// ─────────────────────────────────────────────────────────────────────────
|
|
469
483
|
// References panel
|
|
470
484
|
// ─────────────────────────────────────────────────────────────────────────
|