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="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>
|
||||
<div className="flex gap-2 w-full max-w-sm">
|
||||
@@ -295,7 +295,7 @@ export function Dropzone({
|
||||
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"
|
||||
>
|
||||
{urlLoading ? "..." : "Add"}
|
||||
{urlLoading ? t.dropzone.urlLoadingIndicator : t.dropzone.addUrlButton}
|
||||
</button>
|
||||
</div>
|
||||
{urlError && <p className="text-xs text-destructive">{urlError}</p>}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Check, FolderOpen, ImageIcon, Loader2, Search, X } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import {
|
||||
apiListFiles,
|
||||
formatHeaders,
|
||||
@@ -62,6 +63,7 @@ interface FileLibraryModalProps {
|
||||
}
|
||||
|
||||
export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [files, setFiles] = useState<UserFile[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
@@ -151,7 +153,7 @@ export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalPr
|
||||
{/* Header */}
|
||||
<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" />
|
||||
<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
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
|
||||
@@ -75,6 +75,7 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
||||
transparent: true,
|
||||
});
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [estimatedSize, setEstimatedSize] = useState<number | null>(null);
|
||||
const [copyStatus, setCopyStatus] = useState<"idle" | "copied">("idle");
|
||||
|
||||
const aspectRatio = canvasSize.width / canvasSize.height;
|
||||
@@ -103,7 +104,22 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
||||
height: canvasSize.height,
|
||||
});
|
||||
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
|
||||
useEffect(() => {
|
||||
@@ -454,6 +470,14 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
||||
alt="Export preview"
|
||||
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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ function useMenuDefinitions(callbacks: MenuBarCallbacks): MenuDef[] {
|
||||
const setSelection = useEditorStore((s) => s.setSelection);
|
||||
const invertSelection = useEditorStore((s) => s.invertSelection);
|
||||
const canvasSize = useEditorStore((s) => s.canvasSize);
|
||||
const setPanOffset = useEditorStore((s) => s.setPanOffset);
|
||||
const rotateCanvas = useEditorStore((s) => s.rotateCanvas);
|
||||
const flipCanvasHorizontal = useEditorStore((s) => s.flipCanvasHorizontal);
|
||||
const flipCanvasVertical = useEditorStore((s) => s.flipCanvasVertical);
|
||||
@@ -327,11 +328,34 @@ function useMenuDefinitions(callbacks: MenuBarCallbacks): MenuDef[] {
|
||||
items: [
|
||||
{ label: "Zoom In", 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",
|
||||
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,
|
||||
},
|
||||
{ label: "Rulers", checked: rulersVisible, action: toggleRulers },
|
||||
|
||||
@@ -10,14 +10,41 @@ export function BrushOptions() {
|
||||
const brushSize = useEditorStore((s) => s.brushSize);
|
||||
const brushOpacity = useEditorStore((s) => s.brushOpacity);
|
||||
const brushHardness = useEditorStore((s) => s.brushHardness);
|
||||
const brushFlow = useEditorStore((s) => s.brushFlow);
|
||||
const setBrushSize = useEditorStore((s) => s.setBrushSize);
|
||||
const setBrushOpacity = useEditorStore((s) => s.setBrushOpacity);
|
||||
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;
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
Size
|
||||
@@ -84,6 +111,24 @@ export function BrushOptions() {
|
||||
<span className="text-[10px]">%</span>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ export function SelectionOptions() {
|
||||
const setMagicWandTolerance = useEditorStore((s) => s.setMagicWandTolerance);
|
||||
const magicWandContiguous = useEditorStore((s) => s.magicWandContiguous);
|
||||
const setMagicWandContiguous = useEditorStore((s) => s.setMagicWandContiguous);
|
||||
const selectionFeather = useEditorStore((s) => s.selectionFeather);
|
||||
const setSelectionFeather = useEditorStore((s) => s.setSelectionFeather);
|
||||
|
||||
const selectionType: SelectionType =
|
||||
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 */}
|
||||
{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 type { TransformToolApi } from "@/components/editor/tools/transform-tool";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -55,7 +55,7 @@ function NumericInput({
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -137,6 +137,24 @@ export function TransformOptions({ api }: { api: TransformToolApi }) {
|
||||
>
|
||||
<FlipVertical2 className="h-4 w-4" />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function useBrushTool() {
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
const { activeTool, foregroundColor, brushSize, brushOpacity, brushHardness, zoom, panOffset } =
|
||||
const { activeTool, foregroundColor, brushSize, brushOpacity, brushHardness, brushFlow, zoom, panOffset } =
|
||||
useEditorStore.getState();
|
||||
|
||||
if (activeTool !== "brush" && activeTool !== "pencil") return;
|
||||
@@ -29,6 +29,15 @@ export function useBrushTool() {
|
||||
const x = (pointer.x - panOffset.x) / 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 shadowBlurValue = activeTool === "pencil" ? 0 : brushSize * 0.4 * (1 - brushHardness);
|
||||
|
||||
@@ -39,7 +48,7 @@ export function useBrushTool() {
|
||||
tension: activeTool === "pencil" ? 0 : 0.5,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
opacity: brushOpacity,
|
||||
opacity: brushOpacity * brushFlow,
|
||||
globalCompositeOperation: "source-over",
|
||||
...(shadowBlurValue > 0 && {
|
||||
shadowBlur: shadowBlurValue,
|
||||
@@ -73,6 +82,15 @@ export function useBrushTool() {
|
||||
const x = (pointer.x - panOffset.x) / 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];
|
||||
|
||||
useEditorStore.getState().updateObject(strokeRef.current.objectId, {
|
||||
|
||||
@@ -18,7 +18,7 @@ export function useEraserTool() {
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
const { activeTool, brushSize, brushOpacity, brushHardness, zoom, panOffset } =
|
||||
const { activeTool, brushSize, brushOpacity, brushHardness, eraserMode, zoom, panOffset } =
|
||||
useEditorStore.getState();
|
||||
|
||||
if (activeTool !== "eraser") return;
|
||||
@@ -29,16 +29,31 @@ export function useEraserTool() {
|
||||
const x = (pointer.x - panOffset.x) / 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 shadowBlurValue = brushSize * 0.4 * (1 - brushHardness);
|
||||
|
||||
const isBlock = eraserMode === "block";
|
||||
|
||||
const attrs: LineAttrs = {
|
||||
points: [x, y],
|
||||
stroke: "#000000",
|
||||
strokeWidth: brushSize,
|
||||
tension: 0.5,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
tension: isBlock ? 0 : 0.5,
|
||||
lineCap: isBlock ? "butt" : "round",
|
||||
lineJoin: isBlock ? "miter" : "round",
|
||||
opacity: brushOpacity,
|
||||
globalCompositeOperation: "destination-out",
|
||||
...(shadowBlurValue > 0 && {
|
||||
@@ -73,6 +88,19 @@ export function useEraserTool() {
|
||||
const x = (pointer.x - panOffset.x) / 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];
|
||||
|
||||
useEditorStore.getState().updateObject(strokeRef.current.objectId, {
|
||||
|
||||
@@ -53,7 +53,20 @@ export function useTransformTool(): TransformToolApi {
|
||||
|
||||
// Read values from selected object(s)
|
||||
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]);
|
||||
if (!obj) return;
|
||||
const a = obj.attrs as unknown as Record<string, unknown>;
|
||||
@@ -82,7 +95,26 @@ export function useTransformTool(): TransformToolApi {
|
||||
}, [isTransforming, selectedObjectIds]);
|
||||
|
||||
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);
|
||||
// Store pre-transform state for cancel
|
||||
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 { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import {
|
||||
apiGetFileDetails,
|
||||
formatHeaders,
|
||||
@@ -74,6 +75,7 @@ interface FileDetailsProps {
|
||||
}
|
||||
|
||||
export function FileDetails({ mobile = false }: FileDetailsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { selectedFileId } = useFilesPageStore();
|
||||
const setFiles = useFileStore((s) => s.setFiles);
|
||||
const navigate = useNavigate();
|
||||
@@ -139,11 +141,11 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
||||
"flex flex-col items-center justify-center text-muted-foreground",
|
||||
mobile
|
||||
? "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" />
|
||||
<p className="text-sm">Select a file to view details</p>
|
||||
<p className="text-sm">{t.files.selectFilePrompt}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -155,7 +157,7 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
||||
"flex items-center justify-center",
|
||||
mobile
|
||||
? "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" />
|
||||
@@ -171,7 +173,7 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
||||
"overflow-y-auto",
|
||||
mobile
|
||||
? "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 */}
|
||||
@@ -187,24 +189,28 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
||||
<div className="flex-1">
|
||||
<div className="rounded-lg border border-border overflow-hidden">
|
||||
<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 className="divide-y divide-border">
|
||||
<DetailRow label="Name" value={details.originalName} />
|
||||
<DetailRow label={t.files.name} value={details.originalName} />
|
||||
<DetailRow
|
||||
label="Format"
|
||||
label={t.files.format}
|
||||
value={details.mimeType.replace("image/", "").toUpperCase()}
|
||||
/>
|
||||
<DetailRow label="Size" value={formatSize(details.size)} />
|
||||
<DetailRow label={t.files.size} value={formatSize(details.size)} />
|
||||
<DetailRow
|
||||
label="Dimensions"
|
||||
label={t.files.dimensions}
|
||||
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
|
||||
label="Tools Used"
|
||||
label={t.files.toolsUsed}
|
||||
value={
|
||||
details.toolChain.length > 0 ? details.toolChain.map(toolName).join(", ") : "None"
|
||||
details.toolChain.length > 0
|
||||
? details.toolChain.map(toolName).join(", ")
|
||||
: t.files.none
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Download, Search, Trash2, Workflow } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { getFileDownloadUrl } from "@/lib/api";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||
import { FileListItem } from "./file-list-item";
|
||||
|
||||
export function FileList() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
files,
|
||||
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" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search files..."
|
||||
placeholder={t.files.searchPlaceholder}
|
||||
value={inputValue}
|
||||
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"
|
||||
@@ -78,7 +81,9 @@ export function FileList() {
|
||||
className="h-4 w-4 accent-primary"
|
||||
/>
|
||||
<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>
|
||||
{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"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
Delete
|
||||
{t.files.delete}
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
<Workflow className="h-3.5 w-3.5" />
|
||||
Pipeline
|
||||
{t.files.pipeline}
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
Download
|
||||
{t.files.download}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -124,7 +129,7 @@ export function FileList() {
|
||||
)}
|
||||
{!loading && !error && files.length === 0 && (
|
||||
<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>
|
||||
)}
|
||||
{!loading && !error && files.map((file) => <FileListItem key={file.id} file={file} />)}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { Clock, Upload } from "lucide-react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||
|
||||
export function FilesNav() {
|
||||
const { t } = useTranslation();
|
||||
const { activeTab, setActiveTab } = useFilesPageStore();
|
||||
const items = [
|
||||
{ id: "recent" as const, label: "Recent", icon: Clock },
|
||||
{ id: "upload" as const, label: "Upload Files", icon: Upload },
|
||||
{ id: "recent" as const, label: t.files.recentTab, icon: Clock },
|
||||
{ id: "upload" as const, label: t.files.uploadTab, icon: Upload },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-48 border-r border-border p-4 shrink-0 hidden md:block">
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3">My Files</h3>
|
||||
<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">{t.files.myFiles}</h3>
|
||||
<div className="space-y-1">
|
||||
{items.map((item) => (
|
||||
<button
|
||||
|
||||
@@ -110,7 +110,7 @@ export function Sidebar({
|
||||
}
|
||||
|
||||
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">
|
||||
<OtterLogo className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
|
||||
@@ -21,10 +21,12 @@ export function CompressControls({ settings: initialSettings, onChange }: Compre
|
||||
const [targetSizeValue, setTargetSizeValue] = useState("");
|
||||
const [sizeUnit, setSizeUnit] = useState<SizeUnit>("KB");
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const prevSettingsKeyRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!initialSettings || initializedRef.current) return;
|
||||
initializedRef.current = true;
|
||||
if (!initialSettings) return;
|
||||
const key = JSON.stringify(initialSettings);
|
||||
if (prevSettingsKeyRef.current === key) return;
|
||||
prevSettingsKeyRef.current = key;
|
||||
if (initialSettings.mode != null) setMode(initialSettings.mode as CompressMode);
|
||||
if (initialSettings.quality != null) setQuality(Number(initialSettings.quality));
|
||||
if (initialSettings.targetSizeKb != null)
|
||||
|
||||
@@ -129,7 +129,7 @@ function SortableStep({
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
title="Remove"
|
||||
title={t.automate.removeStep}
|
||||
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -159,6 +159,7 @@ export function PipelineBuilder({
|
||||
onUpdateSettings,
|
||||
onToggleStep,
|
||||
}: PipelineBuilderProps) {
|
||||
const { t } = useTranslation();
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
@@ -177,10 +178,8 @@ export function PipelineBuilder({
|
||||
<div className="p-4 rounded-full bg-muted/50 mb-4">
|
||||
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-sm font-medium text-foreground mb-1">No steps yet</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-[240px]">
|
||||
Click tools from the palette to build your pipeline
|
||||
</p>
|
||||
<h3 className="text-sm font-medium text-foreground mb-1">{t.automate.noStepsHeading}</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-[240px]">{t.automate.addToolsPrompt}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,12 +66,16 @@ export function ToolPalette({ onAddStep, className }: ToolPaletteProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col h-full", className)}>
|
||||
<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 className="flex-1 overflow-y-auto px-3 pb-3">
|
||||
{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 ? (
|
||||
<div className="space-y-1">
|
||||
{availableTools.map((tool) => (
|
||||
|
||||
@@ -394,6 +394,7 @@ export function useEditorShortcuts(callbacks?: {
|
||||
useHotkeys(
|
||||
"mod+a",
|
||||
(e) => {
|
||||
if (isInputFocused()) return;
|
||||
e.preventDefault();
|
||||
const state = useEditorStore.getState();
|
||||
const allIds = state.objects.map((o) => o.id);
|
||||
@@ -406,6 +407,7 @@ export function useEditorShortcuts(callbacks?: {
|
||||
useHotkeys(
|
||||
"mod+d",
|
||||
(e) => {
|
||||
if (isInputFocused()) return;
|
||||
e.preventDefault();
|
||||
useEditorStore.getState().setSelectedObjects([]);
|
||||
useEditorStore.getState().setSelection(null);
|
||||
@@ -496,6 +498,7 @@ export function useEditorShortcuts(callbacks?: {
|
||||
useHotkeys(
|
||||
"mod+t",
|
||||
(e) => {
|
||||
if (isInputFocused()) return;
|
||||
e.preventDefault();
|
||||
useEditorStore.getState().setTool("transform");
|
||||
},
|
||||
@@ -506,6 +509,7 @@ export function useEditorShortcuts(callbacks?: {
|
||||
useHotkeys(
|
||||
"mod+j",
|
||||
(e) => {
|
||||
if (isInputFocused()) return;
|
||||
e.preventDefault();
|
||||
const state = useEditorStore.getState();
|
||||
state.duplicateLayer(state.activeLayerId);
|
||||
@@ -517,6 +521,7 @@ export function useEditorShortcuts(callbacks?: {
|
||||
useHotkeys(
|
||||
"mod+shift+n",
|
||||
(e) => {
|
||||
if (isInputFocused()) return;
|
||||
e.preventDefault();
|
||||
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,
|
||||
EditorLayer,
|
||||
EditorState,
|
||||
EraserMode,
|
||||
FilterConfig,
|
||||
SelectionMode,
|
||||
StrokeDashStyle,
|
||||
@@ -152,6 +153,8 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
||||
brushSize: 10,
|
||||
brushOpacity: 1,
|
||||
brushHardness: 1,
|
||||
brushFlow: 1,
|
||||
eraserMode: "brush" as EraserMode,
|
||||
|
||||
// --- Colors ---
|
||||
foregroundColor: "#000000",
|
||||
@@ -171,6 +174,7 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
||||
selectionMode: "new" as SelectionMode,
|
||||
magicWandTolerance: 32,
|
||||
magicWandContiguous: true,
|
||||
selectionFeather: 0,
|
||||
|
||||
// --- Crop ---
|
||||
cropState: null,
|
||||
@@ -954,6 +958,7 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
||||
setSelectionMode: (mode) => set({ selectionMode: mode }),
|
||||
setMagicWandTolerance: (v) => set({ magicWandTolerance: v }),
|
||||
setMagicWandContiguous: (v: boolean) => set({ magicWandContiguous: v }),
|
||||
setSelectionFeather: (v) => set({ selectionFeather: v }),
|
||||
|
||||
invertSelection: () => {
|
||||
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)) }),
|
||||
setBrushOpacity: (opacity) => set({ brushOpacity: Math.max(0, Math.min(1, opacity)) }),
|
||||
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
|
||||
copyObjects: () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import { generateId } from "@/lib/utils";
|
||||
|
||||
export interface PipelineStep {
|
||||
@@ -30,51 +31,63 @@ interface PipelineState {
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const usePipelineStore = create<PipelineState>((set, get) => ({
|
||||
steps: [],
|
||||
expandedStepId: null,
|
||||
savedPipelines: [],
|
||||
export const usePipelineStore = create<PipelineState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
steps: [],
|
||||
expandedStepId: null,
|
||||
savedPipelines: [],
|
||||
|
||||
addStep: (toolId) => {
|
||||
const step: PipelineStep = { id: generateId(), toolId, settings: {} };
|
||||
set({ steps: [...get().steps, step], expandedStepId: step.id });
|
||||
},
|
||||
addStep: (toolId) => {
|
||||
const step: PipelineStep = { id: generateId(), toolId, settings: {} };
|
||||
set({ steps: [...get().steps, step], expandedStepId: step.id });
|
||||
},
|
||||
|
||||
removeStep: (id) => {
|
||||
const { steps, expandedStepId } = get();
|
||||
set({
|
||||
steps: steps.filter((s) => s.id !== id),
|
||||
expandedStepId: expandedStepId === id ? null : expandedStepId,
|
||||
});
|
||||
},
|
||||
removeStep: (id) => {
|
||||
const { steps, expandedStepId } = get();
|
||||
set({
|
||||
steps: steps.filter((s) => s.id !== id),
|
||||
expandedStepId: expandedStepId === id ? null : expandedStepId,
|
||||
});
|
||||
},
|
||||
|
||||
reorderSteps: (activeId, overId) => {
|
||||
const { steps } = get();
|
||||
const oldIndex = steps.findIndex((s) => s.id === activeId);
|
||||
const newIndex = steps.findIndex((s) => s.id === overId);
|
||||
if (oldIndex < 0 || newIndex < 0) return;
|
||||
const reordered = [...steps];
|
||||
const [moved] = reordered.splice(oldIndex, 1);
|
||||
reordered.splice(newIndex, 0, moved);
|
||||
set({ steps: reordered });
|
||||
},
|
||||
reorderSteps: (activeId, overId) => {
|
||||
const { steps } = get();
|
||||
const oldIndex = steps.findIndex((s) => s.id === activeId);
|
||||
const newIndex = steps.findIndex((s) => s.id === overId);
|
||||
if (oldIndex < 0 || newIndex < 0) return;
|
||||
const reordered = [...steps];
|
||||
const [moved] = reordered.splice(oldIndex, 1);
|
||||
reordered.splice(newIndex, 0, moved);
|
||||
set({ steps: reordered });
|
||||
},
|
||||
|
||||
updateStepSettings: (id, settings) => {
|
||||
set({ steps: get().steps.map((s) => (s.id === id ? { ...s, settings } : s)) });
|
||||
},
|
||||
updateStepSettings: (id, settings) => {
|
||||
set({ steps: get().steps.map((s) => (s.id === id ? { ...s, settings } : s)) });
|
||||
},
|
||||
|
||||
setExpandedStep: (id) => set({ expandedStepId: id }),
|
||||
setExpandedStep: (id) => set({ expandedStepId: id }),
|
||||
|
||||
loadSteps: (rawSteps) => {
|
||||
const steps = rawSteps.map((s) => ({
|
||||
id: generateId(),
|
||||
toolId: s.toolId,
|
||||
settings: { ...s.settings },
|
||||
}));
|
||||
set({ steps, expandedStepId: null });
|
||||
},
|
||||
loadSteps: (rawSteps) => {
|
||||
const steps = rawSteps.map((s) => ({
|
||||
id: generateId(),
|
||||
toolId: s.toolId,
|
||||
settings: { ...s.settings },
|
||||
}));
|
||||
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
|
||||
|
||||
export type EraserMode = "brush" | "block";
|
||||
|
||||
export type SelectionMode = "new" | "add" | "subtract";
|
||||
|
||||
export type StrokeDashStyle = "solid" | "dashed" | "dotted";
|
||||
@@ -281,6 +283,8 @@ export interface EditorState {
|
||||
brushSize: number;
|
||||
brushOpacity: number;
|
||||
brushHardness: number;
|
||||
brushFlow: number;
|
||||
eraserMode: EraserMode;
|
||||
|
||||
// Colors
|
||||
foregroundColor: string;
|
||||
@@ -300,6 +304,7 @@ export interface EditorState {
|
||||
selectionMode: SelectionMode;
|
||||
magicWandTolerance: number;
|
||||
magicWandContiguous: boolean;
|
||||
selectionFeather: number;
|
||||
|
||||
// Crop
|
||||
cropState: CropState | null;
|
||||
@@ -425,6 +430,7 @@ export interface EditorState {
|
||||
setSelectionMode: (mode: SelectionMode) => void;
|
||||
setMagicWandTolerance: (v: number) => void;
|
||||
setMagicWandContiguous: (v: boolean) => void;
|
||||
setSelectionFeather: (v: number) => void;
|
||||
invertSelection: () => void;
|
||||
|
||||
// Crop
|
||||
@@ -435,6 +441,8 @@ export interface EditorState {
|
||||
setBrushSize: (size: number) => void;
|
||||
setBrushOpacity: (opacity: number) => void;
|
||||
setBrushHardness: (hardness: number) => void;
|
||||
setBrushFlow: (flow: number) => void;
|
||||
setEraserMode: (mode: EraserMode) => void;
|
||||
|
||||
// Clipboard
|
||||
copyObjects: () => void;
|
||||
|
||||
@@ -36,7 +36,9 @@ export const ar: TranslationKeys = {
|
||||
unexpectedError: "حدث خطأ غير متوقع.",
|
||||
retry: "إعادة المحاولة",
|
||||
privacyPolicy: "سياسة الخصوصية",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "الأساسيات",
|
||||
optimization: "التحسين",
|
||||
@@ -1686,6 +1688,9 @@ export const ar: TranslationKeys = {
|
||||
githubLink: "مستودع GitHub",
|
||||
docsLink: "التوثيق",
|
||||
apiRefLink: "مرجع API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1826,7 +1831,9 @@ export const ar: TranslationKeys = {
|
||||
pipelineName: "اسم Pipeline",
|
||||
pipelineDescription: "الوصف (اختياري)",
|
||||
noStepsPrompt: "أضف خطوات لبناء أتمتتك",
|
||||
step: "خطوة",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "خطوة",
|
||||
},
|
||||
nav: {
|
||||
tools: "الأدوات",
|
||||
@@ -1839,10 +1846,29 @@ export const ar: TranslationKeys = {
|
||||
grid: "شبكة",
|
||||
},
|
||||
files: {
|
||||
recentTab: "الأخيرة",
|
||||
uploadTab: "رفع",
|
||||
fileDetailsAriaLabel: "تفاصيل الملف",
|
||||
fileDetailsHeading: "تفاصيل الملف",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "نوع الملف هذا غير مدعوم بهذه الأداة",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const de: TranslationKeys = {
|
||||
unexpectedError: "Ein unerwarteter Fehler ist aufgetreten.",
|
||||
retry: "Erneut versuchen",
|
||||
privacyPolicy: "Datenschutzerklaerung",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Grundlagen",
|
||||
optimization: "Optimierung",
|
||||
@@ -1711,6 +1713,9 @@ export const de: TranslationKeys = {
|
||||
githubLink: "GitHub-Repository",
|
||||
docsLink: "Dokumentation",
|
||||
apiRefLink: "API-Referenz (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1855,7 +1860,9 @@ export const de: TranslationKeys = {
|
||||
pipelineName: "Pipeline-Name",
|
||||
pipelineDescription: "Beschreibung (optional)",
|
||||
noStepsPrompt: "Fuegen Sie Schritte hinzu, um Ihre Automatisierung zu erstellen",
|
||||
step: "Schritt",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Schritt",
|
||||
},
|
||||
nav: {
|
||||
tools: "Werkzeuge",
|
||||
@@ -1868,10 +1875,29 @@ export const de: TranslationKeys = {
|
||||
grid: "Raster",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Zuletzt",
|
||||
uploadTab: "Hochladen",
|
||||
fileDetailsAriaLabel: "Dateidetails",
|
||||
fileDetailsHeading: "Dateidetails",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Dieser Dateityp wird von diesem Werkzeug nicht unterstuetzt",
|
||||
|
||||
@@ -34,6 +34,8 @@ export const en = {
|
||||
somethingWentWrong: "Something went wrong",
|
||||
unexpectedError: "An unexpected error occurred.",
|
||||
privacyPolicy: "Privacy Policy",
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Essentials",
|
||||
@@ -1431,7 +1433,8 @@ export const en = {
|
||||
newPasswordPlaceholder: "New Password",
|
||||
confirmPasswordPlaceholder: "Confirm New Password",
|
||||
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",
|
||||
changeFailed: "Failed to change password",
|
||||
currentPasswordIncorrect: "Current password is incorrect",
|
||||
@@ -1645,6 +1648,9 @@ export const en = {
|
||||
githubLink: "GitHub Repository",
|
||||
docsLink: "Documentation",
|
||||
apiRefLink: "API Reference (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1786,6 +1792,8 @@ export const en = {
|
||||
pipelineName: "Pipeline Name",
|
||||
pipelineDescription: "Description (optional)",
|
||||
noStepsPrompt: "Add steps to build your automation",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Step",
|
||||
},
|
||||
nav: {
|
||||
@@ -1799,10 +1807,29 @@ export const en = {
|
||||
grid: "Grid",
|
||||
},
|
||||
files: {
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "This file type is not supported by this tool",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const es: TranslationKeys = {
|
||||
unexpectedError: "Ocurrio un error inesperado.",
|
||||
retry: "Reintentar",
|
||||
privacyPolicy: "Politica de privacidad",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Esenciales",
|
||||
optimization: "Optimizacion",
|
||||
@@ -1690,6 +1692,9 @@ export const es: TranslationKeys = {
|
||||
githubLink: "Repositorio en GitHub",
|
||||
docsLink: "Documentacion",
|
||||
apiRefLink: "Referencia API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1832,7 +1837,9 @@ export const es: TranslationKeys = {
|
||||
pipelineName: "Nombre del Pipeline",
|
||||
pipelineDescription: "Descripcion (opcional)",
|
||||
noStepsPrompt: "Agrega pasos para construir tu automatizacion",
|
||||
step: "Paso",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Paso",
|
||||
},
|
||||
nav: {
|
||||
tools: "Herramientas",
|
||||
@@ -1845,10 +1852,29 @@ export const es: TranslationKeys = {
|
||||
grid: "Cuadricula",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Recientes",
|
||||
uploadTab: "Subir",
|
||||
fileDetailsAriaLabel: "Detalles del archivo",
|
||||
fileDetailsHeading: "Detalles del archivo",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
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.",
|
||||
retry: "Réessayer",
|
||||
privacyPolicy: "Politique de confidentialite",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Essentiels",
|
||||
optimization: "Optimisation",
|
||||
@@ -1709,6 +1711,9 @@ export const fr: TranslationKeys = {
|
||||
githubLink: "Depot GitHub",
|
||||
docsLink: "Documentation",
|
||||
apiRefLink: "Reference API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1853,7 +1858,9 @@ export const fr: TranslationKeys = {
|
||||
pipelineName: "Nom du Pipeline",
|
||||
pipelineDescription: "Description (optionnel)",
|
||||
noStepsPrompt: "Ajoutez des etapes pour construire votre automatisation",
|
||||
step: "Etape",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Etape",
|
||||
},
|
||||
nav: {
|
||||
tools: "Outils",
|
||||
@@ -1866,10 +1873,29 @@ export const fr: TranslationKeys = {
|
||||
grid: "Grille",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Recents",
|
||||
uploadTab: "Importer",
|
||||
fileDetailsAriaLabel: "Details du fichier",
|
||||
fileDetailsHeading: "Details du fichier",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Ce type de fichier n'est pas pris en charge par cet outil",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const hi: TranslationKeys = {
|
||||
unexpectedError: "एक अप्रत्याशित त्रुटि हुई।",
|
||||
retry: "पुनः प्रयास करें",
|
||||
privacyPolicy: "गोपनीयता नीति",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "आवश्यक टूल्स",
|
||||
optimization: "ऑप्टिमाइज़ेशन",
|
||||
@@ -1682,6 +1684,9 @@ export const hi: TranslationKeys = {
|
||||
githubLink: "GitHub रिपॉज़िटरी",
|
||||
docsLink: "डॉक्यूमेंटेशन",
|
||||
apiRefLink: "API रेफरेंस (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1823,7 +1828,9 @@ export const hi: TranslationKeys = {
|
||||
pipelineName: "Pipeline का नाम",
|
||||
pipelineDescription: "विवरण (वैकल्पिक)",
|
||||
noStepsPrompt: "अपना ऑटोमेशन बनाने के लिए स्टेप्स जोड़ें",
|
||||
step: "स्टेप",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "स्टेप",
|
||||
},
|
||||
nav: {
|
||||
tools: "टूल्स",
|
||||
@@ -1836,10 +1843,29 @@ export const hi: TranslationKeys = {
|
||||
grid: "ग्रिड",
|
||||
},
|
||||
files: {
|
||||
recentTab: "हाल के",
|
||||
uploadTab: "अपलोड",
|
||||
fileDetailsAriaLabel: "फाइल विवरण",
|
||||
fileDetailsHeading: "फाइल विवरण",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "इस फाइल प्रकार को यह टूल सपोर्ट नहीं करता",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const id: TranslationKeys = {
|
||||
unexpectedError: "Terjadi kesalahan yang tidak terduga.",
|
||||
retry: "Coba lagi",
|
||||
privacyPolicy: "Kebijakan Privasi",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Dasar",
|
||||
optimization: "Optimasi",
|
||||
@@ -1698,6 +1700,9 @@ export const id: TranslationKeys = {
|
||||
githubLink: "Repositori GitHub",
|
||||
docsLink: "Dokumentasi",
|
||||
apiRefLink: "Referensi API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1840,7 +1845,9 @@ export const id: TranslationKeys = {
|
||||
pipelineName: "Nama Pipeline",
|
||||
pipelineDescription: "Deskripsi (opsional)",
|
||||
noStepsPrompt: "Tambahkan langkah untuk membangun otomasi Anda",
|
||||
step: "Langkah",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Langkah",
|
||||
},
|
||||
nav: {
|
||||
tools: "Alat",
|
||||
@@ -1853,10 +1860,29 @@ export const id: TranslationKeys = {
|
||||
grid: "Grid",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Terbaru",
|
||||
uploadTab: "Unggah",
|
||||
fileDetailsAriaLabel: "Detail File",
|
||||
fileDetailsHeading: "Detail File",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Jenis file ini tidak didukung oleh alat ini",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const it: TranslationKeys = {
|
||||
unexpectedError: "Si e verificato un errore imprevisto.",
|
||||
retry: "Riprova",
|
||||
privacyPolicy: "Informativa sulla privacy",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Essenziali",
|
||||
optimization: "Ottimizzazione",
|
||||
@@ -1704,6 +1706,9 @@ export const it: TranslationKeys = {
|
||||
githubLink: "Repository GitHub",
|
||||
docsLink: "Documentazione",
|
||||
apiRefLink: "Riferimento API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1847,7 +1852,9 @@ export const it: TranslationKeys = {
|
||||
pipelineName: "Nome del Pipeline",
|
||||
pipelineDescription: "Descrizione (opzionale)",
|
||||
noStepsPrompt: "Aggiungi passaggi per costruire la tua automazione",
|
||||
step: "Passaggio",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Passaggio",
|
||||
},
|
||||
nav: {
|
||||
tools: "Strumenti",
|
||||
@@ -1860,10 +1867,29 @@ export const it: TranslationKeys = {
|
||||
grid: "Griglia",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Recenti",
|
||||
uploadTab: "Carica",
|
||||
fileDetailsAriaLabel: "Dettagli file",
|
||||
fileDetailsHeading: "Dettagli file",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Questo tipo di file non e supportato da questo strumento",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const ja: TranslationKeys = {
|
||||
unexpectedError: "予期しないエラーが発生しました。",
|
||||
retry: "再試行",
|
||||
privacyPolicy: "プライバシーポリシー",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "基本ツール",
|
||||
optimization: "最適化",
|
||||
@@ -1655,6 +1657,9 @@ export const ja: TranslationKeys = {
|
||||
githubLink: "GitHubリポジトリ",
|
||||
docsLink: "ドキュメント",
|
||||
apiRefLink: "APIリファレンス(Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1796,7 +1801,9 @@ export const ja: TranslationKeys = {
|
||||
pipelineName: "Pipeline名",
|
||||
pipelineDescription: "説明(任意)",
|
||||
noStepsPrompt: "ステップを追加して自動化を構築",
|
||||
step: "ステップ",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "ステップ",
|
||||
},
|
||||
nav: {
|
||||
tools: "ツール",
|
||||
@@ -1809,10 +1816,29 @@ export const ja: TranslationKeys = {
|
||||
grid: "グリッド",
|
||||
},
|
||||
files: {
|
||||
recentTab: "最近",
|
||||
uploadTab: "アップロード",
|
||||
fileDetailsAriaLabel: "ファイル詳細",
|
||||
fileDetailsHeading: "ファイル詳細",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "このツールではサポートされていないファイルタイプです",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const ko: TranslationKeys = {
|
||||
unexpectedError: "예기치 않은 오류가 발생했습니다.",
|
||||
retry: "재시도",
|
||||
privacyPolicy: "개인정보 처리방침",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "기본 도구",
|
||||
optimization: "최적화",
|
||||
@@ -1640,6 +1642,9 @@ export const ko: TranslationKeys = {
|
||||
githubLink: "GitHub 저장소",
|
||||
docsLink: "문서",
|
||||
apiRefLink: "API 레퍼런스 (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1781,7 +1786,9 @@ export const ko: TranslationKeys = {
|
||||
pipelineName: "Pipeline 이름",
|
||||
pipelineDescription: "설명 (선택)",
|
||||
noStepsPrompt: "단계를 추가하여 자동화를 구성하세요",
|
||||
step: "단계",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "단계",
|
||||
},
|
||||
nav: {
|
||||
tools: "도구",
|
||||
@@ -1794,10 +1801,29 @@ export const ko: TranslationKeys = {
|
||||
grid: "그리드",
|
||||
},
|
||||
files: {
|
||||
recentTab: "최근",
|
||||
uploadTab: "업로드",
|
||||
fileDetailsAriaLabel: "파일 상세",
|
||||
fileDetailsHeading: "파일 상세",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "이 도구에서 지원하지 않는 파일 형식입니다",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const nl: TranslationKeys = {
|
||||
unexpectedError: "Er is een onverwachte fout opgetreden.",
|
||||
retry: "Opnieuw proberen",
|
||||
privacyPolicy: "Privacybeleid",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Basistools",
|
||||
optimization: "Optimalisatie",
|
||||
@@ -1701,6 +1703,9 @@ export const nl: TranslationKeys = {
|
||||
githubLink: "GitHub-repository",
|
||||
docsLink: "Documentatie",
|
||||
apiRefLink: "API-referentie (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1843,7 +1848,9 @@ export const nl: TranslationKeys = {
|
||||
pipelineName: "Pipeline-naam",
|
||||
pipelineDescription: "Beschrijving (optioneel)",
|
||||
noStepsPrompt: "Voeg stappen toe om je automatisering te bouwen",
|
||||
step: "Stap",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Stap",
|
||||
},
|
||||
nav: {
|
||||
tools: "Tools",
|
||||
@@ -1856,10 +1863,29 @@ export const nl: TranslationKeys = {
|
||||
grid: "Raster",
|
||||
},
|
||||
files: {
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Uploaden",
|
||||
fileDetailsAriaLabel: "Bestandsdetails",
|
||||
fileDetailsHeading: "Bestandsdetails",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Dit bestandstype wordt niet ondersteund door deze tool",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const pl: TranslationKeys = {
|
||||
unexpectedError: "Wystąpił nieoczekiwany błąd.",
|
||||
retry: "Ponów",
|
||||
privacyPolicy: "Polityka prywatności",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Podstawowe",
|
||||
optimization: "Optymalizacja",
|
||||
@@ -1707,6 +1709,9 @@ export const pl: TranslationKeys = {
|
||||
githubLink: "Repozytorium GitHub",
|
||||
docsLink: "Dokumentacja",
|
||||
apiRefLink: "Dokumentacja API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1850,7 +1855,9 @@ export const pl: TranslationKeys = {
|
||||
pipelineName: "Nazwa Pipeline",
|
||||
pipelineDescription: "Opis (opcjonalnie)",
|
||||
noStepsPrompt: "Dodaj kroki, aby zbudować automatyzację",
|
||||
step: "Krok",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Krok",
|
||||
},
|
||||
nav: {
|
||||
tools: "Narzędzia",
|
||||
@@ -1863,10 +1870,29 @@ export const pl: TranslationKeys = {
|
||||
grid: "Siatka",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Ostatnie",
|
||||
uploadTab: "Przesyłanie",
|
||||
fileDetailsAriaLabel: "Szczegóły pliku",
|
||||
fileDetailsHeading: "Szczegóły pliku",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
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.",
|
||||
retry: "Tentar novamente",
|
||||
privacyPolicy: "Politica de privacidade",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Essenciais",
|
||||
optimization: "Otimizacao",
|
||||
@@ -1700,6 +1702,9 @@ export const ptBR: TranslationKeys = {
|
||||
githubLink: "Repositorio no GitHub",
|
||||
docsLink: "Documentacao",
|
||||
apiRefLink: "Referencia da API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1843,7 +1848,9 @@ export const ptBR: TranslationKeys = {
|
||||
pipelineName: "Nome do Pipeline",
|
||||
pipelineDescription: "Descricao (opcional)",
|
||||
noStepsPrompt: "Adicione passos para construir sua automacao",
|
||||
step: "Passo",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Passo",
|
||||
},
|
||||
nav: {
|
||||
tools: "Ferramentas",
|
||||
@@ -1856,10 +1863,29 @@ export const ptBR: TranslationKeys = {
|
||||
grid: "Grade",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Recentes",
|
||||
uploadTab: "Enviar",
|
||||
fileDetailsAriaLabel: "Detalhes do arquivo",
|
||||
fileDetailsHeading: "Detalhes do arquivo",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Este tipo de arquivo nao e suportado por esta ferramenta",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const ru: TranslationKeys = {
|
||||
unexpectedError: "Произошла непредвиденная ошибка.",
|
||||
retry: "Повторить",
|
||||
privacyPolicy: "Политика конфиденциальности",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Основные",
|
||||
optimization: "Оптимизация",
|
||||
@@ -1700,6 +1702,9 @@ export const ru: TranslationKeys = {
|
||||
githubLink: "Репозиторий GitHub",
|
||||
docsLink: "Документация",
|
||||
apiRefLink: "Справочник API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1842,7 +1847,9 @@ export const ru: TranslationKeys = {
|
||||
pipelineName: "Название Pipeline",
|
||||
pipelineDescription: "Описание (необязательно)",
|
||||
noStepsPrompt: "Добавьте шаги для построения автоматизации",
|
||||
step: "Шаг",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Шаг",
|
||||
},
|
||||
nav: {
|
||||
tools: "Инструменты",
|
||||
@@ -1855,10 +1862,29 @@ export const ru: TranslationKeys = {
|
||||
grid: "Сетка",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Недавние",
|
||||
uploadTab: "Загрузка",
|
||||
fileDetailsAriaLabel: "Сведения о файле",
|
||||
fileDetailsHeading: "Сведения о файле",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Этот тип файла не поддерживается данным инструментом",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const sv: TranslationKeys = {
|
||||
unexpectedError: "Ett ovantat fel uppstod.",
|
||||
retry: "Försök igen",
|
||||
privacyPolicy: "Integritetspolicy",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Grundlaggande",
|
||||
optimization: "Optimering",
|
||||
@@ -1696,6 +1698,9 @@ export const sv: TranslationKeys = {
|
||||
githubLink: "GitHub-arkiv",
|
||||
docsLink: "Dokumentation",
|
||||
apiRefLink: "API-referens (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1837,7 +1842,9 @@ export const sv: TranslationKeys = {
|
||||
pipelineName: "Pipeline-namn",
|
||||
pipelineDescription: "Beskrivning (valfritt)",
|
||||
noStepsPrompt: "Lagg till steg for att bygga din automatisering",
|
||||
step: "Steg",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Steg",
|
||||
},
|
||||
nav: {
|
||||
tools: "Verktyg",
|
||||
@@ -1850,10 +1857,29 @@ export const sv: TranslationKeys = {
|
||||
grid: "Rutnat",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Senaste",
|
||||
uploadTab: "Ladda upp",
|
||||
fileDetailsAriaLabel: "Fildetaljer",
|
||||
fileDetailsHeading: "Fildetaljer",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Denna filtyp stods inte av detta verktyg",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const th: TranslationKeys = {
|
||||
unexpectedError: "เกิดข้อผิดพลาดที่ไม่คาดคิด",
|
||||
retry: "ลองใหม่",
|
||||
privacyPolicy: "นโยบายความเป็นส่วนตัว",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "พื้นฐาน",
|
||||
optimization: "การเพิ่มประสิทธิภาพ",
|
||||
@@ -1674,6 +1676,9 @@ export const th: TranslationKeys = {
|
||||
githubLink: "คลังเก็บโค้ด GitHub",
|
||||
docsLink: "เอกสารประกอบ",
|
||||
apiRefLink: "อ้างอิง API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1814,7 +1819,9 @@ export const th: TranslationKeys = {
|
||||
pipelineName: "ชื่อ Pipeline",
|
||||
pipelineDescription: "คำอธิบาย (ไม่บังคับ)",
|
||||
noStepsPrompt: "เพิ่มขั้นตอนเพื่อสร้างการทำงานอัตโนมัติ",
|
||||
step: "ขั้นตอน",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "ขั้นตอน",
|
||||
},
|
||||
nav: {
|
||||
tools: "เครื่องมือ",
|
||||
@@ -1827,10 +1834,29 @@ export const th: TranslationKeys = {
|
||||
grid: "กริด",
|
||||
},
|
||||
files: {
|
||||
recentTab: "ล่าสุด",
|
||||
uploadTab: "อัปโหลด",
|
||||
fileDetailsAriaLabel: "รายละเอียดไฟล์",
|
||||
fileDetailsHeading: "รายละเอียดไฟล์",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "ไฟล์ประเภทนี้ไม่รองรับโดยเครื่องมือนี้",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const tr: TranslationKeys = {
|
||||
unexpectedError: "Beklenmeyen bir hata oluştu.",
|
||||
retry: "Yeniden dene",
|
||||
privacyPolicy: "Gizlilik Politikası",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Temel Araçlar",
|
||||
optimization: "Optimizasyon",
|
||||
@@ -1704,6 +1706,9 @@ export const tr: TranslationKeys = {
|
||||
githubLink: "GitHub Deposu",
|
||||
docsLink: "Dokümantasyon",
|
||||
apiRefLink: "API Referansı (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1847,7 +1852,9 @@ export const tr: TranslationKeys = {
|
||||
pipelineName: "Pipeline Adı",
|
||||
pipelineDescription: "Açıklama (isteğe bağlı)",
|
||||
noStepsPrompt: "Otomasyonunuzu oluşturmak için adımlar ekleyin",
|
||||
step: "Adım",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Adım",
|
||||
},
|
||||
nav: {
|
||||
tools: "Araçlar",
|
||||
@@ -1860,10 +1867,29 @@ export const tr: TranslationKeys = {
|
||||
grid: "Izgara",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Son Kullanılanlar",
|
||||
uploadTab: "Yükle",
|
||||
fileDetailsAriaLabel: "Dosya Ayrıntıları",
|
||||
fileDetailsHeading: "Dosya Ayrıntıları",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Bu dosya türü bu araç tarafından desteklenmiyor",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const uk: TranslationKeys = {
|
||||
unexpectedError: "Сталася неочікувана помилка.",
|
||||
retry: "Повторити",
|
||||
privacyPolicy: "Політика конфіденційності",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "Основні",
|
||||
optimization: "Оптимізація",
|
||||
@@ -1700,6 +1702,9 @@ export const uk: TranslationKeys = {
|
||||
githubLink: "Репозиторій GitHub",
|
||||
docsLink: "Документація",
|
||||
apiRefLink: "Довідник API (Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1843,7 +1848,9 @@ export const uk: TranslationKeys = {
|
||||
pipelineName: "Назва Pipeline",
|
||||
pipelineDescription: "Опис (необов'язково)",
|
||||
noStepsPrompt: "Додайте кроки для побудови автоматизації",
|
||||
step: "Крок",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "Крок",
|
||||
},
|
||||
nav: {
|
||||
tools: "Інструменти",
|
||||
@@ -1856,10 +1863,29 @@ export const uk: TranslationKeys = {
|
||||
grid: "Сітка",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Нещодавні",
|
||||
uploadTab: "Завантаження",
|
||||
fileDetailsAriaLabel: "Відомості про файл",
|
||||
fileDetailsHeading: "Відомості про файл",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "Цей тип файлу не підтримується цим інструментом",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const vi: TranslationKeys = {
|
||||
unexpectedError: "Đã xảy ra lỗi không mong muốn.",
|
||||
retry: "Thử lại",
|
||||
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: {
|
||||
essentials: "Cơ bản",
|
||||
optimization: "Tối ưu hóa",
|
||||
@@ -1696,6 +1698,9 @@ export const vi: TranslationKeys = {
|
||||
githubLink: "Kho mã nguồn GitHub",
|
||||
docsLink: "Tài liệu",
|
||||
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: {
|
||||
@@ -1837,7 +1842,9 @@ export const vi: TranslationKeys = {
|
||||
pipelineName: "Tên Pipeline",
|
||||
pipelineDescription: "Mô tả (tùy chọn)",
|
||||
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: {
|
||||
tools: "Công cụ",
|
||||
@@ -1850,10 +1857,29 @@ export const vi: TranslationKeys = {
|
||||
grid: "Lưới",
|
||||
},
|
||||
files: {
|
||||
recentTab: "Gần đây",
|
||||
uploadTab: "Tải lên",
|
||||
fileDetailsAriaLabel: "Chi tiết tệp",
|
||||
fileDetailsHeading: "Chi tiết tệp",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
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: "发生了意外错误。",
|
||||
retry: "重试",
|
||||
privacyPolicy: "隐私政策",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "基础工具",
|
||||
optimization: "优化",
|
||||
@@ -1626,6 +1628,9 @@ export const zhCN: TranslationKeys = {
|
||||
githubLink: "GitHub 仓库",
|
||||
docsLink: "文档",
|
||||
apiRefLink: "API 参考(Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1765,7 +1770,9 @@ export const zhCN: TranslationKeys = {
|
||||
pipelineName: "Pipeline 名称",
|
||||
pipelineDescription: "描述(可选)",
|
||||
noStepsPrompt: "添加步骤来构建自动化流程",
|
||||
step: "步骤",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "步骤",
|
||||
},
|
||||
nav: {
|
||||
tools: "工具",
|
||||
@@ -1778,10 +1785,29 @@ export const zhCN: TranslationKeys = {
|
||||
grid: "网格",
|
||||
},
|
||||
files: {
|
||||
recentTab: "最近",
|
||||
uploadTab: "上传",
|
||||
fileDetailsAriaLabel: "文件详情",
|
||||
fileDetailsHeading: "文件详情",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
unsupportedFileType: "此工具不支持该文件类型",
|
||||
|
||||
@@ -36,7 +36,9 @@ export const zhTW: TranslationKeys = {
|
||||
unexpectedError: "發生了非預期的錯誤。",
|
||||
retry: "重試",
|
||||
privacyPolicy: "隱私權政策",
|
||||
},
|
||||
pageNotFound: "Page not found",
|
||||
pageNotFoundDescription: "The page you are looking for does not exist or has been moved.",
|
||||
},
|
||||
categories: {
|
||||
essentials: "基本工具",
|
||||
optimization: "最佳化",
|
||||
@@ -1624,6 +1626,9 @@ export const zhTW: TranslationKeys = {
|
||||
githubLink: "GitHub儲存庫",
|
||||
docsLink: "說明文件",
|
||||
apiRefLink: "API參考(Swagger)",
|
||||
licenseLabel: "License:",
|
||||
licenseDescription:
|
||||
"SnapOtter is open-source software licensed under the GNU Affero General Public License v3 (AGPLv3).",
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -1763,7 +1768,9 @@ export const zhTW: TranslationKeys = {
|
||||
pipelineName: "Pipeline名稱",
|
||||
pipelineDescription: "描述(選填)",
|
||||
noStepsPrompt: "加入步驟來建構自動化",
|
||||
step: "步驟",
|
||||
noStepsHeading: "No steps yet",
|
||||
searchToolsPlaceholder: "Search tools...",
|
||||
step: "步驟",
|
||||
},
|
||||
nav: {
|
||||
tools: "工具",
|
||||
@@ -1776,10 +1783,29 @@ export const zhTW: TranslationKeys = {
|
||||
grid: "格線",
|
||||
},
|
||||
files: {
|
||||
recentTab: "最近",
|
||||
uploadTab: "上傳",
|
||||
fileDetailsAriaLabel: "檔案詳情",
|
||||
fileDetailsHeading: "檔案詳情",
|
||||
myFiles: "My Files",
|
||||
recentTab: "Recent",
|
||||
uploadTab: "Upload Files",
|
||||
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: {
|
||||
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