diff --git a/config/scripts/locale-ko-key-overrides.json b/config/scripts/locale-ko-key-overrides.json index 9b40ad61f50..da2d69dc68e 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 00000000000..b9923d0fc11 --- /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 a692a6cdaf1..5b6284ae648 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 d83fafd3fc1..4b2ce9631ca 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,104 @@ -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('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 a807ade9fe4..1fe945a42cb 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,6 +119,13 @@ export function measureContextualTourOverlayRenderState(args: { ) const activeStep = args.tour.steps[args.activeStepIndex] const target = activeStep ? getMeasurableContextualTourTarget(activeStep.targetSelector) : null + 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, @@ -135,8 +172,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 30ff9561d26..b3ce98a4edd 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -13500,7 +13500,8 @@ "4a9568f773": "Back", "4f86e2a10b": "Skip tour", "d974f32a83": "Dismiss tour", - "ffa4412b66": "next" + "ffa4412b66": "next", + "complete": "Done" }, "ContextualTourProgressDots": { "7734cb8ad3": "of", @@ -13510,7 +13511,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 ran, 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 ac8c5964157..e45434e3f6e 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -13294,7 +13294,8 @@ "4a9568f773": "뒤로", "4f86e2a10b": "둘러보기 건너뛰기", "d974f32a83": "투어 닫기", - "ffa4412b66": "다음" + "ffa4412b66": "다음", + "complete": "완료" }, "ContextualTourProgressDots": { "7734cb8ad3": "중", @@ -13304,7 +13305,17 @@ "tour": { "overlay": { "measurement": { - "38b3155418": "다음" + "38b3155418": "다음", + "automations": { + "intro": { + "title": "자동화란 무엇인가요?", + "body": "자동화는 일정에 따라 agent 작업을 실행합니다. 이 버튼을 눌러 자동화를 추가하세요." + }, + "results": { + "title": "결과 확인", + "body": "실행 내역에서 자동화가 언제 실행되었는지, 어떤 일이 발생했는지, 출력을 어디서 확인할 수 있는지 볼 수 있습니다." + } + } } } } diff --git a/src/shared/contextual-tours.ts b/src/shared/contextual-tours.ts index e57161d3860..623a0bbd6ec 100644 --- a/src/shared/contextual-tours.ts +++ b/src/shared/contextual-tours.ts @@ -30,6 +30,8 @@ export type ContextualTourStepAction = { export type ContextualTourStepPlacement = 'top' | 'right' | 'bottom' | 'left' export type ContextualTourStep = { + // Stable anchor for localized copy — position-keyed translations shift onto the wrong step when one is inserted. + id?: string title: string body: string targetSelector: string @@ -146,14 +148,16 @@ 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 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"]' } ]