diff --git a/src/__tests__/required-props-for-components.ts b/src/__tests__/required-props-for-components.ts index 14d4bf233ac..37d6772212d 100644 --- a/src/__tests__/required-props-for-components.ts +++ b/src/__tests__/required-props-for-components.ts @@ -86,8 +86,8 @@ const defaultProps: Record> = { }, 'tutorial-panel': { i18nStrings: { - stepTitle: () => '', - labelTotalSteps: () => '', + tutorialListDescription: '', + labelLearnMoreExternalIcon: '', }, tutorials: [], }, diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index faa0a22efec..1c71d0cc58a 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -35239,27 +35239,28 @@ exports[`Components definition for tutorial-panel matches the snapshot: tutorial }, { "description": "An object containing all the necessary localized strings required by the component.", + "i18nTag": true, "inlineType": { "name": "TutorialPanelProps.I18nStrings", "properties": [ { "name": "completionScreenTitle", - "optional": false, + "optional": true, "type": "string", }, { "name": "dismissTutorialButtonText", - "optional": false, + "optional": true, "type": "string", }, { "name": "feedbackLinkText", - "optional": false, + "optional": true, "type": "string", }, { "name": "labelExitTutorial", - "optional": false, + "optional": true, "type": "string", }, { @@ -35274,29 +35275,29 @@ exports[`Components definition for tutorial-panel matches the snapshot: tutorial }, { "inlineType": { - "name": "{ pending: string; 'in-progress': string; success: string; }", + "name": "{ pending?: string | undefined; 'in-progress'?: string | undefined; success?: string | undefined; }", "properties": [ { "name": "in-progress", - "optional": false, + "optional": true, "type": "string", }, { "name": "pending", - "optional": false, + "optional": true, "type": "string", }, { "name": "success", - "optional": false, + "optional": true, "type": "string", }, ], "type": "object", }, "name": "labelsTaskStatus", - "optional": false, - "type": "{ pending: string; 'in-progress': string; success: string; }", + "optional": true, + "type": "{ pending?: string | undefined; 'in-progress'?: string | undefined; success?: string | undefined; }", }, { "inlineType": { @@ -35311,8 +35312,8 @@ exports[`Components definition for tutorial-panel matches the snapshot: tutorial "type": "function", }, "name": "labelTotalSteps", - "optional": false, - "type": "(totalStepCount: number) => string", + "optional": true, + "type": "((totalStepCount: number) => string)", }, { "name": "labelTutorialListDownloadLink", @@ -35321,22 +35322,22 @@ exports[`Components definition for tutorial-panel matches the snapshot: tutorial }, { "name": "learnMoreLinkText", - "optional": false, + "optional": true, "type": "string", }, { "name": "loadingText", - "optional": false, + "optional": true, "type": "string", }, { "name": "restartTutorialButtonText", - "optional": false, + "optional": true, "type": "string", }, { "name": "startTutorialButtonText", - "optional": false, + "optional": true, "type": "string", }, { @@ -35356,8 +35357,8 @@ exports[`Components definition for tutorial-panel matches the snapshot: tutorial "type": "function", }, "name": "stepTitle", - "optional": false, - "type": "(stepIndex: number, stepTitle: string) => string", + "optional": true, + "type": "((stepIndex: number, stepTitle: string) => string)", }, { "inlineType": { @@ -35376,12 +35377,12 @@ exports[`Components definition for tutorial-panel matches the snapshot: tutorial "type": "function", }, "name": "taskTitle", - "optional": false, - "type": "(taskIndex: number, taskTitle: string) => string", + "optional": true, + "type": "((taskIndex: number, taskTitle: string) => string)", }, { "name": "tutorialCompletedText", - "optional": false, + "optional": true, "type": "string", }, { @@ -35391,12 +35392,12 @@ exports[`Components definition for tutorial-panel matches the snapshot: tutorial }, { "name": "tutorialListDownloadLinkText", - "optional": false, + "optional": true, "type": "string", }, { "name": "tutorialListTitle", - "optional": false, + "optional": true, "type": "string", }, ], diff --git a/src/tutorial-panel/__tests__/tutorial-panel.test.tsx b/src/tutorial-panel/__tests__/tutorial-panel.test.tsx index e9da9c17185..133f3c003dd 100644 --- a/src/tutorial-panel/__tests__/tutorial-panel.test.tsx +++ b/src/tutorial-panel/__tests__/tutorial-panel.test.tsx @@ -7,6 +7,8 @@ import { HotspotContext as HotspotContextType, hotspotContext as HotspotContext, } from '../../../lib/components/annotation-context/context'; +import { I18nProvider } from '../../../lib/components/i18n'; +import messages from '../../../lib/components/i18n/messages/all.en'; import createWrapper from '../../../lib/components/test-utils/dom'; import TutorialPanel, { TutorialPanelProps } from '../../../lib/components/tutorial-panel'; import { getTutorials, i18nStrings } from './data'; @@ -335,3 +337,62 @@ describe('URL sanitization', () => { }); }); }); + +describe('i18n', () => { + const requiredI18nStrings: TutorialPanelProps.I18nStrings = { + tutorialListDescription: 'DESCRIPTION', + labelLearnMoreExternalIcon: 'EXTERNAL_ICON', + }; + + function renderWithProvider( + props: Pick, + contextProps?: Partial + ) { + const tutorials = getTutorials(); + const { container } = render( + + + {}} {...props} /> + + + ); + return { wrapper: createWrapper(container).findTutorialPanel()!, tutorials }; + } + + test('falls back to provider strings in the tutorial list', () => { + const { wrapper } = renderWithProvider({ + i18nStrings: { ...requiredI18nStrings, tutorialListTitle: 'CUSTOM_TITLE' }, + }); + + expect(wrapper.getElement()).toHaveTextContent('CUSTOM_TITLE'); + expect(wrapper.findDownloadLink()!.getElement()).toHaveTextContent('Download PDF version'); + expect(wrapper.findDownloadLink()!.getElement()).toHaveAttribute( + 'aria-label', + 'Download PDF version of this tutorial' + ); + const tutorial = wrapper.findTutorials()[0]; + expect(tutorial.findLearnMoreLink()!.getElement()).toHaveTextContent('Learn more'); + expect(tutorial.findStartButton()!.getElement()).toHaveTextContent('Start tutorial'); + }); + + test('falls back to provider strings in the tutorial detail view', () => { + const tutorial = getTutorials()[0]; + const { wrapper } = renderWithProvider({ i18nStrings: requiredI18nStrings }, { currentTutorial: tutorial }); + + const task = wrapper.findTaskList()[0]; + expect(task.findTitle().getElement()).toHaveTextContent('Task 1: FIRST_TASK_TEST'); + expect(task.findStepsTitle().getElement()).toHaveTextContent('Total steps: 1'); + expect(task.findSteps()[0].getElement()).toHaveTextContent('Step 1: FIRST_STEP_TEST'); + expect(wrapper.findDismissButton()!.getElement()).toHaveTextContent('Dismiss tutorial'); + }); + + test('falls back to provider strings in the completion screen', () => { + const tutorial = { ...getTutorials()[0], completed: true }; + const { wrapper } = renderWithProvider({ i18nStrings: requiredI18nStrings }, { currentTutorial: tutorial }); + + expect(wrapper.findCompletionScreenTitle()!.getElement()).toHaveTextContent( + 'Congratulations! You completed the tutorial.' + ); + expect(wrapper.findFeedbackLink()!.getElement()).toHaveTextContent('Feedback'); + }); +}); diff --git a/src/tutorial-panel/components/tutorial-detail-view/congratulation-screen.tsx b/src/tutorial-panel/components/tutorial-detail-view/congratulation-screen.tsx index 179320294f2..8a41f3cc6d7 100644 --- a/src/tutorial-panel/components/tutorial-detail-view/congratulation-screen.tsx +++ b/src/tutorial-panel/components/tutorial-detail-view/congratulation-screen.tsx @@ -14,7 +14,7 @@ import styles from './styles.css.js'; interface CongratulationScreenProps { children: React.ReactNode; onFeedbackClick?: () => void; - i18nStrings: TutorialPanelProps['i18nStrings']; + i18nStrings: TutorialPanelProps.I18nStrings; } export function CongratulationScreen({ children, onFeedbackClick, i18nStrings }: CongratulationScreenProps) { diff --git a/src/tutorial-panel/components/tutorial-detail-view/index.tsx b/src/tutorial-panel/components/tutorial-detail-view/index.tsx index 87d4a72b0e3..e73141193e8 100644 --- a/src/tutorial-panel/components/tutorial-detail-view/index.tsx +++ b/src/tutorial-panel/components/tutorial-detail-view/index.tsx @@ -26,7 +26,7 @@ export default function TutorialDetailView({ onExitTutorial: HotspotContext['onExitTutorial']; currentStepIndex: HotspotContext['currentStepIndex']; onFeedbackClick: TutorialPanelProps['onFeedbackClick']; - i18nStrings: TutorialPanelProps['i18nStrings']; + i18nStrings: TutorialPanelProps.I18nStrings; }) { const isRefresh = useVisualRefresh(); diff --git a/src/tutorial-panel/components/tutorial-detail-view/task-list.tsx b/src/tutorial-panel/components/tutorial-detail-view/task-list.tsx index a17760db710..0952606fc4a 100644 --- a/src/tutorial-panel/components/tutorial-detail-view/task-list.tsx +++ b/src/tutorial-panel/components/tutorial-detail-view/task-list.tsx @@ -16,7 +16,7 @@ interface TaskListProps { tasks: ReadonlyArray; onExitTutorial: () => void; currentGlobalStepIndex: HotspotContext['currentStepIndex']; - i18nStrings: TutorialPanelProps['i18nStrings']; + i18nStrings: TutorialPanelProps.I18nStrings; } export function TaskList({ tasks, onExitTutorial, currentGlobalStepIndex, i18nStrings }: TaskListProps) { diff --git a/src/tutorial-panel/components/tutorial-detail-view/task.tsx b/src/tutorial-panel/components/tutorial-detail-view/task.tsx index cfee4609717..0453e5f7645 100644 --- a/src/tutorial-panel/components/tutorial-detail-view/task.tsx +++ b/src/tutorial-panel/components/tutorial-detail-view/task.tsx @@ -27,7 +27,7 @@ interface TaskProps { currentTaskIndex: number; expanded: boolean; onToggleExpand: (step: number) => void; - i18nStrings: TutorialPanelProps['i18nStrings']; + i18nStrings: TutorialPanelProps.I18nStrings; } export function Task({ task, taskIndex, currentTaskIndex, expanded, onToggleExpand, i18nStrings }: TaskProps) { @@ -44,7 +44,7 @@ export function Task({ task, taskIndex, currentTaskIndex, expanded, onToggleExpa @@ -54,7 +54,7 @@ export function Task({ task, taskIndex, currentTaskIndex, expanded, onToggleExpa fontSize="heading-s" color={taskIndex < currentTaskIndex ? 'text-status-success' : 'text-status-inactive'} > - {i18nStrings.taskTitle(taskIndex, task.title)} + {i18nStrings.taskTitle?.(taskIndex, task.title)} @@ -62,14 +62,14 @@ export function Task({ task, taskIndex, currentTaskIndex, expanded, onToggleExpa - {i18nStrings.labelTotalSteps(task.steps.length)} + {i18nStrings.labelTotalSteps?.(task.steps.length)} } expanded={expanded} onChange={onExpandChange} headerAriaLabel={joinStrings( - i18nStrings.taskTitle(taskIndex, task.title), - i18nStrings.labelTotalSteps(task.steps.length) + i18nStrings.taskTitle?.(taskIndex, task.title), + i18nStrings.labelTotalSteps?.(task.steps.length) )} >
    @@ -81,7 +81,7 @@ export function Task({ task, taskIndex, currentTaskIndex, expanded, onToggleExpa padding={{ left: 'l' }} className={styles['step-title']} > - {i18nStrings.stepTitle(stepIndex, step.title)} + {i18nStrings.stepTitle?.(stepIndex, step.title)} ))} diff --git a/src/tutorial-panel/components/tutorial-list/index.tsx b/src/tutorial-panel/components/tutorial-list/index.tsx index 1d74ff7e0ac..8fde1ffb8ec 100644 --- a/src/tutorial-panel/components/tutorial-list/index.tsx +++ b/src/tutorial-panel/components/tutorial-list/index.tsx @@ -26,7 +26,7 @@ interface TutorialListProps { loading?: boolean; tutorials: TutorialPanelProps['tutorials']; onStartTutorial: HotspotContext['onStartTutorial']; - i18nStrings: TutorialPanelProps['i18nStrings']; + i18nStrings: TutorialPanelProps.I18nStrings; downloadUrl: TutorialPanelProps['downloadUrl']; headingId?: string; } @@ -98,7 +98,7 @@ function Tutorial({ }: { tutorial: TutorialPanelProps.Tutorial; onStartTutorial: HotspotContext['onStartTutorial']; - i18nStrings: TutorialPanelProps['i18nStrings']; + i18nStrings: TutorialPanelProps.I18nStrings; }) { checkSafeUrl('TutorialPanel', tutorial.learnMoreUrl); const controlId = useUniqueId(); diff --git a/src/tutorial-panel/index.tsx b/src/tutorial-panel/index.tsx index c2c553c687c..87eb264c250 100644 --- a/src/tutorial-panel/index.tsx +++ b/src/tutorial-panel/index.tsx @@ -7,6 +7,7 @@ import clsx from 'clsx'; import { useMergeRefs, useUniqueId } from '@cloudscape-design/component-toolkit/internal'; import { hotspotContext } from '../annotation-context/context'; +import { useInternalI18n } from '../i18n/context'; import { getBaseProps } from '../internal/base-component'; import useBaseComponent from '../internal/hooks/use-base-component'; import { applyDisplayName } from '../internal/utils/apply-display-name'; @@ -34,6 +35,49 @@ export default function TutorialPanel({ const baseProps = getBaseProps(restProps); const context = useContext(hotspotContext); + const i18n = useInternalI18n('tutorial-panel'); + const resolvedI18nStrings: TutorialPanelProps.I18nStrings = { + ...i18nStrings, + loadingText: i18n('i18nStrings.loadingText', i18nStrings.loadingText), + tutorialListTitle: i18n('i18nStrings.tutorialListTitle', i18nStrings.tutorialListTitle), + tutorialListDownloadLinkText: i18n( + 'i18nStrings.tutorialListDownloadLinkText', + i18nStrings.tutorialListDownloadLinkText + ), + labelTutorialListDownloadLink: i18n( + 'i18nStrings.labelTutorialListDownloadLink', + i18nStrings.labelTutorialListDownloadLink + ), + tutorialCompletedText: i18n('i18nStrings.tutorialCompletedText', i18nStrings.tutorialCompletedText), + learnMoreLinkText: i18n('i18nStrings.learnMoreLinkText', i18nStrings.learnMoreLinkText), + startTutorialButtonText: i18n('i18nStrings.startTutorialButtonText', i18nStrings.startTutorialButtonText), + restartTutorialButtonText: i18n('i18nStrings.restartTutorialButtonText', i18nStrings.restartTutorialButtonText), + completionScreenTitle: i18n('i18nStrings.completionScreenTitle', i18nStrings.completionScreenTitle), + feedbackLinkText: i18n('i18nStrings.feedbackLinkText', i18nStrings.feedbackLinkText), + dismissTutorialButtonText: i18n('i18nStrings.dismissTutorialButtonText', i18nStrings.dismissTutorialButtonText), + labelExitTutorial: i18n('i18nStrings.labelExitTutorial', i18nStrings.labelExitTutorial), + taskTitle: i18n( + 'i18nStrings.taskTitle', + i18nStrings.taskTitle, + format => (taskIndex, taskTitle) => format({ taskNumber: taskIndex + 1, taskTitle }) + ), + stepTitle: i18n( + 'i18nStrings.stepTitle', + i18nStrings.stepTitle, + format => (stepIndex, stepTitle) => format({ stepNumber: stepIndex + 1, stepTitle }) + ), + labelTotalSteps: i18n( + 'i18nStrings.labelTotalSteps', + i18nStrings.labelTotalSteps, + format => totalStepCount => format({ totalStepCount }) + ), + labelsTaskStatus: { + pending: i18n('i18nStrings.labelsTaskStatus.pending', i18nStrings.labelsTaskStatus?.pending), + 'in-progress': i18n('i18nStrings.labelsTaskStatus.in-progress', i18nStrings.labelsTaskStatus?.['in-progress']), + success: i18n('i18nStrings.labelsTaskStatus.success', i18nStrings.labelsTaskStatus?.success), + }, + }; + function handleExitTutorial(event: NonCancelableCustomEvent) { context.onExitTutorial(event); panelRef.current?.focus(); @@ -57,7 +101,7 @@ export default function TutorialPanel({ > {context.currentTutorial ? ( ) : ( string; - stepTitle: (stepIndex: number, stepTitle: string) => string; + taskTitle?: (taskIndex: number, taskTitle: string) => string; + stepTitle?: (stepIndex: number, stepTitle: string) => string; - labelExitTutorial: string; - labelTotalSteps: (totalStepCount: number) => string; + labelExitTutorial?: string; + labelTotalSteps?: (totalStepCount: number) => string; labelLearnMoreExternalIcon: string; - labelsTaskStatus: { - pending: string; - 'in-progress': string; - success: string; + labelsTaskStatus?: { + pending?: string; + 'in-progress'?: string; + success?: string; }; labelTutorialListDownloadLink?: string;