@formatjs/editor 1.3.0 → 1.4.0
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/README.md +208 -3
- package/package.json +6 -2
- package/ui.d.ts +177 -0
- package/ui.js +212 -0
- package/ui.js.map +1 -0
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Headless message editor
|
|
2
2
|
|
|
3
3
|
Install `@formatjs/editor` alongside React 19. The published package exports
|
|
4
|
-
headless APIs and TypeScript declarations
|
|
4
|
+
headless APIs and TypeScript declarations at the root, plus optional reusable UI
|
|
5
|
+
at `@formatjs/editor/ui`. The styled demos remain repository-only.
|
|
5
6
|
|
|
6
7
|
The React 19 editor exposes behavior without DOM, styles, a design system, an
|
|
7
8
|
IntlProvider, or network requests. Consumers own catalogs, persistence, loading
|
|
@@ -182,8 +183,212 @@ categories inherit the source `other` branch's argument contract. Repeated
|
|
|
182
183
|
placeholders do not change that contract. Validation is structural, not a check
|
|
183
184
|
of translation quality.
|
|
184
185
|
|
|
185
|
-
The optional `TranslationEditorDemo` in `demo/workflow-demo.tsx`
|
|
186
|
-
`
|
|
186
|
+
The optional `TranslationEditorDemo` in `demo/workflow-demo.tsx` uses the public
|
|
187
|
+
`TranslationEditorView` with a StyleX component adapter and localized labels. It includes locale/catalog/status filters,
|
|
187
188
|
pagination, source locations, localized validation, reset, and save feedback.
|
|
188
189
|
Supply an `IntlProvider` and the same StyleX Vite integration used by the demo.
|
|
189
190
|
It is separate from the headless entry point; consumers can use any design system.
|
|
191
|
+
|
|
192
|
+
## Reusable UI with your design system
|
|
193
|
+
|
|
194
|
+
Import `TranslationEditorView`, `MessageList`, `SourceMessage`, and
|
|
195
|
+
`TranslationField` from `@formatjs/editor/ui`. This separate entry point owns
|
|
196
|
+
message-row selection wiring, field labels and error descriptions, draft status,
|
|
197
|
+
and copy/reset/save controls. It ships unstyled native controls and requires only
|
|
198
|
+
React. The headless root does not import the UI, StyleX, icons, or React Intl.
|
|
199
|
+
|
|
200
|
+
Mount one workflow above the views and pass its existing drafts:
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import {useTranslationEditor} from '@formatjs/editor'
|
|
204
|
+
import {TranslationEditorView} from '@formatjs/editor/ui'
|
|
205
|
+
|
|
206
|
+
const workflow = useTranslationEditor({messages, locales, onSave: persist})
|
|
207
|
+
const selected = workflow.selectedMessage
|
|
208
|
+
return (
|
|
209
|
+
<TranslationEditorView
|
|
210
|
+
messages={workflow.pageMessages}
|
|
211
|
+
selectedMessage={selected}
|
|
212
|
+
onSelect={workflow.editor.selectMessage}
|
|
213
|
+
search={{
|
|
214
|
+
value: workflow.editor.query,
|
|
215
|
+
onValueChange: workflow.editor.setQuery,
|
|
216
|
+
}}
|
|
217
|
+
translations={
|
|
218
|
+
selected
|
|
219
|
+
? visibleLocales.map(locale => {
|
|
220
|
+
const draft = workflow.getTranslation(selected.id, locale)!
|
|
221
|
+
return {
|
|
222
|
+
locale,
|
|
223
|
+
draft,
|
|
224
|
+
onSave: () => {
|
|
225
|
+
void draft.save()
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
: []
|
|
230
|
+
}
|
|
231
|
+
/>
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`visibleLocales` must be a subset of the workflow's available `locales`. Hiding a
|
|
236
|
+
view does not remove its draft. The view does not create a workflow or own
|
|
237
|
+
selection, fetching, filtering, pagination, locale visibility, or persistence.
|
|
238
|
+
For server-side search, pass the loaded page directly as `messages`, your search
|
|
239
|
+
value/callback as `search`, and your externally selected detail as
|
|
240
|
+
`selectedMessage`. Selection can remain outside the loaded page. `loading` marks
|
|
241
|
+
navigation busy and displays a status; it does not clear the controlled list.
|
|
242
|
+
|
|
243
|
+
### Component adapters
|
|
244
|
+
|
|
245
|
+
`EditorDesignSystemProvider` accepts partial overrides of `EditorComponents`: `Button`,
|
|
246
|
+
`TextInput`, `TextArea`, `MessageRow`, `Panel`, and `Layout`. Unspecified entries
|
|
247
|
+
inherit from the nearest provider, falling back to `nativeEditorComponents`. Adapters map a design system's control API to
|
|
248
|
+
semantic `onPress`, `onSelect`, and `onValueChange` callbacks. Define adapters at
|
|
249
|
+
module scope so React preserves focus and control state between edits:
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import {
|
|
253
|
+
EditorDesignSystemProvider,
|
|
254
|
+
useEditorDesignSystem,
|
|
255
|
+
type EditorComponents,
|
|
256
|
+
} from '@formatjs/editor/ui'
|
|
257
|
+
import {Button, Textarea} from './controls'
|
|
258
|
+
|
|
259
|
+
const components: Partial<EditorComponents> = {
|
|
260
|
+
Button: ({onPress, ...props}) => (
|
|
261
|
+
<Button {...props} onClick={() => onPress()} />
|
|
262
|
+
),
|
|
263
|
+
TextArea: ({onValueChange, ...props}) => (
|
|
264
|
+
<Textarea {...props} onValueChange={onValueChange} />
|
|
265
|
+
),
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Configure the design system once around your application or editor subtree:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
function EditorWorkspace() {
|
|
273
|
+
return (
|
|
274
|
+
<EditorDesignSystemProvider components={components}>
|
|
275
|
+
<TranslationEditorView {...viewProps} />
|
|
276
|
+
<CustomToolbar />
|
|
277
|
+
</EditorDesignSystemProvider>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function CustomToolbar() {
|
|
282
|
+
const {Button} = useEditorDesignSystem()
|
|
283
|
+
return (
|
|
284
|
+
<Button variant="secondary" onPress={openReview}>
|
|
285
|
+
Review
|
|
286
|
+
</Button>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
All built-in views and downstream consumers use `useEditorDesignSystem()`;
|
|
292
|
+
there is no component-registry prop on individual views. The hook returns the
|
|
293
|
+
resolved, read-only `EditorComponents` contract. Providers are React-tree scoped,
|
|
294
|
+
so sibling editors (and separate server-rendered trees) do not share mutable
|
|
295
|
+
configuration. Nested providers override only specified components and inherit
|
|
296
|
+
the rest. Changing the registry updates consumers; keeping each component type
|
|
297
|
+
stable preserves field focus and local state. No provider is needed for native
|
|
298
|
+
controls.
|
|
299
|
+
|
|
300
|
+
Each component has an exported props contract:
|
|
301
|
+
|
|
302
|
+
| Component | Inputs | Output callback |
|
|
303
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------ |
|
|
304
|
+
| `Button` / `EditorButtonProps` | `children`, `variant`, optional `disabled` | `onPress(): void` |
|
|
305
|
+
| `TextInput` / `EditorTextInputProps` | `id`, `value`, `type` (`text` or `search`), optional disabled/error-description attributes | `onValueChange(value: string): void` |
|
|
306
|
+
| `TextArea` / `EditorTextAreaProps` | `id`, `value`, optional `rows` (default six), disabled/error-description attributes | `onValueChange(value: string): void` |
|
|
307
|
+
| `MessageRow` / `EditorMessageRowProps` | `children`, controlled `selected` | `onSelect(): void` |
|
|
308
|
+
| `Panel` / `EditorPanelProps` | `children`, accessible `label`, `kind` (`source` or `translation`) | None; layout only |
|
|
309
|
+
| `Layout` / `EditorLayoutProps` | `toolbar`, `navigation`, `content` nodes | None; layout only |
|
|
310
|
+
|
|
311
|
+
`EditorInputProps` defines the shared input attributes explicitly: `id`, `value`,
|
|
312
|
+
`onValueChange`, optional `disabled`, `aria-invalid`, and `aria-describedby`.
|
|
313
|
+
Callbacks never receive DOM events. Inputs remain controlled; callbacks request
|
|
314
|
+
a change and the caller supplies the next value. Disabled buttons must not invoke
|
|
315
|
+
`onPress`. Panel and Layout do not invent interaction callbacks.
|
|
316
|
+
|
|
317
|
+
The complete StyleX adapter lives in `demo/design-system/editor-components.tsx`;
|
|
318
|
+
its layout, tokens, and native-control wrappers are not bundled into the package.
|
|
319
|
+
|
|
320
|
+
Adapter requirements:
|
|
321
|
+
|
|
322
|
+
- Inputs forward `id`, `value`, `disabled`, `aria-invalid`, and
|
|
323
|
+
`aria-describedby` to their focusable control. They report strings through
|
|
324
|
+
`onValueChange` and stay associated with the view's visible label.
|
|
325
|
+
- Buttons honor `disabled`, support keyboard activation, and do not submit a
|
|
326
|
+
surrounding form. `onPress` and `onSelect` take no event argument.
|
|
327
|
+
- Message rows expose selection (the native adapter uses `aria-current`) and
|
|
328
|
+
preserve keyboard activation. Icons and selection styling belong in the adapter.
|
|
329
|
+
- Panels retain their accessible label and render their children. Layout receives
|
|
330
|
+
`toolbar`, `navigation`, and `content` nodes, which it can arrange responsively.
|
|
331
|
+
Preserve a meaningful reading and keyboard order.
|
|
332
|
+
|
|
333
|
+
The reusable pieces retain semantic labels, headings, lists, alerts, and status
|
|
334
|
+
nodes. Adapters control the interactive controls and outer presentation; use the
|
|
335
|
+
standalone pieces when a different page composition is needed.
|
|
336
|
+
|
|
337
|
+
### Application slots and localization
|
|
338
|
+
|
|
339
|
+
`filters`, `pagination`, `context`, and `notice` accept React nodes. Supply your
|
|
340
|
+
own locale picker or filter controls in `filters`; no native-select API is
|
|
341
|
+
imposed on applications with multi-select or asynchronous selectors.
|
|
342
|
+
`sourcePreview` and each translation's `preview` can render a custom preview.
|
|
343
|
+
Each translation also accepts a readable `label` and an `actions` slot (`null`
|
|
344
|
+
suppresses default actions).
|
|
345
|
+
|
|
346
|
+
A translation's `onSave` is a command callback. It can open a confirmation dialog
|
|
347
|
+
that retains the supplied draft's `save` action, then supply typed context and
|
|
348
|
+
handle the returned result. The view never calls persistence itself or interprets
|
|
349
|
+
application receipts. Without `onSave`, default actions include copy and reset
|
|
350
|
+
but no save button. Validation and pending state disable the default save button;
|
|
351
|
+
custom action slots own their own disabled/confirmation behavior.
|
|
352
|
+
|
|
353
|
+
All built-in strings can be overridden through `labels`, including individual
|
|
354
|
+
`labels.validation` entries. Supply already-localized strings from your preferred
|
|
355
|
+
library. The workflow demo demonstrates a React Intl consumer without making
|
|
356
|
+
`IntlProvider` a requirement for the public UI.
|
|
357
|
+
|
|
358
|
+
## Browser interaction tests
|
|
359
|
+
|
|
360
|
+
Write native Playwright `*.spec.ts` files in `packages/editor/vrt/`. The
|
|
361
|
+
`e2eConfig` helper supplies `baseURL`, so specs can use `page.goto('/')`,
|
|
362
|
+
accessible locators, clicks, and web-first assertions. VRT captures are generated from the `.visual.tsx` module. Both targets share the consumer-owned
|
|
363
|
+
server, shell, declared inputs, and pinned Testcontainers browser.
|
|
364
|
+
|
|
365
|
+
```sh
|
|
366
|
+
bazel test //packages/editor/vrt:e2e_test --test_output=errors
|
|
367
|
+
bazel test //packages/editor/vrt:e2e_test --test_arg=--grep=translation
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
E2E covers editing, search, selection, copy/clear, ICU error recovery, locale
|
|
371
|
+
drafts, and saving. It requires Docker and runs manually, locally, and uncached.
|
|
372
|
+
CI should explicitly select both `e2e_test` and `visual_test`. Failures retain
|
|
373
|
+
JUnit, screenshots, and Playwright traces in undeclared test outputs.
|
|
374
|
+
|
|
375
|
+
## Component browser tests
|
|
376
|
+
|
|
377
|
+
`bazel test //packages/editor/vrt:component_test --test_output=errors` runs
|
|
378
|
+
Playwright 1.63 native `mount()` specs for the real editor. The typed
|
|
379
|
+
`editor.visual.tsx` uses the existing provider shell and demo; `gallery.tsx` owns
|
|
380
|
+
mount/update/unmount. The tests check provider updates without losing a draft,
|
|
381
|
+
clear, ICU validation, and isolation between mounts.
|
|
382
|
+
|
|
383
|
+
`component_browser_test` shares the custom server, root npm dependencies, strict
|
|
384
|
+
typechecks, and pinned Testcontainers browser with E2E and VRT.
|
|
385
|
+
`componentBrowserConfig` discovers `*.browser.spec.ts` separately from the E2E
|
|
386
|
+
`*.spec.ts` and generated VRT capture cases. CI should explicitly run all three
|
|
387
|
+
manual browser targets. Screenshot baselines and updates remain in `visual_test`.
|
|
388
|
+
|
|
389
|
+
The default export of `editor.visual.tsx` is a `ComponentVisualModule`: it declares
|
|
390
|
+
renderable cases, browser-side capture hooks, and VRT options. The gallery registers
|
|
391
|
+
that module with `installVisualGallery`. The shared runtime generates all six
|
|
392
|
+
screenshot tests; no `editor.visual.spec.ts` is maintained. Interaction tests stay
|
|
393
|
+
in `editor.browser.spec.tsx`, and the existing PNG names remain explicit in the
|
|
394
|
+
visual declarations.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formatjs/editor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Headless ICU MessageFormat editor for React",
|
|
6
6
|
"keywords": [
|
|
@@ -34,7 +34,11 @@
|
|
|
34
34
|
"sideEffects": false,
|
|
35
35
|
"types": "index.d.ts",
|
|
36
36
|
"exports": {
|
|
37
|
-
".": "./index.js"
|
|
37
|
+
".": "./index.js",
|
|
38
|
+
"./ui": {
|
|
39
|
+
"types": "./ui.d.ts",
|
|
40
|
+
"default": "./ui.js"
|
|
41
|
+
}
|
|
38
42
|
},
|
|
39
43
|
"dependencies": {
|
|
40
44
|
"@formatjs/icu-messageformat-parser": "3.5.17"
|
package/ui.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from "react";
|
|
2
|
+
import "@formatjs/icu-messageformat-parser";
|
|
3
|
+
//#region packages/editor/validation.d.ts
|
|
4
|
+
type TranslationValidationError = "empty" | "invalid-source" | "invalid-translation" | "structure";
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region packages/editor/workflow.d.ts
|
|
7
|
+
interface SourceLocation {
|
|
8
|
+
file: string;
|
|
9
|
+
start?: number;
|
|
10
|
+
end?: number;
|
|
11
|
+
}
|
|
12
|
+
interface EditorMessage {
|
|
13
|
+
id: string;
|
|
14
|
+
defaultMessage: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
catalogs?: readonly string[];
|
|
17
|
+
locations?: readonly SourceLocation[];
|
|
18
|
+
translations: Readonly<Record<string, string | undefined>>;
|
|
19
|
+
}
|
|
20
|
+
type TranslationSaveResult<TResult = void> = {
|
|
21
|
+
status: "saved";
|
|
22
|
+
value: TResult;
|
|
23
|
+
} | {
|
|
24
|
+
status: "failed";
|
|
25
|
+
error: Error;
|
|
26
|
+
} | {
|
|
27
|
+
status: "invalid";
|
|
28
|
+
validationError: TranslationValidationError;
|
|
29
|
+
} | {
|
|
30
|
+
status: "skipped";
|
|
31
|
+
reason: "unavailable" | "unchanged" | "pending";
|
|
32
|
+
};
|
|
33
|
+
/** A render snapshot and actions for one message/locale pair. */
|
|
34
|
+
interface TranslationDraftState<TContext = void, TResult = void> {
|
|
35
|
+
readonly value: string;
|
|
36
|
+
readonly baseline: string;
|
|
37
|
+
readonly validationError: TranslationValidationError | null;
|
|
38
|
+
readonly changed: boolean;
|
|
39
|
+
readonly isSaving: boolean;
|
|
40
|
+
readonly saveError: Error | null;
|
|
41
|
+
readonly saved: boolean;
|
|
42
|
+
setTranslation: (value: string) => void;
|
|
43
|
+
reset: () => void;
|
|
44
|
+
save: (context?: TContext) => Promise<TranslationSaveResult<TResult>>;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region packages/editor/ui.d.ts
|
|
48
|
+
export interface EditorButtonProps {
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
/** Called once per activation, without a DOM event; disabled controls must not call it. */
|
|
51
|
+
onPress: () => void;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
variant: "primary" | "secondary";
|
|
54
|
+
}
|
|
55
|
+
export interface EditorInputProps {
|
|
56
|
+
id: string;
|
|
57
|
+
value: string;
|
|
58
|
+
/** Reports the complete next string value; never a DOM event. */
|
|
59
|
+
onValueChange: (value: string) => void;
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
"aria-invalid"?: boolean;
|
|
62
|
+
"aria-describedby"?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface EditorTextInputProps extends EditorInputProps {
|
|
65
|
+
type: "text" | "search";
|
|
66
|
+
}
|
|
67
|
+
export interface EditorTextAreaProps extends EditorInputProps {
|
|
68
|
+
/** Visible rows when supported by the control; defaults to six. */
|
|
69
|
+
rows?: number;
|
|
70
|
+
}
|
|
71
|
+
export interface EditorMessageRowProps {
|
|
72
|
+
children: ReactNode;
|
|
73
|
+
selected: boolean;
|
|
74
|
+
/** Activates this row without changing controlled selection itself. */
|
|
75
|
+
onSelect: () => void;
|
|
76
|
+
}
|
|
77
|
+
export interface EditorPanelProps {
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
label: string;
|
|
80
|
+
kind: "source" | "translation";
|
|
81
|
+
}
|
|
82
|
+
export interface EditorLayoutProps {
|
|
83
|
+
toolbar: ReactNode;
|
|
84
|
+
navigation: ReactNode;
|
|
85
|
+
content: ReactNode;
|
|
86
|
+
}
|
|
87
|
+
/** Define adapters outside render so controls retain focus across edits. */
|
|
88
|
+
export interface EditorComponents {
|
|
89
|
+
Button: ComponentType<EditorButtonProps>;
|
|
90
|
+
TextInput: ComponentType<EditorTextInputProps>;
|
|
91
|
+
TextArea: ComponentType<EditorTextAreaProps>;
|
|
92
|
+
MessageRow: ComponentType<EditorMessageRowProps>;
|
|
93
|
+
Panel: ComponentType<EditorPanelProps>;
|
|
94
|
+
Layout: ComponentType<EditorLayoutProps>;
|
|
95
|
+
}
|
|
96
|
+
export interface EditorLabels {
|
|
97
|
+
search: string;
|
|
98
|
+
messages: string;
|
|
99
|
+
source: string;
|
|
100
|
+
noMessages: string;
|
|
101
|
+
noSelection: string;
|
|
102
|
+
loading: string;
|
|
103
|
+
copySource: string;
|
|
104
|
+
reset: string;
|
|
105
|
+
save: string;
|
|
106
|
+
saving: string;
|
|
107
|
+
saved: string;
|
|
108
|
+
unsaved: string;
|
|
109
|
+
unchanged: string;
|
|
110
|
+
validation: Record<TranslationValidationError, string>;
|
|
111
|
+
}
|
|
112
|
+
export type EditorLabelOverrides = Partial<Omit<EditorLabels, "validation">> & {
|
|
113
|
+
validation?: Partial<EditorLabels["validation"]>;
|
|
114
|
+
};
|
|
115
|
+
/** Unstyled native controls; no CSS, icons, or localization provider is required. */
|
|
116
|
+
export declare const nativeEditorComponents: EditorComponents;
|
|
117
|
+
export interface EditorDesignSystemProviderProps {
|
|
118
|
+
/** Overrides inherit unspecified components from the nearest provider. */
|
|
119
|
+
components: Partial<EditorComponents>;
|
|
120
|
+
children: ReactNode;
|
|
121
|
+
}
|
|
122
|
+
/** Configure a tree once; sibling providers remain independent. */
|
|
123
|
+
export declare function EditorDesignSystemProvider({ components, children }: EditorDesignSystemProviderProps): ReactNode;
|
|
124
|
+
/** Read the resolved controls, including native defaults outside a provider. */
|
|
125
|
+
export declare function useEditorDesignSystem(): Readonly<EditorComponents>;
|
|
126
|
+
interface ViewOptions {
|
|
127
|
+
labels?: EditorLabelOverrides;
|
|
128
|
+
}
|
|
129
|
+
export type EditorViewMessage = Pick<EditorMessage, "id" | "defaultMessage" | "description">;
|
|
130
|
+
export interface EditorSearch {
|
|
131
|
+
value: string;
|
|
132
|
+
onValueChange: (value: string) => void;
|
|
133
|
+
}
|
|
134
|
+
export interface MessageListProps extends ViewOptions {
|
|
135
|
+
messages: readonly EditorViewMessage[];
|
|
136
|
+
selectedId?: string;
|
|
137
|
+
onSelect: (id: string) => void;
|
|
138
|
+
search?: EditorSearch;
|
|
139
|
+
loading?: boolean;
|
|
140
|
+
pagination?: ReactNode;
|
|
141
|
+
}
|
|
142
|
+
/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */
|
|
143
|
+
export declare function MessageList({ messages, selectedId, onSelect, search, loading, pagination, labels }: MessageListProps): ReactNode;
|
|
144
|
+
export interface SourceMessageProps extends ViewOptions {
|
|
145
|
+
message: EditorViewMessage;
|
|
146
|
+
preview?: ReactNode;
|
|
147
|
+
context?: ReactNode;
|
|
148
|
+
}
|
|
149
|
+
export declare function SourceMessage({ message, preview, context, labels }: SourceMessageProps): ReactNode;
|
|
150
|
+
export type TranslationFieldDraft = Pick<TranslationDraftState, "value" | "validationError" | "changed" | "isSaving" | "saveError" | "saved" | "setTranslation" | "reset">;
|
|
151
|
+
export interface TranslationFieldProps extends ViewOptions {
|
|
152
|
+
locale: string;
|
|
153
|
+
/** Human-readable label; defaults to the locale identifier. */
|
|
154
|
+
label?: string;
|
|
155
|
+
source: string;
|
|
156
|
+
draft: TranslationFieldDraft;
|
|
157
|
+
/** The caller owns confirmation, context, persistence, and receipt presentation. */
|
|
158
|
+
onSave?: () => void;
|
|
159
|
+
/** Overrides the default copy/reset/save actions, including with null. */
|
|
160
|
+
actions?: ReactNode;
|
|
161
|
+
preview?: ReactNode;
|
|
162
|
+
}
|
|
163
|
+
export declare function TranslationField({ locale, label, source, draft, onSave, actions, preview, labels }: TranslationFieldProps): ReactNode;
|
|
164
|
+
export type EditorTranslation = Omit<TranslationFieldProps, "source" | "labels">;
|
|
165
|
+
export interface TranslationEditorViewProps extends Omit<MessageListProps, "selectedId"> {
|
|
166
|
+
/** May be outside the loaded list, e.g. during a server-side page change. */
|
|
167
|
+
selectedMessage?: EditorViewMessage;
|
|
168
|
+
translations: readonly EditorTranslation[];
|
|
169
|
+
filters?: ReactNode;
|
|
170
|
+
context?: ReactNode;
|
|
171
|
+
sourcePreview?: ReactNode;
|
|
172
|
+
notice?: ReactNode;
|
|
173
|
+
}
|
|
174
|
+
/** A stateless composition over caller-owned navigation and locale drafts. */
|
|
175
|
+
export declare function TranslationEditorView({ selectedMessage, translations, filters, context, sourcePreview, notice, labels, ...list }: TranslationEditorViewProps): ReactNode;
|
|
176
|
+
//#endregion
|
|
177
|
+
//# sourceMappingURL=ui.d.ts.map
|
package/ui.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createContext, createElement, useContext, useId, useMemo } from "react";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region packages/editor/ui.tsx
|
|
4
|
+
const defaultLabels = {
|
|
5
|
+
search: "Search messages",
|
|
6
|
+
messages: "Messages",
|
|
7
|
+
source: "Source message",
|
|
8
|
+
noMessages: "No matching messages",
|
|
9
|
+
noSelection: "Select a message",
|
|
10
|
+
loading: "Loading messages…",
|
|
11
|
+
copySource: "Copy source",
|
|
12
|
+
reset: "Reset",
|
|
13
|
+
save: "Save translation",
|
|
14
|
+
saving: "Saving…",
|
|
15
|
+
saved: "Translation saved.",
|
|
16
|
+
unsaved: "Unsaved changes",
|
|
17
|
+
unchanged: "No unsaved changes",
|
|
18
|
+
validation: {
|
|
19
|
+
empty: "Enter a translation before saving.",
|
|
20
|
+
"invalid-source": "The source contains invalid ICU syntax.",
|
|
21
|
+
"invalid-translation": "The translation contains invalid ICU syntax.",
|
|
22
|
+
structure: "Preserve ICU arguments, tags, formatting styles, and selector branches."
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function resolveLabels(labels) {
|
|
26
|
+
return {
|
|
27
|
+
...defaultLabels,
|
|
28
|
+
...labels,
|
|
29
|
+
validation: {
|
|
30
|
+
...defaultLabels.validation,
|
|
31
|
+
...labels?.validation
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Unstyled native controls; no CSS, icons, or localization provider is required. */
|
|
36
|
+
const nativeEditorComponents = {
|
|
37
|
+
Button: ({ children, onPress, disabled }) => /* @__PURE__ */ jsx("button", {
|
|
38
|
+
type: "button",
|
|
39
|
+
disabled,
|
|
40
|
+
onClick: () => onPress(),
|
|
41
|
+
children
|
|
42
|
+
}),
|
|
43
|
+
TextInput: ({ onValueChange, ...props }) => /* @__PURE__ */ jsx("input", {
|
|
44
|
+
...props,
|
|
45
|
+
onChange: (event) => onValueChange(event.target.value)
|
|
46
|
+
}),
|
|
47
|
+
TextArea: ({ onValueChange, rows = 6, ...props }) => /* @__PURE__ */ jsx("textarea", {
|
|
48
|
+
...props,
|
|
49
|
+
rows,
|
|
50
|
+
onChange: (event) => onValueChange(event.target.value)
|
|
51
|
+
}),
|
|
52
|
+
MessageRow: ({ children, selected, onSelect }) => /* @__PURE__ */ jsx("button", {
|
|
53
|
+
type: "button",
|
|
54
|
+
"aria-current": selected ? "true" : void 0,
|
|
55
|
+
onClick: () => onSelect(),
|
|
56
|
+
children
|
|
57
|
+
}),
|
|
58
|
+
Panel: ({ children, label }) => /* @__PURE__ */ jsx("section", {
|
|
59
|
+
"aria-label": label,
|
|
60
|
+
children
|
|
61
|
+
}),
|
|
62
|
+
Layout: ({ toolbar, navigation, content }) => /* @__PURE__ */ jsxs("div", { children: [toolbar, /* @__PURE__ */ jsxs("div", { children: [navigation, content] })] })
|
|
63
|
+
};
|
|
64
|
+
const EditorDesignSystemContext = createContext(nativeEditorComponents);
|
|
65
|
+
/** Configure a tree once; sibling providers remain independent. */
|
|
66
|
+
function EditorDesignSystemProvider({ components, children }) {
|
|
67
|
+
const parent = useEditorDesignSystem();
|
|
68
|
+
const value = useMemo(() => ({
|
|
69
|
+
...parent,
|
|
70
|
+
...components
|
|
71
|
+
}), [parent, components]);
|
|
72
|
+
return /* @__PURE__ */ jsx(EditorDesignSystemContext, {
|
|
73
|
+
value,
|
|
74
|
+
children
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Read the resolved controls, including native defaults outside a provider. */
|
|
78
|
+
function useEditorDesignSystem() {
|
|
79
|
+
return useContext(EditorDesignSystemContext);
|
|
80
|
+
}
|
|
81
|
+
/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */
|
|
82
|
+
function MessageList({ messages, selectedId, onSelect, search, loading = false, pagination, labels }) {
|
|
83
|
+
const { TextInput, MessageRow } = useEditorDesignSystem();
|
|
84
|
+
const text = resolveLabels(labels);
|
|
85
|
+
const searchId = useId();
|
|
86
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
87
|
+
"aria-label": text.messages,
|
|
88
|
+
"aria-busy": loading,
|
|
89
|
+
children: [
|
|
90
|
+
search && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
91
|
+
htmlFor: searchId,
|
|
92
|
+
children: text.search
|
|
93
|
+
}), /* @__PURE__ */ jsx(TextInput, {
|
|
94
|
+
id: searchId,
|
|
95
|
+
type: "search",
|
|
96
|
+
value: search.value,
|
|
97
|
+
onValueChange: search.onValueChange
|
|
98
|
+
})] }),
|
|
99
|
+
loading && /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.loading }) }),
|
|
100
|
+
/* @__PURE__ */ jsx("ul", { children: messages.map((message) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(MessageRow, {
|
|
101
|
+
selected: message.id === selectedId,
|
|
102
|
+
onSelect: () => onSelect(message.id),
|
|
103
|
+
children: [
|
|
104
|
+
/* @__PURE__ */ jsx("span", { children: message.defaultMessage }),
|
|
105
|
+
" ",
|
|
106
|
+
/* @__PURE__ */ jsx("code", { children: message.id })
|
|
107
|
+
]
|
|
108
|
+
}) }, message.id)) }),
|
|
109
|
+
!loading && messages.length === 0 && /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.noMessages }) }),
|
|
110
|
+
pagination
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function SourceMessage({ message, preview, context, labels }) {
|
|
115
|
+
const { Panel } = useEditorDesignSystem();
|
|
116
|
+
const text = resolveLabels(labels);
|
|
117
|
+
return /* @__PURE__ */ jsxs(Panel, {
|
|
118
|
+
kind: "source",
|
|
119
|
+
label: text.source,
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ jsx("h2", { children: text.source }),
|
|
122
|
+
/* @__PURE__ */ jsx("code", { children: message.id }),
|
|
123
|
+
preview ?? /* @__PURE__ */ jsx("pre", { children: message.defaultMessage }),
|
|
124
|
+
message.description && /* @__PURE__ */ jsx("p", { children: message.description }),
|
|
125
|
+
context
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function TranslationField({ locale, label = locale, source, draft, onSave, actions, preview, labels }) {
|
|
130
|
+
const { Panel, TextArea, Button } = useEditorDesignSystem();
|
|
131
|
+
const text = resolveLabels(labels);
|
|
132
|
+
const id = useId();
|
|
133
|
+
const errorId = `${id}-error`;
|
|
134
|
+
const statusId = `${id}-status`;
|
|
135
|
+
const validation = draft.validationError ? text.validation[draft.validationError] : null;
|
|
136
|
+
const error = validation ?? draft.saveError?.message;
|
|
137
|
+
return /* @__PURE__ */ jsxs(Panel, {
|
|
138
|
+
kind: "translation",
|
|
139
|
+
label,
|
|
140
|
+
children: [
|
|
141
|
+
/* @__PURE__ */ jsx("label", {
|
|
142
|
+
htmlFor: id,
|
|
143
|
+
children: label
|
|
144
|
+
}),
|
|
145
|
+
/* @__PURE__ */ jsx(TextArea, {
|
|
146
|
+
id,
|
|
147
|
+
value: draft.value,
|
|
148
|
+
onValueChange: draft.setTranslation,
|
|
149
|
+
"aria-invalid": !!validation,
|
|
150
|
+
"aria-describedby": error ? `${errorId} ${statusId}` : statusId
|
|
151
|
+
}),
|
|
152
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
153
|
+
id: errorId,
|
|
154
|
+
role: "alert",
|
|
155
|
+
children: error
|
|
156
|
+
}),
|
|
157
|
+
/* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", {
|
|
158
|
+
id: statusId,
|
|
159
|
+
children: draft.isSaving ? text.saving : draft.saved ? text.saved : draft.changed ? text.unsaved : text.unchanged
|
|
160
|
+
}) }),
|
|
161
|
+
preview,
|
|
162
|
+
actions !== void 0 ? actions : /* @__PURE__ */ jsxs("div", { children: [
|
|
163
|
+
/* @__PURE__ */ jsx(Button, {
|
|
164
|
+
variant: "secondary",
|
|
165
|
+
disabled: draft.isSaving,
|
|
166
|
+
onPress: () => draft.setTranslation(source),
|
|
167
|
+
children: text.copySource
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ jsx(Button, {
|
|
170
|
+
variant: "secondary",
|
|
171
|
+
disabled: !draft.changed || draft.isSaving,
|
|
172
|
+
onPress: draft.reset,
|
|
173
|
+
children: text.reset
|
|
174
|
+
}),
|
|
175
|
+
onSave && /* @__PURE__ */ jsx(Button, {
|
|
176
|
+
variant: "primary",
|
|
177
|
+
disabled: !draft.changed || !!draft.validationError || draft.isSaving,
|
|
178
|
+
onPress: onSave,
|
|
179
|
+
children: draft.isSaving ? text.saving : text.save
|
|
180
|
+
})
|
|
181
|
+
] })
|
|
182
|
+
]
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/** A stateless composition over caller-owned navigation and locale drafts. */
|
|
186
|
+
function TranslationEditorView({ selectedMessage, translations, filters, context, sourcePreview, notice, labels, ...list }) {
|
|
187
|
+
const { Layout } = useEditorDesignSystem();
|
|
188
|
+
const text = resolveLabels(labels);
|
|
189
|
+
return /* @__PURE__ */ jsx(Layout, {
|
|
190
|
+
toolbar: filters,
|
|
191
|
+
navigation: /* @__PURE__ */ jsx(MessageList, {
|
|
192
|
+
...list,
|
|
193
|
+
selectedId: selectedMessage?.id,
|
|
194
|
+
labels
|
|
195
|
+
}),
|
|
196
|
+
content: /* @__PURE__ */ jsxs(Fragment, { children: [notice, selectedMessage ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SourceMessage, {
|
|
197
|
+
message: selectedMessage,
|
|
198
|
+
preview: sourcePreview,
|
|
199
|
+
context,
|
|
200
|
+
labels
|
|
201
|
+
}), translations.map((translation) => /* @__PURE__ */ createElement(TranslationField, {
|
|
202
|
+
...translation,
|
|
203
|
+
key: `${selectedMessage.id}:${translation.locale}`,
|
|
204
|
+
source: selectedMessage.defaultMessage,
|
|
205
|
+
labels
|
|
206
|
+
}))] }) : /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.noSelection }) })] })
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
export { EditorDesignSystemProvider, MessageList, SourceMessage, TranslationEditorView, TranslationField, nativeEditorComponents, useEditorDesignSystem };
|
|
211
|
+
|
|
212
|
+
//# sourceMappingURL=ui.js.map
|
package/ui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.js","names":[],"sources":["../ui.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useId,\n useMemo,\n type ComponentType,\n type ReactNode,\n} from 'react'\nimport type {\n EditorMessage,\n TranslationDraftState,\n} from '#packages/editor/workflow.js'\nimport type {TranslationValidationError} from '#packages/editor/validation.js'\n\nexport interface EditorButtonProps {\n children: ReactNode\n /** Called once per activation, without a DOM event; disabled controls must not call it. */\n onPress: () => void\n disabled?: boolean\n variant: 'primary' | 'secondary'\n}\nexport interface EditorInputProps {\n id: string\n value: string\n /** Reports the complete next string value; never a DOM event. */\n onValueChange: (value: string) => void\n disabled?: boolean\n 'aria-invalid'?: boolean\n 'aria-describedby'?: string\n}\nexport interface EditorTextInputProps extends EditorInputProps {\n type: 'text' | 'search'\n}\nexport interface EditorTextAreaProps extends EditorInputProps {\n /** Visible rows when supported by the control; defaults to six. */\n rows?: number\n}\nexport interface EditorMessageRowProps {\n children: ReactNode\n selected: boolean\n /** Activates this row without changing controlled selection itself. */\n onSelect: () => void\n}\nexport interface EditorPanelProps {\n children: ReactNode\n label: string\n kind: 'source' | 'translation'\n}\nexport interface EditorLayoutProps {\n toolbar: ReactNode\n navigation: ReactNode\n content: ReactNode\n}\n/** Define adapters outside render so controls retain focus across edits. */\nexport interface EditorComponents {\n Button: ComponentType<EditorButtonProps>\n TextInput: ComponentType<EditorTextInputProps>\n TextArea: ComponentType<EditorTextAreaProps>\n MessageRow: ComponentType<EditorMessageRowProps>\n Panel: ComponentType<EditorPanelProps>\n Layout: ComponentType<EditorLayoutProps>\n}\nexport interface EditorLabels {\n search: string\n messages: string\n source: string\n noMessages: string\n noSelection: string\n loading: string\n copySource: string\n reset: string\n save: string\n saving: string\n saved: string\n unsaved: string\n unchanged: string\n validation: Record<TranslationValidationError, string>\n}\nconst defaultLabels: EditorLabels = {\n search: 'Search messages',\n messages: 'Messages',\n source: 'Source message',\n noMessages: 'No matching messages',\n noSelection: 'Select a message',\n loading: 'Loading messages…',\n copySource: 'Copy source',\n reset: 'Reset',\n save: 'Save translation',\n saving: 'Saving…',\n saved: 'Translation saved.',\n unsaved: 'Unsaved changes',\n unchanged: 'No unsaved changes',\n validation: {\n empty: 'Enter a translation before saving.',\n 'invalid-source': 'The source contains invalid ICU syntax.',\n 'invalid-translation': 'The translation contains invalid ICU syntax.',\n structure:\n 'Preserve ICU arguments, tags, formatting styles, and selector branches.',\n },\n}\nexport type EditorLabelOverrides = Partial<Omit<EditorLabels, 'validation'>> & {\n validation?: Partial<EditorLabels['validation']>\n}\nfunction resolveLabels(labels?: EditorLabelOverrides): EditorLabels {\n return {\n ...defaultLabels,\n ...labels,\n validation: {...defaultLabels.validation, ...labels?.validation},\n }\n}\n/** Unstyled native controls; no CSS, icons, or localization provider is required. */\nexport const nativeEditorComponents: EditorComponents = {\n Button: ({children, onPress, disabled}) => (\n <button type=\"button\" disabled={disabled} onClick={() => onPress()}>\n {children}\n </button>\n ),\n TextInput: ({onValueChange, ...props}) => (\n <input {...props} onChange={event => onValueChange(event.target.value)} />\n ),\n TextArea: ({onValueChange, rows = 6, ...props}) => (\n <textarea\n {...props}\n rows={rows}\n onChange={event => onValueChange(event.target.value)}\n />\n ),\n MessageRow: ({children, selected, onSelect}) => (\n <button\n type=\"button\"\n aria-current={selected ? 'true' : undefined}\n onClick={() => onSelect()}\n >\n {children}\n </button>\n ),\n Panel: ({children, label}) => (\n <section aria-label={label}>{children}</section>\n ),\n Layout: ({toolbar, navigation, content}) => (\n <div>\n {toolbar}\n <div>\n {navigation}\n {content}\n </div>\n </div>\n ),\n}\nconst EditorDesignSystemContext = createContext<Readonly<EditorComponents>>(\n nativeEditorComponents\n)\n\nexport interface EditorDesignSystemProviderProps {\n /** Overrides inherit unspecified components from the nearest provider. */\n components: Partial<EditorComponents>\n children: ReactNode\n}\n/** Configure a tree once; sibling providers remain independent. */\nexport function EditorDesignSystemProvider({\n components,\n children,\n}: EditorDesignSystemProviderProps): ReactNode {\n const parent = useEditorDesignSystem()\n const value = useMemo(\n () => ({...parent, ...components}),\n [parent, components]\n )\n return (\n <EditorDesignSystemContext value={value}>\n {children}\n </EditorDesignSystemContext>\n )\n}\n/** Read the resolved controls, including native defaults outside a provider. */\nexport function useEditorDesignSystem(): Readonly<EditorComponents> {\n return useContext(EditorDesignSystemContext)\n}\ninterface ViewOptions {\n labels?: EditorLabelOverrides\n}\nexport type EditorViewMessage = Pick<\n EditorMessage,\n 'id' | 'defaultMessage' | 'description'\n>\nexport interface EditorSearch {\n value: string\n onValueChange: (value: string) => void\n}\nexport interface MessageListProps extends ViewOptions {\n messages: readonly EditorViewMessage[]\n selectedId?: string\n onSelect: (id: string) => void\n search?: EditorSearch\n loading?: boolean\n pagination?: ReactNode\n}\n/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */\nexport function MessageList({\n messages,\n selectedId,\n onSelect,\n search,\n loading = false,\n pagination,\n labels,\n}: MessageListProps): ReactNode {\n const {TextInput, MessageRow} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n const searchId = useId()\n return (\n <nav aria-label={text.messages} aria-busy={loading}>\n {search && (\n <div>\n <label htmlFor={searchId}>{text.search}</label>\n <TextInput\n id={searchId}\n type=\"search\"\n value={search.value}\n onValueChange={search.onValueChange}\n />\n </div>\n )}\n {loading && (\n <p>\n <output>{text.loading}</output>\n </p>\n )}\n <ul>\n {messages.map(message => (\n <li key={message.id}>\n <MessageRow\n selected={message.id === selectedId}\n onSelect={() => onSelect(message.id)}\n >\n <span>{message.defaultMessage}</span> <code>{message.id}</code>\n </MessageRow>\n </li>\n ))}\n </ul>\n {!loading && messages.length === 0 && (\n <p>\n <output>{text.noMessages}</output>\n </p>\n )}\n {pagination}\n </nav>\n )\n}\nexport interface SourceMessageProps extends ViewOptions {\n message: EditorViewMessage\n preview?: ReactNode\n context?: ReactNode\n}\nexport function SourceMessage({\n message,\n preview,\n context,\n labels,\n}: SourceMessageProps): ReactNode {\n const {Panel} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n return (\n <Panel kind=\"source\" label={text.source}>\n <h2>{text.source}</h2>\n <code>{message.id}</code>\n {preview ?? <pre>{message.defaultMessage}</pre>}\n {message.description && <p>{message.description}</p>}\n {context}\n </Panel>\n )\n}\nexport type TranslationFieldDraft = Pick<\n TranslationDraftState,\n | 'value'\n | 'validationError'\n | 'changed'\n | 'isSaving'\n | 'saveError'\n | 'saved'\n | 'setTranslation'\n | 'reset'\n>\nexport interface TranslationFieldProps extends ViewOptions {\n locale: string\n /** Human-readable label; defaults to the locale identifier. */\n label?: string\n source: string\n draft: TranslationFieldDraft\n /** The caller owns confirmation, context, persistence, and receipt presentation. */\n onSave?: () => void\n /** Overrides the default copy/reset/save actions, including with null. */\n actions?: ReactNode\n preview?: ReactNode\n}\nexport function TranslationField({\n locale,\n label = locale,\n source,\n draft,\n onSave,\n actions,\n preview,\n labels,\n}: TranslationFieldProps): ReactNode {\n const {Panel, TextArea, Button} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n const id = useId()\n const errorId = `${id}-error`\n const statusId = `${id}-status`\n const validation = draft.validationError\n ? text.validation[draft.validationError]\n : null\n const error = validation ?? draft.saveError?.message\n return (\n <Panel kind=\"translation\" label={label}>\n <label htmlFor={id}>{label}</label>\n <TextArea\n id={id}\n value={draft.value}\n onValueChange={draft.setTranslation}\n aria-invalid={!!validation}\n aria-describedby={error ? `${errorId} ${statusId}` : statusId}\n />\n {error && (\n <p id={errorId} role=\"alert\">\n {error}\n </p>\n )}\n <p>\n <output id={statusId}>\n {draft.isSaving\n ? text.saving\n : draft.saved\n ? text.saved\n : draft.changed\n ? text.unsaved\n : text.unchanged}\n </output>\n </p>\n {preview}\n {actions !== undefined ? (\n actions\n ) : (\n <div>\n <Button\n variant=\"secondary\"\n disabled={draft.isSaving}\n onPress={() => draft.setTranslation(source)}\n >\n {text.copySource}\n </Button>\n <Button\n variant=\"secondary\"\n disabled={!draft.changed || draft.isSaving}\n onPress={draft.reset}\n >\n {text.reset}\n </Button>\n {onSave && (\n <Button\n variant=\"primary\"\n disabled={\n !draft.changed || !!draft.validationError || draft.isSaving\n }\n onPress={onSave}\n >\n {draft.isSaving ? text.saving : text.save}\n </Button>\n )}\n </div>\n )}\n </Panel>\n )\n}\nexport type EditorTranslation = Omit<TranslationFieldProps, 'source' | 'labels'>\nexport interface TranslationEditorViewProps extends Omit<\n MessageListProps,\n 'selectedId'\n> {\n /** May be outside the loaded list, e.g. during a server-side page change. */\n selectedMessage?: EditorViewMessage\n translations: readonly EditorTranslation[]\n filters?: ReactNode\n context?: ReactNode\n sourcePreview?: ReactNode\n notice?: ReactNode\n}\n/** A stateless composition over caller-owned navigation and locale drafts. */\nexport function TranslationEditorView({\n selectedMessage,\n translations,\n filters,\n context,\n sourcePreview,\n notice,\n labels,\n ...list\n}: TranslationEditorViewProps): ReactNode {\n const {Layout} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n return (\n <Layout\n toolbar={filters}\n navigation={\n <MessageList\n {...list}\n selectedId={selectedMessage?.id}\n labels={labels}\n />\n }\n content={\n <>\n {notice}\n {selectedMessage ? (\n <>\n <SourceMessage\n message={selectedMessage}\n preview={sourcePreview}\n context={context}\n labels={labels}\n />\n {translations.map(translation => (\n <TranslationField\n {...translation}\n key={`${selectedMessage.id}:${translation.locale}`}\n source={selectedMessage.defaultMessage}\n labels={labels}\n />\n ))}\n </>\n ) : (\n <p>\n <output>{text.noSelection}</output>\n </p>\n )}\n </>\n }\n />\n )\n}\n"],"mappings":"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"}
|