Skip to content

Commit

Permalink
feat(app): implement ODD choose language screen, app language toggles (
Browse files Browse the repository at this point in the history
…#16573)

# Overview

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.

the screen can be tested locally by manually editing
`unfinishedUnboxingFlowRoute` in `config.json` and commenting out the
line `dev-shell-odd: export
OT_APP_ON_DEVICE_DISPLAY_SETTINGS__UNFINISHED_UNBOXING_FLOW_ROUTE := 0`
in the app makefile.

implements the language setting hi-fi designs on desktop and ODD.

closes PLAT-537, PLAT-506

<img width="1136" alt="Screen Shot 2024-10-22 at 5 06 03 PM"
src="https://github.com/user-attachments/assets/0222b95f-e260-4ec5-a472-9e3645d38d54">
<img width="1136" alt="Screen Shot 2024-10-22 at 5 06 15 PM"
src="https://github.com/user-attachments/assets/a82573f3-0446-4ac1-a624-9a680194c9b6">

<img width="1136" alt="Screen Shot 2024-10-24 at 3 05 24 PM"
src="https://github.com/user-attachments/assets/8ac6a54d-b842-41ca-828a-4e0f28293cfe">
<img width="1136" alt="Screen Shot 2024-10-24 at 3 05 29 PM"
src="https://github.com/user-attachments/assets/f1aee946-955f-4ce8-99aa-1b3da3e00815">

## Test Plan and Hands on Testing

added unit tests for the screen and toggles, verified all behavior

## Changelog

 - Implements ODD Choose Language screen
 - Implements ODD language setting toggle


## Review requests

check out the screens and toggles

## Risk assessment

low, screen not active until config migrated, toggles behind feature
flag
  • Loading branch information
brenthagen authored Oct 25, 2024
1 parent 025d5e1 commit d96e2b6
Show file tree
Hide file tree
Showing 21 changed files with 486 additions and 114 deletions.
4 changes: 4 additions & 0 deletions app/src/App/OnDeviceDisplayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MaintenanceRunTakeover } from '/app/organisms/TakeoverModal'
import { FirmwareUpdateTakeover } from '/app/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover'
import { IncompatibleModuleTakeover } from '/app/organisms/IncompatibleModule'
import { EstopTakeover } from '/app/organisms/EmergencyStop'
import { ChooseLanguage } from '/app/pages/ODD/ChooseLanguage'
import { ConnectViaEthernet } from '/app/pages/ODD/ConnectViaEthernet'
import { ConnectViaUSB } from '/app/pages/ODD/ConnectViaUSB'
import { ConnectViaWifi } from '/app/pages/ODD/ConnectViaWifi'
Expand Down Expand Up @@ -66,6 +67,7 @@ import type { Dispatch } from '/app/redux/types'
hackWindowNavigatorOnLine()

export const ON_DEVICE_DISPLAY_PATHS = [
'/choose-language',
'/dashboard',
'/deck-configuration',
'/emergency-stop',
Expand Down Expand Up @@ -94,6 +96,8 @@ function getPathComponent(
path: typeof ON_DEVICE_DISPLAY_PATHS[number]
): JSX.Element {
switch (path) {
case '/choose-language':
return <ChooseLanguage />
case '/dashboard':
return <RobotDashboard />
case '/deck-configuration':
Expand Down
6 changes: 6 additions & 0 deletions app/src/App/__tests__/OnDeviceDisplayApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom'
import { renderWithProviders } from '/app/__testing-utils__'
import { i18n } from '/app/i18n'
import { LocalizationProvider } from '../../LocalizationProvider'
import { ChooseLanguage } from '/app/pages/ODD/ChooseLanguage'
import { ConnectViaEthernet } from '/app/pages/ODD/ConnectViaEthernet'
import { ConnectViaUSB } from '/app/pages/ODD/ConnectViaUSB'
import { ConnectViaWifi } from '/app/pages/ODD/ConnectViaWifi'
Expand Down Expand Up @@ -48,6 +49,7 @@ vi.mock('@opentrons/react-api-client', async () => {
vi.mock('../../LocalizationProvider')
vi.mock('/app/pages/ODD/Welcome')
vi.mock('/app/pages/ODD/NetworkSetupMenu')
vi.mock('/app/pages/ODD/ChooseLanguage')
vi.mock('/app/pages/ODD/ConnectViaEthernet')
vi.mock('/app/pages/ODD/ConnectViaUSB')
vi.mock('/app/pages/ODD/ConnectViaWifi')
Expand Down Expand Up @@ -109,6 +111,10 @@ describe('OnDeviceDisplayApp', () => {
vi.resetAllMocks()
})

it('renders ChooseLanguage component from /choose-language', () => {
render('/choose-language')
expect(vi.mocked(ChooseLanguage)).toHaveBeenCalled()
})
it('renders Welcome component from /welcome', () => {
render('/welcome')
expect(vi.mocked(Welcome)).toHaveBeenCalled()
Expand Down
5 changes: 5 additions & 0 deletions app/src/assets/localization/en/app_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
"additional_labware_folder_title": "Additional Custom Labware Source Folder",
"advanced": "Advanced",
"app_changes": "App Changes in ",
"app_language_description": "All app features use this language. Protocols and other user content will not change language.",
"app_language_preferences": "App Language Preferences",
"app_settings": "App Settings",
"bug_fixes": "Bug Fixes",
"cal_block": "Always use calibration block to calibrate",
"change_folder_button": "Change labware source folder",
"channel": "Channel",
"choose_your_language": "Choose your language",
"clear_confirm": "Clear unavailable robots",
"clear_robots_button": "Clear unavailable robots list",
"clear_robots_description": "Clear the list of unavailable robots on the Devices page. This action cannot be undone.",
Expand Down Expand Up @@ -48,6 +51,7 @@
"ip_available": "Available",
"ip_description_first": "Enter an IP address or hostname to connect to a robot.",
"language_preference": "Language preference",
"language": "Language",
"manage_versions": "It is very important for the robot and app software to be on the same version. Manage the robot software versions via Robot Settings > Advanced.",
"new_features": "New Features",
"no_folder": "No additional source folder specified",
Expand All @@ -73,6 +77,7 @@
"restarting_app": "Download complete, restarting the app...",
"restore_previous": "See how to restore a previous software version",
"searching": "Searching for 30s",
"select_a_language": "Select a language to personalize your experience.",
"select_language": "Select language",
"setup_connection": "Set up connection",
"share_display_usage": "Share display usage",
Expand Down
14 changes: 14 additions & 0 deletions app/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ import { titleCase } from '@opentrons/shared-data'

import type { InitOptions } from 'i18next'

export const US_ENGLISH = 'en-US'
export const SIMPLIFIED_CHINESE = 'zh-CN'

// these strings will not be translated so should not be localized
export const US_ENGLISH_DISPLAY_NAME = 'English (US)'
export const SIMPLIFIED_CHINESE_DISPLAY_NAME = '中文'

export type Language = typeof US_ENGLISH | typeof SIMPLIFIED_CHINESE

export const LANGUAGES: Array<{ name: string; value: Language }> = [
{ name: US_ENGLISH_DISPLAY_NAME, value: US_ENGLISH },
{ name: SIMPLIFIED_CHINESE_DISPLAY_NAME, value: SIMPLIFIED_CHINESE },
]

const i18nConfig: InitOptions = {
resources,
lng: 'en',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('SystemLanguagePreferenceModal', () => {

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

render()

Expand All @@ -116,7 +116,7 @@ describe('SystemLanguagePreferenceModal', () => {
'language.appLanguage',
MOCK_DEFAULT_LANGUAGE
)
expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'en-UK')
expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'en-GB')
})

it('should render the correct header, description, and buttons when system language changes', () => {
Expand Down
17 changes: 6 additions & 11 deletions app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
StyledText,
} from '@opentrons/components'

import { LANGUAGES } from '/app/i18n'
import {
getAppLanguage,
getStoredSystemLanguage,
Expand All @@ -26,18 +27,12 @@ import { getSystemLanguage } from '/app/redux/shell'
import type { DropdownOption } from '@opentrons/components'
import type { Dispatch } from '/app/redux/types'

// these strings will not be translated so should not be localized
const languageOptions: DropdownOption[] = [
{ name: 'English (US)', value: 'en-US' },
{ name: '中文', value: 'zh-CN' },
]

export function SystemLanguagePreferenceModal(): JSX.Element | null {
const { i18n, t } = useTranslation(['app_settings', 'shared', 'branded'])
const enableLocalization = useFeatureFlag('enableLocalization')

const [currentOption, setCurrentOption] = useState<DropdownOption>(
languageOptions[0]
LANGUAGES[0]
)

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

const handleDropdownClick = (value: string): void => {
const selectedOption = languageOptions.find(lng => lng.value === value)
const selectedOption = LANGUAGES.find(lng => lng.value === value)

if (selectedOption != null) {
setCurrentOption(selectedOption)
Expand All @@ -89,8 +84,8 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null {
if (systemLanguage != null) {
// prefer match entire locale, then match just language e.g. zh-Hant and zh-CN
const matchedSystemLanguageOption =
languageOptions.find(lng => lng.value === systemLanguage) ??
languageOptions.find(
LANGUAGES.find(lng => lng.value === systemLanguage) ??
LANGUAGES.find(
lng =>
new Intl.Locale(lng.value).language ===
new Intl.Locale(systemLanguage).language
Expand All @@ -115,7 +110,7 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null {
</StyledText>
{showBootModal ? (
<DropdownMenu
filterOptions={languageOptions}
filterOptions={LANGUAGES}
currentOption={currentOption}
onClick={handleDropdownClick}
title={t('select_language')}
Expand Down
92 changes: 92 additions & 0 deletions app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'

import {
BORDERS,
COLORS,
CURSOR_POINTER,
DIRECTION_COLUMN,
Flex,
SPACING,
StyledText,
} from '@opentrons/components'

import { LANGUAGES } from '/app/i18n'
import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation'
import { getAppLanguage, updateConfigValue } from '/app/redux/config'

import type { Dispatch } from '/app/redux/types'
import type { SetSettingOption } from './types'

interface LabelProps {
isSelected?: boolean
}

const SettingButton = styled.input`
display: none;
`

const SettingButtonLabel = styled.label<LabelProps>`
padding: ${SPACING.spacing24};
border-radius: ${BORDERS.borderRadius16};
cursor: ${CURSOR_POINTER};
background: ${({ isSelected }) =>
isSelected === true ? COLORS.blue50 : COLORS.blue35};
color: ${({ isSelected }) => isSelected === true && COLORS.white};
`

interface LanguageSettingProps {
setCurrentOption: SetSettingOption
}

export function LanguageSetting({
setCurrentOption,
}: LanguageSettingProps): JSX.Element {
const { t } = useTranslation('app_settings')
const dispatch = useDispatch<Dispatch>()

const appLanguage = useSelector(getAppLanguage)

const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
dispatch(updateConfigValue('language.appLanguage', event.target.value))
}

return (
<Flex flexDirection={DIRECTION_COLUMN}>
<ChildNavigation
header={t('language')}
onClickBack={() => {
setCurrentOption(null)
}}
/>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing8}
marginTop="7.75rem"
padding={`${SPACING.spacing16} ${SPACING.spacing40} ${SPACING.spacing40} ${SPACING.spacing40}`}
>
{LANGUAGES.map(lng => (
<React.Fragment key={`language_setting_${lng.name}`}>
<SettingButton
id={lng.name}
type="radio"
value={lng.value}
checked={lng.value === appLanguage}
onChange={handleChange}
/>
<SettingButtonLabel
htmlFor={lng.name}
isSelected={lng.value === appLanguage}
>
<StyledText oddStyle="level4HeaderSemiBold">
{lng.name}
</StyledText>
</SettingButtonLabel>
</React.Fragment>
))}
</Flex>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type * as React from 'react'
import { fireEvent, screen } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import '@testing-library/jest-dom/vitest'

import {
i18n,
US_ENGLISH_DISPLAY_NAME,
US_ENGLISH,
SIMPLIFIED_CHINESE_DISPLAY_NAME,
SIMPLIFIED_CHINESE,
} from '/app/i18n'
import { getAppLanguage, updateConfigValue } from '/app/redux/config'
import { renderWithProviders } from '/app/__testing-utils__'

import { LanguageSetting } from '../LanguageSetting'

vi.mock('/app/redux/config')

const mockSetCurrentOption = vi.fn()

const render = (props: React.ComponentProps<typeof LanguageSetting>) => {
return renderWithProviders(<LanguageSetting {...props} />, {
i18nInstance: i18n,
})
}

describe('LanguageSetting', () => {
let props: React.ComponentProps<typeof LanguageSetting>
beforeEach(() => {
props = {
setCurrentOption: mockSetCurrentOption,
}
vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH)
})

it('should render text and buttons', () => {
render(props)
screen.getByText('Language')
screen.getByText(US_ENGLISH_DISPLAY_NAME)
screen.getByText(SIMPLIFIED_CHINESE_DISPLAY_NAME)
})

it('should call mock function when tapping a language button', () => {
render(props)
const button = screen.getByText(SIMPLIFIED_CHINESE_DISPLAY_NAME)
fireEvent.click(button)
expect(updateConfigValue).toHaveBeenCalledWith(
'language.appLanguage',
SIMPLIFIED_CHINESE
)
})

it('should call mock function when tapping back button', () => {
render(props)
const button = screen.getByRole('button')
fireEvent.click(button)
expect(props.setCurrentOption).toHaveBeenCalled()
})
})
1 change: 1 addition & 0 deletions app/src/organisms/ODD/RobotSettingsDashboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './DeviceReset'
export * from './LanguageSetting'
export * from './NetworkSettings/RobotSettingsJoinOtherNetwork'
export * from './NetworkSettings/RobotSettingsSelectAuthenticationType'
export * from './NetworkSettings/RobotSettingsSetWifiCred'
Expand Down
1 change: 1 addition & 0 deletions app/src/organisms/ODD/RobotSettingsDashboard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export type SettingOption =
| 'RobotSettingsSetWifiCred'
| 'RobotSettingsWifi'
| 'RobotSettingsWifiConnect'
| 'LanguageSetting'

export type SetSettingOption = (option: SettingOption | null) => void
Loading

0 comments on commit d96e2b6

Please sign in to comment.