@formatjs/editor 1.1.48 → 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/LICENSE.md +1 -1
- package/README.md +393 -2
- package/index.d.ts +143 -3
- package/index.js +334 -118
- package/index.js.map +1 -0
- package/package.json +29 -21
- package/ui.d.ts +177 -0
- package/ui.js +212 -0
- package/ui.js.map +1 -0
- package/header.d.ts +0 -6
- package/header.d.ts.map +0 -1
- package/header.js +0 -16
- package/index.d.ts.map +0 -1
- package/lib/header.d.ts +0 -6
- package/lib/header.d.ts.map +0 -1
- package/lib/header.js +0 -13
- package/lib/index.d.ts +0 -4
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -116
- package/lib/main.d.ts +0 -2
- package/lib/main.d.ts.map +0 -1
- package/lib/main.js +0 -4
- package/lib/message.d.ts +0 -7
- package/lib/message.d.ts.map +0 -1
- package/lib/message.js +0 -45
- package/lib/messages.d.ts +0 -10
- package/lib/messages.d.ts.map +0 -1
- package/lib/messages.js +0 -21
- package/lib/types.d.ts +0 -6
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -1
- package/main.d.ts +0 -2
- package/main.d.ts.map +0 -1
- package/main.js +0 -7
- package/message.d.ts +0 -7
- package/message.d.ts.map +0 -1
- package/message.js +0 -47
- package/messages.d.ts +0 -10
- package/messages.d.ts.map +0 -1
- package/messages.js +0 -24
- package/types.d.ts +0 -6
- package/types.d.ts.map +0 -1
- package/types.js +0 -2
package/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2023 FormatJS
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
6
|
|
package/README.md
CHANGED
|
@@ -1,3 +1,394 @@
|
|
|
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 at the root, plus optional reusable UI
|
|
5
|
+
at `@formatjs/editor/ui`. The styled demos remain repository-only.
|
|
6
|
+
|
|
7
|
+
The React 19 editor exposes behavior without DOM, styles, a design system, an
|
|
8
|
+
IntlProvider, or network requests. Consumers own catalogs, persistence, loading
|
|
9
|
+
states, labels, layout, and providers. The StyleX view in `demo/demo.tsx` is an
|
|
10
|
+
example consumer, not part of the headless entry point.
|
|
11
|
+
|
|
12
|
+
The optional `demo/design-system/` layer provides tokens and native button, input,
|
|
13
|
+
textarea, badge, and panel components. Customize `tokens.stylex.ts` or supply a
|
|
14
|
+
StyleX theme. The demo adds responsive layout, keyboard focus, validation states,
|
|
15
|
+
and logical spacing for RTL. Its Vite setup uses `@stylexjs/unplugin`; consumers
|
|
16
|
+
using another design system need neither StyleX nor these components.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import {useMessageEditor} from '@formatjs/editor'
|
|
20
|
+
|
|
21
|
+
function TranslationEditor({messages, onMessageChange}) {
|
|
22
|
+
const editor = useMessageEditor({messages, onMessageChange})
|
|
23
|
+
return (
|
|
24
|
+
<YourTextArea
|
|
25
|
+
label="Translation"
|
|
26
|
+
value={editor.selectedMessage?.translatedMessage ?? ''}
|
|
27
|
+
onValueChange={editor.setTranslation}
|
|
28
|
+
invalid={!!editor.translation?.error}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use your design system's controls and localize their labels in the consumer.
|
|
35
|
+
`Editor` also accepts a render function as `children` with the same state.
|
|
36
|
+
Neither API adds elements or requires a provider.
|
|
37
|
+
|
|
38
|
+
- `messages` is controlled; `onMessageChange` receives the edited message.
|
|
39
|
+
Apply it to the parent catalog by ID. Invalid ICU text is preserved for editing.
|
|
40
|
+
- `selectMessage`, `selectedMessage`, `query`, and `setQuery` control navigation.
|
|
41
|
+
Search covers IDs, source text, translations, and descriptions. Filtering does
|
|
42
|
+
not discard selection or edits. A missing selection falls back to the first
|
|
43
|
+
catalog message; an empty catalog has no selection.
|
|
44
|
+
- `setTranslation`, `copySource`, and `clearTranslation` edit the selected message.
|
|
45
|
+
Copy means copying source text into the translation, not the system clipboard.
|
|
46
|
+
- `source` and `translation` expose `{ast, error}` parse results. The AST retains
|
|
47
|
+
all plural/select branches, rich-text tags, and skeletons. `Message` passes the
|
|
48
|
+
same result to its render function; `parseMessage` is available without React.
|
|
49
|
+
|
|
50
|
+
Message IDs must be unique within a catalog. Remount the editor when switching
|
|
51
|
+
catalogs if selection and search should reset. Persistence and validation policy
|
|
52
|
+
(such as forbidding saves with ICU errors) belong to the consumer.
|
|
53
|
+
|
|
54
|
+
This replaces the WIP Material UI app entry point. Import `Editor` or
|
|
55
|
+
`useMessageEditor`; mount `EditorDemo` explicitly for the example view. Standalone
|
|
56
|
+
entry points use React's `createRoot`. No Material UI dependency remains.
|
|
57
|
+
|
|
58
|
+
Run `bazel test //packages/editor:unit_test` for state and renderer coverage.
|
|
59
|
+
See [visual tests](vrt/README.md) for browser coverage and baseline updates.
|
|
60
|
+
|
|
61
|
+
## Translation workflow
|
|
62
|
+
|
|
63
|
+
`useTranslationEditor` layers draft storage and persistence on `useMessageEditor`.
|
|
64
|
+
The existing `Editor`, `Message`, and `useMessageEditor` APIs stay unchanged.
|
|
65
|
+
It adds no DOM, styling, provider, or network dependency.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
const workflow = useTranslationEditor({
|
|
69
|
+
messages, // {id, defaultMessage, description?, catalogs?, locations?, translations}
|
|
70
|
+
locales: ['fr', 'ru'],
|
|
71
|
+
defaultLocale: 'fr',
|
|
72
|
+
pageSize: 100,
|
|
73
|
+
onSave: async update => {
|
|
74
|
+
await persist(update) // {id, locale, translation}
|
|
75
|
+
setMessages(current =>
|
|
76
|
+
current.map(message =>
|
|
77
|
+
message.id === update.id
|
|
78
|
+
? {
|
|
79
|
+
...message,
|
|
80
|
+
translations: {
|
|
81
|
+
...message.translations,
|
|
82
|
+
[update.locale]: update.translation,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: message
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use `workflow.editor` for editing/search/selection, `pageMessages` for the current
|
|
93
|
+
page, and `setLocale`, `setCatalog`, `setStatus`, and `setPage` for navigation.
|
|
94
|
+
`selectedMessage` exposes catalog and source-location context. Status filters
|
|
95
|
+
reflect saved translations, so typing does not move a message out of the list.
|
|
96
|
+
Locales may load asynchronously; an absent selection falls back to the first
|
|
97
|
+
available locale. Pagination clamps when messages or page size change.
|
|
98
|
+
|
|
99
|
+
Drafts, reset baselines, errors, and pending saves are scoped by message ID and
|
|
100
|
+
locale. Switching messages, filters, or locales retains drafts. `save()` validates
|
|
101
|
+
the selected draft, ignores duplicate submissions for that key, and reports
|
|
102
|
+
failure through `saveError`. A completed save updates only its submitted key;
|
|
103
|
+
newer edits remain dirty. Update controlled messages after persistence succeeds.
|
|
104
|
+
Clean drafts adopt external changes; dirty drafts retain their text. `reset()`
|
|
105
|
+
restores the latest saved baseline. Remount when switching unrelated catalogs
|
|
106
|
+
that reuse message IDs, or when intentionally discarding all drafts.
|
|
107
|
+
|
|
108
|
+
### Multiple locale views
|
|
109
|
+
|
|
110
|
+
Mount one workflow above your locale views. `getTranslation(id, locale)` exposes
|
|
111
|
+
the value, baseline, validation, save feedback, and actions for that pair, sharing
|
|
112
|
+
the same draft store as the selected-message API:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
const draft = workflow.getTranslation(messageId, locale)
|
|
116
|
+
if (!draft) return null
|
|
117
|
+
return (
|
|
118
|
+
<YourTextArea
|
|
119
|
+
value={draft.value}
|
|
120
|
+
onValueChange={draft.setTranslation}
|
|
121
|
+
invalid={!!draft.validationError}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Each view can call `draft.reset()` and `draft.save()` independently. Hiding or
|
|
127
|
+
unmounting a view preserves its draft while the owning workflow remains mounted.
|
|
128
|
+
Keep available locales in the workflow's `locales` option; choose which views
|
|
129
|
+
to display in your UI. The getter returns `undefined` for IDs or locales absent
|
|
130
|
+
from the current options. Drafts survive their temporary removal, including saves
|
|
131
|
+
that complete while a message is outside a loaded page.
|
|
132
|
+
|
|
133
|
+
### Save results and context
|
|
134
|
+
|
|
135
|
+
`save(context?)` resolves to a discriminated result. Successful persistence returns
|
|
136
|
+
`{status: 'saved', value}` with the value returned by `onSave`. Failures return
|
|
137
|
+
`{status: 'failed', error}` and also populate `saveError`. Validation failures return
|
|
138
|
+
`{status: 'invalid', validationError}`. Saves that do not call persistence return
|
|
139
|
+
`{status: 'skipped', reason}`, where the reason is `unavailable`, `unchanged`, or
|
|
140
|
+
`pending`. The pending guard is scoped to a message/locale pair, so different pairs
|
|
141
|
+
can save concurrently.
|
|
142
|
+
|
|
143
|
+
The optional caller context and persistence result are generic types:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
type SaveContext = {intent: 'save' | 'review'}
|
|
147
|
+
type Receipt = {revision: string}
|
|
148
|
+
|
|
149
|
+
const workflow = useTranslationEditor<SaveContext, Receipt>({
|
|
150
|
+
messages,
|
|
151
|
+
locales,
|
|
152
|
+
onSave: async (update, snapshot) => {
|
|
153
|
+
return persist(update, {
|
|
154
|
+
intent: snapshot.context?.intent ?? 'save',
|
|
155
|
+
previousTranslation: snapshot.baselineTranslation,
|
|
156
|
+
source: snapshot.source,
|
|
157
|
+
})
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const result = await workflow.save({intent: 'review'})
|
|
162
|
+
if (result.status === 'saved') showReceipt(result.value.revision)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`onSave` receives the submitted translation and a frozen metadata object containing
|
|
166
|
+
the source, baseline translation, and context. Draft state and its `save` action
|
|
167
|
+
are render snapshots: retaining an action for a confirmation dialog retains that
|
|
168
|
+
translation, source, and baseline even if selection or edits subsequently change.
|
|
169
|
+
Context is passed by reference, not cloned; pass immutable context values.
|
|
170
|
+
Persistence policy, confirmation UI, and receipt presentation remain with the
|
|
171
|
+
consumer.
|
|
172
|
+
|
|
173
|
+
Existing one-argument `onSave` callbacks and callers that await or ignore `save()`
|
|
174
|
+
continue to work. Callers that explicitly annotate `save()` as `Promise<void>`
|
|
175
|
+
must change that annotation to `Promise<TranslationSaveResult>` (with their result
|
|
176
|
+
type parameter, if needed).
|
|
177
|
+
|
|
178
|
+
`validateTranslation(source, translation)` returns `null` or a localizable error
|
|
179
|
+
code: `empty`, `invalid-source`, `invalid-translation`, or `structure`. It checks
|
|
180
|
+
arguments, tag nesting, formatting styles, select branches, plural type/offset,
|
|
181
|
+
and exact selectors. Locale-specific plural categories are allowed; new
|
|
182
|
+
categories inherit the source `other` branch's argument contract. Repeated
|
|
183
|
+
placeholders do not change that contract. Validation is structural, not a check
|
|
184
|
+
of translation quality.
|
|
185
|
+
|
|
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,
|
|
188
|
+
pagination, source locations, localized validation, reset, and save feedback.
|
|
189
|
+
Supply an `IntlProvider` and the same StyleX Vite integration used by the demo.
|
|
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/index.d.ts
CHANGED
|
@@ -1,4 +1,144 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { MessageFormatElement } from "@formatjs/icu-messageformat-parser";
|
|
3
|
+
//#region packages/editor/message.d.ts
|
|
4
|
+
type ParsedMessage = {
|
|
5
|
+
ast: MessageFormatElement[];
|
|
6
|
+
error: null;
|
|
7
|
+
} | {
|
|
8
|
+
ast: null;
|
|
9
|
+
error: Error;
|
|
10
|
+
};
|
|
11
|
+
/** Retains every ICU branch and skeleton; incomplete edits are valid input. */
|
|
12
|
+
export declare function parseMessage(message: string): ParsedMessage;
|
|
13
|
+
interface MessageProps {
|
|
14
|
+
message: string;
|
|
15
|
+
children: (parsed: ParsedMessage) => ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare function Message({ message, children }: MessageProps): ReactNode;
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region packages/editor/types.d.ts
|
|
20
|
+
interface TranslatedMessage {
|
|
21
|
+
id: string;
|
|
22
|
+
defaultMessage: string;
|
|
23
|
+
translatedMessage: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region packages/editor/core.d.ts
|
|
28
|
+
interface EditorOptions {
|
|
29
|
+
messages: readonly TranslatedMessage[];
|
|
30
|
+
/** Apply the edit to consumer state; persistence stays with the consumer. */
|
|
31
|
+
onMessageChange: (message: TranslatedMessage) => void;
|
|
32
|
+
defaultSelectedId?: string;
|
|
33
|
+
}
|
|
34
|
+
interface EditorState {
|
|
35
|
+
messages: readonly TranslatedMessage[];
|
|
36
|
+
selectedMessage: TranslatedMessage | undefined;
|
|
37
|
+
selectMessage: (id: string) => void;
|
|
38
|
+
query: string;
|
|
39
|
+
setQuery: (query: string) => void;
|
|
40
|
+
source: ParsedMessage | undefined;
|
|
41
|
+
translation: ParsedMessage | undefined;
|
|
42
|
+
setTranslation: (value: string) => void;
|
|
43
|
+
copySource: () => void;
|
|
44
|
+
clearTranslation: () => void;
|
|
45
|
+
}
|
|
46
|
+
/** Controlled message data with no DOM, styling, providers, or network access. */
|
|
47
|
+
export declare function useMessageEditor({ messages, onMessageChange, defaultSelectedId }: EditorOptions): EditorState;
|
|
48
|
+
interface EditorProps extends EditorOptions {
|
|
49
|
+
children: (editor: EditorState) => ReactNode;
|
|
50
|
+
}
|
|
51
|
+
export declare function Editor({ children, ...options }: EditorProps): ReactNode;
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region packages/editor/validation.d.ts
|
|
54
|
+
type TranslationValidationError = "empty" | "invalid-source" | "invalid-translation" | "structure";
|
|
55
|
+
/** Returns a stable error code; consumers own localized error copy. */
|
|
56
|
+
export declare function validateTranslation(source: string, translation: string): TranslationValidationError | null;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region packages/editor/workflow.d.ts
|
|
59
|
+
interface SourceLocation {
|
|
60
|
+
file: string;
|
|
61
|
+
start?: number;
|
|
62
|
+
end?: number;
|
|
63
|
+
}
|
|
64
|
+
interface EditorMessage {
|
|
65
|
+
id: string;
|
|
66
|
+
defaultMessage: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
catalogs?: readonly string[];
|
|
69
|
+
locations?: readonly SourceLocation[];
|
|
70
|
+
translations: Readonly<Record<string, string | undefined>>;
|
|
71
|
+
}
|
|
72
|
+
interface TranslationUpdate {
|
|
73
|
+
id: string;
|
|
74
|
+
locale: string;
|
|
75
|
+
translation: string;
|
|
76
|
+
}
|
|
77
|
+
type MessageStatus = "all" | "translated" | "missing";
|
|
78
|
+
/** Render-snapshot metadata plus consumer-owned context supplied to save. */
|
|
79
|
+
interface TranslationSaveSnapshot<TContext = void> {
|
|
80
|
+
readonly source: string;
|
|
81
|
+
readonly baselineTranslation: string;
|
|
82
|
+
readonly context: TContext | undefined;
|
|
83
|
+
}
|
|
84
|
+
type TranslationSaveResult<TResult = void> = {
|
|
85
|
+
status: "saved";
|
|
86
|
+
value: TResult;
|
|
87
|
+
} | {
|
|
88
|
+
status: "failed";
|
|
89
|
+
error: Error;
|
|
90
|
+
} | {
|
|
91
|
+
status: "invalid";
|
|
92
|
+
validationError: TranslationValidationError;
|
|
93
|
+
} | {
|
|
94
|
+
status: "skipped";
|
|
95
|
+
reason: "unavailable" | "unchanged" | "pending";
|
|
96
|
+
};
|
|
97
|
+
interface TranslationEditorOptions<TContext = void, TResult = void> {
|
|
98
|
+
messages: readonly EditorMessage[];
|
|
99
|
+
locales: readonly string[];
|
|
100
|
+
onSave: (update: TranslationUpdate, snapshot: TranslationSaveSnapshot<TContext>) => TResult | Promise<TResult>;
|
|
101
|
+
defaultLocale?: string;
|
|
102
|
+
pageSize?: number;
|
|
103
|
+
}
|
|
104
|
+
/** A render snapshot and actions for one message/locale pair. */
|
|
105
|
+
interface TranslationDraftState<TContext = void, TResult = void> {
|
|
106
|
+
readonly value: string;
|
|
107
|
+
readonly baseline: string;
|
|
108
|
+
readonly validationError: TranslationValidationError | null;
|
|
109
|
+
readonly changed: boolean;
|
|
110
|
+
readonly isSaving: boolean;
|
|
111
|
+
readonly saveError: Error | null;
|
|
112
|
+
readonly saved: boolean;
|
|
113
|
+
setTranslation: (value: string) => void;
|
|
114
|
+
reset: () => void;
|
|
115
|
+
save: (context?: TContext) => Promise<TranslationSaveResult<TResult>>;
|
|
116
|
+
}
|
|
117
|
+
interface TranslationEditorState<TContext = void, TResult = void> {
|
|
118
|
+
editor: EditorState;
|
|
119
|
+
selectedMessage: EditorMessage | undefined;
|
|
120
|
+
locale: string | undefined;
|
|
121
|
+
setLocale: (locale: string) => void;
|
|
122
|
+
catalogs: readonly string[];
|
|
123
|
+
catalog: string;
|
|
124
|
+
setCatalog: (catalog: string) => void;
|
|
125
|
+
status: MessageStatus;
|
|
126
|
+
setStatus: (status: MessageStatus) => void;
|
|
127
|
+
page: number;
|
|
128
|
+
pageCount: number;
|
|
129
|
+
setPage: (page: number) => void;
|
|
130
|
+
pageMessages: EditorState["messages"];
|
|
131
|
+
validationError: TranslationValidationError | null;
|
|
132
|
+
changed: boolean;
|
|
133
|
+
isSaving: boolean;
|
|
134
|
+
saveError: Error | null;
|
|
135
|
+
saved: boolean;
|
|
136
|
+
save: (context?: TContext) => Promise<TranslationSaveResult<TResult>>;
|
|
137
|
+
reset: () => void;
|
|
138
|
+
getTranslation: (id: string, locale: string) => TranslationDraftState<TContext, TResult> | undefined;
|
|
139
|
+
}
|
|
140
|
+
/** Per-message, per-locale drafts layered on the existing headless editor. */
|
|
141
|
+
export declare function useTranslationEditor<TContext = void, TResult = void>({ messages, locales, onSave, defaultLocale, pageSize }: TranslationEditorOptions<TContext, TResult>): TranslationEditorState<TContext, TResult>;
|
|
142
|
+
//#endregion
|
|
143
|
+
export type { EditorMessage, EditorOptions, EditorProps, EditorState, MessageProps, MessageStatus, ParsedMessage, SourceLocation, TranslatedMessage, TranslationDraftState, TranslationEditorOptions, TranslationEditorState, TranslationSaveResult, TranslationSaveSnapshot, TranslationUpdate, TranslationValidationError };
|
|
4
144
|
//# sourceMappingURL=index.d.ts.map
|