From a80ad2c66b917ece7039b4b32859be4a7aea756e Mon Sep 17 00:00:00 2001 From: 5Hyeons Date: Sat, 1 Aug 2026 15:45:27 +0900 Subject: [PATCH] fix(sidebar): keep bottom toolbar tooltips in the selected UI language SidebarToolbar is a React.memo boundary whose props never change when the UI language does, so React skips it and its translate() output stays frozen at whatever language was active when it last rendered. The app always boots in English and applies the persisted locale asynchronously once the lazy catalog loads, so the toolbar and the ScrollToCurrentWorkspaceToolbarButton it renders keep showing "Workspace board" / "Reveal active workspace" even with a non-English UI language. Whether the toolbar wins that race is timing-dependent, which is why this reproduces on Windows but not macOS. Subscribe the memo boundary to language changes with useTranslation(), the same fix SidebarNav and AgentKanbanCard already carry. Co-Authored-By: Claude Opus 5 (1M context) --- .../sidebar/SidebarToolbar.test.tsx | 27 ++++++++++++++++++- .../src/components/sidebar/SidebarToolbar.tsx | 7 +++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/sidebar/SidebarToolbar.test.tsx b/src/renderer/src/components/sidebar/SidebarToolbar.test.tsx index f3dde9621ea1..1ef570524cb9 100644 --- a/src/renderer/src/components/sidebar/SidebarToolbar.test.tsx +++ b/src/renderer/src/components/sidebar/SidebarToolbar.test.tsx @@ -4,6 +4,8 @@ import { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AppState } from '@/store' +import { i18n } from '@/i18n/i18n' +import ko from '@/i18n/locales/ko.json' import SidebarToolbar from './SidebarToolbar' const mocks = vi.hoisted(() => ({ @@ -82,12 +84,15 @@ describe('SidebarToolbar moved workspace board hint', () => { } }) - afterEach(() => { + afterEach(async () => { roots.splice(0).forEach((root) => { act(() => root.unmount()) }) document.body.replaceChildren() vi.clearAllMocks() + // Why: the language-change case mutates the shared i18n instance; restore + // here so a failing assertion cannot leave later tests running in Korean. + await i18n.changeLanguage('en') }) it('does not show the moved hint to brand-new users after their first board click', async () => { @@ -131,6 +136,26 @@ describe('SidebarToolbar moved workspace board hint', () => { expect(window.localStorage.getItem('orca.workspaceBoardMovedHintSeen.v1')).toBe('true') }) + // Why: the toolbar is a React.memo boundary whose props never change on a + // language switch, so without its own useTranslation() subscription it keeps + // the English copy it rendered at boot (the persisted locale is applied + // asynchronously, after the lazy catalog loads). + it('relabels itself when the UI language changes after mount', async () => { + const { container } = await renderToolbar() + expect(container.querySelector('button[aria-label="Workspace board"]')).not.toBeNull() + + await act(async () => { + await i18n.changeLanguage('ko') + }) + + // Why: read the expected copy from the catalog instead of hardcoding it, so + // editing the Korean wording cannot fail this test as a bogus stale-render + // report. + const localized = ko.auto.components.sidebar.SidebarToolbar['49f62c5665'] + expect(localized).not.toBe('Workspace board') + expect(container.querySelector(`button[aria-label="${localized}"]`)).not.toBeNull() + }) + it('renders the profile switcher before settings in the footer controls', async () => { const { container } = await renderToolbar() const html = container.innerHTML diff --git a/src/renderer/src/components/sidebar/SidebarToolbar.tsx b/src/renderer/src/components/sidebar/SidebarToolbar.tsx index ff2e030c7a51..dcf572488398 100644 --- a/src/renderer/src/components/sidebar/SidebarToolbar.tsx +++ b/src/renderer/src/components/sidebar/SidebarToolbar.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Kanban } from 'lucide-react' +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' import { ScrollToCurrentWorkspaceToolbarButton } from './ScrollToCurrentWorkspaceToolbarButton' @@ -23,6 +24,12 @@ const SidebarToolbar = React.memo(function SidebarToolbar({ workspaceBoardDragPreviewOpen = false, onWorkspaceBoardToggle }: SidebarToolbarProps) { + // Why: this memo boundary needs its own language subscription, while + // translate() preserves Orca's pseudo-localization behavior. Without it the + // toolbar (and the ScrollToCurrentWorkspaceToolbarButton it renders) keeps + // whatever language was active at boot — English, since the persisted locale + // is applied asynchronously after the lazy catalog loads. + useTranslation() const [workspaceBoardMovedHintOpen, setWorkspaceBoardMovedHintOpen] = React.useState(false) const movedHintEligibleRef = React.useRef(null) const persistedUIReady = useAppStore((state) => state.persistedUIReady)