diff --git a/src/renderer/src/components/editor/IpynbCellRun.tsx b/src/renderer/src/components/editor/IpynbCellRun.tsx
index 370588d6b52..c0398398f88 100644
--- a/src/renderer/src/components/editor/IpynbCellRun.tsx
+++ b/src/renderer/src/components/editor/IpynbCellRun.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from 'react'
+import { memo, useMemo } from 'react'
import { formatToolDuration } from '../../../../shared/native-chat/tools/native-chat-tool-identity'
import { IpynbCellOutputs } from './IpynbCellOutputs'
import { IpynbRunPrompt } from './IpynbCellToolbar'
@@ -30,7 +30,8 @@ export function IpynbCellRunPrompt({
}
/** A cell's outputs: live from its run until the document takes them in, then from the document. */
-export function IpynbCellRunOutputs({
+// Why: every keystroke re-renders the viewer, and re-parsing each cell's saved outputs costs ms.
+export const IpynbCellRunOutputs = memo(function IpynbCellRunOutputs({
filePath,
cellKey,
cell
@@ -50,4 +51,4 @@ export function IpynbCellRunOutputs({
[cell, run]
)
return
-}
+})
diff --git a/src/renderer/src/components/editor/IpynbCellRunOutputs.language.test.tsx b/src/renderer/src/components/editor/IpynbCellRunOutputs.language.test.tsx
new file mode 100644
index 00000000000..5d5d2392e0f
--- /dev/null
+++ b/src/renderer/src/components/editor/IpynbCellRunOutputs.language.test.tsx
@@ -0,0 +1,40 @@
+// @vitest-environment happy-dom
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { i18n } from '@/i18n/i18n'
+import { IpynbCellRunOutputs } from './IpynbCellRun'
+import type { IpynbCell } from './ipynb-parse'
+
+vi.mock('@/hooks/use-document-dark-theme', () => ({ useDocumentDarkTheme: () => false }))
+
+const cell: IpynbCell = {
+ id: 'cell-1',
+ kind: 'code',
+ language: 'python',
+ source: 'df',
+ executionCount: 1,
+ outputs: [
+ {
+ kind: 'display',
+ outputType: 'display_data',
+ executionCount: null,
+ items: [{ mime: 'text/html', value: '
' }]
+ }
+ ]
+}
+
+afterEach(async () => {
+ cleanup()
+ await i18n.changeLanguage('en')
+})
+
+describe('memoized notebook cell outputs', () => {
+ it('relabels HTML output when the UI language changes without new props', async () => {
+ await i18n.changeLanguage('en')
+ render()
+ expect(screen.getByTitle('Notebook HTML output')).toBeTruthy()
+
+ await i18n.changeLanguage('es')
+ expect(await screen.findByTitle('Salida HTML del notebook')).toBeTruthy()
+ })
+})
diff --git a/src/renderer/src/components/editor/IpynbHtmlOutput.tsx b/src/renderer/src/components/editor/IpynbHtmlOutput.tsx
index a60490e4e70..61c9dcbbe13 100644
--- a/src/renderer/src/components/editor/IpynbHtmlOutput.tsx
+++ b/src/renderer/src/components/editor/IpynbHtmlOutput.tsx
@@ -1,5 +1,6 @@
import { useMemo } from 'react'
import DOMPurify from 'dompurify'
+import { useTranslation } from 'react-i18next'
import { translate } from '@/i18n/i18n'
import { useDocumentDarkTheme } from '@/hooks/use-document-dark-theme'
@@ -19,6 +20,8 @@ const OUTPUT_DOCUMENT_HEAD = `
`
export function IpynbHtmlOutput({ html }: { html: string }): React.JSX.Element {
+ // Why: memoized cell outputs skip parent re-renders, so subscribe to language changes here.
+ useTranslation()
const colorScheme = useDocumentDarkTheme() ? 'dark' : 'light'
const srcDoc = useMemo(
() =>