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)