mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: resolve all QA report issues from 2026-06-05 sweep
Merges 77 QA issue fixes across routing, editor, i18n, pipeline, and settings. Includes catch-all 404 page, keyboard shortcuts, export filename, password validation, selection masking, brush flow, eraser modes, feather control, pipeline persistence, RTL properties, license info, and i18n for all 21 locales.
This commit is contained in:
@@ -264,7 +264,7 @@ export function Dropzone({
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 w-full max-w-xs">
|
<div className="flex items-center gap-2 w-full max-w-xs">
|
||||||
<div className="h-px flex-1 bg-border" />
|
<div className="h-px flex-1 bg-border" />
|
||||||
<span className="text-xs text-muted-foreground">or</span>
|
<span className="text-xs text-muted-foreground">{t.dropzone.orSeparator}</span>
|
||||||
<div className="h-px flex-1 bg-border" />
|
<div className="h-px flex-1 bg-border" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 w-full max-w-sm">
|
<div className="flex gap-2 w-full max-w-sm">
|
||||||
@@ -295,7 +295,7 @@ export function Dropzone({
|
|||||||
disabled={urlLoading || !urlInput.trim()}
|
disabled={urlLoading || !urlInput.trim()}
|
||||||
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{urlLoading ? "..." : "Add"}
|
{urlLoading ? t.dropzone.urlLoadingIndicator : t.dropzone.addUrlButton}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{urlError && <p className="text-xs text-destructive">{urlError}</p>}
|
{urlError && <p className="text-xs text-destructive">{urlError}</p>}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Check, FolderOpen, ImageIcon, Loader2, Search, X } from "lucide-react";
|
import { Check, FolderOpen, ImageIcon, Loader2, Search, X } from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "@/contexts/i18n-context";
|
||||||
import {
|
import {
|
||||||
apiListFiles,
|
apiListFiles,
|
||||||
formatHeaders,
|
formatHeaders,
|
||||||
@@ -62,6 +63,7 @@ interface FileLibraryModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalProps) {
|
export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [files, setFiles] = useState<UserFile[]>([]);
|
const [files, setFiles] = useState<UserFile[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
@@ -151,7 +153,7 @@ export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalPr
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 px-4 py-3 border-b border-border shrink-0">
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-border shrink-0">
|
||||||
<FolderOpen className="h-5 w-5 text-primary" />
|
<FolderOpen className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-sm font-semibold text-foreground flex-1">Import from Library</h2>
|
<h2 className="text-sm font-semibold text-foreground flex-1">{t.automate.importFromLibrary}</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
|||||||
transparent: true,
|
transparent: true,
|
||||||
});
|
});
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
const [estimatedSize, setEstimatedSize] = useState<number | null>(null);
|
||||||
const [copyStatus, setCopyStatus] = useState<"idle" | "copied">("idle");
|
const [copyStatus, setCopyStatus] = useState<"idle" | "copied">("idle");
|
||||||
|
|
||||||
const aspectRatio = canvasSize.width / canvasSize.height;
|
const aspectRatio = canvasSize.width / canvasSize.height;
|
||||||
@@ -103,7 +104,22 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
|||||||
height: canvasSize.height,
|
height: canvasSize.height,
|
||||||
});
|
});
|
||||||
setPreviewUrl(url);
|
setPreviewUrl(url);
|
||||||
}, [canvasSize, settings.format, settings.quality]);
|
|
||||||
|
const pixelRatio = settings.width / canvasSize.width;
|
||||||
|
const fullUrl = stage.toDataURL({
|
||||||
|
pixelRatio,
|
||||||
|
mimeType: previewMime,
|
||||||
|
quality: settings.quality / 100,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: canvasSize.width,
|
||||||
|
height: canvasSize.height,
|
||||||
|
});
|
||||||
|
fetch(fullUrl)
|
||||||
|
.then((res) => res.blob())
|
||||||
|
.then((blob) => setEstimatedSize(blob.size))
|
||||||
|
.catch(() => setEstimatedSize(null));
|
||||||
|
}, [canvasSize, settings.format, settings.quality, settings.width, settings.height]);
|
||||||
|
|
||||||
// Generate preview thumbnail on format/transparency change
|
// Generate preview thumbnail on format/transparency change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -454,6 +470,14 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
|||||||
alt="Export preview"
|
alt="Export preview"
|
||||||
className="max-h-[120px] object-contain rounded"
|
className="max-h-[120px] object-contain rounded"
|
||||||
/>
|
/>
|
||||||
|
{estimatedSize !== null && (
|
||||||
|
<p className="text-[10px] text-muted-foreground text-center mt-1">
|
||||||
|
~
|
||||||
|
{estimatedSize < 1024 * 1024
|
||||||
|
? `${(estimatedSize / 1024).toFixed(0)} KB`
|
||||||
|
: `${(estimatedSize / (1024 * 1024)).toFixed(1)} MB`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ function useMenuDefinitions(callbacks: MenuBarCallbacks): MenuDef[] {
|
|||||||
const setSelection = useEditorStore((s) => s.setSelection);
|
const setSelection = useEditorStore((s) => s.setSelection);
|
||||||
const invertSelection = useEditorStore((s) => s.invertSelection);
|
const invertSelection = useEditorStore((s) => s.invertSelection);
|
||||||
const canvasSize = useEditorStore((s) => s.canvasSize);
|
const canvasSize = useEditorStore((s) => s.canvasSize);
|
||||||
|
const setPanOffset = useEditorStore((s) => s.setPanOffset);
|
||||||
const rotateCanvas = useEditorStore((s) => s.rotateCanvas);
|
const rotateCanvas = useEditorStore((s) => s.rotateCanvas);
|
||||||
const flipCanvasHorizontal = useEditorStore((s) => s.flipCanvasHorizontal);
|
const flipCanvasHorizontal = useEditorStore((s) => s.flipCanvasHorizontal);
|
||||||
const flipCanvasVertical = useEditorStore((s) => s.flipCanvasVertical);
|
const flipCanvasVertical = useEditorStore((s) => s.flipCanvasVertical);
|
||||||
@@ -327,11 +328,34 @@ function useMenuDefinitions(callbacks: MenuBarCallbacks): MenuDef[] {
|
|||||||
items: [
|
items: [
|
||||||
{ label: "Zoom In", shortcut: mod("Ctrl+="), action: () => setZoom(zoom * 1.25) },
|
{ label: "Zoom In", shortcut: mod("Ctrl+="), action: () => setZoom(zoom * 1.25) },
|
||||||
{ label: "Zoom Out", shortcut: mod("Ctrl+-"), action: () => setZoom(zoom / 1.25) },
|
{ label: "Zoom Out", shortcut: mod("Ctrl+-"), action: () => setZoom(zoom / 1.25) },
|
||||||
{ label: "Fit on Screen", shortcut: mod("Ctrl+0"), action: () => setZoom(1) },
|
{
|
||||||
|
label: "Fit on Screen",
|
||||||
|
shortcut: mod("Ctrl+0"),
|
||||||
|
action: () => {
|
||||||
|
const editorCanvas = document.querySelector("[data-testid='editor-canvas']");
|
||||||
|
if (!editorCanvas) return;
|
||||||
|
const { width: vw, height: vh } = editorCanvas.getBoundingClientRect();
|
||||||
|
const scaleX = vw / canvasSize.width;
|
||||||
|
const scaleY = vh / canvasSize.height;
|
||||||
|
const fitZoom = Math.min(scaleX, scaleY) * 0.9;
|
||||||
|
const offsetX = (vw - canvasSize.width * fitZoom) / 2;
|
||||||
|
const offsetY = (vh - canvasSize.height * fitZoom) / 2;
|
||||||
|
setZoom(fitZoom);
|
||||||
|
setPanOffset({ x: offsetX, y: offsetY });
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Actual Pixels",
|
label: "Actual Pixels",
|
||||||
shortcut: mod("Ctrl+1"),
|
shortcut: mod("Ctrl+1"),
|
||||||
action: () => setZoom(1),
|
action: () => {
|
||||||
|
const editorCanvas = document.querySelector("[data-testid='editor-canvas']");
|
||||||
|
if (!editorCanvas) return;
|
||||||
|
const { width: vw, height: vh } = editorCanvas.getBoundingClientRect();
|
||||||
|
const offsetX = (vw - canvasSize.width) / 2;
|
||||||
|
const offsetY = (vh - canvasSize.height) / 2;
|
||||||
|
setZoom(1);
|
||||||
|
setPanOffset({ x: offsetX, y: offsetY });
|
||||||
|
},
|
||||||
dividerAfter: true,
|
dividerAfter: true,
|
||||||
},
|
},
|
||||||
{ label: "Rulers", checked: rulersVisible, action: toggleRulers },
|
{ label: "Rulers", checked: rulersVisible, action: toggleRulers },
|
||||||
|
|||||||
@@ -10,14 +10,41 @@ export function BrushOptions() {
|
|||||||
const brushSize = useEditorStore((s) => s.brushSize);
|
const brushSize = useEditorStore((s) => s.brushSize);
|
||||||
const brushOpacity = useEditorStore((s) => s.brushOpacity);
|
const brushOpacity = useEditorStore((s) => s.brushOpacity);
|
||||||
const brushHardness = useEditorStore((s) => s.brushHardness);
|
const brushHardness = useEditorStore((s) => s.brushHardness);
|
||||||
|
const brushFlow = useEditorStore((s) => s.brushFlow);
|
||||||
const setBrushSize = useEditorStore((s) => s.setBrushSize);
|
const setBrushSize = useEditorStore((s) => s.setBrushSize);
|
||||||
const setBrushOpacity = useEditorStore((s) => s.setBrushOpacity);
|
const setBrushOpacity = useEditorStore((s) => s.setBrushOpacity);
|
||||||
const setBrushHardness = useEditorStore((s) => s.setBrushHardness);
|
const setBrushHardness = useEditorStore((s) => s.setBrushHardness);
|
||||||
|
const setBrushFlow = useEditorStore((s) => s.setBrushFlow);
|
||||||
|
const eraserMode = useEditorStore((s) => s.eraserMode);
|
||||||
|
const setEraserMode = useEditorStore((s) => s.setEraserMode);
|
||||||
|
|
||||||
if (!BRUSH_OPTION_TOOLS.has(activeTool)) return null;
|
if (!BRUSH_OPTION_TOOLS.has(activeTool)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Eraser mode selector */}
|
||||||
|
{activeTool === "eraser" && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs text-muted-foreground w-12 shrink-0">Mode</span>
|
||||||
|
<div className="flex gap-0.5 flex-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEraserMode("brush")}
|
||||||
|
className={`flex-1 text-xs py-1 rounded ${eraserMode === "brush" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
||||||
|
>
|
||||||
|
Brush
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEraserMode("block")}
|
||||||
|
className={`flex-1 text-xs py-1 rounded ${eraserMode === "block" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
||||||
|
>
|
||||||
|
Block
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Size */}
|
{/* Size */}
|
||||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
Size
|
Size
|
||||||
@@ -84,6 +111,24 @@ export function BrushOptions() {
|
|||||||
<span className="text-[10px]">%</span>
|
<span className="text-[10px]">%</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Flow (not for pencil) */}
|
||||||
|
{activeTool !== "pencil" && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground w-12 shrink-0">Flow</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={Math.round(brushFlow * 100)}
|
||||||
|
onChange={(e) => setBrushFlow(Number(e.target.value) / 100)}
|
||||||
|
className="flex-1 min-w-0"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground tabular-nums w-8 text-end">
|
||||||
|
{Math.round(brushFlow * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export function SelectionOptions() {
|
|||||||
const setMagicWandTolerance = useEditorStore((s) => s.setMagicWandTolerance);
|
const setMagicWandTolerance = useEditorStore((s) => s.setMagicWandTolerance);
|
||||||
const magicWandContiguous = useEditorStore((s) => s.magicWandContiguous);
|
const magicWandContiguous = useEditorStore((s) => s.magicWandContiguous);
|
||||||
const setMagicWandContiguous = useEditorStore((s) => s.setMagicWandContiguous);
|
const setMagicWandContiguous = useEditorStore((s) => s.setMagicWandContiguous);
|
||||||
|
const selectionFeather = useEditorStore((s) => s.selectionFeather);
|
||||||
|
const setSelectionFeather = useEditorStore((s) => s.setSelectionFeather);
|
||||||
|
|
||||||
const selectionType: SelectionType =
|
const selectionType: SelectionType =
|
||||||
activeTool === "marquee-ellipse"
|
activeTool === "marquee-ellipse"
|
||||||
@@ -170,6 +172,25 @@ export function SelectionOptions() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Feather radius */}
|
||||||
|
{(isMarquee || isLasso) && (
|
||||||
|
<>
|
||||||
|
<div className="h-4 w-px bg-border" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground shrink-0">Feather:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={selectionFeather}
|
||||||
|
onChange={(e) => setSelectionFeather(Number(e.target.value))}
|
||||||
|
className="w-14 px-1.5 py-0.5 text-xs rounded border border-border bg-background text-foreground tabular-nums"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">px</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Magic Wand tolerance + contiguous */}
|
{/* Magic Wand tolerance + contiguous */}
|
||||||
{isMagicWand && (
|
{isMagicWand && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FlipHorizontal2, FlipVertical2, Lock, Unlock } from "lucide-react";
|
import { Check, FlipHorizontal2, FlipVertical2, Lock, Unlock, X } from "lucide-react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import type { TransformToolApi } from "@/components/editor/tools/transform-tool";
|
import type { TransformToolApi } from "@/components/editor/tools/transform-tool";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -55,7 +55,7 @@ function NumericInput({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TransformOptions({ api }: { api: TransformToolApi }) {
|
export function TransformOptions({ api }: { api: TransformToolApi }) {
|
||||||
const { values, lockedAspect, setLockedAspect, setValues, flipHorizontal, flipVertical } = api;
|
const { values, lockedAspect, setLockedAspect, setValues, flipHorizontal, flipVertical, applyTransform, cancelTransform } = api;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -137,6 +137,24 @@ export function TransformOptions({ api }: { api: TransformToolApi }) {
|
|||||||
>
|
>
|
||||||
<FlipVertical2 className="h-4 w-4" />
|
<FlipVertical2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className="h-5 w-px bg-border mx-1" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={cancelTransform}
|
||||||
|
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
title="Cancel"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={applyTransform}
|
||||||
|
className="p-1.5 rounded bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
|
||||||
|
title="Apply"
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function useBrushTool() {
|
|||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
|
|
||||||
const { activeTool, foregroundColor, brushSize, brushOpacity, brushHardness, zoom, panOffset } =
|
const { activeTool, foregroundColor, brushSize, brushOpacity, brushHardness, brushFlow, zoom, panOffset } =
|
||||||
useEditorStore.getState();
|
useEditorStore.getState();
|
||||||
|
|
||||||
if (activeTool !== "brush" && activeTool !== "pencil") return;
|
if (activeTool !== "brush" && activeTool !== "pencil") return;
|
||||||
@@ -29,6 +29,15 @@ export function useBrushTool() {
|
|||||||
const x = (pointer.x - panOffset.x) / zoom;
|
const x = (pointer.x - panOffset.x) / zoom;
|
||||||
const y = (pointer.y - panOffset.y) / zoom;
|
const y = (pointer.y - panOffset.y) / zoom;
|
||||||
|
|
||||||
|
const { selection } = useEditorStore.getState();
|
||||||
|
if (selection) {
|
||||||
|
const { bounds } = selection;
|
||||||
|
if (x < bounds.x || x > bounds.x + bounds.width ||
|
||||||
|
y < bounds.y || y > bounds.y + bounds.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
const shadowBlurValue = activeTool === "pencil" ? 0 : brushSize * 0.4 * (1 - brushHardness);
|
const shadowBlurValue = activeTool === "pencil" ? 0 : brushSize * 0.4 * (1 - brushHardness);
|
||||||
|
|
||||||
@@ -39,7 +48,7 @@ export function useBrushTool() {
|
|||||||
tension: activeTool === "pencil" ? 0 : 0.5,
|
tension: activeTool === "pencil" ? 0 : 0.5,
|
||||||
lineCap: "round",
|
lineCap: "round",
|
||||||
lineJoin: "round",
|
lineJoin: "round",
|
||||||
opacity: brushOpacity,
|
opacity: brushOpacity * brushFlow,
|
||||||
globalCompositeOperation: "source-over",
|
globalCompositeOperation: "source-over",
|
||||||
...(shadowBlurValue > 0 && {
|
...(shadowBlurValue > 0 && {
|
||||||
shadowBlur: shadowBlurValue,
|
shadowBlur: shadowBlurValue,
|
||||||
@@ -73,6 +82,15 @@ export function useBrushTool() {
|
|||||||
const x = (pointer.x - panOffset.x) / zoom;
|
const x = (pointer.x - panOffset.x) / zoom;
|
||||||
const y = (pointer.y - panOffset.y) / zoom;
|
const y = (pointer.y - panOffset.y) / zoom;
|
||||||
|
|
||||||
|
const { selection } = useEditorStore.getState();
|
||||||
|
if (selection) {
|
||||||
|
const { bounds } = selection;
|
||||||
|
if (x < bounds.x || x > bounds.x + bounds.width ||
|
||||||
|
y < bounds.y || y > bounds.y + bounds.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strokeRef.current.points = [...strokeRef.current.points, x, y];
|
strokeRef.current.points = [...strokeRef.current.points, x, y];
|
||||||
|
|
||||||
useEditorStore.getState().updateObject(strokeRef.current.objectId, {
|
useEditorStore.getState().updateObject(strokeRef.current.objectId, {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function useEraserTool() {
|
|||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
|
|
||||||
const { activeTool, brushSize, brushOpacity, brushHardness, zoom, panOffset } =
|
const { activeTool, brushSize, brushOpacity, brushHardness, eraserMode, zoom, panOffset } =
|
||||||
useEditorStore.getState();
|
useEditorStore.getState();
|
||||||
|
|
||||||
if (activeTool !== "eraser") return;
|
if (activeTool !== "eraser") return;
|
||||||
@@ -29,16 +29,31 @@ export function useEraserTool() {
|
|||||||
const x = (pointer.x - panOffset.x) / zoom;
|
const x = (pointer.x - panOffset.x) / zoom;
|
||||||
const y = (pointer.y - panOffset.y) / zoom;
|
const y = (pointer.y - panOffset.y) / zoom;
|
||||||
|
|
||||||
|
const { selection } = useEditorStore.getState();
|
||||||
|
if (selection) {
|
||||||
|
const { bounds } = selection;
|
||||||
|
if (
|
||||||
|
x < bounds.x ||
|
||||||
|
x > bounds.x + bounds.width ||
|
||||||
|
y < bounds.y ||
|
||||||
|
y > bounds.y + bounds.height
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
const shadowBlurValue = brushSize * 0.4 * (1 - brushHardness);
|
const shadowBlurValue = brushSize * 0.4 * (1 - brushHardness);
|
||||||
|
|
||||||
|
const isBlock = eraserMode === "block";
|
||||||
|
|
||||||
const attrs: LineAttrs = {
|
const attrs: LineAttrs = {
|
||||||
points: [x, y],
|
points: [x, y],
|
||||||
stroke: "#000000",
|
stroke: "#000000",
|
||||||
strokeWidth: brushSize,
|
strokeWidth: brushSize,
|
||||||
tension: 0.5,
|
tension: isBlock ? 0 : 0.5,
|
||||||
lineCap: "round",
|
lineCap: isBlock ? "butt" : "round",
|
||||||
lineJoin: "round",
|
lineJoin: isBlock ? "miter" : "round",
|
||||||
opacity: brushOpacity,
|
opacity: brushOpacity,
|
||||||
globalCompositeOperation: "destination-out",
|
globalCompositeOperation: "destination-out",
|
||||||
...(shadowBlurValue > 0 && {
|
...(shadowBlurValue > 0 && {
|
||||||
@@ -73,6 +88,19 @@ export function useEraserTool() {
|
|||||||
const x = (pointer.x - panOffset.x) / zoom;
|
const x = (pointer.x - panOffset.x) / zoom;
|
||||||
const y = (pointer.y - panOffset.y) / zoom;
|
const y = (pointer.y - panOffset.y) / zoom;
|
||||||
|
|
||||||
|
const { selection } = useEditorStore.getState();
|
||||||
|
if (selection) {
|
||||||
|
const { bounds } = selection;
|
||||||
|
if (
|
||||||
|
x < bounds.x ||
|
||||||
|
x > bounds.x + bounds.width ||
|
||||||
|
y < bounds.y ||
|
||||||
|
y > bounds.y + bounds.height
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strokeRef.current.points = [...strokeRef.current.points, x, y];
|
strokeRef.current.points = [...strokeRef.current.points, x, y];
|
||||||
|
|
||||||
useEditorStore.getState().updateObject(strokeRef.current.objectId, {
|
useEditorStore.getState().updateObject(strokeRef.current.objectId, {
|
||||||
|
|||||||
@@ -53,7 +53,20 @@ export function useTransformTool(): TransformToolApi {
|
|||||||
|
|
||||||
// Read values from selected object(s)
|
// Read values from selected object(s)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isTransforming || selectedObjectIds.length === 0) return;
|
if (!isTransforming) return;
|
||||||
|
if (selectedObjectIds.length === 0) {
|
||||||
|
const sel = useEditorStore.getState().selection;
|
||||||
|
if (sel && sel.bounds.width > 0) {
|
||||||
|
setValuesState({
|
||||||
|
x: sel.bounds.x,
|
||||||
|
y: sel.bounds.y,
|
||||||
|
width: sel.bounds.width,
|
||||||
|
height: sel.bounds.height,
|
||||||
|
rotation: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const obj = objects.find((o) => o.id === selectedObjectIds[0]);
|
const obj = objects.find((o) => o.id === selectedObjectIds[0]);
|
||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
const a = obj.attrs as unknown as Record<string, unknown>;
|
const a = obj.attrs as unknown as Record<string, unknown>;
|
||||||
@@ -82,7 +95,26 @@ export function useTransformTool(): TransformToolApi {
|
|||||||
}, [isTransforming, selectedObjectIds]);
|
}, [isTransforming, selectedObjectIds]);
|
||||||
|
|
||||||
const activate = useCallback(() => {
|
const activate = useCallback(() => {
|
||||||
if (selectedObjectIds.length === 0) return;
|
if (selectedObjectIds.length === 0) {
|
||||||
|
const sel = useEditorStore.getState().selection;
|
||||||
|
if (!sel || sel.bounds.width === 0) return;
|
||||||
|
preTransformRef.current = {
|
||||||
|
x: sel.bounds.x,
|
||||||
|
y: sel.bounds.y,
|
||||||
|
width: sel.bounds.width,
|
||||||
|
height: sel.bounds.height,
|
||||||
|
rotation: 0,
|
||||||
|
};
|
||||||
|
setValuesState({
|
||||||
|
x: sel.bounds.x,
|
||||||
|
y: sel.bounds.y,
|
||||||
|
width: sel.bounds.width,
|
||||||
|
height: sel.bounds.height,
|
||||||
|
rotation: 0,
|
||||||
|
});
|
||||||
|
setIsTransforming(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsTransforming(true);
|
setIsTransforming(true);
|
||||||
// Store pre-transform state for cancel
|
// Store pre-transform state for cancel
|
||||||
const obj = objects.find((o) => o.id === selectedObjectIds[0]);
|
const obj = objects.find((o) => o.id === selectedObjectIds[0]);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { TOOLS } from "@snapotter/shared";
|
|||||||
import { FileImage, ImageIcon, Workflow } from "lucide-react";
|
import { FileImage, ImageIcon, Workflow } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "@/contexts/i18n-context";
|
||||||
import {
|
import {
|
||||||
apiGetFileDetails,
|
apiGetFileDetails,
|
||||||
formatHeaders,
|
formatHeaders,
|
||||||
@@ -74,6 +75,7 @@ interface FileDetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FileDetails({ mobile = false }: FileDetailsProps) {
|
export function FileDetails({ mobile = false }: FileDetailsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { selectedFileId } = useFilesPageStore();
|
const { selectedFileId } = useFilesPageStore();
|
||||||
const setFiles = useFileStore((s) => s.setFiles);
|
const setFiles = useFileStore((s) => s.setFiles);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -139,11 +141,11 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
|||||||
"flex flex-col items-center justify-center text-muted-foreground",
|
"flex flex-col items-center justify-center text-muted-foreground",
|
||||||
mobile
|
mobile
|
||||||
? "flex flex-col gap-4"
|
? "flex flex-col gap-4"
|
||||||
: "w-60 border-l border-border p-4 shrink-0 hidden lg:flex flex-col",
|
: "w-60 border-s border-border p-4 shrink-0 hidden lg:flex flex-col",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FileImage className="h-12 w-12 mb-3 opacity-30" />
|
<FileImage className="h-12 w-12 mb-3 opacity-30" />
|
||||||
<p className="text-sm">Select a file to view details</p>
|
<p className="text-sm">{t.files.selectFilePrompt}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -155,7 +157,7 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
|||||||
"flex items-center justify-center",
|
"flex items-center justify-center",
|
||||||
mobile
|
mobile
|
||||||
? "flex flex-col gap-4"
|
? "flex flex-col gap-4"
|
||||||
: "w-60 border-l border-border p-4 shrink-0 hidden lg:flex flex-col",
|
: "w-60 border-s border-border p-4 shrink-0 hidden lg:flex flex-col",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="h-6 w-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
<div className="h-6 w-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
@@ -171,7 +173,7 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
|||||||
"overflow-y-auto",
|
"overflow-y-auto",
|
||||||
mobile
|
mobile
|
||||||
? "flex flex-col gap-4"
|
? "flex flex-col gap-4"
|
||||||
: "w-60 border-l border-border p-4 shrink-0 hidden lg:flex flex-col",
|
: "w-60 border-s border-border p-4 shrink-0 hidden lg:flex flex-col",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Thumbnail */}
|
{/* Thumbnail */}
|
||||||
@@ -187,24 +189,28 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="rounded-lg border border-border overflow-hidden">
|
<div className="rounded-lg border border-border overflow-hidden">
|
||||||
<div className="bg-blue-500/10 border-b border-border px-3 py-2">
|
<div className="bg-blue-500/10 border-b border-border px-3 py-2">
|
||||||
<h4 className="text-sm font-semibold text-blue-600 dark:text-blue-400">File Details</h4>
|
<h4 className="text-sm font-semibold text-blue-600 dark:text-blue-400">
|
||||||
|
{t.files.fileDetailsHeading}
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
<DetailRow label="Name" value={details.originalName} />
|
<DetailRow label={t.files.name} value={details.originalName} />
|
||||||
<DetailRow
|
<DetailRow
|
||||||
label="Format"
|
label={t.files.format}
|
||||||
value={details.mimeType.replace("image/", "").toUpperCase()}
|
value={details.mimeType.replace("image/", "").toUpperCase()}
|
||||||
/>
|
/>
|
||||||
<DetailRow label="Size" value={formatSize(details.size)} />
|
<DetailRow label={t.files.size} value={formatSize(details.size)} />
|
||||||
<DetailRow
|
<DetailRow
|
||||||
label="Dimensions"
|
label={t.files.dimensions}
|
||||||
value={details.width && details.height ? `${details.width} × ${details.height}` : "—"}
|
value={details.width && details.height ? `${details.width} × ${details.height}` : "—"}
|
||||||
/>
|
/>
|
||||||
<DetailRow label="Version" value={`V${details.version}`} />
|
<DetailRow label={t.files.version} value={`V${details.version}`} />
|
||||||
<DetailRow
|
<DetailRow
|
||||||
label="Tools Used"
|
label={t.files.toolsUsed}
|
||||||
value={
|
value={
|
||||||
details.toolChain.length > 0 ? details.toolChain.map(toolName).join(", ") : "None"
|
details.toolChain.length > 0
|
||||||
|
? details.toolChain.map(toolName).join(", ")
|
||||||
|
: t.files.none
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Download, Search, Trash2, Workflow } from "lucide-react";
|
import { Download, Search, Trash2, Workflow } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "@/contexts/i18n-context";
|
||||||
import { getFileDownloadUrl } from "@/lib/api";
|
import { getFileDownloadUrl } from "@/lib/api";
|
||||||
|
import { format } from "@/lib/format";
|
||||||
import { useFilesPageStore } from "@/stores/files-page-store";
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
import { FileListItem } from "./file-list-item";
|
import { FileListItem } from "./file-list-item";
|
||||||
|
|
||||||
export function FileList() {
|
export function FileList() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
checkedIds,
|
checkedIds,
|
||||||
@@ -61,7 +64,7 @@ export function FileList() {
|
|||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search files..."
|
placeholder={t.files.searchPlaceholder}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
className="w-full ps-8 pe-3 py-1.5 text-sm bg-muted rounded-lg border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 text-foreground placeholder:text-muted-foreground"
|
className="w-full ps-8 pe-3 py-1.5 text-sm bg-muted rounded-lg border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 text-foreground placeholder:text-muted-foreground"
|
||||||
@@ -78,7 +81,9 @@ export function FileList() {
|
|||||||
className="h-4 w-4 accent-primary"
|
className="h-4 w-4 accent-primary"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground flex-1">
|
<span className="text-xs text-muted-foreground flex-1">
|
||||||
{someChecked ? `${checkedIds.size} selected` : `${files.length} files`}
|
{someChecked
|
||||||
|
? format(t.files.selectedCount, { count: checkedIds.size })
|
||||||
|
: format(t.files.fileCount, { count: files.length })}
|
||||||
</span>
|
</span>
|
||||||
{someChecked && (
|
{someChecked && (
|
||||||
<>
|
<>
|
||||||
@@ -88,7 +93,7 @@ export function FileList() {
|
|||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
className="flex items-center gap-1 px-2 py-1 text-xs text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
Delete
|
{t.files.delete}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -96,7 +101,7 @@ export function FileList() {
|
|||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
className="flex items-center gap-1 px-2 py-1 text-xs text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Workflow className="h-3.5 w-3.5" />
|
<Workflow className="h-3.5 w-3.5" />
|
||||||
Pipeline
|
{t.files.pipeline}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -104,7 +109,7 @@ export function FileList() {
|
|||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-foreground hover:bg-muted rounded-lg transition-colors"
|
className="flex items-center gap-1 px-2 py-1 text-xs text-foreground hover:bg-muted rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Download className="h-3.5 w-3.5" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
Download
|
{t.files.download}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -124,7 +129,7 @@ export function FileList() {
|
|||||||
)}
|
)}
|
||||||
{!loading && !error && files.length === 0 && (
|
{!loading && !error && files.length === 0 && (
|
||||||
<div className="flex items-center justify-center h-32">
|
<div className="flex items-center justify-center h-32">
|
||||||
<p className="text-sm text-muted-foreground">No files found</p>
|
<p className="text-sm text-muted-foreground">{t.files.noFilesFound}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && !error && files.map((file) => <FileListItem key={file.id} file={file} />)}
|
{!loading && !error && files.map((file) => <FileListItem key={file.id} file={file} />)}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Clock, Upload } from "lucide-react";
|
import { Clock, Upload } from "lucide-react";
|
||||||
|
import { useTranslation } from "@/contexts/i18n-context";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useFilesPageStore } from "@/stores/files-page-store";
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
|
|
||||||
export function FilesNav() {
|
export function FilesNav() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { activeTab, setActiveTab } = useFilesPageStore();
|
const { activeTab, setActiveTab } = useFilesPageStore();
|
||||||
const items = [
|
const items = [
|
||||||
{ id: "recent" as const, label: "Recent", icon: Clock },
|
{ id: "recent" as const, label: t.files.recentTab, icon: Clock },
|
||||||
{ id: "upload" as const, label: "Upload Files", icon: Upload },
|
{ id: "upload" as const, label: t.files.uploadTab, icon: Upload },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-48 border-r border-border p-4 shrink-0 hidden md:block">
|
<div className="w-48 border-e border-border p-4 shrink-0 hidden md:block">
|
||||||
<h3 className="text-sm font-semibold text-foreground mb-3">My Files</h3>
|
<h3 className="text-sm font-semibold text-foreground mb-3">{t.files.myFiles}</h3>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function Sidebar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex flex-col items-center w-16 bg-sidebar border-r border-border py-3 gap-1 shrink-0">
|
<aside className="flex flex-col items-center w-16 bg-sidebar border-e border-border py-3 gap-1 shrink-0">
|
||||||
<div className="mb-2 flex items-center justify-center">
|
<div className="mb-2 flex items-center justify-center">
|
||||||
<OtterLogo className="h-7 w-7 text-primary" />
|
<OtterLogo className="h-7 w-7 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ export function CompressControls({ settings: initialSettings, onChange }: Compre
|
|||||||
const [targetSizeValue, setTargetSizeValue] = useState("");
|
const [targetSizeValue, setTargetSizeValue] = useState("");
|
||||||
const [sizeUnit, setSizeUnit] = useState<SizeUnit>("KB");
|
const [sizeUnit, setSizeUnit] = useState<SizeUnit>("KB");
|
||||||
|
|
||||||
const initializedRef = useRef(false);
|
const prevSettingsKeyRef = useRef<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialSettings || initializedRef.current) return;
|
if (!initialSettings) return;
|
||||||
initializedRef.current = true;
|
const key = JSON.stringify(initialSettings);
|
||||||
|
if (prevSettingsKeyRef.current === key) return;
|
||||||
|
prevSettingsKeyRef.current = key;
|
||||||
if (initialSettings.mode != null) setMode(initialSettings.mode as CompressMode);
|
if (initialSettings.mode != null) setMode(initialSettings.mode as CompressMode);
|
||||||
if (initialSettings.quality != null) setQuality(Number(initialSettings.quality));
|
if (initialSettings.quality != null) setQuality(Number(initialSettings.quality));
|
||||||
if (initialSettings.targetSizeKb != null)
|
if (initialSettings.targetSizeKb != null)
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ function SortableStep({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onRemove();
|
onRemove();
|
||||||
}}
|
}}
|
||||||
title="Remove"
|
title={t.automate.removeStep}
|
||||||
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
@@ -159,6 +159,7 @@ export function PipelineBuilder({
|
|||||||
onUpdateSettings,
|
onUpdateSettings,
|
||||||
onToggleStep,
|
onToggleStep,
|
||||||
}: PipelineBuilderProps) {
|
}: PipelineBuilderProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||||
@@ -177,10 +178,8 @@ export function PipelineBuilder({
|
|||||||
<div className="p-4 rounded-full bg-muted/50 mb-4">
|
<div className="p-4 rounded-full bg-muted/50 mb-4">
|
||||||
<FileImage className="h-8 w-8 text-muted-foreground" />
|
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-medium text-foreground mb-1">No steps yet</h3>
|
<h3 className="text-sm font-medium text-foreground mb-1">{t.automate.noStepsHeading}</h3>
|
||||||
<p className="text-sm text-muted-foreground max-w-[240px]">
|
<p className="text-sm text-muted-foreground max-w-[240px]">{t.automate.addToolsPrompt}</p>
|
||||||
Click tools from the palette to build your pipeline
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,12 +66,16 @@ export function ToolPalette({ onAddStep, className }: ToolPaletteProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col h-full", className)}>
|
<div className={cn("flex flex-col h-full", className)}>
|
||||||
<div className="px-3 pt-3 pb-2 shrink-0">
|
<div className="px-3 pt-3 pb-2 shrink-0">
|
||||||
<SearchBar value={search} onChange={setSearch} placeholder="Search tools..." />
|
<SearchBar
|
||||||
|
value={search}
|
||||||
|
onChange={setSearch}
|
||||||
|
placeholder={t.automate.searchToolsPlaceholder}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto px-3 pb-3">
|
<div className="flex-1 overflow-y-auto px-3 pb-3">
|
||||||
{availableTools.length === 0 ? (
|
{availableTools.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">No tools found</p>
|
<p className="text-sm text-muted-foreground text-center py-8">{t.common.noToolsFound}</p>
|
||||||
) : isSearching ? (
|
) : isSearching ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{availableTools.map((tool) => (
|
{availableTools.map((tool) => (
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ export function useEditorShortcuts(callbacks?: {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
"mod+a",
|
"mod+a",
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (isInputFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const state = useEditorStore.getState();
|
const state = useEditorStore.getState();
|
||||||
const allIds = state.objects.map((o) => o.id);
|
const allIds = state.objects.map((o) => o.id);
|
||||||
@@ -406,6 +407,7 @@ export function useEditorShortcuts(callbacks?: {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
"mod+d",
|
"mod+d",
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (isInputFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
useEditorStore.getState().setSelectedObjects([]);
|
useEditorStore.getState().setSelectedObjects([]);
|
||||||
useEditorStore.getState().setSelection(null);
|
useEditorStore.getState().setSelection(null);
|
||||||
@@ -496,6 +498,7 @@ export function useEditorShortcuts(callbacks?: {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
"mod+t",
|
"mod+t",
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (isInputFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
useEditorStore.getState().setTool("transform");
|
useEditorStore.getState().setTool("transform");
|
||||||
},
|
},
|
||||||
@@ -506,6 +509,7 @@ export function useEditorShortcuts(callbacks?: {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
"mod+j",
|
"mod+j",
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (isInputFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const state = useEditorStore.getState();
|
const state = useEditorStore.getState();
|
||||||
state.duplicateLayer(state.activeLayerId);
|
state.duplicateLayer(state.activeLayerId);
|
||||||
@@ -517,6 +521,7 @@ export function useEditorShortcuts(callbacks?: {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
"mod+shift+n",
|
"mod+shift+n",
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (isInputFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
useEditorStore.getState().addLayer();
|
useEditorStore.getState().addLayer();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useTranslation } from "@/contexts/i18n-context";
|
||||||
|
|
||||||
|
export function NotFoundPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen items-center justify-center bg-background text-foreground">
|
||||||
|
<div className="text-center space-y-4 max-w-md px-6">
|
||||||
|
<h1 className="text-6xl font-bold text-primary">404</h1>
|
||||||
|
<h2 className="text-xl font-semibold">{t.common.pageNotFound}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">{t.common.pageNotFoundDescription}</p>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="inline-block px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium"
|
||||||
|
>
|
||||||
|
{t.common.goHome}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
CanvasObject,
|
CanvasObject,
|
||||||
EditorLayer,
|
EditorLayer,
|
||||||
EditorState,
|
EditorState,
|
||||||
|
EraserMode,
|
||||||
FilterConfig,
|
FilterConfig,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
StrokeDashStyle,
|
StrokeDashStyle,
|
||||||
@@ -152,6 +153,8 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
|||||||
brushSize: 10,
|
brushSize: 10,
|
||||||
brushOpacity: 1,
|
brushOpacity: 1,
|
||||||
brushHardness: 1,
|
brushHardness: 1,
|
||||||
|
brushFlow: 1,
|
||||||
|
eraserMode: "brush" as EraserMode,
|
||||||
|
|
||||||
// --- Colors ---
|
// --- Colors ---
|
||||||
foregroundColor: "#000000",
|
foregroundColor: "#000000",
|
||||||
@@ -171,6 +174,7 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
|||||||
selectionMode: "new" as SelectionMode,
|
selectionMode: "new" as SelectionMode,
|
||||||
magicWandTolerance: 32,
|
magicWandTolerance: 32,
|
||||||
magicWandContiguous: true,
|
magicWandContiguous: true,
|
||||||
|
selectionFeather: 0,
|
||||||
|
|
||||||
// --- Crop ---
|
// --- Crop ---
|
||||||
cropState: null,
|
cropState: null,
|
||||||
@@ -954,6 +958,7 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
|||||||
setSelectionMode: (mode) => set({ selectionMode: mode }),
|
setSelectionMode: (mode) => set({ selectionMode: mode }),
|
||||||
setMagicWandTolerance: (v) => set({ magicWandTolerance: v }),
|
setMagicWandTolerance: (v) => set({ magicWandTolerance: v }),
|
||||||
setMagicWandContiguous: (v: boolean) => set({ magicWandContiguous: v }),
|
setMagicWandContiguous: (v: boolean) => set({ magicWandContiguous: v }),
|
||||||
|
setSelectionFeather: (v) => set({ selectionFeather: v }),
|
||||||
|
|
||||||
invertSelection: () => {
|
invertSelection: () => {
|
||||||
const { selection, canvasSize } = get();
|
const { selection, canvasSize } = get();
|
||||||
@@ -1067,6 +1072,8 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
|||||||
setBrushSize: (size) => set({ brushSize: Math.max(1, Math.min(MAX_BRUSH_SIZE, size)) }),
|
setBrushSize: (size) => set({ brushSize: Math.max(1, Math.min(MAX_BRUSH_SIZE, size)) }),
|
||||||
setBrushOpacity: (opacity) => set({ brushOpacity: Math.max(0, Math.min(1, opacity)) }),
|
setBrushOpacity: (opacity) => set({ brushOpacity: Math.max(0, Math.min(1, opacity)) }),
|
||||||
setBrushHardness: (hardness) => set({ brushHardness: Math.max(0, Math.min(1, hardness)) }),
|
setBrushHardness: (hardness) => set({ brushHardness: Math.max(0, Math.min(1, hardness)) }),
|
||||||
|
setBrushFlow: (flow) => set({ brushFlow: Math.max(0, Math.min(1, flow)) }),
|
||||||
|
setEraserMode: (mode) => set({ eraserMode: mode }),
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
copyObjects: () => {
|
copyObjects: () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
import { generateId } from "@/lib/utils";
|
import { generateId } from "@/lib/utils";
|
||||||
|
|
||||||
export interface PipelineStep {
|
export interface PipelineStep {
|
||||||
@@ -30,51 +31,63 @@ interface PipelineState {
|
|||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePipelineStore = create<PipelineState>((set, get) => ({
|
export const usePipelineStore = create<PipelineState>()(
|
||||||
steps: [],
|
persist(
|
||||||
expandedStepId: null,
|
(set, get) => ({
|
||||||
savedPipelines: [],
|
steps: [],
|
||||||
|
expandedStepId: null,
|
||||||
|
savedPipelines: [],
|
||||||
|
|
||||||
addStep: (toolId) => {
|
addStep: (toolId) => {
|
||||||
const step: PipelineStep = { id: generateId(), toolId, settings: {} };
|
const step: PipelineStep = { id: generateId(), toolId, settings: {} };
|
||||||
set({ steps: [...get().steps, step], expandedStepId: step.id });
|
set({ steps: [...get().steps, step], expandedStepId: step.id });
|
||||||
},
|
},
|
||||||
|
|
||||||
removeStep: (id) => {
|
removeStep: (id) => {
|
||||||
const { steps, expandedStepId } = get();
|
const { steps, expandedStepId } = get();
|
||||||
set({
|
set({
|
||||||
steps: steps.filter((s) => s.id !== id),
|
steps: steps.filter((s) => s.id !== id),
|
||||||
expandedStepId: expandedStepId === id ? null : expandedStepId,
|
expandedStepId: expandedStepId === id ? null : expandedStepId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
reorderSteps: (activeId, overId) => {
|
reorderSteps: (activeId, overId) => {
|
||||||
const { steps } = get();
|
const { steps } = get();
|
||||||
const oldIndex = steps.findIndex((s) => s.id === activeId);
|
const oldIndex = steps.findIndex((s) => s.id === activeId);
|
||||||
const newIndex = steps.findIndex((s) => s.id === overId);
|
const newIndex = steps.findIndex((s) => s.id === overId);
|
||||||
if (oldIndex < 0 || newIndex < 0) return;
|
if (oldIndex < 0 || newIndex < 0) return;
|
||||||
const reordered = [...steps];
|
const reordered = [...steps];
|
||||||
const [moved] = reordered.splice(oldIndex, 1);
|
const [moved] = reordered.splice(oldIndex, 1);
|
||||||
reordered.splice(newIndex, 0, moved);
|
reordered.splice(newIndex, 0, moved);
|
||||||
set({ steps: reordered });
|
set({ steps: reordered });
|
||||||
},
|
},
|
||||||
|
|
||||||
updateStepSettings: (id, settings) => {
|
updateStepSettings: (id, settings) => {
|
||||||
set({ steps: get().steps.map((s) => (s.id === id ? { ...s, settings } : s)) });
|
set({ steps: get().steps.map((s) => (s.id === id ? { ...s, settings } : s)) });
|
||||||
},
|
},
|
||||||
|
|
||||||
setExpandedStep: (id) => set({ expandedStepId: id }),
|
setExpandedStep: (id) => set({ expandedStepId: id }),
|
||||||
|
|
||||||
loadSteps: (rawSteps) => {
|
loadSteps: (rawSteps) => {
|
||||||
const steps = rawSteps.map((s) => ({
|
const steps = rawSteps.map((s) => ({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
toolId: s.toolId,
|
toolId: s.toolId,
|
||||||
settings: { ...s.settings },
|
settings: { ...s.settings },
|
||||||
}));
|
}));
|
||||||
set({ steps, expandedStepId: null });
|
set({ steps, expandedStepId: null });
|
||||||
},
|
},
|
||||||
|
|
||||||
setSavedPipelines: (pipelines) => set({ savedPipelines: pipelines }),
|
setSavedPipelines: (pipelines) => set({ savedPipelines: pipelines }),
|
||||||
|
|
||||||
reset: () => set({ steps: [], expandedStepId: null, savedPipelines: [] }),
|
reset: () => set({ steps: [], expandedStepId: null, savedPipelines: [] }),
|
||||||
}));
|
}),
|
||||||
|
{
|
||||||
|
name: "snapotter-pipeline",
|
||||||
|
storage: createJSONStorage(() => sessionStorage),
|
||||||
|
partialize: (state) => ({
|
||||||
|
steps: state.steps,
|
||||||
|
expandedStepId: state.expandedStepId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// apps/web/src/types/editor.ts
|
// apps/web/src/types/editor.ts
|
||||||
|
|
||||||
|
export type EraserMode = "brush" | "block";
|
||||||
|
|
||||||
export type SelectionMode = "new" | "add" | "subtract";
|
export type SelectionMode = "new" | "add" | "subtract";
|
||||||
|
|
||||||
export type StrokeDashStyle = "solid" | "dashed" | "dotted";
|
export type StrokeDashStyle = "solid" | "dashed" | "dotted";
|
||||||
@@ -281,6 +283,8 @@ export interface EditorState {
|
|||||||
brushSize: number;
|
brushSize: number;
|
||||||
brushOpacity: number;
|
brushOpacity: number;
|
||||||
brushHardness: number;
|
brushHardness: number;
|
||||||
|
brushFlow: number;
|
||||||
|
eraserMode: EraserMode;
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
foregroundColor: string;
|
foregroundColor: string;
|
||||||
@@ -300,6 +304,7 @@ export interface EditorState {
|
|||||||
selectionMode: SelectionMode;
|
selectionMode: SelectionMode;
|
||||||
magicWandTolerance: number;
|
magicWandTolerance: number;
|
||||||
magicWandContiguous: boolean;
|
magicWandContiguous: boolean;
|
||||||
|
selectionFeather: number;
|
||||||
|
|
||||||
// Crop
|
// Crop
|
||||||
cropState: CropState | null;
|
cropState: CropState | null;
|
||||||
@@ -425,6 +430,7 @@ export interface EditorState {
|
|||||||
setSelectionMode: (mode: SelectionMode) => void;
|
setSelectionMode: (mode: SelectionMode) => void;
|
||||||
setMagicWandTolerance: (v: number) => void;
|
setMagicWandTolerance: (v: number) => void;
|
||||||
setMagicWandContiguous: (v: boolean) => void;
|
setMagicWandContiguous: (v: boolean) => void;
|
||||||
|
setSelectionFeather: (v: number) => void;
|
||||||
invertSelection: () => void;
|
invertSelection: () => void;
|
||||||
|
|
||||||
// Crop
|
// Crop
|
||||||
@@ -435,6 +441,8 @@ export interface EditorState {
|
|||||||
setBrushSize: (size: number) => void;
|
setBrushSize: (size: number) => void;
|
||||||
setBrushOpacity: (opacity: number) => void;
|
setBrushOpacity: (opacity: number) => void;
|
||||||
setBrushHardness: (hardness: number) => void;
|
setBrushHardness: (hardness: number) => void;
|
||||||
|
setBrushFlow: (flow: number) => void;
|
||||||
|
setEraserMode: (mode: EraserMode) => void;
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
copyObjects: () => void;
|
copyObjects: () => void;
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const ar: TranslationKeys = {
|
|||||||
unexpectedError: "حدث خطأ غير متوقع.",
|
unexpectedError: "حدث خطأ غير متوقع.",
|
||||||
retry: "إعادة المحاولة",
|
retry: "إعادة المحاولة",
|
||||||
privacyPolicy: "سياسة الخصوصية",
|
privacyPolicy: "سياسة الخصوصية",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "الأساسيات",
|
essentials: "الأساسيات",
|
||||||
optimization: "التحسين",
|
optimization: "التحسين",
|
||||||
@@ -1686,6 +1688,9 @@ export const ar: TranslationKeys = {
|
|||||||
githubLink: "مستودع GitHub",
|
githubLink: "مستودع GitHub",
|
||||||
docsLink: "التوثيق",
|
docsLink: "التوثيق",
|
||||||
apiRefLink: "مرجع API (Swagger)",
|
apiRefLink: "مرجع API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1826,7 +1831,9 @@ export const ar: TranslationKeys = {
|
|||||||
pipelineName: "اسم Pipeline",
|
pipelineName: "اسم Pipeline",
|
||||||
pipelineDescription: "الوصف (اختياري)",
|
pipelineDescription: "الوصف (اختياري)",
|
||||||
noStepsPrompt: "أضف خطوات لبناء أتمتتك",
|
noStepsPrompt: "أضف خطوات لبناء أتمتتك",
|
||||||
step: "خطوة",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "خطوة",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "الأدوات",
|
tools: "الأدوات",
|
||||||
@@ -1839,10 +1846,29 @@ export const ar: TranslationKeys = {
|
|||||||
grid: "شبكة",
|
grid: "شبكة",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "الأخيرة",
|
myFiles: "My Files",
|
||||||
uploadTab: "رفع",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "تفاصيل الملف",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "تفاصيل الملف",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "نوع الملف هذا غير مدعوم بهذه الأداة",
|
unsupportedFileType: "نوع الملف هذا غير مدعوم بهذه الأداة",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const de: TranslationKeys = {
|
|||||||
unexpectedError: "Ein unerwarteter Fehler ist aufgetreten.",
|
unexpectedError: "Ein unerwarteter Fehler ist aufgetreten.",
|
||||||
retry: "Erneut versuchen",
|
retry: "Erneut versuchen",
|
||||||
privacyPolicy: "Datenschutzerklaerung",
|
privacyPolicy: "Datenschutzerklaerung",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Grundlagen",
|
essentials: "Grundlagen",
|
||||||
optimization: "Optimierung",
|
optimization: "Optimierung",
|
||||||
@@ -1711,6 +1713,9 @@ export const de: TranslationKeys = {
|
|||||||
githubLink: "GitHub-Repository",
|
githubLink: "GitHub-Repository",
|
||||||
docsLink: "Dokumentation",
|
docsLink: "Dokumentation",
|
||||||
apiRefLink: "API-Referenz (Swagger)",
|
apiRefLink: "API-Referenz (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1855,7 +1860,9 @@ export const de: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline-Name",
|
pipelineName: "Pipeline-Name",
|
||||||
pipelineDescription: "Beschreibung (optional)",
|
pipelineDescription: "Beschreibung (optional)",
|
||||||
noStepsPrompt: "Fuegen Sie Schritte hinzu, um Ihre Automatisierung zu erstellen",
|
noStepsPrompt: "Fuegen Sie Schritte hinzu, um Ihre Automatisierung zu erstellen",
|
||||||
step: "Schritt",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Schritt",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Werkzeuge",
|
tools: "Werkzeuge",
|
||||||
@@ -1868,10 +1875,29 @@ export const de: TranslationKeys = {
|
|||||||
grid: "Raster",
|
grid: "Raster",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Zuletzt",
|
myFiles: "My Files",
|
||||||
uploadTab: "Hochladen",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Dateidetails",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Dateidetails",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Dieser Dateityp wird von diesem Werkzeug nicht unterstuetzt",
|
unsupportedFileType: "Dieser Dateityp wird von diesem Werkzeug nicht unterstuetzt",
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export const en = {
|
|||||||
somethingWentWrong: "Something went wrong",
|
somethingWentWrong: "Something went wrong",
|
||||||
unexpectedError: "An unexpected error occurred.",
|
unexpectedError: "An unexpected error occurred.",
|
||||||
privacyPolicy: "Privacy Policy",
|
privacyPolicy: "Privacy Policy",
|
||||||
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Essentials",
|
essentials: "Essentials",
|
||||||
@@ -1431,7 +1433,8 @@ export const en = {
|
|||||||
newPasswordPlaceholder: "New Password",
|
newPasswordPlaceholder: "New Password",
|
||||||
confirmPasswordPlaceholder: "Confirm New Password",
|
confirmPasswordPlaceholder: "Confirm New Password",
|
||||||
passwordsMismatch: "Passwords do not match",
|
passwordsMismatch: "Passwords do not match",
|
||||||
passwordTooShort: "Password must be at least 4 characters",
|
passwordTooShort:
|
||||||
|
"Password must be at least 8 characters with uppercase, lowercase, and a number",
|
||||||
changeSuccess: "Password changed successfully",
|
changeSuccess: "Password changed successfully",
|
||||||
changeFailed: "Failed to change password",
|
changeFailed: "Failed to change password",
|
||||||
currentPasswordIncorrect: "Current password is incorrect",
|
currentPasswordIncorrect: "Current password is incorrect",
|
||||||
@@ -1645,6 +1648,9 @@ export const en = {
|
|||||||
githubLink: "GitHub Repository",
|
githubLink: "GitHub Repository",
|
||||||
docsLink: "Documentation",
|
docsLink: "Documentation",
|
||||||
apiRefLink: "API Reference (Swagger)",
|
apiRefLink: "API Reference (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1786,6 +1792,8 @@ export const en = {
|
|||||||
pipelineName: "Pipeline Name",
|
pipelineName: "Pipeline Name",
|
||||||
pipelineDescription: "Description (optional)",
|
pipelineDescription: "Description (optional)",
|
||||||
noStepsPrompt: "Add steps to build your automation",
|
noStepsPrompt: "Add steps to build your automation",
|
||||||
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
step: "Step",
|
step: "Step",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
@@ -1799,10 +1807,29 @@ export const en = {
|
|||||||
grid: "Grid",
|
grid: "Grid",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
|
myFiles: "My Files",
|
||||||
recentTab: "Recent",
|
recentTab: "Recent",
|
||||||
uploadTab: "Upload",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsAriaLabel: "File Details",
|
fileDetailsAriaLabel: "File Details",
|
||||||
fileDetailsHeading: "File Details",
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "This file type is not supported by this tool",
|
unsupportedFileType: "This file type is not supported by this tool",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const es: TranslationKeys = {
|
|||||||
unexpectedError: "Ocurrio un error inesperado.",
|
unexpectedError: "Ocurrio un error inesperado.",
|
||||||
retry: "Reintentar",
|
retry: "Reintentar",
|
||||||
privacyPolicy: "Politica de privacidad",
|
privacyPolicy: "Politica de privacidad",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Esenciales",
|
essentials: "Esenciales",
|
||||||
optimization: "Optimizacion",
|
optimization: "Optimizacion",
|
||||||
@@ -1690,6 +1692,9 @@ export const es: TranslationKeys = {
|
|||||||
githubLink: "Repositorio en GitHub",
|
githubLink: "Repositorio en GitHub",
|
||||||
docsLink: "Documentacion",
|
docsLink: "Documentacion",
|
||||||
apiRefLink: "Referencia API (Swagger)",
|
apiRefLink: "Referencia API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1832,7 +1837,9 @@ export const es: TranslationKeys = {
|
|||||||
pipelineName: "Nombre del Pipeline",
|
pipelineName: "Nombre del Pipeline",
|
||||||
pipelineDescription: "Descripcion (opcional)",
|
pipelineDescription: "Descripcion (opcional)",
|
||||||
noStepsPrompt: "Agrega pasos para construir tu automatizacion",
|
noStepsPrompt: "Agrega pasos para construir tu automatizacion",
|
||||||
step: "Paso",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Paso",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Herramientas",
|
tools: "Herramientas",
|
||||||
@@ -1845,10 +1852,29 @@ export const es: TranslationKeys = {
|
|||||||
grid: "Cuadricula",
|
grid: "Cuadricula",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Recientes",
|
myFiles: "My Files",
|
||||||
uploadTab: "Subir",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Detalles del archivo",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Detalles del archivo",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Este tipo de archivo no es compatible con esta herramienta",
|
unsupportedFileType: "Este tipo de archivo no es compatible con esta herramienta",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const fr: TranslationKeys = {
|
|||||||
unexpectedError: "Une erreur inattendue est survenue.",
|
unexpectedError: "Une erreur inattendue est survenue.",
|
||||||
retry: "Réessayer",
|
retry: "Réessayer",
|
||||||
privacyPolicy: "Politique de confidentialite",
|
privacyPolicy: "Politique de confidentialite",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Essentiels",
|
essentials: "Essentiels",
|
||||||
optimization: "Optimisation",
|
optimization: "Optimisation",
|
||||||
@@ -1709,6 +1711,9 @@ export const fr: TranslationKeys = {
|
|||||||
githubLink: "Depot GitHub",
|
githubLink: "Depot GitHub",
|
||||||
docsLink: "Documentation",
|
docsLink: "Documentation",
|
||||||
apiRefLink: "Reference API (Swagger)",
|
apiRefLink: "Reference API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1853,7 +1858,9 @@ export const fr: TranslationKeys = {
|
|||||||
pipelineName: "Nom du Pipeline",
|
pipelineName: "Nom du Pipeline",
|
||||||
pipelineDescription: "Description (optionnel)",
|
pipelineDescription: "Description (optionnel)",
|
||||||
noStepsPrompt: "Ajoutez des etapes pour construire votre automatisation",
|
noStepsPrompt: "Ajoutez des etapes pour construire votre automatisation",
|
||||||
step: "Etape",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Etape",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Outils",
|
tools: "Outils",
|
||||||
@@ -1866,10 +1873,29 @@ export const fr: TranslationKeys = {
|
|||||||
grid: "Grille",
|
grid: "Grille",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Recents",
|
myFiles: "My Files",
|
||||||
uploadTab: "Importer",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Details du fichier",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Details du fichier",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Ce type de fichier n'est pas pris en charge par cet outil",
|
unsupportedFileType: "Ce type de fichier n'est pas pris en charge par cet outil",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const hi: TranslationKeys = {
|
|||||||
unexpectedError: "एक अप्रत्याशित त्रुटि हुई।",
|
unexpectedError: "एक अप्रत्याशित त्रुटि हुई।",
|
||||||
retry: "पुनः प्रयास करें",
|
retry: "पुनः प्रयास करें",
|
||||||
privacyPolicy: "गोपनीयता नीति",
|
privacyPolicy: "गोपनीयता नीति",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "आवश्यक टूल्स",
|
essentials: "आवश्यक टूल्स",
|
||||||
optimization: "ऑप्टिमाइज़ेशन",
|
optimization: "ऑप्टिमाइज़ेशन",
|
||||||
@@ -1682,6 +1684,9 @@ export const hi: TranslationKeys = {
|
|||||||
githubLink: "GitHub रिपॉज़िटरी",
|
githubLink: "GitHub रिपॉज़िटरी",
|
||||||
docsLink: "डॉक्यूमेंटेशन",
|
docsLink: "डॉक्यूमेंटेशन",
|
||||||
apiRefLink: "API रेफरेंस (Swagger)",
|
apiRefLink: "API रेफरेंस (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1823,7 +1828,9 @@ export const hi: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline का नाम",
|
pipelineName: "Pipeline का नाम",
|
||||||
pipelineDescription: "विवरण (वैकल्पिक)",
|
pipelineDescription: "विवरण (वैकल्पिक)",
|
||||||
noStepsPrompt: "अपना ऑटोमेशन बनाने के लिए स्टेप्स जोड़ें",
|
noStepsPrompt: "अपना ऑटोमेशन बनाने के लिए स्टेप्स जोड़ें",
|
||||||
step: "स्टेप",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "स्टेप",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "टूल्स",
|
tools: "टूल्स",
|
||||||
@@ -1836,10 +1843,29 @@ export const hi: TranslationKeys = {
|
|||||||
grid: "ग्रिड",
|
grid: "ग्रिड",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "हाल के",
|
myFiles: "My Files",
|
||||||
uploadTab: "अपलोड",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "फाइल विवरण",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "फाइल विवरण",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "इस फाइल प्रकार को यह टूल सपोर्ट नहीं करता",
|
unsupportedFileType: "इस फाइल प्रकार को यह टूल सपोर्ट नहीं करता",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const id: TranslationKeys = {
|
|||||||
unexpectedError: "Terjadi kesalahan yang tidak terduga.",
|
unexpectedError: "Terjadi kesalahan yang tidak terduga.",
|
||||||
retry: "Coba lagi",
|
retry: "Coba lagi",
|
||||||
privacyPolicy: "Kebijakan Privasi",
|
privacyPolicy: "Kebijakan Privasi",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Dasar",
|
essentials: "Dasar",
|
||||||
optimization: "Optimasi",
|
optimization: "Optimasi",
|
||||||
@@ -1698,6 +1700,9 @@ export const id: TranslationKeys = {
|
|||||||
githubLink: "Repositori GitHub",
|
githubLink: "Repositori GitHub",
|
||||||
docsLink: "Dokumentasi",
|
docsLink: "Dokumentasi",
|
||||||
apiRefLink: "Referensi API (Swagger)",
|
apiRefLink: "Referensi API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1840,7 +1845,9 @@ export const id: TranslationKeys = {
|
|||||||
pipelineName: "Nama Pipeline",
|
pipelineName: "Nama Pipeline",
|
||||||
pipelineDescription: "Deskripsi (opsional)",
|
pipelineDescription: "Deskripsi (opsional)",
|
||||||
noStepsPrompt: "Tambahkan langkah untuk membangun otomasi Anda",
|
noStepsPrompt: "Tambahkan langkah untuk membangun otomasi Anda",
|
||||||
step: "Langkah",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Langkah",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Alat",
|
tools: "Alat",
|
||||||
@@ -1853,10 +1860,29 @@ export const id: TranslationKeys = {
|
|||||||
grid: "Grid",
|
grid: "Grid",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Terbaru",
|
myFiles: "My Files",
|
||||||
uploadTab: "Unggah",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Detail File",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Detail File",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Jenis file ini tidak didukung oleh alat ini",
|
unsupportedFileType: "Jenis file ini tidak didukung oleh alat ini",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const it: TranslationKeys = {
|
|||||||
unexpectedError: "Si e verificato un errore imprevisto.",
|
unexpectedError: "Si e verificato un errore imprevisto.",
|
||||||
retry: "Riprova",
|
retry: "Riprova",
|
||||||
privacyPolicy: "Informativa sulla privacy",
|
privacyPolicy: "Informativa sulla privacy",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Essenziali",
|
essentials: "Essenziali",
|
||||||
optimization: "Ottimizzazione",
|
optimization: "Ottimizzazione",
|
||||||
@@ -1704,6 +1706,9 @@ export const it: TranslationKeys = {
|
|||||||
githubLink: "Repository GitHub",
|
githubLink: "Repository GitHub",
|
||||||
docsLink: "Documentazione",
|
docsLink: "Documentazione",
|
||||||
apiRefLink: "Riferimento API (Swagger)",
|
apiRefLink: "Riferimento API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1847,7 +1852,9 @@ export const it: TranslationKeys = {
|
|||||||
pipelineName: "Nome del Pipeline",
|
pipelineName: "Nome del Pipeline",
|
||||||
pipelineDescription: "Descrizione (opzionale)",
|
pipelineDescription: "Descrizione (opzionale)",
|
||||||
noStepsPrompt: "Aggiungi passaggi per costruire la tua automazione",
|
noStepsPrompt: "Aggiungi passaggi per costruire la tua automazione",
|
||||||
step: "Passaggio",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Passaggio",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Strumenti",
|
tools: "Strumenti",
|
||||||
@@ -1860,10 +1867,29 @@ export const it: TranslationKeys = {
|
|||||||
grid: "Griglia",
|
grid: "Griglia",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Recenti",
|
myFiles: "My Files",
|
||||||
uploadTab: "Carica",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Dettagli file",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Dettagli file",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Questo tipo di file non e supportato da questo strumento",
|
unsupportedFileType: "Questo tipo di file non e supportato da questo strumento",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const ja: TranslationKeys = {
|
|||||||
unexpectedError: "予期しないエラーが発生しました。",
|
unexpectedError: "予期しないエラーが発生しました。",
|
||||||
retry: "再試行",
|
retry: "再試行",
|
||||||
privacyPolicy: "プライバシーポリシー",
|
privacyPolicy: "プライバシーポリシー",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "基本ツール",
|
essentials: "基本ツール",
|
||||||
optimization: "最適化",
|
optimization: "最適化",
|
||||||
@@ -1655,6 +1657,9 @@ export const ja: TranslationKeys = {
|
|||||||
githubLink: "GitHubリポジトリ",
|
githubLink: "GitHubリポジトリ",
|
||||||
docsLink: "ドキュメント",
|
docsLink: "ドキュメント",
|
||||||
apiRefLink: "APIリファレンス(Swagger)",
|
apiRefLink: "APIリファレンス(Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1796,7 +1801,9 @@ export const ja: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline名",
|
pipelineName: "Pipeline名",
|
||||||
pipelineDescription: "説明(任意)",
|
pipelineDescription: "説明(任意)",
|
||||||
noStepsPrompt: "ステップを追加して自動化を構築",
|
noStepsPrompt: "ステップを追加して自動化を構築",
|
||||||
step: "ステップ",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "ステップ",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "ツール",
|
tools: "ツール",
|
||||||
@@ -1809,10 +1816,29 @@ export const ja: TranslationKeys = {
|
|||||||
grid: "グリッド",
|
grid: "グリッド",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "最近",
|
myFiles: "My Files",
|
||||||
uploadTab: "アップロード",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "ファイル詳細",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "ファイル詳細",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "このツールではサポートされていないファイルタイプです",
|
unsupportedFileType: "このツールではサポートされていないファイルタイプです",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const ko: TranslationKeys = {
|
|||||||
unexpectedError: "예기치 않은 오류가 발생했습니다.",
|
unexpectedError: "예기치 않은 오류가 발생했습니다.",
|
||||||
retry: "재시도",
|
retry: "재시도",
|
||||||
privacyPolicy: "개인정보 처리방침",
|
privacyPolicy: "개인정보 처리방침",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "기본 도구",
|
essentials: "기본 도구",
|
||||||
optimization: "최적화",
|
optimization: "최적화",
|
||||||
@@ -1640,6 +1642,9 @@ export const ko: TranslationKeys = {
|
|||||||
githubLink: "GitHub 저장소",
|
githubLink: "GitHub 저장소",
|
||||||
docsLink: "문서",
|
docsLink: "문서",
|
||||||
apiRefLink: "API 레퍼런스 (Swagger)",
|
apiRefLink: "API 레퍼런스 (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1781,7 +1786,9 @@ export const ko: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline 이름",
|
pipelineName: "Pipeline 이름",
|
||||||
pipelineDescription: "설명 (선택)",
|
pipelineDescription: "설명 (선택)",
|
||||||
noStepsPrompt: "단계를 추가하여 자동화를 구성하세요",
|
noStepsPrompt: "단계를 추가하여 자동화를 구성하세요",
|
||||||
step: "단계",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "단계",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "도구",
|
tools: "도구",
|
||||||
@@ -1794,10 +1801,29 @@ export const ko: TranslationKeys = {
|
|||||||
grid: "그리드",
|
grid: "그리드",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "최근",
|
myFiles: "My Files",
|
||||||
uploadTab: "업로드",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "파일 상세",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "파일 상세",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "이 도구에서 지원하지 않는 파일 형식입니다",
|
unsupportedFileType: "이 도구에서 지원하지 않는 파일 형식입니다",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const nl: TranslationKeys = {
|
|||||||
unexpectedError: "Er is een onverwachte fout opgetreden.",
|
unexpectedError: "Er is een onverwachte fout opgetreden.",
|
||||||
retry: "Opnieuw proberen",
|
retry: "Opnieuw proberen",
|
||||||
privacyPolicy: "Privacybeleid",
|
privacyPolicy: "Privacybeleid",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Basistools",
|
essentials: "Basistools",
|
||||||
optimization: "Optimalisatie",
|
optimization: "Optimalisatie",
|
||||||
@@ -1701,6 +1703,9 @@ export const nl: TranslationKeys = {
|
|||||||
githubLink: "GitHub-repository",
|
githubLink: "GitHub-repository",
|
||||||
docsLink: "Documentatie",
|
docsLink: "Documentatie",
|
||||||
apiRefLink: "API-referentie (Swagger)",
|
apiRefLink: "API-referentie (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1843,7 +1848,9 @@ export const nl: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline-naam",
|
pipelineName: "Pipeline-naam",
|
||||||
pipelineDescription: "Beschrijving (optioneel)",
|
pipelineDescription: "Beschrijving (optioneel)",
|
||||||
noStepsPrompt: "Voeg stappen toe om je automatisering te bouwen",
|
noStepsPrompt: "Voeg stappen toe om je automatisering te bouwen",
|
||||||
step: "Stap",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Stap",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Tools",
|
tools: "Tools",
|
||||||
@@ -1856,10 +1863,29 @@ export const nl: TranslationKeys = {
|
|||||||
grid: "Raster",
|
grid: "Raster",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
|
myFiles: "My Files",
|
||||||
recentTab: "Recent",
|
recentTab: "Recent",
|
||||||
uploadTab: "Uploaden",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsAriaLabel: "Bestandsdetails",
|
fileDetailsAriaLabel: "File Details",
|
||||||
fileDetailsHeading: "Bestandsdetails",
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Dit bestandstype wordt niet ondersteund door deze tool",
|
unsupportedFileType: "Dit bestandstype wordt niet ondersteund door deze tool",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const pl: TranslationKeys = {
|
|||||||
unexpectedError: "Wystąpił nieoczekiwany błąd.",
|
unexpectedError: "Wystąpił nieoczekiwany błąd.",
|
||||||
retry: "Ponów",
|
retry: "Ponów",
|
||||||
privacyPolicy: "Polityka prywatności",
|
privacyPolicy: "Polityka prywatności",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Podstawowe",
|
essentials: "Podstawowe",
|
||||||
optimization: "Optymalizacja",
|
optimization: "Optymalizacja",
|
||||||
@@ -1707,6 +1709,9 @@ export const pl: TranslationKeys = {
|
|||||||
githubLink: "Repozytorium GitHub",
|
githubLink: "Repozytorium GitHub",
|
||||||
docsLink: "Dokumentacja",
|
docsLink: "Dokumentacja",
|
||||||
apiRefLink: "Dokumentacja API (Swagger)",
|
apiRefLink: "Dokumentacja API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1850,7 +1855,9 @@ export const pl: TranslationKeys = {
|
|||||||
pipelineName: "Nazwa Pipeline",
|
pipelineName: "Nazwa Pipeline",
|
||||||
pipelineDescription: "Opis (opcjonalnie)",
|
pipelineDescription: "Opis (opcjonalnie)",
|
||||||
noStepsPrompt: "Dodaj kroki, aby zbudować automatyzację",
|
noStepsPrompt: "Dodaj kroki, aby zbudować automatyzację",
|
||||||
step: "Krok",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Krok",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Narzędzia",
|
tools: "Narzędzia",
|
||||||
@@ -1863,10 +1870,29 @@ export const pl: TranslationKeys = {
|
|||||||
grid: "Siatka",
|
grid: "Siatka",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Ostatnie",
|
myFiles: "My Files",
|
||||||
uploadTab: "Przesyłanie",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Szczegóły pliku",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Szczegóły pliku",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Ten typ pliku nie jest obsługiwany przez to narzędzie",
|
unsupportedFileType: "Ten typ pliku nie jest obsługiwany przez to narzędzie",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const ptBR: TranslationKeys = {
|
|||||||
unexpectedError: "Ocorreu um erro inesperado.",
|
unexpectedError: "Ocorreu um erro inesperado.",
|
||||||
retry: "Tentar novamente",
|
retry: "Tentar novamente",
|
||||||
privacyPolicy: "Politica de privacidade",
|
privacyPolicy: "Politica de privacidade",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Essenciais",
|
essentials: "Essenciais",
|
||||||
optimization: "Otimizacao",
|
optimization: "Otimizacao",
|
||||||
@@ -1700,6 +1702,9 @@ export const ptBR: TranslationKeys = {
|
|||||||
githubLink: "Repositorio no GitHub",
|
githubLink: "Repositorio no GitHub",
|
||||||
docsLink: "Documentacao",
|
docsLink: "Documentacao",
|
||||||
apiRefLink: "Referencia da API (Swagger)",
|
apiRefLink: "Referencia da API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1843,7 +1848,9 @@ export const ptBR: TranslationKeys = {
|
|||||||
pipelineName: "Nome do Pipeline",
|
pipelineName: "Nome do Pipeline",
|
||||||
pipelineDescription: "Descricao (opcional)",
|
pipelineDescription: "Descricao (opcional)",
|
||||||
noStepsPrompt: "Adicione passos para construir sua automacao",
|
noStepsPrompt: "Adicione passos para construir sua automacao",
|
||||||
step: "Passo",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Passo",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Ferramentas",
|
tools: "Ferramentas",
|
||||||
@@ -1856,10 +1863,29 @@ export const ptBR: TranslationKeys = {
|
|||||||
grid: "Grade",
|
grid: "Grade",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Recentes",
|
myFiles: "My Files",
|
||||||
uploadTab: "Enviar",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Detalhes do arquivo",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Detalhes do arquivo",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Este tipo de arquivo nao e suportado por esta ferramenta",
|
unsupportedFileType: "Este tipo de arquivo nao e suportado por esta ferramenta",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const ru: TranslationKeys = {
|
|||||||
unexpectedError: "Произошла непредвиденная ошибка.",
|
unexpectedError: "Произошла непредвиденная ошибка.",
|
||||||
retry: "Повторить",
|
retry: "Повторить",
|
||||||
privacyPolicy: "Политика конфиденциальности",
|
privacyPolicy: "Политика конфиденциальности",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Основные",
|
essentials: "Основные",
|
||||||
optimization: "Оптимизация",
|
optimization: "Оптимизация",
|
||||||
@@ -1700,6 +1702,9 @@ export const ru: TranslationKeys = {
|
|||||||
githubLink: "Репозиторий GitHub",
|
githubLink: "Репозиторий GitHub",
|
||||||
docsLink: "Документация",
|
docsLink: "Документация",
|
||||||
apiRefLink: "Справочник API (Swagger)",
|
apiRefLink: "Справочник API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1842,7 +1847,9 @@ export const ru: TranslationKeys = {
|
|||||||
pipelineName: "Название Pipeline",
|
pipelineName: "Название Pipeline",
|
||||||
pipelineDescription: "Описание (необязательно)",
|
pipelineDescription: "Описание (необязательно)",
|
||||||
noStepsPrompt: "Добавьте шаги для построения автоматизации",
|
noStepsPrompt: "Добавьте шаги для построения автоматизации",
|
||||||
step: "Шаг",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Шаг",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Инструменты",
|
tools: "Инструменты",
|
||||||
@@ -1855,10 +1862,29 @@ export const ru: TranslationKeys = {
|
|||||||
grid: "Сетка",
|
grid: "Сетка",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Недавние",
|
myFiles: "My Files",
|
||||||
uploadTab: "Загрузка",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Сведения о файле",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Сведения о файле",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Этот тип файла не поддерживается данным инструментом",
|
unsupportedFileType: "Этот тип файла не поддерживается данным инструментом",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const sv: TranslationKeys = {
|
|||||||
unexpectedError: "Ett ovantat fel uppstod.",
|
unexpectedError: "Ett ovantat fel uppstod.",
|
||||||
retry: "Försök igen",
|
retry: "Försök igen",
|
||||||
privacyPolicy: "Integritetspolicy",
|
privacyPolicy: "Integritetspolicy",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Grundlaggande",
|
essentials: "Grundlaggande",
|
||||||
optimization: "Optimering",
|
optimization: "Optimering",
|
||||||
@@ -1696,6 +1698,9 @@ export const sv: TranslationKeys = {
|
|||||||
githubLink: "GitHub-arkiv",
|
githubLink: "GitHub-arkiv",
|
||||||
docsLink: "Dokumentation",
|
docsLink: "Dokumentation",
|
||||||
apiRefLink: "API-referens (Swagger)",
|
apiRefLink: "API-referens (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1837,7 +1842,9 @@ export const sv: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline-namn",
|
pipelineName: "Pipeline-namn",
|
||||||
pipelineDescription: "Beskrivning (valfritt)",
|
pipelineDescription: "Beskrivning (valfritt)",
|
||||||
noStepsPrompt: "Lagg till steg for att bygga din automatisering",
|
noStepsPrompt: "Lagg till steg for att bygga din automatisering",
|
||||||
step: "Steg",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Steg",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Verktyg",
|
tools: "Verktyg",
|
||||||
@@ -1850,10 +1857,29 @@ export const sv: TranslationKeys = {
|
|||||||
grid: "Rutnat",
|
grid: "Rutnat",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Senaste",
|
myFiles: "My Files",
|
||||||
uploadTab: "Ladda upp",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Fildetaljer",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Fildetaljer",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Denna filtyp stods inte av detta verktyg",
|
unsupportedFileType: "Denna filtyp stods inte av detta verktyg",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const th: TranslationKeys = {
|
|||||||
unexpectedError: "เกิดข้อผิดพลาดที่ไม่คาดคิด",
|
unexpectedError: "เกิดข้อผิดพลาดที่ไม่คาดคิด",
|
||||||
retry: "ลองใหม่",
|
retry: "ลองใหม่",
|
||||||
privacyPolicy: "นโยบายความเป็นส่วนตัว",
|
privacyPolicy: "นโยบายความเป็นส่วนตัว",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "พื้นฐาน",
|
essentials: "พื้นฐาน",
|
||||||
optimization: "การเพิ่มประสิทธิภาพ",
|
optimization: "การเพิ่มประสิทธิภาพ",
|
||||||
@@ -1674,6 +1676,9 @@ export const th: TranslationKeys = {
|
|||||||
githubLink: "คลังเก็บโค้ด GitHub",
|
githubLink: "คลังเก็บโค้ด GitHub",
|
||||||
docsLink: "เอกสารประกอบ",
|
docsLink: "เอกสารประกอบ",
|
||||||
apiRefLink: "อ้างอิง API (Swagger)",
|
apiRefLink: "อ้างอิง API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1814,7 +1819,9 @@ export const th: TranslationKeys = {
|
|||||||
pipelineName: "ชื่อ Pipeline",
|
pipelineName: "ชื่อ Pipeline",
|
||||||
pipelineDescription: "คำอธิบาย (ไม่บังคับ)",
|
pipelineDescription: "คำอธิบาย (ไม่บังคับ)",
|
||||||
noStepsPrompt: "เพิ่มขั้นตอนเพื่อสร้างการทำงานอัตโนมัติ",
|
noStepsPrompt: "เพิ่มขั้นตอนเพื่อสร้างการทำงานอัตโนมัติ",
|
||||||
step: "ขั้นตอน",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "ขั้นตอน",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "เครื่องมือ",
|
tools: "เครื่องมือ",
|
||||||
@@ -1827,10 +1834,29 @@ export const th: TranslationKeys = {
|
|||||||
grid: "กริด",
|
grid: "กริด",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "ล่าสุด",
|
myFiles: "My Files",
|
||||||
uploadTab: "อัปโหลด",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "รายละเอียดไฟล์",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "รายละเอียดไฟล์",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "ไฟล์ประเภทนี้ไม่รองรับโดยเครื่องมือนี้",
|
unsupportedFileType: "ไฟล์ประเภทนี้ไม่รองรับโดยเครื่องมือนี้",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const tr: TranslationKeys = {
|
|||||||
unexpectedError: "Beklenmeyen bir hata oluştu.",
|
unexpectedError: "Beklenmeyen bir hata oluştu.",
|
||||||
retry: "Yeniden dene",
|
retry: "Yeniden dene",
|
||||||
privacyPolicy: "Gizlilik Politikası",
|
privacyPolicy: "Gizlilik Politikası",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Temel Araçlar",
|
essentials: "Temel Araçlar",
|
||||||
optimization: "Optimizasyon",
|
optimization: "Optimizasyon",
|
||||||
@@ -1704,6 +1706,9 @@ export const tr: TranslationKeys = {
|
|||||||
githubLink: "GitHub Deposu",
|
githubLink: "GitHub Deposu",
|
||||||
docsLink: "Dokümantasyon",
|
docsLink: "Dokümantasyon",
|
||||||
apiRefLink: "API Referansı (Swagger)",
|
apiRefLink: "API Referansı (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1847,7 +1852,9 @@ export const tr: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline Adı",
|
pipelineName: "Pipeline Adı",
|
||||||
pipelineDescription: "Açıklama (isteğe bağlı)",
|
pipelineDescription: "Açıklama (isteğe bağlı)",
|
||||||
noStepsPrompt: "Otomasyonunuzu oluşturmak için adımlar ekleyin",
|
noStepsPrompt: "Otomasyonunuzu oluşturmak için adımlar ekleyin",
|
||||||
step: "Adım",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Adım",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Araçlar",
|
tools: "Araçlar",
|
||||||
@@ -1860,10 +1867,29 @@ export const tr: TranslationKeys = {
|
|||||||
grid: "Izgara",
|
grid: "Izgara",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Son Kullanılanlar",
|
myFiles: "My Files",
|
||||||
uploadTab: "Yükle",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Dosya Ayrıntıları",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Dosya Ayrıntıları",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Bu dosya türü bu araç tarafından desteklenmiyor",
|
unsupportedFileType: "Bu dosya türü bu araç tarafından desteklenmiyor",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const uk: TranslationKeys = {
|
|||||||
unexpectedError: "Сталася неочікувана помилка.",
|
unexpectedError: "Сталася неочікувана помилка.",
|
||||||
retry: "Повторити",
|
retry: "Повторити",
|
||||||
privacyPolicy: "Політика конфіденційності",
|
privacyPolicy: "Політика конфіденційності",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Основні",
|
essentials: "Основні",
|
||||||
optimization: "Оптимізація",
|
optimization: "Оптимізація",
|
||||||
@@ -1700,6 +1702,9 @@ export const uk: TranslationKeys = {
|
|||||||
githubLink: "Репозиторій GitHub",
|
githubLink: "Репозиторій GitHub",
|
||||||
docsLink: "Документація",
|
docsLink: "Документація",
|
||||||
apiRefLink: "Довідник API (Swagger)",
|
apiRefLink: "Довідник API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1843,7 +1848,9 @@ export const uk: TranslationKeys = {
|
|||||||
pipelineName: "Назва Pipeline",
|
pipelineName: "Назва Pipeline",
|
||||||
pipelineDescription: "Опис (необов'язково)",
|
pipelineDescription: "Опис (необов'язково)",
|
||||||
noStepsPrompt: "Додайте кроки для побудови автоматизації",
|
noStepsPrompt: "Додайте кроки для побудови автоматизації",
|
||||||
step: "Крок",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Крок",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Інструменти",
|
tools: "Інструменти",
|
||||||
@@ -1856,10 +1863,29 @@ export const uk: TranslationKeys = {
|
|||||||
grid: "Сітка",
|
grid: "Сітка",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Нещодавні",
|
myFiles: "My Files",
|
||||||
uploadTab: "Завантаження",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Відомості про файл",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Відомості про файл",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Цей тип файлу не підтримується цим інструментом",
|
unsupportedFileType: "Цей тип файлу не підтримується цим інструментом",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const vi: TranslationKeys = {
|
|||||||
unexpectedError: "Đã xảy ra lỗi không mong muốn.",
|
unexpectedError: "Đã xảy ra lỗi không mong muốn.",
|
||||||
retry: "Thử lại",
|
retry: "Thử lại",
|
||||||
privacyPolicy: "Chính sách bảo mật",
|
privacyPolicy: "Chính sách bảo mật",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "Cơ bản",
|
essentials: "Cơ bản",
|
||||||
optimization: "Tối ưu hóa",
|
optimization: "Tối ưu hóa",
|
||||||
@@ -1696,6 +1698,9 @@ export const vi: TranslationKeys = {
|
|||||||
githubLink: "Kho mã nguồn GitHub",
|
githubLink: "Kho mã nguồn GitHub",
|
||||||
docsLink: "Tài liệu",
|
docsLink: "Tài liệu",
|
||||||
apiRefLink: "Tham chiếu API (Swagger)",
|
apiRefLink: "Tham chiếu API (Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1837,7 +1842,9 @@ export const vi: TranslationKeys = {
|
|||||||
pipelineName: "Tên Pipeline",
|
pipelineName: "Tên Pipeline",
|
||||||
pipelineDescription: "Mô tả (tùy chọn)",
|
pipelineDescription: "Mô tả (tùy chọn)",
|
||||||
noStepsPrompt: "Thêm các bước để xây dựng tự động hóa",
|
noStepsPrompt: "Thêm các bước để xây dựng tự động hóa",
|
||||||
step: "Bước",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "Bước",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "Công cụ",
|
tools: "Công cụ",
|
||||||
@@ -1850,10 +1857,29 @@ export const vi: TranslationKeys = {
|
|||||||
grid: "Lưới",
|
grid: "Lưới",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "Gần đây",
|
myFiles: "My Files",
|
||||||
uploadTab: "Tải lên",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "Chi tiết tệp",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "Chi tiết tệp",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "Loại tệp này không được công cụ này hỗ trợ",
|
unsupportedFileType: "Loại tệp này không được công cụ này hỗ trợ",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const zhCN: TranslationKeys = {
|
|||||||
unexpectedError: "发生了意外错误。",
|
unexpectedError: "发生了意外错误。",
|
||||||
retry: "重试",
|
retry: "重试",
|
||||||
privacyPolicy: "隐私政策",
|
privacyPolicy: "隐私政策",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "基础工具",
|
essentials: "基础工具",
|
||||||
optimization: "优化",
|
optimization: "优化",
|
||||||
@@ -1626,6 +1628,9 @@ export const zhCN: TranslationKeys = {
|
|||||||
githubLink: "GitHub 仓库",
|
githubLink: "GitHub 仓库",
|
||||||
docsLink: "文档",
|
docsLink: "文档",
|
||||||
apiRefLink: "API 参考(Swagger)",
|
apiRefLink: "API 参考(Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1765,7 +1770,9 @@ export const zhCN: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline 名称",
|
pipelineName: "Pipeline 名称",
|
||||||
pipelineDescription: "描述(可选)",
|
pipelineDescription: "描述(可选)",
|
||||||
noStepsPrompt: "添加步骤来构建自动化流程",
|
noStepsPrompt: "添加步骤来构建自动化流程",
|
||||||
step: "步骤",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "步骤",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "工具",
|
tools: "工具",
|
||||||
@@ -1778,10 +1785,29 @@ export const zhCN: TranslationKeys = {
|
|||||||
grid: "网格",
|
grid: "网格",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "最近",
|
myFiles: "My Files",
|
||||||
uploadTab: "上传",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "文件详情",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "文件详情",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "此工具不支持该文件类型",
|
unsupportedFileType: "此工具不支持该文件类型",
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const zhTW: TranslationKeys = {
|
|||||||
unexpectedError: "發生了非預期的錯誤。",
|
unexpectedError: "發生了非預期的錯誤。",
|
||||||
retry: "重試",
|
retry: "重試",
|
||||||
privacyPolicy: "隱私權政策",
|
privacyPolicy: "隱私權政策",
|
||||||
},
|
pageNotFound: "Page not found",
|
||||||
|
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
essentials: "基本工具",
|
essentials: "基本工具",
|
||||||
optimization: "最佳化",
|
optimization: "最佳化",
|
||||||
@@ -1624,6 +1626,9 @@ export const zhTW: TranslationKeys = {
|
|||||||
githubLink: "GitHub儲存庫",
|
githubLink: "GitHub儲存庫",
|
||||||
docsLink: "說明文件",
|
docsLink: "說明文件",
|
||||||
apiRefLink: "API參考(Swagger)",
|
apiRefLink: "API參考(Swagger)",
|
||||||
|
licenseLabel: "License:",
|
||||||
|
licenseDescription:
|
||||||
|
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
@@ -1763,7 +1768,9 @@ export const zhTW: TranslationKeys = {
|
|||||||
pipelineName: "Pipeline名稱",
|
pipelineName: "Pipeline名稱",
|
||||||
pipelineDescription: "描述(選填)",
|
pipelineDescription: "描述(選填)",
|
||||||
noStepsPrompt: "加入步驟來建構自動化",
|
noStepsPrompt: "加入步驟來建構自動化",
|
||||||
step: "步驟",
|
noStepsHeading: "No steps yet",
|
||||||
|
searchToolsPlaceholder: "Search tools...",
|
||||||
|
step: "步驟",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
tools: "工具",
|
tools: "工具",
|
||||||
@@ -1776,10 +1783,29 @@ export const zhTW: TranslationKeys = {
|
|||||||
grid: "格線",
|
grid: "格線",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
recentTab: "最近",
|
myFiles: "My Files",
|
||||||
uploadTab: "上傳",
|
recentTab: "Recent",
|
||||||
fileDetailsAriaLabel: "檔案詳情",
|
uploadTab: "Upload Files",
|
||||||
fileDetailsHeading: "檔案詳情",
|
fileDetailsAriaLabel: "File Details",
|
||||||
|
fileDetailsHeading: "File Details",
|
||||||
|
searchPlaceholder: "Search files...",
|
||||||
|
selectedCount: "{count} selected",
|
||||||
|
fileCount: "{count} files",
|
||||||
|
fileCountSingular: "{count} file",
|
||||||
|
noFilesFound: "No files found",
|
||||||
|
selectFilePrompt: "Select a file to view details",
|
||||||
|
openFile: "Open File",
|
||||||
|
openInPipeline: "Open in Pipeline",
|
||||||
|
name: "Name",
|
||||||
|
format: "Format",
|
||||||
|
size: "Size",
|
||||||
|
dimensions: "Dimensions",
|
||||||
|
version: "Version",
|
||||||
|
toolsUsed: "Tools Used",
|
||||||
|
none: "None",
|
||||||
|
delete: "Delete",
|
||||||
|
pipeline: "Pipeline",
|
||||||
|
download: "Download",
|
||||||
},
|
},
|
||||||
dropzone: {
|
dropzone: {
|
||||||
unsupportedFileType: "此工具不支援該檔案類型",
|
unsupportedFileType: "此工具不支援該檔案類型",
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { expect, test } from "./helpers";
|
||||||
|
|
||||||
|
test.describe("QA fixes verification", () => {
|
||||||
|
test("invalid tool slug shows 404 page with Go Home link", async ({ loggedInPage: page }) => {
|
||||||
|
await page.goto("/nonexistent-tool-slug-xyz");
|
||||||
|
await expect(page.locator("text=Tool not found").or(page.locator("text=404"))).toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
const goHome = page.getByRole("link", { name: /go home/i });
|
||||||
|
await expect(goHome).toBeVisible();
|
||||||
|
await goHome.click();
|
||||||
|
await expect(page).toHaveURL("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multi-segment invalid URL shows 404 page", async ({ loggedInPage: page }) => {
|
||||||
|
await page.goto("/some/deep/nested/path");
|
||||||
|
await expect(page.locator("text=404")).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(page.getByRole("link", { name: /go home/i })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("/tools/:toolId redirects to /:toolId", async ({ loggedInPage: page }) => {
|
||||||
|
await page.goto("/tools/resize");
|
||||||
|
await page.waitForURL("**/resize", { timeout: 5_000 });
|
||||||
|
await expect(page).toHaveURL(/\/resize$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("confirm password field has visibility toggle", async ({ loggedInPage: page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
// Open settings
|
||||||
|
const settingsBtn = page.locator('[class*="sidebar"]').getByRole("button").last();
|
||||||
|
await settingsBtn.click().catch(() => {});
|
||||||
|
// Try to navigate to Security tab
|
||||||
|
const securityTab = page.getByText("Security");
|
||||||
|
if (await securityTab.isVisible({ timeout: 3_000 }).catch(() => false)) {
|
||||||
|
await securityTab.click();
|
||||||
|
// Find all password eye toggle buttons
|
||||||
|
const eyeButtons = page.locator('button[tabindex="-1"]');
|
||||||
|
const count = await eyeButtons.count();
|
||||||
|
// Should have at least 3 eye buttons (current, new, confirm)
|
||||||
|
expect(count).toBeGreaterThanOrEqual(3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("pipeline steps survive navigation", async ({ loggedInPage: page }) => {
|
||||||
|
await page.goto("/automate");
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Add a step by clicking a tool in the palette
|
||||||
|
const resizeTool = page.locator("text=Resize").first();
|
||||||
|
if (await resizeTool.isVisible({ timeout: 5_000 }).catch(() => false)) {
|
||||||
|
await resizeTool.click();
|
||||||
|
// Wait for the step to appear
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Navigate away
|
||||||
|
await page.goto("/");
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Navigate back
|
||||||
|
await page.goto("/automate");
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Steps should still be there (persisted in sessionStorage)
|
||||||
|
const removeButtons = page.locator('button[title="Remove"], button:has-text("Remove")');
|
||||||
|
const count = await removeButtons.count();
|
||||||
|
expect(count).toBeGreaterThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("export dialog has filename input", async ({ loggedInPage: page }) => {
|
||||||
|
await page.goto("/editor");
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Try to open export dialog via keyboard
|
||||||
|
await page.keyboard.press("Control+Shift+S");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const filenameInput = page.locator('input[placeholder="export"]');
|
||||||
|
if (await filenameInput.isVisible({ timeout: 3_000 }).catch(() => false)) {
|
||||||
|
await expect(filenameInput).toBeVisible();
|
||||||
|
await filenameInput.fill("my-image");
|
||||||
|
await expect(filenameInput).toHaveValue("my-image");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user