@sutrata/editor 0.4.0 → 0.4.1

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.
@@ -3,9 +3,11 @@ import { useState, useEffect } from 'react';
3
3
  import { useAI } from '../extensions/ai-provider';
4
4
  import { formatTranscript as formatWithAI } from './format-transcript';
5
5
  import { SparklesIcon } from '../shell/icons';
6
+ import { useTranslation } from '../i18n/useTranslation';
6
7
  /** Rendered only where useAI() is non-null (VoiceToolbar, SelectionMenu's Format). */
7
8
  export function VoiceConfirmDialog({ rawTranscript, langCode, onConfirm, onCancel }) {
8
9
  const ai = useAI();
10
+ const { t } = useTranslation();
9
11
  const [formattedText, setFormattedText] = useState('');
10
12
  const [isLoading, setIsLoading] = useState(true);
11
13
  const [error, setError] = useState('');
@@ -33,5 +35,5 @@ export function VoiceConfirmDialog({ rawTranscript, langCode, onConfirm, onCance
33
35
  localStorage.setItem('sutrata_always_auto_insert', alwaysAutoInsert ? 'true' : 'false');
34
36
  onConfirm(formattedText);
35
37
  };
36
- return (_jsx("div", { className: "cs-dialog-overlay", onClick: onCancel, style: { zIndex: 310 }, children: _jsxs("div", { className: "cs-dialog cs-voice-confirm-dialog", role: "dialog", "aria-modal": "true", "aria-label": "Review Formatting", onClick: e => e.stopPropagation(), children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx(SparklesIcon, { size: 16 }), _jsx("span", { className: "cs-dialog-title", children: "Review Formatting" })] }), _jsx("button", { type: "button", className: "cs-fr-close", onClick: onCancel, "aria-label": "Close", children: "\u00D7" })] }), _jsx("div", { className: "cs-voice-confirm-body", children: isLoading ? (_jsxs("div", { className: "cs-voice-loading-state", children: [_jsx("div", { className: "cs-spinner", style: { width: '28px', height: '28px', borderWidth: '2.5px' } }), _jsx("span", { style: { fontSize: '13px', color: 'var(--cs-ui-text-dark-secondary, #a8a69d)' }, children: "Formatting into Sutra screenplay format..." })] })) : (_jsxs(_Fragment, { children: [error && (_jsxs("div", { className: "cs-voice-error", role: "alert", children: ["AI formatting notice: ", error, " (showing raw input below)"] })), _jsxs("div", { className: "cs-voice-grid", children: [_jsxs("div", { className: "cs-voice-column", children: [_jsx("span", { className: "cs-voice-col-title", children: "Raw Content" }), _jsx("div", { className: "cs-voice-box-raw", tabIndex: 0, children: rawTranscript })] }), _jsxs("div", { className: "cs-voice-column", children: [_jsx("span", { className: "cs-voice-col-title", children: "Formatted Sutra" }), _jsx("textarea", { className: "cs-voice-box-formatted", value: formattedText, onChange: e => setFormattedText(e.target.value), rows: 8 })] })] }), _jsx("div", { className: "cs-voice-options", children: _jsxs("label", { className: "cs-voice-checkbox-label", children: [_jsx("input", { type: "checkbox", checked: alwaysAutoInsert, onChange: e => setAlwaysAutoInsert(e.target.checked) }), _jsx("span", { children: "Always auto-insert directly into screenplay (skip review)" })] }) }), _jsxs("div", { className: "cs-dialog-actions", style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '12px' }, children: [_jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: onCancel, children: "Discard" }), _jsx("button", { type: "button", className: "cs-voice-btn-primary", onClick: handleConfirm, children: "Insert Into Screenplay" })] })] })) })] }) }));
38
+ return (_jsx("div", { className: "cs-dialog-overlay", onClick: onCancel, style: { zIndex: 310 }, children: _jsxs("div", { className: "cs-dialog cs-voice-confirm-dialog", role: "dialog", "aria-modal": "true", "aria-label": t('voice.title'), onClick: e => e.stopPropagation(), children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx(SparklesIcon, { size: 16 }), _jsx("span", { className: "cs-dialog-title", children: t('voice.title') })] }), _jsx("button", { type: "button", className: "cs-fr-close", onClick: onCancel, "aria-label": t('common.close'), children: "\u00D7" })] }), _jsx("div", { className: "cs-voice-confirm-body", children: isLoading ? (_jsxs("div", { className: "cs-voice-loading-state", children: [_jsx("div", { className: "cs-spinner", style: { width: '28px', height: '28px', borderWidth: '2.5px' } }), _jsx("span", { style: { fontSize: '13px', color: 'var(--cs-ui-text-dark-secondary, #a8a69d)' }, children: t('voice.formatting') })] })) : (_jsxs(_Fragment, { children: [error && (_jsx("div", { className: "cs-voice-error", role: "alert", children: t('voice.notice').replace('{error}', error) })), _jsxs("div", { className: "cs-voice-grid", children: [_jsxs("div", { className: "cs-voice-column", children: [_jsx("span", { className: "cs-voice-col-title", children: t('voice.raw') }), _jsx("div", { className: "cs-voice-box-raw", tabIndex: 0, children: rawTranscript })] }), _jsxs("div", { className: "cs-voice-column", children: [_jsx("span", { className: "cs-voice-col-title", children: t('voice.formatted') }), _jsx("textarea", { className: "cs-voice-box-formatted", value: formattedText, onChange: e => setFormattedText(e.target.value), rows: 8 })] })] }), _jsx("div", { className: "cs-voice-options", children: _jsxs("label", { className: "cs-voice-checkbox-label", children: [_jsx("input", { type: "checkbox", checked: alwaysAutoInsert, onChange: e => setAlwaysAutoInsert(e.target.checked) }), _jsx("span", { children: t('voice.autoInsert') })] }) }), _jsxs("div", { className: "cs-dialog-actions", style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '12px' }, children: [_jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: onCancel, children: t('voice.discard') }), _jsx("button", { type: "button", className: "cs-voice-btn-primary", onClick: handleConfirm, children: t('voice.insert') })] })] })) })] }) }));
37
39
  }
@@ -26,7 +26,7 @@ export function VoiceToolbar() {
26
26
  const ai = useAI();
27
27
  const speech = useSpeech();
28
28
  const sessionRef = useRef(null);
29
- const { locale } = useTranslation();
29
+ const { locale, t } = useTranslation();
30
30
  const defaultLang = VOICE_LANGUAGES.find(l => l.base === locale)?.code || 'en-US';
31
31
  const [langCode, setLangCode] = useState(defaultLang);
32
32
  const [isListening, setIsListening] = useState(false);
@@ -84,10 +84,10 @@ export function VoiceToolbar() {
84
84
  const result = await formatTranscript(ai, fullTranscript, langCode);
85
85
  insertSutra(result.trim(), mode);
86
86
  setVoiceActive(false);
87
- showToast('Voice dictation inserted.', 'success');
87
+ showToast(t('voice.toast.inserted'), 'success');
88
88
  }
89
89
  catch (e) {
90
- showToast(`Auto-insert failed: ${e.message || String(e)}. Opening confirmation dialog.`, 'warn');
90
+ showToast(t('voice.toast.autoFailed').replace('{error}', e.message || String(e)), 'warn');
91
91
  setShowConfirm(true);
92
92
  }
93
93
  finally {
@@ -101,11 +101,11 @@ export function VoiceToolbar() {
101
101
  const handleStart = async () => {
102
102
  if (!(await ai.isConfigured())) {
103
103
  if (ai.openSetup) {
104
- showToast('AI must be set up to format dictated text. Opening Setup...', 'info');
104
+ showToast(t('voice.toast.setup'), 'info');
105
105
  ai.openSetup();
106
106
  }
107
107
  else {
108
- showToast(`${ai.displayName} is not available right now.`, 'warn');
108
+ showToast(t('ai.unavailable').replace('{name}', ai.displayName), 'warn');
109
109
  }
110
110
  return;
111
111
  }
@@ -161,7 +161,7 @@ export function VoiceToolbar() {
161
161
  insertSutra(formattedText, mode);
162
162
  setShowConfirm(false);
163
163
  setVoiceActive(false);
164
- showToast('Screenplay formatted and inserted.', 'success');
164
+ showToast(t('voice.toast.formatted'), 'success');
165
165
  };
166
166
  const activeBaseLang = VOICE_LANGUAGES.find(l => l.code === langCode)?.base || 'en';
167
167
  const cheatsheet = LOCALIZED_COMMANDS[activeBaseLang] || LOCALIZED_COMMANDS.en;
@@ -172,10 +172,10 @@ export function VoiceToolbar() {
172
172
  setVoiceActive(false);
173
173
  } }));
174
174
  }
175
- return (_jsxs("div", { className: "cs-voice-toolbar", role: "region", "aria-label": "Voice Dictation Toolbar", children: [_jsxs("div", { className: "cs-voice-toolbar-row", children: [_jsxs("div", { className: "cs-voice-toolbar-badge", children: [_jsx(MicIcon, { size: 14 }), _jsx("span", { children: "Voice Dictation" })] }), _jsx("select", { className: "cs-voice-select", value: langCode, onChange: e => setLangCode(e.target.value), disabled: isListening, "aria-label": "Voice Language", children: VOICE_LANGUAGES.map(l => (_jsx("option", { value: l.code, children: l.label }, l.code))) }), isListening ? (_jsxs("div", { className: "cs-voice-listening-group", children: [_jsx("button", { type: "button", className: "cs-voice-btn-danger", onClick: handleStopListening, children: "Stop & Format" }), _jsx("canvas", { ref: canvasRef, width: 110, height: 26, className: "cs-voice-waveform", role: "img", "aria-label": "Audio waveform visualization" })] })) : (_jsxs("div", { className: "cs-voice-actions-group", children: [_jsxs("button", { type: "button", className: "cs-voice-btn-primary", onClick: handleStart, disabled: !isSupported || isFormatting, children: [_jsx(MicIcon, { size: 14 }), _jsx("span", { children: isFormatting ? 'Formatting...' : 'Start Dictation' })] }), transcript && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "cs-voice-btn-secondary", onClick: handleFormatCaptured, disabled: isFormatting, children: "Re-format" }), _jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: () => {
175
+ return (_jsxs("div", { className: "cs-voice-toolbar", role: "region", "aria-label": t('voice.toolbar.aria'), children: [_jsxs("div", { className: "cs-voice-toolbar-row", children: [_jsxs("div", { className: "cs-voice-toolbar-badge", children: [_jsx(MicIcon, { size: 14 }), _jsx("span", { children: t('appbar.voice') })] }), _jsx("select", { className: "cs-voice-select", value: langCode, onChange: e => setLangCode(e.target.value), disabled: isListening, "aria-label": t('voice.language'), children: VOICE_LANGUAGES.map(l => (_jsx("option", { value: l.code, children: l.label }, l.code))) }), isListening ? (_jsxs("div", { className: "cs-voice-listening-group", children: [_jsx("button", { type: "button", className: "cs-voice-btn-danger", onClick: handleStopListening, children: t('voice.stopFormat') }), _jsx("canvas", { ref: canvasRef, width: 110, height: 26, className: "cs-voice-waveform", role: "img", "aria-label": t('voice.wave') })] })) : (_jsxs("div", { className: "cs-voice-actions-group", children: [_jsxs("button", { type: "button", className: "cs-voice-btn-primary", onClick: handleStart, disabled: !isSupported || isFormatting, children: [_jsx(MicIcon, { size: 14 }), _jsx("span", { children: isFormatting ? t('voice.formattingBtn') : t('voice.start') })] }), transcript && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "cs-voice-btn-secondary", onClick: handleFormatCaptured, disabled: isFormatting, children: t('voice.reformat') }), _jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: () => {
176
176
  setTranscript('');
177
177
  setError('');
178
- }, children: "Clear" })] }))] })), _jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: handleCancel, children: "Close" }), !isSupported && (_jsxs("span", { className: "cs-voice-warning", children: [speech.displayName, " is not supported in this browser."] }))] }), (isListening || transcript || interimResult) && (isListening ? (_jsxs("div", { className: "cs-voice-transcript-box", children: [transcript, interimResult && _jsxs("span", { className: "cs-voice-interim", children: [" ", interimResult] }), !transcript && !interimResult && _jsx("span", { className: "cs-voice-placeholder", children: "Speak now to dictate..." })] })) : (_jsx("textarea", { className: "cs-voice-textarea", value: transcript, onChange: (e) => setTranscript(e.target.value), placeholder: "Edit captured text before formatting...", rows: 2 }))), error && (_jsx("div", { className: "cs-voice-error", role: "alert", children: error })), _jsxs("div", { className: "cs-voice-cheatsheet", children: [_jsx("span", { className: "cs-voice-cheatsheet-title", children: "Commands:" }), cheatsheet.map(c => {
178
+ }, children: t('voice.clear') })] }))] })), _jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: handleCancel, children: t('common.close') }), !isSupported && (_jsx("span", { className: "cs-voice-warning", children: t('voice.notSupported').replace('{name}', speech.displayName) }))] }), (isListening || transcript || interimResult) && (isListening ? (_jsxs("div", { className: "cs-voice-transcript-box", children: [transcript, interimResult && _jsxs("span", { className: "cs-voice-interim", children: [" ", interimResult] }), !transcript && !interimResult && _jsx("span", { className: "cs-voice-placeholder", children: t('voice.speakNow') })] })) : (_jsx("textarea", { className: "cs-voice-textarea", value: transcript, onChange: (e) => setTranscript(e.target.value), placeholder: t('voice.editCaptured'), rows: 2 }))), error && (_jsx("div", { className: "cs-voice-error", role: "alert", children: error })), _jsxs("div", { className: "cs-voice-cheatsheet", children: [_jsx("span", { className: "cs-voice-cheatsheet-title", children: t('voice.commands') }), cheatsheet.map(c => {
179
179
  if (c.type === 'literal')
180
180
  return null;
181
181
  return (_jsxs("span", { className: "cs-voice-tag", children: [_jsx("span", { className: "cs-voice-tag-type", children: c.type }), ": \u201C", c.keywords[0], "\u201D"] }, c.type));
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useTranslation } from '../i18n/useTranslation';
2
3
  import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react';
3
4
  import { parse, importFountain, fountainDocToSutra } from '@sutrata/parser';
4
5
  import { StorageAdapterProvider } from '../extensions/storage-adapter';
@@ -66,6 +67,11 @@ function useRegistryProp(value, create) {
66
67
  return useMemo(() => (Array.isArray(value) || !value ? create(value ?? []) : value), [value]); // eslint-disable-line react-hooks/exhaustive-deps
67
68
  }
68
69
  export function DocumentProvider({ children, storageAdapter, aiProvider, speechProvider, session, panels, commands, exporters, decorationProviders, collabBinding, }) {
70
+ const { t } = useTranslation();
71
+ // Callbacks and mount effects read the latest translator through a ref so a
72
+ // language switch never re-runs them.
73
+ const tRef = useRef(t);
74
+ tRef.current = t;
69
75
  const panelRegistry = useRegistryProp(panels, createPanelRegistry);
70
76
  const commandRegistry = useRegistryProp(commands, createCommandRegistry);
71
77
  const exportRegistry = useRegistryProp(exporters, createExportRegistry);
@@ -170,7 +176,7 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
170
176
  if (!cancelled && handle)
171
177
  fileHandleRef.current = handle;
172
178
  }
173
- showToast('Recovered unsaved changes from your last session.', 'info');
179
+ showToast(tRef.current('doc.recovered'), 'info');
174
180
  }
175
181
  catch {
176
182
  // Best-effort recovery; fall back to the empty template on failure.
@@ -258,7 +264,7 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
258
264
  catch {
259
265
  if (!autosaveFailedRef.current) {
260
266
  autosaveFailedRef.current = true;
261
- showToast('Autosave failed — your browser storage may be full or unavailable.', 'warn');
267
+ showToast(tRef.current('doc.autosaveFailed'), 'warn');
262
268
  }
263
269
  }
264
270
  }, [storageAdapter, showToast, applyExternalText]);
@@ -301,9 +307,9 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
301
307
  return;
302
308
  }
303
309
  showConfirm({
304
- title: 'Discard Unsaved Changes?',
305
- message: 'Starting a new screenplay will discard all unsaved edits to your current document.',
306
- confirmLabel: 'Discard & Create New',
310
+ title: tRef.current('doc.discardTitle'),
311
+ message: tRef.current('doc.newMsg'),
312
+ confirmLabel: tRef.current('doc.newConfirm'),
307
313
  destructive: true,
308
314
  onConfirm: doNew,
309
315
  });
@@ -355,9 +361,9 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
355
361
  }
356
362
  await new Promise((resolve) => {
357
363
  showConfirm({
358
- title: 'Discard Unsaved Changes?',
359
- message: 'Opening a different screenplay will discard any unsaved edits to your current document.',
360
- confirmLabel: 'Discard & Open',
364
+ title: tRef.current('doc.discardTitle'),
365
+ message: tRef.current('doc.openMsg'),
366
+ confirmLabel: tRef.current('doc.openConfirm'),
361
367
  destructive: true,
362
368
  onConfirm: () => { void doOpen().then(resolve); },
363
369
  onCancel: () => resolve(),
@@ -389,7 +395,7 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
389
395
  if (result.handle)
390
396
  void storageAdapter.persistFileHandle?.(result.savedName, result.handle);
391
397
  void refreshVersions(result.savedName);
392
- showToast(`Saved ${result.savedName}`, 'success');
398
+ showToast(tRef.current('doc.saved').replace('{name}', result.savedName), 'success');
393
399
  }
394
400
  }, [filePath, text, showToast, storageAdapter, refreshVersions]);
395
401
  const saveFileAs = useCallback(async () => {
@@ -406,7 +412,7 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
406
412
  if (result.handle)
407
413
  void storageAdapter.persistFileHandle?.(result.savedName, result.handle);
408
414
  void refreshVersions(result.savedName);
409
- showToast(`Saved as ${result.savedName}`, 'success');
415
+ showToast(tRef.current('doc.savedAs').replace('{name}', result.savedName), 'success');
410
416
  }
411
417
  }, [filePath, text, showToast, storageAdapter, refreshVersions]);
412
418
  // Reimports a .docx previously exported via exportToDocx (optionally corrected/proofread
@@ -422,15 +428,15 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
422
428
  return warnings;
423
429
  return new Promise((resolve) => {
424
430
  showConfirm({
425
- title: 'Import Word Document',
426
- message: 'Replace the current screenplay content with the imported Word document? This cannot be undone.',
427
- confirmLabel: 'Replace Screenplay',
431
+ title: tRef.current('doc.importTitle'),
432
+ message: tRef.current('doc.importMsg'),
433
+ confirmLabel: tRef.current('doc.importConfirm'),
428
434
  destructive: true,
429
435
  onConfirm: () => {
430
436
  const frontmatterRaw = ast.frontmatter?.raw;
431
437
  const newText = frontmatterRaw ? `${frontmatterRaw}\n${bodyText}\n` : `${bodyText}\n`;
432
438
  setText(newText);
433
- showToast('Word document imported successfully', 'success');
439
+ showToast(tRef.current('doc.imported'), 'success');
434
440
  resolve(warnings);
435
441
  },
436
442
  onCancel: () => resolve(null),
@@ -450,25 +456,25 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
450
456
  const restoreVersion = useCallback((entry) => {
451
457
  setText(entry.content); // setText already marks the document dirty
452
458
  setVersionHistoryVisible(false);
453
- showToast('Version restored — remember to save.', 'info');
459
+ showToast(tRef.current('doc.versionRestored'), 'info');
454
460
  }, [setText, showToast]);
455
461
  const clearVersionHistory = useCallback(async () => {
456
462
  if (!filePath)
457
463
  return;
458
464
  return new Promise((resolve) => {
459
465
  showConfirm({
460
- title: 'Delete Version History?',
461
- message: `Delete all ${versions.length} saved versions of ${filePath}? This cannot be undone.`,
462
- confirmLabel: 'Delete Version History',
466
+ title: tRef.current('doc.clearVersionsTitle'),
467
+ message: tRef.current('doc.clearVersionsMsg').replace('{count}', String(versions.length)).replace('{file}', filePath),
468
+ confirmLabel: tRef.current('version.delete'),
463
469
  destructive: true,
464
470
  onConfirm: async () => {
465
471
  try {
466
472
  await storageAdapter.deleteAllVersions?.(filePath);
467
473
  setVersions([]);
468
- showToast('Version history cleared', 'success');
474
+ showToast(tRef.current('doc.versionsCleared'), 'success');
469
475
  }
470
476
  catch {
471
- showToast('Failed to clear version history', 'error');
477
+ showToast(tRef.current('doc.versionsClearFailed'), 'error');
472
478
  }
473
479
  resolve();
474
480
  },
@@ -669,10 +675,10 @@ export function DocumentProvider({ children, storageAdapter, aiProvider, speechP
669
675
  return;
670
676
  }
671
677
  showConfirm({
672
- title: 'Unsaved Changes',
673
- message: 'You have unsaved changes. Save them before closing?',
674
- confirmLabel: 'Save & Close',
675
- cancelLabel: 'Cancel',
678
+ title: tRef.current('doc.unsavedTitle'),
679
+ message: tRef.current('doc.unsavedMsg'),
680
+ confirmLabel: tRef.current('doc.saveClose'),
681
+ cancelLabel: tRef.current('common.cancel'),
676
682
  onConfirm: () => {
677
683
  void saveFile().then(() => {
678
684
  // saveFile only clears isDirty on success — if the user cancelled a
@@ -6,6 +6,7 @@ import { insertSutra } from './insert-helper';
6
6
  import { VoiceConfirmDialog } from '../ai/VoiceConfirmDialog';
7
7
  import { SparklesIcon } from '../shell/icons';
8
8
  import { useAI } from '../extensions/ai-provider';
9
+ import { useTranslation } from '../i18n/useTranslation';
9
10
  const VOICE_LANG_MAP = {
10
11
  en: 'en-US',
11
12
  hi: 'hi-IN',
@@ -35,6 +36,7 @@ function isSelectionInEditor(sel) {
35
36
  export function SelectionMenu() {
36
37
  const { currentLanguage } = useLanguage();
37
38
  const { mode } = useDocument();
39
+ const { t } = useTranslation();
38
40
  const ai = useAI();
39
41
  const [menuPosition, setMenuPosition] = useState(null);
40
42
  const [selectedText, setSelectedText] = useState('');
@@ -100,5 +102,5 @@ export function SelectionMenu() {
100
102
  return (_jsxs("div", { className: "cs-selection-menu", style: {
101
103
  left: `${menuPosition.x}px`,
102
104
  top: `${menuPosition.y}px`,
103
- }, role: "toolbar", "aria-label": "Text Selection Actions", children: [ai && (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "cs-selection-btn cs-selection-btn-format", onClick: handleFormat, title: "Format selected text using AI", children: [_jsx(SparklesIcon, { size: 12 }), _jsx("span", { children: "Format" })] }), _jsx("div", { className: "cs-selection-sep" })] })), _jsx("button", { type: "button", className: "cs-selection-btn", onClick: handleCut, children: "Cut" }), _jsx("button", { type: "button", className: "cs-selection-btn", onClick: handleCopy, children: "Copy" })] }));
105
+ }, role: "toolbar", "aria-label": t('selection.label'), children: [ai && (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "cs-selection-btn cs-selection-btn-format", onClick: handleFormat, title: t('selection.formatTitle'), children: [_jsx(SparklesIcon, { size: 12 }), _jsx("span", { children: t('selection.format') })] }), _jsx("div", { className: "cs-selection-sep" })] })), _jsx("button", { type: "button", className: "cs-selection-btn", onClick: handleCut, children: t('selection.cut') }), _jsx("button", { type: "button", className: "cs-selection-btn", onClick: handleCopy, children: t('selection.copy') })] }));
104
106
  }
@@ -1,26 +1,22 @@
1
1
  import { exportFountain } from '@sutrata/parser';
2
2
  import { exportToDocx } from './docx-exporter';
3
3
  import { extractWorkflowData, exportOneLinerCsv, exportShotListCsv, exportCastReportCsv, exportOneLinerDocx, exportShotListDocx, exportCastReportDocx, openPrintPreview, openScreenplayPrintPreview, } from './workflow-reports';
4
- /** Short descriptions of the built-in report families, shown under each report's title. */
5
- const REPORT_DESCRIPTIONS = {
6
- 'One-Liner Schedule': 'A scene-by-scene production stripboard breakdown with locations, cast, and timings.',
7
- 'Shot List': 'Camera shot specifications extracted from scene headings and production notes.',
8
- 'Cast Breakdown': 'Character occurrences, dialogue counts, and assigned actors list.',
9
- };
10
4
  const suffix = (ctx) => (ctx.options.romanized ? '_romanized' : '');
11
5
  const titleSuffix = (ctx) => (ctx.options.romanized ? ' (ROMANIZED)' : '');
12
6
  /** CSV with a BOM so Excel reads UTF-8 (Indic text) correctly. */
13
7
  function csv(content) {
14
8
  return new Blob(['\uFEFF', content], { type: 'text/csv;charset=utf-8' });
15
9
  }
16
- function report(id, family, formatLabel, ext, stem, run, successMessage) {
10
+ function report(t, id, familyKey, formatKey, ext, stem, run, successKey) {
11
+ const family = t(`export.family.${familyKey}`);
12
+ const formatLabel = t(`export.format.${formatKey}`);
17
13
  return {
18
14
  id, label: `${family} — ${formatLabel}`, group: 'report', family, formatLabel,
19
- description: REPORT_DESCRIPTIONS[family],
15
+ description: t(`export.family.${familyKey}.desc`),
20
16
  supports: { romanize: true },
21
17
  run,
22
18
  fileName: ctx => `${ctx.baseName}_${stem}${suffix(ctx)}.${ext ?? 'html'}`,
23
- successMessage,
19
+ successMessage: t(successKey),
24
20
  };
25
21
  }
26
22
  /**
@@ -31,42 +27,42 @@ function report(id, family, formatLabel, ext, stem, run, successMessage) {
31
27
  export function builtinExporters(t) {
32
28
  return [
33
29
  {
34
- id: 'pdf', label: 'PDF / Print Preview', group: 'screenplay',
35
- description: 'Print-ready industry screenplay layout styled according to your active visual template.',
30
+ id: 'pdf', label: t('export.pdfLabel'), group: 'screenplay',
31
+ description: t('export.pdfDesc'),
36
32
  supports: { style: true, skipNotes: true, romanize: true },
37
33
  run: async (ctx) => {
38
34
  const title = `${ctx.baseName.toUpperCase()} - SCREENPLAY${titleSuffix(ctx)}`;
39
35
  await openScreenplayPrintPreview(ctx.ast, title, ctx.options.styleId, ctx.customStyles, ctx.options.skipNotes);
40
36
  },
41
37
  fileName: ctx => `${ctx.baseName}${suffix(ctx)}.pdf`,
42
- successMessage: 'Opened Screenplay Print Preview',
38
+ successMessage: t('export.done.pdf'),
43
39
  },
44
40
  {
45
- id: 'docx', label: 'Microsoft Word (.docx)', group: 'screenplay',
41
+ id: 'docx', label: t('export.docxLabel'), group: 'screenplay',
46
42
  description: t('export.docxDesc'),
47
43
  supports: { style: true, skipNotes: true, romanize: true },
48
44
  run: ctx => exportToDocx(ctx.ast, ctx.options.styleId, ctx.customStyles, ctx.options.skipNotes),
49
45
  fileName: ctx => `${ctx.baseName}${suffix(ctx)}.docx`,
50
- successMessage: 'Exported Word document (.docx)',
46
+ successMessage: t('export.done.docx'),
51
47
  },
52
48
  {
53
- id: 'fountain', label: 'Fountain (.fountain)', group: 'screenplay',
49
+ id: 'fountain', label: t('export.fountainLabel'), group: 'screenplay',
54
50
  description: t('export.fountainDesc'),
55
51
  run: async (ctx) => {
56
52
  const result = exportFountain(ctx.ast);
57
53
  return { blob: new Blob([result.text], { type: 'text/plain;charset=utf-8' }), warnings: result.warnings };
58
54
  },
59
55
  fileName: ctx => `${ctx.baseName}.fountain`,
60
- successMessage: 'Exported Fountain screenplay',
56
+ successMessage: t('export.done.fountain'),
61
57
  },
62
- report('oneliner-print', 'One-Liner Schedule', 'Print View', null, 'one_liner', async (ctx) => {
58
+ report(t, 'oneliner-print', 'oneliner', 'print', null, 'one_liner', async (ctx) => {
63
59
  await openPrintPreview(extractWorkflowData(ctx.ast).scenes, `${ctx.baseName.toUpperCase()} - ONE-LINER SCHEDULE${titleSuffix(ctx)}`);
64
- }, 'Opened One-Liner Schedule'),
65
- report('oneliner-docx', 'One-Liner Schedule', 'Word (.docx)', 'docx', 'one_liner', ctx => exportOneLinerDocx(extractWorkflowData(ctx.ast).scenes), 'Exported One-Liner DOCX'),
66
- report('oneliner-csv', 'One-Liner Schedule', 'Spreadsheet (.csv)', 'csv', 'one_liner', async (ctx) => csv(exportOneLinerCsv(extractWorkflowData(ctx.ast).scenes)), 'Exported One-Liner CSV'),
67
- report('shotlist-docx', 'Shot List', 'Word (.docx)', 'docx', 'shot_list', ctx => exportShotListDocx(extractWorkflowData(ctx.ast).scenes), 'Exported Shot List DOCX'),
68
- report('shotlist-csv', 'Shot List', 'Spreadsheet (.csv)', 'csv', 'shot_list', async (ctx) => csv(exportShotListCsv(extractWorkflowData(ctx.ast).scenes)), 'Exported Shot List CSV'),
69
- report('cast-docx', 'Cast Breakdown', 'Word (.docx)', 'docx', 'cast_report', ctx => exportCastReportDocx(extractWorkflowData(ctx.ast).characters), 'Exported Cast Report DOCX'),
70
- report('cast-csv', 'Cast Breakdown', 'Spreadsheet (.csv)', 'csv', 'cast_report', async (ctx) => csv(exportCastReportCsv(extractWorkflowData(ctx.ast).characters)), 'Exported Cast Report CSV'),
60
+ }, 'export.done.oneliner.print'),
61
+ report(t, 'oneliner-docx', 'oneliner', 'docx', 'docx', 'one_liner', ctx => exportOneLinerDocx(extractWorkflowData(ctx.ast).scenes), 'export.done.oneliner.docx'),
62
+ report(t, 'oneliner-csv', 'oneliner', 'csv', 'csv', 'one_liner', async (ctx) => csv(exportOneLinerCsv(extractWorkflowData(ctx.ast).scenes)), 'export.done.oneliner.csv'),
63
+ report(t, 'shotlist-docx', 'shotlist', 'docx', 'docx', 'shot_list', ctx => exportShotListDocx(extractWorkflowData(ctx.ast).scenes), 'export.done.shotlist.docx'),
64
+ report(t, 'shotlist-csv', 'shotlist', 'csv', 'csv', 'shot_list', async (ctx) => csv(exportShotListCsv(extractWorkflowData(ctx.ast).scenes)), 'export.done.shotlist.csv'),
65
+ report(t, 'cast-docx', 'cast', 'docx', 'docx', 'cast_report', ctx => exportCastReportDocx(extractWorkflowData(ctx.ast).characters), 'export.done.cast.docx'),
66
+ report(t, 'cast-csv', 'cast', 'csv', 'csv', 'cast_report', async (ctx) => csv(exportCastReportCsv(extractWorkflowData(ctx.ast).characters)), 'export.done.cast.csv'),
71
67
  ];
72
68
  }
@@ -171,6 +171,6 @@ export function FindReplace() {
171
171
  // Read-only sessions can search but not replace.
172
172
  const isReplace = findMode === 'replace' && canEdit;
173
173
  const hasMatches = matches.length > 0;
174
- return (_jsxs("div", { className: "cs-find-replace", role: "dialog", "aria-label": isReplace ? 'Find and Replace' : 'Find', children: [_jsxs("div", { className: "cs-fr-header", children: [_jsxs("div", { className: "cs-fr-mode-tabs", children: [_jsx("button", { className: `cs-fr-tab${!isReplace ? ' cs-fr-tab-active' : ''}`, onClick: () => setFindMode('find'), children: t('findreplace.findOnly') }), canEdit && _jsx("button", { className: `cs-fr-tab${isReplace ? ' cs-fr-tab-active' : ''}`, onClick: () => setFindMode('replace'), children: t('findreplace.title') })] }), _jsx("button", { className: "cs-fr-close", "aria-label": "Close", onClick: () => setFindReplaceVisible(false), children: "\u00D7" })] }), _jsxs("div", { className: "cs-fr-row", children: [_jsx("input", { ref: queryRef, className: "cs-fr-input", type: "text", placeholder: t('findreplace.find'), value: query, onChange: e => setQuery(e.target.value), onKeyDown: handleQueryKeyDown, "aria-label": "Find" }), _jsx("button", { type: "button", className: "cs-fr-nav-btn", "aria-label": "Previous match", title: "Previous match (Shift+Enter)", onClick: goPrev, disabled: !hasMatches, children: "\u25B2" }), _jsx("button", { type: "button", className: "cs-fr-nav-btn", "aria-label": "Next match", title: "Next match (Enter)", onClick: goNext, disabled: !hasMatches, children: "\u25BC" }), _jsx("button", { type: "button", className: `cs-fr-toggle${caseSensitive ? ' cs-fr-active' : ''}`, onClick: () => setCaseSensitive(v => !v), title: t('findreplace.caseSensitive'), "aria-pressed": caseSensitive, children: "Aa" })] }), _jsxs("div", { className: "cs-fr-status", children: [query && !hasMatches && (_jsx("span", { className: "cs-fr-nomatch", children: t('findreplace.noMatches') })), hasMatches && (_jsxs("span", { className: "cs-fr-nav-count", children: [currentIndex + 1, " of ", matches.length] }))] }), isReplace && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-fr-row", children: _jsx("input", { className: "cs-fr-input", type: "text", placeholder: t('findreplace.replace'), value: replacement, onChange: e => setReplacement(e.target.value), onKeyDown: e => { if (e.key === 'Escape')
175
- setFindReplaceVisible(false); }, "aria-label": "Replace with" }) }), _jsxs("div", { className: "cs-fr-actions", children: [_jsx("button", { className: "cs-fr-btn", onClick: handleReplaceOne, disabled: !hasMatches, children: t('findreplace.replaceOne') }), _jsx("button", { className: "cs-fr-btn", onClick: handleReplaceAll, disabled: !query || !hasMatches, children: t('findreplace.replaceAll') })] })] }))] }));
174
+ return (_jsxs("div", { className: "cs-find-replace", role: "dialog", "aria-label": isReplace ? 'Find and Replace' : 'Find', children: [_jsxs("div", { className: "cs-fr-header", children: [_jsxs("div", { className: "cs-fr-mode-tabs", children: [_jsx("button", { className: `cs-fr-tab${!isReplace ? ' cs-fr-tab-active' : ''}`, onClick: () => setFindMode('find'), children: t('findreplace.findOnly') }), canEdit && _jsx("button", { className: `cs-fr-tab${isReplace ? ' cs-fr-tab-active' : ''}`, onClick: () => setFindMode('replace'), children: t('findreplace.title') })] }), _jsx("button", { className: "cs-fr-close", "aria-label": t('common.close'), onClick: () => setFindReplaceVisible(false), children: "\u00D7" })] }), _jsxs("div", { className: "cs-fr-row", children: [_jsx("input", { ref: queryRef, className: "cs-fr-input", type: "text", placeholder: t('findreplace.find'), value: query, onChange: e => setQuery(e.target.value), onKeyDown: handleQueryKeyDown, "aria-label": t('findreplace.find') }), _jsx("button", { type: "button", className: "cs-fr-nav-btn", "aria-label": t('findreplace.prev'), title: t('findreplace.prevTitle'), onClick: goPrev, disabled: !hasMatches, children: "\u25B2" }), _jsx("button", { type: "button", className: "cs-fr-nav-btn", "aria-label": t('findreplace.next'), title: t('findreplace.nextTitle'), onClick: goNext, disabled: !hasMatches, children: "\u25BC" }), _jsx("button", { type: "button", className: `cs-fr-toggle${caseSensitive ? ' cs-fr-active' : ''}`, onClick: () => setCaseSensitive(v => !v), title: t('findreplace.caseSensitive'), "aria-pressed": caseSensitive, children: "Aa" })] }), _jsxs("div", { className: "cs-fr-status", children: [query && !hasMatches && (_jsx("span", { className: "cs-fr-nomatch", children: t('findreplace.noMatches') })), hasMatches && (_jsx("span", { className: "cs-fr-nav-count", children: t('findreplace.countOf').replace('{current}', String(currentIndex + 1)).replace('{total}', String(matches.length)) }))] }), isReplace && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-fr-row", children: _jsx("input", { className: "cs-fr-input", type: "text", placeholder: t('findreplace.replace'), value: replacement, onChange: e => setReplacement(e.target.value), onKeyDown: e => { if (e.key === 'Escape')
175
+ setFindReplaceVisible(false); }, "aria-label": t('findreplace.replace') }) }), _jsxs("div", { className: "cs-fr-actions", children: [_jsx("button", { className: "cs-fr-btn", onClick: handleReplaceOne, disabled: !hasMatches, children: t('findreplace.replaceOne') }), _jsx("button", { className: "cs-fr-btn", onClick: handleReplaceAll, disabled: !query || !hasMatches, children: t('findreplace.replaceAll') })] })] }))] }));
176
176
  }
@@ -17,7 +17,7 @@ const LOCALES = { en, hi, ta, te, ml, kn, bn, gu, mr, pa, or, si };
17
17
  const TranslationContext = createContext({
18
18
  locale: 'en',
19
19
  setLocale: () => { },
20
- t: (key) => key,
20
+ t: (key) => en[key] ?? key,
21
21
  });
22
22
  const STORAGE_KEY = 'sutrata-ui-locale';
23
23
  export function TranslationProvider({ children }) {
@@ -1,22 +1,23 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useMemo, useRef, useState } from 'react';
3
3
  import { useDocument } from '../context/DocumentContext';
4
+ import { useTranslation } from '../i18n/useTranslation';
4
5
  import { useAI } from '../extensions/ai-provider';
5
6
  import { readImportFile, IMPORT_ACCEPT } from './extract';
6
7
  import { runAiImport, splitBlocks, blockKind, retypeBlock } from './ai-import';
7
8
  import { applyImport } from './apply-import';
8
- const KIND_LABELS = {
9
- scene_heading: 'Scene heading',
10
- action: 'Action',
11
- character: 'Character + dialogue',
12
- transition: 'Transition',
13
- centered: 'Centered',
14
- lyrics: 'Lyrics',
15
- note: 'Note',
16
- section: 'Section',
17
- other: 'Other',
9
+ const KIND_LABEL_KEYS = {
10
+ scene_heading: 'toolbar.scene',
11
+ action: 'toolbar.action',
12
+ character: 'aiimport.kind.character',
13
+ transition: 'toolbar.transition',
14
+ centered: 'toolbar.centered',
15
+ lyrics: 'toolbar.lyrics',
16
+ note: 'toolbar.note',
17
+ section: 'toolbar.section',
18
+ other: 'aiimport.kind.other',
18
19
  };
19
- const KIND_OPTIONS = Object.keys(KIND_LABELS).filter(k => k !== 'other');
20
+ const KIND_OPTIONS = Object.keys(KIND_LABEL_KEYS).filter(k => k !== 'other');
20
21
  /**
21
22
  * AI-assisted import (OSS spec §5.5): pick a Word/text/Markdown file (or paste
22
23
  * text), confirm the provider's data policy, let the AI format it as Sutra in
@@ -26,6 +27,7 @@ const KIND_OPTIONS = Object.keys(KIND_LABELS).filter(k => k !== 'other');
26
27
  */
27
28
  export function AIImportDialog({ onClose }) {
28
29
  const ai = useAI();
30
+ const { t } = useTranslation();
29
31
  const { text, setText, showToast } = useDocument();
30
32
  const [step, setStep] = useState({ name: 'source' });
31
33
  const [pasted, setPasted] = useState('');
@@ -78,7 +80,7 @@ export function AIImportDialog({ onClose }) {
78
80
  signal: controller.signal,
79
81
  onProgress: (done, total) => setStep({ name: 'running', done, total }),
80
82
  });
81
- const warnings = result.chunks.flatMap(c => c.problems.map(p => `Part ${c.index + 1}: ${p}`));
83
+ const warnings = result.chunks.flatMap(c => c.problems.map(p => t('aiimport.part').replace('{n}', String(c.index + 1)).replace('{problem}', p)));
82
84
  setStep({ name: 'review', fileName, source, blocks: splitBlocks(result.sutra), chunks: result.chunks, warnings });
83
85
  }
84
86
  catch (e) {
@@ -97,16 +99,16 @@ export function AIImportDialog({ onClose }) {
97
99
  if (step.name !== 'review')
98
100
  return;
99
101
  applyImport(formatted, how, text, setText);
100
- showToast(`Imported ${step.fileName} (${step.blocks.length} blocks).`, 'success');
102
+ showToast(t('aiimport.imported').replace('{file}', step.fileName).replace('{n}', String(step.blocks.length)), 'success');
101
103
  onClose();
102
104
  };
103
- return (_jsx("div", { className: "cs-dialog-overlay", onClick: close, children: _jsxs("div", { className: "cs-dialog cs-ai-import-dialog", role: "dialog", "aria-modal": "true", "aria-label": "Import with AI", onClick: e => e.stopPropagation(), onKeyDown: e => { if (e.key === 'Escape')
104
- close(); }, style: { maxWidth: step.name === 'review' ? '1100px' : '560px', width: '94%' }, children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsx("span", { className: "cs-dialog-title", children: "Import with AI" }), _jsx("button", { className: "cs-fr-close", "aria-label": "Close", onClick: close, children: "\u00D7" })] }), _jsxs("div", { className: "cs-ai-import-body", children: [error && _jsx("div", { className: "cs-ai-import-error", role: "alert", children: error }), step.name === 'source' && (_jsxs(_Fragment, { children: [_jsx("p", { className: "cs-ai-import-hint", children: "Import a screenplay written in Word, a text file or Markdown. The AI formats it as Sutra; you review the result before anything changes. A Word file exported from Sutrata is imported directly, without AI." }), _jsx("input", { ref: fileRef, type: "file", accept: IMPORT_ACCEPT, "aria-label": "Choose a file to import", onChange: e => void pickFile(e.target.files?.[0]) }), _jsx("label", { className: "cs-settings-label", htmlFor: "cs-ai-import-paste", style: { display: 'block', marginTop: 12 }, children: "\u2026or paste the text" }), _jsx("textarea", { id: "cs-ai-import-paste", className: "cs-ai-import-paste", rows: 8, value: pasted, onChange: e => setPasted(e.target.value) }), _jsx("div", { className: "cs-ai-import-actions", children: _jsx("button", { type: "button", className: "cs-confirm-btn-primary", disabled: !pasted.trim(), onClick: () => setStep({ name: 'confirm', fileName: 'pasted text', text: pasted.trim() }), children: "Continue" }) })] })), step.name === 'confirm' && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "cs-ai-import-hint", children: [_jsx("strong", { children: step.fileName }), " (", step.text.length.toLocaleString(), " characters) will be sent to", ' ', _jsx("strong", { children: ai.displayName }), " to be formatted."] }), _jsx("div", { className: "cs-ai-import-policy", "aria-label": "Data policy", children: ai.dataPolicyText }), _jsxs("label", { className: "cs-ai-import-agree", children: [_jsx("input", { type: "checkbox", checked: agreed, onChange: e => setAgreed(e.target.checked) }), "I understand, send this text to ", ai.displayName] }), _jsxs("div", { className: "cs-ai-import-actions", children: [_jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: () => setStep({ name: 'source' }), children: "Back" }), _jsx("button", { type: "button", className: "cs-confirm-btn-primary", disabled: !agreed, onClick: () => void start(step.fileName, step.text), children: "Format with AI" })] })] })), step.name === 'running' && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "cs-ai-import-hint", role: "status", children: [_jsx("span", { className: "cs-spinner", style: { width: 10, height: 10, borderWidth: 1.5, marginRight: 8 } }), "Formatting part ", Math.min(step.done + 1, step.total), " of ", step.total, "\u2026"] }), _jsx("div", { className: "cs-ai-import-actions", children: _jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: () => abortRef.current?.abort(), children: "Cancel" }) })] })), step.name === 'review' && (_jsxs(_Fragment, { children: [step.warnings.length > 0 && (_jsxs("div", { className: "cs-export-warnings cs-ai-import-warnings", role: "alert", children: [_jsx("div", { className: "cs-export-warn-title", children: "Check these parts before importing" }), step.warnings.map((w, i) => _jsxs("div", { className: "cs-export-warn-item", children: ["\u2022 ", w] }, i))] })), _jsxs("div", { className: "cs-ai-import-review", children: [_jsxs("section", { "aria-label": "Source", children: [_jsx("div", { className: "cs-ai-import-col-title", children: "Source" }), _jsx("pre", { className: "cs-ai-import-source", children: step.source })] }), _jsxs("section", { "aria-label": "Formatted", children: [_jsxs("div", { className: "cs-ai-import-col-title", children: ["Formatted (", step.blocks.length, " blocks)"] }), _jsx("ol", { className: "cs-ai-import-blocks", children: step.blocks.map((block, i) => {
105
+ return (_jsx("div", { className: "cs-dialog-overlay", onClick: close, children: _jsxs("div", { className: "cs-dialog cs-ai-import-dialog", role: "dialog", "aria-modal": "true", "aria-label": t('appbar.importAI'), onClick: e => e.stopPropagation(), onKeyDown: e => { if (e.key === 'Escape')
106
+ close(); }, style: { maxWidth: step.name === 'review' ? '1100px' : '560px', width: '94%' }, children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsx("span", { className: "cs-dialog-title", children: t('appbar.importAI') }), _jsx("button", { className: "cs-fr-close", "aria-label": t('common.close'), onClick: close, children: "\u00D7" })] }), _jsxs("div", { className: "cs-ai-import-body", children: [error && _jsx("div", { className: "cs-ai-import-error", role: "alert", children: error }), step.name === 'source' && (_jsxs(_Fragment, { children: [_jsx("p", { className: "cs-ai-import-hint", children: t('aiimport.hint') }), _jsx("input", { ref: fileRef, type: "file", accept: IMPORT_ACCEPT, "aria-label": t('aiimport.choose'), onChange: e => void pickFile(e.target.files?.[0]) }), _jsx("label", { className: "cs-settings-label", htmlFor: "cs-ai-import-paste", style: { display: 'block', marginTop: 12 }, children: t('aiimport.paste') }), _jsx("textarea", { id: "cs-ai-import-paste", className: "cs-ai-import-paste", rows: 8, value: pasted, onChange: e => setPasted(e.target.value) }), _jsx("div", { className: "cs-ai-import-actions", children: _jsx("button", { type: "button", className: "cs-confirm-btn-primary", disabled: !pasted.trim(), onClick: () => setStep({ name: 'confirm', fileName: t('aiimport.pastedText'), text: pasted.trim() }), children: t('aiimport.continue') }) })] })), step.name === 'confirm' && (_jsxs(_Fragment, { children: [_jsx("p", { className: "cs-ai-import-hint", children: t('aiimport.summary').replace('{file}', step.fileName).replace('{chars}', step.text.length.toLocaleString()).replace('{name}', ai.displayName) }), _jsx("div", { className: "cs-ai-import-policy", "aria-label": t('aiimport.policy'), children: ai.dataPolicyText }), _jsxs("label", { className: "cs-ai-import-agree", children: [_jsx("input", { type: "checkbox", checked: agreed, onChange: e => setAgreed(e.target.checked) }), t('aiimport.agree').replace('{name}', ai.displayName)] }), _jsxs("div", { className: "cs-ai-import-actions", children: [_jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: () => setStep({ name: 'source' }), children: t('aiimport.back') }), _jsx("button", { type: "button", className: "cs-confirm-btn-primary", disabled: !agreed, onClick: () => void start(step.fileName, step.text), children: t('aiimport.format') })] })] })), step.name === 'running' && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "cs-ai-import-hint", role: "status", children: [_jsx("span", { className: "cs-spinner", style: { width: 10, height: 10, borderWidth: 1.5, marginRight: 8 } }), t('aiimport.progress').replace('{done}', String(Math.min(step.done + 1, step.total))).replace('{total}', String(step.total))] }), _jsx("div", { className: "cs-ai-import-actions", children: _jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: () => abortRef.current?.abort(), children: t('common.cancel') }) })] })), step.name === 'review' && (_jsxs(_Fragment, { children: [step.warnings.length > 0 && (_jsxs("div", { className: "cs-export-warnings cs-ai-import-warnings", role: "alert", children: [_jsx("div", { className: "cs-export-warn-title", children: t('aiimport.check') }), step.warnings.map((w, i) => _jsxs("div", { className: "cs-export-warn-item", children: ["\u2022 ", w] }, i))] })), _jsxs("div", { className: "cs-ai-import-review", children: [_jsxs("section", { "aria-label": t('aiimport.source'), children: [_jsx("div", { className: "cs-ai-import-col-title", children: t('aiimport.source') }), _jsx("pre", { className: "cs-ai-import-source", children: step.source })] }), _jsxs("section", { "aria-label": t('aiimport.formatted').replace('{n}', String(step.blocks.length)), children: [_jsx("div", { className: "cs-ai-import-col-title", children: t('aiimport.formatted').replace('{n}', String(step.blocks.length)) }), _jsx("ol", { className: "cs-ai-import-blocks", children: step.blocks.map((block, i) => {
105
107
  const kind = blockKind(block);
106
- return (_jsxs("li", { className: "cs-ai-import-block", children: [_jsxs("select", { "aria-label": `Block ${i + 1} type`, className: "cs-settings-select", value: kind, onChange: e => {
108
+ return (_jsxs("li", { className: "cs-ai-import-block", children: [_jsxs("select", { "aria-label": t('aiimport.blockType').replace('{n}', String(i + 1)), className: "cs-settings-select", value: kind, onChange: e => {
107
109
  const blocks = [...step.blocks];
108
110
  blocks[i] = retypeBlock(block, e.target.value);
109
111
  setStep({ ...step, blocks });
110
- }, children: [kind === 'other' && _jsx("option", { value: "other", children: KIND_LABELS.other }), KIND_OPTIONS.map(k => _jsx("option", { value: k, children: KIND_LABELS[k] }, k))] }), _jsx("pre", { children: block })] }, i));
111
- }) })] })] }), _jsxs("div", { className: "cs-ai-import-actions", children: [_jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: close, children: "Cancel" }), _jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: () => finish('append'), disabled: !step.blocks.length, children: "Append to document" }), _jsx("button", { type: "button", className: "cs-confirm-btn-primary", title: "Replaces the script; the title page is kept", onClick: () => finish('replace'), disabled: !step.blocks.length, children: "Replace script" })] })] }))] })] }) }));
112
+ }, children: [kind === 'other' && _jsx("option", { value: "other", children: t(KIND_LABEL_KEYS.other) }), KIND_OPTIONS.map(k => _jsx("option", { value: k, children: t(KIND_LABEL_KEYS[k]) }, k))] }), _jsx("pre", { children: block })] }, i));
113
+ }) })] })] }), _jsxs("div", { className: "cs-ai-import-actions", children: [_jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: close, children: t('common.cancel') }), _jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: () => finish('append'), disabled: !step.blocks.length, children: t('aiimport.append') }), _jsx("button", { type: "button", className: "cs-confirm-btn-primary", title: t('aiimport.replaceTitle'), onClick: () => finish('replace'), disabled: !step.blocks.length, children: t('aiimport.replace') })] })] }))] })] }) }));
112
114
  }