i18n: use automatic type inference of translation keys

Finally figured out a way to make typescript strongly type translation
keys so we can avoid the tedium of creating enums of translation keys.
This commit is contained in:
David Lechner
2022-07-22 13:48:50 -05:00
parent 3e10096b6e
commit d6df128860
80 changed files with 402 additions and 875 deletions
+23 -27
View File
@@ -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 (
<div className="pb-settings">
<FormGroup
label={i18n.translate(I18nId.AppearanceTitle)}
helperText={i18n.translate(I18nId.AppearanceZoomHelp, {
label={i18n.translate('appearance.title')}
helperText={i18n.translate('appearance.zoom.help', {
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
})}
>
<ControlGroup>
<Switch
label={i18n.translate(I18nId.AppearanceDocumentationLabel)}
label={i18n.translate('appearance.documentation.label')}
checked={isSettingShowDocsEnabled}
onChange={(e) =>
setIsSettingShowDocsEnabled(
@@ -72,15 +72,13 @@ const Settings: React.VoidFunctionComponent = () => {
}
/>
<HelpButton
helpForLabel={i18n.translate(
I18nId.AppearanceDocumentationLabel,
)}
content={i18n.translate(I18nId.AppearanceDocumentationHelp)}
helpForLabel={i18n.translate('appearance.documentation.label')}
content={i18n.translate('appearance.documentation.help')}
/>
</ControlGroup>
<ControlGroup>
<Switch
label={i18n.translate(I18nId.AppearanceDarkModeLabel)}
label={i18n.translate('appearance.darkMode.label')}
checked={isDarkMode}
onChange={(e) =>
setTernaryDarkMode(
@@ -91,42 +89,42 @@ const Settings: React.VoidFunctionComponent = () => {
}
/>
<HelpButton
helpForLabel={i18n.translate(I18nId.AppearanceDarkModeLabel)}
content={i18n.translate(I18nId.AppearanceDarkModeHelp)}
helpForLabel={i18n.translate('appearance.darkMode.label')}
content={i18n.translate('appearance.darkMode.help')}
/>
</ControlGroup>
</FormGroup>
<FormGroup label={i18n.translate(I18nId.FirmwareTitle)}>
<FormGroup label={i18n.translate('firmware.title')}>
<Button
minimal={true}
icon="download"
label={i18n.translate(I18nId.FirmwareFlashPybricksLabel)}
label={i18n.translate('firmware.flashPybricksButton.label')}
onPress={() => dispatch(firmwareInstallPybricks())}
/>
<InstallPybricksDialog />
<Button
minimal={true}
icon="download"
label={i18n.translate(I18nId.FirmwareFlashLegoLabel)}
label={i18n.translate('firmware.flashLegoButton.label')}
onPress={() => dispatch(firmwareRestoreLego())}
/>
</FormGroup>
<FormGroup label={i18n.translate(I18nId.HelpTitle)}>
<FormGroup label={i18n.translate('help.title')}>
<ButtonGroup minimal={true} vertical={true} alignText="left">
<AnchorButton
icon="lightbulb"
href={pybricksProjectsUrl}
target="blank_"
>
{i18n.translate(I18nId.HelpProjectsLabel)}
{i18n.translate('help.projects.label')}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton icon="help" href={pybricksSupportUrl} target="blank_">
{i18n.translate(I18nId.HelpSupportLabel)}
{i18n.translate('help.support.label')}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton icon="chat" href={pybricksGitterUrl} target="blank_">
{i18n.translate(I18nId.HelpChatLabel)}
{i18n.translate('help.chat.label')}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
@@ -134,7 +132,7 @@ const Settings: React.VoidFunctionComponent = () => {
href={pybricksBugReportsUrl}
target="blank_"
>
{i18n.translate(I18nId.HelpBugsLabel)}
{i18n.translate('help.bugs.label')}
<ExternalLinkIcon />
</AnchorButton>
<AboutDialog
@@ -144,15 +142,13 @@ const Settings: React.VoidFunctionComponent = () => {
</ButtonGroup>
</FormGroup>
<FormGroup
label={i18n.translate(I18nId.AppTitle)}
helperText={
readyForOfflineUse && i18n.translate(I18nId.AppOfflineUseHelp)
}
label={i18n.translate('app.title')}
helperText={readyForOfflineUse && i18n.translate('app.offlineUseHelp')}
>
<ButtonGroup minimal={true} vertical={true} alignText="left">
{hasUnresolvedInstallPrompt && (
<Button
label={i18n.translate(I18nId.AppInstallLabel)}
label={i18n.translate('app.install.label')}
icon="add"
onPress={() => dispatch(appShowInstallPrompt())}
loading={promptingInstall}
@@ -161,7 +157,7 @@ const Settings: React.VoidFunctionComponent = () => {
{(process.env.NODE_ENV === 'development' ||
(isServiceWorkerRegistered && !updateAvailable)) && (
<Button
label={i18n.translate(I18nId.AppCheckForUpdateLabel)}
label={i18n.translate('app.checkForUpdate.label')}
icon="refresh"
onPress={() => dispatch(appCheckForUpdate())}
loading={checkingForUpdate}
@@ -170,13 +166,13 @@ const Settings: React.VoidFunctionComponent = () => {
{(process.env.NODE_ENV === 'development' ||
(isServiceWorkerRegistered && updateAvailable)) && (
<Button
label={i18n.translate(I18nId.AppRestartLabel)}
label={i18n.translate('app.restart.label')}
icon="refresh"
onPress={() => dispatch(appReload())}
/>
)}
<Button
label={i18n.translate(I18nId.AppAboutLabel)}
label={i18n.translate('app.about.label')}
icon="info-sign"
onPress={() => {
setIsAboutDialogOpen(true);
-12
View File
@@ -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();
});
});
+5 -29
View File
@@ -1,36 +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<typeof translations> {
// istanbul ignore next: babel-loader rewrites this line
const [i18n] = useShopifyI18n();
return i18n;
}
export enum I18nId {
Title = 'title',
AppearanceTitle = 'appearance.title',
AppearanceDocumentationLabel = 'appearance.documentation.label',
AppearanceDocumentationHelp = 'appearance.documentation.help',
AppearanceDarkModeLabel = 'appearance.darkMode.label',
AppearanceDarkModeHelp = 'appearance.darkMode.help',
AppearanceZoomHelp = 'appearance.zoom.help',
FirmwareTitle = 'firmware.title',
FirmwareFlashPybricksLabel = 'firmware.flashPybricksButton.label',
FirmwareFlashLegoLabel = 'firmware.flashLegoButton.label',
HelpTitle = 'help.title',
HelpProjectsLabel = 'help.projects.label',
HelpSupportLabel = 'help.support.label',
HelpChatLabel = 'help.chat.label',
HelpBugsLabel = 'help.bugs.label',
AppTitle = 'app.title',
AppOfflineUseHelp = 'app.offlineUseHelp',
AppInstallLabel = 'app.install.label',
AppCheckForUpdateLabel = 'app.checkForUpdate.label',
AppRestartLabel = 'app.restart.label',
AppAboutLabel = 'app.about.label',
}