Skip to content

Commit 3f9e4d4

Browse files
committed
feat(app): implement ODD choose language screen
implements the choose language screen that will be the first step of the ODD unboxing flow. migration of the unboxing flow route config value from '/welcome' to '/choose-langauge' will happen in a future PR removing the localization feature flag. closes PLAT-537
1 parent d9dd918 commit 3f9e4d4

File tree

10 files changed

+171
-16
lines changed

10 files changed

+171
-16
lines changed

app/src/App/OnDeviceDisplayApp.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { MaintenanceRunTakeover } from '/app/organisms/TakeoverModal'
2222
import { FirmwareUpdateTakeover } from '/app/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover'
2323
import { IncompatibleModuleTakeover } from '/app/organisms/IncompatibleModule'
2424
import { EstopTakeover } from '/app/organisms/EmergencyStop'
25+
import { ChooseLanguage } from '/app/pages/ODD/ChooseLanguage'
2526
import { ConnectViaEthernet } from '/app/pages/ODD/ConnectViaEthernet'
2627
import { ConnectViaUSB } from '/app/pages/ODD/ConnectViaUSB'
2728
import { ConnectViaWifi } from '/app/pages/ODD/ConnectViaWifi'
@@ -66,6 +67,7 @@ import type { Dispatch } from '/app/redux/types'
6667
hackWindowNavigatorOnLine()
6768

6869
export const ON_DEVICE_DISPLAY_PATHS = [
70+
'/choose-language',
6971
'/dashboard',
7072
'/deck-configuration',
7173
'/emergency-stop',
@@ -94,6 +96,8 @@ function getPathComponent(
9496
path: typeof ON_DEVICE_DISPLAY_PATHS[number]
9597
): JSX.Element {
9698
switch (path) {
99+
case '/choose-language':
100+
return <ChooseLanguage />
97101
case '/dashboard':
98102
return <RobotDashboard />
99103
case '/deck-configuration':

app/src/App/__tests__/OnDeviceDisplayApp.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom'
55
import { renderWithProviders } from '/app/__testing-utils__'
66
import { i18n } from '/app/i18n'
77
import { LocalizationProvider } from '../../LocalizationProvider'
8+
import { ChooseLanguage } from '/app/pages/ODD/ChooseLanguage'
89
import { ConnectViaEthernet } from '/app/pages/ODD/ConnectViaEthernet'
910
import { ConnectViaUSB } from '/app/pages/ODD/ConnectViaUSB'
1011
import { ConnectViaWifi } from '/app/pages/ODD/ConnectViaWifi'
@@ -48,6 +49,7 @@ vi.mock('@opentrons/react-api-client', async () => {
4849
vi.mock('../../LocalizationProvider')
4950
vi.mock('/app/pages/ODD/Welcome')
5051
vi.mock('/app/pages/ODD/NetworkSetupMenu')
52+
vi.mock('/app/pages/ODD/ChooseLanguage')
5153
vi.mock('/app/pages/ODD/ConnectViaEthernet')
5254
vi.mock('/app/pages/ODD/ConnectViaUSB')
5355
vi.mock('/app/pages/ODD/ConnectViaWifi')
@@ -109,6 +111,10 @@ describe('OnDeviceDisplayApp', () => {
109111
vi.resetAllMocks()
110112
})
111113

114+
it('renders ChooseLanguage component from /choose-language', () => {
115+
render('/choose-language')
116+
expect(vi.mocked(ChooseLanguage)).toHaveBeenCalled()
117+
})
112118
it('renders Welcome component from /welcome', () => {
113119
render('/welcome')
114120
expect(vi.mocked(Welcome)).toHaveBeenCalled()

app/src/assets/localization/en/app_settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"cal_block": "Always use calibration block to calibrate",
2121
"change_folder_button": "Change labware source folder",
2222
"channel": "Channel",
23+
"choose_your_language": "Choose your language",
2324
"clear_confirm": "Clear unavailable robots",
2425
"clear_robots_button": "Clear unavailable robots list",
2526
"clear_robots_description": "Clear the list of unavailable robots on the Devices page. This action cannot be undone.",
@@ -73,6 +74,7 @@
7374
"restarting_app": "Download complete, restarting the app...",
7475
"restore_previous": "See how to restore a previous software version",
7576
"searching": "Searching for 30s",
77+
"select_a_language": "Select a language to personalize your experience.",
7678
"select_language": "Select language",
7779
"setup_connection": "Set up connection",
7880
"share_display_usage": "Share display usage",

app/src/i18n.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ import { titleCase } from '@opentrons/shared-data'
77

88
import type { InitOptions } from 'i18next'
99

10+
export const US_ENGLISH = 'en-US'
11+
export const SIMPLIFIED_CHINESE = 'zh-CN'
12+
13+
export type Language = typeof US_ENGLISH | typeof SIMPLIFIED_CHINESE
14+
15+
// these strings will not be translated so should not be localized
16+
export const LANGUAGES: Array<{ name: string; value: Language }> = [
17+
{ name: 'English (US)', value: US_ENGLISH },
18+
{ name: '中文', value: SIMPLIFIED_CHINESE },
19+
]
20+
1021
const i18nConfig: InitOptions = {
1122
resources,
1223
lng: 'en',

app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe('SystemLanguagePreferenceModal', () => {
9999

100100
it('should set a supported app language when system language is an unsupported locale of the same language', () => {
101101
vi.mocked(getAppLanguage).mockReturnValue(null)
102-
vi.mocked(getSystemLanguage).mockReturnValue('en-UK')
102+
vi.mocked(getSystemLanguage).mockReturnValue('en-GB')
103103

104104
render()
105105

@@ -116,7 +116,7 @@ describe('SystemLanguagePreferenceModal', () => {
116116
'language.appLanguage',
117117
MOCK_DEFAULT_LANGUAGE
118118
)
119-
expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'en-UK')
119+
expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'en-GB')
120120
})
121121

122122
it('should render the correct header, description, and buttons when system language changes', () => {

app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
StyledText,
1616
} from '@opentrons/components'
1717

18+
import { LANGUAGES } from '/app/i18n'
1819
import {
1920
getAppLanguage,
2021
getStoredSystemLanguage,
@@ -26,18 +27,12 @@ import { getSystemLanguage } from '/app/redux/shell'
2627
import type { DropdownOption } from '@opentrons/components'
2728
import type { Dispatch } from '/app/redux/types'
2829

29-
// these strings will not be translated so should not be localized
30-
const languageOptions: DropdownOption[] = [
31-
{ name: 'English (US)', value: 'en-US' },
32-
{ name: '中文', value: 'zh-CN' },
33-
]
34-
3530
export function SystemLanguagePreferenceModal(): JSX.Element | null {
3631
const { i18n, t } = useTranslation(['app_settings', 'shared', 'branded'])
3732
const enableLocalization = useFeatureFlag('enableLocalization')
3833

3934
const [currentOption, setCurrentOption] = useState<DropdownOption>(
40-
languageOptions[0]
35+
LANGUAGES[0]
4136
)
4237

4338
const dispatch = useDispatch<Dispatch>()
@@ -76,7 +71,7 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null {
7671
}
7772

7873
const handleDropdownClick = (value: string): void => {
79-
const selectedOption = languageOptions.find(lng => lng.value === value)
74+
const selectedOption = LANGUAGES.find(lng => lng.value === value)
8075

8176
if (selectedOption != null) {
8277
setCurrentOption(selectedOption)
@@ -89,8 +84,8 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null {
8984
if (systemLanguage != null) {
9085
// prefer match entire locale, then match just language e.g. zh-Hant and zh-CN
9186
const matchedSystemLanguageOption =
92-
languageOptions.find(lng => lng.value === systemLanguage) ??
93-
languageOptions.find(
87+
LANGUAGES.find(lng => lng.value === systemLanguage) ??
88+
LANGUAGES.find(
9489
lng =>
9590
new Intl.Locale(lng.value).language ===
9691
new Intl.Locale(systemLanguage).language
@@ -115,7 +110,7 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null {
115110
</StyledText>
116111
{showBootModal ? (
117112
<DropdownMenu
118-
filterOptions={languageOptions}
113+
filterOptions={LANGUAGES}
119114
currentOption={currentOption}
120115
onClick={handleDropdownClick}
121116
title={t('select_language')}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { vi, it, describe, expect } from 'vitest'
2+
import { fireEvent, screen } from '@testing-library/react'
3+
import { MemoryRouter } from 'react-router-dom'
4+
5+
import { renderWithProviders } from '/app/__testing-utils__'
6+
import { i18n } from '/app/i18n'
7+
import { updateConfigValue } from '/app/redux/config'
8+
import { ChooseLanguage } from '..'
9+
10+
import type { NavigateFunction } from 'react-router-dom'
11+
12+
const mockNavigate = vi.fn()
13+
vi.mock('react-router-dom', async importOriginal => {
14+
const actual = await importOriginal<NavigateFunction>()
15+
return {
16+
...actual,
17+
useNavigate: () => mockNavigate,
18+
}
19+
})
20+
vi.mock('/app/redux/config')
21+
22+
const render = () => {
23+
return renderWithProviders(
24+
<MemoryRouter>
25+
<ChooseLanguage />
26+
</MemoryRouter>,
27+
{
28+
i18nInstance: i18n,
29+
}
30+
)
31+
}
32+
33+
describe('ChooseLanguage', () => {
34+
it('should render text, language options, and continue button', () => {
35+
render()
36+
screen.getByText('Choose your language')
37+
screen.getByText('Select a language to personalize your experience.')
38+
screen.getByRole('label', { name: 'English (US)' })
39+
screen.getByRole('label', { name: '中文' })
40+
screen.getByRole('button', { name: 'Continue' })
41+
})
42+
43+
it('should initialize english', () => {
44+
render()
45+
expect(updateConfigValue).toBeCalledWith('language.appLanguage', 'en-US')
46+
})
47+
48+
it('should change language when language option selected', () => {
49+
render()
50+
fireEvent.click(screen.getByRole('label', { name: '中文' }))
51+
expect(updateConfigValue).toBeCalledWith('language.appLanguage', 'zh-CN')
52+
})
53+
54+
it('should call mockNavigate when tapping continue', () => {
55+
render()
56+
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
57+
expect(mockNavigate).toHaveBeenCalledWith('/welcome')
58+
})
59+
})
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useEffect } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { useDispatch, useSelector } from 'react-redux'
4+
import { useNavigate } from 'react-router-dom'
5+
6+
import {
7+
DIRECTION_COLUMN,
8+
Flex,
9+
JUSTIFY_SPACE_BETWEEN,
10+
RadioButton,
11+
SPACING,
12+
StyledText,
13+
TYPOGRAPHY,
14+
} from '@opentrons/components'
15+
16+
import { MediumButton } from '/app/atoms/buttons'
17+
import { LANGUAGES, US_ENGLISH } from '/app/i18n'
18+
import { RobotSetupHeader } from '/app/organisms/ODD/RobotSetupHeader'
19+
import { getAppLanguage, updateConfigValue } from '/app/redux/config'
20+
21+
import type { Dispatch } from '/app/redux/types'
22+
23+
export function ChooseLanguage(): JSX.Element {
24+
const { i18n, t } = useTranslation(['app_settings', 'shared'])
25+
const navigate = useNavigate()
26+
const dispatch = useDispatch<Dispatch>()
27+
28+
const appLanguage = useSelector(getAppLanguage)
29+
30+
useEffect(() => {
31+
// initialize en-US language on mount
32+
dispatch(updateConfigValue('language.appLanguage', US_ENGLISH))
33+
// eslint-disable-next-line react-hooks/exhaustive-deps
34+
}, [])
35+
36+
return (
37+
<Flex
38+
flexDirection={DIRECTION_COLUMN}
39+
height="100%"
40+
padding={`0 ${SPACING.spacing40} ${SPACING.spacing40} ${SPACING.spacing40}`}
41+
>
42+
<RobotSetupHeader header={t('choose_your_language')} />
43+
<Flex
44+
flex="1"
45+
flexDirection={DIRECTION_COLUMN}
46+
justifyContent={JUSTIFY_SPACE_BETWEEN}
47+
>
48+
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing24}>
49+
<StyledText
50+
oddStyle="level4HeaderRegular"
51+
textAlign={TYPOGRAPHY.textAlignCenter}
52+
>
53+
{t('select_a_language')}
54+
</StyledText>
55+
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
56+
{LANGUAGES.map(lng => (
57+
<RadioButton
58+
key={lng.value}
59+
buttonLabel={lng.name}
60+
buttonValue={lng.value}
61+
isSelected={lng.value === appLanguage}
62+
onChange={() => {
63+
dispatch(updateConfigValue('language.appLanguage', lng.value))
64+
}}
65+
></RadioButton>
66+
))}
67+
</Flex>
68+
</Flex>
69+
<MediumButton
70+
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
71+
onClick={() => {
72+
navigate('/welcome')
73+
}}
74+
width="100%"
75+
/>
76+
</Flex>
77+
</Flex>
78+
)
79+
}

app/src/redux/config/schema-types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { LogLevel } from '../../logger'
2+
import type { Language } from '/app/i18n'
23
import type { ProtocolSort } from '/app/redux/protocol-storage'
34

45
export type UrlProtocol = 'file:' | 'http:'
@@ -31,8 +32,6 @@ export type QuickTransfersOnDeviceSortKey =
3132
| 'recentCreated'
3233
| 'oldCreated'
3334

34-
export type Language = 'en-US' | 'zh-CN'
35-
3635
export interface OnDeviceDisplaySettings {
3736
sleepMs: number
3837
brightness: number

app/src/redux/config/selectors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import type {
88
ProtocolsOnDeviceSortKey,
99
QuickTransfersOnDeviceSortKey,
1010
OnDeviceDisplaySettings,
11-
Language,
1211
} from './types'
12+
import type { Language } from '/app/i18n'
1313
import type { ProtocolSort } from '/app/redux/protocol-storage'
1414

1515
export interface SelectOption {

0 commit comments

Comments
 (0)