diff --git a/src/about/AboutDialog.tsx b/src/about/AboutDialog.tsx index 76206bf89..a53d56513 100644 --- a/src/about/AboutDialog.tsx +++ b/src/about/AboutDialog.tsx @@ -16,7 +16,7 @@ import { } from '../app/constants'; import LicenseDialog from '../licenses/LicenseDialog'; import ExternalLinkIcon from '../utils/ExternalLinkIcon'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; import './about.scss'; @@ -32,7 +32,7 @@ const AboutDialog: React.VoidFunctionComponent = ({ return ( @@ -41,7 +41,7 @@ const AboutDialog: React.VoidFunctionComponent = ({

- {i18n.translate(I18nId.Description)} + {i18n.translate('description')}

{`v${firmwareVersion} (${appName} v${appVersion})`}

{pybricksCopyright}

@@ -52,14 +52,14 @@ const AboutDialog: React.VoidFunctionComponent = ({

- {i18n.translate(I18nId.ChangelogButtonLabel)} + {i18n.translate('changelogButton.label')} - {i18n.translate(I18nId.WebsiteButtonLabel)} + {i18n.translate('websiteButton.label')}
diff --git a/src/about/i18n.test.ts b/src/about/i18n.test.ts deleted file mode 100644 index 925768f19..000000000 --- a/src/about/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors - -import { lookup } from '../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/about/i18n.ts b/src/about/i18n.ts index 6f255e4d5..47ec27906 100644 --- a/src/about/i18n.ts +++ b/src/about/i18n.ts @@ -1,20 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors -// About dialog translation keys. +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../i18n'; +import type translations from './translations/en.json'; -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; - -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Title = 'title', - Description = 'description', - LicenseButtonLabel = 'licenseButton.label', - ChangelogButtonLabel = 'changelogButton.label', - WebsiteButtonLabel = 'websiteButton.label', -} diff --git a/src/activities/Activities.tsx b/src/activities/Activities.tsx index 1951ba496..7eb27f318 100644 --- a/src/activities/Activities.tsx +++ b/src/activities/Activities.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { useLocalStorage } from 'usehooks-ts'; import Explorer from '../explorer/Explorer'; import Settings from '../settings/Settings'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; /** Indicates the selected activity. */ export enum Activity { @@ -101,7 +101,7 @@ const Activities: React.VoidFunctionComponent = () => { return ( { ref={tabsRef} > @@ -125,15 +125,11 @@ const Activities: React.VoidFunctionComponent = () => { onMouseDown={(e) => e.stopPropagation()} /> + } panel={} panelClassName="pb-activities-tabview" diff --git a/src/activities/i18n.test.ts b/src/activities/i18n.test.ts deleted file mode 100644 index 56d5d1672..000000000 --- a/src/activities/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -import { lookup } from '../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/activities/i18n.ts b/src/activities/i18n.ts index 703906be9..47ec27906 100644 --- a/src/activities/i18n.ts +++ b/src/activities/i18n.ts @@ -1,18 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -// -// Explorer translation keys. -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Title = 'title', - Explorer = 'explorer', - Settings = 'settings', -} diff --git a/src/alerts/UnexpectedErrorAlert.tsx b/src/alerts/UnexpectedErrorAlert.tsx index ebbf2978d..9239dd3f4 100644 --- a/src/alerts/UnexpectedErrorAlert.tsx +++ b/src/alerts/UnexpectedErrorAlert.tsx @@ -6,7 +6,7 @@ import { AnchorButton, Button, ButtonGroup, Collapse, Intent } from '@blueprintj import React, { useState } from 'react'; import { useId } from 'react-aria'; import { CreateToast } from '../i18nToaster'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; type UnexpectedErrorAlertProps = { error: Error; @@ -21,7 +21,7 @@ const UnexpectedErrorAlert: React.VoidFunctionComponent -

{i18n.translate(I18nId.Message, { errorMessage: error.message })}

+

{i18n.translate('message', { errorMessage: error.message })}

- {i18n.translate(I18nId.ReportBug)} + {i18n.translate('reportBug')} diff --git a/src/alerts/i18n.test.ts b/src/alerts/i18n.test.ts deleted file mode 100644 index d0ceda4a7..000000000 --- a/src/alerts/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors - -import { lookup } from '../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/alerts/i18n.ts b/src/alerts/i18n.ts index 773d6e0f8..47ec27906 100644 --- a/src/alerts/i18n.ts +++ b/src/alerts/i18n.ts @@ -1,17 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Message = 'message', - TechnicalInfo = 'technicalInfo', - CopyErrorMessage = 'copyErrorMessage', - ReportBug = 'reportBug', -} diff --git a/src/ble/alerts/BluetoothNotAvailable.tsx b/src/ble/alerts/BluetoothNotAvailable.tsx index 91db98225..23385d359 100644 --- a/src/ble/alerts/BluetoothNotAvailable.tsx +++ b/src/ble/alerts/BluetoothNotAvailable.tsx @@ -4,14 +4,14 @@ import { Intent } from '@blueprintjs/core'; import React from 'react'; import { CreateToast } from '../../i18nToaster'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; const BluetoothNotAvailable: React.VoidFunctionComponent = () => { const i18n = useI18n(); return ( <> -

{i18n.translate(I18nId.BluetoothNotAvailableMessage)}

-

{i18n.translate(I18nId.BluetoothNotAvailableSuggestion)}

+

{i18n.translate('bluetoothNotAvailable.message')}

+

{i18n.translate('bluetoothNotAvailable.suggestion')}

); }; diff --git a/src/ble/alerts/MissingService.tsx b/src/ble/alerts/MissingService.tsx index caef23907..af3d67b53 100644 --- a/src/ble/alerts/MissingService.tsx +++ b/src/ble/alerts/MissingService.tsx @@ -4,7 +4,7 @@ import { Intent } from '@blueprintjs/core'; import React from 'react'; import { CreateToast } from '../../i18nToaster'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; type MissingServiceProps = { serviceName: string; @@ -18,9 +18,9 @@ const MissingService: React.VoidFunctionComponent = ({ const i18n = useI18n(); return ( <> -

{i18n.translate(I18nId.MissingServiceMessage, { serviceName })}

-

{i18n.translate(I18nId.MissingServiceSuggestion1)}

-

{i18n.translate(I18nId.MissingServiceSuggestion2, { hubName })}

+

{i18n.translate('missingService.message', { serviceName })}

+

{i18n.translate('missingService.suggestion1')}

+

{i18n.translate('missingService.suggestion2', { hubName })}

); }; diff --git a/src/ble/alerts/NoGatt.tsx b/src/ble/alerts/NoGatt.tsx index 78649013c..a44c8d3d8 100644 --- a/src/ble/alerts/NoGatt.tsx +++ b/src/ble/alerts/NoGatt.tsx @@ -4,11 +4,11 @@ import { Intent } from '@blueprintjs/core'; import React from 'react'; import { CreateToast } from '../../i18nToaster'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; const NoGatt: React.VoidFunctionComponent = () => { const i18n = useI18n(); - return

{i18n.translate(I18nId.NoGattMessage)}

; + return

{i18n.translate('noGatt.message')}

; }; export const noGatt: CreateToast = (onAction) => { diff --git a/src/ble/alerts/NoHub.tsx b/src/ble/alerts/NoHub.tsx index 61fc07287..0dba06089 100644 --- a/src/ble/alerts/NoHub.tsx +++ b/src/ble/alerts/NoHub.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { appName, pybricksBluetoothTroubleshootingUrl } from '../../app/constants'; import { CreateToast } from '../../i18nToaster'; import ExternalLinkIcon from '../../utils/ExternalLinkIcon'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; type NoHubProps = { onFlashFirmware: () => void; @@ -18,28 +18,26 @@ const NoHub: React.VoidFunctionComponent = ({ onFlashFirmware }) => return ( <> -

{i18n.translate(I18nId.NoHubMessage)}

+

{i18n.translate('noHub.message')}

- {i18n.translate(I18nId.NoHubSuggestion1, { + {i18n.translate('noHub.suggestion1', { appName, buttonName: ( - - {i18n.translate(I18nId.NoHubFlashFirmwareButton)} - + {i18n.translate('noHub.flashFirmwareButton')} ), })}

-

{i18n.translate(I18nId.NoHubSuggestion2)}

+

{i18n.translate('noHub.suggestion2')}

- {i18n.translate(I18nId.NoHubTroubleshootButton)} + {i18n.translate('noHub.troubleshootButton')}
diff --git a/src/ble/alerts/NoWebBluetooth.tsx b/src/ble/alerts/NoWebBluetooth.tsx index 175cd7844..cb1c82ebf 100644 --- a/src/ble/alerts/NoWebBluetooth.tsx +++ b/src/ble/alerts/NoWebBluetooth.tsx @@ -5,19 +5,19 @@ import { Button, Intent } from '@blueprintjs/core'; import React from 'react'; import { CreateToast } from '../../i18nToaster'; import { isIOS, isLinux } from '../../utils/os'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; const NoWebBluetooth: React.VoidFunctionComponent = () => { const i18n = useI18n(); return ( <> -

{i18n.translate(I18nId.NoWebBluetoothMessage)}

+

{i18n.translate('noWebBluetooth.message')}

{!isLinux() && !isIOS() && ( -

{i18n.translate(I18nId.NoWebBluetoothSuggestion)}

+

{i18n.translate('noWebBluetooth.suggestion')}

)} {isLinux() && ( <> -

{i18n.translate(I18nId.NoWebBluetoothLinux)}

+

{i18n.translate('noWebBluetooth.linux')}

chrome://flags/#enable-experimental-web-platform-features diff --git a/src/ble/alerts/OldFirmware.tsx b/src/ble/alerts/OldFirmware.tsx index a396bbc00..6f3a1173d 100644 --- a/src/ble/alerts/OldFirmware.tsx +++ b/src/ble/alerts/OldFirmware.tsx @@ -5,7 +5,7 @@ import './index.scss'; import { Button, Intent } from '@blueprintjs/core'; import React from 'react'; import { CreateToast } from '../../i18nToaster'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; type OldFirmwareProps = { onFlashFirmware: () => void; @@ -18,10 +18,10 @@ const OldFirmware: React.VoidFunctionComponent = ({ return ( <> -

{i18n.translate(I18nId.OldFirmwareMessage)}

+

{i18n.translate('oldFirmware.message')}

diff --git a/src/ble/alerts/i18n.test.ts b/src/ble/alerts/i18n.test.ts deleted file mode 100644 index e706ba281..000000000 --- a/src/ble/alerts/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -import { lookup } from '../../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/ble/alerts/i18n.ts b/src/ble/alerts/i18n.ts index 20fca0a10..eb8dc4868 100644 --- a/src/ble/alerts/i18n.ts +++ b/src/ble/alerts/i18n.ts @@ -1,29 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - NoWebBluetoothMessage = 'noWebBluetooth.message', - NoWebBluetoothSuggestion = 'noWebBluetooth.suggestion', - NoWebBluetoothLinux = 'noWebBluetooth.linux', - BluetoothNotAvailableMessage = 'bluetoothNotAvailable.message', - BluetoothNotAvailableSuggestion = 'bluetoothNotAvailable.suggestion', - NoGattMessage = 'noGatt.message', - MissingServiceMessage = 'missingService.message', - MissingServiceSuggestion1 = 'missingService.suggestion1', - MissingServiceSuggestion2 = 'missingService.suggestion2', - NoHubMessage = 'noHub.message', - NoHubSuggestion1 = 'noHub.suggestion1', - NoHubSuggestion2 = 'noHub.suggestion2', - NoHubFlashFirmwareButton = 'noHub.flashFirmwareButton', - NoHubTroubleshootButton = 'noHub.troubleshootButton', - OldFirmwareMessage = 'oldFirmware.message', - OldFirmwareFlashFirmwareLabel = 'oldFirmware.flashFirmware.label', -} diff --git a/src/components/HelpButton.tsx b/src/components/HelpButton.tsx index 8235a7e6d..ca9a55364 100644 --- a/src/components/HelpButton.tsx +++ b/src/components/HelpButton.tsx @@ -7,7 +7,7 @@ import { OverlayContainer } from 'react-aria'; import { useBoolean } from 'usehooks-ts'; import { Button } from './Button'; import HelpDialog from './HelpDialog'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; type HelpButtonProps = { /** The label of the control this button provides help for. */ @@ -49,9 +49,9 @@ const HelpButton: React.VoidFunctionComponent = ({ return ( <> diff --git a/src/explorer/duplicateFileDialog/i18n.test.ts b/src/explorer/duplicateFileDialog/i18n.test.ts deleted file mode 100644 index e706ba281..000000000 --- a/src/explorer/duplicateFileDialog/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -import { lookup } from '../../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/explorer/duplicateFileDialog/i18n.ts b/src/explorer/duplicateFileDialog/i18n.ts index 2a535dd7e..eb8dc4868 100644 --- a/src/explorer/duplicateFileDialog/i18n.ts +++ b/src/explorer/duplicateFileDialog/i18n.ts @@ -1,15 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Title = 'title', - ActionAccept = 'action.accept', -} diff --git a/src/explorer/fileNameFormGroup/FileNameFormGroup.tsx b/src/explorer/fileNameFormGroup/FileNameFormGroup.tsx index 02bf4460f..757433507 100644 --- a/src/explorer/fileNameFormGroup/FileNameFormGroup.tsx +++ b/src/explorer/fileNameFormGroup/FileNameFormGroup.tsx @@ -6,7 +6,7 @@ import type { AriaButtonProps } from '@react-types/button'; import React, { useCallback, useRef } from 'react'; import { useButton } from 'react-aria'; import { FileNameValidationResult } from '../../pybricksMicropython/lib'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; /** * Trims trailing and leading whitespace and replaces additional whitespace @@ -51,7 +51,7 @@ const FixItButton: React.VoidFunctionComponent = (props) => { ref, ); - return {i18n.translate(I18nId.HelpTextFixIt)}; + return {i18n.translate('helpText.fixIt')}; }; type FileNameHelpTextProps = { @@ -87,27 +87,27 @@ const FileNameHelpText: React.VoidFunctionComponent = ({ switch (validation) { case FileNameValidationResult.IsOk: - return <>{i18n.translate(I18nId.HelpTextIsOk)}; + return <>{i18n.translate('helpText.isOk')}; case FileNameValidationResult.IsEmpty: - return <>{i18n.translate(I18nId.HelpTextIsEmpty)}; + return <>{i18n.translate('helpText.isEmpty')}; case FileNameValidationResult.HasSpaces: return ( <> - {i18n.translate(I18nId.HelpTextHasSpaces)}{' '} + {i18n.translate('helpText.hasSpaces')}{' '} ); case FileNameValidationResult.HasFileExtension: return ( <> - {i18n.translate(I18nId.HelpTextHasFileExtension)}{' '} + {i18n.translate('helpText.hasFileExtension')}{' '} ); case FileNameValidationResult.HasInvalidFirstCharacter: return ( <> - {i18n.translate(I18nId.HelpTextHasInvalidFirstCharacter, { + {i18n.translate('helpText.hasInvalidFirstCharacter', { letters: a…z, underscore: _, })} @@ -116,7 +116,7 @@ const FileNameHelpText: React.VoidFunctionComponent = ({ case FileNameValidationResult.HasInvalidCharacters: return ( <> - {i18n.translate(I18nId.HelpTextHasInvalidCharacters, { + {i18n.translate('helpText.hasInvalidCharacters', { letters: a…z, numbers: 0…9, dash: -, @@ -126,7 +126,7 @@ const FileNameHelpText: React.VoidFunctionComponent = ({ ); case FileNameValidationResult.AlreadyExists: - return <>{i18n.translate(I18nId.HelpTextAlreadyExists)}; + return <>{i18n.translate('helpText.alreadyExists')}; } }; @@ -162,7 +162,7 @@ const FileNameFormGroup: React.VoidFunctionComponent = ( return ( { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/explorer/fileNameFormGroup/i18n.ts b/src/explorer/fileNameFormGroup/i18n.ts index bae37f620..eb8dc4868 100644 --- a/src/explorer/fileNameFormGroup/i18n.ts +++ b/src/explorer/fileNameFormGroup/i18n.ts @@ -1,22 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Label = 'label', - HelpTextIsOk = 'helpText.isOk', - HelpTextIsEmpty = 'helpText.isEmpty', - HelpTextHasSpaces = 'helpText.hasSpaces', - HelpTextHasFileExtension = 'helpText.hasFileExtension', - HelpTextHasInvalidFirstCharacter = 'helpText.hasInvalidFirstCharacter', - HelpTextHasInvalidCharacters = 'helpText.hasInvalidCharacters', - HelpTextAlreadyExists = 'helpText.alreadyExists', - HelpTextFixIt = 'helpText.fixIt', -} diff --git a/src/explorer/i18n.test.ts b/src/explorer/i18n.test.ts deleted file mode 100644 index 56d5d1672..000000000 --- a/src/explorer/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -import { lookup } from '../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/explorer/i18n.ts b/src/explorer/i18n.ts index e4193ba68..47ec27906 100644 --- a/src/explorer/i18n.ts +++ b/src/explorer/i18n.ts @@ -1,32 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -// -// Explorer translation keys. -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - HeaderToolbarAddNew = 'header.toolbar.addNew', - HeaderToolbarExportAll = 'header.toolbar.exportAll', - HeaderToolbarImport = 'header.toolbar.import', - HeaderToolbarTitle = 'header.toolbar.title', - TreeItemDeleteTooltip = 'treeItem.deleteTooltip', - TreeItemDuplicateTooltip = 'treeItem.duplicateTooltip', - TreeItemExportTooltip = 'treeItem.exportTooltip', - TreeItemRenameTooltip = 'treeItem.renameTooltip', - TreeLabel = 'tree.label', - TreeLiveDescriptorIntroAccessibilityGuide = 'tree.liveDescriptor.intro.accessibilityGuide', - TreeLiveDescriptorIntroKeybindingsDelete = 'tree.liveDescriptor.intro.keybindings.delete', - TreeLiveDescriptorIntroKeybindingsDuplicate = 'tree.liveDescriptor.intro.keybindings.duplicate', - TreeLiveDescriptorIntroKeybindingsExport = 'tree.liveDescriptor.intro.keybindings.export', - TreeLiveDescriptorIntroKeybindingsPrimaryAction = 'tree.liveDescriptor.intro.keybindings.primaryAction', - TreeLiveDescriptorIntroKeybindingsRename = 'tree.liveDescriptor.intro.keybindings.rename', - TreeLiveDescriptorIntroNavigation = 'tree.liveDescriptor.intro.navigation', - TreeLiveDescriptorSearching = 'tree.liveDescriptor.searching', -} diff --git a/src/explorer/newFileWizard/NewFileWizard.tsx b/src/explorer/newFileWizard/NewFileWizard.tsx index 549817360..09d54a826 100644 --- a/src/explorer/newFileWizard/NewFileWizard.tsx +++ b/src/explorer/newFileWizard/NewFileWizard.tsx @@ -16,7 +16,7 @@ import { import { useSelector } from '../../reducers'; import FileNameFormGroup from '../fileNameFormGroup/FileNameFormGroup'; import { newFileWizardDidAccept, newFileWizardDidCancel } from './actions'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; // This should be set to the most commonly used hub. const defaultHub = Hub.Technic; @@ -54,7 +54,7 @@ const NewFileWizard: React.VoidFunctionComponent = () => { return ( setFileName('')} onOpened={() => fileNameInputRef.current?.focus()} @@ -69,7 +69,7 @@ const NewFileWizard: React.VoidFunctionComponent = () => { inputRef={fileNameInputRef} onChange={setFileName} /> - + @@ -84,7 +84,7 @@ const NewFileWizard: React.VoidFunctionComponent = () => { type="submit" > - {i18n.translate(I18nId.ActionCreate)} + {i18n.translate('action.create')} diff --git a/src/explorer/newFileWizard/i18n.test.ts b/src/explorer/newFileWizard/i18n.test.ts deleted file mode 100644 index e706ba281..000000000 --- a/src/explorer/newFileWizard/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -import { lookup } from '../../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/explorer/newFileWizard/i18n.ts b/src/explorer/newFileWizard/i18n.ts index d15943b94..eb8dc4868 100644 --- a/src/explorer/newFileWizard/i18n.ts +++ b/src/explorer/newFileWizard/i18n.ts @@ -1,16 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Title = 'title', - SmartHubLabel = 'smartHub.label', - ActionCreate = 'action.create', -} diff --git a/src/explorer/renameFileDialog/RenameFileDialog.tsx b/src/explorer/renameFileDialog/RenameFileDialog.tsx index 2b6459760..36ce76fd6 100644 --- a/src/explorer/renameFileDialog/RenameFileDialog.tsx +++ b/src/explorer/renameFileDialog/RenameFileDialog.tsx @@ -12,7 +12,7 @@ import { import { useSelector } from '../../reducers'; import FileNameFormGroup from '../fileNameFormGroup/FileNameFormGroup'; import { renameFileDialogDidAccept, renameFileDialogDidCancel } from './actions'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; const RenameFileDialog: React.VFC = () => { const i18n = useI18n(); @@ -46,7 +46,7 @@ const RenameFileDialog: React.VFC = () => { return ( { disabled={result !== FileNameValidationResult.IsOk} type="submit" > - {i18n.translate(I18nId.ActionRename)} + {i18n.translate('action.rename')} diff --git a/src/explorer/renameFileDialog/i18n.test.ts b/src/explorer/renameFileDialog/i18n.test.ts deleted file mode 100644 index e706ba281..000000000 --- a/src/explorer/renameFileDialog/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -import { lookup } from '../../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/explorer/renameFileDialog/i18n.ts b/src/explorer/renameFileDialog/i18n.ts index 0836aec1a..eb8dc4868 100644 --- a/src/explorer/renameFileDialog/i18n.ts +++ b/src/explorer/renameFileDialog/i18n.ts @@ -1,15 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Title = 'title', - ActionRename = 'action.rename', -} diff --git a/src/firmware/alerts/FirmwareMismatch.tsx b/src/firmware/alerts/FirmwareMismatch.tsx index ba9522dfc..959e079f0 100644 --- a/src/firmware/alerts/FirmwareMismatch.tsx +++ b/src/firmware/alerts/FirmwareMismatch.tsx @@ -4,11 +4,11 @@ import { Intent } from '@blueprintjs/core'; import React from 'react'; import { CreateToast } from '../../i18nToaster'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; const FirmwareMismatch: React.VoidFunctionComponent = () => { const i18n = useI18n(); - return

{i18n.translate(I18nId.FirmwareMismatchMessage)}

; + return

{i18n.translate('firmwareMismatch.message')}

; }; export const firmwareMismatch: CreateToast = (onAction) => { diff --git a/src/firmware/alerts/NoDfuHub.tsx b/src/firmware/alerts/NoDfuHub.tsx index a065da105..2ede5557a 100644 --- a/src/firmware/alerts/NoDfuHub.tsx +++ b/src/firmware/alerts/NoDfuHub.tsx @@ -7,26 +7,26 @@ import { pybricksUsbDfuTroubleshootingUrl } from '../../app/constants'; import { CreateToast } from '../../i18nToaster'; import ExternalLinkIcon from '../../utils/ExternalLinkIcon'; import { isLinux, isWindows } from '../../utils/os'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; const NoDfuHub: React.VoidFunctionComponent = () => { const i18n = useI18n(); return ( <> -

{i18n.translate(I18nId.NoDfuHubMessage)}

+

{i18n.translate('noDfuHub.message')}

- {isWindows() &&

{i18n.translate(I18nId.NoDfuHubSuggestion1Windows)}

} - {isLinux() &&

{i18n.translate(I18nId.NoDfuHubSuggestion1Linux)}

} + {isWindows() &&

{i18n.translate('noDfuHub.suggestion1.windows')}

} + {isLinux() &&

{i18n.translate('noDfuHub.suggestion1.linux')}

} -

{i18n.translate(I18nId.NoDfuHubSuggestion2)}

+

{i18n.translate('noDfuHub.suggestion2')}

- {i18n.translate(I18nId.NoDfuHubTroubleshootButton)} + {i18n.translate('noDfuHub.suggestion2')} diff --git a/src/firmware/alerts/NoDfuInterface.tsx b/src/firmware/alerts/NoDfuInterface.tsx index 9751bbdbf..f8061b102 100644 --- a/src/firmware/alerts/NoDfuInterface.tsx +++ b/src/firmware/alerts/NoDfuInterface.tsx @@ -4,11 +4,11 @@ import { Intent } from '@blueprintjs/core'; import React from 'react'; import { CreateToast } from '../../i18nToaster'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; const NoDfuInterface: React.VoidFunctionComponent = () => { const i18n = useI18n(); - return

{i18n.translate(I18nId.NoDfuInterfaceMessage)}

; + return

{i18n.translate('noDfuInterface.message')}

; }; export const noDfuInterface: CreateToast = (onAction) => { diff --git a/src/firmware/alerts/NoWebUsb.tsx b/src/firmware/alerts/NoWebUsb.tsx index 3303c0234..6a9d168ad 100644 --- a/src/firmware/alerts/NoWebUsb.tsx +++ b/src/firmware/alerts/NoWebUsb.tsx @@ -4,14 +4,14 @@ import { Intent } from '@blueprintjs/core'; import React from 'react'; import { CreateToast } from '../../i18nToaster'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; const NoWebUsb: React.VoidFunctionComponent = () => { const i18n = useI18n(); return ( <> -

{i18n.translate(I18nId.NoWebUsbMessage)}

-

{i18n.translate(I18nId.NoWebUsbSuggestion)}

+

{i18n.translate('noWebUsb.message')}

+

{i18n.translate('noWebUsb.suggestion')}

); }; diff --git a/src/firmware/alerts/i18n.test.ts b/src/firmware/alerts/i18n.test.ts deleted file mode 100644 index e706ba281..000000000 --- a/src/firmware/alerts/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -import { lookup } from '../../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/firmware/alerts/i18n.ts b/src/firmware/alerts/i18n.ts index 35dfee632..eb8dc4868 100644 --- a/src/firmware/alerts/i18n.ts +++ b/src/firmware/alerts/i18n.ts @@ -1,22 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - NoWebUsbMessage = 'noWebUsb.message', - NoWebUsbSuggestion = 'noWebUsb.suggestion', - NoDfuHubMessage = 'noDfuHub.message', - NoDfuHubSuggestion1Windows = 'noDfuHub.suggestion1.windows', - NoDfuHubSuggestion1Linux = 'noDfuHub.suggestion1.linux', - NoDfuHubSuggestion2 = 'noDfuHub.suggestion2', - NoDfuHubTroubleshootButton = 'noDfuHub.troubleshootButton', - NoDfuInterfaceMessage = 'noDfuInterface.message', - FirmwareMismatchMessage = 'firmwareMismatch.message', -} diff --git a/src/firmware/installPybricksDialog/InstallPybricksDialog.tsx b/src/firmware/installPybricksDialog/InstallPybricksDialog.tsx index 3236c55e3..937727054 100644 --- a/src/firmware/installPybricksDialog/InstallPybricksDialog.tsx +++ b/src/firmware/installPybricksDialog/InstallPybricksDialog.tsx @@ -42,7 +42,7 @@ import { firmwareInstallPybricksDialogCancel, } from './actions'; import { useFirmware } from './hooks'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; import { validateHubName } from '.'; const dialogBody = classNames( @@ -63,7 +63,7 @@ const SelectHubPanel: React.VoidFunctionComponent = ({ return (
-

{i18n.translate(I18nId.SelectHubPanelMessage)}

+

{i18n.translate('selectHubPanel.message')}

= ({

{i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoMindstormsTitle, + 'selectHubPanel.notOnListButton.info.mindstorms.title', )}

  • {i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoMindstormsRcx, + 'selectHubPanel.notOnListButton.info.mindstorms.rcx', )}
  • {i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoMindstormsNxt, + 'selectHubPanel.notOnListButton.info.mindstorms.nxt', )}
  • {i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoMindstormsEv3, + 'selectHubPanel.notOnListButton.info.mindstorms.ev3', )}

{i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpTitle, + 'selectHubPanel.notOnListButton.info.poweredUp.title', )}

  • {i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpWedo2, + 'selectHubPanel.notOnListButton.info.poweredUp.wedo2', )} *
  • {i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpDuploTrain, + 'selectHubPanel.notOnListButton.info.poweredUp.duploTrain', )} *
  • {i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpMario, + 'selectHubPanel.notOnListButton.info.poweredUp.mario', )}
@@ -120,7 +120,7 @@ const SelectHubPanel: React.VoidFunctionComponent = ({ *{' '} {i18n.translate( - I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpFootnote, + 'selectHubPanel.notOnListButton.info.poweredUp.footnote', )}
@@ -130,7 +130,7 @@ const SelectHubPanel: React.VoidFunctionComponent = ({ elementRef={ref as IRef} {...targetProps} > - {i18n.translate(I18nId.SelectHubPanelNotOnListButtonLabel)} + {i18n.translate('selectHubPanel.notOnListButton.label')} )} /> @@ -163,16 +163,14 @@ const AcceptLicensePanel: React.VoidFunctionComponent = icon={error ? 'error' : } description={ error - ? i18n.translate( - I18nId.LicensePanelLicenseTextError, - ) + ? i18n.translate('licensePanel.licenseText.error') : undefined } /> )}
onLicenseAcceptedChanged(e.currentTarget.checked)} disabled={!data} @@ -208,8 +206,8 @@ const ConfigureOptionsPanel: React.VoidFunctionComponent {(hubHasExternalFlash(hubType) && (

{i18n.translate( - I18nId.OptionsPanelCustomMainNotApplicableMessage, + 'optionsPanel.customMain.notApplicable.message', )}

)) || ( main.py
}, )} checked={includeProgram} @@ -280,7 +278,7 @@ const ConfigureOptionsPanel: React.VoidFunctionComponent } @@ -294,7 +292,7 @@ const ConfigureOptionsPanel: React.VoidFunctionComponent return { button: i18n.translate( hubHasBluetoothButton(hubType) - ? I18nId.BootloaderPanelButtonBluetooth - : I18nId.BootloaderPanelButtonPower, + ? 'bootloaderPanel.button.bluetooth' + : 'bootloaderPanel.button.power', ), light: i18n.translate( hubHasBluetoothButton(hubType) - ? I18nId.BootloaderPanelLightBluetooth - : I18nId.BootloaderPanelLightStatus, + ? 'bootloaderPanel.light.bluetooth' + : 'bootloaderPanel.light.status', ), lightPattern: i18n.translate( hubHasBluetoothButton(hubType) - ? I18nId.BootloaderPanelLightPatternBluetooth - : I18nId.BootloaderPanelLightPatternStatus, + ? 'bootloaderPanel.lightPattern.bluetooth' + : 'bootloaderPanel.lightPattern.status', ), }; }, [i18n, hubType]); return (
-

{i18n.translate(I18nId.BootloaderPanelInstruction1)}

+

{i18n.translate('bootloaderPanel.instruction1')}

    {hubHasUSB(hubType) && ( -
  1. {i18n.translate(I18nId.BootloaderPanelStepDisconnectUsb)}
  2. +
  3. {i18n.translate('bootloaderPanel.step.disconnectUsb')}
  4. )} -
  5. {i18n.translate(I18nId.BootloaderPanelStepPowerOff)}
  6. +
  7. {i18n.translate('bootloaderPanel.step.powerOff')}
  8. {/* City hub has power issues and requires disconnecting motors/sensors */} {hubType === Hub.City && ( -
  9. {i18n.translate(I18nId.BootloaderPanelStepDisconnectIo)}
  10. +
  11. {i18n.translate('bootloaderPanel.step.disconnectIo')}
  12. )} -
  13. - {i18n.translate(I18nId.BootloaderPanelStepHoldButton, { button })} -
  14. +
  15. {i18n.translate('bootloaderPanel.step.holdButton', { button })}
  16. {hubHasUSB(hubType) && ( -
  17. {i18n.translate(I18nId.BootloaderPanelStepConnectUsb)}
  18. +
  19. {i18n.translate('bootloaderPanel.step.connectUsb')}
  20. )}
  21. - {i18n.translate(I18nId.BootloaderPanelStepWaitForLight, { + {i18n.translate('bootloaderPanel.step.waitForLight', { button, light, lightPattern, @@ -383,8 +379,8 @@ const BootloaderModePanel: React.VoidFunctionComponent {i18n.translate( /* hubs with USB will keep the power on, but other hubs won't */ hubHasUSB(hubType) - ? I18nId.BootloaderPanelStepReleaseButton - : I18nId.BootloaderPanelStepKeepHolding, + ? 'bootloaderPanel.step.releaseButton' + : 'bootloaderPanel.step.keepHolding', { button, }, @@ -392,11 +388,9 @@ const BootloaderModePanel: React.VoidFunctionComponent

- {i18n.translate(I18nId.BootloaderPanelInstruction2, { + {i18n.translate('bootloaderPanel.instruction2', { flashFirmware: ( - - {i18n.translate(I18nId.FlashFirmwareButtonLabel)} - + {i18n.translate('flashFirmwareButton.label')} ), })}

@@ -419,11 +413,11 @@ export const InstallPybricksDialog: React.VoidFunctionComponent = () => { return ( dispatch(firmwareInstallPybricksDialogCancel())} finalButtonProps={{ - text: i18n.translate(I18nId.FlashFirmwareButtonLabel), + text: i18n.translate('flashFirmwareButton.label'), onClick: () => dispatch( firmwareInstallPybricksDialogAccept( @@ -437,13 +431,13 @@ export const InstallPybricksDialog: React.VoidFunctionComponent = () => { > } - nextButtonProps={{ text: i18n.translate(I18nId.NextButtonLabel) }} + nextButtonProps={{ text: i18n.translate('nextButton.label') }} /> { onLicenseAcceptedChanged={setLicenseAccepted} /> } - backButtonProps={{ text: i18n.translate(I18nId.BackButtonLabel) }} + backButtonProps={{ text: i18n.translate('backButton.label') }} nextButtonProps={{ disabled: !licenseAccepted, - text: i18n.translate(I18nId.NextButtonLabel), + text: i18n.translate('nextButton.label'), }} /> { onChangeSelectedIncludeFile={setSelectedIncludeFile} /> } - backButtonProps={{ text: i18n.translate(I18nId.BackButtonLabel) }} - nextButtonProps={{ text: i18n.translate(I18nId.NextButtonLabel) }} + backButtonProps={{ text: i18n.translate('backButton.label') }} + nextButtonProps={{ text: i18n.translate('nextButton.label') }} /> } - backButtonProps={{ text: i18n.translate(I18nId.BackButtonLabel) }} + backButtonProps={{ text: i18n.translate('backButton.label') }} /> ); diff --git a/src/firmware/installPybricksDialog/i18n.en.test.ts b/src/firmware/installPybricksDialog/i18n.en.test.ts deleted file mode 100644 index 3b098b20f..000000000 --- a/src/firmware/installPybricksDialog/i18n.en.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors - -import { lookup } from '../../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/firmware/installPybricksDialog/i18n.ts b/src/firmware/installPybricksDialog/i18n.ts index d7a314713..eb8dc4868 100644 --- a/src/firmware/installPybricksDialog/i18n.ts +++ b/src/firmware/installPybricksDialog/i18n.ts @@ -1,63 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors -// -// Settings translation keys. +// Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Title = 'title', - SelectHubPanelTitle = 'selectHubPanel.title', - SelectHubPanelMessage = 'selectHubPanel.message', - SelectHubPanelNotOnListButtonLabel = 'selectHubPanel.notOnListButton.label', - SelectHubPanelNotOnListButtonInfoMindstormsTitle = 'selectHubPanel.notOnListButton.info.mindstorms.title', - SelectHubPanelNotOnListButtonInfoMindstormsRcx = 'selectHubPanel.notOnListButton.info.mindstorms.rcx', - SelectHubPanelNotOnListButtonInfoMindstormsNxt = 'selectHubPanel.notOnListButton.info.mindstorms.nxt', - SelectHubPanelNotOnListButtonInfoMindstormsEv3 = 'selectHubPanel.notOnListButton.info.mindstorms.ev3', - SelectHubPanelNotOnListButtonInfoPoweredUpTitle = 'selectHubPanel.notOnListButton.info.poweredUp.title', - SelectHubPanelNotOnListButtonInfoPoweredUpWedo2 = 'selectHubPanel.notOnListButton.info.poweredUp.wedo2', - SelectHubPanelNotOnListButtonInfoPoweredUpDuploTrain = 'selectHubPanel.notOnListButton.info.poweredUp.duploTrain', - SelectHubPanelNotOnListButtonInfoPoweredUpMario = 'selectHubPanel.notOnListButton.info.poweredUp.mario', - SelectHubPanelNotOnListButtonInfoPoweredUpFootnote = 'selectHubPanel.notOnListButton.info.poweredUp.footnote', - LicensePanelTitle = 'licensePanel.title', - LicensePanelLicenseTextError = 'licensePanel.licenseText.error', - LicensePanelAcceptCheckboxLabel = 'licensePanel.acceptCheckbox.label', - OptionsPanelTitle = 'optionsPanel.title', - OptionsPanelHubNameLabel = 'optionsPanel.hubName.label', - OptionsPanelHubNameLabelInfo = 'optionsPanel.hubName.labelInfo', - OptionsPanelHubNameHelp = 'optionsPanel.hubName.help', - OptionsPanelHubNameError = 'optionsPanel.hubName.error', - OptionsPanelCustomMainLabel = 'optionsPanel.customMain.label', - OptionsPanelCustomMainLabelInfo = 'optionsPanel.customMain.labelInfo', - OptionsPanelCustomMainNotApplicableMessage = 'optionsPanel.customMain.notApplicable.message', - OptionsPanelCustomMainIncludeLabel = 'optionsPanel.customMain.include.label', - OptionsPanelCustomMainIncludeNoSelection = 'optionsPanel.customMain.include.noSelection', - OptionsPanelCustomMainIncludeNoFiles = 'optionsPanel.customMain.include.noFiles', - OptionsPanelCustomMainIncludeHelp = 'optionsPanel.customMain.include.help', - BootloaderPanelTitle = 'bootloaderPanel.title', - BootloaderPanelInstruction1 = 'bootloaderPanel.instruction1', - BootloaderPanelButtonBluetooth = 'bootloaderPanel.button.bluetooth', - BootloaderPanelButtonPower = 'bootloaderPanel.button.power', - BootloaderPanelLightBluetooth = 'bootloaderPanel.light.bluetooth', - BootloaderPanelLightStatus = 'bootloaderPanel.light.status', - BootloaderPanelLightPatternBluetooth = 'bootloaderPanel.lightPattern.bluetooth', - BootloaderPanelLightPatternStatus = 'bootloaderPanel.lightPattern.status', - BootloaderPanelStepDisconnectUsb = 'bootloaderPanel.step.disconnectUsb', - BootloaderPanelStepPowerOff = 'bootloaderPanel.step.powerOff', - BootloaderPanelStepDisconnectIo = 'bootloaderPanel.step.disconnectIo', - BootloaderPanelStepHoldButton = 'bootloaderPanel.step.holdButton', - BootloaderPanelStepConnectUsb = 'bootloaderPanel.step.connectUsb', - BootloaderPanelStepWaitForLight = 'bootloaderPanel.step.waitForLight', - BootloaderPanelStepReleaseButton = 'bootloaderPanel.step.releaseButton', - BootloaderPanelStepKeepHolding = 'bootloaderPanel.step.keepHolding', - BootloaderPanelInstruction2 = 'bootloaderPanel.instruction2', - NextButtonLabel = 'nextButton.label', - BackButtonLabel = 'backButton.label', - FlashFirmwareButtonLabel = 'flashFirmwareButton.label', -} diff --git a/src/i18n.ts b/src/i18n.ts index 2731a64e3..b8171bd99 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,7 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2021 The Pybricks Authors +// Copyright (c) 2020-2022 The Pybricks Authors -import { I18nManager } from '@shopify/react-i18n'; +import { I18n, I18nManager, TranslateOptions } from '@shopify/react-i18n'; +import type { + ComplexReplacementDictionary, + PrimitiveReplacementDictionary, + TranslationDictionary, +} from '@shopify/react-i18n/build/ts/types'; // TODO: add locale setting and use browser preferred language as default @@ -15,3 +20,61 @@ export const i18nManager = new I18nManager({ export function pseudolocalize(pseudolocalize: boolean): void { i18nManager.update({ ...i18nManager.details, pseudolocalize }); } + +// brilliant magic to turn nested json keys into types +// https://newbedev.com/typescript-deep-keyof-of-a-nested-object + +type Join = K extends string | number + ? P extends string | number + ? `${K}${'' extends P ? '' : '.'}${P}` + : never + : never; + +// prettier-ignore +type Prev = [ + never, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ...0[], +]; + +type Paths = [D] extends [never] + ? never + : T extends object + ? { + [K in keyof T]-?: K extends string | number + ? `${K}` | Join> + : never; + }[keyof T] + : ''; + +/** + * Interface for {@link I18n} that provides strong type checking for + * translations ids. + * + * @typeParam T - The type of the translation json file. + */ +export type TypedI18n = Omit< + I18n, + 'translate' | 'getTranslationTree' | 'translationKeyExists' +> & { + translate( + id: Paths, + options: TranslateOptions, + replacements?: PrimitiveReplacementDictionary, + ): string; + translate( + id: Paths, + options: TranslateOptions, + replacements?: ComplexReplacementDictionary, + ): React.ReactElement; + translate(id: Paths, replacements?: PrimitiveReplacementDictionary): string; + translate( + id: Paths, + replacements?: ComplexReplacementDictionary, + ): React.ReactElement; + getTranslationTree( + id: Paths, + replacements?: PrimitiveReplacementDictionary | ComplexReplacementDictionary, + ): string | TranslationDictionary; + translationKeyExists(id: Paths): boolean; +}; diff --git a/src/licenses/LicenseDialog.tsx b/src/licenses/LicenseDialog.tsx index ca6805510..aaa851579 100644 --- a/src/licenses/LicenseDialog.tsx +++ b/src/licenses/LicenseDialog.tsx @@ -21,7 +21,7 @@ import React, { useCallback, useState } from 'react'; import { mergeProps, useFocusRing, useListBox, useOption } from 'react-aria'; import { useFetch } from 'usehooks-ts'; import { appName } from '../app/constants'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; interface LicenseInfo { readonly name: string; @@ -158,11 +158,11 @@ const LicenseListPanel: React.VoidFunctionComponent = ({
{data === undefined ? ( - {error ? i18n.translate(I18nId.ErrorFetchFailed) : } + {error ? i18n.translate('error.fetchFailed') : } ) : (
{licenseInfo === undefined ? ( - {i18n.translate(I18nId.SelectPackageHelp)} + {i18n.translate('help.selectPackage')} ) : (

- {i18n.translate(I18nId.PackageLabel)}{' '} + {i18n.translate('packageLabel')}{' '} {licenseInfo.name}{' '} v{licenseInfo.version} @@ -201,14 +201,12 @@ const LicenseInfoPanel = React.forwardRef

{licenseInfo.author && (

- - {i18n.translate(I18nId.AuthorLabel)} - {' '} + {i18n.translate('authorLabel')}{' '} {licenseInfo.author}

)}

- {i18n.translate(I18nId.LicenseLabel)}{' '} + {i18n.translate('licenseLabel')}{' '} {licenseInfo.license}

@@ -240,13 +238,13 @@ const LicenseDialog: React.VoidFunctionComponent = ({ return (
- {i18n.translate(I18nId.Description, { + {i18n.translate('description', { name: appName, })} diff --git a/src/licenses/i18n.test.ts b/src/licenses/i18n.test.ts deleted file mode 100644 index 925768f19..000000000 --- a/src/licenses/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors - -import { lookup } from '../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/licenses/i18n.ts b/src/licenses/i18n.ts index 40bdd053d..47ec27906 100644 --- a/src/licenses/i18n.ts +++ b/src/licenses/i18n.ts @@ -1,23 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors -// License dialog translation keys. +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../i18n'; +import type translations from './translations/en.json'; -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; - -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Title = 'title', - Description = 'description', - PackageListLabel = 'packageList.label', - PackageLabel = 'packageLabel', - AuthorLabel = 'authorLabel', - LicenseLabel = 'licenseLabel', - ErrorFetchFailed = 'error.fetchFailed', - SelectPackageHelp = 'help.selectPackage', -} diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 67c5aca28..08232510d 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -28,7 +28,7 @@ import { useSelector } from '../reducers'; import ExternalLinkIcon from '../utils/ExternalLinkIcon'; import { isMacOS } from '../utils/os'; import { useSettingIsShowDocsEnabled } from './hooks'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; import './settings.scss'; const Settings: React.VoidFunctionComponent = () => { @@ -55,15 +55,15 @@ const Settings: React.VoidFunctionComponent = () => { return (
{isMacOS() ? 'Cmd' : 'Ctrl'}-+, out: {isMacOS() ? 'Cmd' : 'Ctrl'}--, })} > setIsSettingShowDocsEnabled( @@ -72,15 +72,13 @@ const Settings: React.VoidFunctionComponent = () => { } /> setTernaryDarkMode( @@ -91,42 +89,42 @@ const Settings: React.VoidFunctionComponent = () => { } /> - + @@ -120,14 +116,12 @@ const BatteryIndicator: React.VoidFunctionComponent = () => { {...commonPopoverProps} content={ - {i18n.translate( - lowBatteryWarning ? I18nId.BatteryLow : I18nId.BatteryOk, - )} + {i18n.translate(lowBatteryWarning ? 'battery.low' : 'battery.ok')} } >
diff --git a/src/status-bar/i18n.test.ts b/src/status-bar/i18n.test.ts deleted file mode 100644 index d0ceda4a7..000000000 --- a/src/status-bar/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors - -import { lookup } from '../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/status-bar/i18n.ts b/src/status-bar/i18n.ts index f6252dde6..47ec27906 100644 --- a/src/status-bar/i18n.ts +++ b/src/status-bar/i18n.ts @@ -1,27 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors -// -// Status bar translation keys. +// Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - CompletionEngineStatusLabel = 'completionEngineStatus.label', - CompletionEngineStatusMessageLoading = 'completionEngineStatus.message.loading', - CompletionEngineStatusMessageReady = 'completionEngineStatus.message.ready', - CompletionEngineStatusMessageFailed = 'completionEngineStatus.message.failed', - CompletionEngineStatusMessageUnknown = 'completionEngineStatus.message.unknown', - BatteryTitle = 'battery.title', - BatteryLow = 'battery.low', - BatteryOk = 'battery.ok', - HubInfoTitle = 'hubInfo.title', - HubInfoConnectedTo = 'hubInfo.connectedTo', - HubInfoHubType = 'hubInfo.hubType', - HubInfoFirmware = 'hubInfo.firmware', -} diff --git a/src/terminal/Terminal.tsx b/src/terminal/Terminal.tsx index e6def0989..f2900abde 100644 --- a/src/terminal/Terminal.tsx +++ b/src/terminal/Terminal.tsx @@ -11,7 +11,7 @@ import { FitAddon } from 'xterm-addon-fit'; import { isMacOS } from '../utils/os'; import { TerminalContext } from './TerminalContext'; import { receiveData } from './actions'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; import 'xterm/css/xterm.css'; @@ -64,7 +64,7 @@ function createContextMenu( navigator.clipboard.writeText(selected); } }} - text={i18n.translate(I18nId.Copy)} + text={i18n.translate('copy')} icon="duplicate" label={isMacOS() ? 'Cmd-C' : 'Ctrl-Shift-C'} disabled={!xterm.hasSelection()} @@ -73,19 +73,19 @@ function createContextMenu( onClick={async (): Promise => { xterm.paste(await navigator.clipboard.readText()); }} - text={i18n.translate(I18nId.Paste)} + text={i18n.translate('paste')} icon="clipboard" label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'} /> xterm.selectAll()} - text={i18n.translate(I18nId.SelectAll)} + text={i18n.translate('selectAll')} icon="blank" /> xterm.clear()} - text={i18n.translate(I18nId.Clear)} + text={i18n.translate('clear')} icon="trash" /> diff --git a/src/terminal/i18n.test.ts b/src/terminal/i18n.test.ts deleted file mode 100644 index d0ceda4a7..000000000 --- a/src/terminal/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors - -import { lookup } from '../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/terminal/i18n.ts b/src/terminal/i18n.ts index 842f75530..47ec27906 100644 --- a/src/terminal/i18n.ts +++ b/src/terminal/i18n.ts @@ -1,19 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors -// -// Terminal translation keys. +// Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Copy = 'copy', - Paste = 'paste', - SelectAll = 'selectAll', - Clear = 'clear', -} diff --git a/src/toolbar/buttons/bluetooth/BluetoothButton.tsx b/src/toolbar/buttons/bluetooth/BluetoothButton.tsx index 05caaaae6..02c0824ca 100644 --- a/src/toolbar/buttons/bluetooth/BluetoothButton.tsx +++ b/src/toolbar/buttons/bluetooth/BluetoothButton.tsx @@ -10,7 +10,7 @@ import { useSelector } from '../../../reducers'; import ActionButton, { ActionButtonProps } from '../../ActionButton'; import connectedIcon from './connected.svg'; import disconnectedIcon from './disconnected.svg'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; type BluetoothButtonProps = Pick; @@ -28,9 +28,9 @@ const BluetoothButton: React.VoidFunctionComponent = ({ id return ( { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/toolbar/buttons/bluetooth/i18n.ts b/src/toolbar/buttons/bluetooth/i18n.ts index a2cb4aeb0..6c6df61d3 100644 --- a/src/toolbar/buttons/bluetooth/i18n.ts +++ b/src/toolbar/buttons/bluetooth/i18n.ts @@ -1,16 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Label = 'label', - TooltipConnect = 'tooltip.connect', - TooltipDisconnect = 'tooltip.disconnect', -} diff --git a/src/toolbar/buttons/repl/ReplButton.tsx b/src/toolbar/buttons/repl/ReplButton.tsx index 2ad36d9f5..b9c392bbc 100644 --- a/src/toolbar/buttons/repl/ReplButton.tsx +++ b/src/toolbar/buttons/repl/ReplButton.tsx @@ -7,7 +7,7 @@ import { repl } from '../../../hub/actions'; import { HubRuntimeState } from '../../../hub/reducers'; import { useSelector } from '../../../reducers'; import ActionButton, { ActionButtonProps } from '../../ActionButton'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; import icon from './icon.svg'; type ReplButtonProps = Pick; @@ -22,8 +22,8 @@ const ReplButton: React.VoidFunctionComponent = ({ id }) => { return ( { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/toolbar/buttons/repl/i18n.ts b/src/toolbar/buttons/repl/i18n.ts index 14dedb9a3..6c6df61d3 100644 --- a/src/toolbar/buttons/repl/i18n.ts +++ b/src/toolbar/buttons/repl/i18n.ts @@ -1,15 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Label = 'label', - Tooltip = 'tooltip', -} diff --git a/src/toolbar/buttons/run/RunButton.tsx b/src/toolbar/buttons/run/RunButton.tsx index 357f11872..4eb265cdf 100644 --- a/src/toolbar/buttons/run/RunButton.tsx +++ b/src/toolbar/buttons/run/RunButton.tsx @@ -7,7 +7,7 @@ import { downloadAndRun } from '../../../hub/actions'; import { HubRuntimeState } from '../../../hub/reducers'; import { useSelector } from '../../../reducers'; import ActionButton, { ActionButtonProps } from '../../ActionButton'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; import icon from './icon.svg'; type RunButtonProps = Pick; @@ -25,14 +25,14 @@ const RunButton: React.VoidFunctionComponent = ({ id }) => { return ( { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/toolbar/buttons/run/i18n.ts b/src/toolbar/buttons/run/i18n.ts index 729392036..6c6df61d3 100644 --- a/src/toolbar/buttons/run/i18n.ts +++ b/src/toolbar/buttons/run/i18n.ts @@ -1,16 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Label = 'label', - TooltipAction = 'tooltip.action', - TooltipProgress = 'tooltip.progress', -} diff --git a/src/toolbar/buttons/stop/StopButton.tsx b/src/toolbar/buttons/stop/StopButton.tsx index 82adcc32c..dfd2ade31 100644 --- a/src/toolbar/buttons/stop/StopButton.tsx +++ b/src/toolbar/buttons/stop/StopButton.tsx @@ -7,7 +7,7 @@ import { stop } from '../../../hub/actions'; import { HubRuntimeState } from '../../../hub/reducers'; import { useSelector } from '../../../reducers'; import ActionButton, { ActionButtonProps } from '../../ActionButton'; -import { I18nId, useI18n } from './i18n'; +import { useI18n } from './i18n'; import icon from './icon.svg'; type StopButtonProps = Pick; @@ -22,9 +22,9 @@ const StopButton: React.VoidFunctionComponent = ({ id }) => { return ( dispatch(stop())} diff --git a/src/toolbar/buttons/stop/i18n.test.ts b/src/toolbar/buttons/stop/i18n.test.ts deleted file mode 100644 index b8f901e0d..000000000 --- a/src/toolbar/buttons/stop/i18n.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors - -import { lookup } from '../../../../test'; -import { I18nId } from './i18n'; -import en from './translations/en.json'; - -describe('Ensure .json file has matches for I18nId', () => { - test.each(Object.values(I18nId))('%s', (id) => { - expect(lookup(en, id)).toBeDefined(); - }); -}); diff --git a/src/toolbar/buttons/stop/i18n.ts b/src/toolbar/buttons/stop/i18n.ts index 14dedb9a3..6c6df61d3 100644 --- a/src/toolbar/buttons/stop/i18n.ts +++ b/src/toolbar/buttons/stop/i18n.ts @@ -1,15 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors -import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../../i18n'; +import type translations from './translations/en.json'; -export function useI18n(): I18n { +export function useI18n(): TypedI18n { // istanbul ignore next: babel-loader rewrites this line const [i18n] = useShopifyI18n(); return i18n; } - -export enum I18nId { - Label = 'label', - Tooltip = 'tooltip', -}