Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions config/scripts/locale-ko-key-overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "보내지 않음"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLElement | null> = { 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 (
<ContextualTourOverlaySurface
activeTourId="automations"
renderState={renderState}
panelRef={panelRef}
panelHost={null}
onSkip={vi.fn()}
onBack={vi.fn()}
onNext={vi.fn()}
onStepAction={vi.fn()}
onOverlayKeyDownCapture={handleContextualTourOverlayKeyDown}
/>
)
}

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<')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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('자동화란 무엇인가요?')
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('shows all defined browser steps in progress even when step 3 is hidden', () => {
const tour = getContextualTour('browser')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, { title: () => 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[]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 13 additions & 2 deletions src/renderer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13235,7 +13235,8 @@
"4a9568f773": "Back",
"4f86e2a10b": "Skip tour",
"d974f32a83": "Dismiss tour",
"ffa4412b66": "next"
"ffa4412b66": "next",
"complete": "Done"
},
"ContextualTourProgressDots": {
"7734cb8ad3": "of",
Expand All @@ -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 ran, what happened, and where to inspect their output."
}
}
}
}
}
Expand Down
15 changes: 13 additions & 2 deletions src/renderer/src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -13175,7 +13175,8 @@
"4a9568f773": "뒤로",
"4f86e2a10b": "둘러보기 건너뛰기",
"d974f32a83": "투어 닫기",
"ffa4412b66": "다음"
"ffa4412b66": "다음",
"complete": "완료"
},
"ContextualTourProgressDots": {
"7734cb8ad3": "중",
Expand All @@ -13185,7 +13186,17 @@
"tour": {
"overlay": {
"measurement": {
"38b3155418": "다음"
"38b3155418": "다음",
"automations": {
"intro": {
"title": "자동화란 무엇인가요?",
"body": "자동화는 일정에 따라 agent 작업을 실행합니다. 이 버튼을 눌러 자동화를 추가하세요."
},
"results": {
"title": "결과 확인",
"body": "실행 내역에서 자동화가 언제 실행되었는지, 어떤 일이 발생했는지, 출력을 어디서 확인할 수 있는지 볼 수 있습니다."
}
}
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/shared/contextual-tours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]'
}
]
Expand Down
Loading