@elytracms/studio 0.0.81 → 0.0.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/redirect-revalidate.test.ts +130 -0
- package/convex/revalidate.ts +9 -7
- package/convex/triggers.ts +33 -3
- package/dist/components/ai/ai-chat-panel.js +26 -4
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +27 -0
- package/dist/components/ai/proposal-card.js +31 -3
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.d.ts +55 -0
- package/dist/components/ai/proposal-issues.js +69 -0
- package/dist/components/ai/proposal-issues.js.map +1 -0
- package/dist/components/ai/translate-document-dialog.js +38 -3
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +1 -1
- package/dist/components/app-views/studio-config.js +1 -1
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +3 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +8 -5
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +18 -3
- package/dist/components/cms/cell-edit.js +27 -9
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +14 -1
- package/dist/components/cms/cms-store.js +93 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +1 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +7 -12
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +12 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/field-control.js +20 -7
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +36 -0
- package/dist/components/cms/gate-validation.js +17 -0
- package/dist/components/cms/gate-validation.js.map +1 -0
- package/dist/components/cms/inline-field-errors.d.ts +8 -0
- package/dist/components/cms/inline-field-errors.js +52 -0
- package/dist/components/cms/inline-field-errors.js.map +1 -0
- package/dist/components/cms/issues-pane.js +8 -4
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/slug-collision-report.d.ts +41 -0
- package/dist/components/cms/slug-collision-report.js +52 -0
- package/dist/components/cms/slug-collision-report.js.map +1 -0
- package/dist/components/deployment-setup-gate.d.ts +24 -0
- package/dist/components/deployment-setup-gate.js +29 -4
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/issue-severity.d.ts +73 -0
- package/dist/components/issue-severity.js +56 -0
- package/dist/components/issue-severity.js.map +1 -0
- package/dist/components/nav-issues.d.ts +68 -0
- package/dist/components/nav-issues.js +78 -0
- package/dist/components/nav-issues.js.map +1 -0
- package/dist/components/operation-error.js +3 -3
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/section-nav.js +5 -38
- package/dist/components/section-nav.js.map +1 -1
- package/dist/lib/ai/apply-content.js +6 -7
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +32 -26
- package/dist/lib/ai/chat-context.js +81 -9
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +42 -0
- package/dist/lib/ai/constraints.js +259 -0
- package/dist/lib/ai/constraints.js.map +1 -0
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/proposal-validation.d.ts +26 -0
- package/dist/lib/ai/proposal-validation.js +17 -0
- package/dist/lib/ai/proposal-validation.js.map +1 -0
- package/dist/lib/ai/shape.js +60 -13
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -4
- package/dist/lib/ai/tools.js +43 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +35 -1
- package/dist/lib/ai/translate-document.js +16 -1
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +11 -6
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +8 -6
- package/dist/lib/ai/update-fields.js +9 -22
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +5 -0
- package/dist/lib/clipboard/paste-issues.js +16 -6
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/cms/document-gate.d.ts +37 -0
- package/dist/lib/cms/document-gate.js +40 -0
- package/dist/lib/cms/document-gate.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +5 -0
- package/dist/lib/cms-views/view-model.js +10 -4
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.js +1 -3
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/i18n.d.ts +51 -16
- package/dist/lib/i18n/i18n.js +119 -43
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +8 -1
- package/dist/lib/i18n/index.js +1 -1
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +68 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +59 -0
- package/dist/lib/i18n/messages.en.js +80 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/i18n/types.d.ts +15 -9
- package/dist/lib/i18n/types.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +19 -2
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.js +10 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +44 -9
- package/dist/lib/persona/labels.js +143 -60
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/webhooks/dispatcher.d.ts +5 -2
- package/dist/lib/webhooks/dispatcher.js.map +1 -1
- package/dist/lib/webhooks/tags.js +150 -17
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/config-issues.d.ts +3 -1
- package/dist/lib/workspace/config-issues.js +3 -1
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +15 -8
- package/dist/lib/workspace/index.js +17 -18
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +27 -0
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +13 -6
- package/dist/previews/manifest-format.js +9 -2
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/root.d.ts +36 -1
- package/dist/router/root.js +54 -7
- package/dist/router/root.js.map +1 -1
- package/package.json +5 -2
package/dist/lib/i18n/i18n.js
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
2
2
|
import { resolveLabel } from '@elytracms/core/cms-core';
|
|
3
3
|
import { resolveDeploymentI18n } from '@elytracms/studio/lib/workspace';
|
|
4
4
|
import { EN_MESSAGES } from './messages.en.js';
|
|
5
5
|
/**
|
|
6
|
-
* Studio-UI localization runtime (EC-241). Resolves the active UI locale
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* Studio-UI localization runtime (EC-241). Resolves the active UI locale, then
|
|
7
|
+
* translates message keys with an active-locale → English → visible-fallback chain.
|
|
8
|
+
*
|
|
9
|
+
* **Locale resolution is client-side** (EC-513, owner-ratified 2026-08-01: "the studio
|
|
10
|
+
* is primarily client-side and locales should be too"). Two mechanisms, in precedence
|
|
11
|
+
* order:
|
|
12
|
+
*
|
|
13
|
+
* 1. **`defineStudio({ i18n: { locale } })` — the supported way to fix the UI language.**
|
|
14
|
+
* A pin is read from *config*, which is imported identically on server and client, so
|
|
15
|
+
* SSR and the browser render the same locale with no header plumbing and no flash.
|
|
16
|
+
* Both production consumers pin `de`.
|
|
17
|
+
* 2. **Browser detection** (`navigator.languages`) when nothing is pinned. It is
|
|
18
|
+
* **deferred until after hydration** — see {@link subscribeToUiLocale}. The server
|
|
19
|
+
* has no `navigator`, so it renders the default/pinned locale; the first client
|
|
20
|
+
* render must produce byte-identical output or React discards the whole tree with a
|
|
21
|
+
* hydration error (measured EC-513 under `de-DE`: the auth gate's "Checking session…").
|
|
22
|
+
*
|
|
23
|
+
* There is deliberately **no server-side resolver seam**. `setServerLanguagesResolver` +
|
|
24
|
+
* `parseAcceptLanguage` shipped exported and unit-tested from EC-241 but were never
|
|
25
|
+
* called by anything in production — not the demo, not the scaffold template, neither
|
|
26
|
+
* consumer — and EC-241 itself had already measured that TanStack Start's request-header
|
|
27
|
+
* helper is unreachable from a normal SSR route. EC-513 deleted both.
|
|
28
|
+
*
|
|
29
|
+
* No persisted locale selector in v1 (that rides EC-240's deferred account settings).
|
|
12
30
|
*/
|
|
13
31
|
const BUILTIN_DEFAULT_LOCALE = 'en';
|
|
14
32
|
/**
|
|
@@ -28,50 +46,95 @@ export function resolveActiveLocale(available, preferred, defaultLocale) {
|
|
|
28
46
|
return defaultLocale;
|
|
29
47
|
}
|
|
30
48
|
let localeOverride = null;
|
|
31
|
-
|
|
49
|
+
// --- post-hydration browser-locale detection (EC-513) ---
|
|
50
|
+
let browserDetectionEnabled = false;
|
|
51
|
+
const localeListeners = new Set();
|
|
32
52
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
53
|
+
* Is `navigator` allowed to influence the resolved locale yet?
|
|
54
|
+
*
|
|
55
|
+
* In a **browser** it is not, until the first committed render of a locale consumer has
|
|
56
|
+
* run — the hydration render has to reproduce the server's markup exactly, and the
|
|
57
|
+
* server never sees `navigator`. Outside a browser (SSR, node tests) there is nothing
|
|
58
|
+
* to defer: `navigatorLanguages()` there is either genuinely absent (server → `[]`, the
|
|
59
|
+
* default locale, which is precisely what must be reproduced) or deliberately stubbed
|
|
60
|
+
* by a test.
|
|
40
61
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
function browserDetectionReady() {
|
|
63
|
+
return browserDetectionEnabled || typeof window === 'undefined';
|
|
43
64
|
}
|
|
44
65
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
66
|
+
* Subscribe to the resolved UI locale, and — on the first subscription — switch browser
|
|
67
|
+
* detection on (EC-513).
|
|
68
|
+
*
|
|
69
|
+
* This is the whole post-hydration handover. `useSyncExternalStore` subscribes in a
|
|
70
|
+
* commit effect, i.e. **after** React has hydrated the server markup, so every render up
|
|
71
|
+
* to that point resolves the same default/pinned locale the server did. Flipping the flag
|
|
72
|
+
* changes the snapshot, React re-renders, and from then on `navigator.languages` decides.
|
|
73
|
+
*
|
|
74
|
+
* The flip is one-way and global rather than per-subscriber because the runtime is a
|
|
75
|
+
* per-boot singleton: 948 of the studio's ~956 localized render sites call the non-hook
|
|
76
|
+
* {@link translate} (measured 2026-08-01), so a per-hook flag would leave almost the whole
|
|
77
|
+
* chrome reading a locale nobody re-rendered for. The studio root subscribes once
|
|
78
|
+
* (`router/root.tsx`), which re-renders the tree exactly once when the handover happens.
|
|
79
|
+
*
|
|
80
|
+
* Exported (module-level only — deliberately not on the `lib/i18n` barrel) so the
|
|
81
|
+
* handover is testable without a React renderer, per the house test pattern.
|
|
49
82
|
*/
|
|
50
|
-
export function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
export function subscribeToUiLocale(listener) {
|
|
84
|
+
localeListeners.add(listener);
|
|
85
|
+
if (!browserDetectionEnabled) {
|
|
86
|
+
browserDetectionEnabled = true;
|
|
87
|
+
for (const l of [...localeListeners])
|
|
88
|
+
l();
|
|
89
|
+
}
|
|
90
|
+
return () => {
|
|
91
|
+
localeListeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function uiLocaleSnapshot() {
|
|
95
|
+
return resolveStudioI18n().locale;
|
|
96
|
+
}
|
|
97
|
+
// --- post-hydration rendering gate (EC-523) ---
|
|
98
|
+
const noopUnsubscribe = () => { };
|
|
99
|
+
const subscribeNever = () => noopUnsubscribe;
|
|
100
|
+
const hydratedSnapshot = () => true;
|
|
101
|
+
const preHydrationSnapshot = () => false;
|
|
102
|
+
/**
|
|
103
|
+
* Has React finished hydrating? `false` for every server render **and** for the
|
|
104
|
+
* hydration render itself; `true` from the first committed client render onwards.
|
|
105
|
+
*
|
|
106
|
+
* **Gate any localized text that can reach the prerendered shell on this** (EC-523).
|
|
107
|
+
* The studio builds as a static SPA, so `dist/client/_shell.html` is rendered at BUILD
|
|
108
|
+
* time: no request, no `Accept-Language`, no session. Every localized string baked into
|
|
109
|
+
* that file is therefore guaranteed wrong for some visitor — and that includes
|
|
110
|
+
* `aria-label` / `sr-only` text, because an accessible name is still a string and a
|
|
111
|
+
* screen-reader user cannot even notice it flip. The honest shape is to render no words
|
|
112
|
+
* at all until hydration, then announce in the visitor's real locale.
|
|
113
|
+
*
|
|
114
|
+
* Mechanism, and why it cannot itself cause a mismatch: `useSyncExternalStore` reads
|
|
115
|
+
* `getServerSnapshot` for the prerender *and* for the hydration render, so the two agree
|
|
116
|
+
* (a text mismatch makes React discard the whole tree — EC-513), then switches to
|
|
117
|
+
* `getSnapshot` in the commit that follows. The store never changes again, so `subscribe`
|
|
118
|
+
* is an inert module-level constant; a fresh closure per render would resubscribe on
|
|
119
|
+
* every render.
|
|
120
|
+
*
|
|
121
|
+
* Lives beside the EC-513 locale handover deliberately: both answer the same question —
|
|
122
|
+
* *is it safe to render locale-dependent output yet?* — and a contributor chasing "why is
|
|
123
|
+
* my string baked into the shell?" lands here.
|
|
124
|
+
*/
|
|
125
|
+
export function useHydrated() {
|
|
126
|
+
return useSyncExternalStore(subscribeNever, hydratedSnapshot, preHydrationSnapshot);
|
|
64
127
|
}
|
|
65
128
|
function navigatorLanguages() {
|
|
129
|
+
if (!browserDetectionReady())
|
|
130
|
+
return [];
|
|
66
131
|
if (typeof navigator !== 'undefined') {
|
|
67
132
|
if (navigator.languages && navigator.languages.length > 0)
|
|
68
133
|
return [...navigator.languages];
|
|
69
134
|
if (navigator.language)
|
|
70
135
|
return [navigator.language];
|
|
71
136
|
}
|
|
72
|
-
|
|
73
|
-
const server = serverLanguagesResolver?.();
|
|
74
|
-
return server ? [...server] : [];
|
|
137
|
+
return [];
|
|
75
138
|
}
|
|
76
139
|
const warned = new Set();
|
|
77
140
|
function warnOnce(message) {
|
|
@@ -185,12 +248,20 @@ export function localizedLabel(value, fallback = '') {
|
|
|
185
248
|
return resolveLabel(value, locale, defaultLocale) ?? fallback;
|
|
186
249
|
}
|
|
187
250
|
// --- React hooks ---
|
|
188
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* The active studio-UI locale — config-pinned, else browser-resolved.
|
|
253
|
+
*
|
|
254
|
+
* Subscribing (EC-513) is what makes the post-hydration handover visible: the hydration
|
|
255
|
+
* render gets the server's locale, and the component re-renders once when browser
|
|
256
|
+
* detection takes over. `router/root.tsx` calls this at the root so the whole chrome —
|
|
257
|
+
* including the ~948 non-hook {@link translate} sites — re-renders with it.
|
|
258
|
+
*/
|
|
189
259
|
export function useUiLocale() {
|
|
190
|
-
return
|
|
260
|
+
return useSyncExternalStore(subscribeToUiLocale, uiLocaleSnapshot, uiLocaleSnapshot);
|
|
191
261
|
}
|
|
192
262
|
/** A `t(key, vars?)` translator bound to the active locale. */
|
|
193
263
|
export function useT() {
|
|
264
|
+
useUiLocale(); // re-render this component when the locale handover happens (EC-513)
|
|
194
265
|
return translate;
|
|
195
266
|
}
|
|
196
267
|
/**
|
|
@@ -199,7 +270,8 @@ export function useT() {
|
|
|
199
270
|
* a locale resolved before the mount seeded the i18n config.
|
|
200
271
|
*/
|
|
201
272
|
export function useLabel() {
|
|
202
|
-
const
|
|
273
|
+
const locale = useUiLocale(); // subscribes to the EC-513 post-hydration handover
|
|
274
|
+
const { defaultLocale } = resolveStudioI18n();
|
|
203
275
|
return useMemo(() => (value, fallback) => resolveMsgLabel(value) ?? resolveLabel(value, locale, defaultLocale) ?? fallback ?? '', [locale, defaultLocale]);
|
|
204
276
|
}
|
|
205
277
|
/** Test seam: force the active locale. Never used by app code. */
|
|
@@ -207,9 +279,13 @@ export function setUiLocaleForTests(locale) {
|
|
|
207
279
|
localeOverride = locale;
|
|
208
280
|
warned.clear();
|
|
209
281
|
}
|
|
210
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* Test seam: reset i18n state — once-per-boot warnings + the EC-513 browser-detection
|
|
284
|
+
* handover flag and its subscribers. Never used by app code.
|
|
285
|
+
*/
|
|
211
286
|
export function resetStudioI18nForTests() {
|
|
212
287
|
warned.clear();
|
|
213
|
-
|
|
288
|
+
browserDetectionEnabled = false;
|
|
289
|
+
localeListeners.clear();
|
|
214
290
|
}
|
|
215
291
|
//# sourceMappingURL=i18n.js.map
|
|
@@ -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 // 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"]}
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../../../src/lib/i18n/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,sBAAsB,GAAG,IAAI,CAAA;AAEnC;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAA4B,EAC5B,SAA4B,EAC5B,aAAqB;IAErB,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA;IACnD,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;QAC5B,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QACjC,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAA;QACxD,IAAI,EAAE,IAAI,CAAC;YAAE,OAAO,SAAS,CAAC,EAAE,CAAE,CAAA;IACpC,CAAC;IACD,OAAO,aAAa,CAAA;AACtB,CAAC;AAUD,IAAI,cAAc,GAAkB,IAAI,CAAA;AAExC,2DAA2D;AAE3D,IAAI,uBAAuB,GAAG,KAAK,CAAA;AACnC,MAAM,eAAe,GAAG,IAAI,GAAG,EAAc,CAAA;AAE7C;;;;;;;;;GASG;AACH,SAAS,qBAAqB;IAC5B,OAAO,uBAAuB,IAAI,OAAO,MAAM,KAAK,WAAW,CAAA;AACjE,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAoB;IACtD,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC7B,IAAI,CAAC,uBAAuB,EAAE,CAAC;QAC7B,uBAAuB,GAAG,IAAI,CAAA;QAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC;YAAE,CAAC,EAAE,CAAA;IAC3C,CAAC;IACD,OAAO,GAAG,EAAE;QACV,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAClC,CAAC,CAAA;AACH,CAAC;AAED,SAAS,gBAAgB;IACvB,OAAO,iBAAiB,EAAE,CAAC,MAAM,CAAA;AACnC,CAAC;AAED,iDAAiD;AAEjD,MAAM,eAAe,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;AAChC,MAAM,cAAc,GAAG,GAAG,EAAE,CAAC,eAAe,CAAA;AAC5C,MAAM,gBAAgB,GAAG,GAAG,EAAE,CAAC,IAAI,CAAA;AACnC,MAAM,oBAAoB,GAAG,GAAG,EAAE,CAAC,KAAK,CAAA;AAExC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,oBAAoB,CAAC,cAAc,EAAE,gBAAgB,EAAE,oBAAoB,CAAC,CAAA;AACrF,CAAC;AAED,SAAS,kBAAkB;IACzB,IAAI,CAAC,qBAAqB,EAAE;QAAE,OAAO,EAAE,CAAA;IACvC,IAAI,OAAO,SAAS,KAAK,WAAW,EAAE,CAAC;QACrC,IAAI,SAAS,CAAC,SAAS,IAAI,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,GAAG,SAAS,CAAC,SAAS,CAAC,CAAA;QAC1F,IAAI,SAAS,CAAC,QAAQ;YAAE,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IACrD,CAAC;IACD,OAAO,EAAE,CAAA;AACX,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAA;AAChC,SAAS,QAAQ,CAAC,OAAe;IAC/B,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAM;IAC/B,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACnB,IAAI,OAAO,OAAO,KAAK,WAAW;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;AAC3D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,MAAM,GAAG,qBAAqB,EAAE,IAAI,EAAE,CAAA;IAC5C,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,sBAAsB,CAAA;IACpE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAA;IACpC,MAAM,SAAS,GAAG,CAAC,aAAa,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAA;IAC7F,sFAAsF;IACtF,gFAAgF;IAChF,kFAAkF;IAClF,uDAAuD;IACvD,IAAI,MAA0B,CAAA;IAC9B,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;YAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;;YAE3D,QAAQ,CACN,yBAAyB,MAAM,CAAC,MAAM,gCAAgC,aAAa,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,6BAA6B,CAC5J,CAAA;IACL,CAAC;IACD,MAAM,MAAM,GACV,cAAc,IAAI,MAAM,IAAI,mBAAmB,CAAC,SAAS,EAAE,kBAAkB,EAAE,EAAE,aAAa,CAAC,CAAA;IACjG,MAAM,MAAM,GAAG,MAAM,KAAK,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;IACtE,mFAAmF;IACnF,IAAI,MAAM,KAAK,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAChD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAA;QACtE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,QAAQ,CACN,kBAAkB,MAAM,gBAAgB,OAAO,CAAC,MAAM,iDAAiD,CACxG,CAAA;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;AAC/D,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,IAAqC;IACtE,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;AACtG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,IAAsC;IAC3E,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAA;IAChC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACzD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,QAAQ,CAAC,gCAAgC,GAAG,GAAG,CAAC,CAAA;QAChD,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;AAC5C,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,kBAAkB,GAAG,QAAQ,CAAA;AACnC,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,OAAO,kBAAkB,GAAG,GAAG,CAAA;AACjC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC,kBAAkB,CAAC;QACtE,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC,CAAC,SAAS,CAAA;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,KAAkC,EAAE,QAAQ,GAAG,EAAE;IAC9E,MAAM,WAAW,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,WAAW,CAAA;IACjD,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,iBAAiB,EAAE,CAAA;IACrD,OAAO,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,IAAI,QAAQ,CAAA;AAC/D,CAAC;AAED,sBAAsB;AAEtB;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,oBAAoB,CAAC,mBAAmB,EAAE,gBAAgB,EAAE,gBAAgB,CAAC,CAAA;AACtF,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,IAAI;IAClB,WAAW,EAAE,CAAA,CAAC,qEAAqE;IACnF,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA,CAAC,mDAAmD;IAChF,MAAM,EAAE,aAAa,EAAE,GAAG,iBAAiB,EAAE,CAAA;IAC7C,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CACxB,eAAe,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,IAAI,QAAQ,IAAI,EAAE,EACxF,CAAC,MAAM,EAAE,aAAa,CAAC,CACxB,CAAA;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,mBAAmB,CAAC,MAAqB;IACvD,cAAc,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,KAAK,EAAE,CAAA;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,CAAC,KAAK,EAAE,CAAA;IACd,uBAAuB,GAAG,KAAK,CAAA;IAC/B,eAAe,CAAC,KAAK,EAAE,CAAA;AACzB,CAAC","sourcesContent":["import { useMemo, useSyncExternalStore } 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, then\n * translates message keys with an active-locale → English → visible-fallback chain.\n *\n * **Locale resolution is client-side** (EC-513, owner-ratified 2026-08-01: \"the studio\n * is primarily client-side and locales should be too\"). Two mechanisms, in precedence\n * order:\n *\n * 1. **`defineStudio({ i18n: { locale } })` — the supported way to fix the UI language.**\n * A pin is read from *config*, which is imported identically on server and client, so\n * SSR and the browser render the same locale with no header plumbing and no flash.\n * Both production consumers pin `de`.\n * 2. **Browser detection** (`navigator.languages`) when nothing is pinned. It is\n * **deferred until after hydration** — see {@link subscribeToUiLocale}. The server\n * has no `navigator`, so it renders the default/pinned locale; the first client\n * render must produce byte-identical output or React discards the whole tree with a\n * hydration error (measured EC-513 under `de-DE`: the auth gate's \"Checking session…\").\n *\n * There is deliberately **no server-side resolver seam**. `setServerLanguagesResolver` +\n * `parseAcceptLanguage` shipped exported and unit-tested from EC-241 but were never\n * called by anything in production — not the demo, not the scaffold template, neither\n * consumer — and EC-241 itself had already measured that TanStack Start's request-header\n * helper is unreachable from a normal SSR route. EC-513 deleted both.\n *\n * No persisted locale selector in v1 (that rides EC-240's deferred account settings).\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\n// --- post-hydration browser-locale detection (EC-513) ---\n\nlet browserDetectionEnabled = false\nconst localeListeners = new Set<() => void>()\n\n/**\n * Is `navigator` allowed to influence the resolved locale yet?\n *\n * In a **browser** it is not, until the first committed render of a locale consumer has\n * run — the hydration render has to reproduce the server's markup exactly, and the\n * server never sees `navigator`. Outside a browser (SSR, node tests) there is nothing\n * to defer: `navigatorLanguages()` there is either genuinely absent (server → `[]`, the\n * default locale, which is precisely what must be reproduced) or deliberately stubbed\n * by a test.\n */\nfunction browserDetectionReady(): boolean {\n return browserDetectionEnabled || typeof window === 'undefined'\n}\n\n/**\n * Subscribe to the resolved UI locale, and — on the first subscription — switch browser\n * detection on (EC-513).\n *\n * This is the whole post-hydration handover. `useSyncExternalStore` subscribes in a\n * commit effect, i.e. **after** React has hydrated the server markup, so every render up\n * to that point resolves the same default/pinned locale the server did. Flipping the flag\n * changes the snapshot, React re-renders, and from then on `navigator.languages` decides.\n *\n * The flip is one-way and global rather than per-subscriber because the runtime is a\n * per-boot singleton: 948 of the studio's ~956 localized render sites call the non-hook\n * {@link translate} (measured 2026-08-01), so a per-hook flag would leave almost the whole\n * chrome reading a locale nobody re-rendered for. The studio root subscribes once\n * (`router/root.tsx`), which re-renders the tree exactly once when the handover happens.\n *\n * Exported (module-level only — deliberately not on the `lib/i18n` barrel) so the\n * handover is testable without a React renderer, per the house test pattern.\n */\nexport function subscribeToUiLocale(listener: () => void): () => void {\n localeListeners.add(listener)\n if (!browserDetectionEnabled) {\n browserDetectionEnabled = true\n for (const l of [...localeListeners]) l()\n }\n return () => {\n localeListeners.delete(listener)\n }\n}\n\nfunction uiLocaleSnapshot(): string {\n return resolveStudioI18n().locale\n}\n\n// --- post-hydration rendering gate (EC-523) ---\n\nconst noopUnsubscribe = () => {}\nconst subscribeNever = () => noopUnsubscribe\nconst hydratedSnapshot = () => true\nconst preHydrationSnapshot = () => false\n\n/**\n * Has React finished hydrating? `false` for every server render **and** for the\n * hydration render itself; `true` from the first committed client render onwards.\n *\n * **Gate any localized text that can reach the prerendered shell on this** (EC-523).\n * The studio builds as a static SPA, so `dist/client/_shell.html` is rendered at BUILD\n * time: no request, no `Accept-Language`, no session. Every localized string baked into\n * that file is therefore guaranteed wrong for some visitor — and that includes\n * `aria-label` / `sr-only` text, because an accessible name is still a string and a\n * screen-reader user cannot even notice it flip. The honest shape is to render no words\n * at all until hydration, then announce in the visitor's real locale.\n *\n * Mechanism, and why it cannot itself cause a mismatch: `useSyncExternalStore` reads\n * `getServerSnapshot` for the prerender *and* for the hydration render, so the two agree\n * (a text mismatch makes React discard the whole tree — EC-513), then switches to\n * `getSnapshot` in the commit that follows. The store never changes again, so `subscribe`\n * is an inert module-level constant; a fresh closure per render would resubscribe on\n * every render.\n *\n * Lives beside the EC-513 locale handover deliberately: both answer the same question —\n * *is it safe to render locale-dependent output yet?* — and a contributor chasing \"why is\n * my string baked into the shell?\" lands here.\n */\nexport function useHydrated(): boolean {\n return useSyncExternalStore(subscribeNever, hydratedSnapshot, preHydrationSnapshot)\n}\n\nfunction navigatorLanguages(): string[] {\n if (!browserDetectionReady()) return []\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 return []\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/**\n * The active studio-UI locale — config-pinned, else browser-resolved.\n *\n * Subscribing (EC-513) is what makes the post-hydration handover visible: the hydration\n * render gets the server's locale, and the component re-renders once when browser\n * detection takes over. `router/root.tsx` calls this at the root so the whole chrome —\n * including the ~948 non-hook {@link translate} sites — re-renders with it.\n */\nexport function useUiLocale(): string {\n return useSyncExternalStore(subscribeToUiLocale, uiLocaleSnapshot, uiLocaleSnapshot)\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 useUiLocale() // re-render this component when the locale handover happens (EC-513)\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 = useUiLocale() // subscribes to the EC-513 post-hydration handover\n const { 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/**\n * Test seam: reset i18n state — once-per-boot warnings + the EC-513 browser-detection\n * handover flag and its subscribers. Never used by app code.\n */\nexport function resetStudioI18nForTests(): void {\n warned.clear()\n browserDetectionEnabled = false\n localeListeners.clear()\n}\n"]}
|
package/dist/lib/i18n/index.d.ts
CHANGED
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
* from `@elytracms/studio/customization` so a consumer authors catalogs there.
|
|
6
6
|
* - Studio components localize chrome via `translate`/`useT` and schema labels via
|
|
7
7
|
* `useLabel`.
|
|
8
|
+
* - **`defineStudio({ i18n: { locale } })` is the supported way to fix the UI language**
|
|
9
|
+
* (EC-513); unpinned deployments follow the browser, resolved client-side after
|
|
10
|
+
* hydration. There is no server-side resolver seam — `setServerLanguagesResolver` and
|
|
11
|
+
* `parseAcceptLanguage` were deleted in EC-513, having never had a production caller.
|
|
12
|
+
* - **Anything that can render into the build-time SPA shell must gate its text on
|
|
13
|
+
* `useHydrated`** (EC-523). The shell is prerendered with no request, so a localized
|
|
14
|
+
* string baked into it — visible copy *or* an accessible name — is wrong for someone.
|
|
8
15
|
*/
|
|
9
16
|
export type { SourceMessage, SourceCatalog, MessageCatalog, StudioI18nConfig } from './types.js';
|
|
10
17
|
export { msg } from './types.js';
|
|
11
18
|
export { EN_MESSAGES } from './messages.en.js';
|
|
12
19
|
export type { MessageKey, TranslationCatalog } from './messages.en.js';
|
|
13
|
-
export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, msgLabel, resolveMsgLabel, useT, useUiLocale, useLabel,
|
|
20
|
+
export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, msgLabel, resolveMsgLabel, useT, useUiLocale, useLabel, useHydrated, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n.js';
|
package/dist/lib/i18n/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { msg } from './types.js';
|
|
2
2
|
export { EN_MESSAGES } from './messages.en.js';
|
|
3
|
-
export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, msgLabel, resolveMsgLabel, useT, useUiLocale, useLabel,
|
|
3
|
+
export { resolveActiveLocale, resolveStudioI18n, translate, localizedLabel, msgLabel, resolveMsgLabel, useT, useUiLocale, useLabel, useHydrated, setUiLocaleForTests, resetStudioI18nForTests, } from './i18n.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/i18n/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/i18n/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAE3C,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,QAAQ,EACR,eAAe,EACf,IAAI,EACJ,WAAW,EACX,QAAQ,EACR,WAAW,EACX,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 * - **`defineStudio({ i18n: { locale } })` is the supported way to fix the UI language**\n * (EC-513); unpinned deployments follow the browser, resolved client-side after\n * hydration. There is no server-side resolver seam — `setServerLanguagesResolver` and\n * `parseAcceptLanguage` were deleted in EC-513, having never had a production caller.\n * - **Anything that can render into the build-time SPA shell must gate its text on\n * `useHydrated`** (EC-523). The shell is prerendered with no request, so a localized\n * string baked into it — visible copy *or* an accessible name — is wrong for someone.\n */\nexport type { SourceMessage, SourceCatalog, MessageCatalog, StudioI18nConfig } from './types'\nexport { msg } from './types'\nexport { EN_MESSAGES } from './messages.en'\nexport type { MessageKey, TranslationCatalog } from './messages.en'\nexport {\n resolveActiveLocale,\n resolveStudioI18n,\n translate,\n localizedLabel,\n msgLabel,\n resolveMsgLabel,\n useT,\n useUiLocale,\n useLabel,\n useHydrated,\n setUiLocaleForTests,\n resetStudioI18nForTests,\n} from './i18n'\n"]}
|
|
@@ -217,6 +217,44 @@ export const deMessages = {
|
|
|
217
217
|
// the session-expired predicate; re-auth happens in place, no navigation.
|
|
218
218
|
'error.sessionExpiredTitle': 'Sie wurden abgemeldet',
|
|
219
219
|
'error.sessionExpired': 'Sie wurden abgemeldet, daher werden Ihre letzten Änderungen nicht gespeichert. Melden Sie sich erneut an, um weiterzuarbeiten — Ihre ungespeicherten Änderungen bleiben genau hier erhalten.',
|
|
220
|
+
// EC-494 — Betriebs-Fehler, die die Operations-Grenze überqueren. Bezeichner
|
|
221
|
+
// (Operationsnamen, Entitätsarten, Dokumentschlüssel, Feldnamen) bleiben unübersetzt.
|
|
222
|
+
// EC-494 — die Titel der Fehler-Badges im Banner.
|
|
223
|
+
'operations.errorLabel.validation': 'Konnte nicht gespeichert werden',
|
|
224
|
+
'operations.errorLabel.not-found': 'Nicht gefunden',
|
|
225
|
+
'operations.errorLabel.conflict': 'Existiert bereits',
|
|
226
|
+
'operations.errorLabel.backend-unavailable': 'Verbindungsproblem',
|
|
227
|
+
'operations.errorLabel.unauthorized': 'Nicht erlaubt',
|
|
228
|
+
'operations.errorLabel.unknown': 'Etwas ist schiefgelaufen',
|
|
229
|
+
'operations.error.invalidInput': 'Die Eingabe für „{operation}“ ist ungültig.',
|
|
230
|
+
'operations.error.notFound': 'Kein {entity} für „{target}“ gefunden.',
|
|
231
|
+
'operations.error.conflict': '„{target}“ steht im Konflikt mit einem vorhandenen Datensatz.',
|
|
232
|
+
'operations.error.backendUnavailable': 'Das Persistenz-Backend ist nicht erreichbar.',
|
|
233
|
+
'operations.error.unauthorized': 'Die Operation „{operation}“ wurde vom Autorisierungs-Hook abgelehnt.',
|
|
234
|
+
'operations.error.unknown': 'Es ist ein unbekannter Fehler bei der Operation aufgetreten.',
|
|
235
|
+
'operations.error.documentSchemaViolation': 'Das Dokument „{collection}:{id}“ verletzt das Schema seiner Sammlung.',
|
|
236
|
+
'operations.error.strictReferencesBlockDelete': '„{target}“ kann nicht gelöscht werden: {count} Dokumente verweisen über strikte Beziehungsfelder darauf — {named}{more}. Entfernen Sie zuerst diese Verweise.',
|
|
237
|
+
'operations.error.strictReferencesBlockDeleteOne': '„{target}“ kann nicht gelöscht werden: {count} Dokument verweist über strikte Beziehungsfelder darauf — {named}. Entfernen Sie zuerst diese Verweise.',
|
|
238
|
+
'operations.error.strictReferencesBlockUnpublish': 'Die Veröffentlichung von „{target}“ kann nicht zurückgenommen werden: {count} Dokumente verweisen über strikte Beziehungsfelder darauf — {named}{more}. Entfernen Sie zuerst diese Verweise.',
|
|
239
|
+
'operations.error.strictReferencesBlockUnpublishOne': 'Die Veröffentlichung von „{target}“ kann nicht zurückgenommen werden: {count} Dokument verweist über strikte Beziehungsfelder darauf — {named}. Entfernen Sie zuerst diese Verweise.',
|
|
240
|
+
'operations.errorDetail.andMore': ' und {count} weitere',
|
|
241
|
+
// EC-494 — Fehler von Inhaltsabfragen (Filterleiste + Listing-Hinweis im Canvas).
|
|
242
|
+
// „filterable“, „asc“, „desc“, „in“, „empty“ sind Schlüsselwörter aus der Konfiguration
|
|
243
|
+
// und bleiben wörtlich stehen.
|
|
244
|
+
'content.queryError.nonFilterableField': 'Das Feld "{field}" in "{collection}" ist nicht als filterable deklariert.',
|
|
245
|
+
'content.queryError.unknownField': 'Das Feld "{field}" existiert in "{collection}" nicht.',
|
|
246
|
+
'content.queryError.unknownCollection': 'Die Sammlung "{collection}" ist nicht definiert.',
|
|
247
|
+
'content.queryError.filterShape': 'Der Filter für "{field}" muss ein Skalar oder ein Operator-Objekt sein.',
|
|
248
|
+
'content.queryError.filterNoOperators': 'Der Filter für "{field}" enthält keine Operatoren.',
|
|
249
|
+
'content.queryError.unknownOperator': 'Unbekannter Filteroperator "{operator}" auf "{field}" — das Vokabular ist genau {vocabulary}.',
|
|
250
|
+
'content.queryError.operatorInRequiresArray': 'Der Operator "in" auf "{field}" erwartet ein Array aus Skalaren.',
|
|
251
|
+
'content.queryError.operatorRequiresString': 'Der Operator "{operator}" auf "{field}" erwartet einen Zeichenkettenwert.',
|
|
252
|
+
'content.queryError.operatorEmptyRequiresBoolean': 'Der Operator "empty" auf "{field}" erwartet einen Wahrheitswert (true = ist leer).',
|
|
253
|
+
'content.queryError.operatorRequiresScalar': 'Der Operator "{operator}" auf "{field}" erwartet einen skalaren Wert.',
|
|
254
|
+
'content.queryError.sortNotFilterable': 'Das Sortierfeld "{field}" in "{collection}" ist nicht als filterable deklariert.',
|
|
255
|
+
'content.queryError.sortUnknownField': 'Das Sortierfeld "{field}" existiert in "{collection}" nicht.',
|
|
256
|
+
'content.queryError.sortDirection': 'Die Sortierrichtung muss "asc" oder "desc" sein.',
|
|
257
|
+
'content.queryError.limitPositiveInteger': 'Das Limit muss eine positive ganze Zahl sein.',
|
|
220
258
|
// ─────────────────────────────────────────────────────────────────────────
|
|
221
259
|
// CMS — collection list / view bar / filters / cells
|
|
222
260
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -280,6 +318,9 @@ export const deMessages = {
|
|
|
280
318
|
'cms.list.bulk.noSettableFields': 'Keine filterbaren Skalarfelder zum Setzen.',
|
|
281
319
|
'cms.list.bulk.setField': 'Feld setzen',
|
|
282
320
|
'cms.list.bulk.clearSelection': 'Auswahl aufheben',
|
|
321
|
+
// EC-494 — die beiden studio-seitigen Fehlergründe je Eintrag.
|
|
322
|
+
'cms.list.bulk.error.notInSession': 'Das Dokument ist nicht mehr in der Sitzung.',
|
|
323
|
+
'cms.list.bulk.error.notValidToPublish': 'Das Dokument ist nicht gültig zum Veröffentlichen.',
|
|
283
324
|
'cms.list.bulk.set.set': 'Setze',
|
|
284
325
|
'cms.list.bulk.set.to': 'auf',
|
|
285
326
|
'cms.list.bulk.set.valueAria': 'Wert setzen',
|
|
@@ -303,6 +344,9 @@ export const deMessages = {
|
|
|
303
344
|
'cms.filter.fieldAria': 'Filterfeld',
|
|
304
345
|
'cms.filter.operatorAria': 'Filteroperator',
|
|
305
346
|
'cms.filter.remove': 'Filter entfernen',
|
|
347
|
+
// EC-494 — die beiden studio-seitigen Klausel-Probleme.
|
|
348
|
+
'cms.filter.issue.stateOperator': 'Status-Filter unterstützen nur "eq" und "in".',
|
|
349
|
+
'cms.filter.issue.stateValue': 'Der Status muss "draft" oder "published" sein.',
|
|
306
350
|
'cms.filter.valueAria': 'Filterwert',
|
|
307
351
|
'cms.filter.valueListPlaceholder': 'Wert, Wert, …',
|
|
308
352
|
'cms.filter.pickRelation': 'Auswählen…',
|
|
@@ -424,6 +468,7 @@ export const deMessages = {
|
|
|
424
468
|
'cms.issue.reserved-field-name': 'Reservierter Feldname',
|
|
425
469
|
'cms.issue.missing-required-field': 'Pflichtfeld fehlt',
|
|
426
470
|
'cms.issue.invalid-field-value': 'Ungültiger Wert',
|
|
471
|
+
'cms.issue.invalid-field-value.warning': 'Wert prüfen',
|
|
427
472
|
'cms.issue.unknown-relation-target': 'Fehlende Referenz',
|
|
428
473
|
'cms.issue.unpublished-relation-target': 'Unveröffentlichte Referenz',
|
|
429
474
|
'cms.issue.cardinality-violation': 'Falsche Anzahl von Werten',
|
|
@@ -441,6 +486,7 @@ export const deMessages = {
|
|
|
441
486
|
'cms.issue.unknown-alias': 'Unbekannter Slot',
|
|
442
487
|
'cms.issue.unfilled-slot': 'Leerer Slot',
|
|
443
488
|
'cms.issue.custom-validation': 'Validierungsregel',
|
|
489
|
+
'cms.issue.unaddressable-url': 'Nicht erreichbare URL',
|
|
444
490
|
'cms.issue.unknown-version': 'Unbekannte Version',
|
|
445
491
|
// EC-443 — issue-card body templates (see messages.en.ts for the fallback contract).
|
|
446
492
|
'cms.issueBody.missing-required-field': 'Das Pflichtfeld „{field}“ ist nicht ausgefüllt.',
|
|
@@ -507,6 +553,7 @@ export const deMessages = {
|
|
|
507
553
|
'cms.issueMessage.duplicateSelectOption': 'Doppelte Auswahloption „{option}“ im Feld „{field}“.',
|
|
508
554
|
'cms.issueMessage.duplicateObjectField': 'Doppeltes Feld „{field}“ im Objektfeld „{parent}“.',
|
|
509
555
|
'cms.issueMessage.assetCollectionSingleton': 'Die Mediensammlung „{collection}“ kann kein Einzelstück sein.',
|
|
556
|
+
'cms.issueMessage.unknownPresentationBodyField': 'Die Sammlung „{collection}“ deklariert „{field}“ als Inhaltsfeld der Darstellung, definiert dieses Feld aber nicht.',
|
|
510
557
|
'cms.issueMessage.duplicateCollectionField': 'Doppeltes Feld „{field}“ in der Sammlung „{collection}“.',
|
|
511
558
|
'cms.issueMessage.duplicateCollectionId': 'Doppelte Sammlungs-id „{collection}“.',
|
|
512
559
|
'cms.issueMessage.unknownLocale': 'Die Sprache „{locale}“ gehört nicht zum konfigurierten Sprachsatz.',
|
|
@@ -520,6 +567,7 @@ export const deMessages = {
|
|
|
520
567
|
'cms.issueMessage.routeBoundDocumentNotFound': 'Das an die Route gebundene Dokument „{collection}:{id}“ wurde nicht gefunden.',
|
|
521
568
|
'cms.issueMessage.routeParamNotCaptured': 'Die Route „{route}“ bindet ihr Dokument an den URL-Parameter „:{param}“, den das Muster „{pattern}“ nicht erfasst.',
|
|
522
569
|
'cms.issueMessage.collectionNotDefined': 'Die Sammlung „{collection}“ ist nicht definiert.',
|
|
570
|
+
'cms.issueMessage.unaddressableLocaleCollision': 'Keine URL erreicht „{id}“ in der Sprache „{locale}“: die Adresse wäre „{url}“, aber das erste Segment „{segment}“ ist ein deklarierter Sprachcode — die Website liest diese URL daher als Sprache „{segment}“, Pfad „{readPath}“. Benennen Sie dieses Segment um — an diesem Dokument oder an der übergeordneten Seite, zu der es gehört — damit es nicht mit einem Sprachcode kollidiert.',
|
|
523
571
|
'cms.issueDetail.expectsString': 'erwartet eine Zeichenkette',
|
|
524
572
|
'cms.issueDetail.expectsRichText': 'erwartet Rich-Text-Inhalt (eine Zeichenkette oder einen Rich-Text-Wert)',
|
|
525
573
|
'cms.issueDetail.expectsNumber': 'erwartet eine Zahl',
|
|
@@ -541,6 +589,8 @@ export const deMessages = {
|
|
|
541
589
|
'cms.issueDetail.greaterThanMax': 'ist größer als das Maximum {max}',
|
|
542
590
|
'cms.issueDetail.fewerThanMinItems': 'hat weniger als {min} Einträge',
|
|
543
591
|
'cms.issueDetail.moreThanMaxItems': 'hat mehr als {max} Einträge',
|
|
592
|
+
'cms.issueDetail.duplicateValue': 'muss innerhalb der Sammlung eindeutig sein, aber das Dokument „{other}“ hat diesen Wert bereits',
|
|
593
|
+
'cms.issueDetail.duplicateValueForLocale': 'muss innerhalb der Sammlung eindeutig sein, aber das Dokument „{other}“ hat diesen Wert bereits für die Sprache „{locale}“',
|
|
544
594
|
'cms.issueDetail.singleValuedManyTargets': 'ist einwertig, hat aber {count} Ziele',
|
|
545
595
|
'cms.issueDetail.notADocumentReference': 'enthält einen Wert, der keine Dokumentreferenz ist',
|
|
546
596
|
'cms.issueDetail.pointsAtOtherCollection': 'zeigt auf die Sammlung „{got}“, zielt aber auf „{target}“',
|
|
@@ -1020,6 +1070,23 @@ export const deMessages = {
|
|
|
1020
1070
|
'builder.clipboard.pastedInto': '{summary} · in {destination}',
|
|
1021
1071
|
'builder.clipboard.issueComponent': 'Komponente',
|
|
1022
1072
|
'builder.clipboard.issueBinding': 'Bindung',
|
|
1073
|
+
// EC-494 — Zwischenablage-Fehler aus der Engine plus der studio-seitige Einfüge-Diff.
|
|
1074
|
+
'builder.clipboard.error.corruptJson': 'Der Inhalt der Zwischenablage ist kein gültiges JSON: {detail}',
|
|
1075
|
+
'builder.clipboard.error.notEnvelope': 'Der Inhalt der Zwischenablage ist kein Elytra-Fragment-Umschlag.',
|
|
1076
|
+
'builder.clipboard.error.foreignFormat': 'Der Inhalt der Zwischenablage ist kein Elytra-Fragment (erwartetes Format "{expected}").',
|
|
1077
|
+
'builder.clipboard.error.versionNewer': 'Die Fragmentversion {version} ist neuer als von diesem Studio unterstützt (Version {supported}).',
|
|
1078
|
+
'builder.clipboard.error.versionUnsupported': 'Die Fragmentversion {version} wird nicht unterstützt (dieses Studio liest Version {supported}).',
|
|
1079
|
+
'builder.clipboard.error.invalidPayload': 'Das Fragment der Zwischenablage verletzt das Fragment-Schema.',
|
|
1080
|
+
'builder.clipboard.error.nodeNotFound': 'In der Komposition existiert kein Knoten "{nodeId}".',
|
|
1081
|
+
'builder.clipboard.error.fixtureNotFound': 'Die Komponente "{component}" deklariert kein Fixture namens "{fixture}".',
|
|
1082
|
+
'builder.clipboard.issue.unresolvedBinding': 'Die Datenquelle "{sourceId}" ist in diesem Projekt nicht verfügbar.',
|
|
1083
|
+
'builder.clipboard.issue.missingComponent': 'Die Komponente "{componentId}" ist in diesem Projekt nicht registriert.',
|
|
1084
|
+
'builder.clipboard.summary.pasted': 'Eingefügt',
|
|
1085
|
+
'builder.clipboard.summary.pastedWith': 'Eingefügt — {parts}',
|
|
1086
|
+
'builder.clipboard.summary.bindingUnresolvedOne': '{count} Bindung nicht aufgelöst',
|
|
1087
|
+
'builder.clipboard.summary.bindingsUnresolved': '{count} Bindungen nicht aufgelöst',
|
|
1088
|
+
'builder.clipboard.summary.componentMissingOne': '{count} Komponente fehlt',
|
|
1089
|
+
'builder.clipboard.summary.componentsMissing': '{count} Komponenten fehlen',
|
|
1023
1090
|
'builder.reuse.makeReusable': 'Wiederverwendbar machen',
|
|
1024
1091
|
'builder.reuse.detachToCopy': 'Als Kopie loslösen',
|
|
1025
1092
|
'builder.reuse.defaultElementName': 'Wiederverwendbares Element',
|
|
@@ -1197,6 +1264,7 @@ export const deMessages = {
|
|
|
1197
1264
|
'ai.error.route': 'Der Assistent ist derzeit nicht verfügbar. Bitte versuchen Sie es erneut.',
|
|
1198
1265
|
'ai.error.generic': 'Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.',
|
|
1199
1266
|
'ai.error.applyFailed': 'Der Entwurf konnte nicht übernommen werden.',
|
|
1267
|
+
'ai.error.applyInvalid': 'Der Vorschlag passt nicht mehr zum aktuellen Dokument: {issues}',
|
|
1200
1268
|
'ai.error.stepCap': 'Der Assistent hat nach {max} Werkzeugschritten angehalten. Senden Sie eine weitere Nachricht, um fortzufahren.',
|
|
1201
1269
|
// EC-332 — full-document translation (header action + chat tool)
|
|
1202
1270
|
'ai.translate.action': 'Dokument übersetzen',
|