diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx index fd27a627..eca94744 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -22,6 +22,7 @@ import { } from '../../../../hooks/useFilesAPI'; import { randomId } from 'helpers/random-id'; import { copyToClipboard } from 'helpers/clipboard'; +import { useTranslation } from '../../../../locales'; const playDing = () => { const ctx = new AudioContext(); @@ -66,6 +67,7 @@ const AgenticTaskRunner = ({ taskInfo, context, }: AgenticTaskRunnerProps) => { + const { t } = useTranslation(); const [phase, setPhase] = useState('ready'); const chat = useChat(undefined, initialModel, { replaceUrl: false, taskInfo }); const availableModels = useUserVisibleModels(); @@ -249,17 +251,17 @@ const AgenticTaskRunner = ({ className="flex items-center gap-2 px-4 py-1.5 rounded-lg bg-destructive/10 text-destructive text-sm font-medium hover:bg-destructive/20 transition-colors cursor-pointer" > - Stop + {t('taskRunner.stop')} ) : accRef.current.some((m) => m.role === 'error') ? ( - Task failed + {t('taskRunner.taskFailed')} ) : ( - Task complete + {t('taskRunner.taskComplete')} )} @@ -291,10 +293,13 @@ type TaskInputFormProps = { hideTrackPickers?: boolean; }; -const trackLabel = (track: { title: string; lang: string; id: number }) => +// Takes `t` because it is a module-level helper, not a component — the alternative is a hook call +// outside a component body. +type TaskRunnerT = ReturnType['t']; +const trackLabel = (track: { title: string; lang: string; id: number }, t: TaskRunnerT) => track.title || (track.lang && track.lang !== 'und' && track.lang !== 'unknown' ? track.lang.toUpperCase() : '') || - `Track ${track.id + 1}`; + t('taskRunner.track', { n: track.id + 1 }); // Channel count → friendly layout name; audio meta → "stereo · aac 193k"-style detail for a track row. const chLabel = (n: number) => @@ -326,6 +331,7 @@ export const TaskInputForm = ({ entryType, hideTrackPickers, }: TaskInputFormProps) => { + const { t } = useTranslation(); const configurableInputs = Object.entries(inputDefs).filter(([key]) => !autoFilledKeys.has(key)); if (configurableInputs.length === 0) return null; @@ -336,7 +342,7 @@ export const TaskInputForm = ({ const tracks = subtitleTracks ?? []; const byId = new Map(parseSubtitleSpec(values[key]).map((e) => [e.id, e])); const entryFor = (track: SubtitleTrack): SubtitleEditEntry => - byId.get(track.id) ?? { id: track.id, keep: true, label: trackLabel(track) }; + byId.get(track.id) ?? { id: track.id, keep: true, label: trackLabel(track, t) }; const update = (id: number, patch: Partial) => onChange( key, @@ -351,10 +357,10 @@ export const TaskInputForm = ({ {probing ? ( {' '} - {entryType === 'directory' ? 'Analyzing files…' : 'Probing…'} + {entryType === 'directory' ? t('taskRunner.analyzing') : 'Probing…'} ) : tracks.length === 0 ? ( - No subtitles + {t('taskRunner.noSubtitles')} ) : ( <>
@@ -417,10 +423,10 @@ export const TaskInputForm = ({ {probing ? ( {' '} - {entryType === 'directory' ? 'Analyzing files…' : 'Probing…'} + {entryType === 'directory' ? t('taskRunner.analyzing') : 'Probing…'} ) : tracks.length === 0 ? ( - None + {t('taskRunner.none')} ) : (
{tracks.map((track) => ( @@ -435,7 +441,7 @@ export const TaskInputForm = ({ className="accent-duck-teal cursor-pointer" /> - {trackLabel(track)} + {trackLabel(track, t)} {isAudio && ( {' '} @@ -580,12 +586,13 @@ type FolderSummaryProps = { }; const FolderSummary = ({ folder, keepAll = false, onKeepAllChange }: FolderSummaryProps) => { + const { t } = useTranslation(); const { fileCount, majorityCount, skipped } = folder; if (fileCount === 0) { return (
- No videos found in this folder. + {t('taskRunner.noVideos')}
); } @@ -608,7 +615,7 @@ const FolderSummary = ({ folder, keepAll = false, onKeepAllChange }: FolderSumma {majorityCount} of {fileCount} videos share this layout and will be processed.
- Different layout — run these separately: + {t('taskRunner.differentLayout')}
    {skipped.map((f) => (
  • @@ -658,7 +665,7 @@ const FolderSummary = ({ folder, keepAll = false, onKeepAllChange }: FolderSumma
{!keepAll && (
- Skipped (convert separately): + {t('taskRunner.skipped')}
    {skipped.map((f) => (
  • @@ -694,14 +701,14 @@ const pickerKinds = (defs: Record | null): PickerKinds => subEdit: Object.values(defs ?? {}).some((d) => d.type === 'subtitle_edit'), }); -const defaultGroupSel = (g: FolderTrackGroup): GroupSel => ({ +const defaultGroupSel = (g: FolderTrackGroup, t: TaskRunnerT): GroupSel => ({ audio: g.audioTracks.map((track) => track.id).join(','), // audio: keep all subs: 'none', // subtitles: keep none by default — you usually want only a few of many - subEdit: g.subtitleTracks.map((track) => ({ id: track.id, keep: false, label: trackLabel(track) })), + subEdit: g.subtitleTracks.map((track) => ({ id: track.id, keep: false, label: trackLabel(track, t) })), }); // Does a group's selection change anything vs keep-all + original labels? -const groupChanges = (g: FolderTrackGroup, s: GroupSel, has: PickerKinds): boolean => { +const groupChanges = (g: FolderTrackGroup, s: GroupSel, has: PickerKinds, t: TaskRunnerT): boolean => { if (has.audio && (s.audio === 'none' ? g.audioTracks.length > 0 : parseCsv(s.audio).size < g.audioTracks.length)) return true; if (has.subs && (s.subs === 'none' ? g.subtitleTracks.length > 0 : parseCsv(s.subs).size < g.subtitleTracks.length)) @@ -709,7 +716,7 @@ const groupChanges = (g: FolderTrackGroup, s: GroupSel, has: PickerKinds): boole if (has.subEdit) { for (const track of g.subtitleTracks) { const e = s.subEdit.find((x) => x.id === track.id); - if (!e || !e.keep || e.label !== trackLabel(track)) return true; + if (!e || !e.keep || e.label !== trackLabel(track, t)) return true; } } return false; @@ -717,12 +724,18 @@ const groupChanges = (g: FolderTrackGroup, s: GroupSel, has: PickerKinds): boole // Build the JSON group config the scripts consume ($INPUT_GROUP_CONFIG). allFiles=true (convert) keeps // every group; otherwise only groups that actually change are included. -const buildGroupConfig = (groups: FolderTrackGroup[], sel: GroupSel[], has: PickerKinds, allFiles: boolean) => { +const buildGroupConfig = ( + groups: FolderTrackGroup[], + sel: GroupSel[], + has: PickerKinds, + allFiles: boolean, + t: TaskRunnerT, +) => { const spec = (csv: string, count: number) => csv === 'none' ? 'none' : parseCsv(csv).size >= count ? 'all' : [...parseCsv(csv)].sort((a, b) => a - b).join(','); return groups - .map((g, gi) => ({ g, s: sel[gi] ?? defaultGroupSel(g) })) - .filter(({ g, s }) => allFiles || groupChanges(g, s, has)) + .map((g, gi) => ({ g, s: sel[gi] ?? defaultGroupSel(g, t) })) + .filter(({ g, s }) => allFiles || groupChanges(g, s, has, t)) .map(({ g, s }) => ({ files: g.files, ...(has.audio ? { audio: spec(s.audio, g.audioTracks.length) } : {}), @@ -731,7 +744,7 @@ const buildGroupConfig = (groups: FolderTrackGroup[], sel: GroupSel[], has: Pick ? { subEdit: g.subtitleTracks.map( (track) => - s.subEdit.find((x) => x.id === track.id) ?? { id: track.id, keep: true, label: trackLabel(track) }, + s.subEdit.find((x) => x.id === track.id) ?? { id: track.id, keep: true, label: trackLabel(track, t) }, ), } : {}), @@ -747,17 +760,18 @@ type PerGroupTrackConfigProps = { }; const PerGroupTrackConfig = ({ groups, has, sel, onChange, probing }: PerGroupTrackConfigProps) => { + const { t } = useTranslation(); if (probing) { return (
    - Analyzing files… + {t('taskRunner.analyzing')}
    ); } if (groups.length === 0) { return (
    - No videos found in this folder. + {t('taskRunner.noVideos')}
    ); } @@ -772,7 +786,7 @@ const PerGroupTrackConfig = ({ groups, has, sel, onChange, probing }: PerGroupTr {groups.length} track layout{groups.length !== 1 ? 's' : ''} — configure each; all groups run in one pass. {groups.map((g, gi) => { - const s = sel[gi] ?? defaultGroupSel(g); + const s = sel[gi] ?? defaultGroupSel(g, t); const aSel = parseCsv(s.audio); const sSel = parseCsv(s.subs); const toggleA = (id: number) => { @@ -786,7 +800,7 @@ const PerGroupTrackConfig = ({ groups, has, sel, onChange, probing }: PerGroupTr onChange(gi, { subs: n.size ? [...n].sort((a, b) => a - b).join(',') : 'none' }); }; const entryFor = (track: SubtitleTrack): SubtitleEditEntry => - s.subEdit.find((x) => x.id === track.id) ?? { id: track.id, keep: true, label: trackLabel(track) }; + s.subEdit.find((x) => x.id === track.id) ?? { id: track.id, keep: true, label: trackLabel(track, t) }; const updateEdit = (id: number, patch: Partial) => onChange(gi, { subEdit: g.subtitleTracks.map((track) => @@ -824,7 +838,7 @@ const PerGroupTrackConfig = ({ groups, has, sel, onChange, probing }: PerGroupTr className="accent-duck-teal cursor-pointer shrink-0" /> - {trackLabel(track)} + {trackLabel(track, t)} · {audioMeta(track)} @@ -851,7 +865,7 @@ const PerGroupTrackConfig = ({ groups, has, sel, onChange, probing }: PerGroupTr className="accent-duck-teal cursor-pointer shrink-0" /> - {trackLabel(track)} + {trackLabel(track, t)} · {track.codec} @@ -935,6 +949,7 @@ const ScriptRunner = ({ selectedNames, onClose, }: ScriptRunnerProps) => { + const { t } = useTranslation(); const runner = useTaskRunner(); const client = useClient(); const navigate = useNavigate(); @@ -1026,7 +1041,7 @@ const ScriptRunner = ({ if (def.type === 'subtitle_tracks') next[key] = 'none'; if (def.type === 'subtitle_edit') next[key] = JSON.stringify( - sub.map((track) => ({ id: track.id, keep: false, label: trackLabel(track) })), + sub.map((track) => ({ id: track.id, keep: false, label: trackLabel(track, t) })), ); } return next; @@ -1061,7 +1076,7 @@ const ScriptRunner = ({ // Per-group mode uses every group with its own pickers, each defaulting to keep-all. setAllGroups(groups); - setGroupSel(groups.map(defaultGroupSel)); + setGroupSel(groups.map((g) => defaultGroupSel(g, t))); const majority = groups[0]; if (!majority) { @@ -1135,12 +1150,12 @@ const ScriptRunner = ({ const perGroupHasWork = perGroupTracks && entryType === 'directory' && - buildGroupConfig(allGroups, groupSel, has, perGroupAllFiles).length > 0; + buildGroupConfig(allGroups, groupSel, has, perGroupAllFiles, t).length > 0; // Collect the final input map (per-group config / include list / keep-all overrides all fold in here). const buildAllInputs = (): Record => { if (perGroupTracks && entryType === 'directory') { - const config = buildGroupConfig(allGroups, groupSel, has, perGroupAllFiles); + const config = buildGroupConfig(allGroups, groupSel, has, perGroupAllFiles, t); return { ...formValues, ...(config.length ? { group_config: JSON.stringify(config) } : {}), ...autoInputs }; } const overrides: Record = {}; @@ -1197,7 +1212,7 @@ const ScriptRunner = ({ onClick={onClose} className="px-5 py-2 rounded-lg text-sm font-medium text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 cursor-pointer" > - Close + {t('taskRunner.close')}
@@ -1209,7 +1224,7 @@ const ScriptRunner = ({
{selectedNames && selectedNames.length > 1 && (
- Running on {selectedNames.length} selected items + {t('taskRunner.runningOn')} {selectedNames.length} selected items
)}
@@ -1280,7 +1295,7 @@ const ScriptRunner = ({ disabled={jobDisabled} className={`${base} bg-duck-dark/10 dark:bg-foreground/10 text-duck-dark dark:text-foreground hover:bg-duck-dark/15 dark:hover:bg-foreground/15`} > - Queue + {t('taskRunner.queue')} )} @@ -1341,7 +1356,7 @@ const ScriptRunner = ({ className="flex items-center gap-2 px-4 py-1.5 rounded-lg bg-destructive/10 text-destructive text-sm font-medium hover:bg-destructive/20 transition-colors cursor-pointer" > - Stop + {t('taskRunner.stop')} ) : runner.exitCode !== 0 ? ( @@ -1351,7 +1366,7 @@ const ScriptRunner = ({ ) : ( - Task complete + {t('taskRunner.taskComplete')} )}
@@ -1369,6 +1384,7 @@ type PipelineRunnerProps = { }; const PipelineRunner = ({ taskDirName, context, cwd, onClose }: PipelineRunnerProps) => { + const { t } = useTranslation(); const pipeline = usePipelineRunner(); const navigate = useNavigate(); const client = useClient(); @@ -1450,7 +1466,9 @@ const PipelineRunner = ({ taskDirName, context, cwd, onClose }: PipelineRunnerPr {hasConcurrentSteps && (
- Concurrency + + {t('taskRunner.concurrency')} + parallel runners per step
)}
- Model + {t('taskRunner.model')}