@formatjs/editor 1.3.0 → 1.5.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 +363 -3
- package/package.json +6 -2
- package/ui.d.ts +307 -0
- package/ui.js +524 -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,367 @@ 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 native controls and uses React plus the package's existing ICU parser. The headless root does not import the UI, StyleX, icons, or React Intl.
|
|
198
|
+
|
|
199
|
+
Mount one workflow above the views and pass its existing drafts:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
import {useTranslationEditor} from '@formatjs/editor'
|
|
203
|
+
import {TranslationEditorView} from '@formatjs/editor/ui'
|
|
204
|
+
|
|
205
|
+
const workflow = useTranslationEditor({messages, locales, onSave: persist})
|
|
206
|
+
const selected = workflow.selectedMessage
|
|
207
|
+
return (
|
|
208
|
+
<TranslationEditorView
|
|
209
|
+
messages={workflow.pageMessages}
|
|
210
|
+
selectedMessage={selected}
|
|
211
|
+
onSelect={workflow.editor.selectMessage}
|
|
212
|
+
search={{
|
|
213
|
+
value: workflow.editor.query,
|
|
214
|
+
onValueChange: workflow.editor.setQuery,
|
|
215
|
+
}}
|
|
216
|
+
translations={
|
|
217
|
+
selected
|
|
218
|
+
? visibleLocales.map(locale => {
|
|
219
|
+
const draft = workflow.getTranslation(selected.id, locale)!
|
|
220
|
+
return {
|
|
221
|
+
locale,
|
|
222
|
+
draft,
|
|
223
|
+
onSave: () => {
|
|
224
|
+
void draft.save()
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
: []
|
|
229
|
+
}
|
|
230
|
+
/>
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`visibleLocales` must be a subset of the workflow's available `locales`. Hiding a
|
|
235
|
+
view does not remove its draft. The view does not create a workflow or own
|
|
236
|
+
selection, fetching, filtering, pagination, locale visibility, or persistence.
|
|
237
|
+
For server-side search, pass the loaded page directly as `messages`, your search
|
|
238
|
+
value/callback as `search`, and your externally selected detail as
|
|
239
|
+
`selectedMessage`. Selection can remain outside the loaded page. `loading` marks
|
|
240
|
+
navigation busy and displays a status; it does not clear the controlled list.
|
|
241
|
+
|
|
242
|
+
### Component adapters
|
|
243
|
+
|
|
244
|
+
`EditorDesignSystemProvider` accepts partial overrides of `EditorComponents`: `Button`,
|
|
245
|
+
`TextInput`, `TextArea`, `MessageRow`, `Panel`, and `Layout`, plus the optional tool
|
|
246
|
+
adapters described below. Unspecified or explicitly `undefined` 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 `ResolvedEditorComponents` contract, including native defaults for every optional tool adapter. 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`, optional `sidebar` 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
|
+
### Reusable translation tools
|
|
338
|
+
|
|
339
|
+
`LocalePicker`, `MessagePreview`, `CopyTextButton`, and `MessageContext` are also
|
|
340
|
+
exported from `@formatjs/editor/ui`. They use the same `EditorDesignSystemProvider`
|
|
341
|
+
and work independently or inside the composed view's slots:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
import {
|
|
345
|
+
CopyTextButton,
|
|
346
|
+
LocalePicker,
|
|
347
|
+
MessageContext,
|
|
348
|
+
MessagePreview,
|
|
349
|
+
} from '@formatjs/editor/ui'
|
|
350
|
+
|
|
351
|
+
function TranslationTools() {
|
|
352
|
+
return (
|
|
353
|
+
<>
|
|
354
|
+
<LocalePicker
|
|
355
|
+
locales={availableLocales}
|
|
356
|
+
selectedLocales={selectedLocales}
|
|
357
|
+
onChange={setSelectedLocales}
|
|
358
|
+
getLocaleLabel={locale => displayNames.of(locale) ?? locale}
|
|
359
|
+
/>
|
|
360
|
+
|
|
361
|
+
<MessagePreview message={draft.value} />
|
|
362
|
+
<CopyTextButton value={draft.value} label="translation" />
|
|
363
|
+
<MessageContext message={selectedMessage} />
|
|
364
|
+
</>
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
- **LocalePicker** reports unique available locale codes in input order through
|
|
370
|
+
`onChange(locales: string[])`. Selection is controlled; duplicate and unavailable
|
|
371
|
+
selections do not distort select-all or mixed state. A selection change drops
|
|
372
|
+
unavailable codes. The picker uses unique label/control IDs and balanced,
|
|
373
|
+
contiguous columns that collapse without changing reading order. Empty lists
|
|
374
|
+
disable select-all. `getLocaleLabel` defaults to the code so display language
|
|
375
|
+
stays explicit. `labels` overrides `title`, `empty`, `clear`, and the formatter
|
|
376
|
+
callbacks `selectAll(count)`, `selected(count)`, and `trigger(summary)`.
|
|
377
|
+
- **MessagePreview** shows ICU structure, not a formatted sample result: all plural
|
|
378
|
+
and select branches, ordinal types, plural offsets, rich-text tags, and exact
|
|
379
|
+
number/date/time format tokens (including skeletons). Literals retain whitespace;
|
|
380
|
+
tags are displayed as text, never executed as HTML. Incomplete syntax produces
|
|
381
|
+
an alert; `formatError(error)` can localize its contents. No argument values are
|
|
382
|
+
required, and empty text is valid.
|
|
383
|
+
- **CopyTextButton** copies `value` verbatim through the browser Clipboard API.
|
|
384
|
+
`writeText(value): Promise<void>` can supply a different clipboard implementation,
|
|
385
|
+
including a host-specific fallback. Unsupported access and rejected writes show
|
|
386
|
+
failure feedback, never success. Pending writes disable the control. Changing
|
|
387
|
+
the value or writer, or unmounting, invalidates old results and callbacks.
|
|
388
|
+
`onCopy(value)` and `onError(error, value)` report completed current requests.
|
|
389
|
+
Feedback expires after `feedbackDurationMs` (default 1500); `labels` provides
|
|
390
|
+
`copy`, `copying`, `copied`, and `failed` formatters receiving the supplied label.
|
|
391
|
+
Native clipboard access happens only when the user presses the button.
|
|
392
|
+
- **MessageContext** renders an ID, optional description, catalogs, and source
|
|
393
|
+
locations from the existing `EditorMessage` metadata contract. Locations render
|
|
394
|
+
as plain text with optional start/end ranges; `renderLocation(location)` can
|
|
395
|
+
customize them. `copyId` defaults to true and `copyOptions` passes clipboard
|
|
396
|
+
callbacks, labels, writer, and timing to the ID's copy control. `labels` overrides
|
|
397
|
+
`title`, `id`, `description`, `catalogs`, and `locations`. A missing message clears
|
|
398
|
+
the content; absent metadata sections are omitted.
|
|
399
|
+
|
|
400
|
+
Tool adapters are optional in `EditorComponents`, so existing complete registries
|
|
401
|
+
remain valid. The hook resolves all of them to native defaults. Override them in
|
|
402
|
+
one provider to use the same design system across fields and standalone tools:
|
|
403
|
+
|
|
404
|
+
| Adapter / props | Inputs | Output callback |
|
|
405
|
+
| ------------------------------------------------------ | --------------------------------------------------------------------------------------- | ----------------------------------- |
|
|
406
|
+
| `Checkbox` / `EditorCheckboxProps` | `id`, `checked` (boolean or `indeterminate`), optional `disabled` | `onCheckedChange(checked: boolean)` |
|
|
407
|
+
| `LocalePickerLayout` / `EditorLocalePickerLayoutProps` | `id`, `title`, `summary`, `triggerLabel`, `open`, `controls`, ordered `columns` | `onOpenChange(open: boolean)` |
|
|
408
|
+
| `PreviewToken` / `EditorPreviewTokenProps` | `children`, `kind` (argument, number, date, time, tag, selector, plural, pound, syntax) | None |
|
|
409
|
+
| `CopyButton` / `EditorCopyButtonProps` | `label`, `status` (idle, copying, copied, error), `disabled` | `onPress()` |
|
|
410
|
+
| `Metadata` / `EditorMetadataProps` | Accessible `label`, `children` | None |
|
|
411
|
+
|
|
412
|
+
Checkbox adapters forward IDs, expose mixed state, honor disabled state, and report
|
|
413
|
+
booleans without DOM events. Locale-picker layouts can use a popover, disclosure,
|
|
414
|
+
or dialog: they own open-state interaction, focus handling, and responsive layout,
|
|
415
|
+
while preserving column order and rendering every control. Copy-button adapters
|
|
416
|
+
can map status to icons, but must retain the accessible label and must not submit
|
|
417
|
+
forms. Preview-token adapters render children as text/content, never raw HTML.
|
|
418
|
+
The tools demo (`demo/tools-demo.tsx`, browser fixture `/?tools=1`) combines these
|
|
419
|
+
components with the StyleX registry.
|
|
420
|
+
|
|
421
|
+
### Custom message rows and composed layouts
|
|
422
|
+
|
|
423
|
+
`MessageList` and `TranslationEditorView` infer your message type from `messages`,
|
|
424
|
+
so row renderers retain application metadata without casts. `renderMessage` receives
|
|
425
|
+
`(message, {selected})` and returns **noninteractive** content inside the existing
|
|
426
|
+
selection control. `renderMessageActions` receives the same inputs and renders
|
|
427
|
+
sibling controls in the list item; pressing an action does not select the row.
|
|
428
|
+
Actions should have accessible names and use non-submitting buttons. The library
|
|
429
|
+
continues to own row keys, selection callbacks, list semantics, and search wiring.
|
|
430
|
+
Omit a renderer for the standard content; return `null` to suppress that slot.
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
const messages = [
|
|
434
|
+
{id: 'greeting', defaultMessage: 'Hello {name}', translatedCount: 3},
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
function CatalogEditor() {
|
|
438
|
+
const {Button} = useEditorDesignSystem()
|
|
439
|
+
return (
|
|
440
|
+
<TranslationEditorView
|
|
441
|
+
messages={messages}
|
|
442
|
+
selectedId={selectedId}
|
|
443
|
+
selectedMessage={loadedDetail}
|
|
444
|
+
onSelect={selectMessage}
|
|
445
|
+
translations={translations}
|
|
446
|
+
listSummary={<output>{total} results</output>}
|
|
447
|
+
renderMessage={(message, {selected}) => (
|
|
448
|
+
<>
|
|
449
|
+
<span>{message.defaultMessage}</span>
|
|
450
|
+
<span>{message.translatedCount} translations</span>
|
|
451
|
+
{selected && <span>Selected</span>}
|
|
452
|
+
</>
|
|
453
|
+
)}
|
|
454
|
+
renderMessageActions={message => (
|
|
455
|
+
<Button variant="secondary" onPress={() => openReview(message.id)}>
|
|
456
|
+
Review {message.id}
|
|
457
|
+
</Button>
|
|
458
|
+
)}
|
|
459
|
+
renderTranslations={fields => <div className="locale-grid">{fields}</div>}
|
|
460
|
+
renderContent={content => (
|
|
461
|
+
<section aria-label="Translation details">
|
|
462
|
+
{content}
|
|
463
|
+
<p>Drafts remain available when you hide a locale.</p>
|
|
464
|
+
</section>
|
|
465
|
+
)}
|
|
466
|
+
sidebar={<SourceMetadata message={loadedDetail} />}
|
|
467
|
+
emptyState={<output>Loading message details…</output>}
|
|
468
|
+
/>
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`selectedId` controls list highlighting independently of fetched detail; it defaults
|
|
474
|
+
to `selectedMessage?.id`. `selectedMessage` can remain outside the loaded page.
|
|
475
|
+
`listSummary` appears after search and before the rows/loading status. Lists remain
|
|
476
|
+
caller-controlled: passing `loading` does not clear existing rows.
|
|
477
|
+
|
|
478
|
+
`renderTranslations` wraps the generated locale fields (including an empty array)
|
|
479
|
+
when a message is selected. `renderContent` wraps the whole detail region, including
|
|
480
|
+
notices and the empty state. These are render functions, not component types;
|
|
481
|
+
keep any component types they return stable so fields retain focus. The wrappers
|
|
482
|
+
must render their provided children to retain the built-in editing UI. Draft
|
|
483
|
+
lifetime still belongs to the caller's workflow.
|
|
484
|
+
|
|
485
|
+
`sidebar` is passed separately to the context's `Layout` adapter, which chooses
|
|
486
|
+
its placement and responsive behavior. Layout adapters must render this optional
|
|
487
|
+
prop to support sidebars; existing adapters that do not use sidebars remain valid.
|
|
488
|
+
`emptyState` replaces the default no-selection status (`null` suppresses it).
|
|
489
|
+
Per-locale `translations[].labels` override shared labels, with validation labels
|
|
490
|
+
merged individually, so locale-specific save labels do not drop shared errors.
|
|
491
|
+
|
|
492
|
+
### Application slots and localization
|
|
493
|
+
|
|
494
|
+
`filters`, `pagination`, `context`, and `notice` accept React nodes. Supply your
|
|
495
|
+
own locale picker or filter controls in `filters`; no native-select API is
|
|
496
|
+
imposed on applications with multi-select or asynchronous selectors.
|
|
497
|
+
`sourcePreview` and each translation's `preview` can render a custom preview.
|
|
498
|
+
Each translation also accepts a readable `label` and an `actions` slot (`null`
|
|
499
|
+
suppresses default actions).
|
|
500
|
+
|
|
501
|
+
A translation's `onSave` is a command callback. It can open a confirmation dialog
|
|
502
|
+
that retains the supplied draft's `save` action, then supply typed context and
|
|
503
|
+
handle the returned result. The view never calls persistence itself or interprets
|
|
504
|
+
application receipts. Without `onSave`, default actions include copy and reset
|
|
505
|
+
but no save button. Validation and pending state disable the default save button;
|
|
506
|
+
custom action slots own their own disabled/confirmation behavior.
|
|
507
|
+
|
|
508
|
+
All built-in strings can be overridden through `labels`, including individual
|
|
509
|
+
`labels.validation` entries. Supply already-localized strings from your preferred
|
|
510
|
+
library. The workflow demo demonstrates a React Intl consumer without making
|
|
511
|
+
`IntlProvider` a requirement for the public UI.
|
|
512
|
+
|
|
513
|
+
## Browser interaction tests
|
|
514
|
+
|
|
515
|
+
Write native Playwright `*.spec.ts` files in `packages/editor/vrt/`. The
|
|
516
|
+
`e2eConfig` helper supplies `baseURL`, so specs can use `page.goto('/')`,
|
|
517
|
+
accessible locators, clicks, and web-first assertions. VRT captures are generated from the `.visual.tsx` module. Both targets share the consumer-owned
|
|
518
|
+
server, shell, declared inputs, and pinned Testcontainers browser.
|
|
519
|
+
|
|
520
|
+
```sh
|
|
521
|
+
bazel test //packages/editor/vrt:e2e_test --test_output=errors
|
|
522
|
+
bazel test //packages/editor/vrt:e2e_test --test_arg=--grep=translation
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
E2E covers editing, search, selection, copy/clear, ICU error recovery, locale
|
|
526
|
+
drafts, and saving. It requires Docker and runs manually, locally, and uncached.
|
|
527
|
+
CI should explicitly select both `e2e_test` and `visual_test`. Failures retain
|
|
528
|
+
JUnit, screenshots, and Playwright traces in undeclared test outputs.
|
|
529
|
+
|
|
530
|
+
## Component browser tests
|
|
531
|
+
|
|
532
|
+
`bazel test //packages/editor/vrt:component_test --test_output=errors` runs
|
|
533
|
+
Playwright 1.63 native `mount()` specs for the real editor. The typed
|
|
534
|
+
`editor.visual.tsx` uses the existing provider shell and demo; `gallery.tsx` owns
|
|
535
|
+
mount/update/unmount. The tests check provider updates without losing a draft,
|
|
536
|
+
clear, ICU validation, and isolation between mounts.
|
|
537
|
+
|
|
538
|
+
`component_browser_test` shares the custom server, root npm dependencies, strict
|
|
539
|
+
typechecks, and pinned Testcontainers browser with E2E and VRT.
|
|
540
|
+
`componentBrowserConfig` discovers `*.browser.spec.ts` separately from the E2E
|
|
541
|
+
`*.spec.ts` and generated VRT capture cases. CI should explicitly run all three
|
|
542
|
+
manual browser targets. Screenshot baselines and updates remain in `visual_test`.
|
|
543
|
+
|
|
544
|
+
The default export of `editor.visual.tsx` is a `ComponentVisualModule`: it declares
|
|
545
|
+
renderable cases, browser-side capture hooks, and VRT options. The gallery registers
|
|
546
|
+
that module with `installVisualGallery`. The shared runtime generates all six
|
|
547
|
+
screenshot tests; no `editor.visual.spec.ts` is maintained. Interaction tests stay
|
|
548
|
+
in `editor.browser.spec.tsx`, and the existing PNG names remain explicit in the
|
|
549
|
+
visual declarations.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formatjs/editor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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,307 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from "react";
|
|
2
|
+
import "@formatjs/icu-messageformat-parser";
|
|
3
|
+
//#region packages/editor/design-system.d.ts
|
|
4
|
+
interface EditorButtonProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Called once per activation, without a DOM event; disabled controls must not call it. */
|
|
7
|
+
onPress: () => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
variant: "primary" | "secondary";
|
|
10
|
+
}
|
|
11
|
+
interface EditorInputProps {
|
|
12
|
+
id: string;
|
|
13
|
+
value: string;
|
|
14
|
+
/** Reports the complete next string value; never a DOM event. */
|
|
15
|
+
onValueChange: (value: string) => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
"aria-invalid"?: boolean;
|
|
18
|
+
"aria-describedby"?: string;
|
|
19
|
+
}
|
|
20
|
+
interface EditorTextInputProps extends EditorInputProps {
|
|
21
|
+
type: "text" | "search";
|
|
22
|
+
}
|
|
23
|
+
interface EditorTextAreaProps extends EditorInputProps {
|
|
24
|
+
/** Visible rows when supported by the control; defaults to six. */
|
|
25
|
+
rows?: number;
|
|
26
|
+
}
|
|
27
|
+
interface EditorMessageRowProps {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
selected: boolean;
|
|
30
|
+
/** Activates this row without changing controlled selection itself. */
|
|
31
|
+
onSelect: () => void;
|
|
32
|
+
}
|
|
33
|
+
interface EditorPanelProps {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
label: string;
|
|
36
|
+
kind: "source" | "translation";
|
|
37
|
+
}
|
|
38
|
+
interface EditorLayoutProps {
|
|
39
|
+
toolbar: ReactNode;
|
|
40
|
+
navigation: ReactNode;
|
|
41
|
+
content: ReactNode;
|
|
42
|
+
/** Optional secondary content, arranged by the design-system layout. */
|
|
43
|
+
sidebar?: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
/** Define adapters outside render so controls retain focus across edits. */
|
|
46
|
+
interface EditorComponents extends Partial<EditorToolComponents> {
|
|
47
|
+
Button: ComponentType<EditorButtonProps>;
|
|
48
|
+
TextInput: ComponentType<EditorTextInputProps>;
|
|
49
|
+
TextArea: ComponentType<EditorTextAreaProps>;
|
|
50
|
+
MessageRow: ComponentType<EditorMessageRowProps>;
|
|
51
|
+
Panel: ComponentType<EditorPanelProps>;
|
|
52
|
+
Layout: ComponentType<EditorLayoutProps>;
|
|
53
|
+
}
|
|
54
|
+
interface EditorCheckboxProps {
|
|
55
|
+
id: string;
|
|
56
|
+
checked: boolean | "indeterminate";
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
onCheckedChange: (checked: boolean) => void;
|
|
59
|
+
}
|
|
60
|
+
interface EditorLocalePickerLayoutProps {
|
|
61
|
+
id: string;
|
|
62
|
+
title: string;
|
|
63
|
+
summary: string;
|
|
64
|
+
triggerLabel: string;
|
|
65
|
+
open: boolean;
|
|
66
|
+
onOpenChange: (open: boolean) => void;
|
|
67
|
+
controls: ReactNode;
|
|
68
|
+
/** Contiguous, balanced columns in the caller's locale order. */
|
|
69
|
+
columns: readonly ReactNode[];
|
|
70
|
+
}
|
|
71
|
+
interface EditorPreviewTokenProps {
|
|
72
|
+
children: ReactNode;
|
|
73
|
+
kind: "argument" | "number" | "date" | "time" | "tag" | "selector" | "plural" | "pound" | "syntax";
|
|
74
|
+
}
|
|
75
|
+
type EditorCopyStatus = "idle" | "copying" | "copied" | "error";
|
|
76
|
+
interface EditorCopyButtonProps {
|
|
77
|
+
label: string;
|
|
78
|
+
status: EditorCopyStatus;
|
|
79
|
+
disabled: boolean;
|
|
80
|
+
onPress: () => void;
|
|
81
|
+
}
|
|
82
|
+
interface EditorMetadataProps {
|
|
83
|
+
label: string;
|
|
84
|
+
children: ReactNode;
|
|
85
|
+
}
|
|
86
|
+
/** Optional additions preserve existing complete design-system registries. */
|
|
87
|
+
interface EditorToolComponents {
|
|
88
|
+
Checkbox: ComponentType<EditorCheckboxProps>;
|
|
89
|
+
LocalePickerLayout: ComponentType<EditorLocalePickerLayoutProps>;
|
|
90
|
+
PreviewToken: ComponentType<EditorPreviewTokenProps>;
|
|
91
|
+
CopyButton: ComponentType<EditorCopyButtonProps>;
|
|
92
|
+
Metadata: ComponentType<EditorMetadataProps>;
|
|
93
|
+
}
|
|
94
|
+
type ResolvedEditorComponents = Required<EditorComponents>;
|
|
95
|
+
/** Unstyled native controls; no CSS, icons, or localization provider is required. */
|
|
96
|
+
export declare const nativeEditorComponents: ResolvedEditorComponents;
|
|
97
|
+
interface EditorDesignSystemProviderProps {
|
|
98
|
+
/** Overrides inherit unspecified components from the nearest provider. */
|
|
99
|
+
components: Partial<EditorComponents>;
|
|
100
|
+
children: ReactNode;
|
|
101
|
+
}
|
|
102
|
+
/** Configure a tree once; sibling providers remain independent. */
|
|
103
|
+
export declare function EditorDesignSystemProvider({ components, children }: EditorDesignSystemProviderProps): ReactNode;
|
|
104
|
+
/** Read the resolved controls, including native defaults outside a provider. */
|
|
105
|
+
export declare function useEditorDesignSystem(): Readonly<ResolvedEditorComponents>;
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region packages/editor/locale-picker.d.ts
|
|
108
|
+
interface LocalePickerLabels {
|
|
109
|
+
title: string;
|
|
110
|
+
empty: string;
|
|
111
|
+
clear: string;
|
|
112
|
+
selectAll: (count: number) => string;
|
|
113
|
+
selected: (count: number) => string;
|
|
114
|
+
trigger: (summary: string) => string;
|
|
115
|
+
}
|
|
116
|
+
interface LocalePickerProps {
|
|
117
|
+
locales: readonly string[];
|
|
118
|
+
selectedLocales: readonly string[];
|
|
119
|
+
/** Reports unique available locales in their input order; never mutates selection. */
|
|
120
|
+
onChange: (locales: string[]) => void;
|
|
121
|
+
/** Defaults to the locale code; consumers choose the display language. */
|
|
122
|
+
getLocaleLabel?: (locale: string) => string;
|
|
123
|
+
labels?: Partial<LocalePickerLabels>;
|
|
124
|
+
}
|
|
125
|
+
export declare function LocalePicker({ locales, selectedLocales, onChange, getLocaleLabel, labels }: LocalePickerProps): ReactNode;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region packages/editor/message-preview.d.ts
|
|
128
|
+
interface MessagePreviewProps {
|
|
129
|
+
message: string;
|
|
130
|
+
/** Customize a parser error without suppressing the alert semantics. */
|
|
131
|
+
formatError?: (error: Error) => ReactNode;
|
|
132
|
+
}
|
|
133
|
+
/** Structural ICU preview: shows every branch, without evaluating values or HTML. */
|
|
134
|
+
export declare function MessagePreview({ message, formatError }: MessagePreviewProps): ReactNode;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region packages/editor/copy-text-button.d.ts
|
|
137
|
+
interface CopyTextLabels {
|
|
138
|
+
copy: (label: string) => string;
|
|
139
|
+
copying: (label: string) => string;
|
|
140
|
+
copied: (label: string) => string;
|
|
141
|
+
failed: (label: string) => string;
|
|
142
|
+
}
|
|
143
|
+
interface CopyTextButtonProps {
|
|
144
|
+
value: string;
|
|
145
|
+
label: string;
|
|
146
|
+
disabled?: boolean;
|
|
147
|
+
/** Injectable clipboard boundary; rejects when the write fails. */
|
|
148
|
+
writeText?: (value: string) => Promise<void>;
|
|
149
|
+
onCopy?: (value: string) => void;
|
|
150
|
+
onError?: (error: Error, value: string) => void;
|
|
151
|
+
feedbackDurationMs?: number;
|
|
152
|
+
labels?: Partial<CopyTextLabels>;
|
|
153
|
+
}
|
|
154
|
+
/** Copies exact text; feedback belongs to the current value and mounted control. */
|
|
155
|
+
export declare function CopyTextButton({ value, label, disabled, writeText, onCopy, onError, feedbackDurationMs, labels }: CopyTextButtonProps): ReactNode;
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region packages/editor/validation.d.ts
|
|
158
|
+
type TranslationValidationError = "empty" | "invalid-source" | "invalid-translation" | "structure";
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region packages/editor/workflow.d.ts
|
|
161
|
+
interface SourceLocation {
|
|
162
|
+
file: string;
|
|
163
|
+
start?: number;
|
|
164
|
+
end?: number;
|
|
165
|
+
}
|
|
166
|
+
interface EditorMessage {
|
|
167
|
+
id: string;
|
|
168
|
+
defaultMessage: string;
|
|
169
|
+
description?: string;
|
|
170
|
+
catalogs?: readonly string[];
|
|
171
|
+
locations?: readonly SourceLocation[];
|
|
172
|
+
translations: Readonly<Record<string, string | undefined>>;
|
|
173
|
+
}
|
|
174
|
+
type TranslationSaveResult<TResult = void> = {
|
|
175
|
+
status: "saved";
|
|
176
|
+
value: TResult;
|
|
177
|
+
} | {
|
|
178
|
+
status: "failed";
|
|
179
|
+
error: Error;
|
|
180
|
+
} | {
|
|
181
|
+
status: "invalid";
|
|
182
|
+
validationError: TranslationValidationError;
|
|
183
|
+
} | {
|
|
184
|
+
status: "skipped";
|
|
185
|
+
reason: "unavailable" | "unchanged" | "pending";
|
|
186
|
+
};
|
|
187
|
+
/** A render snapshot and actions for one message/locale pair. */
|
|
188
|
+
interface TranslationDraftState<TContext = void, TResult = void> {
|
|
189
|
+
readonly value: string;
|
|
190
|
+
readonly baseline: string;
|
|
191
|
+
readonly validationError: TranslationValidationError | null;
|
|
192
|
+
readonly changed: boolean;
|
|
193
|
+
readonly isSaving: boolean;
|
|
194
|
+
readonly saveError: Error | null;
|
|
195
|
+
readonly saved: boolean;
|
|
196
|
+
setTranslation: (value: string) => void;
|
|
197
|
+
reset: () => void;
|
|
198
|
+
save: (context?: TContext) => Promise<TranslationSaveResult<TResult>>;
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region packages/editor/message-context.d.ts
|
|
202
|
+
interface MessageContextLabels {
|
|
203
|
+
title: string;
|
|
204
|
+
id: string;
|
|
205
|
+
description: string;
|
|
206
|
+
catalogs: string;
|
|
207
|
+
locations: string;
|
|
208
|
+
}
|
|
209
|
+
interface MessageContextProps {
|
|
210
|
+
message?: Pick<EditorMessage, "id" | "description" | "catalogs" | "locations"> | null;
|
|
211
|
+
labels?: Partial<MessageContextLabels>;
|
|
212
|
+
copyId?: boolean;
|
|
213
|
+
copyOptions?: Pick<CopyTextButtonProps, "writeText" | "onCopy" | "onError" | "labels" | "feedbackDurationMs">;
|
|
214
|
+
/** Default locations are plain text, never file URLs or HTML. */
|
|
215
|
+
renderLocation?: (location: SourceLocation) => ReactNode;
|
|
216
|
+
}
|
|
217
|
+
export declare function MessageContext({ message, labels, copyId, copyOptions, renderLocation }: MessageContextProps): ReactNode;
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region packages/editor/ui.d.ts
|
|
220
|
+
export interface EditorLabels {
|
|
221
|
+
search: string;
|
|
222
|
+
messages: string;
|
|
223
|
+
source: string;
|
|
224
|
+
noMessages: string;
|
|
225
|
+
noSelection: string;
|
|
226
|
+
loading: string;
|
|
227
|
+
copySource: string;
|
|
228
|
+
reset: string;
|
|
229
|
+
save: string;
|
|
230
|
+
saving: string;
|
|
231
|
+
saved: string;
|
|
232
|
+
unsaved: string;
|
|
233
|
+
unchanged: string;
|
|
234
|
+
validation: Record<TranslationValidationError, string>;
|
|
235
|
+
}
|
|
236
|
+
export type EditorLabelOverrides = Partial<Omit<EditorLabels, "validation">> & {
|
|
237
|
+
validation?: Partial<EditorLabels["validation"]>;
|
|
238
|
+
};
|
|
239
|
+
interface ViewOptions {
|
|
240
|
+
labels?: EditorLabelOverrides;
|
|
241
|
+
}
|
|
242
|
+
export type EditorViewMessage = Pick<EditorMessage, "id" | "defaultMessage" | "description">;
|
|
243
|
+
export interface EditorSearch {
|
|
244
|
+
value: string;
|
|
245
|
+
onValueChange: (value: string) => void;
|
|
246
|
+
}
|
|
247
|
+
export interface EditorMessageRenderState {
|
|
248
|
+
selected: boolean;
|
|
249
|
+
}
|
|
250
|
+
export interface MessageListProps<Message extends EditorViewMessage = EditorViewMessage> extends ViewOptions {
|
|
251
|
+
messages: readonly Message[];
|
|
252
|
+
selectedId?: string;
|
|
253
|
+
onSelect: (id: string) => void;
|
|
254
|
+
search?: EditorSearch;
|
|
255
|
+
loading?: boolean;
|
|
256
|
+
pagination?: ReactNode;
|
|
257
|
+
/** Summary or controls between search and the loaded rows. */
|
|
258
|
+
listSummary?: ReactNode;
|
|
259
|
+
/** Noninteractive content inside the design-system selection control. */
|
|
260
|
+
renderMessage?: (message: Message, state: EditorMessageRenderState) => ReactNode;
|
|
261
|
+
/** Interactive actions rendered beside, never inside, the selection control. */
|
|
262
|
+
renderMessageActions?: (message: Message, state: EditorMessageRenderState) => ReactNode;
|
|
263
|
+
}
|
|
264
|
+
/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */
|
|
265
|
+
export declare function MessageList<Message extends EditorViewMessage = EditorViewMessage>({ messages, selectedId, onSelect, search, loading, pagination, listSummary, renderMessage, renderMessageActions, labels }: MessageListProps<Message>): ReactNode;
|
|
266
|
+
export interface SourceMessageProps extends ViewOptions {
|
|
267
|
+
message: EditorViewMessage;
|
|
268
|
+
preview?: ReactNode;
|
|
269
|
+
context?: ReactNode;
|
|
270
|
+
}
|
|
271
|
+
export declare function SourceMessage({ message, preview, context, labels }: SourceMessageProps): ReactNode;
|
|
272
|
+
export type TranslationFieldDraft = Pick<TranslationDraftState, "value" | "validationError" | "changed" | "isSaving" | "saveError" | "saved" | "setTranslation" | "reset">;
|
|
273
|
+
export interface TranslationFieldProps extends ViewOptions {
|
|
274
|
+
locale: string;
|
|
275
|
+
/** Human-readable label; defaults to the locale identifier. */
|
|
276
|
+
label?: string;
|
|
277
|
+
source: string;
|
|
278
|
+
draft: TranslationFieldDraft;
|
|
279
|
+
/** The caller owns confirmation, context, persistence, and receipt presentation. */
|
|
280
|
+
onSave?: () => void;
|
|
281
|
+
/** Overrides the default copy/reset/save actions, including with null. */
|
|
282
|
+
actions?: ReactNode;
|
|
283
|
+
preview?: ReactNode;
|
|
284
|
+
}
|
|
285
|
+
export declare function TranslationField({ locale, label, source, draft, onSave, actions, preview, labels }: TranslationFieldProps): ReactNode;
|
|
286
|
+
export type EditorTranslation = Omit<TranslationFieldProps, "source">;
|
|
287
|
+
export interface TranslationEditorViewProps<Message extends EditorViewMessage = EditorViewMessage> extends MessageListProps<Message> {
|
|
288
|
+
/** May be outside the loaded list, e.g. during a server-side page change. */
|
|
289
|
+
selectedMessage?: EditorViewMessage;
|
|
290
|
+
translations: readonly EditorTranslation[];
|
|
291
|
+
filters?: ReactNode;
|
|
292
|
+
context?: ReactNode;
|
|
293
|
+
sourcePreview?: ReactNode;
|
|
294
|
+
notice?: ReactNode;
|
|
295
|
+
sidebar?: ReactNode;
|
|
296
|
+
/** Replaces the no-selection status; null suppresses it. */
|
|
297
|
+
emptyState?: ReactNode;
|
|
298
|
+
/** Wrap the generated locale fields without recreating their wiring. */
|
|
299
|
+
renderTranslations?: (fields: ReactNode) => ReactNode;
|
|
300
|
+
/** Wrap the entire detail region, including notices and the empty state. */
|
|
301
|
+
renderContent?: (content: ReactNode) => ReactNode;
|
|
302
|
+
}
|
|
303
|
+
/** A stateless composition over caller-owned navigation and locale drafts. */
|
|
304
|
+
export declare function TranslationEditorView<Message extends EditorViewMessage = EditorViewMessage>({ selectedMessage, selectedId, translations, filters, context, sourcePreview, notice, sidebar, emptyState, renderTranslations, renderContent, labels, ...list }: TranslationEditorViewProps<Message>): ReactNode;
|
|
305
|
+
//#endregion
|
|
306
|
+
export type { CopyTextButtonProps, CopyTextLabels, EditorButtonProps, EditorCheckboxProps, EditorComponents, EditorCopyButtonProps, EditorCopyStatus, EditorDesignSystemProviderProps, EditorInputProps, EditorLayoutProps, EditorLocalePickerLayoutProps, EditorMessageRowProps, EditorMetadataProps, EditorPanelProps, EditorPreviewTokenProps, EditorTextAreaProps, EditorTextInputProps, EditorToolComponents, LocalePickerLabels, LocalePickerProps, MessageContextLabels, MessageContextProps, MessagePreviewProps, ResolvedEditorComponents };
|
|
307
|
+
//# sourceMappingURL=ui.d.ts.map
|
package/ui.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { createContext, createElement, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { TYPE, parse } from "@formatjs/icu-messageformat-parser";
|
|
4
|
+
//#region packages/editor/design-system.tsx
|
|
5
|
+
/** Unstyled native controls; no CSS, icons, or localization provider is required. */
|
|
6
|
+
const nativeEditorComponents = {
|
|
7
|
+
Checkbox: function NativeCheckbox({ checked, onCheckedChange, ...props }) {
|
|
8
|
+
const ref = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (ref.current) ref.current.indeterminate = checked === "indeterminate";
|
|
11
|
+
}, [checked]);
|
|
12
|
+
return /* @__PURE__ */ jsx("input", {
|
|
13
|
+
...props,
|
|
14
|
+
ref,
|
|
15
|
+
type: "checkbox",
|
|
16
|
+
checked: checked === true,
|
|
17
|
+
"aria-checked": checked === "indeterminate" ? "mixed" : checked,
|
|
18
|
+
onChange: (event) => onCheckedChange(event.target.checked)
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
LocalePickerLayout: ({ id, title, summary, triggerLabel, open, onOpenChange, controls, columns }) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("button", {
|
|
22
|
+
type: "button",
|
|
23
|
+
"aria-label": triggerLabel,
|
|
24
|
+
"aria-expanded": open,
|
|
25
|
+
"aria-controls": id,
|
|
26
|
+
onClick: () => onOpenChange(!open),
|
|
27
|
+
children: summary
|
|
28
|
+
}), /* @__PURE__ */ jsxs("fieldset", {
|
|
29
|
+
id,
|
|
30
|
+
hidden: !open,
|
|
31
|
+
children: [
|
|
32
|
+
/* @__PURE__ */ jsx("legend", { children: title }),
|
|
33
|
+
controls,
|
|
34
|
+
/* @__PURE__ */ jsx("div", {
|
|
35
|
+
style: {
|
|
36
|
+
display: "grid",
|
|
37
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 16rem), 1fr))",
|
|
38
|
+
maxHeight: "60vh",
|
|
39
|
+
overflowY: "auto"
|
|
40
|
+
},
|
|
41
|
+
children: columns.map((column, index) => /* @__PURE__ */ jsx("div", { children: column }, index))
|
|
42
|
+
})
|
|
43
|
+
]
|
|
44
|
+
})] }),
|
|
45
|
+
PreviewToken: ({ children }) => /* @__PURE__ */ jsx("code", { children }),
|
|
46
|
+
CopyButton: ({ label, onPress, disabled }) => /* @__PURE__ */ jsx("button", {
|
|
47
|
+
type: "button",
|
|
48
|
+
disabled,
|
|
49
|
+
onClick: () => onPress(),
|
|
50
|
+
children: label
|
|
51
|
+
}),
|
|
52
|
+
Metadata: ({ label, children }) => /* @__PURE__ */ jsx("aside", {
|
|
53
|
+
"aria-label": label,
|
|
54
|
+
children
|
|
55
|
+
}),
|
|
56
|
+
Button: ({ children, onPress, disabled }) => /* @__PURE__ */ jsx("button", {
|
|
57
|
+
type: "button",
|
|
58
|
+
disabled,
|
|
59
|
+
onClick: () => onPress(),
|
|
60
|
+
children
|
|
61
|
+
}),
|
|
62
|
+
TextInput: ({ onValueChange, ...props }) => /* @__PURE__ */ jsx("input", {
|
|
63
|
+
...props,
|
|
64
|
+
onChange: (event) => onValueChange(event.target.value)
|
|
65
|
+
}),
|
|
66
|
+
TextArea: ({ onValueChange, rows = 6, ...props }) => /* @__PURE__ */ jsx("textarea", {
|
|
67
|
+
...props,
|
|
68
|
+
rows,
|
|
69
|
+
onChange: (event) => onValueChange(event.target.value)
|
|
70
|
+
}),
|
|
71
|
+
MessageRow: ({ children, selected, onSelect }) => /* @__PURE__ */ jsx("button", {
|
|
72
|
+
type: "button",
|
|
73
|
+
"aria-current": selected ? "true" : void 0,
|
|
74
|
+
onClick: () => onSelect(),
|
|
75
|
+
children
|
|
76
|
+
}),
|
|
77
|
+
Panel: ({ children, label }) => /* @__PURE__ */ jsx("section", {
|
|
78
|
+
"aria-label": label,
|
|
79
|
+
children
|
|
80
|
+
}),
|
|
81
|
+
Layout: ({ toolbar, navigation, content, sidebar }) => /* @__PURE__ */ jsxs("div", { children: [toolbar, /* @__PURE__ */ jsxs("div", { children: [
|
|
82
|
+
navigation,
|
|
83
|
+
content,
|
|
84
|
+
sidebar
|
|
85
|
+
] })] })
|
|
86
|
+
};
|
|
87
|
+
const EditorDesignSystemContext = createContext(nativeEditorComponents);
|
|
88
|
+
/** Configure a tree once; sibling providers remain independent. */
|
|
89
|
+
function EditorDesignSystemProvider({ components, children }) {
|
|
90
|
+
const parent = useEditorDesignSystem();
|
|
91
|
+
const value = useMemo(() => ({
|
|
92
|
+
...parent,
|
|
93
|
+
...Object.fromEntries(Object.entries(components).filter(([, value]) => value !== void 0))
|
|
94
|
+
}), [parent, components]);
|
|
95
|
+
return /* @__PURE__ */ jsx(EditorDesignSystemContext, {
|
|
96
|
+
value,
|
|
97
|
+
children
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Read the resolved controls, including native defaults outside a provider. */
|
|
101
|
+
function useEditorDesignSystem() {
|
|
102
|
+
return useContext(EditorDesignSystemContext);
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region packages/editor/locale-picker.tsx
|
|
106
|
+
const defaults$2 = {
|
|
107
|
+
title: "Locales",
|
|
108
|
+
empty: "No locales selected",
|
|
109
|
+
clear: "Clear",
|
|
110
|
+
selectAll: (count) => `Select all ${count} ${count === 1 ? "locale" : "locales"}`,
|
|
111
|
+
selected: (count) => `${count} locales selected`,
|
|
112
|
+
trigger: (summary) => `Locales: ${summary}`
|
|
113
|
+
};
|
|
114
|
+
const localeCode = (locale) => locale;
|
|
115
|
+
function LocalePicker({ locales, selectedLocales, onChange, getLocaleLabel = localeCode, labels }) {
|
|
116
|
+
const { Checkbox, Button, LocalePickerLayout } = useEditorDesignSystem();
|
|
117
|
+
const id = useId();
|
|
118
|
+
const [open, setOpen] = useState(false);
|
|
119
|
+
const available = [...new Set(locales)];
|
|
120
|
+
const selected = new Set(selectedLocales);
|
|
121
|
+
const active = available.filter((locale) => selected.has(locale));
|
|
122
|
+
const text = {
|
|
123
|
+
...defaults$2,
|
|
124
|
+
...labels
|
|
125
|
+
};
|
|
126
|
+
const summary = active.length === 0 ? text.empty : active.length === 1 ? getLocaleLabel(active[0]) : text.selected(active.length);
|
|
127
|
+
const all = available.length > 0 && active.length === available.length;
|
|
128
|
+
const midpoint = Math.ceil(available.length / 2);
|
|
129
|
+
const columns = [available.slice(0, midpoint), available.slice(midpoint)];
|
|
130
|
+
return /* @__PURE__ */ jsx(LocalePickerLayout, {
|
|
131
|
+
id,
|
|
132
|
+
title: text.title,
|
|
133
|
+
summary,
|
|
134
|
+
triggerLabel: text.trigger(summary),
|
|
135
|
+
open,
|
|
136
|
+
onOpenChange: setOpen,
|
|
137
|
+
controls: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("label", {
|
|
138
|
+
htmlFor: `${id}-all`,
|
|
139
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
140
|
+
id: `${id}-all`,
|
|
141
|
+
disabled: available.length === 0,
|
|
142
|
+
checked: all ? true : active.length ? "indeterminate" : false,
|
|
143
|
+
onCheckedChange: (checked) => onChange(checked ? available : [])
|
|
144
|
+
}), text.selectAll(available.length)]
|
|
145
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
146
|
+
variant: "secondary",
|
|
147
|
+
disabled: active.length === 0,
|
|
148
|
+
onPress: () => onChange([]),
|
|
149
|
+
children: text.clear
|
|
150
|
+
})] }),
|
|
151
|
+
columns: columns.map((column, columnIndex) => column.map((locale, index) => {
|
|
152
|
+
const checkboxId = `${id}-${columnIndex}-${index}`;
|
|
153
|
+
return /* @__PURE__ */ jsxs("label", {
|
|
154
|
+
htmlFor: checkboxId,
|
|
155
|
+
style: {
|
|
156
|
+
display: "flex",
|
|
157
|
+
alignItems: "center",
|
|
158
|
+
gap: 8
|
|
159
|
+
},
|
|
160
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
161
|
+
id: checkboxId,
|
|
162
|
+
checked: selected.has(locale),
|
|
163
|
+
onCheckedChange: (checked) => onChange(available.filter((candidate) => candidate === locale ? checked : selected.has(candidate)))
|
|
164
|
+
}), getLocaleLabel(locale)]
|
|
165
|
+
}, locale);
|
|
166
|
+
}))
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region packages/editor/message-preview.tsx
|
|
171
|
+
/** Structural ICU preview: shows every branch, without evaluating values or HTML. */
|
|
172
|
+
function MessagePreview({ message, formatError }) {
|
|
173
|
+
const { PreviewToken } = useEditorDesignSystem();
|
|
174
|
+
const parsed = useMemo(() => {
|
|
175
|
+
try {
|
|
176
|
+
return {
|
|
177
|
+
ast: parse(message, { captureLocation: true }),
|
|
178
|
+
error: null
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return {
|
|
182
|
+
ast: null,
|
|
183
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}, [message]);
|
|
187
|
+
if (parsed.error) return /* @__PURE__ */ jsx("p", {
|
|
188
|
+
role: "alert",
|
|
189
|
+
children: formatError ? formatError(parsed.error) : parsed.error.message
|
|
190
|
+
});
|
|
191
|
+
const token = (text, kind, key) => /* @__PURE__ */ jsx(PreviewToken, {
|
|
192
|
+
kind,
|
|
193
|
+
children: /* @__PURE__ */ jsx("bdi", {
|
|
194
|
+
dir: "ltr",
|
|
195
|
+
children: text
|
|
196
|
+
})
|
|
197
|
+
}, key);
|
|
198
|
+
const elements = (ast, prefix) => ast.flatMap((element, index) => {
|
|
199
|
+
const key = `${prefix}-${index}`;
|
|
200
|
+
switch (element.type) {
|
|
201
|
+
case TYPE.literal: return /* @__PURE__ */ jsx("span", { children: element.value }, key);
|
|
202
|
+
case TYPE.pound: return token("#", "pound", key);
|
|
203
|
+
case TYPE.argument:
|
|
204
|
+
case TYPE.number:
|
|
205
|
+
case TYPE.date:
|
|
206
|
+
case TYPE.time: return token(message.slice(element.location.start.offset, element.location.end.offset), TYPE[element.type], key);
|
|
207
|
+
case TYPE.tag: return [
|
|
208
|
+
token(`<${element.value}>`, "tag", `${key}-open`),
|
|
209
|
+
...elements(element.children, `${key}-children`),
|
|
210
|
+
token(`</${element.value}>`, "tag", `${key}-close`)
|
|
211
|
+
];
|
|
212
|
+
case TYPE.select:
|
|
213
|
+
case TYPE.plural: {
|
|
214
|
+
const kind = element.type === TYPE.select ? "select" : element.pluralType === "ordinal" ? "selectordinal" : "plural";
|
|
215
|
+
const offset = element.type === TYPE.plural && element.offset ? ` offset:${element.offset}` : "";
|
|
216
|
+
return [
|
|
217
|
+
token(`{${element.value}, ${kind},${offset}`, element.type === TYPE.select ? "selector" : "plural", `${key}-open`),
|
|
218
|
+
...Object.entries(element.options).flatMap(([selector, option]) => [
|
|
219
|
+
token(`${selector} {`, "selector", `${key}-${selector}`),
|
|
220
|
+
...elements(option.value, `${key}-${selector}`),
|
|
221
|
+
token("}", "syntax", `${key}-${selector}-close`)
|
|
222
|
+
]),
|
|
223
|
+
token("}", "syntax", `${key}-close`)
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return /* @__PURE__ */ jsx("div", {
|
|
229
|
+
dir: "auto",
|
|
230
|
+
style: {
|
|
231
|
+
whiteSpace: "pre-wrap",
|
|
232
|
+
overflowWrap: "anywhere"
|
|
233
|
+
},
|
|
234
|
+
children: elements(parsed.ast, "message")
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region packages/editor/copy-text-button.tsx
|
|
239
|
+
const defaults$1 = {
|
|
240
|
+
copy: (label) => `Copy ${label}`,
|
|
241
|
+
copying: (label) => `Copying ${label}…`,
|
|
242
|
+
copied: (label) => `Copied ${label}`,
|
|
243
|
+
failed: (label) => `Could not copy ${label}`
|
|
244
|
+
};
|
|
245
|
+
async function writeClipboard(value) {
|
|
246
|
+
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) throw new Error("Clipboard access is unavailable");
|
|
247
|
+
await navigator.clipboard.writeText(value);
|
|
248
|
+
}
|
|
249
|
+
/** Copies exact text; feedback belongs to the current value and mounted control. */
|
|
250
|
+
function CopyTextButton({ value, label, disabled = false, writeText = writeClipboard, onCopy, onError, feedbackDurationMs = 1500, labels }) {
|
|
251
|
+
const { CopyButton } = useEditorDesignSystem();
|
|
252
|
+
const [status, setStatus] = useState("idle");
|
|
253
|
+
const operation = useRef({
|
|
254
|
+
generation: 0,
|
|
255
|
+
pending: false,
|
|
256
|
+
timer: void 0
|
|
257
|
+
});
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
const current = operation.current;
|
|
260
|
+
current.generation++;
|
|
261
|
+
current.pending = false;
|
|
262
|
+
setStatus("idle");
|
|
263
|
+
clearTimeout(current.timer);
|
|
264
|
+
return () => {
|
|
265
|
+
current.generation++;
|
|
266
|
+
clearTimeout(current.timer);
|
|
267
|
+
};
|
|
268
|
+
}, [value, writeText]);
|
|
269
|
+
const copy = async () => {
|
|
270
|
+
const current = operation.current;
|
|
271
|
+
if (disabled || current.pending) return;
|
|
272
|
+
current.pending = true;
|
|
273
|
+
const request = ++current.generation;
|
|
274
|
+
clearTimeout(current.timer);
|
|
275
|
+
setStatus("copying");
|
|
276
|
+
let error;
|
|
277
|
+
try {
|
|
278
|
+
await writeText(value);
|
|
279
|
+
} catch (failure) {
|
|
280
|
+
error = failure instanceof Error ? failure : new Error(String(failure));
|
|
281
|
+
}
|
|
282
|
+
if (request !== current.generation) return;
|
|
283
|
+
current.pending = false;
|
|
284
|
+
setStatus(error ? "error" : "copied");
|
|
285
|
+
current.timer = setTimeout(() => {
|
|
286
|
+
if (request === current.generation) setStatus("idle");
|
|
287
|
+
}, Math.max(0, feedbackDurationMs));
|
|
288
|
+
if (error) onError?.(error, value);
|
|
289
|
+
else onCopy?.(value);
|
|
290
|
+
};
|
|
291
|
+
const text = {
|
|
292
|
+
...defaults$1,
|
|
293
|
+
...labels
|
|
294
|
+
};
|
|
295
|
+
const buttonLabel = (status === "copying" ? text.copying : status === "copied" ? text.copied : status === "error" ? text.failed : text.copy)(label);
|
|
296
|
+
return /* @__PURE__ */ jsxs("span", { children: [
|
|
297
|
+
/* @__PURE__ */ jsx(CopyButton, {
|
|
298
|
+
label: buttonLabel,
|
|
299
|
+
status,
|
|
300
|
+
disabled: disabled || status === "copying",
|
|
301
|
+
onPress: () => {
|
|
302
|
+
copy();
|
|
303
|
+
}
|
|
304
|
+
}),
|
|
305
|
+
status === "copied" && /* @__PURE__ */ jsx("output", { children: text.copied(label) }),
|
|
306
|
+
status === "error" && /* @__PURE__ */ jsx("span", {
|
|
307
|
+
role: "alert",
|
|
308
|
+
children: text.failed(label)
|
|
309
|
+
})
|
|
310
|
+
] });
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region packages/editor/message-context.tsx
|
|
314
|
+
const defaults = {
|
|
315
|
+
title: "Message context",
|
|
316
|
+
id: "Message ID",
|
|
317
|
+
description: "Description",
|
|
318
|
+
catalogs: "Source catalogs",
|
|
319
|
+
locations: "Source locations"
|
|
320
|
+
};
|
|
321
|
+
function locationText(location) {
|
|
322
|
+
const start = location.start === void 0 ? "" : `:${location.start}`;
|
|
323
|
+
const end = location.end === void 0 ? "" : `${location.start === void 0 ? ":" : "–"}${location.end}`;
|
|
324
|
+
return `${location.file}${start}${end}`;
|
|
325
|
+
}
|
|
326
|
+
function MessageContext({ message, labels, copyId = true, copyOptions, renderLocation = locationText }) {
|
|
327
|
+
const { Metadata } = useEditorDesignSystem();
|
|
328
|
+
const text = {
|
|
329
|
+
...defaults,
|
|
330
|
+
...labels
|
|
331
|
+
};
|
|
332
|
+
return /* @__PURE__ */ jsx(Metadata, {
|
|
333
|
+
label: text.title,
|
|
334
|
+
children: message && /* @__PURE__ */ jsxs("dl", { children: [
|
|
335
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: text.id }), /* @__PURE__ */ jsxs("dd", { children: [/* @__PURE__ */ jsx("code", { children: message.id }), copyId && /* @__PURE__ */ jsx(CopyTextButton, {
|
|
336
|
+
...copyOptions,
|
|
337
|
+
value: message.id,
|
|
338
|
+
label: text.id
|
|
339
|
+
})] })] }),
|
|
340
|
+
message.description && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: text.description }), /* @__PURE__ */ jsx("dd", { children: message.description })] }),
|
|
341
|
+
!!message.catalogs?.length && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: text.catalogs }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("ul", { children: message.catalogs.map((catalog, index) => /* @__PURE__ */ jsx("li", { children: catalog }, index)) }) })] }),
|
|
342
|
+
!!message.locations?.length && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: text.locations }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("ul", { children: message.locations.map((location, index) => /* @__PURE__ */ jsx("li", { children: renderLocation(location) }, index)) }) })] })
|
|
343
|
+
] })
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region packages/editor/ui.tsx
|
|
348
|
+
const defaultLabels = {
|
|
349
|
+
search: "Search messages",
|
|
350
|
+
messages: "Messages",
|
|
351
|
+
source: "Source message",
|
|
352
|
+
noMessages: "No matching messages",
|
|
353
|
+
noSelection: "Select a message",
|
|
354
|
+
loading: "Loading messages…",
|
|
355
|
+
copySource: "Copy source",
|
|
356
|
+
reset: "Reset",
|
|
357
|
+
save: "Save translation",
|
|
358
|
+
saving: "Saving…",
|
|
359
|
+
saved: "Translation saved.",
|
|
360
|
+
unsaved: "Unsaved changes",
|
|
361
|
+
unchanged: "No unsaved changes",
|
|
362
|
+
validation: {
|
|
363
|
+
empty: "Enter a translation before saving.",
|
|
364
|
+
"invalid-source": "The source contains invalid ICU syntax.",
|
|
365
|
+
"invalid-translation": "The translation contains invalid ICU syntax.",
|
|
366
|
+
structure: "Preserve ICU arguments, tags, formatting styles, and selector branches."
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
function resolveLabels(labels) {
|
|
370
|
+
return {
|
|
371
|
+
...defaultLabels,
|
|
372
|
+
...labels,
|
|
373
|
+
validation: {
|
|
374
|
+
...defaultLabels.validation,
|
|
375
|
+
...labels?.validation
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */
|
|
380
|
+
function MessageList({ messages, selectedId, onSelect, search, loading = false, pagination, listSummary, renderMessage, renderMessageActions, labels }) {
|
|
381
|
+
const { TextInput, MessageRow } = useEditorDesignSystem();
|
|
382
|
+
const text = resolveLabels(labels);
|
|
383
|
+
const searchId = useId();
|
|
384
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
385
|
+
"aria-label": text.messages,
|
|
386
|
+
"aria-busy": loading,
|
|
387
|
+
children: [
|
|
388
|
+
search && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
389
|
+
htmlFor: searchId,
|
|
390
|
+
children: text.search
|
|
391
|
+
}), /* @__PURE__ */ jsx(TextInput, {
|
|
392
|
+
id: searchId,
|
|
393
|
+
type: "search",
|
|
394
|
+
value: search.value,
|
|
395
|
+
onValueChange: search.onValueChange
|
|
396
|
+
})] }),
|
|
397
|
+
listSummary,
|
|
398
|
+
loading && /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.loading }) }),
|
|
399
|
+
/* @__PURE__ */ jsx("ul", { children: messages.map((message) => {
|
|
400
|
+
const state = { selected: message.id === selectedId };
|
|
401
|
+
return /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx(MessageRow, {
|
|
402
|
+
selected: state.selected,
|
|
403
|
+
onSelect: () => onSelect(message.id),
|
|
404
|
+
children: renderMessage ? renderMessage(message, state) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
405
|
+
/* @__PURE__ */ jsx("span", { children: message.defaultMessage }),
|
|
406
|
+
" ",
|
|
407
|
+
/* @__PURE__ */ jsx("code", { children: message.id })
|
|
408
|
+
] })
|
|
409
|
+
}), renderMessageActions?.(message, state)] }, message.id);
|
|
410
|
+
}) }),
|
|
411
|
+
!loading && messages.length === 0 && /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.noMessages }) }),
|
|
412
|
+
pagination
|
|
413
|
+
]
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function SourceMessage({ message, preview, context, labels }) {
|
|
417
|
+
const { Panel } = useEditorDesignSystem();
|
|
418
|
+
const text = resolveLabels(labels);
|
|
419
|
+
return /* @__PURE__ */ jsxs(Panel, {
|
|
420
|
+
kind: "source",
|
|
421
|
+
label: text.source,
|
|
422
|
+
children: [
|
|
423
|
+
/* @__PURE__ */ jsx("h2", { children: text.source }),
|
|
424
|
+
/* @__PURE__ */ jsx("code", { children: message.id }),
|
|
425
|
+
preview ?? /* @__PURE__ */ jsx("pre", { children: message.defaultMessage }),
|
|
426
|
+
message.description && /* @__PURE__ */ jsx("p", { children: message.description }),
|
|
427
|
+
context
|
|
428
|
+
]
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function TranslationField({ locale, label = locale, source, draft, onSave, actions, preview, labels }) {
|
|
432
|
+
const { Panel, TextArea, Button } = useEditorDesignSystem();
|
|
433
|
+
const text = resolveLabels(labels);
|
|
434
|
+
const id = useId();
|
|
435
|
+
const errorId = `${id}-error`;
|
|
436
|
+
const statusId = `${id}-status`;
|
|
437
|
+
const validation = draft.validationError ? text.validation[draft.validationError] : null;
|
|
438
|
+
const error = validation ?? draft.saveError?.message;
|
|
439
|
+
return /* @__PURE__ */ jsxs(Panel, {
|
|
440
|
+
kind: "translation",
|
|
441
|
+
label,
|
|
442
|
+
children: [
|
|
443
|
+
/* @__PURE__ */ jsx("label", {
|
|
444
|
+
htmlFor: id,
|
|
445
|
+
children: label
|
|
446
|
+
}),
|
|
447
|
+
/* @__PURE__ */ jsx(TextArea, {
|
|
448
|
+
id,
|
|
449
|
+
value: draft.value,
|
|
450
|
+
onValueChange: draft.setTranslation,
|
|
451
|
+
"aria-invalid": !!validation,
|
|
452
|
+
"aria-describedby": error ? `${errorId} ${statusId}` : statusId
|
|
453
|
+
}),
|
|
454
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
455
|
+
id: errorId,
|
|
456
|
+
role: "alert",
|
|
457
|
+
children: error
|
|
458
|
+
}),
|
|
459
|
+
/* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", {
|
|
460
|
+
id: statusId,
|
|
461
|
+
children: draft.isSaving ? text.saving : draft.saved ? text.saved : draft.changed ? text.unsaved : text.unchanged
|
|
462
|
+
}) }),
|
|
463
|
+
preview,
|
|
464
|
+
actions !== void 0 ? actions : /* @__PURE__ */ jsxs("div", { children: [
|
|
465
|
+
/* @__PURE__ */ jsx(Button, {
|
|
466
|
+
variant: "secondary",
|
|
467
|
+
disabled: draft.isSaving,
|
|
468
|
+
onPress: () => draft.setTranslation(source),
|
|
469
|
+
children: text.copySource
|
|
470
|
+
}),
|
|
471
|
+
/* @__PURE__ */ jsx(Button, {
|
|
472
|
+
variant: "secondary",
|
|
473
|
+
disabled: !draft.changed || draft.isSaving,
|
|
474
|
+
onPress: draft.reset,
|
|
475
|
+
children: text.reset
|
|
476
|
+
}),
|
|
477
|
+
onSave && /* @__PURE__ */ jsx(Button, {
|
|
478
|
+
variant: "primary",
|
|
479
|
+
disabled: !draft.changed || !!draft.validationError || draft.isSaving,
|
|
480
|
+
onPress: onSave,
|
|
481
|
+
children: draft.isSaving ? text.saving : text.save
|
|
482
|
+
})
|
|
483
|
+
] })
|
|
484
|
+
]
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/** A stateless composition over caller-owned navigation and locale drafts. */
|
|
488
|
+
function TranslationEditorView({ selectedMessage, selectedId = selectedMessage?.id, translations, filters, context, sourcePreview, notice, sidebar, emptyState, renderTranslations, renderContent, labels, ...list }) {
|
|
489
|
+
const { Layout } = useEditorDesignSystem();
|
|
490
|
+
const text = resolveLabels(labels);
|
|
491
|
+
const fields = selectedMessage ? translations.map((translation) => /* @__PURE__ */ createElement(TranslationField, {
|
|
492
|
+
...translation,
|
|
493
|
+
key: `${selectedMessage.id}:${translation.locale}`,
|
|
494
|
+
source: selectedMessage.defaultMessage,
|
|
495
|
+
labels: {
|
|
496
|
+
...labels,
|
|
497
|
+
...translation.labels,
|
|
498
|
+
validation: {
|
|
499
|
+
...labels?.validation,
|
|
500
|
+
...translation.labels?.validation
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
})) : null;
|
|
504
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [notice, selectedMessage ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SourceMessage, {
|
|
505
|
+
message: selectedMessage,
|
|
506
|
+
preview: sourcePreview,
|
|
507
|
+
context,
|
|
508
|
+
labels
|
|
509
|
+
}), renderTranslations ? renderTranslations(fields) : fields] }) : emptyState !== void 0 ? emptyState : /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.noSelection }) })] });
|
|
510
|
+
return /* @__PURE__ */ jsx(Layout, {
|
|
511
|
+
toolbar: filters,
|
|
512
|
+
navigation: /* @__PURE__ */ jsx(MessageList, {
|
|
513
|
+
...list,
|
|
514
|
+
selectedId,
|
|
515
|
+
labels
|
|
516
|
+
}),
|
|
517
|
+
content: renderContent ? renderContent(content) : content,
|
|
518
|
+
sidebar
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
export { CopyTextButton, EditorDesignSystemProvider, LocalePicker, MessageContext, MessageList, MessagePreview, SourceMessage, TranslationEditorView, TranslationField, nativeEditorComponents, useEditorDesignSystem };
|
|
523
|
+
|
|
524
|
+
//# sourceMappingURL=ui.js.map
|
package/ui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.js","names":["defaults","defaults"],"sources":["../design-system.tsx","../locale-picker.tsx","../message-preview.tsx","../copy-text-button.tsx","../message-context.tsx","../ui.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n type ComponentType,\n type ReactNode,\n} from 'react'\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 /** Optional secondary content, arranged by the design-system layout. */\n sidebar?: ReactNode\n}\n/** Define adapters outside render so controls retain focus across edits. */\nexport interface EditorComponents extends Partial<EditorToolComponents> {\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 EditorCheckboxProps {\n id: string\n checked: boolean | 'indeterminate'\n disabled?: boolean\n onCheckedChange: (checked: boolean) => void\n}\nexport interface EditorLocalePickerLayoutProps {\n id: string\n title: string\n summary: string\n triggerLabel: string\n open: boolean\n onOpenChange: (open: boolean) => void\n controls: ReactNode\n /** Contiguous, balanced columns in the caller's locale order. */\n columns: readonly ReactNode[]\n}\nexport interface EditorPreviewTokenProps {\n children: ReactNode\n kind:\n | 'argument'\n | 'number'\n | 'date'\n | 'time'\n | 'tag'\n | 'selector'\n | 'plural'\n | 'pound'\n | 'syntax'\n}\nexport type EditorCopyStatus = 'idle' | 'copying' | 'copied' | 'error'\nexport interface EditorCopyButtonProps {\n label: string\n status: EditorCopyStatus\n disabled: boolean\n onPress: () => void\n}\nexport interface EditorMetadataProps {\n label: string\n children: ReactNode\n}\n/** Optional additions preserve existing complete design-system registries. */\nexport interface EditorToolComponents {\n Checkbox: ComponentType<EditorCheckboxProps>\n LocalePickerLayout: ComponentType<EditorLocalePickerLayoutProps>\n PreviewToken: ComponentType<EditorPreviewTokenProps>\n CopyButton: ComponentType<EditorCopyButtonProps>\n Metadata: ComponentType<EditorMetadataProps>\n}\nexport type ResolvedEditorComponents = Required<EditorComponents>\n/** Unstyled native controls; no CSS, icons, or localization provider is required. */\nexport const nativeEditorComponents: ResolvedEditorComponents = {\n Checkbox: function NativeCheckbox({checked, onCheckedChange, ...props}) {\n const ref = useRef<HTMLInputElement>(null)\n useEffect(() => {\n if (ref.current) ref.current.indeterminate = checked === 'indeterminate'\n }, [checked])\n return (\n <input\n {...props}\n ref={ref}\n type=\"checkbox\"\n checked={checked === true}\n aria-checked={checked === 'indeterminate' ? 'mixed' : checked}\n onChange={event => onCheckedChange(event.target.checked)}\n />\n )\n },\n LocalePickerLayout: ({\n id,\n title,\n summary,\n triggerLabel,\n open,\n onOpenChange,\n controls,\n columns,\n }) => (\n <div>\n <button\n type=\"button\"\n aria-label={triggerLabel}\n aria-expanded={open}\n aria-controls={id}\n onClick={() => onOpenChange(!open)}\n >\n {summary}\n </button>\n <fieldset id={id} hidden={!open}>\n <legend>{title}</legend>\n {controls}\n <div\n style={{\n display: 'grid',\n gridTemplateColumns:\n 'repeat(auto-fit, minmax(min(100%, 16rem), 1fr))',\n maxHeight: '60vh',\n overflowY: 'auto',\n }}\n >\n {columns.map((column, index) => (\n <div key={index}>{column}</div>\n ))}\n </div>\n </fieldset>\n </div>\n ),\n PreviewToken: ({children}) => <code>{children}</code>,\n CopyButton: ({label, onPress, disabled}) => (\n <button type=\"button\" disabled={disabled} onClick={() => onPress()}>\n {label}\n </button>\n ),\n Metadata: ({label, children}) => <aside aria-label={label}>{children}</aside>,\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, sidebar}) => (\n <div>\n {toolbar}\n <div>\n {navigation}\n {content}\n {sidebar}\n </div>\n </div>\n ),\n}\nconst EditorDesignSystemContext = createContext<\n Readonly<ResolvedEditorComponents>\n>(nativeEditorComponents)\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 () => ({\n ...parent,\n ...Object.fromEntries(\n Object.entries(components).filter(([, value]) => value !== undefined)\n ),\n }),\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<ResolvedEditorComponents> {\n return useContext(EditorDesignSystemContext)\n}\n","import {useId, useState, type ReactNode} from 'react'\nimport {useEditorDesignSystem} from '#packages/editor/design-system.js'\n\nexport interface LocalePickerLabels {\n title: string\n empty: string\n clear: string\n selectAll: (count: number) => string\n selected: (count: number) => string\n trigger: (summary: string) => string\n}\nexport interface LocalePickerProps {\n locales: readonly string[]\n selectedLocales: readonly string[]\n /** Reports unique available locales in their input order; never mutates selection. */\n onChange: (locales: string[]) => void\n /** Defaults to the locale code; consumers choose the display language. */\n getLocaleLabel?: (locale: string) => string\n labels?: Partial<LocalePickerLabels>\n}\nconst defaults: LocalePickerLabels = {\n title: 'Locales',\n empty: 'No locales selected',\n clear: 'Clear',\n selectAll: count =>\n `Select all ${count} ${count === 1 ? 'locale' : 'locales'}`,\n selected: count => `${count} locales selected`,\n trigger: summary => `Locales: ${summary}`,\n}\nconst localeCode = (locale: string): string => locale\n\nexport function LocalePicker({\n locales,\n selectedLocales,\n onChange,\n getLocaleLabel = localeCode,\n labels,\n}: LocalePickerProps): ReactNode {\n const {Checkbox, Button, LocalePickerLayout} = useEditorDesignSystem()\n const id = useId()\n const [open, setOpen] = useState(false)\n const available = [...new Set(locales)]\n const selected = new Set(selectedLocales)\n const active = available.filter(locale => selected.has(locale))\n const text = {...defaults, ...labels}\n const summary =\n active.length === 0\n ? text.empty\n : active.length === 1\n ? getLocaleLabel(active[0]!)\n : text.selected(active.length)\n const all = available.length > 0 && active.length === available.length\n const midpoint = Math.ceil(available.length / 2)\n const columns = [available.slice(0, midpoint), available.slice(midpoint)]\n return (\n <LocalePickerLayout\n id={id}\n title={text.title}\n summary={summary}\n triggerLabel={text.trigger(summary)}\n open={open}\n onOpenChange={setOpen}\n controls={\n <div>\n <label htmlFor={`${id}-all`}>\n <Checkbox\n id={`${id}-all`}\n disabled={available.length === 0}\n checked={all ? true : active.length ? 'indeterminate' : false}\n onCheckedChange={checked => onChange(checked ? available : [])}\n />\n {text.selectAll(available.length)}\n </label>\n <Button\n variant=\"secondary\"\n disabled={active.length === 0}\n onPress={() => onChange([])}\n >\n {text.clear}\n </Button>\n </div>\n }\n columns={columns.map((column, columnIndex) =>\n column.map((locale, index) => {\n const checkboxId = `${id}-${columnIndex}-${index}`\n return (\n <label\n key={locale}\n htmlFor={checkboxId}\n style={{display: 'flex', alignItems: 'center', gap: 8}}\n >\n <Checkbox\n id={checkboxId}\n checked={selected.has(locale)}\n onCheckedChange={checked =>\n onChange(\n available.filter(candidate =>\n candidate === locale ? checked : selected.has(candidate)\n )\n )\n }\n />\n {getLocaleLabel(locale)}\n </label>\n )\n })\n )}\n />\n )\n}\n","import {\n parse,\n TYPE,\n type MessageFormatElement,\n} from '@formatjs/icu-messageformat-parser'\nimport {useMemo, type ReactNode} from 'react'\nimport {\n useEditorDesignSystem,\n type EditorPreviewTokenProps,\n} from '#packages/editor/design-system.js'\n\nexport interface MessagePreviewProps {\n message: string\n /** Customize a parser error without suppressing the alert semantics. */\n formatError?: (error: Error) => ReactNode\n}\n\n/** Structural ICU preview: shows every branch, without evaluating values or HTML. */\nexport function MessagePreview({\n message,\n formatError,\n}: MessagePreviewProps): ReactNode {\n const {PreviewToken} = useEditorDesignSystem()\n const parsed = useMemo(() => {\n try {\n return {ast: parse(message, {captureLocation: true}), error: null}\n } catch (error) {\n return {\n ast: null,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }, [message])\n if (parsed.error)\n return (\n <p role=\"alert\">\n {formatError ? formatError(parsed.error) : parsed.error.message}\n </p>\n )\n const token = (\n text: string,\n kind: EditorPreviewTokenProps['kind'],\n key: string\n ): ReactNode => (\n <PreviewToken key={key} kind={kind}>\n <bdi dir=\"ltr\">{text}</bdi>\n </PreviewToken>\n )\n const elements = (ast: MessageFormatElement[], prefix: string): ReactNode[] =>\n ast.flatMap((element, index) => {\n const key = `${prefix}-${index}`\n switch (element.type) {\n case TYPE.literal:\n return <span key={key}>{element.value}</span>\n case TYPE.pound:\n return token('#', 'pound', key)\n case TYPE.argument:\n case TYPE.number:\n case TYPE.date:\n case TYPE.time:\n return token(\n message.slice(\n element.location!.start.offset,\n element.location!.end.offset\n ),\n TYPE[element.type] as 'argument' | 'number' | 'date' | 'time',\n key\n )\n case TYPE.tag:\n return [\n token(`<${element.value}>`, 'tag', `${key}-open`),\n ...elements(element.children, `${key}-children`),\n token(`</${element.value}>`, 'tag', `${key}-close`),\n ]\n case TYPE.select:\n case TYPE.plural: {\n const kind =\n element.type === TYPE.select\n ? 'select'\n : element.pluralType === 'ordinal'\n ? 'selectordinal'\n : 'plural'\n const offset =\n element.type === TYPE.plural && element.offset\n ? ` offset:${element.offset}`\n : ''\n return [\n token(\n `{${element.value}, ${kind},${offset}`,\n element.type === TYPE.select ? 'selector' : 'plural',\n `${key}-open`\n ),\n ...Object.entries(element.options).flatMap(([selector, option]) => [\n token(`${selector} {`, 'selector', `${key}-${selector}`),\n ...elements(option.value, `${key}-${selector}`),\n token('}', 'syntax', `${key}-${selector}-close`),\n ]),\n token('}', 'syntax', `${key}-close`),\n ]\n }\n }\n })\n return (\n <div dir=\"auto\" style={{whiteSpace: 'pre-wrap', overflowWrap: 'anywhere'}}>\n {elements(parsed.ast!, 'message')}\n </div>\n )\n}\n","import {useEffect, useRef, useState, type ReactNode} from 'react'\nimport {\n useEditorDesignSystem,\n type EditorCopyStatus,\n} from '#packages/editor/design-system.js'\n\nexport interface CopyTextLabels {\n copy: (label: string) => string\n copying: (label: string) => string\n copied: (label: string) => string\n failed: (label: string) => string\n}\nexport interface CopyTextButtonProps {\n value: string\n label: string\n disabled?: boolean\n /** Injectable clipboard boundary; rejects when the write fails. */\n writeText?: (value: string) => Promise<void>\n onCopy?: (value: string) => void\n onError?: (error: Error, value: string) => void\n feedbackDurationMs?: number\n labels?: Partial<CopyTextLabels>\n}\nconst defaults: CopyTextLabels = {\n copy: label => `Copy ${label}`,\n copying: label => `Copying ${label}…`,\n copied: label => `Copied ${label}`,\n failed: label => `Could not copy ${label}`,\n}\nasync function writeClipboard(value: string): Promise<void> {\n if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {\n throw new Error('Clipboard access is unavailable')\n }\n await navigator.clipboard.writeText(value)\n}\n\n/** Copies exact text; feedback belongs to the current value and mounted control. */\nexport function CopyTextButton({\n value,\n label,\n disabled = false,\n writeText = writeClipboard,\n onCopy,\n onError,\n feedbackDurationMs = 1500,\n labels,\n}: CopyTextButtonProps): ReactNode {\n const {CopyButton} = useEditorDesignSystem()\n const [status, setStatus] = useState<EditorCopyStatus>('idle')\n const operation = useRef({\n generation: 0,\n pending: false,\n timer: undefined as ReturnType<typeof setTimeout> | undefined,\n })\n useEffect(() => {\n const current = operation.current\n current.generation++\n current.pending = false\n setStatus('idle')\n clearTimeout(current.timer)\n return () => {\n current.generation++\n clearTimeout(current.timer)\n }\n }, [value, writeText])\n const copy = async (): Promise<void> => {\n const current = operation.current\n if (disabled || current.pending) return\n current.pending = true\n const request = ++current.generation\n clearTimeout(current.timer)\n setStatus('copying')\n let error: Error | undefined\n try {\n await writeText(value)\n } catch (failure) {\n error = failure instanceof Error ? failure : new Error(String(failure))\n }\n if (request !== current.generation) return\n current.pending = false\n setStatus(error ? 'error' : 'copied')\n current.timer = setTimeout(\n () => {\n if (request === current.generation) setStatus('idle')\n },\n Math.max(0, feedbackDurationMs)\n )\n if (error) onError?.(error, value)\n else onCopy?.(value)\n }\n const text = {...defaults, ...labels}\n const buttonLabel = (\n status === 'copying'\n ? text.copying\n : status === 'copied'\n ? text.copied\n : status === 'error'\n ? text.failed\n : text.copy\n )(label)\n return (\n <span>\n <CopyButton\n label={buttonLabel}\n status={status}\n disabled={disabled || status === 'copying'}\n onPress={() => {\n void copy()\n }}\n />\n {status === 'copied' && <output>{text.copied(label)}</output>}\n {status === 'error' && <span role=\"alert\">{text.failed(label)}</span>}\n </span>\n )\n}\n","import type {ReactNode} from 'react'\nimport {useEditorDesignSystem} from '#packages/editor/design-system.js'\nimport {\n CopyTextButton,\n type CopyTextButtonProps,\n} from '#packages/editor/copy-text-button.js'\nimport type {EditorMessage, SourceLocation} from '#packages/editor/workflow.js'\n\nexport interface MessageContextLabels {\n title: string\n id: string\n description: string\n catalogs: string\n locations: string\n}\nexport interface MessageContextProps {\n message?: Pick<\n EditorMessage,\n 'id' | 'description' | 'catalogs' | 'locations'\n > | null\n labels?: Partial<MessageContextLabels>\n copyId?: boolean\n copyOptions?: Pick<\n CopyTextButtonProps,\n 'writeText' | 'onCopy' | 'onError' | 'labels' | 'feedbackDurationMs'\n >\n /** Default locations are plain text, never file URLs or HTML. */\n renderLocation?: (location: SourceLocation) => ReactNode\n}\nconst defaults: MessageContextLabels = {\n title: 'Message context',\n id: 'Message ID',\n description: 'Description',\n catalogs: 'Source catalogs',\n locations: 'Source locations',\n}\nfunction locationText(location: SourceLocation): string {\n const start = location.start === undefined ? '' : `:${location.start}`\n const end =\n location.end === undefined\n ? ''\n : `${location.start === undefined ? ':' : '–'}${location.end}`\n return `${location.file}${start}${end}`\n}\n\nexport function MessageContext({\n message,\n labels,\n copyId = true,\n copyOptions,\n renderLocation = locationText,\n}: MessageContextProps): ReactNode {\n const {Metadata} = useEditorDesignSystem()\n const text = {...defaults, ...labels}\n return (\n <Metadata label={text.title}>\n {message && (\n <dl>\n <div>\n <dt>{text.id}</dt>\n <dd>\n <code>{message.id}</code>\n {copyId && (\n <CopyTextButton\n {...copyOptions}\n value={message.id}\n label={text.id}\n />\n )}\n </dd>\n </div>\n {message.description && (\n <div>\n <dt>{text.description}</dt>\n <dd>{message.description}</dd>\n </div>\n )}\n {!!message.catalogs?.length && (\n <div>\n <dt>{text.catalogs}</dt>\n <dd>\n <ul>\n {message.catalogs.map((catalog, index) => (\n <li key={index}>{catalog}</li>\n ))}\n </ul>\n </dd>\n </div>\n )}\n {!!message.locations?.length && (\n <div>\n <dt>{text.locations}</dt>\n <dd>\n <ul>\n {message.locations.map((location, index) => (\n <li key={index}>{renderLocation(location)}</li>\n ))}\n </ul>\n </dd>\n </div>\n )}\n </dl>\n )}\n </Metadata>\n )\n}\n","import {useId, type ReactNode} from 'react'\nimport {useEditorDesignSystem} from '#packages/editor/design-system.js'\nexport {\n type EditorButtonProps,\n type EditorInputProps,\n type EditorTextInputProps,\n type EditorTextAreaProps,\n type EditorMessageRowProps,\n type EditorPanelProps,\n type EditorLayoutProps,\n type EditorComponents,\n type EditorCheckboxProps,\n type EditorLocalePickerLayoutProps,\n type EditorPreviewTokenProps,\n type EditorCopyStatus,\n type EditorCopyButtonProps,\n type EditorMetadataProps,\n type EditorToolComponents,\n type ResolvedEditorComponents,\n nativeEditorComponents,\n type EditorDesignSystemProviderProps,\n EditorDesignSystemProvider,\n useEditorDesignSystem,\n} from '#packages/editor/design-system.js'\nexport {\n type LocalePickerLabels,\n type LocalePickerProps,\n LocalePicker,\n} from '#packages/editor/locale-picker.js'\nexport {\n type MessagePreviewProps,\n MessagePreview,\n} from '#packages/editor/message-preview.js'\nexport {\n type CopyTextLabels,\n type CopyTextButtonProps,\n CopyTextButton,\n} from '#packages/editor/copy-text-button.js'\nexport {\n type MessageContextLabels,\n type MessageContextProps,\n MessageContext,\n} from '#packages/editor/message-context.js'\nimport type {\n EditorMessage,\n TranslationDraftState,\n} from '#packages/editor/workflow.js'\nimport type {TranslationValidationError} from '#packages/editor/validation.js'\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}\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 EditorMessageRenderState {\n selected: boolean\n}\nexport interface MessageListProps<\n Message extends EditorViewMessage = EditorViewMessage,\n> extends ViewOptions {\n messages: readonly Message[]\n selectedId?: string\n onSelect: (id: string) => void\n search?: EditorSearch\n loading?: boolean\n pagination?: ReactNode\n /** Summary or controls between search and the loaded rows. */\n listSummary?: ReactNode\n /** Noninteractive content inside the design-system selection control. */\n renderMessage?: (\n message: Message,\n state: EditorMessageRenderState\n ) => ReactNode\n /** Interactive actions rendered beside, never inside, the selection control. */\n renderMessageActions?: (\n message: Message,\n state: EditorMessageRenderState\n ) => ReactNode\n}\n/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */\nexport function MessageList<\n Message extends EditorViewMessage = EditorViewMessage,\n>({\n messages,\n selectedId,\n onSelect,\n search,\n loading = false,\n pagination,\n listSummary,\n renderMessage,\n renderMessageActions,\n labels,\n}: MessageListProps<Message>): 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 {listSummary}\n {loading && (\n <p>\n <output>{text.loading}</output>\n </p>\n )}\n <ul>\n {messages.map(message => {\n const state = {selected: message.id === selectedId}\n return (\n <li key={message.id}>\n <MessageRow\n selected={state.selected}\n onSelect={() => onSelect(message.id)}\n >\n {renderMessage ? (\n renderMessage(message, state)\n ) : (\n <>\n <span>{message.defaultMessage}</span>{' '}\n <code>{message.id}</code>\n </>\n )}\n </MessageRow>\n {renderMessageActions?.(message, state)}\n </li>\n )\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'>\nexport interface TranslationEditorViewProps<\n Message extends EditorViewMessage = EditorViewMessage,\n> extends MessageListProps<Message> {\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 sidebar?: ReactNode\n /** Replaces the no-selection status; null suppresses it. */\n emptyState?: ReactNode\n /** Wrap the generated locale fields without recreating their wiring. */\n renderTranslations?: (fields: ReactNode) => ReactNode\n /** Wrap the entire detail region, including notices and the empty state. */\n renderContent?: (content: ReactNode) => ReactNode\n}\n/** A stateless composition over caller-owned navigation and locale drafts. */\nexport function TranslationEditorView<\n Message extends EditorViewMessage = EditorViewMessage,\n>({\n selectedMessage,\n selectedId = selectedMessage?.id,\n translations,\n filters,\n context,\n sourcePreview,\n notice,\n sidebar,\n emptyState,\n renderTranslations,\n renderContent,\n labels,\n ...list\n}: TranslationEditorViewProps<Message>): ReactNode {\n const {Layout} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n const fields = selectedMessage\n ? translations.map(translation => (\n <TranslationField\n {...translation}\n key={`${selectedMessage.id}:${translation.locale}`}\n source={selectedMessage.defaultMessage}\n labels={{\n ...labels,\n ...translation.labels,\n validation: {\n ...labels?.validation,\n ...translation.labels?.validation,\n },\n }}\n />\n ))\n : null\n const content = (\n <>\n {notice}\n {selectedMessage ? (\n <>\n <SourceMessage\n message={selectedMessage}\n preview={sourcePreview}\n context={context}\n labels={labels}\n />\n {renderTranslations ? renderTranslations(fields) : fields}\n </>\n ) : emptyState !== undefined ? (\n emptyState\n ) : (\n <p>\n <output>{text.noSelection}</output>\n </p>\n )}\n </>\n )\n return (\n <Layout\n toolbar={filters}\n navigation={\n <MessageList {...list} selectedId={selectedId} labels={labels} />\n }\n content={renderContent ? renderContent(content) : content}\n sidebar={sidebar}\n />\n )\n}\n"],"mappings":"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"}
|