From 0d30e113da285a04ed91a5ad4a7174994b26e0a3 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Wed, 27 May 2020 17:32:18 -0500 Subject: [PATCH] use i18n for buttons --- src/components/ActionButton.tsx | 15 +++++++++++---- src/components/BluetoothButton.tsx | 5 +++-- src/components/FlashButton.tsx | 3 ++- src/components/OpenButton.tsx | 3 ++- src/components/OpenFileButton.tsx | 17 ++++++++++++----- src/components/ReplButton.tsx | 3 ++- src/components/RunButton.tsx | 2 ++ src/components/SaveAsButton.tsx | 3 ++- src/components/StopButton.tsx | 3 ++- src/components/button.en.json | 12 ++++++++++++ src/components/button.test.ts | 21 +++++++++++++++++++++ src/components/button.ts | 13 +++++++++++++ 12 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 src/components/button.en.json create mode 100644 src/components/button.test.ts create mode 100644 src/components/button.ts diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx index 838f65ec..b2f8ccf5 100644 --- a/src/components/ActionButton.tsx +++ b/src/components/ActionButton.tsx @@ -1,17 +1,20 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020 The Pybricks Authors +import { WithI18nProps, withI18n } from '@shopify/react-i18n'; import React from 'react'; import Button from 'react-bootstrap/Button'; import Image from 'react-bootstrap/Image'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import Tooltip from 'react-bootstrap/Tooltip'; +import { TooltipId } from './button'; +import en from './button.en.json'; export interface ActionButtonProps { /** A unique id for each instance. */ readonly id: string; /** Tooltip text that appears when hovering over the button. */ - readonly tooltip: string; + readonly tooltip: TooltipId; /** Icon shown on the button. */ readonly icon: string; /** When true or undefined, the button is enabled. */ @@ -20,14 +23,16 @@ export interface ActionButtonProps { readonly onAction: () => void; } -class ActionButton extends React.Component { +type Props = ActionButtonProps & WithI18nProps; + +class ActionButton extends React.Component { render(): JSX.Element { return ( - {this.props.tooltip}. + {this.props.i18n.translate(this.props.tooltip)}. } > @@ -48,4 +53,6 @@ class ActionButton extends React.Component { } } -export default ActionButton; +export default withI18n({ id: 'actionButton', fallback: en, translations: { en } })( + ActionButton, +); diff --git a/src/components/BluetoothButton.tsx b/src/components/BluetoothButton.tsx index 9397275e..6b8b5cda 100644 --- a/src/components/BluetoothButton.tsx +++ b/src/components/BluetoothButton.tsx @@ -8,6 +8,7 @@ import { RootState } from '../reducers'; import { BLEConnectionState } from '../reducers/ble'; import { BootloaderConnectionState } from '../reducers/bootloader'; import ActionButton, { ActionButtonProps } from './ActionButton'; +import { TooltipId } from './button'; import btConnectedIcon from './images/bt-connected.svg'; import btDisconnectedIcon from './images/bt-disconnected.svg'; @@ -20,13 +21,13 @@ const mapStateToProps = (state: RootState): StateProps => { state.bootloader.connection === BootloaderConnectionState.Disconnected ) { return { - tooltip: 'Connect using Bluetooth', + tooltip: TooltipId.BluetoothConnect, icon: btDisconnectedIcon, enabled: true, }; } else { return { - tooltip: 'Disconnect Bluetooth', + tooltip: TooltipId.BluetoothDisconnect, icon: btConnectedIcon, enabled: state.ble.connection === BLEConnectionState.Connected, }; diff --git a/src/components/FlashButton.tsx b/src/components/FlashButton.tsx index 5135d0ac..a37ab4bb 100644 --- a/src/components/FlashButton.tsx +++ b/src/components/FlashButton.tsx @@ -8,6 +8,7 @@ import * as notification from '../actions/notification'; import { RootState } from '../reducers'; import { BootloaderConnectionState } from '../reducers/bootloader'; import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton'; +import { TooltipId } from './button'; import firmwareIcon from './images/firmware.svg'; type StateProps = Pick; @@ -38,7 +39,7 @@ const mergeProps = ( ownProps: OwnProps, ): OpenFileButtonProps => ({ fileExtension: '.zip', - tooltip: 'Flash hub firmware', + tooltip: TooltipId.Flash, icon: firmwareIcon, ...ownProps, ...stateProps, diff --git a/src/components/OpenButton.tsx b/src/components/OpenButton.tsx index 5d9dad06..cc9ff87e 100644 --- a/src/components/OpenButton.tsx +++ b/src/components/OpenButton.tsx @@ -7,6 +7,7 @@ import * as editor from '../actions/editor'; import * as notification from '../actions/notification'; import { RootState } from '../reducers'; import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton'; +import { TooltipId } from './button'; import openIcon from './images/open.svg'; type StateProps = Pick; @@ -34,7 +35,7 @@ const mergeProps = ( ownProps: OwnProps, ): OpenFileButtonProps => ({ fileExtension: '.py', - tooltip: 'Open file', + tooltip: TooltipId.Open, icon: openIcon, ...ownProps, ...stateProps, diff --git a/src/components/OpenFileButton.tsx b/src/components/OpenFileButton.tsx index 3a890f88..c29ed6cb 100644 --- a/src/components/OpenFileButton.tsx +++ b/src/components/OpenFileButton.tsx @@ -1,12 +1,15 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020 The Pybricks Authors +import { WithI18nProps, withI18n } from '@shopify/react-i18n'; import React from 'react'; import Button from 'react-bootstrap/Button'; import Image from 'react-bootstrap/Image'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import Tooltip from 'react-bootstrap/Tooltip'; import Dropzone, { FileRejection } from 'react-dropzone'; +import { TooltipId } from './button'; +import en from './button.en.json'; export interface OpenFileButtonProps { /** A unique id for each instance. */ @@ -14,7 +17,7 @@ export interface OpenFileButtonProps { /** The accepted file extension */ readonly fileExtension: string; /** Tooltip text that appears when hovering over the button. */ - readonly tooltip: string; + readonly tooltip: TooltipId; /** Icon shown on the button. */ readonly icon: string; /** When true or undefined, the button is enabled. */ @@ -27,11 +30,13 @@ export interface OpenFileButtonProps { readonly onClick?: () => void; } +type Props = OpenFileButtonProps & WithI18nProps; + /** * Button that opens a file chooser dialog or accepts files dropped on it. */ -class OpenFileButton extends React.Component { - constructor(props: OpenFileButtonProps) { +class OpenFileButton extends React.Component { + constructor(props: Props) { super(props); this.onDropAccepted = this.onDropAccepted.bind(this); this.onDropRejected = this.onDropRejected.bind(this); @@ -80,7 +85,7 @@ class OpenFileButton extends React.Component { placement="bottom" overlay={ - {this.props.tooltip}. + {this.props.i18n.translate(this.props.tooltip)}. } > @@ -110,4 +115,6 @@ class OpenFileButton extends React.Component { } } -export default OpenFileButton; +export default withI18n({ id: 'openFileButton', fallback: en, translations: { en } })( + OpenFileButton, +); diff --git a/src/components/ReplButton.tsx b/src/components/ReplButton.tsx index eafe63bc..54356cdb 100644 --- a/src/components/ReplButton.tsx +++ b/src/components/ReplButton.tsx @@ -7,6 +7,7 @@ import { repl } from '../actions/hub'; import { RootState } from '../reducers'; import { HubRuntimeState } from '../reducers/hub'; import ActionButton, { ActionButtonProps } from './ActionButton'; +import { TooltipId } from './button'; import replIcon from './images/repl.svg'; type StateProps = Pick; @@ -28,7 +29,7 @@ const mergeProps = ( dispatchProps: DispatchProps, ownProps: OwnProps, ): ActionButtonProps => ({ - tooltip: 'Start REPL in terminal', + tooltip: TooltipId.Repl, icon: replIcon, ...ownProps, ...stateProps, diff --git a/src/components/RunButton.tsx b/src/components/RunButton.tsx index dd2e5428..882849b5 100644 --- a/src/components/RunButton.tsx +++ b/src/components/RunButton.tsx @@ -7,6 +7,7 @@ import { downloadAndRun } from '../actions/hub'; import { RootState } from '../reducers'; import { HubRuntimeState } from '../reducers/hub'; import ActionButton, { ActionButtonProps } from './ActionButton'; +import { TooltipId } from './button'; import runIcon from './images/run.svg'; type StateProps = Pick; @@ -27,6 +28,7 @@ const mergeProps = ( dispatchProps: DispatchProps, ownProps: OwnProps, ): ActionButtonProps => ({ + tooltip: TooltipId.Run, icon: runIcon, ...ownProps, ...stateProps, diff --git a/src/components/SaveAsButton.tsx b/src/components/SaveAsButton.tsx index a0a3c32e..360e4744 100644 --- a/src/components/SaveAsButton.tsx +++ b/src/components/SaveAsButton.tsx @@ -6,6 +6,7 @@ import { Dispatch } from '../actions'; import * as editor from '../actions/editor'; import { RootState } from '../reducers'; import ActionButton, { ActionButtonProps } from './ActionButton'; +import { TooltipId } from './button'; import downloadIcon from './images/download.svg'; type StateProps = Pick; @@ -27,7 +28,7 @@ const mergeProps = ( dispatchProps: DispatchProps, ownProps: OwnProps, ): ActionButtonProps => ({ - tooltip: 'Download file', + tooltip: TooltipId.SaveAs, icon: downloadIcon, ...ownProps, ...stateProps, diff --git a/src/components/StopButton.tsx b/src/components/StopButton.tsx index de1653c0..11b06ccb 100644 --- a/src/components/StopButton.tsx +++ b/src/components/StopButton.tsx @@ -7,6 +7,7 @@ import { stop } from '../actions/hub'; import { RootState } from '../reducers'; import { HubRuntimeState } from '../reducers/hub'; import ActionButton, { ActionButtonProps } from './ActionButton'; +import { TooltipId } from './button'; import stopIcon from './images/stop.svg'; type StateProps = Pick; @@ -26,7 +27,7 @@ const mergeProps = ( dispatchProps: DispatchProps, ownProps: OwnProps, ): ActionButtonProps => ({ - tooltip: 'Stop everything', + tooltip: TooltipId.Stop, icon: stopIcon, ...ownProps, ...stateProps, diff --git a/src/components/button.en.json b/src/components/button.en.json new file mode 100644 index 00000000..9cb23f21 --- /dev/null +++ b/src/components/button.en.json @@ -0,0 +1,12 @@ +{ + "open": { "tooltip": "Open file" }, + "saveAs": { "tooltip": "Download file" }, + "stop": { "tooltip": "Stop everything" }, + "run": { "tooltip": "Download and run this program" }, + "repl": { "tooltip": "Start REPL in terminal" }, + "bluetooth": { + "connect": { "tooltip": "Connect using Bluetooth" }, + "disconnect": { "tooltip": "Disconnect Bluetooth" } + }, + "flash": { "tooltip": "Flash hub firmware" } +} diff --git a/src/components/button.test.ts b/src/components/button.test.ts new file mode 100644 index 00000000..1483a68e --- /dev/null +++ b/src/components/button.test.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020 The Pybricks Authors + +import { TooltipId } from './button'; +import en from './button.en.json'; + +function lookup(obj: object, id: string): string | undefined { + const value = id + .split('.') + .reduce((pv, cv) => pv && (pv as Record)[cv], obj); + if (typeof value === 'string') { + return value; + } + return undefined; +} + +describe('Ensure .json file has matches for TooltipIds', () => { + test.each(Object.values(TooltipId))('%s', (id) => { + expect(lookup(en, id)).toBeDefined(); + }); +}); diff --git a/src/components/button.ts b/src/components/button.ts new file mode 100644 index 00000000..8cb8a3b3 --- /dev/null +++ b/src/components/button.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020 The Pybricks Authors + +export enum TooltipId { + Open = 'open.tooltip', + SaveAs = 'saveAs.tooltip', + Run = 'run.tooltip', + Stop = 'stop.tooltip', + Repl = 'repl.tooltip', + Flash = 'flash.tooltip', + BluetoothConnect = 'bluetooth.connect.tooltip', + BluetoothDisconnect = 'bluetooth.disconnect.tooltip', +}