mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
i18n: use babel-loader plugin
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
+24
-44
@@ -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}>a…z</code>,
|
||||
underscore: <code className={Classes.CODE}>_</code>,
|
||||
},
|
||||
)}
|
||||
{i18n.translate(I18nId.HelpTextHasInvalidFirstCharacter, {
|
||||
letters: <code className={Classes.CODE}>a…z</code>,
|
||||
underscore: <code className={Classes.CODE}>_</code>,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
case FileNameValidationResult.HasInvalidCharacters:
|
||||
return (
|
||||
<>
|
||||
{i18n.translate(
|
||||
NewFileWizardStringId.FileNameHelpTextHasInvalidCharacters,
|
||||
{
|
||||
letters: <code className={Classes.CODE}>a…z</code>,
|
||||
numbers: <code className={Classes.CODE}>0…9</code>,
|
||||
dash: <code className={Classes.CODE}>-</code>,
|
||||
underscore: <code className={Classes.CODE}>_</code>,
|
||||
},
|
||||
)}
|
||||
{i18n.translate(I18nId.HelpTextHasInvalidCharacters, {
|
||||
letters: <code className={Classes.CODE}>a…z</code>,
|
||||
numbers: <code className={Classes.CODE}>0…9</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"
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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())}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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)}</>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// Notification translation keys.
|
||||
|
||||
export enum MessageId {
|
||||
export enum I18nId {
|
||||
AppNoUpdateFound = 'app.noUpdateFound',
|
||||
CopyErrorMessage = 'copyErrorMessage',
|
||||
ReportBug = 'reportBug',
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"terminal": {
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"selectAll": "Select All",
|
||||
"clear": "Clear"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"selectAll": "Select All",
|
||||
"clear": "Clear"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user