@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.
- package/dist/ai/VoiceConfirmDialog.js +3 -1
- package/dist/ai/VoiceToolbar.js +8 -8
- package/dist/context/DocumentContext.js +30 -24
- package/dist/editor/SelectionMenu.js +3 -1
- package/dist/file/builtin-exporters.js +20 -24
- package/dist/findreplace/FindReplace.js +2 -2
- package/dist/i18n/useTranslation.js +1 -1
- package/dist/import/AIImportDialog.js +20 -18
- package/dist/locales/bn.json +195 -13
- package/dist/locales/en.json +194 -12
- package/dist/locales/gu.json +195 -13
- package/dist/locales/hi.json +195 -13
- package/dist/locales/kn.json +195 -13
- package/dist/locales/ml.json +195 -13
- package/dist/locales/mr.json +195 -13
- package/dist/locales/or.json +195 -13
- package/dist/locales/pa.json +194 -12
- package/dist/locales/si.json +195 -13
- package/dist/locales/ta.json +195 -13
- package/dist/locales/te.json +195 -13
- package/dist/navigator/SceneNavigator.js +21 -21
- package/dist/navigator/StatisticsDialog.js +4 -2
- package/dist/shell/AppBar.js +2 -2
- package/dist/shell/AppShell.js +1 -1
- package/dist/shell/ConfirmDialog.js +4 -2
- package/dist/shell/ElementToolbar.js +4 -2
- package/dist/shell/LanguageStatus.js +3 -1
- package/dist/shell/StatusBar.js +1 -1
- package/dist/shell/VersionHistory.js +4 -2
- package/dist/titlepage/ExportDialog.js +6 -6
- package/dist/titlepage/TitlePageDialog.js +2 -2
- package/package.json +1 -1
|
@@ -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":
|
|
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
|
}
|
package/dist/ai/VoiceToolbar.js
CHANGED
|
@@ -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('
|
|
87
|
+
showToast(t('voice.toast.inserted'), 'success');
|
|
88
88
|
}
|
|
89
89
|
catch (e) {
|
|
90
|
-
showToast(
|
|
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('
|
|
104
|
+
showToast(t('voice.toast.setup'), 'info');
|
|
105
105
|
ai.openSetup();
|
|
106
106
|
}
|
|
107
107
|
else {
|
|
108
|
-
showToast(
|
|
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('
|
|
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":
|
|
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:
|
|
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('
|
|
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('
|
|
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: '
|
|
305
|
-
message: '
|
|
306
|
-
confirmLabel: '
|
|
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: '
|
|
359
|
-
message: '
|
|
360
|
-
confirmLabel: '
|
|
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(
|
|
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(
|
|
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: '
|
|
426
|
-
message: '
|
|
427
|
-
confirmLabel: '
|
|
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('
|
|
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('
|
|
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: '
|
|
461
|
-
message:
|
|
462
|
-
confirmLabel: '
|
|
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('
|
|
474
|
+
showToast(tRef.current('doc.versionsCleared'), 'success');
|
|
469
475
|
}
|
|
470
476
|
catch {
|
|
471
|
-
showToast('
|
|
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: '
|
|
673
|
-
message: '
|
|
674
|
-
confirmLabel: '
|
|
675
|
-
cancelLabel: '
|
|
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":
|
|
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,
|
|
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:
|
|
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: '
|
|
35
|
-
description: '
|
|
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: '
|
|
38
|
+
successMessage: t('export.done.pdf'),
|
|
43
39
|
},
|
|
44
40
|
{
|
|
45
|
-
id: 'docx', label: '
|
|
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: '
|
|
46
|
+
successMessage: t('export.done.docx'),
|
|
51
47
|
},
|
|
52
48
|
{
|
|
53
|
-
id: 'fountain', label: '
|
|
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: '
|
|
56
|
+
successMessage: t('export.done.fountain'),
|
|
61
57
|
},
|
|
62
|
-
report('oneliner-print', '
|
|
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
|
-
}, '
|
|
65
|
-
report('oneliner-docx', '
|
|
66
|
-
report('oneliner-csv', '
|
|
67
|
-
report('shotlist-docx', '
|
|
68
|
-
report('shotlist-csv', '
|
|
69
|
-
report('cast-docx', '
|
|
70
|
-
report('cast-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":
|
|
175
|
-
setFindReplaceVisible(false); }, "aria-label":
|
|
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
|
|
9
|
-
scene_heading: '
|
|
10
|
-
action: '
|
|
11
|
-
character: '
|
|
12
|
-
transition: '
|
|
13
|
-
centered: '
|
|
14
|
-
lyrics: '
|
|
15
|
-
note: '
|
|
16
|
-
section: '
|
|
17
|
-
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(
|
|
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 =>
|
|
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(
|
|
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":
|
|
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:
|
|
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":
|
|
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:
|
|
111
|
-
}) })] })] }), _jsxs("div", { className: "cs-ai-import-actions", children: [_jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: close, children:
|
|
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
|
}
|