file viewer panel keyed (445 -> 414)
VideoRenderer used t as its track loop variable in two maps, so keying the panel shadowed the translation function — renamed to track. That is the third panel with this collision; it is worth knowing that a one-letter loop variable is the main mechanical hazard in this phase. The useFileViewer context Error stays English: thrown, not rendered.
This commit is contained in:
@@ -12,8 +12,10 @@ import { TextRenderer } from './renderers/TextRenderer';
|
||||
import { ScrollToTopButton } from './renderers/ScrollToTopButton';
|
||||
import { EditorRenderer } from './renderers/EditorRenderer';
|
||||
import { JsonEditorRenderer } from './renderers/JsonEditorRenderer';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
export const FileViewerBody = () => {
|
||||
const { t } = useTranslation();
|
||||
const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent, handleDownload } =
|
||||
useFileViewer();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -49,7 +51,7 @@ export const FileViewerBody = () => {
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-duck-dark/20 px-3 py-1.5 text-xs text-duck-dark transition-colors hover:bg-duck-dark/5 cursor-pointer"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
Download instead
|
||||
{t('fileViewer.downloadInstead')}
|
||||
</button>
|
||||
</div>
|
||||
) : fileType === 'pdf' ? (
|
||||
@@ -57,7 +59,7 @@ export const FileViewerBody = () => {
|
||||
) : fileType === 'archive' ? (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-duck-dark/50">
|
||||
<FolderArchive className="h-10 w-10" />
|
||||
<span className="text-sm">Archive file</span>
|
||||
<span className="text-sm">{t('fileViewer.archiveFile')}</span>
|
||||
<span className="text-xs">{getArchiveBaseName(fileName)}</span>
|
||||
</div>
|
||||
) : fileType === 'image' ? (
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
import { getIcon } from 'material-file-icons';
|
||||
import { getLang } from './file-types';
|
||||
import { useFileViewer } from './FileViewerContext';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
export const FileViewerHeader = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
fileName,
|
||||
fileType,
|
||||
@@ -81,7 +83,7 @@ export const FileViewerHeader = () => {
|
||||
onClick={handleReadAloud}
|
||||
disabled={ttsLoading}
|
||||
className="p-1 rounded hover:bg-current/10 disabled:opacity-40 transition-colors cursor-pointer"
|
||||
title="Read Aloud"
|
||||
title={t('fileViewer.readAloud')}
|
||||
>
|
||||
{ttsLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Volume2 className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
@@ -91,7 +93,7 @@ export const FileViewerHeader = () => {
|
||||
onClick={handleOcr}
|
||||
disabled={ocrLoading}
|
||||
className="p-1 rounded hover:bg-current/10 disabled:opacity-40 transition-colors cursor-pointer"
|
||||
title="Extract Text (OCR)"
|
||||
title={t('fileViewer.ocr')}
|
||||
>
|
||||
{ocrLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <ScanText className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
@@ -101,7 +103,7 @@ export const FileViewerHeader = () => {
|
||||
onClick={handleTranscribe}
|
||||
disabled={transcribeLoading}
|
||||
className="p-1 rounded hover:bg-current/10 disabled:opacity-40 transition-colors cursor-pointer"
|
||||
title="Transcribe"
|
||||
title={t('fileViewer.transcribe')}
|
||||
>
|
||||
{transcribeLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <FileText className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
@@ -111,7 +113,7 @@ export const FileViewerHeader = () => {
|
||||
onClick={handleExtractAudio}
|
||||
disabled={extractAudioLoading}
|
||||
className="p-1 rounded hover:bg-current/10 disabled:opacity-40 transition-colors cursor-pointer"
|
||||
title="Extract Audio"
|
||||
title={t('fileViewer.extractAudio')}
|
||||
>
|
||||
{extractAudioLoading ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
@@ -125,7 +127,7 @@ export const FileViewerHeader = () => {
|
||||
onClick={handleExtract}
|
||||
disabled={extractLoading}
|
||||
className="p-1 rounded hover:bg-current/10 disabled:opacity-40 transition-colors cursor-pointer"
|
||||
title="Extract"
|
||||
title={t('fileViewer.extract')}
|
||||
>
|
||||
{extractLoading ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
@@ -139,7 +141,7 @@ export const FileViewerHeader = () => {
|
||||
onClick={handleSaveResult}
|
||||
disabled={saveResultLoading}
|
||||
className="p-1 rounded hover:bg-current/10 disabled:opacity-40 transition-colors cursor-pointer"
|
||||
title="Save next to original"
|
||||
title={t('fileViewer.saveNextTo')}
|
||||
>
|
||||
{saveResultLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Save className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
@@ -148,7 +150,7 @@ export const FileViewerHeader = () => {
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="p-1 rounded hover:bg-current/10 transition-colors cursor-pointer"
|
||||
title="Download"
|
||||
title={t('fileViewer.download')}
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useFilesRefresh } from '../../channels';
|
||||
import { toast } from 'sonner';
|
||||
import { getFileType } from './file-types';
|
||||
import { FileViewerContext } from './FileViewerContext';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
type FileViewerProviderProps = {
|
||||
filePath: string;
|
||||
@@ -48,6 +49,7 @@ export const FileViewerProvider = ({
|
||||
autoPlay = false,
|
||||
children,
|
||||
}: FileViewerProviderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [content, setContent] = useState<string | null>(directContent ?? null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -104,7 +106,7 @@ export const FileViewerProvider = ({
|
||||
bumpFilesRefresh();
|
||||
onReplaceView?.(filePath, root, audioPath, audioRoot);
|
||||
} catch {
|
||||
toast.error('Failed to generate speech audio');
|
||||
toast.error(t('fileViewer.speechFailed'));
|
||||
} finally {
|
||||
setTtsLoading(false);
|
||||
}
|
||||
@@ -117,7 +119,7 @@ export const FileViewerProvider = ({
|
||||
bumpFilesRefresh();
|
||||
onReplaceView?.(filePath, root, ocrPath, ocrRoot);
|
||||
} catch {
|
||||
toast.error('Failed to extract text from image');
|
||||
toast.error(t('fileViewer.ocrFailed'));
|
||||
} finally {
|
||||
setOcrLoading(false);
|
||||
}
|
||||
@@ -130,7 +132,7 @@ export const FileViewerProvider = ({
|
||||
bumpFilesRefresh();
|
||||
onReplaceView?.(filePath, root, transcriptionPath, transcriptionRoot);
|
||||
} catch {
|
||||
toast.error('Failed to transcribe audio');
|
||||
toast.error(t('fileViewer.transcribeFailed'));
|
||||
} finally {
|
||||
setTranscribeLoading(false);
|
||||
}
|
||||
@@ -142,7 +144,7 @@ export const FileViewerProvider = ({
|
||||
const { audioPath, audioRoot } = await files.extractAudio(filePath);
|
||||
onOpenFile?.(audioPath, audioRoot);
|
||||
} catch {
|
||||
toast.error('Failed to extract audio from video');
|
||||
toast.error(t('fileViewer.extractAudioFailed'));
|
||||
} finally {
|
||||
setExtractAudioLoading(false);
|
||||
}
|
||||
@@ -157,9 +159,9 @@ export const FileViewerProvider = ({
|
||||
const { savedPath } = await files.saveResult(filePath);
|
||||
const savedName = savedPath.split('/').pop() ?? savedPath;
|
||||
bumpFilesRefresh();
|
||||
toast.success(`Saved as "${savedName}"`);
|
||||
toast.success(t('fileViewer.savedAs', { name: savedName }));
|
||||
} catch {
|
||||
toast.error('Failed to save file');
|
||||
toast.error(t('fileViewer.saveFailed'));
|
||||
} finally {
|
||||
setSaveResultLoading(false);
|
||||
}
|
||||
@@ -171,9 +173,9 @@ export const FileViewerProvider = ({
|
||||
try {
|
||||
const { extractedPath } = await files.extract(filePath);
|
||||
const folderName = extractedPath.split('/').pop() ?? extractedPath;
|
||||
toast.success(`Extracted to "${folderName}"`);
|
||||
toast.success(t('fileViewer.extractedTo', { name: folderName }));
|
||||
} catch {
|
||||
toast.error('Failed to extract archive');
|
||||
toast.error(t('fileViewer.extractFailed'));
|
||||
} finally {
|
||||
setExtractLoading(false);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { Loader2, Music, Play, Pause, Volume2, VolumeX } from 'lucide-react';
|
||||
import { formatTime } from '../file-types';
|
||||
import { useSeekBar, SeekBar } from './SeekBar';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
type AudioRendererProps = {
|
||||
src: string;
|
||||
@@ -10,6 +11,7 @@ type AudioRendererProps = {
|
||||
};
|
||||
|
||||
export const AudioRenderer = ({ src, fileName, autoPlay = false }: AudioRendererProps) => {
|
||||
const { t } = useTranslation();
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [currentTime, setCurrent] = useState(0);
|
||||
@@ -80,7 +82,7 @@ export const AudioRenderer = ({ src, fileName, autoPlay = false }: AudioRenderer
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-sm text-destructive">Failed to load audio file</span>
|
||||
<span className="text-sm text-destructive">{t('fileViewer.audioFailed')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { editor as MonacoEditor } from 'monaco-editor';
|
||||
import { toast } from 'sonner';
|
||||
import { getLanguage } from '../../CodeEditor/language-map';
|
||||
import { useFilesAPI } from '../../../hooks/useFilesAPI';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
type EditorRendererProps = {
|
||||
content: string;
|
||||
@@ -14,6 +15,7 @@ type EditorRendererProps = {
|
||||
};
|
||||
|
||||
export const EditorRenderer = ({ content, fileName, filePath, root, onSave }: EditorRendererProps) => {
|
||||
const { t } = useTranslation();
|
||||
const editorRef = useRef<MonacoEditor.IStandaloneCodeEditor | null>(null);
|
||||
const { writeFile } = useFilesAPI(root);
|
||||
|
||||
@@ -24,9 +26,9 @@ export const EditorRenderer = ({ content, fileName, filePath, root, onSave }: Ed
|
||||
try {
|
||||
await writeFile(filePath, newContent);
|
||||
onSave?.(newContent);
|
||||
toast.success('File saved');
|
||||
toast.success(t('fileViewer.saved'));
|
||||
} catch {
|
||||
toast.error('Failed to save file');
|
||||
toast.error(t('fileViewer.saveFailed'));
|
||||
}
|
||||
}, [filePath, writeFile, onSave]);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Loader2, ZoomIn, ZoomOut, RotateCw } from 'lucide-react';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
const ZOOM_STEPS = [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 5];
|
||||
const MIN_ZOOM = ZOOM_STEPS[0]!;
|
||||
@@ -11,6 +12,7 @@ type ImageRendererProps = {
|
||||
};
|
||||
|
||||
export const ImageRenderer = ({ src, fileName }: ImageRendererProps) => {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [rotation, setRotation] = useState(0);
|
||||
@@ -78,7 +80,7 @@ export const ImageRenderer = ({ src, fileName }: ImageRendererProps) => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-sm text-destructive">Failed to load image</span>
|
||||
<span className="text-sm text-destructive">{t('fileViewer.imageFailed')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { JsonEditor } from '@visual-json/react';
|
||||
import type { JsonValue } from '@visual-json/core';
|
||||
import { toast } from 'sonner';
|
||||
import { useFilesAPI } from '../../../hooks/useFilesAPI';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
type JsonEditorRendererProps = {
|
||||
content: string;
|
||||
@@ -12,6 +13,7 @@ type JsonEditorRendererProps = {
|
||||
};
|
||||
|
||||
export const JsonEditorRenderer = ({ content, filePath, root, onSave }: JsonEditorRendererProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { writeFile } = useFilesAPI(root);
|
||||
const valueRef = useRef<JsonValue>(JSON.parse(content));
|
||||
|
||||
@@ -24,9 +26,9 @@ export const JsonEditorRenderer = ({ content, filePath, root, onSave }: JsonEdit
|
||||
try {
|
||||
await writeFile(filePath, newContent);
|
||||
onSave?.(newContent);
|
||||
toast.success('File saved');
|
||||
toast.success(t('fileViewer.saved'));
|
||||
} catch {
|
||||
toast.error('Failed to save file');
|
||||
toast.error(t('fileViewer.saveFailed'));
|
||||
}
|
||||
}, [filePath, writeFile, onSave]);
|
||||
|
||||
@@ -40,12 +42,7 @@ export const JsonEditorRenderer = ({ content, filePath, root, onSave }: JsonEdit
|
||||
}
|
||||
}}
|
||||
>
|
||||
<JsonEditor
|
||||
defaultValue={valueRef.current}
|
||||
onChange={handleChange}
|
||||
height="100%"
|
||||
width="100%"
|
||||
/>
|
||||
<JsonEditor defaultValue={valueRef.current} onChange={handleChange} height="100%" width="100%" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export const PdfRenderer = ({ src }: { src: string }) => (
|
||||
<iframe src={src} className="w-full h-full border-0" title="PDF viewer" />
|
||||
);
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
export const PdfRenderer = ({ src }: { src: string }) => {
|
||||
const { t } = useTranslation();
|
||||
return <iframe src={src} className="w-full h-full border-0" title={t('fileViewer.pdfViewer')} />;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowUp } from 'lucide-react';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
type ScrollToTopButtonProps = {
|
||||
scrollContainer: React.RefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
export const ScrollToTopButton = ({ scrollContainer }: ScrollToTopButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ export const ScrollToTopButton = ({ scrollContainer }: ScrollToTopButtonProps) =
|
||||
<button
|
||||
onClick={() => scrollContainer.current?.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
className="sticky bottom-4 float-right mr-4 z-10 p-2.5 rounded-full bg-duck-teal text-white shadow-lg hover:bg-duck-teal/90 active:scale-95 transition-all cursor-pointer"
|
||||
title="Scroll to top"
|
||||
title={t('fileViewer.scrollTop')}
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { Loader2, Maximize2, Minimize2, Play, Pause, Volume2, VolumeX, Captions, Languages } from 'lucide-react';
|
||||
import { getExt, formatTime, getRawUrl, getSubtitlesUrl, getSubtitleVttUrl, getAudioTracksUrl } from '../file-types';
|
||||
import { useSeekBar, SeekBar } from './SeekBar';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
type VideoRendererProps = {
|
||||
src: string;
|
||||
@@ -14,6 +15,7 @@ type SubtitleTrack = { id: number; lang: string; label: string };
|
||||
type AudioOption = { id: number; label: string };
|
||||
|
||||
export const VideoRenderer = ({ src, fileName, filePath, root }: VideoRendererProps) => {
|
||||
const { t } = useTranslation();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const hideTimer = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
@@ -105,9 +107,14 @@ export const VideoRenderer = ({ src, fileName, filePath, root }: VideoRendererPr
|
||||
.then((list: Array<{ id: number; lang: string; title: string }>) => {
|
||||
if (cancelled || !Array.isArray(list)) return;
|
||||
setSubtitles(
|
||||
list.map((t) => {
|
||||
const realLang = t.lang && t.lang !== 'und' && t.lang !== 'unknown' ? t.lang.toUpperCase() : '';
|
||||
return { id: t.id, lang: t.lang || 'und', label: t.title || realLang || `Track ${t.id + 1}` };
|
||||
list.map((track) => {
|
||||
const realLang =
|
||||
track.lang && track.lang !== 'und' && track.lang !== 'unknown' ? track.lang.toUpperCase() : '';
|
||||
return {
|
||||
id: track.id,
|
||||
lang: track.lang || 'und',
|
||||
label: track.title || realLang || t('fileViewer.track', { n: track.id + 1 }),
|
||||
};
|
||||
}),
|
||||
);
|
||||
})
|
||||
@@ -126,9 +133,10 @@ export const VideoRenderer = ({ src, fileName, filePath, root }: VideoRendererPr
|
||||
.then((list: Array<{ id: number; lang: string; title: string }>) => {
|
||||
if (cancelled || !Array.isArray(list)) return;
|
||||
setAudioTracks(
|
||||
list.map((t) => {
|
||||
const realLang = t.lang && t.lang !== 'und' && t.lang !== 'unknown' ? t.lang.toUpperCase() : '';
|
||||
return { id: t.id, label: t.title || realLang || `Track ${t.id + 1}` };
|
||||
list.map((track) => {
|
||||
const realLang =
|
||||
track.lang && track.lang !== 'und' && track.lang !== 'unknown' ? track.lang.toUpperCase() : '';
|
||||
return { id: track.id, label: track.title || realLang || t('fileViewer.track', { n: track.id + 1 }) };
|
||||
}),
|
||||
);
|
||||
})
|
||||
@@ -213,7 +221,7 @@ export const VideoRenderer = ({ src, fileName, filePath, root }: VideoRendererPr
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-sm text-destructive">Failed to load video</span>
|
||||
<span className="text-sm text-destructive">{t('fileViewer.videoFailed')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -252,7 +260,7 @@ export const VideoRenderer = ({ src, fileName, filePath, root }: VideoRendererPr
|
||||
<button
|
||||
onClick={() => setShowAudioMenu((s) => !s)}
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-black/50 backdrop-blur-sm text-xs text-white/90 hover:bg-black/70 transition-colors cursor-pointer"
|
||||
title="Audio track"
|
||||
title={t('fileViewer.audioTrack')}
|
||||
>
|
||||
<Languages className="h-3.5 w-3.5" />
|
||||
<span className="max-w-[140px] truncate">{audioTracks[activeAudio]?.label ?? 'Audio'}</span>
|
||||
@@ -337,8 +345,8 @@ export const VideoRenderer = ({ src, fileName, filePath, root }: VideoRendererPr
|
||||
<button
|
||||
onClick={() => setShowSubMenu((s) => !s)}
|
||||
className={`p-1 transition-colors cursor-pointer ${activeSub !== null ? 'text-duck-teal' : 'text-white/70 hover:text-white'}`}
|
||||
title="Subtitles"
|
||||
aria-label="Subtitles"
|
||||
title={t('fileViewer.subtitles')}
|
||||
aria-label={t('fileViewer.subtitles')}
|
||||
>
|
||||
<Captions className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -160,5 +160,33 @@
|
||||
"grid": "2x2 Grid",
|
||||
"colsBottom": "Cols + Bottom"
|
||||
}
|
||||
},
|
||||
"fileViewer": {
|
||||
"readAloud": "Read Aloud",
|
||||
"ocr": "Extract Text (OCR)",
|
||||
"transcribe": "Transcribe",
|
||||
"extractAudio": "Extract Audio",
|
||||
"extract": "Extract",
|
||||
"saveNextTo": "Save next to original",
|
||||
"download": "Download",
|
||||
"pdfViewer": "PDF viewer",
|
||||
"videoFailed": "Failed to load video",
|
||||
"imageFailed": "Failed to load image",
|
||||
"audioFailed": "Failed to load audio file",
|
||||
"audioTrack": "Audio track",
|
||||
"subtitles": "Subtitles",
|
||||
"track": "Track {{n}}",
|
||||
"saved": "File saved",
|
||||
"saveFailed": "Failed to save file",
|
||||
"scrollTop": "Scroll to top",
|
||||
"speechFailed": "Failed to generate speech audio",
|
||||
"ocrFailed": "Failed to extract text from image",
|
||||
"transcribeFailed": "Failed to transcribe audio",
|
||||
"extractAudioFailed": "Failed to extract audio from video",
|
||||
"extractFailed": "Failed to extract archive",
|
||||
"savedAs": "Saved as “{{name}}”",
|
||||
"extractedTo": "Extracted to “{{name}}”",
|
||||
"downloadInstead": "Download instead",
|
||||
"archiveFile": "Archive file"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,5 +160,33 @@
|
||||
"grid": "Grelha 2x2",
|
||||
"colsBottom": "Colunas + inferior"
|
||||
}
|
||||
},
|
||||
"fileViewer": {
|
||||
"readAloud": "Ler em voz alta",
|
||||
"ocr": "Extrair texto (OCR)",
|
||||
"transcribe": "Transcrever",
|
||||
"extractAudio": "Extrair áudio",
|
||||
"extract": "Extrair",
|
||||
"saveNextTo": "Guardar ao lado do original",
|
||||
"download": "Descarregar",
|
||||
"pdfViewer": "Visualizador de PDF",
|
||||
"videoFailed": "Falha ao carregar o vídeo",
|
||||
"imageFailed": "Falha ao carregar a imagem",
|
||||
"audioFailed": "Falha ao carregar o ficheiro de áudio",
|
||||
"audioTrack": "Faixa de áudio",
|
||||
"subtitles": "Legendas",
|
||||
"track": "Faixa {{n}}",
|
||||
"saved": "Ficheiro guardado",
|
||||
"saveFailed": "Falha ao guardar o ficheiro",
|
||||
"scrollTop": "Ir para o topo",
|
||||
"speechFailed": "Falha ao gerar o áudio de fala",
|
||||
"ocrFailed": "Falha ao extrair texto da imagem",
|
||||
"transcribeFailed": "Falha ao transcrever o áudio",
|
||||
"extractAudioFailed": "Falha ao extrair o áudio do vídeo",
|
||||
"extractFailed": "Falha ao extrair o arquivo",
|
||||
"savedAs": "Guardado como “{{name}}”",
|
||||
"extractedTo": "Extraído para “{{name}}”",
|
||||
"downloadInstead": "Descarregar em vez disso",
|
||||
"archiveFile": "Ficheiro de arquivo"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user