From 270930db9ed25f8b70a9a493b1044a08f175edcf Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 4 Sep 2020 15:34:15 +0200 Subject: [PATCH] Plumbing for i18n (key -> key) (#429) * Plumbing for i18n (key -> key) * Update test text * Apply suggestions from code review * i18next-scanner for extraction --- CHANGELOG.md | 1 + i18next-scanner.config.js | 34 + package.json | 10 +- packages/extension-base/package.json | 2 +- .../src/Popup/Accounts/Account.tsx | 14 +- .../src/Popup/Accounts/AddAccount.tsx | 4 +- .../extension-ui/src/Popup/Accounts/index.tsx | 4 +- .../src/Popup/Authorize/Request.tsx | 7 +- .../src/Popup/Authorize/index.tsx | 4 +- .../src/Popup/CreateAccount/AccountName.tsx | 4 +- .../CreateAccount/CreateAccount.test.tsx | 2 +- .../src/Popup/CreateAccount/CreationStep.tsx | 6 +- .../src/Popup/CreateAccount/Mnemonic.tsx | 13 +- .../src/Popup/CreateAccount/index.tsx | 5 +- .../src/Popup/Derive/DerivationPath.tsx | 9 +- .../src/Popup/Derive/SelectParent.tsx | 12 +- .../extension-ui/src/Popup/Derive/index.tsx | 6 +- packages/extension-ui/src/Popup/Export.tsx | 12 +- packages/extension-ui/src/Popup/Forget.tsx | 12 +- packages/extension-ui/src/Popup/ImportQr.tsx | 6 +- .../extension-ui/src/Popup/ImportSeed.tsx | 8 +- .../src/Popup/Metadata/Request.tsx | 22 +- .../extension-ui/src/Popup/Metadata/index.tsx | 4 +- .../extension-ui/src/Popup/RestoreJson.tsx | 21 +- .../extension-ui/src/Popup/Signing/Bytes.tsx | 8 +- .../src/Popup/Signing/Extrinsic.tsx | 35 +- .../extension-ui/src/Popup/Signing/Qr.tsx | 4 +- .../src/Popup/Signing/Request.tsx | 8 +- .../extension-ui/src/Popup/Signing/Unlock.tsx | 4 +- .../extension-ui/src/Popup/Signing/index.tsx | 6 +- packages/extension-ui/src/Popup/Welcome.tsx | 16 +- .../extension-ui/src/components/Address.tsx | 8 +- .../src/components/InputFileWithLabel.tsx | 11 +- .../extension-ui/src/components/Loading.tsx | 6 +- .../src/components/MnemonicSeed.tsx | 7 +- .../extension-ui/src/hooks/useTranslation.ts | 20 + packages/extension-ui/src/partials/Back.tsx | 5 +- .../src/partials/HeaderWithSteps.tsx | 1 + .../extension-ui/src/partials/MenuAdd.tsx | 12 +- .../src/partials/MenuSettings.tsx | 12 +- packages/extension-ui/src/partials/Name.tsx | 10 +- .../extension-ui/src/partials/Password.tsx | 8 +- .../public/locales/en/translation.json | 98 +++ yarn.lock | 676 +++++++++++++++--- 44 files changed, 972 insertions(+), 205 deletions(-) create mode 100644 i18next-scanner.config.js create mode 100644 packages/extension-ui/src/hooks/useTranslation.ts create mode 100644 packages/extension/public/locales/en/translation.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 42b78660..c09d6abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Changes: - Handling signing rejections (any order) transparently - Small overall UI and use adjustments - Latest upstream polkadot-js dependencies +- Prepare for i18n translations with setup of key -> key ## 0.32.1 Jul 27, 2020 diff --git a/i18next-scanner.config.js b/i18next-scanner.config.js new file mode 100644 index 00000000..15db788f --- /dev/null +++ b/i18next-scanner.config.js @@ -0,0 +1,34 @@ +// Copyright 2019-2020 @polkadot/extension authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +module.exports = { + input: [ + 'packages/extension-ui/src/**/*.{ts,tsx}', + // Use ! to filter out files or directories + '!packages/*/src/**/*.spec.{ts,tsx}', + '!packages/*/src/i18n/**', + '!**/node_modules/**' + ], + options: { + debug: true, + defaultLng: 'en', + func: { + extensions: ['.tsx', '.ts'], + list: ['t', 'i18next.t', 'i18n.t'] + }, + keySeparator: false, // key separator + lngs: ['en'], + nsSeparator: false, // namespace separator + resource: { + jsonIndent: 2, + lineEnding: '\n', + loadPath: 'packages/extension/public/locales/{{lng}}/{{ns}}.json', + savePath: 'packages/extension/public/locales/{{lng}}/{{ns}}.json' + }, + trans: { + component: 'Trans' + } + }, + output: './' +}; diff --git a/package.json b/package.json index 137f7e99..4b9e8f93 100644 --- a/package.json +++ b/package.json @@ -8,17 +8,18 @@ "packages/*" ], "resolutions": { - "@polkadot/api": "^1.32.0-beta.18", + "@polkadot/api": "^1.32.0-beta.19", "@polkadot/keyring": "^3.4.1", - "@polkadot/types": "^1.32.0-beta.18", + "@polkadot/types": "^1.32.0-beta.19", "@polkadot/util": "^3.4.1", "@polkadot/util-crypto": "^3.4.1", "babel-core": "^7.0.0-bridge.0", "typescript": "^4.0.2" }, "scripts": { - "build": "yarn build:code && yarn build:zip", + "build": "yarn build:i18n && yarn build:code && yarn build:zip", "build:code": "NODE_ENV=production polkadot-dev-build-ts", + "build:i18n": "i18next-scanner --config i18next-scanner.config.js", "build:release": "polkadot-ci-ghact-build", "build:zip": "yarn build:zip:dst && yarn build:zip:src", "build:zip:dst": "rm -rf ./master-build.zip && cd packages/extension/build && zip -r -FS ../../../master-build.zip .", @@ -34,9 +35,10 @@ "devDependencies": { "@babel/core": "^7.11.5", "@polkadot/dev": "^0.55.45", - "@polkadot/typegen": "^1.32.0-beta.18", + "@polkadot/typegen": "^1.32.0-beta.19", "@types/jest": "^26.0.13", "copy-webpack-plugin": "^6.1.0", + "i18next-scanner": "^2.11.0", "sinon-chrome": "^3.0.1", "stylelint": "^13.7.0", "stylelint-config-recommended": "^3.0.0", diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index b2f1aa10..43055871 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -6,7 +6,7 @@ "license": "Apache-2", "dependencies": { "@babel/runtime": "^7.11.2", - "@polkadot/api": "^1.32.0-beta.18", + "@polkadot/api": "^1.32.0-beta.19", "@polkadot/extension-inject": "0.33.0-beta.18", "@polkadot/keyring": "^3.4.1", "@polkadot/ui-keyring": "^0.59.0-beta.0" diff --git a/packages/extension-ui/src/Popup/Accounts/Account.tsx b/packages/extension-ui/src/Popup/Accounts/Account.tsx index a09870b8..464feee7 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.tsx @@ -9,6 +9,7 @@ import styled from 'styled-components'; import genesisOptions from '@polkadot/extension-chains/genesisHashes'; import { ActionContext, Address, Checkbox, Dropdown, Link, MenuDivider } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; import { editAccount, showAccount, tieAccount } from '../../messaging'; import { Name } from '../../partials'; @@ -23,6 +24,7 @@ interface EditState { } function Account ({ address, className, genesisHash, isExternal, isHidden, parentName, suri }: Props): React.ReactElement { + const { t } = useTranslation(); const onAction = useContext(ActionContext); const [{ isEditing, toggleActions }, setEditing] = useState({ isEditing: false, toggleActions: 0 }); const [editedName, setName] = useState(null); @@ -67,14 +69,14 @@ function Account ({ address, className, genesisHash, isExternal, isHidden, paren className='menuItem' onClick={_toggleEdit} > - Rename + {t('Rename')} {!isExternal && ( - Derive New Account + {t('Derive New Account')} )} @@ -84,7 +86,7 @@ function Account ({ address, className, genesisHash, isExternal, isHidden, paren isDanger to={`/account/export/${address}`} > - Export Account + {t('Export Account')} )} - Forget Account + {t('Forget Account')}
@@ -113,7 +115,7 @@ function Account ({ address, className, genesisHash, isExternal, isHidden, paren /> - ), [_onChangeGenesis, _toggleEdit, _toggleVisibility, address, genesisHash, isExternal, isHidden]); + ), [_onChangeGenesis, _toggleEdit, _toggleVisibility, address, genesisHash, isExternal, isHidden, t]); return (
diff --git a/packages/extension-ui/src/Popup/Accounts/AddAccount.tsx b/packages/extension-ui/src/Popup/Accounts/AddAccount.tsx index 289f4413..62de2c78 100644 --- a/packages/extension-ui/src/Popup/Accounts/AddAccount.tsx +++ b/packages/extension-ui/src/Popup/Accounts/AddAccount.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useContext } from 'react'; import styled from 'styled-components'; import { ActionContext } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; import Header from '../../partials/Header'; import AddAccountImage from './AddAccountImage'; @@ -16,6 +17,7 @@ interface Props extends ThemeProps { } function AddAccount ({ className }: Props): React.ReactElement { + const { t } = useTranslation(); const onAction = useContext(ActionContext); const _onClick = useCallback( () => onAction('/account/create'), @@ -34,7 +36,7 @@ function AddAccount ({ className }: Props): React.ReactElement {
-

You currently don't have any accounts. Create your first account to get started.

+

{t("You currently don't have any accounts. Create your first account to get started.")}

diff --git a/packages/extension-ui/src/Popup/Accounts/index.tsx b/packages/extension-ui/src/Popup/Accounts/index.tsx index a0ed19a0..00cc2c87 100644 --- a/packages/extension-ui/src/Popup/Accounts/index.tsx +++ b/packages/extension-ui/src/Popup/Accounts/index.tsx @@ -6,11 +6,13 @@ import React, { useContext } from 'react'; import styled from 'styled-components'; import { AccountContext } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; import { Header } from '../../partials'; import AccountsTree from './AccountsTree'; import AddAccount from './AddAccount'; export default function Accounts (): React.ReactElement { + const { t } = useTranslation(); const { hierarchy } = useContext(AccountContext); return ( @@ -22,7 +24,7 @@ export default function Accounts (): React.ReactElement {
{hierarchy.map((json, index): React.ReactNode => ( diff --git a/packages/extension-ui/src/Popup/Authorize/Request.tsx b/packages/extension-ui/src/Popup/Authorize/Request.tsx index 0f6f6b54..7ac9130f 100644 --- a/packages/extension-ui/src/Popup/Authorize/Request.tsx +++ b/packages/extension-ui/src/Popup/Authorize/Request.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useContext } from 'react'; import styled from 'styled-components'; import { ActionBar, ActionContext, Button, Icon, Link, Warning } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; import { approveAuthRequest, rejectAuthRequest } from '../../messaging'; interface Props { @@ -20,6 +21,7 @@ interface Props { } function Request ({ authId, className, isFirst, request: { origin }, url }: Props): React.ReactElement { + const { t } = useTranslation(); const onAction = useContext(ActionContext); const _onApprove = useCallback( @@ -36,6 +38,7 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop [authId, onAction] ); + // FIXME i18next component to be used here return (
@@ -57,8 +60,8 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop {isFirst && ( <> - Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts. - Yes, allow this application access + {t('Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.')} + {t('Yes, allow this application access')} )} diff --git a/packages/extension-ui/src/Popup/Authorize/index.tsx b/packages/extension-ui/src/Popup/Authorize/index.tsx index 62b7d2fe..e23ef5d7 100644 --- a/packages/extension-ui/src/Popup/Authorize/index.tsx +++ b/packages/extension-ui/src/Popup/Authorize/index.tsx @@ -6,16 +6,18 @@ import React, { useContext } from 'react'; import styled from 'styled-components'; import { AuthorizeReqContext } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; import { Header } from '../../partials'; import Request from './Request'; export default function Authorize (): React.ReactElement { + const { t } = useTranslation(); const requests = useContext(AuthorizeReqContext); return ( <> -
+
{requests.map(({ id, request, url }, index): React.ReactNode => ( { + const { t } = useTranslation(); const [name, setName] = useState(null); const [password, setPassword] = useState(null); @@ -39,7 +41,7 @@ function AccountName ({ isBusy, onBackClick, onCreate }: Props): React.ReactElem isDisabled={!password || !name} onClick={_onCreate} > - Add the account with the generated seed + {t('Add the account with the generated seed')} diff --git a/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx b/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx index 24e3de83..6c5b47cb 100644 --- a/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx +++ b/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx @@ -76,7 +76,7 @@ describe('Create Account', () => { it('clicking on Next activates phase 2', () => { check(wrapper.find('input[type="checkbox"]')); wrapper.find('button').simulate('click'); - expect(wrapper.find(Header).text()).toBe('Create an account: 2/2Cancel'); + expect(wrapper.find(Header).text()).toBe('Create an account2/2Cancel'); }); }); diff --git a/packages/extension-ui/src/Popup/CreateAccount/CreationStep.tsx b/packages/extension-ui/src/Popup/CreateAccount/CreationStep.tsx index 775d0327..19fe5328 100644 --- a/packages/extension-ui/src/Popup/CreateAccount/CreationStep.tsx +++ b/packages/extension-ui/src/Popup/CreateAccount/CreationStep.tsx @@ -8,6 +8,7 @@ import React from 'react'; import styled from 'styled-components'; import { ActionText, Title } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; interface Props { step: number; @@ -16,6 +17,8 @@ interface Props { } function CreationStep ({ className, onClick, step }: Props): React.ReactElement { + const { t } = useTranslation(); + return (
@@ -25,12 +28,13 @@ function CreationStep ({ className, onClick, step }: Props): React.ReactElement<
); } +// FIXME i18n const CreateAnAccount = styled(Title).attrs(() => ({ children: 'Create an account:' }))` diff --git a/packages/extension-ui/src/Popup/CreateAccount/Mnemonic.tsx b/packages/extension-ui/src/Popup/CreateAccount/Mnemonic.tsx index eb834882..d67cc2d5 100644 --- a/packages/extension-ui/src/Popup/CreateAccount/Mnemonic.tsx +++ b/packages/extension-ui/src/Popup/CreateAccount/Mnemonic.tsx @@ -6,6 +6,7 @@ import React, { useCallback, useState } from 'react'; import { ButtonArea, Checkbox, MnemonicSeed, NextStepButton, VerticalSpace, Warning } from '../../components'; import useToast from '../../hooks/useToast'; +import useTranslation from '../../hooks/useTranslation'; interface Props { onNextStep: () => void; @@ -24,12 +25,14 @@ const onCopy = (): void => { }; function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement { + const { t } = useTranslation(); const [isMnemonicSaved, setIsMnemonicSaved] = useState(false); const { show } = useToast(); + const _onCopy = useCallback((): void => { onCopy(); - show('Copied'); - }, [show]); + show(t('Copied')); + }, [show, t]); return ( <> @@ -37,11 +40,11 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement { onCopy={_onCopy} seed={seed} /> - Please write down your wallet’s mnemonic seed and keep it in a safe place. The mnemonic can be used to restore your wallet. Keep it carefully to not lose your assets. + {t("Please write down your wallet's mnemonic seed and keep it in a safe place. The mnemonic can be used to restore your wallet. Keep it carefully to not lose your assets.")} @@ -49,7 +52,7 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement { isDisabled={!isMnemonicSaved} onClick={onNextStep} > - Next step + {t('Next step')} diff --git a/packages/extension-ui/src/Popup/CreateAccount/index.tsx b/packages/extension-ui/src/Popup/CreateAccount/index.tsx index ff455708..321fcac2 100644 --- a/packages/extension-ui/src/Popup/CreateAccount/index.tsx +++ b/packages/extension-ui/src/Popup/CreateAccount/index.tsx @@ -3,13 +3,16 @@ // of the Apache-2.0 license. See the LICENSE file for details. import React, { useCallback, useContext, useEffect, useState } from 'react'; + import { ActionContext, Address, Loading } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; import { createAccountSuri, createSeed } from '../../messaging'; import { HeaderWithSteps } from '../../partials'; import AccountName from './AccountName'; import Mnemonic from './Mnemonic'; export default function CreateAccount (): React.ReactElement { + const { t } = useTranslation(); const onAction = useContext(ActionContext); const [isBusy, setIsBusy] = useState(false); const [step, setStep] = useState(1); @@ -46,7 +49,7 @@ export default function CreateAccount (): React.ReactElement { <>
diff --git a/packages/extension-ui/src/Popup/Derive/DerivationPath.tsx b/packages/extension-ui/src/Popup/Derive/DerivationPath.tsx index d8c50bbc..ba9121fb 100644 --- a/packages/extension-ui/src/Popup/Derive/DerivationPath.tsx +++ b/packages/extension-ui/src/Popup/Derive/DerivationPath.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components'; import lockIcon from '../../assets/lock.svg'; import unlockIcon from '../../assets/unlock.svg'; import { InputWithLabel, Svg, Button } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; interface Props { className?: string; @@ -21,8 +22,10 @@ interface Props { } function DerivationPath ({ className, defaultPath, isError, onChange }: Props): React.ReactElement { + const { t } = useTranslation(); const [path, setPath] = useState(defaultPath); const [isDisabled, setIsDisabled] = useState(false); + const _onExpand = useCallback(() => setIsDisabled(!isDisabled), [isDisabled]); const _onChange = useCallback((newPath: string): void => { @@ -40,11 +43,11 @@ function DerivationPath ({ className, defaultPath, isError, onChange }: Props): isError={isError || !path} label={ isDisabled - ? 'Derivation Path' - : 'Derivation Path (unlock to edit)' + ? t('Derivation Path') + : t('Derivation Path (unlock to edit)') } onChange={_onChange} - placeholder='//hard/soft' + placeholder={t('//hard/soft')} value={path} /> diff --git a/packages/extension-ui/src/Popup/Derive/SelectParent.tsx b/packages/extension-ui/src/Popup/Derive/SelectParent.tsx index f8fefe4b..2a90e3ef 100644 --- a/packages/extension-ui/src/Popup/Derive/SelectParent.tsx +++ b/packages/extension-ui/src/Popup/Derive/SelectParent.tsx @@ -7,6 +7,7 @@ import styled from 'styled-components'; import { assert } from '@polkadot/util'; import { AccountContext, ActionContext, Address, ButtonArea, Checkbox, InputWithLabel, Label, NextStepButton, VerticalSpace } from '../../components'; +import useTranslation from '../../hooks/useTranslation'; import { validateAccount, validateDerivationPath } from '../../messaging'; import { nextDerivationPath } from '../../util/nextDerivationPath'; import AddressDropdown from './AddressDropdown'; @@ -20,6 +21,7 @@ interface Props { } export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement { + const { t } = useTranslation(); const onAction = useContext(ActionContext); const [isBusy, setIsBusy] = useState(false); const { accounts, hierarchy } = useContext(AccountContext); @@ -93,7 +95,7 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress, {!isLocked && ( )} @@ -106,7 +108,7 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress, /> ) : ( -