From b664f1cf1754b3f35ae77589839bf46c3554a410 Mon Sep 17 00:00:00 2001 From: 5Hyeons Date: Fri, 31 Jul 2026 14:02:23 +0900 Subject: [PATCH 1/4] fix(i18n): localize automation contextual tour --- config/scripts/locale-ko-key-overrides.json | 15 +++++ ...alTourOverlaySurface.localization.test.tsx | 56 +++++++++++++++++ .../ContextualTourOverlaySurface.tsx | 7 ++- ...ontextual-tour-overlay-measurement.test.ts | 61 ++++++++++++++++++- .../contextual-tour-overlay-measurement.ts | 33 +++++++++- src/renderer/src/i18n/locales/en.json | 15 ++++- src/renderer/src/i18n/locales/ko.json | 15 ++++- 7 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsx diff --git a/config/scripts/locale-ko-key-overrides.json b/config/scripts/locale-ko-key-overrides.json index 4618f052f1ee..52628c0a81af 100644 --- a/config/scripts/locale-ko-key-overrides.json +++ b/config/scripts/locale-ko-key-overrides.json @@ -752,6 +752,21 @@ "auto.components.contextual.tours.ContextualTourProgressDots.dcd6e6b03e": { "ko": "{{value1}}단계 중 {{value0}}단계" }, + "auto.components.contextual.tours.ContextualTourOverlaySurface.complete": { + "ko": "완료" + }, + "auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.intro.body": { + "ko": "자동화는 일정에 따라 agent 작업을 실행합니다. 이 버튼을 눌러 자동화를 추가하세요." + }, + "auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.intro.title": { + "ko": "자동화란 무엇인가요?" + }, + "auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.body": { + "ko": "실행 내역에서 자동화가 언제 실행되었는지, 어떤 일이 발생했는지, 출력을 어디서 확인할 수 있는지 볼 수 있습니다." + }, + "auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.title": { + "ko": "결과 확인" + }, "auto.components.crash.report.CrashReportDialog.88fea8e84e": { "ko": "보내지 않음" }, diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsx new file mode 100644 index 000000000000..b9923d0fc112 --- /dev/null +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsx @@ -0,0 +1,56 @@ +// @vitest-environment happy-dom + +import type { ReactElement, RefObject } from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { setRendererUiLanguage } from '@/i18n/i18n' +import { + ContextualTourOverlaySurface, + handleContextualTourOverlayKeyDown, + type ActiveTourRenderState +} from './ContextualTourOverlaySurface' + +afterEach(async () => { + await setRendererUiLanguage('en') +}) + +function renderSurface(isLastStep: boolean): ReactElement { + const panelRef: RefObject = { current: null } + const renderState: ActiveTourRenderState = { + rect: new DOMRect(0, 0, 20, 20), + targetElement: document.createElement('button'), + progress: { current: isLastStep ? 2 : 1, total: 2 }, + title: 'Tour title', + body: 'Tour body', + isLastStep, + isFirstStep: !isLastStep, + panelHost: null + } + return ( + + ) +} + +describe('ContextualTourOverlaySurface localization', () => { + it('renders default tour actions in Korean when the UI locale is Korean', async () => { + await setRendererUiLanguage('ko') + + const firstStep = renderToStaticMarkup(renderSurface(false)) + const finalStep = renderToStaticMarkup(renderSurface(true)) + + expect(firstStep).toContain('다음') + expect(firstStep).not.toContain('>Next<') + expect(finalStep).toContain('완료') + expect(finalStep).not.toContain('>Done<') + }) +}) diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx index a692a6cdaf19..5b6284ae6483 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx @@ -107,7 +107,12 @@ export function ContextualTourOverlaySurface({ const stepKey = `${activeTourId}-${renderState.progress.current}` const defaultPrimaryAction = { kind: renderState.isLastStep ? 'complete' : 'next', - label: renderState.isLastStep ? 'Done' : 'Next' + label: renderState.isLastStep + ? translate('auto.components.contextual.tours.ContextualTourOverlaySurface.complete', 'Done') + : translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.38b3155418', + 'Next' + ) } satisfies ContextualTourStepAction const primaryAction = renderState.primaryAction ?? (renderState.hidePrimaryAction ? null : defaultPrimaryAction) diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts index d83fafd3fc17..db6fcdac722a 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts @@ -1,12 +1,71 @@ -import { describe, expect, it } from 'vitest' +// @vitest-environment happy-dom + +import { afterEach, describe, expect, it } from 'vitest' import { getContextualTour } from '../../../../shared/contextual-tours' +import { setRendererUiLanguage } from '@/i18n/i18n' import { getContextualTourDisplayProgress, getContextualTourMeasurementAction, + measureContextualTourOverlayRenderState, isContextualTourLastDisplayStep } from './contextual-tour-overlay-measurement' +afterEach(async () => { + document.body.replaceChildren() + await setRendererUiLanguage('en') +}) + describe('contextual tour overlay measurement', () => { + it('renders the automation tour copy in Korean when the UI locale is Korean', async () => { + await setRendererUiLanguage('ko') + const target = document.createElement('button') + target.setAttribute('data-contextual-tour-target', 'automations-create') + target.getBoundingClientRect = () => new DOMRect(0, 0, 20, 20) + document.body.appendChild(target) + + const result = measureContextualTourOverlayRenderState({ + tour: getContextualTour('automations'), + activeStepIndex: 0, + sidebarOpen: true, + keybindings: undefined, + previousTelemetryTotalSteps: 0 + }) + + expect(result.kind).toBe('render') + if (result.kind !== 'render') { + throw new Error(`Expected render result, received ${result.kind}`) + } + expect(result.renderState.title).toBe('자동화란 무엇인가요?') + expect(result.renderState.body).toBe( + '자동화는 일정에 따라 agent 작업을 실행합니다. 이 버튼을 눌러 자동화를 추가하세요.' + ) + }) + + it('renders the automation results step in Korean when the UI locale is Korean', async () => { + await setRendererUiLanguage('ko') + const target = document.createElement('div') + target.setAttribute('data-contextual-tour-target', 'automations-runs') + target.getBoundingClientRect = () => new DOMRect(0, 0, 20, 20) + document.body.appendChild(target) + + const result = measureContextualTourOverlayRenderState({ + tour: getContextualTour('automations'), + activeStepIndex: 1, + sidebarOpen: true, + keybindings: undefined, + previousTelemetryTotalSteps: 0 + }) + + expect(result.kind).toBe('render') + if (result.kind !== 'render') { + throw new Error(`Expected render result, received ${result.kind}`) + } + expect(result.renderState.title).toBe('결과 확인') + expect(result.renderState.body).toBe( + '실행 내역에서 자동화가 언제 실행되었는지, 어떤 일이 발생했는지, 출력을 어디서 확인할 수 있는지 볼 수 있습니다.' + ) + }) + it('shows all defined browser steps in progress even when step 3 is hidden', () => { const tour = getContextualTour('browser') diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts index a807ade9fe41..b049b9eeadb0 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts @@ -89,6 +89,32 @@ export function measureContextualTourOverlayRenderState(args: { ) const activeStep = args.tour.steps[args.activeStepIndex] const target = activeStep ? getMeasurableContextualTourTarget(activeStep.targetSelector) : null + const localizedTitle = + args.tour.id === 'automations' && args.activeStepIndex === 0 + ? translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.intro.title', + 'What is an automation?' + ) + : args.tour.id === 'automations' && args.activeStepIndex === 1 + ? translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.title', + 'Find the results' + ) + : activeStep?.title + const localizedBody = + args.tour.id === 'automations' && args.activeStepIndex === 0 + ? translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.intro.body', + 'Automations run agent work on a schedule. Add an automation by clicking this button.' + ) + : args.tour.id === 'automations' && args.activeStepIndex === 1 + ? translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.body', + 'Runs show when automations executed, what happened, and where to inspect their output.' + ) + : activeStep + ? getContextualTourStepCopy(activeStep) + : undefined const progress = getContextualTourDisplayProgress({ tour: args.tour, visibleStepIndexes, @@ -135,8 +161,11 @@ export function measureContextualTourOverlayRenderState(args: { rect: target.rect, targetElement: target.element, progress, - title: activeStep.title, - body: formatContextualTourStepCopy(getContextualTourStepCopy(activeStep), args.keybindings), + title: localizedTitle ?? activeStep.title, + body: formatContextualTourStepCopy( + localizedBody ?? getContextualTourStepCopy(activeStep), + args.keybindings + ), control: activeStep.control, primaryAction, secondaryAction, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 5b74621aad73..0d12220097b7 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -13235,7 +13235,8 @@ "4a9568f773": "Back", "4f86e2a10b": "Skip tour", "d974f32a83": "Dismiss tour", - "ffa4412b66": "next" + "ffa4412b66": "next", + "complete": "Done" }, "ContextualTourProgressDots": { "7734cb8ad3": "of", @@ -13245,7 +13246,17 @@ "tour": { "overlay": { "measurement": { - "38b3155418": "Next" + "38b3155418": "Next", + "automations": { + "intro": { + "title": "What is an automation?", + "body": "Automations run agent work on a schedule. Add an automation by clicking this button." + }, + "results": { + "title": "Find the results", + "body": "Runs show when automations executed, what happened, and where to inspect their output." + } + } } } } diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index e1cc97e3a0dd..909dccac2014 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -13175,7 +13175,8 @@ "4a9568f773": "뒤로", "4f86e2a10b": "둘러보기 건너뛰기", "d974f32a83": "투어 닫기", - "ffa4412b66": "다음" + "ffa4412b66": "다음", + "complete": "완료" }, "ContextualTourProgressDots": { "7734cb8ad3": "중", @@ -13185,7 +13186,17 @@ "tour": { "overlay": { "measurement": { - "38b3155418": "다음" + "38b3155418": "다음", + "automations": { + "intro": { + "title": "자동화란 무엇인가요?", + "body": "자동화는 일정에 따라 agent 작업을 실행합니다. 이 버튼을 눌러 자동화를 추가하세요." + }, + "results": { + "title": "결과 확인", + "body": "실행 내역에서 자동화가 언제 실행되었는지, 어떤 일이 발생했는지, 출력을 어디서 확인할 수 있는지 볼 수 있습니다." + } + } } } } From bba6f6dd8c1cb0515568bcdb736bfa2f57a72992 Mon Sep 17 00:00:00 2001 From: 5Hyeons Date: Fri, 31 Jul 2026 23:23:08 +0900 Subject: [PATCH 2/4] fix(i18n): correct automation results copy --- .../contextual-tours/contextual-tour-overlay-measurement.ts | 2 +- src/renderer/src/i18n/locales/en.json | 2 +- src/shared/contextual-tours.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts index b049b9eeadb0..dd7451f2e30f 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts @@ -110,7 +110,7 @@ export function measureContextualTourOverlayRenderState(args: { : args.tour.id === 'automations' && args.activeStepIndex === 1 ? translate( 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.body', - 'Runs show when automations executed, what happened, and where to inspect their output.' + 'Runs show when automations ran, what happened, and where to inspect their output.' ) : activeStep ? getContextualTourStepCopy(activeStep) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 0d12220097b7..1e07e2c10e68 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -13254,7 +13254,7 @@ }, "results": { "title": "Find the results", - "body": "Runs show when automations executed, what happened, and where to inspect their output." + "body": "Runs show when automations ran, what happened, and where to inspect their output." } } } diff --git a/src/shared/contextual-tours.ts b/src/shared/contextual-tours.ts index e57161d38604..375efe9a57e6 100644 --- a/src/shared/contextual-tours.ts +++ b/src/shared/contextual-tours.ts @@ -153,7 +153,7 @@ export const CONTEXTUAL_TOURS = [ }, { title: 'Find the results', - body: 'Runs show when automations executed, what happened, and where to inspect their output.', + body: 'Runs show when automations ran, what happened, and where to inspect their output.', targetSelector: '[data-contextual-tour-target="automations-runs"]' } ] From 09003f6c7c14cd194fe2a817224c944fc1bb4d22 Mon Sep 17 00:00:00 2001 From: 5Hyeons Date: Sun, 2 Aug 2026 12:31:06 +0900 Subject: [PATCH 3/4] fix(i18n): key tour copy off the step id, not its position Localized automation tour copy was selected by matching tour id plus the raw activeStepIndex. Inserting a step ahead of those two, or adding a third, silently shifts the mapping: the intro copy lands on the wrong step and the new one falls through to untranslated English, with no type or test error to catch it. Give ContextualTourStep an optional stable `id` and look the copy up by that instead. Thunks keep translate() out of module scope so the lookup still resolves in the language active at render time, and the four keys stay statically referenced for the extraction gate. Addresses the review comment on #11694. Co-Authored-By: Claude Opus 5 (1M context) --- ...ontextual-tour-overlay-measurement.test.ts | 36 +++++++++++ .../contextual-tour-overlay-measurement.ts | 63 +++++++++++-------- src/shared/contextual-tours.ts | 6 ++ 3 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts index db6fcdac722a..75b5336efca1 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts @@ -66,6 +66,42 @@ describe('contextual tour overlay measurement', () => { ) }) + // Why: localized copy used to be keyed off the step's position, so inserting + // a step ahead of it silently moved the copy onto the wrong step — with no + // type or test error. Keying off the step id has to survive that insertion. + it('keeps localized copy on its own step when a step is inserted before it', async () => { + await setRendererUiLanguage('ko') + const target = document.createElement('button') + target.setAttribute('data-contextual-tour-target', 'automations-create') + target.getBoundingClientRect = () => new DOMRect(0, 0, 20, 20) + document.body.appendChild(target) + + const automations = getContextualTour('automations') + const result = measureContextualTourOverlayRenderState({ + tour: { + ...automations, + steps: [ + { + title: 'Inserted step', + body: 'Added ahead of the localized steps.', + targetSelector: '[data-contextual-tour-target="automations-create"]' + }, + ...automations.steps + ] + }, + activeStepIndex: 1, + sidebarOpen: true, + keybindings: undefined, + previousTelemetryTotalSteps: 0 + }) + + expect(result.kind).toBe('render') + if (result.kind !== 'render') { + throw new Error(`Expected render result, received ${result.kind}`) + } + expect(result.renderState.title).toBe('자동화란 무엇인가요?') + }) + it('shows all defined browser steps in progress even when step 3 is hidden', () => { const tour = getContextualTour('browser') diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts index dd7451f2e30f..1fe945a42cbf 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.ts @@ -28,6 +28,36 @@ export type ContextualTourOverlayMeasurementResult = telemetryTotalSteps: number } +// Why: keyed by the step's stable id, not its position — inserting a step must +// not shift localized copy onto a neighbour. Thunks keep translate() out of +// module scope so the lookup resolves in the language active at render time. +const LOCALIZED_STEP_COPY: Record string; body: () => string }> = { + 'automations-intro': { + title: () => + translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.intro.title', + 'What is an automation?' + ), + body: () => + translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.intro.body', + 'Automations run agent work on a schedule. Add an automation by clicking this button.' + ) + }, + 'automations-results': { + title: () => + translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.title', + 'Find the results' + ), + body: () => + translate( + 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.body', + 'Runs show when automations ran, what happened, and where to inspect their output.' + ) + } +} + export function getContextualTourDisplayProgress(args: { tour: ContextualTour visibleStepIndexes: readonly number[] @@ -89,32 +119,13 @@ export function measureContextualTourOverlayRenderState(args: { ) const activeStep = args.tour.steps[args.activeStepIndex] const target = activeStep ? getMeasurableContextualTourTarget(activeStep.targetSelector) : null - const localizedTitle = - args.tour.id === 'automations' && args.activeStepIndex === 0 - ? translate( - 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.intro.title', - 'What is an automation?' - ) - : args.tour.id === 'automations' && args.activeStepIndex === 1 - ? translate( - 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.title', - 'Find the results' - ) - : activeStep?.title - const localizedBody = - args.tour.id === 'automations' && args.activeStepIndex === 0 - ? translate( - 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.intro.body', - 'Automations run agent work on a schedule. Add an automation by clicking this button.' - ) - : args.tour.id === 'automations' && args.activeStepIndex === 1 - ? translate( - 'auto.components.contextual.tours.contextual.tour.overlay.measurement.automations.results.body', - 'Runs show when automations ran, what happened, and where to inspect their output.' - ) - : activeStep - ? getContextualTourStepCopy(activeStep) - : undefined + const localizedCopy = activeStep?.id ? LOCALIZED_STEP_COPY[activeStep.id] : undefined + const localizedTitle = localizedCopy ? localizedCopy.title() : activeStep?.title + const localizedBody = localizedCopy + ? localizedCopy.body() + : activeStep + ? getContextualTourStepCopy(activeStep) + : undefined const progress = getContextualTourDisplayProgress({ tour: args.tour, visibleStepIndexes, diff --git a/src/shared/contextual-tours.ts b/src/shared/contextual-tours.ts index 375efe9a57e6..8e0164a2a43c 100644 --- a/src/shared/contextual-tours.ts +++ b/src/shared/contextual-tours.ts @@ -30,6 +30,10 @@ export type ContextualTourStepAction = { export type ContextualTourStepPlacement = 'top' | 'right' | 'bottom' | 'left' export type ContextualTourStep = { + // Why: a stable anchor for localized copy. Keying translations off the step's + // position lets an inserted step silently shift copy onto the wrong step, + // with no type or test error to catch it. + id?: string title: string body: string targetSelector: string @@ -146,12 +150,14 @@ export const CONTEXTUAL_TOURS = [ id: 'automations', steps: [ { + id: 'automations-intro', title: 'What is an automation?', body: 'Automations run agent work on a schedule. Add an automation by clicking this button.', targetSelector: '[data-contextual-tour-target="automations-create"]', requiredForStart: true }, { + id: 'automations-results', title: 'Find the results', body: 'Runs show when automations ran, what happened, and where to inspect their output.', targetSelector: '[data-contextual-tour-target="automations-runs"]' From 3af175cada5d64ca5bf562739592d9a7970247f7 Mon Sep 17 00:00:00 2001 From: 5Hyeons Date: Mon, 3 Aug 2026 16:06:44 +0900 Subject: [PATCH 4/4] style(contextual-tours): condense stable-id rationale comments Co-Authored-By: Claude Opus 5 (1M context) Co-authored-by: Orca --- .../contextual-tour-overlay-measurement.test.ts | 3 --- src/shared/contextual-tours.ts | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts index 75b5336efca1..4b2ce9631ca1 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts @@ -66,9 +66,6 @@ describe('contextual tour overlay measurement', () => { ) }) - // Why: localized copy used to be keyed off the step's position, so inserting - // a step ahead of it silently moved the copy onto the wrong step — with no - // type or test error. Keying off the step id has to survive that insertion. it('keeps localized copy on its own step when a step is inserted before it', async () => { await setRendererUiLanguage('ko') const target = document.createElement('button') diff --git a/src/shared/contextual-tours.ts b/src/shared/contextual-tours.ts index 8e0164a2a43c..623a0bbd6ec8 100644 --- a/src/shared/contextual-tours.ts +++ b/src/shared/contextual-tours.ts @@ -30,9 +30,7 @@ export type ContextualTourStepAction = { export type ContextualTourStepPlacement = 'top' | 'right' | 'bottom' | 'left' export type ContextualTourStep = { - // Why: a stable anchor for localized copy. Keying translations off the step's - // position lets an inserted step silently shift copy onto the wrong step, - // with no type or test error to catch it. + // Stable anchor for localized copy — position-keyed translations shift onto the wrong step when one is inserted. id?: string title: string body: string