From f03b087c13f4f4ad77c779c8bbc2ee4a98d017ae Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Fri, 13 Mar 2026 18:47:04 +0100 Subject: [PATCH] Fix translation language badges not updating after auto-translation (#54) Co-authored-by: hugo --- src/main/ipc/handlers.ts | 2 + src/renderer/App.tsx | 18 +++++++ src/renderer/components/Editor/PostEditor.tsx | 16 +++++++ tests/ipc/handlers.test.ts | 47 +++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index ae61ee3..ac69b1c 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -2018,6 +2018,8 @@ export function registerEventForwarding(bundle: EngineBundle): void { postEngine.on('postCreated', forwardEvent('post:created')); postEngine.on('postUpdated', forwardEvent('post:updated')); postEngine.on('postDeleted', forwardEvent('post:deleted')); + postEngine.on('postTranslationCreated', forwardEvent('post:translationCreated')); + postEngine.on('postTranslationUpdated', forwardEvent('post:translationUpdated')); postEngine.on('rebuildStarted', forwardEvent('posts:rebuildStarted')); postEngine.on('databaseRebuilt', forwardEvent('posts:databaseRebuilt')); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b2b7018..7c5fc57 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -167,6 +167,24 @@ const App: React.FC = () => { }) || (() => {}) ); + // Post translation events (refresh post to update availableLanguages for sidebar badges) + const handlePostTranslationChange = (data: unknown) => { + const translation = data as { translationFor?: string }; + if (translation.translationFor) { + window.electronAPI?.posts.get(translation.translationFor).then((post) => { + if (post) { + updatePost((post as PostData).id, post as PostData); + } + }); + } + }; + unsubscribers.push( + window.electronAPI?.on('post:translationCreated', handlePostTranslationChange) || (() => {}) + ); + unsubscribers.push( + window.electronAPI?.on('post:translationUpdated', handlePostTranslationChange) || (() => {}) + ); + // Media events unsubscribers.push( window.electronAPI?.on('media:imported', (media: unknown) => { diff --git a/src/renderer/components/Editor/PostEditor.tsx b/src/renderer/components/Editor/PostEditor.tsx index 13fee90..93c239f 100644 --- a/src/renderer/components/Editor/PostEditor.tsx +++ b/src/renderer/components/Editor/PostEditor.tsx @@ -416,6 +416,22 @@ export const PostEditor: React.FC = ({ postId }) => { }); }, [loadTranslations]); + // Refresh translations when auto-translation completes for this post + useEffect(() => { + const handleTranslationEvent = (data: unknown) => { + const translation = data as { translationFor?: string }; + if (translation.translationFor === postId) { + loadTranslations().catch(() => {}); + } + }; + const unsubCreated = window.electronAPI?.on('post:translationCreated', handleTranslationEvent); + const unsubUpdated = window.electronAPI?.on('post:translationUpdated', handleTranslationEvent); + return () => { + unsubCreated?.(); + unsubUpdated?.(); + }; + }, [loadTranslations, postId]); + // Debounce content for lightbox-only computations (not time-critical) const debouncedContent = useDebouncedValue(content, 500); diff --git a/tests/ipc/handlers.test.ts b/tests/ipc/handlers.test.ts index c10d824..1bb984d 100644 --- a/tests/ipc/handlers.test.ts +++ b/tests/ipc/handlers.test.ts @@ -26,6 +26,8 @@ vi.mock('electron', () => ({ handle: vi.fn((channel: string, handler: (...args: any[]) => Promise) => { registeredHandlers.set(channel, handler); }), + emit: vi.fn(), + on: vi.fn(), }, dialog: { showOpenDialog: vi.fn(), @@ -3718,4 +3720,49 @@ describe('IPC Handlers', () => { expect(mockTaskManager.runTask).not.toHaveBeenCalled(); }); }); + + describe('registerEventForwarding: post translation events', () => { + it('should forward postTranslationCreated to renderer', async () => { + const { registerEventForwarding } = await import('../../src/main/ipc/handlers'); + mockPostEngine.on.mockClear(); + registerEventForwarding(mockBundle as any); + + const translationCreatedCalls = mockPostEngine.on.mock.calls.filter( + ([event]: [string]) => event === 'postTranslationCreated' + ); + expect(translationCreatedCalls.length).toBeGreaterThanOrEqual(1); + + // Verify the handler calls ipcMain.emit to forward + const { ipcMain } = await import('electron'); + (ipcMain.emit as any).mockClear(); + const handler = translationCreatedCalls[translationCreatedCalls.length - 1][1]; + handler({ id: 't1', translationFor: 'p1', language: 'fr' }); + expect(ipcMain.emit).toHaveBeenCalledWith( + 'forward-to-renderer', + 'post:translationCreated', + { id: 't1', translationFor: 'p1', language: 'fr' } + ); + }); + + it('should forward postTranslationUpdated to renderer', async () => { + const { registerEventForwarding } = await import('../../src/main/ipc/handlers'); + mockPostEngine.on.mockClear(); + registerEventForwarding(mockBundle as any); + + const translationUpdatedCalls = mockPostEngine.on.mock.calls.filter( + ([event]: [string]) => event === 'postTranslationUpdated' + ); + expect(translationUpdatedCalls.length).toBeGreaterThanOrEqual(1); + + const { ipcMain } = await import('electron'); + (ipcMain.emit as any).mockClear(); + const handler = translationUpdatedCalls[translationUpdatedCalls.length - 1][1]; + handler({ id: 't2', translationFor: 'p1', language: 'de' }); + expect(ipcMain.emit).toHaveBeenCalledWith( + 'forward-to-renderer', + 'post:translationUpdated', + { id: 't2', translationFor: 'p1', language: 'de' } + ); + }); + }); });