Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app,app-shell,app-shell-odd): add language setting to app config #16393

Draft
wants to merge 3 commits into
base: edge
Choose a base branch
from
Draft
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
7 changes: 7 additions & 0 deletions app-shell-odd/src/config/__fixtures__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
ConfigV22,
ConfigV23,
ConfigV24,
ConfigV25,
} from '@opentrons/app/src/redux/config/types'

const PKG_VERSION: string = _PKG_VERSION_
Expand Down Expand Up @@ -171,3 +172,9 @@ export const MOCK_CONFIG_V24: ConfigV24 = {
userId: 'MOCK_UUIDv4',
},
}

export const MOCK_CONFIG_V25: ConfigV25 = {
...MOCK_CONFIG_V24,
version: 25,
language: null,
}
14 changes: 11 additions & 3 deletions app-shell-odd/src/config/__tests__/migrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import {
MOCK_CONFIG_V22,
MOCK_CONFIG_V23,
MOCK_CONFIG_V24,
MOCK_CONFIG_V25,
} from '../__fixtures__'
import { migrate } from '../migrate'

vi.mock('uuid/v4')

const NEWEST_VERSION = 24
const NEWEST_MOCK_CONFIG = MOCK_CONFIG_V24
const NEWEST_VERSION = 25
const NEWEST_MOCK_CONFIG = MOCK_CONFIG_V25

describe('config migration', () => {
beforeEach(() => {
Expand Down Expand Up @@ -121,10 +122,17 @@ describe('config migration', () => {
expect(result.version).toBe(NEWEST_VERSION)
expect(result).toEqual(NEWEST_MOCK_CONFIG)
})
it('should keep version 24', () => {
it('should migrate version 24 to latest', () => {
const v24Config = MOCK_CONFIG_V24
const result = migrate(v24Config)

expect(result.version).toBe(NEWEST_VERSION)
expect(result).toEqual(NEWEST_MOCK_CONFIG)
})
it('should keep version 25', () => {
const v25Config = MOCK_CONFIG_V25
const result = migrate(v25Config)

expect(result.version).toBe(NEWEST_VERSION)
expect(result).toEqual(NEWEST_MOCK_CONFIG)
})
Expand Down
17 changes: 15 additions & 2 deletions app-shell-odd/src/config/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ import type {
ConfigV22,
ConfigV23,
ConfigV24,
ConfigV25,
} from '@opentrons/app/src/redux/config/types'
// format
// base config v12 defaults
// any default values for later config versions are specified in the migration
// functions for those version below

const CONFIG_VERSION_LATEST = 23 // update this after each config version bump
const CONFIG_VERSION_LATEST = 25 // update this after each config version bump

const PKG_VERSION: string = _PKG_VERSION_
export const DEFAULTS_V12: ConfigV12 = {
Expand Down Expand Up @@ -226,6 +227,15 @@ const toVersion24 = (prevConfig: ConfigV23): ConfigV24 => {
}
}

const toVersion25 = (prevConfig: ConfigV24): ConfigV25 => {
const nextConfig = {
...prevConfig,
version: 25 as const,
language: null,
}
return nextConfig
}

const MIGRATIONS: [
(prevConfig: ConfigV12) => ConfigV13,
(prevConfig: ConfigV13) => ConfigV14,
Expand All @@ -238,7 +248,8 @@ const MIGRATIONS: [
(prevConfig: ConfigV20) => ConfigV21,
(prevConfig: ConfigV21) => ConfigV22,
(prevConfig: ConfigV22) => ConfigV23,
(prevConfig: ConfigV23) => ConfigV24
(prevConfig: ConfigV23) => ConfigV24,
(prevConfig: ConfigV24) => ConfigV25
] = [
toVersion13,
toVersion14,
Expand All @@ -252,6 +263,7 @@ const MIGRATIONS: [
toVersion22,
toVersion23,
toVersion24,
toVersion25,
]

export const DEFAULTS: Config = migrate(DEFAULTS_V12)
Expand All @@ -271,6 +283,7 @@ export function migrate(
| ConfigV22
| ConfigV23
| ConfigV24
| ConfigV25
): Config {
let result = prevConfig
// loop through the migrations, skipping any migrations that are unnecessary
Expand Down
7 changes: 7 additions & 0 deletions app-shell/src/__fixtures__/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
ConfigV22,
ConfigV23,
ConfigV24,
ConfigV25,
} from '@opentrons/app/src/redux/config/types'

export const MOCK_CONFIG_V0: ConfigV0 = {
Expand Down Expand Up @@ -302,3 +303,9 @@ export const MOCK_CONFIG_V24: ConfigV24 = {
userId: 'MOCK_UUIDv4',
},
}

export const MOCK_CONFIG_V25: ConfigV25 = {
...MOCK_CONFIG_V24,
version: 25,
language: null,
}
14 changes: 11 additions & 3 deletions app-shell/src/config/__tests__/migrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import {
MOCK_CONFIG_V22,
MOCK_CONFIG_V23,
MOCK_CONFIG_V24,
MOCK_CONFIG_V25,
} from '../../__fixtures__'
import { migrate } from '../migrate'

vi.mock('uuid/v4')

const NEWEST_VERSION = 24
const NEWEST_MOCK_CONFIG = MOCK_CONFIG_V24
const NEWEST_VERSION = 25
const NEWEST_MOCK_CONFIG = MOCK_CONFIG_V25

describe('config migration', () => {
beforeEach(() => {
Expand Down Expand Up @@ -226,10 +227,17 @@ describe('config migration', () => {
expect(result.version).toBe(NEWEST_VERSION)
expect(result).toEqual(NEWEST_MOCK_CONFIG)
})
it('should keep version 24', () => {
it('should migrate version 24 to latest', () => {
const v24Config = MOCK_CONFIG_V24
const result = migrate(v24Config)

expect(result.version).toBe(NEWEST_VERSION)
expect(result).toEqual(NEWEST_MOCK_CONFIG)
})
it('should keep version 25', () => {
const v25Config = MOCK_CONFIG_V25
const result = migrate(v25Config)

expect(result.version).toBe(NEWEST_VERSION)
expect(result).toEqual(NEWEST_MOCK_CONFIG)
})
Expand Down
17 changes: 15 additions & 2 deletions app-shell/src/config/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import type {
ConfigV22,
ConfigV23,
ConfigV24,
ConfigV25,
} from '@opentrons/app/src/redux/config/types'
// format
// base config v0 defaults
// any default values for later config versions are specified in the migration
// functions for those version below

const CONFIG_VERSION_LATEST = 23
const CONFIG_VERSION_LATEST = 25

export const DEFAULTS_V0: ConfigV0 = {
version: 0,
Expand Down Expand Up @@ -430,6 +431,15 @@ const toVersion24 = (prevConfig: ConfigV23): ConfigV24 => {
}
}

const toVersion25 = (prevConfig: ConfigV24): ConfigV25 => {
const nextConfig = {
...prevConfig,
version: 25 as const,
language: null,
}
return nextConfig
}

const MIGRATIONS: [
(prevConfig: ConfigV0) => ConfigV1,
(prevConfig: ConfigV1) => ConfigV2,
Expand All @@ -454,7 +464,8 @@ const MIGRATIONS: [
(prevConfig: ConfigV20) => ConfigV21,
(prevConfig: ConfigV21) => ConfigV22,
(prevConfig: ConfigV22) => ConfigV23,
(prevConfig: ConfigV23) => ConfigV24
(prevConfig: ConfigV23) => ConfigV24,
(prevConfig: ConfigV24) => ConfigV25
] = [
toVersion1,
toVersion2,
Expand All @@ -480,6 +491,7 @@ const MIGRATIONS: [
toVersion22,
toVersion23,
toVersion24,
toVersion25,
]

export const DEFAULTS: Config = migrate(DEFAULTS_V0)
Expand Down Expand Up @@ -511,6 +523,7 @@ export function migrate(
| ConfigV22
| ConfigV23
| ConfigV24
| ConfigV25
): Config {
const prevVersion = prevConfig.version
let result = prevConfig
Expand Down
7 changes: 3 additions & 4 deletions app/src/App/DesktopApp.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState, Fragment } from 'react'
import { Navigate, Route, Routes, useMatch } from 'react-router-dom'
import { ErrorBoundary } from 'react-error-boundary'
import { I18nextProvider } from 'react-i18next'

import {
Box,
Expand All @@ -12,7 +11,7 @@ import {
import { ApiHostProvider } from '@opentrons/react-api-client'
import NiceModal from '@ebay/nice-modal-react'

import { i18n } from '/app/i18n'
import { LocalizationProvider } from '/app/LocalizationProvider'
import { Alerts } from '/app/organisms/Desktop/Alerts'
import { Breadcrumbs } from '/app/organisms/Desktop/Breadcrumbs'
import { ToasterOven } from '/app/organisms/ToasterOven'
Expand Down Expand Up @@ -106,7 +105,7 @@ export const DesktopApp = (): JSX.Element => {

return (
<NiceModal.Provider>
<I18nextProvider i18n={i18n}>
<LocalizationProvider>
<ErrorBoundary FallbackComponent={DesktopAppFallback}>
<Navbar routes={desktopRoutes} />
<ToasterOven>
Expand Down Expand Up @@ -155,7 +154,7 @@ export const DesktopApp = (): JSX.Element => {
</EmergencyStopContext.Provider>
</ToasterOven>
</ErrorBoundary>
</I18nextProvider>
</LocalizationProvider>
</NiceModal.Provider>
)
}
Expand Down
6 changes: 3 additions & 3 deletions app/src/App/OnDeviceDisplayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ApiHostProvider } from '@opentrons/react-api-client'
import NiceModal from '@ebay/nice-modal-react'

import { SleepScreen } from '/app/atoms/SleepScreen'
import { OnDeviceLocalizationProvider } from '../LocalizationProvider'
import { LocalizationProvider } from '../LocalizationProvider'
import { ToasterOven } from '/app/organisms/ToasterOven'
import { MaintenanceRunTakeover } from '/app/organisms/TakeoverModal'
import { FirmwareUpdateTakeover } from '/app/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover'
Expand Down Expand Up @@ -180,7 +180,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => {
return (
<ApiHostProvider hostname="127.0.0.1">
<InitialLoadingScreen>
<OnDeviceLocalizationProvider>
<LocalizationProvider>
<ErrorBoundary FallbackComponent={OnDeviceDisplayAppFallback}>
<Box width="100%" css="user-select: none;">
{isIdle ? (
Expand All @@ -203,7 +203,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => {
</Box>
<ODDTopLevelRedirects />
</ErrorBoundary>
</OnDeviceLocalizationProvider>
</LocalizationProvider>
</InitialLoadingScreen>
</ApiHostProvider>
)
Expand Down
12 changes: 6 additions & 6 deletions app/src/App/__tests__/OnDeviceDisplayApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MemoryRouter } from 'react-router-dom'

import { renderWithProviders } from '/app/__testing-utils__'
import { i18n } from '/app/i18n'
import { OnDeviceLocalizationProvider } from '../../LocalizationProvider'
import { LocalizationProvider } from '../../LocalizationProvider'
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 @@ -32,7 +32,7 @@ import { ODDTopLevelRedirects } from '../ODDTopLevelRedirects'

import type { UseQueryResult } from 'react-query'
import type { RobotSettingsResponse } from '@opentrons/api-client'
import type { OnDeviceLocalizationProviderProps } from '../../LocalizationProvider'
import type { LocalizationProviderProps } from '../../LocalizationProvider'
import type { OnDeviceDisplaySettings } from '/app/redux/config/schema-types'

vi.mock('@opentrons/react-api-client', async () => {
Expand Down Expand Up @@ -100,8 +100,8 @@ describe('OnDeviceDisplayApp', () => {
} as any)
// TODO(bh, 2024-03-27): implement testing of branded and anonymous i18n, but for now pass through
vi.mocked(
OnDeviceLocalizationProvider
).mockImplementation((props: OnDeviceLocalizationProviderProps) => (
LocalizationProvider
).mockImplementation((props: LocalizationProviderProps) => (
<>{props.children}</>
))
})
Expand Down Expand Up @@ -163,14 +163,14 @@ describe('OnDeviceDisplayApp', () => {
})
it('renders the localization provider and not the loading screen when app-shell is ready', () => {
render('/')
expect(vi.mocked(OnDeviceLocalizationProvider)).toHaveBeenCalled()
expect(vi.mocked(LocalizationProvider)).toHaveBeenCalled()
expect(screen.queryByLabelText('loading indicator')).toBeNull()
})
it('renders the loading screen when app-shell is not ready', () => {
vi.mocked(getIsShellReady).mockReturnValue(false)
render('/')
screen.getByLabelText('loading indicator')
expect(vi.mocked(OnDeviceLocalizationProvider)).not.toHaveBeenCalled()
expect(vi.mocked(LocalizationProvider)).not.toHaveBeenCalled()
})
it('renders EmergencyStop component from /emergency-stop', () => {
render('/emergency-stop')
Expand Down
12 changes: 8 additions & 4 deletions app/src/LocalizationProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import type * as React from 'react'
import { I18nextProvider } from 'react-i18next'
import { useSelector } from 'react-redux'
import reduce from 'lodash/reduce'

import { resources } from './assets/localization'
import { getLanguage } from './redux/config'
import { useIsOEMMode } from './resources/robot-settings/hooks'
import { i18n, i18nCb, i18nConfig } from './i18n'

export interface OnDeviceLocalizationProviderProps {
export interface LocalizationProviderProps {
children?: React.ReactNode
}

export const BRANDED_RESOURCE = 'branded'
export const ANONYMOUS_RESOURCE = 'anonymous'

// TODO(bh, 2024-03-26): anonymization limited to ODD for now, may change in future OEM phases
export function OnDeviceLocalizationProvider(
props: OnDeviceLocalizationProviderProps
export function LocalizationProvider(
props: LocalizationProviderProps
): JSX.Element | null {
const isOEMMode = useIsOEMMode()

const language = useSelector(getLanguage)

// iterate through language resources, nested files, substitute anonymous file for branded file for OEM mode
const anonResources = reduce(
resources,
Expand All @@ -44,6 +47,7 @@ export function OnDeviceLocalizationProvider(
const anonI18n = i18n.createInstance(
{
...i18nConfig,
lng: language ?? 'en',
resources: anonResources,
},
i18nCb
Expand Down
Loading
Loading