@formatjs/editor 1.4.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 +160 -5
- package/package.json +1 -1
- package/ui.d.ts +193 -63
- package/ui.js +371 -59
- package/ui.js.map +1 -1
package/README.md
CHANGED
|
@@ -194,8 +194,7 @@ It is separate from the headless entry point; consumers can use any design syste
|
|
|
194
194
|
Import `TranslationEditorView`, `MessageList`, `SourceMessage`, and
|
|
195
195
|
`TranslationField` from `@formatjs/editor/ui`. This separate entry point owns
|
|
196
196
|
message-row selection wiring, field labels and error descriptions, draft status,
|
|
197
|
-
and copy/reset/save controls. It ships
|
|
198
|
-
React. The headless root does not import the UI, StyleX, icons, or React Intl.
|
|
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.
|
|
199
198
|
|
|
200
199
|
Mount one workflow above the views and pass its existing drafts:
|
|
201
200
|
|
|
@@ -243,7 +242,8 @@ navigation busy and displays a status; it does not clear the controlled list.
|
|
|
243
242
|
### Component adapters
|
|
244
243
|
|
|
245
244
|
`EditorDesignSystemProvider` accepts partial overrides of `EditorComponents`: `Button`,
|
|
246
|
-
`TextInput`, `TextArea`, `MessageRow`, `Panel`, and `Layout
|
|
245
|
+
`TextInput`, `TextArea`, `MessageRow`, `Panel`, and `Layout`, plus the optional tool
|
|
246
|
+
adapters described below. Unspecified or explicitly `undefined` entries
|
|
247
247
|
inherit from the nearest provider, falling back to `nativeEditorComponents`. Adapters map a design system's control API to
|
|
248
248
|
semantic `onPress`, `onSelect`, and `onValueChange` callbacks. Define adapters at
|
|
249
249
|
module scope so React preserves focus and control state between edits:
|
|
@@ -290,7 +290,7 @@ function CustomToolbar() {
|
|
|
290
290
|
|
|
291
291
|
All built-in views and downstream consumers use `useEditorDesignSystem()`;
|
|
292
292
|
there is no component-registry prop on individual views. The hook returns the
|
|
293
|
-
resolved, read-only `
|
|
293
|
+
resolved, read-only `ResolvedEditorComponents` contract, including native defaults for every optional tool adapter. Providers are React-tree scoped,
|
|
294
294
|
so sibling editors (and separate server-rendered trees) do not share mutable
|
|
295
295
|
configuration. Nested providers override only specified components and inherit
|
|
296
296
|
the rest. Changing the registry updates consumers; keeping each component type
|
|
@@ -306,7 +306,7 @@ Each component has an exported props contract:
|
|
|
306
306
|
| `TextArea` / `EditorTextAreaProps` | `id`, `value`, optional `rows` (default six), disabled/error-description attributes | `onValueChange(value: string): void` |
|
|
307
307
|
| `MessageRow` / `EditorMessageRowProps` | `children`, controlled `selected` | `onSelect(): void` |
|
|
308
308
|
| `Panel` / `EditorPanelProps` | `children`, accessible `label`, `kind` (`source` or `translation`) | None; layout only |
|
|
309
|
-
| `Layout` / `EditorLayoutProps` | `toolbar`, `navigation`, `content` nodes
|
|
309
|
+
| `Layout` / `EditorLayoutProps` | `toolbar`, `navigation`, `content`, optional `sidebar` nodes | None; layout only |
|
|
310
310
|
|
|
311
311
|
`EditorInputProps` defines the shared input attributes explicitly: `id`, `value`,
|
|
312
312
|
`onValueChange`, optional `disabled`, `aria-invalid`, and `aria-describedby`.
|
|
@@ -334,6 +334,161 @@ The reusable pieces retain semantic labels, headings, lists, alerts, and status
|
|
|
334
334
|
nodes. Adapters control the interactive controls and outer presentation; use the
|
|
335
335
|
standalone pieces when a different page composition is needed.
|
|
336
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
|
+
|
|
337
492
|
### Application slots and localization
|
|
338
493
|
|
|
339
494
|
`filters`, `pagination`, `context`, and `notice` accept React nodes. Supply your
|
package/package.json
CHANGED
package/ui.d.ts
CHANGED
|
@@ -1,5 +1,159 @@
|
|
|
1
1
|
import { ComponentType, ReactNode } from "react";
|
|
2
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
|
|
3
157
|
//#region packages/editor/validation.d.ts
|
|
4
158
|
type TranslationValidationError = "empty" | "invalid-source" | "invalid-translation" | "structure";
|
|
5
159
|
//#endregion
|
|
@@ -44,55 +198,25 @@ interface TranslationDraftState<TContext = void, TResult = void> {
|
|
|
44
198
|
save: (context?: TContext) => Promise<TranslationSaveResult<TResult>>;
|
|
45
199
|
}
|
|
46
200
|
//#endregion
|
|
47
|
-
//#region packages/editor/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/** Called once per activation, without a DOM event; disabled controls must not call it. */
|
|
51
|
-
onPress: () => void;
|
|
52
|
-
disabled?: boolean;
|
|
53
|
-
variant: "primary" | "secondary";
|
|
54
|
-
}
|
|
55
|
-
export interface EditorInputProps {
|
|
201
|
+
//#region packages/editor/message-context.d.ts
|
|
202
|
+
interface MessageContextLabels {
|
|
203
|
+
title: string;
|
|
56
204
|
id: string;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
disabled?: boolean;
|
|
61
|
-
"aria-invalid"?: boolean;
|
|
62
|
-
"aria-describedby"?: string;
|
|
63
|
-
}
|
|
64
|
-
export interface EditorTextInputProps extends EditorInputProps {
|
|
65
|
-
type: "text" | "search";
|
|
66
|
-
}
|
|
67
|
-
export interface EditorTextAreaProps extends EditorInputProps {
|
|
68
|
-
/** Visible rows when supported by the control; defaults to six. */
|
|
69
|
-
rows?: number;
|
|
70
|
-
}
|
|
71
|
-
export interface EditorMessageRowProps {
|
|
72
|
-
children: ReactNode;
|
|
73
|
-
selected: boolean;
|
|
74
|
-
/** Activates this row without changing controlled selection itself. */
|
|
75
|
-
onSelect: () => void;
|
|
76
|
-
}
|
|
77
|
-
export interface EditorPanelProps {
|
|
78
|
-
children: ReactNode;
|
|
79
|
-
label: string;
|
|
80
|
-
kind: "source" | "translation";
|
|
205
|
+
description: string;
|
|
206
|
+
catalogs: string;
|
|
207
|
+
locations: string;
|
|
81
208
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
Button: ComponentType<EditorButtonProps>;
|
|
90
|
-
TextInput: ComponentType<EditorTextInputProps>;
|
|
91
|
-
TextArea: ComponentType<EditorTextAreaProps>;
|
|
92
|
-
MessageRow: ComponentType<EditorMessageRowProps>;
|
|
93
|
-
Panel: ComponentType<EditorPanelProps>;
|
|
94
|
-
Layout: ComponentType<EditorLayoutProps>;
|
|
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;
|
|
95
216
|
}
|
|
217
|
+
export declare function MessageContext({ message, labels, copyId, copyOptions, renderLocation }: MessageContextProps): ReactNode;
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region packages/editor/ui.d.ts
|
|
96
220
|
export interface EditorLabels {
|
|
97
221
|
search: string;
|
|
98
222
|
messages: string;
|
|
@@ -112,17 +236,6 @@ export interface EditorLabels {
|
|
|
112
236
|
export type EditorLabelOverrides = Partial<Omit<EditorLabels, "validation">> & {
|
|
113
237
|
validation?: Partial<EditorLabels["validation"]>;
|
|
114
238
|
};
|
|
115
|
-
/** Unstyled native controls; no CSS, icons, or localization provider is required. */
|
|
116
|
-
export declare const nativeEditorComponents: EditorComponents;
|
|
117
|
-
export interface EditorDesignSystemProviderProps {
|
|
118
|
-
/** Overrides inherit unspecified components from the nearest provider. */
|
|
119
|
-
components: Partial<EditorComponents>;
|
|
120
|
-
children: ReactNode;
|
|
121
|
-
}
|
|
122
|
-
/** Configure a tree once; sibling providers remain independent. */
|
|
123
|
-
export declare function EditorDesignSystemProvider({ components, children }: EditorDesignSystemProviderProps): ReactNode;
|
|
124
|
-
/** Read the resolved controls, including native defaults outside a provider. */
|
|
125
|
-
export declare function useEditorDesignSystem(): Readonly<EditorComponents>;
|
|
126
239
|
interface ViewOptions {
|
|
127
240
|
labels?: EditorLabelOverrides;
|
|
128
241
|
}
|
|
@@ -131,16 +244,25 @@ export interface EditorSearch {
|
|
|
131
244
|
value: string;
|
|
132
245
|
onValueChange: (value: string) => void;
|
|
133
246
|
}
|
|
134
|
-
export interface
|
|
135
|
-
|
|
247
|
+
export interface EditorMessageRenderState {
|
|
248
|
+
selected: boolean;
|
|
249
|
+
}
|
|
250
|
+
export interface MessageListProps<Message extends EditorViewMessage = EditorViewMessage> extends ViewOptions {
|
|
251
|
+
messages: readonly Message[];
|
|
136
252
|
selectedId?: string;
|
|
137
253
|
onSelect: (id: string) => void;
|
|
138
254
|
search?: EditorSearch;
|
|
139
255
|
loading?: boolean;
|
|
140
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;
|
|
141
263
|
}
|
|
142
264
|
/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */
|
|
143
|
-
export declare function MessageList({ messages, selectedId, onSelect, search, loading, pagination, labels }: MessageListProps): ReactNode;
|
|
265
|
+
export declare function MessageList<Message extends EditorViewMessage = EditorViewMessage>({ messages, selectedId, onSelect, search, loading, pagination, listSummary, renderMessage, renderMessageActions, labels }: MessageListProps<Message>): ReactNode;
|
|
144
266
|
export interface SourceMessageProps extends ViewOptions {
|
|
145
267
|
message: EditorViewMessage;
|
|
146
268
|
preview?: ReactNode;
|
|
@@ -161,8 +283,8 @@ export interface TranslationFieldProps extends ViewOptions {
|
|
|
161
283
|
preview?: ReactNode;
|
|
162
284
|
}
|
|
163
285
|
export declare function TranslationField({ locale, label, source, draft, onSave, actions, preview, labels }: TranslationFieldProps): ReactNode;
|
|
164
|
-
export type EditorTranslation = Omit<TranslationFieldProps, "source"
|
|
165
|
-
export interface TranslationEditorViewProps extends
|
|
286
|
+
export type EditorTranslation = Omit<TranslationFieldProps, "source">;
|
|
287
|
+
export interface TranslationEditorViewProps<Message extends EditorViewMessage = EditorViewMessage> extends MessageListProps<Message> {
|
|
166
288
|
/** May be outside the loaded list, e.g. during a server-side page change. */
|
|
167
289
|
selectedMessage?: EditorViewMessage;
|
|
168
290
|
translations: readonly EditorTranslation[];
|
|
@@ -170,8 +292,16 @@ export interface TranslationEditorViewProps extends Omit<MessageListProps, "sele
|
|
|
170
292
|
context?: ReactNode;
|
|
171
293
|
sourcePreview?: ReactNode;
|
|
172
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;
|
|
173
302
|
}
|
|
174
303
|
/** A stateless composition over caller-owned navigation and locale drafts. */
|
|
175
|
-
export declare function TranslationEditorView({ selectedMessage, translations, filters, context, sourcePreview, notice, labels, ...list }: TranslationEditorViewProps): ReactNode;
|
|
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;
|
|
176
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 };
|
|
177
307
|
//# sourceMappingURL=ui.d.ts.map
|
package/ui.js
CHANGED
|
@@ -1,39 +1,58 @@
|
|
|
1
|
-
import { createContext, createElement, useContext, useId, useMemo } from "react";
|
|
1
|
+
import { createContext, createElement, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
search: "Search messages",
|
|
6
|
-
messages: "Messages",
|
|
7
|
-
source: "Source message",
|
|
8
|
-
noMessages: "No matching messages",
|
|
9
|
-
noSelection: "Select a message",
|
|
10
|
-
loading: "Loading messages…",
|
|
11
|
-
copySource: "Copy source",
|
|
12
|
-
reset: "Reset",
|
|
13
|
-
save: "Save translation",
|
|
14
|
-
saving: "Saving…",
|
|
15
|
-
saved: "Translation saved.",
|
|
16
|
-
unsaved: "Unsaved changes",
|
|
17
|
-
unchanged: "No unsaved changes",
|
|
18
|
-
validation: {
|
|
19
|
-
empty: "Enter a translation before saving.",
|
|
20
|
-
"invalid-source": "The source contains invalid ICU syntax.",
|
|
21
|
-
"invalid-translation": "The translation contains invalid ICU syntax.",
|
|
22
|
-
structure: "Preserve ICU arguments, tags, formatting styles, and selector branches."
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
function resolveLabels(labels) {
|
|
26
|
-
return {
|
|
27
|
-
...defaultLabels,
|
|
28
|
-
...labels,
|
|
29
|
-
validation: {
|
|
30
|
-
...defaultLabels.validation,
|
|
31
|
-
...labels?.validation
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|
|
3
|
+
import { TYPE, parse } from "@formatjs/icu-messageformat-parser";
|
|
4
|
+
//#region packages/editor/design-system.tsx
|
|
35
5
|
/** Unstyled native controls; no CSS, icons, or localization provider is required. */
|
|
36
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
|
+
}),
|
|
37
56
|
Button: ({ children, onPress, disabled }) => /* @__PURE__ */ jsx("button", {
|
|
38
57
|
type: "button",
|
|
39
58
|
disabled,
|
|
@@ -59,7 +78,11 @@ const nativeEditorComponents = {
|
|
|
59
78
|
"aria-label": label,
|
|
60
79
|
children
|
|
61
80
|
}),
|
|
62
|
-
Layout: ({ toolbar, navigation, content }) => /* @__PURE__ */ jsxs("div", { children: [toolbar, /* @__PURE__ */ jsxs("div", { children: [
|
|
81
|
+
Layout: ({ toolbar, navigation, content, sidebar }) => /* @__PURE__ */ jsxs("div", { children: [toolbar, /* @__PURE__ */ jsxs("div", { children: [
|
|
82
|
+
navigation,
|
|
83
|
+
content,
|
|
84
|
+
sidebar
|
|
85
|
+
] })] })
|
|
63
86
|
};
|
|
64
87
|
const EditorDesignSystemContext = createContext(nativeEditorComponents);
|
|
65
88
|
/** Configure a tree once; sibling providers remain independent. */
|
|
@@ -67,7 +90,7 @@ function EditorDesignSystemProvider({ components, children }) {
|
|
|
67
90
|
const parent = useEditorDesignSystem();
|
|
68
91
|
const value = useMemo(() => ({
|
|
69
92
|
...parent,
|
|
70
|
-
...components
|
|
93
|
+
...Object.fromEntries(Object.entries(components).filter(([, value]) => value !== void 0))
|
|
71
94
|
}), [parent, components]);
|
|
72
95
|
return /* @__PURE__ */ jsx(EditorDesignSystemContext, {
|
|
73
96
|
value,
|
|
@@ -78,8 +101,283 @@ function EditorDesignSystemProvider({ components, children }) {
|
|
|
78
101
|
function useEditorDesignSystem() {
|
|
79
102
|
return useContext(EditorDesignSystemContext);
|
|
80
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
|
+
}
|
|
81
379
|
/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */
|
|
82
|
-
function MessageList({ messages, selectedId, onSelect, search, loading = false, pagination, labels }) {
|
|
380
|
+
function MessageList({ messages, selectedId, onSelect, search, loading = false, pagination, listSummary, renderMessage, renderMessageActions, labels }) {
|
|
83
381
|
const { TextInput, MessageRow } = useEditorDesignSystem();
|
|
84
382
|
const text = resolveLabels(labels);
|
|
85
383
|
const searchId = useId();
|
|
@@ -96,16 +394,20 @@ function MessageList({ messages, selectedId, onSelect, search, loading = false,
|
|
|
96
394
|
value: search.value,
|
|
97
395
|
onValueChange: search.onValueChange
|
|
98
396
|
})] }),
|
|
397
|
+
listSummary,
|
|
99
398
|
loading && /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.loading }) }),
|
|
100
|
-
/* @__PURE__ */ jsx("ul", { children: messages.map((message) =>
|
|
101
|
-
selected: message.id === selectedId
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}) }),
|
|
109
411
|
!loading && messages.length === 0 && /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.noMessages }) }),
|
|
110
412
|
pagination
|
|
111
413
|
]
|
|
@@ -183,30 +485,40 @@ function TranslationField({ locale, label = locale, source, draft, onSave, actio
|
|
|
183
485
|
});
|
|
184
486
|
}
|
|
185
487
|
/** A stateless composition over caller-owned navigation and locale drafts. */
|
|
186
|
-
function TranslationEditorView({ selectedMessage, translations, filters, context, sourcePreview, notice, labels, ...list }) {
|
|
488
|
+
function TranslationEditorView({ selectedMessage, selectedId = selectedMessage?.id, translations, filters, context, sourcePreview, notice, sidebar, emptyState, renderTranslations, renderContent, labels, ...list }) {
|
|
187
489
|
const { Layout } = useEditorDesignSystem();
|
|
188
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 }) })] });
|
|
189
510
|
return /* @__PURE__ */ jsx(Layout, {
|
|
190
511
|
toolbar: filters,
|
|
191
512
|
navigation: /* @__PURE__ */ jsx(MessageList, {
|
|
192
513
|
...list,
|
|
193
|
-
selectedId
|
|
514
|
+
selectedId,
|
|
194
515
|
labels
|
|
195
516
|
}),
|
|
196
|
-
content:
|
|
197
|
-
|
|
198
|
-
preview: sourcePreview,
|
|
199
|
-
context,
|
|
200
|
-
labels
|
|
201
|
-
}), translations.map((translation) => /* @__PURE__ */ createElement(TranslationField, {
|
|
202
|
-
...translation,
|
|
203
|
-
key: `${selectedMessage.id}:${translation.locale}`,
|
|
204
|
-
source: selectedMessage.defaultMessage,
|
|
205
|
-
labels
|
|
206
|
-
}))] }) : /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("output", { children: text.noSelection }) })] })
|
|
517
|
+
content: renderContent ? renderContent(content) : content,
|
|
518
|
+
sidebar
|
|
207
519
|
});
|
|
208
520
|
}
|
|
209
521
|
//#endregion
|
|
210
|
-
export { EditorDesignSystemProvider, MessageList, SourceMessage, TranslationEditorView, TranslationField, nativeEditorComponents, useEditorDesignSystem };
|
|
522
|
+
export { CopyTextButton, EditorDesignSystemProvider, LocalePicker, MessageContext, MessageList, MessagePreview, SourceMessage, TranslationEditorView, TranslationField, nativeEditorComponents, useEditorDesignSystem };
|
|
211
523
|
|
|
212
524
|
//# sourceMappingURL=ui.js.map
|
package/ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.js","names":[],"sources":["../ui.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useId,\n useMemo,\n type ComponentType,\n type ReactNode,\n} from 'react'\nimport type {\n EditorMessage,\n TranslationDraftState,\n} from '#packages/editor/workflow.js'\nimport type {TranslationValidationError} from '#packages/editor/validation.js'\n\nexport interface EditorButtonProps {\n children: ReactNode\n /** Called once per activation, without a DOM event; disabled controls must not call it. */\n onPress: () => void\n disabled?: boolean\n variant: 'primary' | 'secondary'\n}\nexport interface EditorInputProps {\n id: string\n value: string\n /** Reports the complete next string value; never a DOM event. */\n onValueChange: (value: string) => void\n disabled?: boolean\n 'aria-invalid'?: boolean\n 'aria-describedby'?: string\n}\nexport interface EditorTextInputProps extends EditorInputProps {\n type: 'text' | 'search'\n}\nexport interface EditorTextAreaProps extends EditorInputProps {\n /** Visible rows when supported by the control; defaults to six. */\n rows?: number\n}\nexport interface EditorMessageRowProps {\n children: ReactNode\n selected: boolean\n /** Activates this row without changing controlled selection itself. */\n onSelect: () => void\n}\nexport interface EditorPanelProps {\n children: ReactNode\n label: string\n kind: 'source' | 'translation'\n}\nexport interface EditorLayoutProps {\n toolbar: ReactNode\n navigation: ReactNode\n content: ReactNode\n}\n/** Define adapters outside render so controls retain focus across edits. */\nexport interface EditorComponents {\n Button: ComponentType<EditorButtonProps>\n TextInput: ComponentType<EditorTextInputProps>\n TextArea: ComponentType<EditorTextAreaProps>\n MessageRow: ComponentType<EditorMessageRowProps>\n Panel: ComponentType<EditorPanelProps>\n Layout: ComponentType<EditorLayoutProps>\n}\nexport interface EditorLabels {\n search: string\n messages: string\n source: string\n noMessages: string\n noSelection: string\n loading: string\n copySource: string\n reset: string\n save: string\n saving: string\n saved: string\n unsaved: string\n unchanged: string\n validation: Record<TranslationValidationError, string>\n}\nconst defaultLabels: EditorLabels = {\n search: 'Search messages',\n messages: 'Messages',\n source: 'Source message',\n noMessages: 'No matching messages',\n noSelection: 'Select a message',\n loading: 'Loading messages…',\n copySource: 'Copy source',\n reset: 'Reset',\n save: 'Save translation',\n saving: 'Saving…',\n saved: 'Translation saved.',\n unsaved: 'Unsaved changes',\n unchanged: 'No unsaved changes',\n validation: {\n empty: 'Enter a translation before saving.',\n 'invalid-source': 'The source contains invalid ICU syntax.',\n 'invalid-translation': 'The translation contains invalid ICU syntax.',\n structure:\n 'Preserve ICU arguments, tags, formatting styles, and selector branches.',\n },\n}\nexport type EditorLabelOverrides = Partial<Omit<EditorLabels, 'validation'>> & {\n validation?: Partial<EditorLabels['validation']>\n}\nfunction resolveLabels(labels?: EditorLabelOverrides): EditorLabels {\n return {\n ...defaultLabels,\n ...labels,\n validation: {...defaultLabels.validation, ...labels?.validation},\n }\n}\n/** Unstyled native controls; no CSS, icons, or localization provider is required. */\nexport const nativeEditorComponents: EditorComponents = {\n Button: ({children, onPress, disabled}) => (\n <button type=\"button\" disabled={disabled} onClick={() => onPress()}>\n {children}\n </button>\n ),\n TextInput: ({onValueChange, ...props}) => (\n <input {...props} onChange={event => onValueChange(event.target.value)} />\n ),\n TextArea: ({onValueChange, rows = 6, ...props}) => (\n <textarea\n {...props}\n rows={rows}\n onChange={event => onValueChange(event.target.value)}\n />\n ),\n MessageRow: ({children, selected, onSelect}) => (\n <button\n type=\"button\"\n aria-current={selected ? 'true' : undefined}\n onClick={() => onSelect()}\n >\n {children}\n </button>\n ),\n Panel: ({children, label}) => (\n <section aria-label={label}>{children}</section>\n ),\n Layout: ({toolbar, navigation, content}) => (\n <div>\n {toolbar}\n <div>\n {navigation}\n {content}\n </div>\n </div>\n ),\n}\nconst EditorDesignSystemContext = createContext<Readonly<EditorComponents>>(\n nativeEditorComponents\n)\n\nexport interface EditorDesignSystemProviderProps {\n /** Overrides inherit unspecified components from the nearest provider. */\n components: Partial<EditorComponents>\n children: ReactNode\n}\n/** Configure a tree once; sibling providers remain independent. */\nexport function EditorDesignSystemProvider({\n components,\n children,\n}: EditorDesignSystemProviderProps): ReactNode {\n const parent = useEditorDesignSystem()\n const value = useMemo(\n () => ({...parent, ...components}),\n [parent, components]\n )\n return (\n <EditorDesignSystemContext value={value}>\n {children}\n </EditorDesignSystemContext>\n )\n}\n/** Read the resolved controls, including native defaults outside a provider. */\nexport function useEditorDesignSystem(): Readonly<EditorComponents> {\n return useContext(EditorDesignSystemContext)\n}\ninterface ViewOptions {\n labels?: EditorLabelOverrides\n}\nexport type EditorViewMessage = Pick<\n EditorMessage,\n 'id' | 'defaultMessage' | 'description'\n>\nexport interface EditorSearch {\n value: string\n onValueChange: (value: string) => void\n}\nexport interface MessageListProps extends ViewOptions {\n messages: readonly EditorViewMessage[]\n selectedId?: string\n onSelect: (id: string) => void\n search?: EditorSearch\n loading?: boolean\n pagination?: ReactNode\n}\n/** Controlled list: it never filters, paginates, fetches, or changes selection itself. */\nexport function MessageList({\n messages,\n selectedId,\n onSelect,\n search,\n loading = false,\n pagination,\n labels,\n}: MessageListProps): ReactNode {\n const {TextInput, MessageRow} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n const searchId = useId()\n return (\n <nav aria-label={text.messages} aria-busy={loading}>\n {search && (\n <div>\n <label htmlFor={searchId}>{text.search}</label>\n <TextInput\n id={searchId}\n type=\"search\"\n value={search.value}\n onValueChange={search.onValueChange}\n />\n </div>\n )}\n {loading && (\n <p>\n <output>{text.loading}</output>\n </p>\n )}\n <ul>\n {messages.map(message => (\n <li key={message.id}>\n <MessageRow\n selected={message.id === selectedId}\n onSelect={() => onSelect(message.id)}\n >\n <span>{message.defaultMessage}</span> <code>{message.id}</code>\n </MessageRow>\n </li>\n ))}\n </ul>\n {!loading && messages.length === 0 && (\n <p>\n <output>{text.noMessages}</output>\n </p>\n )}\n {pagination}\n </nav>\n )\n}\nexport interface SourceMessageProps extends ViewOptions {\n message: EditorViewMessage\n preview?: ReactNode\n context?: ReactNode\n}\nexport function SourceMessage({\n message,\n preview,\n context,\n labels,\n}: SourceMessageProps): ReactNode {\n const {Panel} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n return (\n <Panel kind=\"source\" label={text.source}>\n <h2>{text.source}</h2>\n <code>{message.id}</code>\n {preview ?? <pre>{message.defaultMessage}</pre>}\n {message.description && <p>{message.description}</p>}\n {context}\n </Panel>\n )\n}\nexport type TranslationFieldDraft = Pick<\n TranslationDraftState,\n | 'value'\n | 'validationError'\n | 'changed'\n | 'isSaving'\n | 'saveError'\n | 'saved'\n | 'setTranslation'\n | 'reset'\n>\nexport interface TranslationFieldProps extends ViewOptions {\n locale: string\n /** Human-readable label; defaults to the locale identifier. */\n label?: string\n source: string\n draft: TranslationFieldDraft\n /** The caller owns confirmation, context, persistence, and receipt presentation. */\n onSave?: () => void\n /** Overrides the default copy/reset/save actions, including with null. */\n actions?: ReactNode\n preview?: ReactNode\n}\nexport function TranslationField({\n locale,\n label = locale,\n source,\n draft,\n onSave,\n actions,\n preview,\n labels,\n}: TranslationFieldProps): ReactNode {\n const {Panel, TextArea, Button} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n const id = useId()\n const errorId = `${id}-error`\n const statusId = `${id}-status`\n const validation = draft.validationError\n ? text.validation[draft.validationError]\n : null\n const error = validation ?? draft.saveError?.message\n return (\n <Panel kind=\"translation\" label={label}>\n <label htmlFor={id}>{label}</label>\n <TextArea\n id={id}\n value={draft.value}\n onValueChange={draft.setTranslation}\n aria-invalid={!!validation}\n aria-describedby={error ? `${errorId} ${statusId}` : statusId}\n />\n {error && (\n <p id={errorId} role=\"alert\">\n {error}\n </p>\n )}\n <p>\n <output id={statusId}>\n {draft.isSaving\n ? text.saving\n : draft.saved\n ? text.saved\n : draft.changed\n ? text.unsaved\n : text.unchanged}\n </output>\n </p>\n {preview}\n {actions !== undefined ? (\n actions\n ) : (\n <div>\n <Button\n variant=\"secondary\"\n disabled={draft.isSaving}\n onPress={() => draft.setTranslation(source)}\n >\n {text.copySource}\n </Button>\n <Button\n variant=\"secondary\"\n disabled={!draft.changed || draft.isSaving}\n onPress={draft.reset}\n >\n {text.reset}\n </Button>\n {onSave && (\n <Button\n variant=\"primary\"\n disabled={\n !draft.changed || !!draft.validationError || draft.isSaving\n }\n onPress={onSave}\n >\n {draft.isSaving ? text.saving : text.save}\n </Button>\n )}\n </div>\n )}\n </Panel>\n )\n}\nexport type EditorTranslation = Omit<TranslationFieldProps, 'source' | 'labels'>\nexport interface TranslationEditorViewProps extends Omit<\n MessageListProps,\n 'selectedId'\n> {\n /** May be outside the loaded list, e.g. during a server-side page change. */\n selectedMessage?: EditorViewMessage\n translations: readonly EditorTranslation[]\n filters?: ReactNode\n context?: ReactNode\n sourcePreview?: ReactNode\n notice?: ReactNode\n}\n/** A stateless composition over caller-owned navigation and locale drafts. */\nexport function TranslationEditorView({\n selectedMessage,\n translations,\n filters,\n context,\n sourcePreview,\n notice,\n labels,\n ...list\n}: TranslationEditorViewProps): ReactNode {\n const {Layout} = useEditorDesignSystem()\n const text = resolveLabels(labels)\n return (\n <Layout\n toolbar={filters}\n navigation={\n <MessageList\n {...list}\n selectedId={selectedMessage?.id}\n labels={labels}\n />\n }\n content={\n <>\n {notice}\n {selectedMessage ? (\n <>\n <SourceMessage\n message={selectedMessage}\n preview={sourcePreview}\n context={context}\n labels={labels}\n />\n {translations.map(translation => (\n <TranslationField\n {...translation}\n key={`${selectedMessage.id}:${translation.locale}`}\n source={selectedMessage.defaultMessage}\n labels={labels}\n />\n ))}\n </>\n ) : (\n <p>\n <output>{text.noSelection}</output>\n </p>\n )}\n </>\n }\n />\n )\n}\n"],"mappings":"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"}
|
|
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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"}
|