diff --git a/apps/web/src/components/common/dropzone.tsx b/apps/web/src/components/common/dropzone.tsx index 9eb97836..ff114776 100644 --- a/apps/web/src/components/common/dropzone.tsx +++ b/apps/web/src/components/common/dropzone.tsx @@ -264,7 +264,7 @@ export function Dropzone({ <>
- or + {t.dropzone.orSeparator}
@@ -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}
{urlError &&

{urlError}

} diff --git a/apps/web/src/components/common/file-library-modal.tsx b/apps/web/src/components/common/file-library-modal.tsx index 0f18be1e..46da53ac 100644 --- a/apps/web/src/components/common/file-library-modal.tsx +++ b/apps/web/src/components/common/file-library-modal.tsx @@ -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([]); const [loading, setLoading] = useState(false); const [importing, setImporting] = useState(false); @@ -151,7 +153,7 @@ export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalPr {/* Header */}
-

Import from Library

+

{t.automate.importFromLibrary}

)} diff --git a/apps/web/src/components/editor/editor-menu-bar.tsx b/apps/web/src/components/editor/editor-menu-bar.tsx index ea0f28e4..6c3e5e2a 100644 --- a/apps/web/src/components/editor/editor-menu-bar.tsx +++ b/apps/web/src/components/editor/editor-menu-bar.tsx @@ -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 }, diff --git a/apps/web/src/components/editor/options/brush-options.tsx b/apps/web/src/components/editor/options/brush-options.tsx index 68af1a06..e236e373 100644 --- a/apps/web/src/components/editor/options/brush-options.tsx +++ b/apps/web/src/components/editor/options/brush-options.tsx @@ -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 (
+ {/* Eraser mode selector */} + {activeTool === "eraser" && ( +
+ Mode +
+ + +
+
+ )} + {/* Size */} )} + + {/* Flow (not for pencil) */} + {activeTool !== "pencil" && ( +
+ Flow + setBrushFlow(Number(e.target.value) / 100)} + className="flex-1 min-w-0" + /> + + {Math.round(brushFlow * 100)}% + +
+ )}
); } diff --git a/apps/web/src/components/editor/options/selection-options.tsx b/apps/web/src/components/editor/options/selection-options.tsx index 88bab21d..4806cdfa 100644 --- a/apps/web/src/components/editor/options/selection-options.tsx +++ b/apps/web/src/components/editor/options/selection-options.tsx @@ -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) && ( + <> +
+
+ Feather: + 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" + /> + px +
+ + )} + {/* Magic Wand tolerance + contiguous */} {isMagicWand && ( <> diff --git a/apps/web/src/components/editor/options/transform-options.tsx b/apps/web/src/components/editor/options/transform-options.tsx index 69a80c84..d2a7818b 100644 --- a/apps/web/src/components/editor/options/transform-options.tsx +++ b/apps/web/src/components/editor/options/transform-options.tsx @@ -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 (
@@ -137,6 +137,24 @@ export function TransformOptions({ api }: { api: TransformToolApi }) { > + +
+ +
); } diff --git a/apps/web/src/components/editor/tools/brush-tool.tsx b/apps/web/src/components/editor/tools/brush-tool.tsx index 22795cd1..9b833aa5 100644 --- a/apps/web/src/components/editor/tools/brush-tool.tsx +++ b/apps/web/src/components/editor/tools/brush-tool.tsx @@ -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, { diff --git a/apps/web/src/components/editor/tools/eraser-tool.tsx b/apps/web/src/components/editor/tools/eraser-tool.tsx index 61f18904..a84b7394 100644 --- a/apps/web/src/components/editor/tools/eraser-tool.tsx +++ b/apps/web/src/components/editor/tools/eraser-tool.tsx @@ -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, { diff --git a/apps/web/src/components/editor/tools/transform-tool.tsx b/apps/web/src/components/editor/tools/transform-tool.tsx index dce009aa..f3665610 100644 --- a/apps/web/src/components/editor/tools/transform-tool.tsx +++ b/apps/web/src/components/editor/tools/transform-tool.tsx @@ -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; @@ -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]); diff --git a/apps/web/src/components/files/file-details.tsx b/apps/web/src/components/files/file-details.tsx index 33616a56..494e32b9 100644 --- a/apps/web/src/components/files/file-details.tsx +++ b/apps/web/src/components/files/file-details.tsx @@ -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", )} > -

Select a file to view details

+

{t.files.selectFilePrompt}

); } @@ -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", )} >
@@ -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) {
-

File Details

+

+ {t.files.fileDetailsHeading} +

- + - + - + 0 ? details.toolChain.map(toolName).join(", ") : "None" + details.toolChain.length > 0 + ? details.toolChain.map(toolName).join(", ") + : t.files.none } />
diff --git a/apps/web/src/components/files/file-list.tsx b/apps/web/src/components/files/file-list.tsx index 8169f7db..a9e2c6a4 100644 --- a/apps/web/src/components/files/file-list.tsx +++ b/apps/web/src/components/files/file-list.tsx @@ -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() { - {someChecked ? `${checkedIds.size} selected` : `${files.length} files`} + {someChecked + ? format(t.files.selectedCount, { count: checkedIds.size }) + : format(t.files.fileCount, { count: files.length })} {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" > - Delete + {t.files.delete} )} @@ -124,7 +129,7 @@ export function FileList() { )} {!loading && !error && files.length === 0 && (
-

No files found

+

{t.files.noFilesFound}

)} {!loading && !error && files.map((file) => )} diff --git a/apps/web/src/components/files/files-nav.tsx b/apps/web/src/components/files/files-nav.tsx index d092185f..3b47b4bf 100644 --- a/apps/web/src/components/files/files-nav.tsx +++ b/apps/web/src/components/files/files-nav.tsx @@ -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 ( -
-

My Files

+
+

{t.files.myFiles}

{items.map((item) => (
); } diff --git a/apps/web/src/components/tools/tool-palette.tsx b/apps/web/src/components/tools/tool-palette.tsx index c8b8b156..418b4d27 100644 --- a/apps/web/src/components/tools/tool-palette.tsx +++ b/apps/web/src/components/tools/tool-palette.tsx @@ -66,12 +66,16 @@ export function ToolPalette({ onAddStep, className }: ToolPaletteProps) { return (
- +
{availableTools.length === 0 ? ( -

No tools found

+

{t.common.noToolsFound}

) : isSearching ? (
{availableTools.map((tool) => ( diff --git a/apps/web/src/hooks/use-editor-shortcuts.ts b/apps/web/src/hooks/use-editor-shortcuts.ts index 223e249c..54911d49 100644 --- a/apps/web/src/hooks/use-editor-shortcuts.ts +++ b/apps/web/src/hooks/use-editor-shortcuts.ts @@ -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(); }, diff --git a/apps/web/src/pages/not-found-page.tsx b/apps/web/src/pages/not-found-page.tsx new file mode 100644 index 00000000..93a08c2e --- /dev/null +++ b/apps/web/src/pages/not-found-page.tsx @@ -0,0 +1,22 @@ +import { Link } from "react-router-dom"; +import { useTranslation } from "@/contexts/i18n-context"; + +export function NotFoundPage() { + const { t } = useTranslation(); + + return ( +
+
+

404

+

{t.common.pageNotFound}

+

{t.common.pageNotFoundDescription}

+ + {t.common.goHome} + +
+
+ ); +} diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts index 218ae24a..a4c41786 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/stores/editor-store.ts @@ -8,6 +8,7 @@ import type { CanvasObject, EditorLayer, EditorState, + EraserMode, FilterConfig, SelectionMode, StrokeDashStyle, @@ -152,6 +153,8 @@ export const useEditorStore = create()( brushSize: 10, brushOpacity: 1, brushHardness: 1, + brushFlow: 1, + eraserMode: "brush" as EraserMode, // --- Colors --- foregroundColor: "#000000", @@ -171,6 +174,7 @@ export const useEditorStore = create()( selectionMode: "new" as SelectionMode, magicWandTolerance: 32, magicWandContiguous: true, + selectionFeather: 0, // --- Crop --- cropState: null, @@ -954,6 +958,7 @@ export const useEditorStore = create()( 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()( 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: () => { diff --git a/apps/web/src/stores/pipeline-store.ts b/apps/web/src/stores/pipeline-store.ts index 7f9879e8..df1c619a 100644 --- a/apps/web/src/stores/pipeline-store.ts +++ b/apps/web/src/stores/pipeline-store.ts @@ -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((set, get) => ({ - steps: [], - expandedStepId: null, - savedPipelines: [], +export const usePipelineStore = create()( + 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, + }), + }, + ), +); diff --git a/apps/web/src/types/editor.ts b/apps/web/src/types/editor.ts index 34d631c4..43240e1f 100644 --- a/apps/web/src/types/editor.ts +++ b/apps/web/src/types/editor.ts @@ -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; diff --git a/packages/shared/src/i18n/ar.ts b/packages/shared/src/i18n/ar.ts index 4fba10d0..e0a2c2aa 100644 --- a/packages/shared/src/i18n/ar.ts +++ b/packages/shared/src/i18n/ar.ts @@ -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: "نوع الملف هذا غير مدعوم بهذه الأداة", diff --git a/packages/shared/src/i18n/de.ts b/packages/shared/src/i18n/de.ts index 6031e2f9..9631cc94 100644 --- a/packages/shared/src/i18n/de.ts +++ b/packages/shared/src/i18n/de.ts @@ -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", diff --git a/packages/shared/src/i18n/en.ts b/packages/shared/src/i18n/en.ts index a8351789..5e8f6c20 100644 --- a/packages/shared/src/i18n/en.ts +++ b/packages/shared/src/i18n/en.ts @@ -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", diff --git a/packages/shared/src/i18n/es.ts b/packages/shared/src/i18n/es.ts index cecfe36e..f1511ff0 100644 --- a/packages/shared/src/i18n/es.ts +++ b/packages/shared/src/i18n/es.ts @@ -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", diff --git a/packages/shared/src/i18n/fr.ts b/packages/shared/src/i18n/fr.ts index 8de25c01..cbf0c72e 100644 --- a/packages/shared/src/i18n/fr.ts +++ b/packages/shared/src/i18n/fr.ts @@ -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", diff --git a/packages/shared/src/i18n/hi.ts b/packages/shared/src/i18n/hi.ts index 6491561a..ec590886 100644 --- a/packages/shared/src/i18n/hi.ts +++ b/packages/shared/src/i18n/hi.ts @@ -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: "इस फाइल प्रकार को यह टूल सपोर्ट नहीं करता", diff --git a/packages/shared/src/i18n/id.ts b/packages/shared/src/i18n/id.ts index bb506a19..bf9585b0 100644 --- a/packages/shared/src/i18n/id.ts +++ b/packages/shared/src/i18n/id.ts @@ -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", diff --git a/packages/shared/src/i18n/it.ts b/packages/shared/src/i18n/it.ts index b7852550..4f4c4a85 100644 --- a/packages/shared/src/i18n/it.ts +++ b/packages/shared/src/i18n/it.ts @@ -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", diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts index 7f1af33b..4f6f51e7 100644 --- a/packages/shared/src/i18n/ja.ts +++ b/packages/shared/src/i18n/ja.ts @@ -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: "このツールではサポートされていないファイルタイプです", diff --git a/packages/shared/src/i18n/ko.ts b/packages/shared/src/i18n/ko.ts index f814640f..1a910826 100644 --- a/packages/shared/src/i18n/ko.ts +++ b/packages/shared/src/i18n/ko.ts @@ -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: "이 도구에서 지원하지 않는 파일 형식입니다", diff --git a/packages/shared/src/i18n/nl.ts b/packages/shared/src/i18n/nl.ts index 441cbbce..e519ecdf 100644 --- a/packages/shared/src/i18n/nl.ts +++ b/packages/shared/src/i18n/nl.ts @@ -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", diff --git a/packages/shared/src/i18n/pl.ts b/packages/shared/src/i18n/pl.ts index 5b5ed803..34f5a797 100644 --- a/packages/shared/src/i18n/pl.ts +++ b/packages/shared/src/i18n/pl.ts @@ -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", diff --git a/packages/shared/src/i18n/pt-BR.ts b/packages/shared/src/i18n/pt-BR.ts index 426da5df..b4be05a5 100644 --- a/packages/shared/src/i18n/pt-BR.ts +++ b/packages/shared/src/i18n/pt-BR.ts @@ -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", diff --git a/packages/shared/src/i18n/ru.ts b/packages/shared/src/i18n/ru.ts index 235fd1ca..217f3efb 100644 --- a/packages/shared/src/i18n/ru.ts +++ b/packages/shared/src/i18n/ru.ts @@ -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: "Этот тип файла не поддерживается данным инструментом", diff --git a/packages/shared/src/i18n/sv.ts b/packages/shared/src/i18n/sv.ts index f8b62c23..6a508cb0 100644 --- a/packages/shared/src/i18n/sv.ts +++ b/packages/shared/src/i18n/sv.ts @@ -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", diff --git a/packages/shared/src/i18n/th.ts b/packages/shared/src/i18n/th.ts index db43c7b2..cf3d897a 100644 --- a/packages/shared/src/i18n/th.ts +++ b/packages/shared/src/i18n/th.ts @@ -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: "ไฟล์ประเภทนี้ไม่รองรับโดยเครื่องมือนี้", diff --git a/packages/shared/src/i18n/tr.ts b/packages/shared/src/i18n/tr.ts index a43c3a24..a3fcf512 100644 --- a/packages/shared/src/i18n/tr.ts +++ b/packages/shared/src/i18n/tr.ts @@ -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", diff --git a/packages/shared/src/i18n/uk.ts b/packages/shared/src/i18n/uk.ts index f63d4d31..0f94de30 100644 --- a/packages/shared/src/i18n/uk.ts +++ b/packages/shared/src/i18n/uk.ts @@ -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: "Цей тип файлу не підтримується цим інструментом", diff --git a/packages/shared/src/i18n/vi.ts b/packages/shared/src/i18n/vi.ts index c1b4f568..489c1a39 100644 --- a/packages/shared/src/i18n/vi.ts +++ b/packages/shared/src/i18n/vi.ts @@ -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ợ", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 4a9b30e3..8cf3d29b 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -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: "此工具不支持该文件类型", diff --git a/packages/shared/src/i18n/zh-TW.ts b/packages/shared/src/i18n/zh-TW.ts index 9aa7be1a..7a6bbf4a 100644 --- a/packages/shared/src/i18n/zh-TW.ts +++ b/packages/shared/src/i18n/zh-TW.ts @@ -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: "此工具不支援該檔案類型", diff --git a/tests/e2e/gui-qa-fixes.spec.ts b/tests/e2e/gui-qa-fixes.spec.ts new file mode 100644 index 00000000..46313430 --- /dev/null +++ b/tests/e2e/gui-qa-fixes.spec.ts @@ -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"); + } + }); +});