diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a3bcf07..07e20d7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,11 @@ on: push: branches: - main + - v2 pull_request: branches: - main + - v2 workflow_dispatch: permissions: @@ -108,7 +110,7 @@ jobs: - name: Test env: MARKRA_APP_TEST_SHARD: ${{ matrix.shard }} - run: pnpm --filter "${{ matrix.package }}" test -- ${{ matrix.args }} + run: pnpm --filter "${{ matrix.package }}" test ${{ matrix.args }} frontend-test-typecheck: name: Frontend Test Typecheck diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx index ce28eeca..9f7a47b2 100644 --- a/packages/app/src/App.test.tsx +++ b/packages/app/src/App.test.tsx @@ -1290,7 +1290,7 @@ describe("Markra workspace", () => { expect(screen.getByRole("complementary", { name: "Markdown file tree" })).toHaveAttribute("aria-hidden", "false"); expect(screen.getByRole("tab", { name: /browser\.md/ })).toBeInTheDocument(); expect(container.querySelector(".native-titlebar")).toHaveStyle({ - gridTemplateColumns: "minmax(0,1fr) 164px", + gridTemplateColumns: "minmax(0,1fr) 196px", left: "289px" }); expect(container.querySelector(".windows-app-chrome")).not.toBeInTheDocument(); @@ -3260,7 +3260,7 @@ describe("Markra workspace", () => { width: "360px" }); expect(container.querySelector(".native-title-slot")).toHaveStyle({ - marginLeft: "196px" + marginLeft: "164px" }); expect(container.querySelector(".markdown-file-tree")).toHaveStyle({ width: "360px" @@ -3275,7 +3275,7 @@ describe("Markra workspace", () => { gridTemplateColumns: "220px minmax(0,1fr)" }); expect(container.querySelector(".native-title-slot")).toHaveStyle({ - marginLeft: "56px" + marginLeft: "24px" }); expect(container.querySelector(".native-title-slot")).not.toHaveAttribute("data-tauri-drag-region"); expect(container.querySelector(".document-tabs-drag-spacer")).toHaveAttribute("data-tauri-drag-region"); @@ -6515,6 +6515,162 @@ describe("Markra workspace", () => { expect(screen.getByLabelText("Markdown editor")).toHaveAttribute("data-editor-engine", "codemirror"); }); + it("commits pending visual IME content before source mode mounts", async () => { + renderApp(); + + await expectVisibleMarkdownText("Welcome to Markra"); + const visualEditor = screen.getByRole("textbox", { name: "Markdown document" }); + const visualView = getMarkdownSourceView(visualEditor); + const pendingCompositionTasks: VoidFunction[] = []; + const queueMicrotaskSpy = vi + .spyOn(globalThis, "queueMicrotask") + .mockImplementation((task) => pendingCompositionTasks.push(task)); + try { + act(() => { + fireEvent.compositionStart(visualEditor); + visualView.dispatch({ + changes: { + from: 0, + insert: "# Pending visual edit\n\nNot published by the IME yet.", + to: visualView.state.doc.length + } + }); + fireEvent.compositionEnd(visualEditor); + }); + } finally { + queueMicrotaskSpy.mockRestore(); + } + + await selectEditorViewMode("Source code"); + + expect(readMarkdownSource(await screen.findByRole("textbox", { name: "Markdown source" }))).toBe( + "# Pending visual edit\n\nNot published by the IME yet." + ); + act(() => pendingCompositionTasks.forEach((task) => task())); + }); + + it("keeps the active tab content across editor modes after a prior tab finishes IME composition", async () => { + const bulletinPath = "/mock-files/vault/bulletin.md"; + const tempPath = "/mock-files/vault/temp.md"; + mockedGetStoredWorkspaceState.mockResolvedValue({ + aiAgentSessionId: "session-source-tabs", + filePath: bulletinPath, + fileTreeOpen: false, + folderName: null, + folderPath: null, + openFilePaths: [bulletinPath, tempPath] + }); + mockedReadNativeMarkdownFile.mockImplementation(async (path) => path === bulletinPath + ? { + content: "# Bulletin\n\nFirst document.", + name: "bulletin.md", + path + } + : { + content: "# Temporary notes\n\nSecond document.", + name: "temp.md", + path + }); + + renderApp(); + + expect(await screen.findByRole("heading", { name: "Bulletin" })).toBeInTheDocument(); + const bulletinEditor = screen.getByRole("textbox", { name: "Markdown document" }); + const bulletinView = getMarkdownSourceView(bulletinEditor); + const pendingCompositionTasks: VoidFunction[] = []; + const queueMicrotaskSpy = vi + .spyOn(globalThis, "queueMicrotask") + .mockImplementation((task) => pendingCompositionTasks.push(task)); + try { + act(() => { + fireEvent.compositionStart(bulletinEditor); + bulletinView.dispatch({ + changes: { + from: 0, + insert: "# Bulletin\n\nFinished Chinese composition.", + to: bulletinView.state.doc.length + } + }); + fireEvent.compositionEnd(bulletinEditor); + }); + } finally { + queueMicrotaskSpy.mockRestore(); + } + fireEvent.click(screen.getByRole("tab", { name: /temp\.md/ })); + + await selectEditorViewMode("Source code"); + + expect(readMarkdownSource(await screen.findByRole("textbox", { name: "Markdown source" }))).toBe( + "# Temporary notes\n\nSecond document." + ); + act(() => pendingCompositionTasks.forEach((task) => task())); + expect(readMarkdownSource(screen.getByRole("textbox", { name: "Markdown source" }))).toBe( + "# Temporary notes\n\nSecond document." + ); + + fireEvent.click(screen.getByRole("tab", { name: /bulletin\.md/ })); + expect(readMarkdownSource(await screen.findByRole("textbox", { name: "Markdown source" }))).toBe( + "# Bulletin\n\nFinished Chinese composition." + ); + fireEvent.click(screen.getByRole("tab", { name: /temp\.md/ })); + + await selectEditorViewMode("Preview + Source"); + + expect(readMarkdownSource(screen.getByRole("textbox", { name: "Markdown source" }))).toBe( + "# Temporary notes\n\nSecond document." + ); + await expectVisibleCodeMirrorText(document.body, "Temporary notes"); + expect(readMarkdownSource(screen.getByRole("textbox", { name: "Markdown document" }))).toBe( + "# Temporary notes\n\nSecond document." + ); + + await selectEditorViewMode("Preview"); + + await expectVisibleCodeMirrorText(document.body, "Temporary notes"); + expect(readMarkdownSource(screen.getByRole("textbox", { name: "Markdown document" }))).toBe( + "# Temporary notes\n\nSecond document." + ); + }); + + it("isolates source editor instances between document tabs", async () => { + const firstPath = "/mock-files/vault/first.md"; + const secondPath = "/mock-files/vault/second.md"; + mockedGetStoredWorkspaceState.mockResolvedValue({ + aiAgentSessionId: "session-isolated-source-tabs", + filePath: firstPath, + fileTreeOpen: false, + folderName: null, + folderPath: null, + openFilePaths: [firstPath, secondPath] + }); + mockedReadNativeMarkdownFile.mockImplementation(async (path) => path === firstPath + ? { + content: "# First document", + name: "first.md", + path + } + : { + content: "# Second document", + name: "second.md", + path + }); + + renderApp(); + + expect(await screen.findByRole("heading", { name: "First document" })).toBeInTheDocument(); + await selectEditorViewMode("Source code"); + const firstSourceView = getMarkdownSourceView( + await screen.findByRole("textbox", { name: "Markdown source" }) + ); + + fireEvent.click(screen.getByRole("tab", { name: /second\.md/ })); + + const secondSourceEditor = await screen.findByRole("textbox", { name: "Markdown source" }); + const secondSourceView = getMarkdownSourceView(secondSourceEditor); + expect(secondSourceView).not.toBe(firstSourceView); + expect(secondSourceView.state.doc.toString()).toBe("# Second document"); + }); + it("shows optional line numbers in source and split source modes", async () => { mockedGetStoredEditorPreferences.mockResolvedValue(createStoredEditorPreferences({ showLineNumbers: true diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c79f7cf4..7c4dfbee 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -68,6 +68,7 @@ import { useMarkdownDocument, type ActiveDiskFileContentChange } from "./hooks/u import { useMarkdownFileTree } from "./hooks/useMarkdownFileTree"; import { useSelectionToolbarAnchorRefresh } from "./hooks/useSelectionToolbarAnchorRefresh"; import { useSharedEditorHistory } from "./hooks/useSharedEditorHistory"; +import { routeMarkdownChangeToTab } from "./hooks/markdown-document/editor-sync"; import { useSideBySideTabs } from "./hooks/useSideBySideTabs"; import { useAutoUpdater } from "./hooks/useAutoUpdater"; import { useBackupSettings } from "./hooks/useBackupSettings"; @@ -1003,6 +1004,7 @@ function WorkspaceApp() { syncSourceEditsToVisualHistory } = useSharedEditorHistory({ documentContent: document.content, + documentKey: activeTabId, documentRevision: document.revision, largeMarkdownVisualBlocked, replaceEditorMarkdown, @@ -2939,7 +2941,11 @@ function WorkspaceApp() { const handleSideDocumentPaneFocus = useCallback(() => { setDocumentOperationTarget("side"); }, []); - const handleVisualMarkdownChange = useCallback((content: string, options?: { documentRevision?: number }) => { + const handleVisualMarkdownTabChange = useCallback(( + tabId: string, + content: string, + options?: { documentRevision?: number } + ) => { if (isApplyingSourceToVisualSync()) return; if (syncingExternalDocumentHistoryRef.current) return; if (sourceMode) return; @@ -2953,16 +2959,28 @@ function WorkspaceApp() { } if (splitMode && activeEditorSurface !== "source") setActiveEditorSurface("visual"); - handleMarkdownChange(content, { ...options, surface: "visual" }); + // IME completion can publish after another tab becomes active, so preserve the + // originating tab identity instead of writing through the current-document path. + routeMarkdownChangeToTab({ + content, + documentRevision: options?.documentRevision, + handleMarkdownTabChange, + surface: "visual", + tabId + }); }, [ activeEditorSurface, - handleMarkdownChange, + handleMarkdownTabChange, isApplyingSourceToVisualSync, readOnlyMode, sourceMode, splitMode ]); - const handleSourceMarkdownChange = useCallback((content: string, options?: { documentRevision?: number }) => { + const handleSourceMarkdownTabChange = useCallback(( + tabId: string, + content: string, + options?: { documentRevision?: number } + ) => { if (readOnlyMode) return; if ( content !== document.content && @@ -2973,15 +2991,29 @@ function WorkspaceApp() { markSourceEditForHistory(content, options); if (splitMode) setActiveEditorSurface("source"); - handleMarkdownChange(content, { ...options, surface: "source" }); + routeMarkdownChangeToTab({ + content, + documentRevision: options?.documentRevision, + handleMarkdownTabChange, + surface: "source", + tabId + }); }, [ document.content, document.revision, - handleMarkdownChange, + handleMarkdownTabChange, markSourceEditForHistory, readOnlyMode, splitMode ]); + const handleSourceMarkdownChange = useCallback(( + content: string, + options?: { documentRevision?: number } + ) => { + if (!activeTabId) return; + + handleSourceMarkdownTabChange(activeTabId, content, options); + }, [activeTabId, handleSourceMarkdownTabChange]); const syncSplitPaneScrollPosition = useCallback((sourceSurface: EditorSurface, sourceElement: HTMLElement) => { if (!splitMode) return false; @@ -3499,11 +3531,36 @@ function WorkspaceApp() { path: document.path }; }, [activeImageFile, document.content, document.name, document.path, hasOpenDocument]); + const commitActiveVisualMarkdown = useCallback(() => { + if (!activeTabId) return; + if (editorMode === "source") return; + if (editorMode === "split" && activeEditorSurface === "source") return; + + const activeVisualEditor = mainVisualEditorsRef.current.get(activeTabId); + if (!activeVisualEditor) return; + + handleVisualMarkdownTabChange( + activeTabId, + getMarkdownFromEditor(activeVisualEditor, document.content), + { documentRevision: document.revision } + ); + }, [ + activeEditorSurface, + activeTabId, + document.content, + document.revision, + editorMode, + getMarkdownFromEditor, + handleVisualMarkdownTabChange + ]); const handleEditorModeSelect = useCallback((nextMode: EditorMode) => { if (!sourceModeAvailable) return; if (nextMode === editorMode) return; captureActiveDocumentViewState(); + // IME changes can still be pending in the visual surface when source mode + // mounts, so snapshot the originating editor before changing surfaces. + commitActiveVisualMarkdown(); if (nextMode === "visual") { if (sourceMode) syncSourceEditsToVisualHistory(); @@ -3530,6 +3587,7 @@ function WorkspaceApp() { }, [ captureActiveDocumentViewState, clearSideDocumentGroup, + commitActiveVisualMarkdown, editorMode, handleAiCommandClose, queueEditorModeScroll, @@ -4214,15 +4272,11 @@ function WorkspaceApp() { onEditorReady={(readyEditor, disposedEditor) => handleMainVisualEditorReady(tab.id, readyEditor, disposedEditor) } - onMarkdownChange={(content) => { - const options = { documentRevision: tab.revision }; - if (tabActive) { - handleVisualMarkdownChange(content, options); - return; - } - - handleMarkdownTabChange(tab.id, content, { ...options, surface: "visual" }); - }} + onMarkdownChange={(content) => handleVisualMarkdownTabChange( + tab.id, + content, + { documentRevision: tab.revision } + )} onContentWidthChange={editorWidthResizerVisible ? handleEditorContentWidthChange : undefined} onContentWidthResizeEnd={editorWidthResizerVisible ? handleEditorContentWidthResizeEnd : undefined} onSaveClipboardAttachment={handleSaveClipboardAttachment} @@ -4605,6 +4659,7 @@ function WorkspaceApp() {
handleSourceMarkdownChange(content, { - documentRevision: document.revision - })} + onChange={(content) => handleSourceMarkdownTabChange( + activeTabId ?? "untitled:0", + content, + { + documentRevision: document.revision + } + )} onContentWidthChange={editorWidthResizerVisible ? handleEditorContentWidthChange : undefined} onContentWidthResizeEnd={editorWidthResizerVisible ? handleEditorContentWidthResizeEnd : undefined} onScroll={handleSourcePaneScroll} @@ -4641,6 +4700,7 @@ function WorkspaceApp() { {mainVisualEditors} {sourceMode ? ( handleSourceMarkdownChange(content, { - documentRevision: document.revision - })} + onChange={(content) => handleSourceMarkdownTabChange( + activeTabId ?? "untitled:0", + content, + { + documentRevision: document.revision + } + )} onContentWidthChange={editorWidthResizerVisible ? handleEditorContentWidthChange : undefined} onContentWidthResizeEnd={editorWidthResizerVisible ? handleEditorContentWidthResizeEnd : undefined} onScroll={handleSourcePaneScroll} diff --git a/packages/app/src/hooks/markdown-document/editor-sync.test.ts b/packages/app/src/hooks/markdown-document/editor-sync.test.ts index 399c75e8..be0c5335 100644 --- a/packages/app/src/hooks/markdown-document/editor-sync.test.ts +++ b/packages/app/src/hooks/markdown-document/editor-sync.test.ts @@ -1,5 +1,8 @@ -import { describe, expect, it } from "vitest"; -import { createEditorSyncState } from "./editor-sync"; +import { describe, expect, it, vi } from "vitest"; +import { + createEditorSyncState, + routeMarkdownChangeToTab +} from "./editor-sync"; describe("editor sync state", () => { it("recognizes previous clean visual content as stale only after the newer content is saved", () => { @@ -32,4 +35,46 @@ describe("editor sync state", () => { "# Guide\n\nOriginal" )).toBe(false); }); + + it("routes a delayed visual editor change to the tab that produced it", () => { + const handleMarkdownTabChange = vi.fn(); + + routeMarkdownChangeToTab({ + content: "# First document\n\nLate IME update.", + documentRevision: 7, + handleMarkdownTabChange, + surface: "visual", + tabId: "file:/mock-files/first.md" + }); + + expect(handleMarkdownTabChange).toHaveBeenCalledWith( + "file:/mock-files/first.md", + "# First document\n\nLate IME update.", + { + documentRevision: 7, + surface: "visual" + } + ); + }); + + it("routes a delayed source editor change to the tab that produced it", () => { + const handleMarkdownTabChange = vi.fn(); + + routeMarkdownChangeToTab({ + content: "# First document\n\nLate source update.", + documentRevision: 8, + handleMarkdownTabChange, + surface: "source", + tabId: "file:/mock-files/first.md" + }); + + expect(handleMarkdownTabChange).toHaveBeenCalledWith( + "file:/mock-files/first.md", + "# First document\n\nLate source update.", + { + documentRevision: 8, + surface: "source" + } + ); + }); }); diff --git a/packages/app/src/hooks/markdown-document/editor-sync.ts b/packages/app/src/hooks/markdown-document/editor-sync.ts index 9b5e9017..166aa32d 100644 --- a/packages/app/src/hooks/markdown-document/editor-sync.ts +++ b/packages/app/src/hooks/markdown-document/editor-sync.ts @@ -7,6 +7,34 @@ type SavedVisualEditorStaleContent = { staleContent: string; }; +type RouteMarkdownChangeToTabOptions = { + content: string; + documentRevision?: number; + handleMarkdownTabChange: ( + tabId: string, + content: string, + options: { + documentRevision?: number; + surface: EditorSurface; + } + ) => unknown; + surface: EditorSurface; + tabId: string; +}; + +export function routeMarkdownChangeToTab({ + content, + documentRevision, + handleMarkdownTabChange, + surface, + tabId +}: RouteMarkdownChangeToTabOptions) { + return handleMarkdownTabChange(tabId, content, { + documentRevision, + surface + }); +} + export function createEditorSyncState() { const cleanVisualContentBeforeDirty = new Map(); const cleanVisualMarkdownBaseline = new Map(); diff --git a/packages/app/src/hooks/useSharedEditorHistory.test.tsx b/packages/app/src/hooks/useSharedEditorHistory.test.tsx index d456f535..9706b848 100644 --- a/packages/app/src/hooks/useSharedEditorHistory.test.tsx +++ b/packages/app/src/hooks/useSharedEditorHistory.test.tsx @@ -7,6 +7,7 @@ describe("useSharedEditorHistory", () => { const { rerender } = renderHook( ({ sourceSurfaceActive }) => useSharedEditorHistory({ documentContent: "# Split\n\nEdited from visual.", + documentKey: "file:/mock-files/split.md", documentRevision: 1, largeMarkdownVisualBlocked: false, replaceEditorMarkdown, @@ -27,6 +28,7 @@ describe("useSharedEditorHistory", () => { const { result, rerender } = renderHook( ({ documentContent, sourceSurfaceActive }) => useSharedEditorHistory({ documentContent, + documentKey: "file:/mock-files/split.md", documentRevision: 1, largeMarkdownVisualBlocked: false, replaceEditorMarkdown, @@ -55,4 +57,39 @@ describe("useSharedEditorHistory", () => { historyBaselineMarkdown: "# Split\n\nOriginal." }); }); + + it("clears pending source history when the active document changes", () => { + const replaceEditorMarkdown = vi.fn(() => true); + const { result, rerender } = renderHook( + ({ documentContent, documentKey }) => useSharedEditorHistory({ + documentContent, + documentKey, + documentRevision: 1, + largeMarkdownVisualBlocked: false, + replaceEditorMarkdown, + sourceSurfaceActive: true, + syncSourceToVisual: false, + visualEditorReadySequence: 1 + }), + { + initialProps: { + documentContent: "# First\n\nOriginal.", + documentKey: "file:/mock-files/first.md" + } + } + ); + + act(() => { + result.current.markSourceEditForHistory("# First\n\nEdited.", { documentRevision: 1 }); + }); + rerender({ + documentContent: "# Second\n\nOriginal.", + documentKey: "file:/mock-files/second.md" + }); + + act(() => { + expect(result.current.syncSourceEditsToVisualHistory()).toBe(false); + }); + expect(replaceEditorMarkdown).not.toHaveBeenCalled(); + }); }); diff --git a/packages/app/src/hooks/useSharedEditorHistory.ts b/packages/app/src/hooks/useSharedEditorHistory.ts index e62367cd..d001fbba 100644 --- a/packages/app/src/hooks/useSharedEditorHistory.ts +++ b/packages/app/src/hooks/useSharedEditorHistory.ts @@ -18,6 +18,7 @@ type ReplaceEditorMarkdown = ( type SharedEditorHistoryOptions = { documentContent: string; + documentKey: string | null; documentRevision: number; largeMarkdownVisualBlocked: boolean; replaceEditorMarkdown: ReplaceEditorMarkdown; @@ -28,6 +29,7 @@ type SharedEditorHistoryOptions = { export function useSharedEditorHistory({ documentContent, + documentKey, documentRevision, largeMarkdownVisualBlocked, replaceEditorMarkdown, @@ -36,8 +38,16 @@ export function useSharedEditorHistory({ visualEditorReadySequence }: SharedEditorHistoryOptions) { const pendingSourceHistoryRef = useRef(null); + const pendingSourceHistoryDocumentKeyRef = useRef(documentKey); const syncingSourceToVisualRef = useRef(false); + if (pendingSourceHistoryDocumentKeyRef.current !== documentKey) { + // A pending baseline belongs to one tab; carrying it across a tab switch + // would splice the previous document into the current document's undo stack. + pendingSourceHistoryDocumentKeyRef.current = documentKey; + pendingSourceHistoryRef.current = null; + } + const isApplyingSourceToVisualSync = useCallback(() => syncingSourceToVisualRef.current, []); const markSourceEditForHistory = useCallback((content: string, options?: MarkdownChangeOptions) => { if ( diff --git a/packages/shared/src/i18n/locales/de.ts b/packages/shared/src/i18n/locales/de.ts index 66632413..dca74c06 100644 --- a/packages/shared/src/i18n/locales/de.ts +++ b/packages/shared/src/i18n/locales/de.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "Schritte", "app.clipboardAttachmentRequiresSavedDocument": "Speichern Sie das Dokument, bevor Sie Dateianhaenge hinzufuegen.", "app.clipboardAttachmentSaveFailed": "Der Dateianhang konnte nicht gespeichert werden.", + "app.assetCleanup.title": "Unbenutzte Bilder bereinigen", + "app.assetCleanup.close": "Bildbereinigung schließen", + "app.assetCleanup.scanning": "Markdown-Verweise werden durchsucht...", + "app.assetCleanup.refresh": "Erneut scannen", + "app.assetCleanup.scanned": "{count} Markdown-Dateien durchsucht", + "app.assetCleanup.unusedSingular": "{count} unbenutztes Bild", + "app.assetCleanup.unusedPlural": "{count} unbenutzte Bilder", + "app.assetCleanup.unreadableSingular": "{count} Markdown-Datei konnte nicht gelesen werden. Die Bereinigung ist deaktiviert.", + "app.assetCleanup.unreadablePlural": "{count} Markdown-Dateien konnten nicht gelesen werden. Die Bereinigung ist deaktiviert.", + "app.assetCleanup.recent": "Kürzlich hinzugefügt", + "app.assetCleanup.empty": "Keine unbenutzten verwalteten Bilder gefunden.", + "app.assetCleanup.trashSingular": "{count} Bild in den Papierkorb verschieben", + "app.assetCleanup.trashPlural": "{count} Bilder in den Papierkorb verschieben", + "app.assetCleanup.trashing": "Wird in den Papierkorb verschoben...", + "app.assetCleanup.error.scan": "Der Arbeitsbereich konnte nicht durchsucht werden. Es wurden keine Bilder geändert.", + "app.assetCleanup.error.changed": "Der Arbeitsbereich wurde während der Bereinigung geändert. Prüfen Sie die aktualisierten Ergebnisse, bevor Sie es erneut versuchen.", + "app.assetCleanup.error.trash": "Einige Bilder konnten nicht in den Papierkorb verschoben werden.", "menu.file": "Datei", "menu.edit": "Bearbeiten", "menu.format": "Format", diff --git a/packages/shared/src/i18n/locales/es.ts b/packages/shared/src/i18n/locales/es.ts index 2eb78c74..5f841602 100644 --- a/packages/shared/src/i18n/locales/es.ts +++ b/packages/shared/src/i18n/locales/es.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "pasos", "app.clipboardAttachmentRequiresSavedDocument": "Guarda el documento antes de agregar archivos adjuntos.", "app.clipboardAttachmentSaveFailed": "No se pudo guardar el archivo adjunto.", + "app.assetCleanup.title": "Limpiar imágenes sin usar", + "app.assetCleanup.close": "Cerrar limpieza de imágenes", + "app.assetCleanup.scanning": "Buscando referencias Markdown...", + "app.assetCleanup.refresh": "Volver a analizar", + "app.assetCleanup.scanned": "Se analizaron {count} archivos Markdown", + "app.assetCleanup.unusedSingular": "{count} imagen sin usar", + "app.assetCleanup.unusedPlural": "{count} imágenes sin usar", + "app.assetCleanup.unreadableSingular": "No se pudo leer {count} archivo Markdown. La limpieza está desactivada.", + "app.assetCleanup.unreadablePlural": "No se pudieron leer {count} archivos Markdown. La limpieza está desactivada.", + "app.assetCleanup.recent": "Recientes", + "app.assetCleanup.empty": "No se encontraron imágenes administradas sin usar.", + "app.assetCleanup.trashSingular": "Mover {count} imagen a la papelera", + "app.assetCleanup.trashPlural": "Mover {count} imágenes a la papelera", + "app.assetCleanup.trashing": "Moviendo a la papelera...", + "app.assetCleanup.error.scan": "No se pudo analizar el espacio de trabajo. No se modificó ninguna imagen.", + "app.assetCleanup.error.changed": "El espacio de trabajo cambió durante la limpieza. Revisa los resultados actualizados antes de volver a intentarlo.", + "app.assetCleanup.error.trash": "No se pudieron mover algunas imágenes a la papelera.", "menu.file": "Archivo", "menu.edit": "Editar", "menu.format": "Formato", diff --git a/packages/shared/src/i18n/locales/fr.ts b/packages/shared/src/i18n/locales/fr.ts index 7890fac7..40151bad 100644 --- a/packages/shared/src/i18n/locales/fr.ts +++ b/packages/shared/src/i18n/locales/fr.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "étapes", "app.clipboardAttachmentRequiresSavedDocument": "Enregistrez le document avant d'ajouter des pieces jointes.", "app.clipboardAttachmentSaveFailed": "Impossible d'enregistrer la piece jointe.", + "app.assetCleanup.title": "Nettoyer les images inutilisées", + "app.assetCleanup.close": "Fermer le nettoyage des images", + "app.assetCleanup.scanning": "Analyse des références Markdown...", + "app.assetCleanup.refresh": "Analyser à nouveau", + "app.assetCleanup.scanned": "{count} fichiers Markdown analysés", + "app.assetCleanup.unusedSingular": "{count} image inutilisée", + "app.assetCleanup.unusedPlural": "{count} images inutilisées", + "app.assetCleanup.unreadableSingular": "{count} fichier Markdown n’a pas pu être lu. Le nettoyage est désactivé.", + "app.assetCleanup.unreadablePlural": "{count} fichiers Markdown n’ont pas pu être lus. Le nettoyage est désactivé.", + "app.assetCleanup.recent": "Récentes", + "app.assetCleanup.empty": "Aucune image gérée inutilisée n’a été trouvée.", + "app.assetCleanup.trashSingular": "Déplacer {count} image vers la corbeille", + "app.assetCleanup.trashPlural": "Déplacer {count} images vers la corbeille", + "app.assetCleanup.trashing": "Déplacement vers la corbeille...", + "app.assetCleanup.error.scan": "Impossible d’analyser l’espace de travail. Aucune image n’a été modifiée.", + "app.assetCleanup.error.changed": "L’espace de travail a changé pendant le nettoyage. Vérifiez les résultats actualisés avant de réessayer.", + "app.assetCleanup.error.trash": "Certaines images n’ont pas pu être déplacées vers la corbeille.", "menu.file": "Fichier", "menu.edit": "Édition", "menu.format": "Format", diff --git a/packages/shared/src/i18n/locales/it.ts b/packages/shared/src/i18n/locales/it.ts index 542399b9..bdd3f9d3 100644 --- a/packages/shared/src/i18n/locales/it.ts +++ b/packages/shared/src/i18n/locales/it.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "passaggi", "app.clipboardAttachmentRequiresSavedDocument": "Salva il documento prima di aggiungere allegati file.", "app.clipboardAttachmentSaveFailed": "Impossibile salvare l'allegato file.", + "app.assetCleanup.title": "Pulisci immagini inutilizzate", + "app.assetCleanup.close": "Chiudi pulizia immagini", + "app.assetCleanup.scanning": "Scansione dei riferimenti Markdown...", + "app.assetCleanup.refresh": "Scansiona di nuovo", + "app.assetCleanup.scanned": "Analizzati {count} file Markdown", + "app.assetCleanup.unusedSingular": "{count} immagine inutilizzata", + "app.assetCleanup.unusedPlural": "{count} immagini inutilizzate", + "app.assetCleanup.unreadableSingular": "Impossibile leggere {count} file Markdown. La pulizia è disattivata.", + "app.assetCleanup.unreadablePlural": "Impossibile leggere {count} file Markdown. La pulizia è disattivata.", + "app.assetCleanup.recent": "Recenti", + "app.assetCleanup.empty": "Non sono state trovate immagini gestite inutilizzate.", + "app.assetCleanup.trashSingular": "Sposta {count} immagine nel Cestino", + "app.assetCleanup.trashPlural": "Sposta {count} immagini nel Cestino", + "app.assetCleanup.trashing": "Spostamento nel Cestino...", + "app.assetCleanup.error.scan": "Impossibile analizzare l'area di lavoro. Nessuna immagine è stata modificata.", + "app.assetCleanup.error.changed": "L'area di lavoro è cambiata durante la pulizia. Controlla i risultati aggiornati prima di riprovare.", + "app.assetCleanup.error.trash": "Non è stato possibile spostare alcune immagini nel Cestino.", "menu.file": "File", "menu.edit": "Modifica", "menu.format": "Formato", diff --git a/packages/shared/src/i18n/locales/ja.ts b/packages/shared/src/i18n/locales/ja.ts index c97e79b3..bf586bce 100644 --- a/packages/shared/src/i18n/locales/ja.ts +++ b/packages/shared/src/i18n/locales/ja.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "ステップ", "app.clipboardAttachmentRequiresSavedDocument": "ファイル添付を追加する前にドキュメントを保存してください。", "app.clipboardAttachmentSaveFailed": "ファイル添付を保存できませんでした。", + "app.assetCleanup.title": "未使用の画像をクリーンアップ", + "app.assetCleanup.close": "画像クリーンアップを閉じる", + "app.assetCleanup.scanning": "Markdown の参照をスキャン中...", + "app.assetCleanup.refresh": "再スキャン", + "app.assetCleanup.scanned": "{count} 件の Markdown ファイルをスキャンしました", + "app.assetCleanup.unusedSingular": "未使用の画像 {count} 件", + "app.assetCleanup.unusedPlural": "未使用の画像 {count} 件", + "app.assetCleanup.unreadableSingular": "{count} 件の Markdown ファイルを読み込めませんでした。クリーンアップは無効です。", + "app.assetCleanup.unreadablePlural": "{count} 件の Markdown ファイルを読み込めませんでした。クリーンアップは無効です。", + "app.assetCleanup.recent": "最近", + "app.assetCleanup.empty": "未使用の管理対象画像は見つかりませんでした。", + "app.assetCleanup.trashSingular": "{count} 件の画像をゴミ箱に移動", + "app.assetCleanup.trashPlural": "{count} 件の画像をゴミ箱に移動", + "app.assetCleanup.trashing": "ゴミ箱に移動中...", + "app.assetCleanup.error.scan": "ワークスペースをスキャンできませんでした。画像は変更されていません。", + "app.assetCleanup.error.changed": "クリーンアップ中にワークスペースが変更されました。更新された結果を確認してから再試行してください。", + "app.assetCleanup.error.trash": "一部の画像をゴミ箱に移動できませんでした。", "menu.file": "ファイル", "menu.edit": "編集", "menu.format": "フォーマット", diff --git a/packages/shared/src/i18n/locales/ko.ts b/packages/shared/src/i18n/locales/ko.ts index 58c94692..b1e913ef 100644 --- a/packages/shared/src/i18n/locales/ko.ts +++ b/packages/shared/src/i18n/locales/ko.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "단계", "app.clipboardAttachmentRequiresSavedDocument": "파일 첨부를 추가하기 전에 문서를 저장하세요.", "app.clipboardAttachmentSaveFailed": "파일 첨부를 저장할 수 없습니다.", + "app.assetCleanup.title": "사용하지 않는 이미지 정리", + "app.assetCleanup.close": "이미지 정리 닫기", + "app.assetCleanup.scanning": "Markdown 참조 검색 중...", + "app.assetCleanup.refresh": "다시 검색", + "app.assetCleanup.scanned": "Markdown 파일 {count}개 검색됨", + "app.assetCleanup.unusedSingular": "사용하지 않는 이미지 {count}개", + "app.assetCleanup.unusedPlural": "사용하지 않는 이미지 {count}개", + "app.assetCleanup.unreadableSingular": "Markdown 파일 {count}개를 읽을 수 없습니다. 정리가 비활성화되었습니다.", + "app.assetCleanup.unreadablePlural": "Markdown 파일 {count}개를 읽을 수 없습니다. 정리가 비활성화되었습니다.", + "app.assetCleanup.recent": "최근", + "app.assetCleanup.empty": "사용하지 않는 관리 이미지가 없습니다.", + "app.assetCleanup.trashSingular": "이미지 {count}개를 휴지통으로 이동", + "app.assetCleanup.trashPlural": "이미지 {count}개를 휴지통으로 이동", + "app.assetCleanup.trashing": "휴지통으로 이동 중...", + "app.assetCleanup.error.scan": "워크스페이스를 검색할 수 없습니다. 변경된 이미지가 없습니다.", + "app.assetCleanup.error.changed": "정리 중 워크스페이스가 변경되었습니다. 새로 고친 결과를 확인한 후 다시 시도하세요.", + "app.assetCleanup.error.trash": "일부 이미지를 휴지통으로 이동할 수 없습니다.", "menu.file": "파일", "menu.edit": "편집", "menu.format": "서식", diff --git a/packages/shared/src/i18n/locales/pt-BR.ts b/packages/shared/src/i18n/locales/pt-BR.ts index 127e30b0..7be5173c 100644 --- a/packages/shared/src/i18n/locales/pt-BR.ts +++ b/packages/shared/src/i18n/locales/pt-BR.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "etapas", "app.clipboardAttachmentRequiresSavedDocument": "Salve o documento antes de adicionar anexos de arquivo.", "app.clipboardAttachmentSaveFailed": "Nao foi possivel salvar o anexo de arquivo.", + "app.assetCleanup.title": "Limpar imagens não utilizadas", + "app.assetCleanup.close": "Fechar limpeza de imagens", + "app.assetCleanup.scanning": "Verificando referências Markdown...", + "app.assetCleanup.refresh": "Verificar novamente", + "app.assetCleanup.scanned": "{count} arquivos Markdown verificados", + "app.assetCleanup.unusedSingular": "{count} imagem não utilizada", + "app.assetCleanup.unusedPlural": "{count} imagens não utilizadas", + "app.assetCleanup.unreadableSingular": "Não foi possível ler {count} arquivo Markdown. A limpeza está desativada.", + "app.assetCleanup.unreadablePlural": "Não foi possível ler {count} arquivos Markdown. A limpeza está desativada.", + "app.assetCleanup.recent": "Recentes", + "app.assetCleanup.empty": "Nenhuma imagem gerenciada não utilizada foi encontrada.", + "app.assetCleanup.trashSingular": "Mover {count} imagem para a Lixeira", + "app.assetCleanup.trashPlural": "Mover {count} imagens para a Lixeira", + "app.assetCleanup.trashing": "Movendo para a Lixeira...", + "app.assetCleanup.error.scan": "Não foi possível verificar o workspace. Nenhuma imagem foi alterada.", + "app.assetCleanup.error.changed": "O workspace foi alterado durante a limpeza. Revise os resultados atualizados antes de tentar novamente.", + "app.assetCleanup.error.trash": "Não foi possível mover algumas imagens para a Lixeira.", "menu.file": "Arquivo", "menu.edit": "Editar", "menu.format": "Formatar", diff --git a/packages/shared/src/i18n/locales/ru.ts b/packages/shared/src/i18n/locales/ru.ts index 540fe3e5..9408f432 100644 --- a/packages/shared/src/i18n/locales/ru.ts +++ b/packages/shared/src/i18n/locales/ru.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "шагов", "app.clipboardAttachmentRequiresSavedDocument": "Сохраните документ перед добавлением файловых вложений.", "app.clipboardAttachmentSaveFailed": "Не удалось сохранить файловое вложение.", + "app.assetCleanup.title": "Очистить неиспользуемые изображения", + "app.assetCleanup.close": "Закрыть очистку изображений", + "app.assetCleanup.scanning": "Поиск ссылок в Markdown...", + "app.assetCleanup.refresh": "Сканировать снова", + "app.assetCleanup.scanned": "Просканировано файлов Markdown: {count}", + "app.assetCleanup.unusedSingular": "{count} неиспользуемое изображение", + "app.assetCleanup.unusedPlural": "{count} неиспользуемых изображений", + "app.assetCleanup.unreadableSingular": "Не удалось прочитать {count} файл Markdown. Очистка отключена.", + "app.assetCleanup.unreadablePlural": "Не удалось прочитать {count} файлов Markdown. Очистка отключена.", + "app.assetCleanup.recent": "Недавние", + "app.assetCleanup.empty": "Неиспользуемые управляемые изображения не найдены.", + "app.assetCleanup.trashSingular": "Переместить {count} изображение в корзину", + "app.assetCleanup.trashPlural": "Переместить {count} изображений в корзину", + "app.assetCleanup.trashing": "Перемещение в корзину...", + "app.assetCleanup.error.scan": "Не удалось просканировать рабочую область. Изображения не были изменены.", + "app.assetCleanup.error.changed": "Рабочая область изменилась во время очистки. Проверьте обновлённые результаты перед повторной попыткой.", + "app.assetCleanup.error.trash": "Некоторые изображения не удалось переместить в корзину.", "menu.file": "Файл", "menu.edit": "Правка", "menu.format": "Формат", diff --git a/packages/shared/src/i18n/locales/zh-TW.ts b/packages/shared/src/i18n/locales/zh-TW.ts index ffb77097..1cc84442 100644 --- a/packages/shared/src/i18n/locales/zh-TW.ts +++ b/packages/shared/src/i18n/locales/zh-TW.ts @@ -862,6 +862,23 @@ const messages: LocaleMessages = { "app.aiAgentProcessSteps": "步", "app.clipboardAttachmentRequiresSavedDocument": "請先儲存文件,再加入檔案附件。", "app.clipboardAttachmentSaveFailed": "無法儲存檔案附件。", + "app.assetCleanup.title": "清理未引用圖片", + "app.assetCleanup.close": "關閉圖片清理", + "app.assetCleanup.scanning": "正在掃描 Markdown 引用...", + "app.assetCleanup.refresh": "重新掃描", + "app.assetCleanup.scanned": "已掃描 {count} 個 Markdown 檔案", + "app.assetCleanup.unusedSingular": "{count} 張未引用圖片", + "app.assetCleanup.unusedPlural": "{count} 張未引用圖片", + "app.assetCleanup.unreadableSingular": "有 {count} 個 Markdown 檔案無法讀取,已停用清理。", + "app.assetCleanup.unreadablePlural": "有 {count} 個 Markdown 檔案無法讀取,已停用清理。", + "app.assetCleanup.recent": "最近新增", + "app.assetCleanup.empty": "沒有發現未引用的託管圖片。", + "app.assetCleanup.trashSingular": "將 {count} 張圖片移到垃圾桶", + "app.assetCleanup.trashPlural": "將 {count} 張圖片移到垃圾桶", + "app.assetCleanup.trashing": "正在移到垃圾桶...", + "app.assetCleanup.error.scan": "無法掃描工作區,沒有修改任何圖片。", + "app.assetCleanup.error.changed": "清理期間工作區發生變化,請檢查重新整理後的結果再重試。", + "app.assetCleanup.error.trash": "部分圖片無法移到垃圾桶。", "menu.file": "檔案", "menu.edit": "編輯", "menu.format": "格式",