i18n: use babel-loader plugin

This commit is contained in:
David Lechner
2022-03-25 15:54:04 -05:00
parent 45d0b4071a
commit 9f254f035f
68 changed files with 609 additions and 653 deletions
+7 -8
View File
@@ -17,8 +17,7 @@ import {
} from '../app/constants';
import LicenseDialog from '../licenses/LicenseDialog';
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
import { AboutStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import './about.scss';
@@ -30,11 +29,11 @@ const AboutDialog: React.VoidFunctionComponent<AboutDialogProps> = ({
}) => {
const [isLicenseDialogOpen, setIsLicenseDialogOpen] = useState(false);
const [i18n] = useI18n({ id: 'about', translations: { en }, fallback: en });
const [i18n] = useI18n();
return (
<Dialog
title={i18n.translate(AboutStringId.Title, { appName })}
title={i18n.translate(I18nId.Title, { appName })}
isOpen={isOpen}
onClose={onClose}
>
@@ -43,7 +42,7 @@ const AboutDialog: React.VoidFunctionComponent<AboutDialogProps> = ({
<img src="favicon.ico" />
</div>
<p>
<strong>{i18n.translate(AboutStringId.Description)}</strong>
<strong>{i18n.translate(I18nId.Description)}</strong>
</p>
<p>{`v${firmwareVersion} (${appName} v${appVersion})`}</p>
<p>{pybricksCopyright}</p>
@@ -54,14 +53,14 @@ const AboutDialog: React.VoidFunctionComponent<AboutDialogProps> = ({
</p>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={() => setIsLicenseDialogOpen(true)}>
{i18n.translate(AboutStringId.LicenseButtonLabel)}
{i18n.translate(I18nId.LicenseButtonLabel)}
</Button>
<AnchorButton href={changelogUrl} target="blank_">
{i18n.translate(AboutStringId.ChangelogButtonLabel)}
{i18n.translate(I18nId.ChangelogButtonLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton href={pybricksWebsiteUrl} target="blank_">
{i18n.translate(AboutStringId.WebsiteButtonLabel)}
{i18n.translate(I18nId.WebsiteButtonLabel)}
<ExternalLinkIcon />
</AnchorButton>
</div>
-9
View File
@@ -1,9 +0,0 @@
{
"about": {
"title": "About {appName}",
"description": "MicroPython for LEGO® Powered Up smart hubs.",
"licenseButton": { "label": "Software Licenses" },
"changelogButton": { "label": "Changelog" },
"websiteButton": { "label": "Pybricks Website" }
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
import { lookup } from '../../test';
import { AboutStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for AboutStringId', () => {
test.each(Object.values(AboutStringId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+6 -6
View File
@@ -3,10 +3,10 @@
// About dialog translation keys.
export enum AboutStringId {
Title = 'about.title',
Description = 'about.description',
LicenseButtonLabel = 'about.licenseButton.label',
ChangelogButtonLabel = 'about.changelogButton.label',
WebsiteButtonLabel = 'about.websiteButton.label',
export enum I18nId {
Title = 'title',
Description = 'description',
LicenseButtonLabel = 'licenseButton.label',
ChangelogButtonLabel = 'changelogButton.label',
WebsiteButtonLabel = 'websiteButton.label',
}
+7
View File
@@ -0,0 +1,7 @@
{
"title": "About {appName}",
"description": "MicroPython for LEGO® Powered Up smart hubs.",
"licenseButton": { "label": "Software Licenses" },
"changelogButton": { "label": "Changelog" },
"websiteButton": { "label": "Pybricks Website" }
}
+14 -16
View File
@@ -3,7 +3,7 @@
import { Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
import { ContextMenu2, ResizeSensor2 } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import { I18n, useI18n } from '@shopify/react-i18n';
import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eighties.json';
import xcodeTheme from 'monaco-themes/themes/Xcode_default.json';
import React, { useState } from 'react';
@@ -15,8 +15,7 @@ import { fileStorageWriteFile } from '../fileStorage/actions';
import { compile } from '../mpy/actions';
import { settingsToggleShowDocs } from '../settings/actions';
import { isMacOS } from '../utils/os';
import { EditorStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import * as pybricksMicroPython from './pybricksMicroPython';
import { UntitledHintContribution } from './untitledHint';
@@ -62,13 +61,12 @@ monaco.editor.defineTheme(
const xcodeId = 'xcode';
monaco.editor.defineTheme(xcodeId, xcodeTheme as monaco.editor.IStandaloneThemeData);
type EditorContextMenuProps = { editor: EditorType };
type EditorContextMenuProps = { editor: EditorType; i18n: I18n };
const EditorContextMenu: React.VoidFunctionComponent<EditorContextMenuProps> = ({
editor,
i18n,
}) => {
const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en });
const hasEditor = editor !== null;
const selection = editor?.getSelection();
@@ -85,7 +83,7 @@ const EditorContextMenu: React.VoidFunctionComponent<EditorContextMenuProps> = (
editor?.focus();
editor?.trigger(null, 'editor.action.clipboardCopyAction', null);
}}
text={i18n.translate(EditorStringId.Copy)}
text={i18n.translate(I18nId.Copy)}
icon="duplicate"
label={isMacOS() ? 'Cmd-C' : 'Ctrl-C'}
disabled={!hasSelection}
@@ -95,7 +93,7 @@ const EditorContextMenu: React.VoidFunctionComponent<EditorContextMenuProps> = (
editor?.focus();
editor?.trigger(null, 'editor.action.clipboardPasteAction', null);
}}
text={i18n.translate(EditorStringId.Paste)}
text={i18n.translate(I18nId.Paste)}
icon="clipboard"
label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'}
disabled={!hasEditor}
@@ -105,7 +103,7 @@ const EditorContextMenu: React.VoidFunctionComponent<EditorContextMenuProps> = (
editor?.focus();
editor?.trigger(null, 'editor.action.selectAll', null);
}}
text={i18n.translate(EditorStringId.SelectAll)}
text={i18n.translate(I18nId.SelectAll)}
icon="blank"
label={isMacOS() ? 'Cmd-A' : 'Ctrl-A'}
disabled={!hasEditor}
@@ -116,7 +114,7 @@ const EditorContextMenu: React.VoidFunctionComponent<EditorContextMenuProps> = (
editor?.focus();
editor?.trigger(null, 'undo', null);
}}
text={i18n.translate(EditorStringId.Undo)}
text={i18n.translate(I18nId.Undo)}
icon="undo"
label={isMacOS() ? 'Cmd-Z' : 'Ctrl-Z'}
disabled={!canUndo}
@@ -126,7 +124,7 @@ const EditorContextMenu: React.VoidFunctionComponent<EditorContextMenuProps> = (
editor?.focus();
editor?.trigger(null, 'redo', null);
}}
text={i18n.translate(EditorStringId.Redo)}
text={i18n.translate(I18nId.Redo)}
icon="redo"
label={isMacOS() ? 'Cmd-Shift-Z' : 'Ctrl-Shift-Z'}
disabled={!canRedo}
@@ -143,7 +141,7 @@ const Editor: React.VoidFunctionComponent<EditorProps> = ({ onEditorChanged }) =
const [editor, setEditor] = useState<EditorType>(null);
const { isDarkMode } = useTernaryDarkMode();
const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en });
const [i18n] = useI18n();
return (
<ResizeSensor2 onResize={() => editor?.layout()}>
@@ -152,7 +150,7 @@ const Editor: React.VoidFunctionComponent<EditorProps> = ({ onEditorChanged }) =
// NB: we have to create a new context menu each time it is
// shown in order to get some state, like canUndo and canRedo
// that don't have events to monitor changes.
content={() => <EditorContextMenu editor={editor} />}
content={() => <EditorContextMenu editor={editor} i18n={i18n} />}
popoverProps={{ onClosed: () => editor?.focus() }}
>
<MonacoEditor
@@ -172,13 +170,13 @@ const Editor: React.VoidFunctionComponent<EditorProps> = ({ onEditorChanged }) =
subscriptions.push(
new UntitledHintContribution(
editor,
i18n.translate(EditorStringId.Placeholder),
i18n.translate(I18nId.Placeholder),
),
);
subscriptions.push(
editor.addAction({
id: 'pybricks.action.toggleDocs',
label: i18n.translate(EditorStringId.ToggleDocs),
label: i18n.translate(I18nId.ToggleDocs),
run: () => {
// we have to use dispatch here instead of
// toggleIsSettingShowDocsEnabled since this
@@ -193,7 +191,7 @@ const Editor: React.VoidFunctionComponent<EditorProps> = ({ onEditorChanged }) =
subscriptions.push(
editor.addAction({
id: 'pybricks.action.check',
label: i18n.translate(EditorStringId.Check),
label: i18n.translate(I18nId.Check),
// REVISIT: the compile options here might need to be changed - hopefully there is
// one setting that works for all hub types for cases where we aren't connected.
run: (e) => {
-12
View File
@@ -1,12 +0,0 @@
{
"editor": {
"placeholder": "Write your program here...",
"check": "Check Syntax",
"toggleDocs": "Toggle Documentation",
"copy": "Copy",
"paste": "Paste",
"selectAll": "Select All",
"undo": "Undo",
"redo": "Redo"
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../test';
import { EditorStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId as I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for EditorStringId', () => {
test.each(Object.values(EditorStringId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+10 -10
View File
@@ -1,15 +1,15 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
//
// Editor translation keys.
export enum EditorStringId {
Placeholder = 'editor.placeholder',
Check = 'editor.check',
ToggleDocs = 'editor.toggleDocs',
Copy = 'editor.copy',
Paste = 'editor.paste',
SelectAll = 'editor.selectAll',
Undo = 'editor.undo',
Redo = 'editor.redo',
export enum I18nId {
Placeholder = 'placeholder',
Check = 'check',
ToggleDocs = 'toggleDocs',
Copy = 'copy',
Paste = 'paste',
SelectAll = 'selectAll',
Undo = 'undo',
Redo = 'redo',
}
+10
View File
@@ -0,0 +1,10 @@
{
"placeholder": "Write your program here...",
"check": "Check Syntax",
"toggleDocs": "Toggle Documentation",
"copy": "Copy",
"paste": "Paste",
"selectAll": "Select All",
"undo": "Undo",
"redo": "Redo"
}
+57 -38
View File
@@ -11,7 +11,7 @@ import {
IconName,
useHotkeys,
} from '@blueprintjs/core';
import { useI18n } from '@shopify/react-i18n';
import { I18n, useI18n } from '@shopify/react-i18n';
import React, { useCallback, useMemo, useState } from 'react';
import {
ControlledTreeEnvironment,
@@ -32,10 +32,9 @@ import { useSelector } from '../reducers';
import { isMacOS } from '../utils/os';
import { preventBrowserNativeContextMenu } from '../utils/react';
import { TreeItemContext, TreeItemData, renderers } from '../utils/tree-renderer';
import NewFileWizard from './NewFileWizard';
import { explorerDeleteFile, explorerImportFiles, explorerRenameFile } from './actions';
import { ExplorerStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import NewFileWizard from './newFileWizard/NewFileWizard';
import RenameFileDialog from './renameFileDialog/RenameFileDialog';
import './explorer.scss';
@@ -43,13 +42,15 @@ type ActionButtonProps = {
/** The icon to use for the button. */
icon: IconName;
/** The tooltip translation ID for the tooltip text. */
toolTipId: ExplorerStringId;
toolTipId: I18nId;
/** Replacements if required by `toolTipId` */
toolTipReplacements?: { [key: string]: string };
/** If provided, controls button disabled state. */
disabled?: boolean;
/** If false, prevent focus. Default is true. */
focusable?: boolean;
/** Translation context. */
i18n: I18n;
/** Callback for button click event. */
onClick: () => void;
};
@@ -60,10 +61,9 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
toolTipReplacements,
disabled,
focusable,
i18n,
onClick,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
return (
<Button
icon={icon}
@@ -83,10 +83,13 @@ type FileTreeItem = TreeItem<FileTreeItemData>;
type ActionButtonGroupProps = {
/** The name of the file (displayed to user) */
item: TreeItem;
/** Translation context. */
i18n: I18n;
};
const FileActionButtonGroup: React.VoidFunctionComponent<ActionButtonGroupProps> = ({
item,
i18n,
}) => {
const dispatch = useDispatch();
const environment = useTreeEnvironment();
@@ -101,9 +104,10 @@ const FileActionButtonGroup: React.VoidFunctionComponent<ActionButtonGroupProps>
>
<ActionButton
icon="edit"
toolTipId={ExplorerStringId.TreeItemRenameTooltip}
toolTipId={I18nId.TreeItemRenameTooltip}
toolTipReplacements={{ fileName }}
focusable={false}
i18n={i18n}
onClick={() => dispatch(explorerRenameFile(fileName))}
/>
<ActionButton
@@ -113,23 +117,30 @@ const FileActionButtonGroup: React.VoidFunctionComponent<ActionButtonGroupProps>
// archive icon which is also used to indicate an export/
// download operation
icon="import"
toolTipId={ExplorerStringId.TreeItemExportTooltip}
toolTipId={I18nId.TreeItemExportTooltip}
toolTipReplacements={{ fileName: fileName }}
focusable={false}
i18n={i18n}
onClick={() => dispatch(fileStorageExportFile(fileName))}
/>
<ActionButton
icon="trash"
toolTipId={ExplorerStringId.TreeItemDeleteTooltip}
toolTipId={I18nId.TreeItemDeleteTooltip}
toolTipReplacements={{ fileName: fileName }}
focusable={false}
i18n={i18n}
onClick={() => dispatch(explorerDeleteFile(fileName))}
/>
</ButtonGroup>
);
};
const Header: React.VFC = () => {
type HeaderProps = {
/** Translation context. */
i18n: I18n;
};
const Header: React.VoidFunctionComponent<HeaderProps> = ({ i18n }) => {
const [isNewFileWizardOpen, setIsNewFileWizardOpen] = useState(false);
const dispatch = useDispatch();
const fileNames = useSelector((s) => s.fileStorage.fileNames);
@@ -139,8 +150,9 @@ const Header: React.VFC = () => {
<ButtonGroup minimal={true}>
<ActionButton
icon="archive"
toolTipId={ExplorerStringId.HeaderExportAllTooltip}
toolTipId={I18nId.HeaderExportAllTooltip}
disabled={fileNames.length === 0}
i18n={i18n}
onClick={() => dispatch(fileStorageArchiveAllFiles())}
/>
<ActionButton
@@ -148,12 +160,14 @@ const Header: React.VFC = () => {
// what we want here since import is analogous to upload
// even though this is the "import" action
icon="export"
toolTipId={ExplorerStringId.HeaderImportTooltip}
toolTipId={I18nId.HeaderImportTooltip}
i18n={i18n}
onClick={() => dispatch(explorerImportFiles())}
/>
<ActionButton
icon="plus"
toolTipId={ExplorerStringId.HeaderAddNewTooltip}
toolTipId={I18nId.HeaderAddNewTooltip}
i18n={i18n}
onClick={() => setIsNewFileWizardOpen(true)}
/>
<NewFileWizard
@@ -167,43 +181,37 @@ const Header: React.VFC = () => {
/**
* Accessibility live descriptors.
* @param i18n Translation context.
*/
function useLiveDescriptors(): LiveDescriptors {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
function useLiveDescriptors(i18n: I18n): LiveDescriptors {
return useMemo(
() => ({
introduction: `
<p>${i18n.translate(
ExplorerStringId.TreeLiveDescriptorIntroAccessibilityGuide,
{ treeLabel: '{treeLabel}' },
)}</p>
<p>${i18n.translate(
ExplorerStringId.TreeLiveDescriptorIntroNavigation,
)}</p>
<p>${i18n.translate(I18nId.TreeLiveDescriptorIntroAccessibilityGuide, {
treeLabel: '{treeLabel}',
})}</p>
<p>${i18n.translate(I18nId.TreeLiveDescriptorIntroNavigation)}</p>
<ul>
<li>${i18n.translate(
ExplorerStringId.TreeLiveDescriptorIntroKeybindingsPrimaryAction,
I18nId.TreeLiveDescriptorIntroKeybindingsPrimaryAction,
{ key: '{keybinding:primaryAction}' },
)}</li>
<li>${i18n.translate(
ExplorerStringId.TreeLiveDescriptorIntroKeybindingsRename,
I18nId.TreeLiveDescriptorIntroKeybindingsRename,
{ key: 'f2' },
)}</li>
<li>${i18n.translate(
ExplorerStringId.TreeLiveDescriptorIntroKeybindingsExport,
I18nId.TreeLiveDescriptorIntroKeybindingsExport,
{ key: `${isMacOS() ? 'cmd' : 'ctrl'}+e` },
)}</li>
<li>${i18n.translate(
ExplorerStringId.TreeLiveDescriptorIntroKeybindingsDelete,
I18nId.TreeLiveDescriptorIntroKeybindingsDelete,
{ key: 'delete' },
)}</li>
</ul>
`,
renamingItem: 'not used',
searching: `<p>${i18n.translate(
ExplorerStringId.TreeLiveDescriptorSearching,
)}</p>`,
searching: `<p>${i18n.translate(I18nId.TreeLiveDescriptorSearching)}</p>`,
programmaticallyDragging: 'not used',
programmaticallyDraggingTarget: 'not used',
}),
@@ -280,12 +288,16 @@ const renderTreeContainer: typeof renderers.renderTreeContainer = (props) => {
return <div onKeyDown={handleKeyDown}>{renderers.renderTreeContainer(props)}</div>;
};
const FileTree: React.VFC = () => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
type FileTreeProps = {
/** Translation context. */
i18n: I18n;
};
const FileTree: React.VoidFunctionComponent<FileTreeProps> = ({ i18n }) => {
const [focusedItem, setFocusedItem] = useState<TreeItemIndex>();
const fileNames = useSelector((s) => s.fileStorage.fileNames);
const debouncedFileNames = useDebounce(fileNames);
const liveDescriptors = useLiveDescriptors();
const liveDescriptors = useLiveDescriptors(i18n);
const rootItemIndex = '/';
@@ -302,7 +314,12 @@ const FileTree: React.VFC = () => {
icon: 'document',
secondaryLabel: (
<TreeItemContext.Consumer>
{(item) => <FileActionButtonGroup item={item} />}
{(item) => (
<FileActionButtonGroup
item={item}
i18n={i18n}
/>
)}
</TreeItemContext.Consumer>
),
},
@@ -346,7 +363,7 @@ const FileTree: React.VFC = () => {
<Tree
treeId={treeId}
rootItem={rootItemIndex}
treeLabel={i18n.translate(ExplorerStringId.TreeLabel)}
treeLabel={i18n.translate(I18nId.TreeLabel)}
/>
</div>
</ControlledTreeEnvironment>
@@ -354,11 +371,13 @@ const FileTree: React.VFC = () => {
};
const Explorer: React.VFC = () => {
const [i18n] = useI18n();
return (
<div className="h-100" onContextMenu={preventBrowserNativeContextMenu}>
<Header />
<Header i18n={i18n} />
<Divider />
<FileTree />
<FileTree i18n={i18n} />
<RenameFileDialog />
</div>
);
@@ -0,0 +1,12 @@
// 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();
});
});
@@ -2,15 +2,16 @@
// Copyright (c) 2022 The Pybricks Authors
import { Classes, FormGroup, InputGroup, Intent, Tag } from '@blueprintjs/core';
import { useI18n } from '@shopify/react-i18n';
import { I18n, useI18n } from '@shopify/react-i18n';
import React from 'react';
import { FileNameValidationResult } from '../pybricksMicropython/lib';
import { NewFileWizardStringId } from './i18n';
import en from './i18n.en.json';
import { FileNameValidationResult } from '../../pybricksMicropython/lib';
import { I18nId } from './i18n';
type FileNameHelpTextProps = {
/** The result of the file name validation. */
validation: FileNameValidationResult;
/** Translation context. */
i18n: I18n;
};
/**
@@ -18,60 +19,39 @@ type FileNameHelpTextProps = {
*/
const FileNameHelpText: React.VoidFunctionComponent<FileNameHelpTextProps> = ({
validation,
i18n,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
switch (validation) {
case FileNameValidationResult.IsOk:
return <>{i18n.translate(NewFileWizardStringId.FileNameHelpTextIsOk)}</>;
return <>{i18n.translate(I18nId.HelpTextIsOk)}</>;
case FileNameValidationResult.IsEmpty:
return <>{i18n.translate(NewFileWizardStringId.FileNameHelpTextIsEmpty)}</>;
return <>{i18n.translate(I18nId.HelpTextIsEmpty)}</>;
case FileNameValidationResult.HasSpaces:
return (
<>{i18n.translate(NewFileWizardStringId.FileNameHelpTextHasSpaces)}</>
);
return <>{i18n.translate(I18nId.HelpTextHasSpaces)}</>;
case FileNameValidationResult.HasFileExtension:
return (
<>
{i18n.translate(
NewFileWizardStringId.FileNameHelpTextHasFileExtension,
)}
</>
);
return <>{i18n.translate(I18nId.HelpTextHasFileExtension)}</>;
case FileNameValidationResult.HasInvalidFirstCharacter:
return (
<>
{i18n.translate(
NewFileWizardStringId.FileNameHelpTextHasInvalidFirstCharacter,
{
letters: <code className={Classes.CODE}>az</code>,
underscore: <code className={Classes.CODE}>_</code>,
},
)}
{i18n.translate(I18nId.HelpTextHasInvalidFirstCharacter, {
letters: <code className={Classes.CODE}>az</code>,
underscore: <code className={Classes.CODE}>_</code>,
})}
</>
);
case FileNameValidationResult.HasInvalidCharacters:
return (
<>
{i18n.translate(
NewFileWizardStringId.FileNameHelpTextHasInvalidCharacters,
{
letters: <code className={Classes.CODE}>az</code>,
numbers: <code className={Classes.CODE}>09</code>,
dash: <code className={Classes.CODE}>-</code>,
underscore: <code className={Classes.CODE}>_</code>,
},
)}
{i18n.translate(I18nId.HelpTextHasInvalidCharacters, {
letters: <code className={Classes.CODE}>az</code>,
numbers: <code className={Classes.CODE}>09</code>,
dash: <code className={Classes.CODE}>-</code>,
underscore: <code className={Classes.CODE}>_</code>,
})}
</>
);
case FileNameValidationResult.AlreadyExists:
return (
<>
{i18n.translate(
NewFileWizardStringId.FileNameHelpTextAlreadyExists,
)}
</>
);
return <>{i18n.translate(I18nId.HelpTextAlreadyExists)}</>;
}
};
@@ -98,7 +78,7 @@ const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = (
inputRef,
onChange,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
const [i18n] = useI18n();
const fileNameIntent =
validationResult === FileNameValidationResult.IsOk
@@ -107,9 +87,9 @@ const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = (
return (
<FormGroup
label={i18n.translate(NewFileWizardStringId.FileNameLabel)}
label={i18n.translate(I18nId.Label)}
intent={fileNameIntent}
subLabel={<FileNameHelpText validation={validationResult} />}
subLabel={<FileNameHelpText validation={validationResult} i18n={i18n} />}
>
<InputGroup
aria-label="File name"
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
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',
}
@@ -0,0 +1,12 @@
{
"label": "File name",
"helpText": {
"isOk": "OK!",
"isEmpty": "File name cannot be empty.",
"hasSpaces": "File name cannot have spaces.",
"hasFileExtension": "The file extension will be added automatically.",
"hasInvalidFirstCharacter": "File name must start with a letter ({letters}) or underscore ({underscore}).",
"hasInvalidCharacters": "File name can only contain letters ({letters}), numbers ({numbers}), dashes ({dash}) and underscores ({underscore}).",
"alreadyExists": "A file with this name already exists."
}
}
-51
View File
@@ -1,51 +0,0 @@
{
"explorer": {
"header": {
"exportAllTooltip": "Backup all files",
"importTooltip": "Import a file",
"addNewTooltip": "Create a new file"
},
"tree": {
"label": "File Explorer",
"liveDescriptor": {
"intro": {
"accessibilityGuide": "Accessibility guide for tree {treeLabel}.",
"navigation": "Navigate the tree with the arrow keys. Start typing the name of a file to search for a file. Additional keybindings are available:",
"keybindings": {
"primaryAction": "{key} to open the file in the code editor",
"rename": "{key} to start renaming the focused file",
"export": "{key} to export the focused file",
"delete": "{key} to delete the focused file"
}
},
"searching": "Searching."
}
},
"treeItem": {
"deleteTooltip": "Delete {fileName}",
"exportTooltip": "Export {fileName}",
"renameTooltip": "Rename {fileName}"
}
},
"newFileWizard": {
"title": "Create a new file",
"fileName": {
"label": "File name",
"helpText": {
"isOk": "OK!",
"isEmpty": "File name cannot be empty.",
"hasSpaces": "File name cannot have spaces.",
"hasFileExtension": "The file extension will be added automatically.",
"hasInvalidFirstCharacter": "File name must start with a letter ({letters}) or underscore ({underscore}).",
"hasInvalidCharacters": "File name can only contain letters ({letters}), numbers ({numbers}), dashes ({dash}) and underscores ({underscore}).",
"alreadyExists": "A file with this name already exists."
}
},
"smartHub": {
"label": "Smart hub"
},
"action": {
"create": "Create"
}
}
}
+4 -10
View File
@@ -2,17 +2,11 @@
// Copyright (c) 2022 The Pybricks Authors
import { lookup } from '../../test';
import { ExplorerStringId, NewFileWizardStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for ExplorerStringId', () => {
test.each(Object.values(ExplorerStringId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
describe('Ensure .json file has matches for NewFileWizardStringId', () => {
test.each(Object.values(NewFileWizardStringId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+15 -29
View File
@@ -3,33 +3,19 @@
//
// Explorer translation keys.
export enum ExplorerStringId {
HeaderExportAllTooltip = 'explorer.header.exportAllTooltip',
HeaderImportTooltip = 'explorer.header.importTooltip',
HeaderAddNewTooltip = 'explorer.header.addNewTooltip',
TreeLabel = 'explorer.tree.label',
TreeLiveDescriptorIntroAccessibilityGuide = 'explorer.tree.liveDescriptor.intro.accessibilityGuide',
TreeLiveDescriptorIntroNavigation = 'explorer.tree.liveDescriptor.intro.navigation',
TreeLiveDescriptorIntroKeybindingsPrimaryAction = 'explorer.tree.liveDescriptor.intro.keybindings.primaryAction',
TreeLiveDescriptorIntroKeybindingsRename = 'explorer.tree.liveDescriptor.intro.keybindings.rename',
TreeLiveDescriptorIntroKeybindingsExport = 'explorer.tree.liveDescriptor.intro.keybindings.export',
TreeLiveDescriptorIntroKeybindingsDelete = 'explorer.tree.liveDescriptor.intro.keybindings.delete',
TreeLiveDescriptorSearching = 'explorer.tree.liveDescriptor.searching',
TreeItemDeleteTooltip = 'explorer.treeItem.deleteTooltip',
TreeItemExportTooltip = 'explorer.treeItem.exportTooltip',
TreeItemRenameTooltip = 'explorer.treeItem.renameTooltip',
}
export enum NewFileWizardStringId {
Title = 'newFileWizard.title',
FileNameLabel = 'newFileWizard.fileName.label',
FileNameHelpTextIsOk = 'newFileWizard.fileName.helpText.isOk',
FileNameHelpTextIsEmpty = 'newFileWizard.fileName.helpText.isEmpty',
FileNameHelpTextHasSpaces = 'newFileWizard.fileName.helpText.hasSpaces',
FileNameHelpTextHasFileExtension = 'newFileWizard.fileName.helpText.hasFileExtension',
FileNameHelpTextHasInvalidFirstCharacter = 'newFileWizard.fileName.helpText.hasInvalidFirstCharacter',
FileNameHelpTextHasInvalidCharacters = 'newFileWizard.fileName.helpText.hasInvalidCharacters',
FileNameHelpTextAlreadyExists = 'newFileWizard.fileName.helpText.alreadyExists',
SmartHubLabel = 'newFileWizard.smartHub.label',
ActionCreate = 'newFileWizard.action.create',
export enum I18nId {
HeaderExportAllTooltip = 'header.exportAllTooltip',
HeaderImportTooltip = 'header.importTooltip',
HeaderAddNewTooltip = 'header.addNewTooltip',
TreeLabel = 'tree.label',
TreeLiveDescriptorIntroAccessibilityGuide = 'tree.liveDescriptor.intro.accessibilityGuide',
TreeLiveDescriptorIntroNavigation = 'tree.liveDescriptor.intro.navigation',
TreeLiveDescriptorIntroKeybindingsPrimaryAction = 'tree.liveDescriptor.intro.keybindings.primaryAction',
TreeLiveDescriptorIntroKeybindingsRename = 'tree.liveDescriptor.intro.keybindings.rename',
TreeLiveDescriptorIntroKeybindingsExport = 'tree.liveDescriptor.intro.keybindings.export',
TreeLiveDescriptorIntroKeybindingsDelete = 'tree.liveDescriptor.intro.keybindings.delete',
TreeLiveDescriptorSearching = 'tree.liveDescriptor.searching',
TreeItemDeleteTooltip = 'treeItem.deleteTooltip',
TreeItemExportTooltip = 'treeItem.exportTooltip',
TreeItemRenameTooltip = 'treeItem.renameTooltip',
}
+12
View File
@@ -0,0 +1,12 @@
// 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();
});
});
@@ -4,7 +4,7 @@
import { waitFor } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { testRender } from '../../test';
import { testRender } from '../../../test';
import NewFileWizard from './NewFileWizard';
describe('create button', () => {
@@ -16,12 +16,11 @@ import {
FileNameValidationResult,
pythonFileExtension,
validateFileName,
} from '../pybricksMicropython/lib';
import { useSelector } from '../reducers';
import FileNameFormGroup from './FileNameFormGroup';
import { Hub, explorerCreateNewFile } from './actions';
import { NewFileWizardStringId } from './i18n';
import en from './i18n.en.json';
} from '../../pybricksMicropython/lib';
import { useSelector } from '../../reducers';
import FileNameFormGroup from '../fileNameFormGroup/FileNameFormGroup';
import { Hub, explorerCreateNewFile } from './../actions';
import { I18nId } from './i18n';
// This should be set to the most commonly used hub.
const defaultHub = Hub.Technic;
@@ -37,7 +36,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
isOpen,
onClose,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
const [i18n] = useI18n();
const dispatch = useDispatch();
const [fileName, setFileName] = useState('');
@@ -54,7 +53,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
return (
<Dialog
icon="plus"
title={i18n.translate(NewFileWizardStringId.Title)}
title={i18n.translate(I18nId.Title)}
isOpen={isOpen}
onOpening={() => setFileName('')}
onOpened={() => fileNameInputRef.current?.focus()}
@@ -68,7 +67,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
inputRef={fileNameInputRef}
onChange={setFileName}
/>
<FormGroup label={i18n.translate(NewFileWizardStringId.SmartHubLabel)}>
<FormGroup label={i18n.translate(I18nId.SmartHubLabel)}>
<RadioGroup
selectedValue={hubType}
onChange={(e) => setHubType(e.currentTarget.value as Hub)}
@@ -99,7 +98,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
);
}}
>
{i18n.translate(NewFileWizardStringId.ActionCreate)}
{i18n.translate(I18nId.ActionCreate)}
</Button>
</div>
</div>
+8
View File
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
export enum I18nId {
Title = 'title',
SmartHubLabel = 'smartHub.label',
ActionCreate = 'action.create',
}
@@ -0,0 +1,9 @@
{
"title": "Create a new file",
"smartHub": {
"label": "Smart hub"
},
"action": {
"create": "Create"
}
}
+4 -4
View File
@@ -2,11 +2,11 @@
// Copyright (c) 2022 The Pybricks Authors
import { lookup } from '../../../test';
import { RenameFileDialogStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for RenameFileStringId', () => {
test.each(Object.values(RenameFileDialogStringId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
@@ -10,20 +10,15 @@ import {
validateFileName,
} from '../../pybricksMicropython/lib';
import { useSelector } from '../../reducers';
import FileNameFormGroup from '../FileNameFormGroup';
import FileNameFormGroup from '../fileNameFormGroup/FileNameFormGroup';
import { renameFileDialogDidAccept, renameFileDialogDidCancel } from './actions';
import { RenameFileDialogStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
const RenameFileDialog: React.VFC = () => {
const dispatch = useDispatch();
const isOpen = useSelector((s) => s.explorer.renameFileDialog.isOpen);
const oldName = useSelector((s) => s.explorer.renameFileDialog.fileName);
const [i18n] = useI18n({
id: 'renameFileDialog',
translations: { en },
fallback: en,
});
const [i18n] = useI18n();
const [baseName, extension] = oldName.split(/(\.\w+)$/);
@@ -47,7 +42,7 @@ const RenameFileDialog: React.VFC = () => {
return (
<Dialog
title={i18n.translate(RenameFileDialogStringId.Title, {
title={i18n.translate(I18nId.Title, {
fileName: oldName,
})}
isOpen={isOpen}
@@ -75,7 +70,7 @@ const RenameFileDialog: React.VFC = () => {
disabled={result !== FileNameValidationResult.IsOk}
type="submit"
>
{i18n.translate(RenameFileDialogStringId.ActionRename)}
{i18n.translate(I18nId.ActionRename)}
</Button>
</div>
</div>
@@ -1,8 +0,0 @@
{
"renameFileDialog": {
"title": "Rename '{fileName}'",
"action": {
"rename": "Rename"
}
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
export enum RenameFileDialogStringId {
Title = 'renameFileDialog.title',
ActionRename = 'renameFileDialog.action.rename',
export enum I18nId {
Title = 'title',
ActionRename = 'action.rename',
}
@@ -0,0 +1,6 @@
{
"title": "Rename '{fileName}'",
"action": {
"rename": "Rename"
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"header": {
"exportAllTooltip": "Backup all files",
"importTooltip": "Import a file",
"addNewTooltip": "Create a new file"
},
"tree": {
"label": "File Explorer",
"liveDescriptor": {
"intro": {
"accessibilityGuide": "Accessibility guide for tree {treeLabel}.",
"navigation": "Navigate the tree with the arrow keys. Start typing the name of a file to search for a file. Additional keybindings are available:",
"keybindings": {
"primaryAction": "{key} to open the file in the code editor",
"rename": "{key} to start renaming the focused file",
"export": "{key} to export the focused file",
"delete": "{key} to delete the focused file"
}
},
"searching": "Searching."
}
},
"treeItem": {
"deleteTooltip": "Delete {fileName}",
"exportTooltip": "Export {fileName}",
"renameTooltip": "Rename {fileName}"
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ import * as notificationActions from '../notifications/actions';
import { useSelector } from '../reducers';
import { useSettingFlashCurrentProgram, useSettingHubName } from '../settings/hooks';
import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton';
import { TooltipId } from '../toolbar/i18n';
import { I18nId } from '../toolbar/i18n';
import { flashFirmware } from './actions';
import firmwareIcon from './firmware.svg';
@@ -30,7 +30,7 @@ const FlashButton: React.VoidFunctionComponent<FlashButtonProps> = ({ label }) =
label={label}
fileExtension=".zip"
icon={firmwareIcon}
tooltip={flashing ? TooltipId.FlashProgress : TooltipId.Flash}
tooltip={flashing ? I18nId.FlashProgress : I18nId.Flash}
enabled={
bootloaderConnection === BootloaderConnectionState.Disconnected &&
bleConnection === BleConnectionState.Disconnected
+2 -4
View File
@@ -8,7 +8,7 @@ import { BleConnectionState } from '../ble/reducers';
import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import { I18nId } from '../toolbar/i18n';
import btConnectedIcon from './bt-connected.svg';
import btDisconnectedIcon from './bt-disconnected.svg';
@@ -30,9 +30,7 @@ const BluetoothButton: React.VoidFunctionComponent<BluetoothButtonProps> = ({
<ActionButton
label={label}
tooltip={
isDisconnected
? TooltipId.BluetoothConnect
: TooltipId.BluetoothDisconnect
isDisconnected ? I18nId.BluetoothConnect : I18nId.BluetoothDisconnect
}
icon={isDisconnected ? btDisconnectedIcon : btConnectedIcon}
enabled={isDisconnected || bleConnection === BleConnectionState.Connected}
+2 -2
View File
@@ -5,7 +5,7 @@ import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import { I18nId } from '../toolbar/i18n';
import { repl } from './actions';
import { HubRuntimeState } from './reducers';
import replIcon from './repl.svg';
@@ -24,7 +24,7 @@ const ReplButton: React.VoidFunctionComponent<ReplButtonProps> = ({
<ActionButton
label={label}
keyboardShortcut={keyboardShortcut}
tooltip={TooltipId.Repl}
tooltip={I18nId.Repl}
icon={replIcon}
enabled={enabled}
onAction={action}
+3 -3
View File
@@ -5,7 +5,7 @@ import React from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import { I18nId } from '../toolbar/i18n';
import { downloadAndRun } from './actions';
import { HubRuntimeState } from './reducers';
import runIcon from './run.svg';
@@ -26,8 +26,8 @@ const RunButton: React.VoidFunctionComponent<RunButtonProps> = ({
<ActionButton
label={label}
keyboardShortcut={keyboardShortcut}
tooltip={TooltipId.Run}
progressTooltip={TooltipId.RunProgress}
tooltip={I18nId.Run}
progressTooltip={I18nId.RunProgress}
icon={runIcon}
enabled={hasEditor && runtime === HubRuntimeState.Idle}
showProgress={runtime === HubRuntimeState.Loading}
+2 -2
View File
@@ -5,7 +5,7 @@ import React from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import { I18nId } from '../toolbar/i18n';
import { stop } from './actions';
import { HubRuntimeState } from './reducers';
import stopIcon from './stop.svg';
@@ -24,7 +24,7 @@ const StopButton: React.VoidFunctionComponent<StopButtonProps> = ({
<ActionButton
label={label}
keyboardShortcut={keyboardShortcut}
tooltip={TooltipId.Stop}
tooltip={I18nId.Stop}
icon={stopIcon}
enabled={runtime === HubRuntimeState.Running}
onAction={() => dispatch(stop())}
+23 -24
View File
@@ -11,7 +11,7 @@ import {
NonIdealState,
Spinner,
} from '@blueprintjs/core';
import { useI18n } from '@shopify/react-i18n';
import { I18n, useI18n } from '@shopify/react-i18n';
import React, { useCallback, useMemo, useState } from 'react';
import {
ControlledTreeEnvironment,
@@ -23,8 +23,7 @@ import {
import { useFetch } from 'usehooks-ts';
import { appName } from '../app/constants';
import { TreeItemData, renderers } from '../utils/tree-renderer';
import { LicenseStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import './license.scss';
@@ -39,13 +38,16 @@ interface LicenseInfo extends TreeItemData {
type LicenseList = ReadonlyArray<LicenseInfo>;
type LicenseListPanelProps = {
/** Called when item is clicked. */
onItemClick(info?: LicenseInfo): void;
/** Translation context. */
i18n: I18n;
};
const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = ({
onItemClick,
i18n,
}) => {
const [i18n] = useI18n({ id: 'license', translations: { en }, fallback: en });
const { data, error } = useFetch<LicenseList>('static/oss-licenses.json');
const [focusedItem, setFocusedItem] = useState<TreeItemIndex>();
const [activeItem, setActiveItem] = useState<TreeItemIndex>();
@@ -99,11 +101,7 @@ const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = ({
<div className="pb-license-list">
{contents === undefined ? (
<NonIdealState>
{error ? (
i18n.translate(LicenseStringId.ErrorFetchFailed)
) : (
<Spinner />
)}
{error ? i18n.translate(I18nId.ErrorFetchFailed) : <Spinner />}
</NonIdealState>
) : (
<ControlledTreeEnvironment<LicenseInfo>
@@ -126,25 +124,23 @@ const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = ({
type LicenseInfoPanelProps = {
/** The license info to show or undefined if no license info is selected. */
licenseInfo: LicenseInfo | undefined;
/** Translation context. */
i18n: I18n;
};
const LicenseInfoPanel = React.forwardRef<HTMLDivElement, LicenseInfoPanelProps>(
({ licenseInfo }, ref) => {
const [i18n] = useI18n({ id: 'license', translations: { en }, fallback: en });
({ licenseInfo, i18n }, ref) => {
return (
<div className="pb-license-info" ref={ref}>
{licenseInfo === undefined ? (
<NonIdealState>
{i18n.translate(LicenseStringId.SelectPackageHelp)}
{i18n.translate(I18nId.SelectPackageHelp)}
</NonIdealState>
) : (
<div>
<Card>
<p>
<strong>
{i18n.translate(LicenseStringId.PackageLabel)}
</strong>{' '}
<strong>{i18n.translate(I18nId.PackageLabel)}</strong>{' '}
{licenseInfo.name}{' '}
<span className={Classes.TEXT_MUTED}>
v{licenseInfo.version}
@@ -153,15 +149,13 @@ const LicenseInfoPanel = React.forwardRef<HTMLDivElement, LicenseInfoPanelProps>
{licenseInfo.author && (
<p>
<strong>
{i18n.translate(LicenseStringId.AuthorLabel)}
{i18n.translate(I18nId.AuthorLabel)}
</strong>{' '}
{licenseInfo.author}
</p>
)}
<p>
<strong>
{i18n.translate(LicenseStringId.LicenseLabel)}
</strong>{' '}
<strong>{i18n.translate(I18nId.LicenseLabel)}</strong>{' '}
{licenseInfo.license}
</p>
</Card>
@@ -189,18 +183,18 @@ const LicenseDialog: React.VoidFunctionComponent<LicenseDialogProps> = ({
const [licenseInfo, setLicenseInfo] = useState<LicenseInfo | undefined>(undefined);
const infoDiv = React.useRef<HTMLDivElement>(null);
const [i18n] = useI18n({ id: 'license', translations: { en }, fallback: en });
const [i18n] = useI18n();
return (
<Dialog
className="pb-license-dialog"
title={i18n.translate(LicenseStringId.Title)}
title={i18n.translate(I18nId.Title)}
isOpen={isOpen}
onClose={onClose}
>
<div className={Classes.DIALOG_BODY}>
<Callout className={Classes.INTENT_PRIMARY} icon="info-sign">
{i18n.translate(LicenseStringId.Description, {
{i18n.translate(I18nId.Description, {
name: appName,
})}
</Callout>
@@ -210,8 +204,13 @@ const LicenseDialog: React.VoidFunctionComponent<LicenseDialogProps> = ({
infoDiv.current?.scrollTo(0, 0);
setLicenseInfo(info);
}}
i18n={i18n}
/>
<LicenseInfoPanel
licenseInfo={licenseInfo}
ref={infoDiv}
i18n={i18n}
/>
<LicenseInfoPanel licenseInfo={licenseInfo} ref={infoDiv} />
</Callout>
</div>
</Dialog>
-15
View File
@@ -1,15 +0,0 @@
{
"license": {
"title": "Open Source Software Licenses",
"description": "{name} is built on open source software. By using {name} you are agreeing to the terms and conditions of all of the included software licenses.",
"packageLabel": "Package:",
"authorLabel": "Author:",
"licenseLabel": "License:",
"error": {
"fetchFailed": "Failed to load license data."
},
"help": {
"selectPackage": "Select a package to view the license."
}
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
import { lookup } from '../../test';
import { LicenseStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for LicenseStringId', () => {
test.each(Object.values(LicenseStringId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+8 -8
View File
@@ -3,12 +3,12 @@
// License dialog translation keys.
export enum LicenseStringId {
Title = 'license.title',
Description = 'license.description',
PackageLabel = 'license.packageLabel',
AuthorLabel = 'license.authorLabel',
LicenseLabel = 'license.licenseLabel',
ErrorFetchFailed = 'license.error.fetchFailed',
SelectPackageHelp = 'license.help.selectPackage',
export enum I18nId {
Title = 'title',
Description = 'description',
PackageLabel = 'packageLabel',
AuthorLabel = 'authorLabel',
LicenseLabel = 'licenseLabel',
ErrorFetchFailed = 'error.fetchFailed',
SelectPackageHelp = 'help.selectPackage',
}
+13
View File
@@ -0,0 +1,13 @@
{
"title": "Open Source Software Licenses",
"description": "{name} is built on open source software. By using {name} you are agreeing to the terms and conditions of all of the included software licenses.",
"packageLabel": "Package:",
"authorLabel": "Author:",
"licenseLabel": "License:",
"error": {
"fetchFailed": "Failed to load license data."
},
"help": {
"selectPackage": "Select a package to view the license."
}
}
+3 -8
View File
@@ -5,11 +5,10 @@
import { Replacements, useI18n } from '@shopify/react-i18n';
import React from 'react';
import { MessageId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
type NotificationActionProps = {
messageId: MessageId;
messageId: I18nId;
replacements?: Replacements;
};
@@ -17,11 +16,7 @@ const NotificationAction: React.VoidFunctionComponent<NotificationActionProps> =
messageId,
replacements,
}) => {
const [i18n] = useI18n({
id: 'notification',
translations: { en },
fallback: en,
});
const [i18n] = useI18n();
return <>{i18n.translate(messageId, replacements)}</>;
};
+3 -8
View File
@@ -5,11 +5,10 @@
import { Replacements, useI18n } from '@shopify/react-i18n';
import React from 'react';
import { MessageId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
type NotificationMessageProps = {
messageId: MessageId;
messageId: I18nId;
replacements?: Replacements;
};
@@ -17,11 +16,7 @@ const NotificationMessage: React.VoidFunctionComponent<NotificationMessageProps>
messageId,
replacements,
}) => {
const [i18n] = useI18n({
id: 'notification',
translations: { en },
fallback: en,
});
const [i18n] = useI18n();
let message = i18n.translate(messageId, replacements) as
| React.ReactElement
@@ -6,22 +6,17 @@
import { AnchorButton, Button, ButtonGroup, Intent } from '@blueprintjs/core';
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { MessageId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
type UnexpectedErrorNotificationProps = {
messageId: MessageId;
messageId: I18nId;
err: Error;
};
const UnexpectedErrorNotification: React.VoidFunctionComponent<
UnexpectedErrorNotificationProps
> = ({ messageId, err }) => {
const [i18n] = useI18n({
id: 'notification',
translations: { en },
fallback: en,
});
const [i18n] = useI18n();
return (
<>
@@ -37,7 +32,7 @@ const UnexpectedErrorNotification: React.VoidFunctionComponent<
)
}
>
{i18n.translate(MessageId.CopyErrorMessage)}
{i18n.translate(I18nId.CopyErrorMessage)}
</Button>
<AnchorButton
intent={Intent.DANGER}
@@ -47,7 +42,7 @@ const UnexpectedErrorNotification: React.VoidFunctionComponent<
)}+${encodeURIComponent(err.message)}`}
target="_blank"
>
{i18n.translate(MessageId.ReportBug)}
{i18n.translate(I18nId.ReportBug)}
</AnchorButton>
</ButtonGroup>
</div>
+5 -5
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../test';
import { MessageId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for MessageIds', () => {
test.each(Object.values(MessageId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+1 -1
View File
@@ -3,7 +3,7 @@
//
// Notification translation keys.
export enum MessageId {
export enum I18nId {
AppNoUpdateFound = 'app.noUpdateFound',
CopyErrorMessage = 'copyErrorMessage',
ReportBug = 'reportBug',
+6 -10
View File
@@ -44,7 +44,7 @@ import {
} from '../service-worker/actions';
import * as I18nToaster from './I18nToaster';
import { add } from './actions';
import { MessageId } from './i18n';
import { I18nId } from './i18n';
import notification from './sagas';
function createTestToasterSaga(): { toaster: IToaster; saga: AsyncSaga } {
@@ -155,7 +155,7 @@ test.each([
await saga.end();
});
test.each([[didCompile(new Uint8Array()), MessageId.MpyError]])(
test.each([[didCompile(new Uint8Array()), I18nId.MpyError]])(
'actions that should close a notification: %o',
async (action: AnyAction, key: string) => {
const { toaster, saga } = createTestToasterSaga();
@@ -176,7 +176,7 @@ describe('delete file saga', () => {
saga.put(explorerDeleteFile('test.file'));
toaster.dismiss(MessageId.ExplorerDeleteFileMessage);
toaster.dismiss(I18nId.ExplorerDeleteFileMessage);
await saga.end();
});
@@ -188,7 +188,7 @@ describe('delete file saga', () => {
const toast = toaster
.getToasts()
.find((t) => t.key === MessageId.ExplorerDeleteFileMessage);
.find((t) => t.key === I18nId.ExplorerDeleteFileMessage);
expect(toast).toBeDefined();
expect(toast?.action).toBeDefined();
@@ -211,17 +211,13 @@ describe('delete file saga', () => {
saga.put(explorerDeleteFile('test.file'));
expect(
toaster
.getToasts()
.find((t) => t.key === MessageId.ExplorerDeleteFileMessage),
toaster.getToasts().find((t) => t.key === I18nId.ExplorerDeleteFileMessage),
).toBeDefined();
saga.put(fileStorageDidRemoveItem('test.file'));
expect(
toaster
.getToasts()
.find((t) => t.key === MessageId.ExplorerDeleteFileMessage),
toaster.getToasts().find((t) => t.key === I18nId.ExplorerDeleteFileMessage),
).toBeUndefined();
await saga.end();
+42 -49
View File
@@ -40,7 +40,7 @@ import NotificationAction from './NotificationAction';
import NotificationMessage from './NotificationMessage';
import UnexpectedErrorNotification from './UnexpectedErrorNotification';
import { add as addNotification } from './actions';
import { MessageId } from './i18n';
import { I18nId } from './i18n';
type NotificationContext = {
toaster: IToaster;
@@ -102,7 +102,7 @@ function helpAction(helpUrl: string): ActionProps & LinkProps {
}
function dispatchAction(
messageId: MessageId,
messageId: I18nId,
onClick: (event: React.MouseEvent<HTMLElement>) => void,
icon?: IconName,
): ActionProps {
@@ -124,7 +124,7 @@ function dispatchAction(
*/
function* showSingleton(
level: Level,
messageId: MessageId,
messageId: I18nId,
replacements?: Replacements,
action?: ActionProps & LinkProps,
onDismiss?: (didTimeoutExpire: boolean) => void,
@@ -160,7 +160,7 @@ function* showSingleton(
}
/** Shows a special notification for unexpected errors. */
function* showUnexpectedError(messageId: MessageId, err: Error): Generator {
function* showUnexpectedError(messageId: I18nId, err: Error): Generator {
const { toaster } = yield* getContext<NotificationContext>('notification');
toaster.show({
intent: mapIntent(Level.Error),
@@ -175,28 +175,28 @@ function* showBleDeviceDidFailToConnectError(
): Generator {
switch (action.reason) {
case BleDeviceFailToConnectReasonType.NoGatt:
yield* showSingleton(Level.Error, MessageId.BleGattPermission);
yield* showSingleton(Level.Error, I18nId.BleGattPermission);
break;
case BleDeviceFailToConnectReasonType.NoPybricksService:
yield* showSingleton(Level.Error, MessageId.BleGattServiceNotFound, {
yield* showSingleton(Level.Error, I18nId.BleGattServiceNotFound, {
serviceName: 'Pybricks',
hubName: 'Pybricks Hub',
});
break;
case BleDeviceFailToConnectReasonType.NoDeviceInfoService:
yield* showSingleton(Level.Error, MessageId.BleGattServiceNotFound, {
yield* showSingleton(Level.Error, I18nId.BleGattServiceNotFound, {
serviceName: 'Device Information',
hubName: 'Pybricks Hub',
});
break;
case BleDeviceFailToConnectReasonType.NoBluetooth:
yield* showSingleton(Level.Error, MessageId.BleNoBluetooth);
yield* showSingleton(Level.Error, I18nId.BleNoBluetooth);
break;
case BleDeviceFailToConnectReasonType.NoWebBluetooth:
yield* showSingleton(
Level.Error,
MessageId.BleNoWebBluetooth,
I18nId.BleNoWebBluetooth,
undefined,
helpAction(
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
@@ -204,7 +204,7 @@ function* showBleDeviceDidFailToConnectError(
);
break;
case BleDeviceFailToConnectReasonType.Unknown:
yield* showUnexpectedError(MessageId.BleUnexpectedError, action.err);
yield* showUnexpectedError(I18nId.BleUnexpectedError, action.err);
break;
}
}
@@ -214,7 +214,7 @@ function* showBootloaderDidFailToConnectError(
): Generator {
switch (action.reason) {
case BootloaderConnectionFailureReason.GattServiceNotFound:
yield* showSingleton(Level.Error, MessageId.BleGattServiceNotFound, {
yield* showSingleton(Level.Error, I18nId.BleGattServiceNotFound, {
serviceName: 'LEGO Bootloader',
hubName: 'LEGO Bootloader',
});
@@ -222,7 +222,7 @@ function* showBootloaderDidFailToConnectError(
case BootloaderConnectionFailureReason.NoWebBluetooth:
yield* showSingleton(
Level.Error,
MessageId.BleNoWebBluetooth,
I18nId.BleNoWebBluetooth,
undefined,
helpAction(
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
@@ -230,10 +230,10 @@ function* showBootloaderDidFailToConnectError(
);
break;
case BootloaderConnectionFailureReason.NoBluetooth:
yield* showSingleton(Level.Error, MessageId.BleNoBluetooth);
yield* showSingleton(Level.Error, I18nId.BleNoBluetooth);
break;
case BootloaderConnectionFailureReason.Unknown:
yield* showUnexpectedError(MessageId.BleUnexpectedError, action.err);
yield* showUnexpectedError(I18nId.BleUnexpectedError, action.err);
break;
}
}
@@ -243,58 +243,55 @@ function* showFlashFirmwareError(
): Generator {
switch (action.reason.reason) {
case FailToFinishReasonType.TimedOut:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareTimedOut);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareTimedOut);
break;
case FailToFinishReasonType.BleError:
yield* showUnexpectedError(
MessageId.FlashFirmwareBleError,
action.reason.err,
);
yield* showUnexpectedError(I18nId.FlashFirmwareBleError, action.reason.err);
break;
case FailToFinishReasonType.Disconnected:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareDisconnected);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareDisconnected);
break;
case FailToFinishReasonType.HubError:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareHubError);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareHubError);
// istanbul ignore next
if (process.env.NODE_ENV !== 'test') {
console.error(action.reason.hubError);
}
break;
case FailToFinishReasonType.NoFirmware:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareUnsupportedDevice);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareUnsupportedDevice);
break;
case FailToFinishReasonType.DeviceMismatch:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareDeviceMismatch);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareDeviceMismatch);
break;
case FailToFinishReasonType.FailedToFetch:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareFailToFetch, {
yield* showSingleton(Level.Error, I18nId.FlashFirmwareFailToFetch, {
status: action.reason.response.statusText,
});
break;
case FailToFinishReasonType.ZipError:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareBadZipFile);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareBadZipFile);
// istanbul ignore next
if (process.env.NODE_ENV !== 'test') {
console.error(action.reason.err);
}
break;
case FailToFinishReasonType.BadMetadata:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareBadMetadata);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareBadMetadata);
// istanbul ignore next
if (process.env.NODE_ENV !== 'test') {
console.error(action.reason.property, action.reason.problem);
}
break;
case FailToFinishReasonType.FailedToCompile:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareCompileError);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareCompileError);
break;
case FailToFinishReasonType.FirmwareSize:
yield* showSingleton(Level.Error, MessageId.FlashFirmwareSizeTooBig);
yield* showSingleton(Level.Error, I18nId.FlashFirmwareSizeTooBig);
break;
case FailToFinishReasonType.Unknown:
yield* showUnexpectedError(
MessageId.FlashFirmwareUnexpectedError,
I18nId.FlashFirmwareUnexpectedError,
action.reason.err,
);
break;
@@ -303,11 +300,11 @@ function* showFlashFirmwareError(
function* dismissCompilerError(): Generator {
const { toaster } = yield* getContext<NotificationContext>('notification');
toaster.dismiss(MessageId.MpyError);
toaster.dismiss(I18nId.MpyError);
}
function* showCompilerError(action: ReturnType<typeof didFailToCompile>): Generator {
yield* showSingleton(Level.Error, MessageId.MpyError, {
yield* showSingleton(Level.Error, I18nId.MpyError, {
errorMessage: React.createElement(
'pre',
{ style: { whiteSpace: 'pre-wrap', wordBreak: 'keep-all' } },
@@ -331,13 +328,13 @@ function* handleAddNotification(action: ReturnType<typeof addNotification>): Gen
function* showServiceWorkerUpdate(): Generator {
const ch = channel<React.MouseEvent<HTMLElement>>();
const userAction = dispatchAction(
MessageId.ServiceWorkerUpdateAction,
I18nId.ServiceWorkerUpdateAction,
ch.put,
'refresh',
);
yield* showSingleton(
Level.Info,
MessageId.ServiceWorkerUpdateMessage,
I18nId.ServiceWorkerUpdateMessage,
{
appName,
action: React.createElement('strong', undefined, userAction.text),
@@ -362,7 +359,7 @@ function* showNoUpdateInfo(action: ReturnType<typeof appDidCheckForUpdate>): Gen
intent: mapIntent(Level.Info),
icon: mapIcon(Level.Info),
message: React.createElement(NotificationMessage, {
messageId: MessageId.AppNoUpdateFound,
messageId: I18nId.AppNoUpdateFound,
replacements: { appName },
}),
});
@@ -379,32 +376,32 @@ function* checkVersion(
`>=${pythonVersionToSemver(firmwareVersion)}`,
)
) {
yield* showSingleton(Level.Error, MessageId.CheckFirmwareTooOld);
yield* showSingleton(Level.Error, I18nId.CheckFirmwareTooOld);
}
}
function* showFileStorageFailToInitialize(
action: ReturnType<typeof fileStorageDidFailToInitialize>,
): Generator {
yield* showUnexpectedError(MessageId.FileStorageFailedToInitialize, action.error);
yield* showUnexpectedError(I18nId.FileStorageFailedToInitialize, action.error);
}
function* showFileStorageFailToRead(
action: ReturnType<typeof fileStorageDidFailToReadFile>,
): Generator {
yield* showUnexpectedError(MessageId.FileStorageFailedToRead, action.error);
yield* showUnexpectedError(I18nId.FileStorageFailedToRead, action.error);
}
function* showFileStorageFailToWrite(
action: ReturnType<typeof fileStorageDidFailToWriteFile>,
): Generator {
yield* showUnexpectedError(MessageId.FileStorageFailedToWrite, action.error);
yield* showUnexpectedError(I18nId.FileStorageFailedToWrite, action.error);
}
function* showFileStorageFailToDelete(
action: ReturnType<typeof fileStorageDidFailToDeleteFile>,
): Generator {
yield* showUnexpectedError(MessageId.FileStorageFailedToDelete, action.error);
yield* showUnexpectedError(I18nId.FileStorageFailedToDelete, action.error);
}
function* showFileStorageFailToExport(
@@ -415,7 +412,7 @@ function* showFileStorageFailToExport(
return;
}
yield* showUnexpectedError(MessageId.FileStorageFailedToExport, action.error);
yield* showUnexpectedError(I18nId.FileStorageFailedToExport, action.error);
}
function* showFileStorageFailToArchive(
@@ -426,21 +423,17 @@ function* showFileStorageFailToArchive(
return;
}
yield* showUnexpectedError(MessageId.FileStorageFailedToExport, action.error);
yield* showUnexpectedError(I18nId.FileStorageFailedToExport, action.error);
}
function* showDeleteFileWarning(action: ReturnType<typeof explorerDeleteFile>) {
const ch = channel<React.MouseEvent<HTMLElement>>();
const userAction = dispatchAction(
MessageId.ExplorerDeleteFileAction,
ch.put,
'trash',
);
const userAction = dispatchAction(I18nId.ExplorerDeleteFileAction, ch.put, 'trash');
// TODO: this should probably not be a singleton
yield* showSingleton(
Level.Warning,
MessageId.ExplorerDeleteFileMessage,
I18nId.ExplorerDeleteFileMessage,
{
fileName: React.createElement('strong', undefined, action.fileName),
},
@@ -460,7 +453,7 @@ function* showDeleteFileWarning(action: ReturnType<typeof explorerDeleteFile>) {
// shown, close the notification
if (didRemoveFile) {
const { toaster } = yield* getContext<NotificationContext>('notification');
toaster.dismiss(MessageId.ExplorerDeleteFileMessage);
toaster.dismiss(I18nId.ExplorerDeleteFileMessage);
return;
}
@@ -476,7 +469,7 @@ function* showExplorerFailToImportFiles(
return;
}
yield* showUnexpectedError(MessageId.ExplorerFailedToImportFiles, action.error);
yield* showUnexpectedError(I18nId.ExplorerFailedToImportFiles, action.error);
}
export default function* (): Generator {
+2 -2
View File
@@ -3,7 +3,7 @@
import React from 'react';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import { I18nId } from '../toolbar/i18n';
import settingsIcon from './settings.svg';
type SettingsButtonProps = Pick<ActionButtonProps, 'label' | 'onAction'>;
@@ -15,7 +15,7 @@ const SettingsButton: React.VoidFunctionComponent<SettingsButtonProps> = ({
return (
<ActionButton
label={label}
tooltip={TooltipId.Settings}
tooltip={I18nId.Settings}
icon={settingsIcon}
onAction={onAction}
/>
+30 -48
View File
@@ -40,8 +40,7 @@ import {
useSettingHubName,
useSettingIsShowDocsEnabled,
} from './hooks';
import { SettingsStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import './settings.scss';
type SettingsProps = {
@@ -77,17 +76,13 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
const dispatch = useDispatch();
const [i18n] = useI18n({
id: 'settings',
translations: { en },
fallback: en,
});
const [i18n] = useI18n();
const hotkeys = useMemo(
() => [
{
combo: 'mod+d',
label: i18n.translate(SettingsStringId.AppearanceDocumentationTooltip),
label: i18n.translate(I18nId.AppearanceDocumentationTooltip),
global: true,
preventDefault: true,
onKeyDown: toggleIsSettingShowDocsEnabled,
@@ -112,7 +107,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
size={DrawerSize.SMALL}
title={
<span id="settings-drawer-dialog-title">
{i18n.translate(SettingsStringId.Title)}
{i18n.translate(I18nId.Title)}
</span>
}
onOpening={handleDrawerOpening}
@@ -123,18 +118,15 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={i18n.translate(SettingsStringId.AppearanceTitle)}
helperText={i18n.translate(
SettingsStringId.AppearanceZoomHelp,
{
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
},
)}
label={i18n.translate(I18nId.AppearanceTitle)}
helperText={i18n.translate(I18nId.AppearanceZoomHelp, {
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
})}
>
<Tooltip2
content={i18n.translate(
SettingsStringId.AppearanceDocumentationTooltip,
I18nId.AppearanceDocumentationTooltip,
)}
rootBoundary="document"
placement="left"
@@ -143,7 +135,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDocumentationLabel,
I18nId.AppearanceDocumentationLabel,
)}
checked={isSettingShowDocsEnabled}
onChange={(e) =>
@@ -154,18 +146,14 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
/>
</Tooltip2>
<Tooltip2
content={i18n.translate(
SettingsStringId.AppearanceDarkModeTooltip,
)}
content={i18n.translate(I18nId.AppearanceDarkModeTooltip)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDarkModeLabel,
)}
label={i18n.translate(I18nId.AppearanceDarkModeLabel)}
checked={isDarkMode}
onChange={(e) =>
setTernaryDarkMode(
@@ -177,10 +165,10 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
/>
</Tooltip2>
</FormGroup>
<FormGroup label={i18n.translate(SettingsStringId.FirmwareTitle)}>
<FormGroup label={i18n.translate(I18nId.FirmwareTitle)}>
<Tooltip2
content={i18n.translate(
SettingsStringId.FirmwareCurrentProgramTooltip,
I18nId.FirmwareCurrentProgramTooltip,
)}
rootBoundary="document"
placement="left"
@@ -189,7 +177,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
>
<Switch
label={i18n.translate(
SettingsStringId.FirmwareCurrentProgramLabel,
I18nId.FirmwareCurrentProgramLabel,
)}
checked={isFlashCurrentProgramEnabled}
onChange={(e) =>
@@ -201,9 +189,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
</Tooltip2>
<ControlGroup>
<Tooltip2
content={i18n.translate(
SettingsStringId.FirmwareHubNameTooltip,
)}
content={i18n.translate(I18nId.FirmwareHubNameTooltip)}
rootBoundary="document"
placement="left"
targetTagName="div"
@@ -214,9 +200,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
className={Classes.INLINE}
htmlFor="hub-name-input"
>
{i18n.translate(
SettingsStringId.FirmwareHubNameLabel,
)}
{i18n.translate(I18nId.FirmwareHubNameLabel)}
</Label>
</Tooltip2>
<InputGroup
@@ -231,7 +215,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
isHubNameValid ? undefined : (
<Tooltip2
content={i18n.translate(
SettingsStringId.FirmwareHubNameErrorTooltip,
I18nId.FirmwareHubNameErrorTooltip,
)}
rootBoundary="document"
placement="bottom"
@@ -248,14 +232,14 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
/>
</ControlGroup>
</FormGroup>
<FormGroup label={i18n.translate(SettingsStringId.HelpTitle)}>
<FormGroup label={i18n.translate(I18nId.HelpTitle)}>
<ButtonGroup minimal={true} vertical={true} alignText="left">
<AnchorButton
icon="lightbulb"
href={pybricksProjectsUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpProjectsLabel)}
{i18n.translate(I18nId.HelpProjectsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
@@ -263,7 +247,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
href={pybricksSupportUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpSupportLabel)}
{i18n.translate(I18nId.HelpSupportLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
@@ -271,7 +255,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
href={pybricksGitterUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpChatLabel)}
{i18n.translate(I18nId.HelpChatLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
@@ -279,7 +263,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
href={pybricksBugReportsUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpBugsLabel)}
{i18n.translate(I18nId.HelpBugsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AboutDialog
@@ -289,10 +273,10 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
</ButtonGroup>
</FormGroup>
<FormGroup
label={i18n.translate(SettingsStringId.AppTitle)}
label={i18n.translate(I18nId.AppTitle)}
helperText={
readyForOfflineUse &&
i18n.translate(SettingsStringId.AppOfflineUseHelp)
i18n.translate(I18nId.AppOfflineUseHelp)
}
>
<ButtonGroup minimal={true} vertical={true} alignText="left">
@@ -302,7 +286,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
onClick={() => dispatch(appShowInstallPrompt())}
loading={promptingInstall}
>
{i18n.translate(SettingsStringId.AppInstallLabel)}
{i18n.translate(I18nId.AppInstallLabel)}
</Button>
)}
{isServiceWorkerRegistered && !updateAvailable && (
@@ -311,9 +295,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
onClick={() => dispatch(appCheckForUpdate())}
loading={checkingForUpdate}
>
{i18n.translate(
SettingsStringId.AppCheckForUpdateLabel,
)}
{i18n.translate(I18nId.AppCheckForUpdateLabel)}
</Button>
)}
{isServiceWorkerRegistered && updateAvailable && (
@@ -321,7 +303,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
icon="refresh"
onClick={() => dispatch(appReload())}
>
{i18n.translate(SettingsStringId.AppRestartLabel)}
{i18n.translate(I18nId.AppRestartLabel)}
</Button>
)}
<Button
@@ -331,7 +313,7 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
return true;
}}
>
{i18n.translate(SettingsStringId.AppAboutLabel)}
{i18n.translate(I18nId.AppAboutLabel)}
</Button>
</ButtonGroup>
</FormGroup>
-64
View File
@@ -1,64 +0,0 @@
{
"settings": {
"title": "Settings & Help",
"appearance": {
"title": "Appearance",
"documentation": {
"label": "Documentation",
"tooltip": "Show or hide the documentation pane."
},
"dark-mode": {
"label": "Dark mode",
"tooltip": "Enable or disable dark mode."
},
"zoom": {
"help": "Use {in} and {out} to zoom."
}
},
"firmware": {
"title": "Firmware",
"flash-current-program": {
"label": "Include current program",
"tooltip": "Select to include your program when installing the firmware."
},
"hub-name": {
"label": "Hub name",
"tooltip": "Hub name to use when flashing the firmware.",
"error": {
"tooltip": "The name is too long."
}
}
},
"help": {
"title": "Help",
"projects": {
"label": "Example Projects"
},
"support": {
"label": "Support"
},
"chat": {
"label": "Chat"
},
"bugs": {
"label": "Bug Reports"
}
},
"app": {
"title": "App",
"offlineUseHelp": "Ready for offline use.",
"install": {
"label": "Install as App"
},
"checkForUpdate": {
"label": "Check for Update"
},
"restart": {
"label": "Restart to Install Update"
},
"about": {
"label": "About"
}
}
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
import { lookup } from '../../test';
import { SettingsStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for SettingsStringIds', () => {
test.each(Object.values(SettingsStringId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+26 -26
View File
@@ -1,31 +1,31 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
//
// Settings translation keys.
export enum SettingsStringId {
Title = 'settings.title',
AppearanceTitle = 'settings.appearance.title',
AppearanceDocumentationLabel = 'settings.appearance.documentation.label',
AppearanceDocumentationTooltip = 'settings.appearance.documentation.tooltip',
AppearanceDarkModeLabel = 'settings.appearance.dark-mode.label',
AppearanceDarkModeTooltip = 'settings.appearance.dark-mode.tooltip',
AppearanceZoomHelp = 'settings.appearance.zoom.help',
FirmwareTitle = 'settings.firmware.title',
FirmwareCurrentProgramLabel = 'settings.firmware.flash-current-program.label',
FirmwareCurrentProgramTooltip = 'settings.firmware.flash-current-program.tooltip',
FirmwareHubNameLabel = 'settings.firmware.hub-name.label',
FirmwareHubNameTooltip = 'settings.firmware.hub-name.tooltip',
FirmwareHubNameErrorTooltip = 'settings.firmware.hub-name.error.tooltip',
HelpTitle = 'settings.help.title',
HelpProjectsLabel = 'settings.help.projects.label',
HelpSupportLabel = 'settings.help.support.label',
HelpChatLabel = 'settings.help.chat.label',
HelpBugsLabel = 'settings.help.bugs.label',
AppTitle = 'settings.app.title',
AppOfflineUseHelp = 'settings.app.offlineUseHelp',
AppInstallLabel = 'settings.app.install.label',
AppCheckForUpdateLabel = 'settings.app.checkForUpdate.label',
AppRestartLabel = 'settings.app.restart.label',
AppAboutLabel = 'settings.app.about.label',
export enum I18nId {
Title = 'title',
AppearanceTitle = 'appearance.title',
AppearanceDocumentationLabel = 'appearance.documentation.label',
AppearanceDocumentationTooltip = 'appearance.documentation.tooltip',
AppearanceDarkModeLabel = 'appearance.dark-mode.label',
AppearanceDarkModeTooltip = 'appearance.dark-mode.tooltip',
AppearanceZoomHelp = 'appearance.zoom.help',
FirmwareTitle = 'firmware.title',
FirmwareCurrentProgramLabel = 'firmware.flash-current-program.label',
FirmwareCurrentProgramTooltip = 'firmware.flash-current-program.tooltip',
FirmwareHubNameLabel = 'firmware.hub-name.label',
FirmwareHubNameTooltip = 'firmware.hub-name.tooltip',
FirmwareHubNameErrorTooltip = 'firmware.hub-name.error.tooltip',
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',
}
+62
View File
@@ -0,0 +1,62 @@
{
"title": "Settings & Help",
"appearance": {
"title": "Appearance",
"documentation": {
"label": "Documentation",
"tooltip": "Show or hide the documentation pane."
},
"dark-mode": {
"label": "Dark mode",
"tooltip": "Enable or disable dark mode."
},
"zoom": {
"help": "Use {in} and {out} to zoom."
}
},
"firmware": {
"title": "Firmware",
"flash-current-program": {
"label": "Include current program",
"tooltip": "Select to include your program when installing the firmware."
},
"hub-name": {
"label": "Hub name",
"tooltip": "Hub name to use when flashing the firmware.",
"error": {
"tooltip": "The name is too long."
}
}
},
"help": {
"title": "Help",
"projects": {
"label": "Example Projects"
},
"support": {
"label": "Support"
},
"chat": {
"label": "Chat"
},
"bugs": {
"label": "Bug Reports"
}
},
"app": {
"title": "App",
"offlineUseHelp": "Ready for offline use.",
"install": {
"label": "Install as App"
},
"checkForUpdate": {
"label": "Check for Update"
},
"restart": {
"label": "Restart to Install Update"
},
"about": {
"label": "About"
}
}
}
+25 -19
View File
@@ -3,13 +3,12 @@
import { Button, Intent, ProgressBar } from '@blueprintjs/core';
import { Classes as Classes2, Popover2, Popover2Props } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import { I18n, useI18n } from '@shopify/react-i18n';
import React from 'react';
import { BleConnectionState } from '../ble/reducers';
import { useSelector } from '../reducers';
import { preventBrowserNativeContextMenu } from '../utils/react';
import { MessageId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import './status-bar.scss';
@@ -18,13 +17,16 @@ const commonPopoverProps: Partial<Popover2Props> = {
placement: 'top',
};
const HubInfoButton: React.VFC = (_props) => {
type HubInfoButtonProps = {
/** Translation context. */
i18n: I18n;
};
const HubInfoButton: React.VoidFunctionComponent<HubInfoButtonProps> = ({ i18n }) => {
const deviceName = useSelector((s) => s.ble.deviceName);
const deviceType = useSelector((s) => s.ble.deviceType);
const deviceFirmwareVersion = useSelector((s) => s.ble.deviceFirmwareVersion);
const [i18n] = useI18n({ id: 'statusBar', translations: { en }, fallback: en });
return (
<Popover2
{...commonPopoverProps}
@@ -34,23 +36,21 @@ const HubInfoButton: React.VFC = (_props) => {
<tr>
<td>
<strong>
{i18n.translate(MessageId.HubInfoConnectedTo)}
{i18n.translate(I18nId.HubInfoConnectedTo)}
</strong>
</td>
<td>{deviceName}</td>
</tr>
<tr>
<td>
<strong>
{i18n.translate(MessageId.HubInfoHubType)}
</strong>
<strong>{i18n.translate(I18nId.HubInfoHubType)}</strong>
</td>
<td>{deviceType}</td>
</tr>
<tr>
<td>
<strong>
{i18n.translate(MessageId.HubInfoFirmware)}
{i18n.translate(I18nId.HubInfoFirmware)}
</strong>
</td>
<td>v{deviceFirmwareVersion}</td>
@@ -59,32 +59,37 @@ const HubInfoButton: React.VFC = (_props) => {
</table>
}
>
<Button title={i18n.translate(MessageId.HubInfoTitle)} minimal={true}>
<Button title={i18n.translate(I18nId.HubInfoTitle)} minimal={true}>
{deviceName}
</Button>
</Popover2>
);
};
const BatteryIndicator: React.VFC = (_props) => {
type BatteryIndicatorProps = {
/** Translation context. */
i18n: I18n;
};
const BatteryIndicator: React.VoidFunctionComponent<BatteryIndicatorProps> = ({
i18n,
}) => {
const charging = useSelector((s) => s.ble.deviceBatteryCharging);
const lowBatteryWarning = useSelector((s) => s.ble.deviceLowBatteryWarning);
const [i18n] = useI18n({ id: 'statusBar', translations: { en }, fallback: en });
return (
<Popover2
{...commonPopoverProps}
content={
<span className="no-wrap">
{i18n.translate(
lowBatteryWarning ? MessageId.BatteryLow : MessageId.BatteryOk,
lowBatteryWarning ? I18nId.BatteryLow : I18nId.BatteryOk,
)}
</span>
}
>
<div
title={i18n.translate(MessageId.BatteryTitle)}
title={i18n.translate(I18nId.BatteryTitle)}
className="pb-battery-indicator"
style={{ cursor: 'pointer' }}
>
@@ -103,6 +108,7 @@ const BatteryIndicator: React.VFC = (_props) => {
const StatusBar: React.VFC = (_props) => {
const connection = useSelector((s) => s.ble.connection);
const [i18n] = useI18n();
return (
<div
@@ -113,8 +119,8 @@ const StatusBar: React.VFC = (_props) => {
>
{connection === BleConnectionState.Connected && (
<>
<HubInfoButton />
<BatteryIndicator />
<HubInfoButton i18n={i18n} />
<BatteryIndicator i18n={i18n} />
</>
)}
</div>
+5 -5
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../test';
import { MessageId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for MessageIds', () => {
test.each(Object.values(MessageId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+2 -2
View File
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
//
// Status bar translation keys.
export enum MessageId {
export enum I18nId {
BatteryTitle = 'battery.title',
BatteryLow = 'battery.low',
BatteryOk = 'battery.ok',
+6 -7
View File
@@ -12,8 +12,7 @@ import { FitAddon } from 'xterm-addon-fit';
import { isMacOS } from '../utils/os';
import { TerminalContext } from './TerminalContext';
import { receiveData } from './actions';
import { TerminalStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import 'xterm/css/xterm.css';
@@ -55,7 +54,7 @@ function createContextMenu(
xterm: XTerm,
): (props: ContextMenu2ContentProps) => JSX.Element {
const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => {
const [i18n] = useI18n({ id: 'terminal', translations: { en }, fallback: en });
const [i18n] = useI18n();
return (
<Menu>
@@ -66,7 +65,7 @@ function createContextMenu(
navigator.clipboard.writeText(selected);
}
}}
text={i18n.translate(TerminalStringId.Copy)}
text={i18n.translate(I18nId.Copy)}
icon="duplicate"
label={isMacOS() ? 'Cmd-C' : 'Ctrl-Shift-C'}
disabled={!xterm.hasSelection()}
@@ -75,19 +74,19 @@ function createContextMenu(
onClick={async (): Promise<void> => {
xterm.paste(await navigator.clipboard.readText());
}}
text={i18n.translate(TerminalStringId.Paste)}
text={i18n.translate(I18nId.Paste)}
icon="clipboard"
label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'}
/>
<MenuItem
onClick={() => xterm.selectAll()}
text={i18n.translate(TerminalStringId.SelectAll)}
text={i18n.translate(I18nId.SelectAll)}
icon="blank"
/>
<MenuDivider />
<MenuItem
onClick={(): void => xterm.clear()}
text={i18n.translate(TerminalStringId.Clear)}
text={i18n.translate(I18nId.Clear)}
icon="trash"
/>
</Menu>
-8
View File
@@ -1,8 +0,0 @@
{
"terminal": {
"copy": "Copy",
"paste": "Paste",
"selectAll": "Select All",
"clear": "Clear"
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../test';
import { TerminalStringId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for TerminalStringIds', () => {
test.each(Object.values(TerminalStringId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+6 -6
View File
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
//
// Terminal translation keys.
export enum TerminalStringId {
Copy = 'terminal.copy',
Paste = 'terminal.paste',
SelectAll = 'terminal.selectAll',
Clear = 'terminal.clear',
export enum I18nId {
Copy = 'copy',
Paste = 'paste',
SelectAll = 'selectAll',
Clear = 'clear',
}
+6
View File
@@ -0,0 +1,6 @@
{
"copy": "Copy",
"paste": "Paste",
"selectAll": "Select All",
"clear": "Clear"
}
+4 -5
View File
@@ -14,8 +14,7 @@ import { useI18n } from '@shopify/react-i18n';
import React, { useEffect, useMemo, useState } from 'react';
import { tooltipDelay } from '../app/constants';
import { pointerEventsNone } from '../utils/react';
import { TooltipId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
const smallScreenThreshold = 700;
@@ -25,9 +24,9 @@ export interface ActionButtonProps {
/** Keyboard shortcut. */
readonly keyboardShortcut?: string;
/** Tooltip text that appears when hovering over the button. */
readonly tooltip: TooltipId;
readonly tooltip: I18nId;
/** Tooltip text that appears when hovering over the button and @showProgress is true. */
readonly progressTooltip?: TooltipId;
readonly progressTooltip?: I18nId;
/** Icon shown on the button. */
readonly icon: string;
/** When true or undefined, the button is enabled. */
@@ -51,7 +50,7 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
progress,
onAction,
}) => {
const [i18n] = useI18n({ id: 'actionButton', translations: { en }, fallback: en });
const [i18n] = useI18n();
const [isSmallScreen, setIsSmallScreen] = useState(
window.innerWidth <= smallScreenThreshold,
+4 -9
View File
@@ -8,8 +8,7 @@ import React, { useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { tooltipDelay } from '../app/constants';
import { pointerEventsNone } from '../utils/react';
import { TooltipId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
const smallScreenThreshold = 700;
export interface OpenFileButtonProps {
@@ -18,7 +17,7 @@ export interface OpenFileButtonProps {
/** The accepted file extension */
readonly fileExtension: string;
/** Tooltip text that appears when hovering over the button. */
readonly tooltip: TooltipId;
readonly tooltip: I18nId;
/** Icon shown on the button. */
readonly icon: string;
/** When true or undefined, the button is enabled. */
@@ -50,11 +49,7 @@ const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
onReject,
onClick,
}) => {
const [i18n] = useI18n({
id: 'openFileButton',
translations: { en },
fallback: en,
});
const [i18n] = useI18n();
const [isSmallScreen, setIsSmallScreen] = useState(
window.innerWidth <= smallScreenThreshold,
@@ -108,7 +103,7 @@ const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
<Tooltip2
content={i18n.translate(
tooltip,
tooltip === TooltipId.FlashProgress
tooltip === I18nId.FlashProgress
? {
percent:
progress === undefined
+5 -5
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../test';
import { TooltipId } from './i18n';
import en from './i18n.en.json';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for TooltipIds', () => {
test.each(Object.values(TooltipId))('%s', (id) => {
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+1 -1
View File
@@ -3,7 +3,7 @@
//
// Toolbar button translation keys.
export enum TooltipId {
export enum I18nId {
Run = 'run.action.tooltip',
RunProgress = 'run.progress.tooltip',
Stop = 'stop.tooltip',