Plumbing for i18n (key -> key) (#429)

* Plumbing for i18n (key -> key)

* Update test text

* Apply suggestions from code review

* i18next-scanner for extraction
This commit is contained in:
Jaco Greeff 2020-09-04 15:34:15 +02:00 committed by GitHub
parent a3d5fc449a
commit 270930db9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 972 additions and 205 deletions

View File

@ -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

34
i18next-scanner.config.js Normal file
View File

@ -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: './'
};

View File

@ -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",

View File

@ -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"

View File

@ -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<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [{ isEditing, toggleActions }, setEditing] = useState<EditState>({ isEditing: false, toggleActions: 0 });
const [editedName, setName] = useState<string | null>(null);
@ -67,14 +69,14 @@ function Account ({ address, className, genesisHash, isExternal, isHidden, paren
className='menuItem'
onClick={_toggleEdit}
>
Rename
{t('Rename')}
</Link>
{!isExternal && (
<Link
className='menuItem'
to={`/account/derive/${address}/locked`}
>
Derive New Account
{t('Derive New Account')}
</Link>
)}
<MenuDivider />
@ -84,7 +86,7 @@ function Account ({ address, className, genesisHash, isExternal, isHidden, paren
isDanger
to={`/account/export/${address}`}
>
Export Account
{t('Export Account')}
</Link>
)}
<Link
@ -92,14 +94,14 @@ function Account ({ address, className, genesisHash, isExternal, isHidden, paren
isDanger
to={`/account/forget/${address}`}
>
Forget Account
{t('Forget Account')}
</Link>
<MenuDivider />
<div className='menuItem'>
<Checkbox
checked={!isHidden}
className='inputItem'
label='Visible (always inject)'
label={t('Visible (always inject)')}
onClick={_toggleVisibility}
/>
</div>
@ -113,7 +115,7 @@ function Account ({ address, className, genesisHash, isExternal, isHidden, paren
/>
</div>
</>
), [_onChangeGenesis, _toggleEdit, _toggleVisibility, address, genesisHash, isExternal, isHidden]);
), [_onChangeGenesis, _toggleEdit, _toggleVisibility, address, genesisHash, isExternal, isHidden, t]);
return (
<div className={className}>

View File

@ -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<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const _onClick = useCallback(
() => onAction('/account/create'),
@ -34,7 +36,7 @@ function AddAccount ({ className }: Props): React.ReactElement<Props> {
<AddAccountImage onClick={_onClick}/>
</div>
<div className='no-accounts'>
<p>You currently don&apos;t have any accounts. Create your first account to get started.</p>
<p>{t("You currently don't have any accounts. Create your first account to get started.")}</p>
</div>
</div>
</>

View File

@ -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 {
<Header
showAdd
showSettings
text={'Accounts'}
text={t('Accounts')}
/>
<AccountsArea>
{hierarchy.map((json, index): React.ReactNode => (

View File

@ -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<Props> {
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 <Trans /> component to be used here
return (
<div className={className}>
<RequestInfo>
@ -57,8 +60,8 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop
</Info>
{isFirst && (
<>
<RequestWarning>Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.</RequestWarning>
<AcceptButton onClick={_onApprove}>Yes, allow this application access</AcceptButton>
<RequestWarning>{t('Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.')}</RequestWarning>
<AcceptButton onClick={_onApprove}>{t('Yes, allow this application access')}</AcceptButton>
</>
)}
<RejectButton>

View File

@ -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 (
<>
<Scroll isLastRequest={requests.length === 1}>
<Header text='Authorize' />
<Header text={t('Authorize')} />
{requests.map(({ id, request, url }, index): React.ReactNode => (
<Request
authId={id}

View File

@ -5,6 +5,7 @@
import React, { useCallback, useState } from 'react';
import { BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { Name, Password } from '../../partials';
interface Props {
@ -15,6 +16,7 @@ interface Props {
}
function AccountName ({ isBusy, onBackClick, onCreate }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [name, setName] = useState<string | null>(null);
const [password, setPassword] = useState<string | null>(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')}
</NextStepButton>
</ButtonArea>
</>

View File

@ -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');
});
});

View File

@ -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<Props> {
const { t } = useTranslation();
return (
<div className={className}>
<div>
@ -25,12 +28,13 @@ function CreationStep ({ className, onClick, step }: Props): React.ReactElement<
</div>
<ActionText
onClick={onClick}
text={step === 1 ? 'Cancel' : 'Back'}
text={step === 1 ? t('Cancel') : t('Back')}
/>
</div>
);
}
// FIXME i18n
const CreateAnAccount = styled(Title).attrs(() => ({
children: 'Create an account:'
}))`

View File

@ -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<Props> {
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<Props> {
onCopy={_onCopy}
seed={seed}
/>
<Warning>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.</Warning>
<Warning>{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.")}</Warning>
<VerticalSpace />
<Checkbox
checked={isMnemonicSaved}
label='I have saved my mnemonic seed safely.'
label={t('I have saved my mnemonic seed safely.')}
onChange={setIsMnemonicSaved}
/>
<ButtonArea>
@ -49,7 +52,7 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement<Props> {
isDisabled={!isMnemonicSaved}
onClick={onNextStep}
>
Next step
{t('Next step')}
</NextStepButton>
</ButtonArea>
</>

View File

@ -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 {
<>
<HeaderWithSteps
step={step}
text='Create an account:&nbsp;'
text={t('Create an account')}
/>
<Loading>
<div>

View File

@ -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<Props> {
const { t } = useTranslation();
const [path, setPath] = useState<string>(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}
/>
</PathInput>

View File

@ -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<Props> {
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 && (
<CheckboxWithSmallerMargins
checked={shouldAccountBeDerived}
label='Derive new account from existing'
label={t('Derive new account from existing')}
onChange={setShouldAccountBeDerived}
/>
)}
@ -106,7 +108,7 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress,
/>
)
: (
<Label label='Choose Parent Account:'>
<Label label={t('Choose Parent Account:')}>
<AddressDropdown
allAddresses={allAddresses}
onSelect={_onParentChange}
@ -121,7 +123,7 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress,
data-export-password
isError={!isProperParentPassword}
isFocused
label='enter the password for the account you want to derive from'
label={t('enter the password for the account you want to derive from')}
onChange={_onParentPasswordEnter}
type='password'
value={parentPassword}
@ -146,7 +148,7 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress,
isDisabled={!isProperParentPassword || !suriPath}
onClick={_onSubmit}
>
Create a derived account
{t('Create a derived account')}
</NextStepButton>
)
: (
@ -154,7 +156,7 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress,
data-button-action='create root account'
onClick={_goCreate}
>
Create account from new seed
{t('Create account from new seed')}
</NextStepButton>
)
}

View File

@ -6,6 +6,7 @@ import React, { useCallback, useContext, useMemo, useState } from 'react';
import { useParams } from 'react-router';
import { AccountContext, ActionContext, Address, BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { deriveAccount } from '../../messaging';
import { HeaderWithSteps, Name, Password } from '../../partials';
import { SelectParent } from './SelectParent';
@ -28,6 +29,7 @@ interface ConfirmState {
}
function Derive ({ isLocked }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const { accounts } = useContext(AccountContext);
const { address: parentAddress } = useParams<AddressState>();
@ -69,7 +71,7 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
<>
<HeaderWithSteps
step={account ? 2 : 1}
text='Add new account:&nbsp;'
text={t('Add new account')}
/>
{!account && (
<SelectParent
@ -101,7 +103,7 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
isDisabled={!password}
onClick={_onCreate}
>
Create derived account
{t('Create derived account')}
</NextStepButton>
</ButtonArea>
</>

View File

@ -6,6 +6,7 @@ import React, { useCallback, useContext, useState } from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { ActionBar, ActionContext, ActionText, Address, Button, InputWithLabel, Warning } from '../components';
import useTranslation from '../hooks/useTranslation';
import { exportAccount } from '../messaging';
import { Header } from '../partials';
import styled from 'styled-components';
@ -15,6 +16,7 @@ const MIN_LENGTH = 6;
type Props = RouteComponentProps<{ address: string }>;
function Export ({ match: { params: { address } } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [isBusy, setIsBusy] = useState(false);
const [pass, setPass] = useState('');
@ -55,17 +57,17 @@ function Export ({ match: { params: { address } } }: Props): React.ReactElement<
<>
<Header
showBackArrow
text='Export account'
text={t('Export account')}
/>
<div>
<Address address={address}>
<MovedWarning danger>You are exporting your account. Keep it safe and don&apos;t share it with anyone.</MovedWarning>
<MovedWarning danger>{t("You are exporting your account. Keep it safe and don't share it with anyone.")}</MovedWarning>
<ActionArea>
<InputWithLabel
data-export-password
disabled={isBusy}
isError={pass.length < MIN_LENGTH || wrongPasswordHighlight}
label='password for this account'
label={t('password for this account')}
onChange={setPass}
type='password'
/>
@ -77,12 +79,12 @@ function Export ({ match: { params: { address } } }: Props): React.ReactElement<
isDisabled={pass.length === 0}
onClick={_onExportButtonClick}
>
I want to export this account
{t('I want to export this account')}
</Button>
<CancelButton>
<ActionText
onClick={_goHome}
text='Cancel'
text={t('Cancel')}
/>
</CancelButton>
</ActionArea>

View File

@ -7,12 +7,14 @@ import { RouteComponentProps, withRouter } from 'react-router';
import styled from 'styled-components';
import { ActionContext, Address, Button, Warning, ActionBar, ActionText } from '../components';
import useTranslation from '../hooks/useTranslation';
import { forgetAccount } from '../messaging';
import { Header } from '../partials';
type Props = RouteComponentProps<{ address: string }>;
function Forget ({ match: { params: { address } } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const _goHome = useCallback(
@ -32,24 +34,22 @@ function Forget ({ match: { params: { address } } }: Props): React.ReactElement<
<>
<Header
showBackArrow
text='Forget account'
text={t('Forget account')}
/>
<div>
<Address address={address}>
<MovedWarning isDanger>
You are about to remove the account. This means that you will not be able to access it via this extension anymore. If you wish to recover it, you would need to use the seed.
</MovedWarning>
<MovedWarning isDanger>{t('You are about to remove the account. This means that you will not be able to access it via this extension anymore. If you wish to recover it, you would need to use the seed.')}</MovedWarning>
<ActionArea>
<Button
isDanger
onClick={_onClick}
>
I want to forget this account
{t('I want to forget this account')}
</Button>
<CancelButton>
<ActionText
onClick={_goHome}
text='Cancel'
text={t('Cancel')}
/>
</CancelButton>
</ActionArea>

View File

@ -6,10 +6,12 @@ import React, { useContext, useState } from 'react';
import { QrScanAddress } from '@polkadot/react-qr';
import { ActionContext, Address, NextStepButton, ButtonArea, VerticalSpace } from '../components';
import useTranslation from '../hooks/useTranslation';
import { createAccountExternal } from '../messaging';
import { Header, Name } from '../partials';
export default function ImportQr (): React.ReactElement {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [account, setAccount] = useState<null | { content: string; genesisHash: string }>(null);
const [name, setName] = useState<string | null>(null);
@ -27,7 +29,7 @@ export default function ImportQr (): React.ReactElement {
<>
<Header
showBackArrow
text='Scan Address Qr'
text={t('Scan Address Qr')}
/>
{!account && (
<div>
@ -52,7 +54,7 @@ export default function ImportQr (): React.ReactElement {
<NextStepButton
onClick={_onCreate}
>
Add the account with identified address
{t('Add the account with identified address')}
</NextStepButton>
</ButtonArea>
)}

View File

@ -6,6 +6,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { AccountContext, ActionContext, Address, ButtonArea, NextStepButton, TextAreaWithLabel, ValidatedInput, VerticalSpace } from '../components';
import useTranslation from '../hooks/useTranslation';
import { createAccountSuri, validateSeed } from '../messaging';
import { Header, Name, Password } from '../partials';
import { allOf, isNotShorterThan, Result } from '../util/validators';
@ -26,6 +27,7 @@ const isSeedValid = allOf(
);
export default function Import (): React.ReactElement {
const { t } = useTranslation();
const { accounts } = useContext(AccountContext);
const onAction = useContext(ActionContext);
const [isBusy, setIsBusy] = useState(false);
@ -71,7 +73,7 @@ export default function Import (): React.ReactElement {
<>
<HeaderWithSmallerMargin
showBackArrow
text='Import account'
text={t('Import account')}
/>
<div>
<Address
@ -83,7 +85,7 @@ export default function Import (): React.ReactElement {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
component={SeedInput}
isFocused
label='existing 12 or 24-word mnemonic seed'
label={t('existing 12 or 24-word mnemonic seed')}
onValidatedChange={_onChangeSeed}
rowsCount={2}
validator={isSeedValid}
@ -102,7 +104,7 @@ export default function Import (): React.ReactElement {
isBusy={isBusy}
onClick={_onCreate}
>
Add the account with the supplied seed
{t('Add the account with the supplied seed')}
</NextStepButton>
</ButtonArea>
</>

View File

@ -10,6 +10,7 @@ import styled from 'styled-components';
import { ActionBar, ActionContext, Button, Link, Table, Warning } from '../../components';
import useMetadata from '../../hooks/useMetadata';
import useTranslation from '../../hooks/useTranslation';
import { approveMetaRequest, rejectMetaRequest } from '../../messaging';
interface Props {
@ -20,6 +21,7 @@ interface Props {
}
function Request ({ className, metaId, request, url }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const chain = useMetadata(request.genesisHash);
const onAction = useContext(ActionContext);
@ -45,44 +47,44 @@ function Request ({ className, metaId, request, url }: Props): React.ReactElemen
<div className={className}>
<Table>
<tr>
<td className='label'>from</td>
<td className='label'>{t('from')}</td>
<td className='data'>{url}</td>
</tr>
<tr>
<td className='label'>chain</td>
<td className='label'>{t('chain')}</td>
<td className='data'>{request.chain}</td>
</tr>
<tr>
<td className='label'>icon</td>
<td className='label'>{t('icon')}</td>
<td className='data'>{request.icon}</td>
</tr>
<tr>
<td className='label'>decimals</td>
<td className='label'>{t('decimals')}</td>
<td className='data'>{request.tokenDecimals}</td>
</tr>
<tr>
<td className='label'>symbol</td>
<td className='label'>{t('symbol')}</td>
<td className='data'>{request.tokenSymbol}</td>
</tr>
<tr>
<td className='label'>upgrade</td>
<td className='data'>{chain ? chain.specVersion : 'unknown'} -&gt; {request.specVersion}</td>
<td className='label'>{t('upgrade')}</td>
<td className='data'>{chain ? chain.specVersion : t('<unknown>')} -&gt; {request.specVersion}</td>
</tr>
</Table>
<div className='requestInfo'>
<Warning className='requestWarning'>This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.</Warning>
<Warning className='requestWarning'>{t('This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.')}</Warning>
<Button
className='btnAccept'
onClick={_onApprove}
>
Yes, do this metadata update
{t('Yes, do this metadata update')}
</Button>
<ActionBar className='btnReject'>
<Link
isDanger
onClick={_onReject}
>
Reject
{t('Reject')}
</Link>
</ActionBar>
</div>

View File

@ -5,15 +5,17 @@
import React, { useContext } from 'react';
import { Loading, MetadataReqContext } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { Header } from '../../partials';
import Request from './Request';
export default function Metadata (): React.ReactElement {
const { t } = useTranslation();
const requests = useContext(MetadataReqContext);
return (
<>
<Header text='Metadata' />
<Header text={t('Metadata')} />
{requests[0]
? (
<Request

View File

@ -3,12 +3,14 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringPair$Json } from '@polkadot/keyring/types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { AccountContext, ActionContext, InputWithLabel, InputFileWithLabel, Button, Address } from '../components';
import { u8aToString } from '@polkadot/util';
import styled from 'styled-components';
import { jsonRestore, jsonVerifyPassword, jsonVerifyFile } from '../messaging';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { u8aToString } from '@polkadot/util';
import { AccountContext, ActionContext, InputWithLabel, InputFileWithLabel, Button, Address } from '../components';
import useTranslation from '../hooks/useTranslation';
import { jsonRestore, jsonVerifyPassword, jsonVerifyFile } from '../messaging';
import { Header } from '../partials';
interface FileState {
@ -41,6 +43,7 @@ async function parseFile (file: Uint8Array): Promise<FileState> {
}
export default function Upload (): React.ReactElement {
const { t } = useTranslation();
const { accounts } = useContext(AccountContext);
const onAction = useContext(ActionContext);
const [isBusy, setIsBusy] = useState(false);
@ -93,7 +96,7 @@ export default function Upload (): React.ReactElement {
<>
<HeaderWithSmallerMargin
showBackArrow
text='Restore from JSON'
text={t('Restore from JSON')}
/>
<div>
<div>
@ -106,13 +109,13 @@ export default function Upload (): React.ReactElement {
<InputFileWithLabel
accept={acceptedFormats}
isError={!isFileValid}
label={'backup file'}
label={t('backup file')}
onChange={_onChangeFile}
withLabel
/>
<InputWithLabel
isError={!isPassValid}
label='Password for this file'
label={t('Password for this file')}
onChange={_onChangePass}
type='password'
/>
@ -121,7 +124,7 @@ export default function Upload (): React.ReactElement {
isDisabled={!isFileValid || !isPassValid}
onClick={_onRestore}
>
Restore
{t('Restore')}
</Button>
</div>
</>

View File

@ -5,6 +5,8 @@
import styled from 'styled-components';
import React from 'react';
import useTranslation from '../../hooks/useTranslation';
interface Props {
className?: string;
bytes: string;
@ -12,15 +14,17 @@ interface Props {
}
function Bytes ({ bytes, className, url }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<table className={className}>
<tbody>
<tr>
<td className='label'>from</td>
<td className='label'>{t('from')}</td>
<td className='data'>{url}</td>
</tr>
<tr>
<td className='label'>bytes</td>
<td className='label'>{t('bytes')}</td>
<td className='data'>{bytes}</td>
</tr>
</tbody>

View File

@ -12,6 +12,7 @@ import { formatNumber, bnToBn } from '@polkadot/util';
import { Table } from '../../components';
import useMetadata from '../../hooks/useMetadata';
import useTranslation from '../../hooks/useTranslation';
interface Decoded {
args: AnyJson | null;
@ -43,11 +44,11 @@ function decodeMethod (data: string, isDecoded: boolean, chain: Chain | null, sp
return { args, method };
}
function renderMethod (data: string, { args, method }: Decoded): React.ReactNode {
function renderMethod (data: string, { args, method }: Decoded, t: (key: string) => string): React.ReactNode {
if (!args || !method) {
return (
<tr>
<td className='label'>method data</td>
<td className='label'>{t('method data')}</td>
<td className='data'>{data}</td>
</tr>
);
@ -56,7 +57,7 @@ function renderMethod (data: string, { args, method }: Decoded): React.ReactNode
return (
<>
<tr>
<td className='label'>method</td>
<td className='label'>{t('method')}</td>
<td className='data'>
<details>
<summary>{method.sectionName}.{method.methodName}{
@ -70,7 +71,7 @@ function renderMethod (data: string, { args, method }: Decoded): React.ReactNode
</tr>
{method.meta && (
<tr>
<td className='label'>info</td>
<td className='label'>{t('info')}</td>
<td className='data'>
<details>
<summary>{method.meta.documentation.map((d) => d.toString().trim()).join(' ')}</summary>
@ -82,18 +83,24 @@ function renderMethod (data: string, { args, method }: Decoded): React.ReactNode
);
}
function mortalityAsString (era: ExtrinsicEra, hexBlockNumber: string): string {
function mortalityAsString (era: ExtrinsicEra, hexBlockNumber: string, t: (key: string, options?: { replace: Record<string, string> }) => string): string {
if (era.isImmortalEra) {
return 'immortal';
return t('immortal');
}
const blockNumber = bnToBn(hexBlockNumber);
const mortal = era.asMortalEra;
return `mortal, valid from #${formatNumber(mortal.birth(blockNumber))} to #${formatNumber(mortal.death(blockNumber))}`;
return t('mortal, valid from {{birth}} to {{death}}', {
replace: {
birth: formatNumber(mortal.birth(blockNumber)),
death: formatNumber(mortal.death(blockNumber))
}
});
}
function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, request: { blockNumber, genesisHash, method, specVersion: hexSpec }, url }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const chain = useMetadata(genesisHash);
const specVersion = useRef(bnToBn(hexSpec)).current;
const [decoded, setDecoded] = useState<Decoded>({ args: null, method: null });
@ -108,31 +115,31 @@ function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, reques
isFull
>
<tr>
<td className='label'>from</td>
<td className='label'>{t('from')}</td>
<td className='data'>{url}</td>
</tr>
<tr>
<td className='label'>{chain ? 'chain' : 'genesis'}</td>
<td className='label'>{chain ? t('chain') : t('genesis')}</td>
<td className='data'>{chain ? chain.name : genesisHash}</td>
</tr>
<tr>
<td className='label'>version</td>
<td className='label'>{t('version')}</td>
<td className='data'>{specVersion.toNumber()}</td>
</tr>
<tr>
<td className='label'>nonce</td>
<td className='label'>{t('nonce')}</td>
<td className='data'>{formatNumber(nonce)}</td>
</tr>
{!tip.isEmpty && (
<tr>
<td className='label'>tip</td>
<td className='label'>{t('tip')}</td>
<td className='data'>{formatNumber(tip)}</td>
</tr>
)}
{renderMethod(method, decoded)}
{renderMethod(method, decoded, t)}
<tr>
<td className='label'>lifetime</td>
<td className='data'>{mortalityAsString(era, blockNumber)}</td>
<td className='data'>{mortalityAsString(era, blockNumber, t)}</td>
</tr>
</Table>
);

View File

@ -10,6 +10,7 @@ import styled from 'styled-components';
import { QrDisplayPayload, QrScanSignature } from '@polkadot/react-qr';
import { Button } from '../../components';
import useTranslation from '../../hooks/useTranslation';
interface Props {
children?: React.ReactNode;
@ -22,6 +23,7 @@ interface Props {
const CMD_MORTAL = 2;
function Qr ({ className, onSignature, payload, request }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isScanning, setIsScanning] = useState(false);
const [payloadU8a, setPayloadU8a] = useState(new Uint8Array());
@ -49,7 +51,7 @@ function Qr ({ className, onSignature, payload, request }: Props): React.ReactEl
className='scanButton'
onClick={_onShowQr}
>
Scan signature via camera
{t('Scan signature via camera')}
</Button>
)}
</div>

View File

@ -10,6 +10,7 @@ import React, { useCallback, useContext, useState, useEffect } from 'react';
import { TypeRegistry } from '@polkadot/types';
import { ActionBar, ActionContext, Address, Button, ButtonArea, Checkbox, Link, VerticalSpace } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { approveSignPassword, approveSignSignature, cancelSignRequest, isSignLocked } from '../../messaging';
import Bytes from './Bytes';
import Extrinsic from './Extrinsic';
@ -39,6 +40,7 @@ function isRawPayload (payload: SignerPayloadJSON | SignerPayloadRaw): payload i
}
export default function Request ({ account: { isExternal }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null });
const [error, setError] = useState<string | null>(null);
@ -123,7 +125,7 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
>
<Checkbox
checked={!!isSavedPass}
label="Don't ask me again for the next 15 minutes"
label={t("Don't ask me again for the next 15 minutes")}
onChange={setIsSavedPass}
/>
</Unlock>
@ -172,7 +174,7 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
isDanger
onClick={_onCancel}
>
Cancel
{t('Cancel')}
</Link>
</CancelButton>
</SignArea>
@ -198,7 +200,7 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
isDanger
onClick={_onCancel}
>
Reject
{t('Reject')}
</Link>
</CancelButton>
</SignArea>

View File

@ -5,6 +5,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Button, InputWithLabel } from '../../components';
import useTranslation from '../../hooks/useTranslation';
interface Props {
buttonText: string;
@ -16,6 +17,7 @@ interface Props {
}
function Unlock ({ buttonText, children, className, error, isBusy, onSign }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [ownError, setError] = useState<string | null>();
const [password, setPassword] = useState('');
@ -42,7 +44,7 @@ function Unlock ({ buttonText, children, className, error, isBusy, onSign }: Pro
disabled={isBusy}
isError={!password || !!ownError}
isFocused
label='Password for this account'
label={t('Password for this account')}
onChange={_onChangePassword}
onEnter={_onClick}
type='password'

View File

@ -7,11 +7,13 @@ import { SignerPayloadJSON } from '@polkadot/types/types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { Loading, SigningReqContext } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { Header } from '../../partials';
import Request from './Request';
import TransactionIndex from './TransactionIndex';
export default function Signing (): React.ReactElement {
const { t } = useTranslation();
const requests = useContext(SigningReqContext);
const [requestIndex, setRequestIndex] = useState(0);
@ -46,7 +48,7 @@ export default function Signing (): React.ReactElement {
return request
? (
<>
<Header text={isTransaction ? 'Transaction' : 'Sign message'}>
<Header text={isTransaction ? t('Transaction') : t('Sign message')}>
{requests.length > 1 && (
<TransactionIndex
index={requestIndex}
@ -58,7 +60,7 @@ export default function Signing (): React.ReactElement {
</Header>
<Request
account={request.account}
buttonText={isTransaction ? 'Sign the transaction' : 'Sign the message'}
buttonText={isTransaction ? t('Sign the transaction') : t('Sign the message')}
isFirst={requestIndex === 0}
request={request.request}
signId={request.id}

View File

@ -8,9 +8,11 @@ import React, { useContext } from 'react';
import styled from 'styled-components';
import { ActionContext, Box, Button, ButtonArea, List, VerticalSpace } from '../components';
import useTranslation from '../hooks/useTranslation';
import { Header } from '../partials';
export default function Welcome (): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const _onClick = (): void => {
@ -20,19 +22,19 @@ export default function Welcome (): React.ReactElement<Props> {
return (
<>
<Header text='Welcome'/>
<Note>Before we start, just a couple of notes regarding use:</Note>
<Header text={t('Welcome')} />
<Note>{t('Before we start, just a couple of notes regarding use:')}</Note>
<TextBox>
<List>
<li>We do not send any clicks, pageviews or events to a central server</li>
<li>We do not use any trackers or analytics</li>
<li>We don&apos;t collect keys, addresses or any information - your information never leaves this machine</li>
<li>{t('We do not send any clicks, pageviews or events to a central server')}</li>
<li>{t('We do not use any trackers or analytics')}</li>
<li>{t("We don't collect keys, addresses or any information - your information never leaves this machine")}</li>
</List>
</TextBox>
<Note>... we are not in the information collection business (even anonymized).</Note>
<Note>{t('... we are not in the information collection business (even anonymized).')}</Note>
<VerticalSpace />
<ButtonArea>
<Button onClick={_onClick}>Understood, let me continue</Button>
<Button onClick={_onClick}>{t('Understood, let me continue')}</Button>
</ButtonArea>
</>
);

View File

@ -19,6 +19,7 @@ import parentArrow from '../assets/arrowParentLabel.svg';
import { AccountContext, SettingsContext } from './contexts';
import useOutsideClick from '../hooks/useOutsideClick';
import useMetadata from '../hooks/useMetadata';
import useTranslation from '../hooks/useTranslation';
import useToast from '../hooks/useToast';
import Identicon from './Identicon';
import Menu from './Menu';
@ -72,6 +73,7 @@ function recodeAddress (address: string, accounts: AccountWithChildren[], chain:
const ACCOUNTS_SCREEN_HEIGHT = 550;
function Address ({ actions, address, children, className, genesisHash, isHidden, name, parentName, suri, toggleActions }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { accounts } = useContext(AccountContext);
const settings = useContext(SettingsContext);
const chain = useMetadata(genesisHash, true);
@ -107,9 +109,9 @@ function Address ({ actions, address, children, className, genesisHash, isHidden
const theme = ((chain && chain.icon) || 'polkadot') as 'polkadot';
const _onClick = useCallback((): void => setShowActionsMenu(!showActionsMenu), [showActionsMenu]);
const _onCopy = useCallback((): void => show('Copied'), [show]);
const _onCopy = useCallback((): void => show(t('Copied')), [show, t]);
const displayedName = name || (account && account.name) || '<unknown>';
const displayedName = name || (account && account.name) || t('<unknown>');
return (
<div className={className}>
@ -160,7 +162,7 @@ function Address ({ actions, address, children, className, genesisHash, isHidden
</div>
)}
<div className='addressDisplay'>
<FullAddress data-field='address'>{formatted || '<unknown>'}</FullAddress>
<FullAddress data-field='address'>{formatted || t('<unknown>')}</FullAddress>
<CopyToClipboard text={(formatted && formatted) || ''} >
<Svg
onClick={_onCopy}

View File

@ -9,6 +9,7 @@ import Dropzone, { DropzoneRef } from 'react-dropzone';
import styled from 'styled-components';
import { formatNumber, isHex, u8aToString, hexToU8a } from '@polkadot/util';
import useTranslation from '../hooks/useTranslation';
import Label from './Label';
function classes (...classNames: (boolean | null | string | undefined)[]): string {
@ -59,6 +60,7 @@ function convertResult (result: ArrayBuffer, convertHex?: boolean): Uint8Array {
}
function InputFile ({ accept, className = '', clearContent, convertHex, isDisabled, isError = false, label, onChange, placeholder }: InputFileProps): React.ReactElement<InputFileProps> {
const { t } = useTranslation();
const dropRef = createRef<DropzoneRef>();
const [file, setFile] = useState<FileState | undefined>();
@ -103,8 +105,13 @@ function InputFile ({ accept, className = '', clearContent, convertHex, isDisabl
<em className='label' >
{
!file || clearContent
? placeholder || 'click to select or drag and drop the file here'
: placeholder || `${file.name} (${formatNumber(file.size)} bytes)`
? placeholder || t('click to select or drag and drop the file here')
: placeholder || t('{{name}} ({{size}} bytes)', {
replace: {
name: file.name,
size: formatNumber(file.size)
}
})
}
</em>
</div>

View File

@ -4,14 +4,18 @@
import React from 'react';
import useTranslation from '../hooks/useTranslation';
interface Props {
children?: React.ReactNode;
}
export default function Loading ({ children }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
if (!children) {
return (
<div>... loading ...</div>
<div>{t('... loading ...')}</div>
);
}

View File

@ -8,6 +8,7 @@ import React, { MouseEventHandler } from 'react';
import styled from 'styled-components';
import copy from '../assets/copy.svg';
import useTranslation from '../hooks/useTranslation';
import ActionText from './ActionText';
import TextAreaWithLabel from './TextAreaWithLabel';
@ -18,12 +19,14 @@ interface Props {
}
function MnemonicSeed ({ className, onCopy, seed }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<div className={className}>
<TextAreaWithLabel
className='mnemonicDisplay'
isReadOnly
label='Generated 12-word mnemonic seed:'
label={t('Generated 12-word mnemonic seed:')}
value={seed}
/>
<div className='buttonsRow'>
@ -32,7 +35,7 @@ function MnemonicSeed ({ className, onCopy, seed }: Props): React.ReactElement<P
data-seed-action='copy'
icon={copy}
onClick={onCopy}
text='Copy to clipboard'
text={t('Copy to clipboard')}
/>
</div>
</div>

View File

@ -0,0 +1,20 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
interface TranslationOptions {
replace: Record<string, string | number>
}
interface Translation {
t: (key: string, options?: TranslationOptions) => string;
}
const defaultTranslation: Translation = {
t: (key: string, { replace }: TranslationOptions = { replace: {} }) =>
Object.entries(replace).reduce((result: string, [rk, rv]) => result.replace(`{{${rk}}}`, `${rv}`), key)
};
export default function useTranslation (): Translation {
return defaultTranslation;
}

View File

@ -8,6 +8,7 @@ import React from 'react';
import styled from 'styled-components';
import ArrowLeftImage from '../assets/arrowLeft.svg';
import useTranslation from '../hooks/useTranslation';
import { Link, Svg } from '../components';
interface Props extends ThemeProps {
@ -16,11 +17,13 @@ interface Props extends ThemeProps {
}
function Back ({ className, to = '/' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<div className={className}>
<BackLink to={to}>
<ArrowLeft />
Back
{t('Back')}
</BackLink>
</div>
);

View File

@ -55,6 +55,7 @@ export default React.memo(styled(HeaderWithSteps)(({ theme }: Props) => `
display: flex;
justify-content: space-between;
flex-grow: 1;
padding-left: 1em;
padding-right: 24px;
margin-top: 3px;
}

View File

@ -12,6 +12,7 @@ import plusIcon from '../assets/plus.svg';
import qrIcon from '../assets/qr.svg';
import seedIcon from '../assets/secret.svg';
import { AccountContext, Link, MediaContext, Menu, MenuDivider, MenuItem, Svg } from '../components';
import useTranslation from '../hooks/useTranslation';
import { jsonRestoreWindowOpen } from '../messaging';
interface Props extends ThemeProps {
@ -22,6 +23,7 @@ interface Props extends ThemeProps {
const isPopup = window.innerWidth <= 480;
function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { master } = useContext(AccountContext);
const mediaAllowed = useContext(MediaContext);
@ -39,8 +41,8 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
<Svg src={plusIcon} />
<span>{
master
? 'Create new account (root or derived)'
: 'Create new account'
? t('Create new account (root or derived)')
: t('Create new account')
}</span>
</Link>
</MenuItem>
@ -48,7 +50,7 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
<MenuItem className='menuItem'>
<Link to='/account/import-seed'>
<Svg src={seedIcon} />
<span>Import account from pre-existing seed</span>
<span>{t('Import account from pre-existing seed')}</span>
</Link>
</MenuItem>
<MenuItem className='menuItem'>
@ -57,7 +59,7 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
to={isPopup ? undefined : '/account/restore-json'}
>
<Svg src={fileIcon} />
<span>Restore account from backup JSON file</span>
<span>{t('Restore account from backup JSON file')}</span>
</Link>
</MenuItem>
<MenuDivider />
@ -67,7 +69,7 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
to='/account/import-qr'
>
<Svg src={qrIcon} />
<span>Attach external QR-signer account</span>
<span>{t('Attach external QR-signer account')}</span>
</Link>
</MenuItem>
</Menu>

View File

@ -10,6 +10,7 @@ import settings from '@polkadot/ui-settings';
import FullScreenIcon from '../assets/fullscreen.svg';
import { ActionText, Checkbox, Dropdown, Menu, MenuDivider, MenuItem, Svg, Switch, ThemeSwitchContext, themes } from '../components';
import useTranslation from '../hooks/useTranslation';
import { windowOpen } from '../messaging';
interface Option {
@ -22,12 +23,13 @@ interface Props extends ThemeProps {
reference: React.MutableRefObject<null>;
}
const isPopup = window.innerWidth <= 480;
const isPopup = window.innerWidth <= 560;
const prefixOptions = settings.availablePrefixes
.filter(({ value }) => value !== -1)
.map(({ text, value }): Option => ({ text, value: `${value}` }));
function MenuSettings ({ className, reference }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [camera, setCamera] = useState(settings.camera === 'on');
const [prefix, setPrefix] = useState(`${settings.prefix === -1 ? 42 : settings.prefix}`);
const themeContext = useContext<Theme>(ThemeContext);
@ -69,19 +71,19 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement<Prop
<MenuDivider />
<MenuItem
className='setting'
title='External QR accounts and Access'
title={t('External QR accounts and Access')}
>
<Checkbox
checked={camera}
className='checkbox'
label='Allow Camera Access'
label={t('Allow Camera Access')}
onChange={setCamera}
/>
</MenuItem>
<MenuDivider />
<MenuItem
className='setting'
title='Display address format For'
title={t('Display address format For')}
>
<Dropdown
className='dropdown'
@ -99,7 +101,7 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement<Prop
className='openWindow'
icon={FullScreenIcon}
onClick={windowOpen}
text='Open extension in new window'
text={t('Open extension in new window')}
/>
</MenuItem>
</>

View File

@ -5,6 +5,7 @@
import React, { useContext } from 'react';
import { AccountContext, InputWithLabel, ValidatedInput } from '../components';
import useTranslation from '../hooks/useTranslation';
import { isNotShorterThan } from '../util/validators';
interface Props {
@ -16,11 +17,14 @@ interface Props {
onChange: (name: string | null) => void;
}
// FIXME i18n
const isNameValid = isNotShorterThan(3, 'Account name is too short');
export default function Name ({ address, className, isFocused, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement<Props> {
export default function Name ({ address, className, isFocused, label, onBlur, onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { accounts } = useContext(AccountContext);
const account = accounts.find((account): boolean => account.address === address);
const account = accounts.find((account) => account.address === address);
const startValue = account && account.name;
return (
@ -30,7 +34,7 @@ export default function Name ({ address, className, isFocused, label = 'A descri
data-input-name
defaultValue={startValue}
isFocused={isFocused}
label={label}
label={label || t('A descriptive name for your account')}
onBlur={onBlur}
onValidatedChange={onChange}
type='text'

View File

@ -5,6 +5,7 @@
import React, { useState, useEffect } from 'react';
import { InputWithLabel, ValidatedInput } from '../components';
import useTranslation from '../hooks/useTranslation';
import { allOf, isNotShorterThan, isSameAs, Validator } from '../util/validators';
interface Props {
@ -13,6 +14,8 @@ interface Props {
}
const MIN_LENGTH = 6;
// FIXME i18n
const isFirstPasswordValid = isNotShorterThan(MIN_LENGTH, 'Password is too short');
const isSecondPasswordValid = (firstPassword: string): Validator<string> => allOf(
isNotShorterThan(MIN_LENGTH, 'Password is too short'),
@ -20,6 +23,7 @@ const isSecondPasswordValid = (firstPassword: string): Validator<string> => allO
);
export default function Password ({ isFocussed, onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [pass1, setPass1] = useState<string | null>(null);
const [pass2, setPass2] = useState<string | null>(null);
@ -33,7 +37,7 @@ export default function Password ({ isFocussed, onChange }: Props): React.ReactE
component={InputWithLabel}
data-input-password
isFocused={isFocussed}
label='A new password for this account'
label={t('A new password for this account')}
onValidatedChange={setPass1}
type='password'
validator={isFirstPasswordValid}
@ -42,7 +46,7 @@ export default function Password ({ isFocussed, onChange }: Props): React.ReactE
<ValidatedInput
component={InputWithLabel}
data-input-repeat-password
label='Repeat password for verification'
label={t('Repeat password for verification')}
onValidatedChange={setPass2}
type='password'
validator={isSecondPasswordValid(pass1)}

View File

@ -0,0 +1,98 @@
{
"Copied": "",
"<unknown>": "",
"click to select or drag and drop the file here": "",
"{{name}} ({{size}} bytes)": "",
"... loading ...": "",
"Generated 12-word mnemonic seed:": "",
"Copy to clipboard": "",
"Back": "",
"Create new account (root or derived)": "",
"Create new account": "",
"Import account from pre-existing seed": "",
"Restore account from backup JSON file": "",
"Attach external QR-signer account": "",
"External QR accounts and Access": "",
"Allow Camera Access": "",
"Display address format For": "",
"Open extension in new window": "",
"A descriptive name for your account": "",
"A new password for this account": "",
"Repeat password for verification": "",
"Rename": "",
"Derive New Account": "",
"Export Account": "",
"Forget Account": "",
"Visible (always inject)": "",
"You currently don&apos;t have any accounts. Create your first account to get started.": "",
"Accounts": "",
"Authorize": "",
"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": "",
"Add the account with the generated seed": "",
"Cancel": "",
"Create an account": "",
"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.": "",
"I have saved my mnemonic seed safely.": "",
"Next step": "",
"Derivation Path": "",
"Derivation Path (unlock to edit)": "",
"//hard/soft": "",
"Add new account": "",
"Create derived account": "",
"Derive new account from existing": "",
"Choose Parent Account:": "",
"enter the password for the account you want to derive from": "",
"Create a derived account": "",
"Create account from new seed": "",
"Export account": "",
"You are exporting your account. Keep it safe and don&apos;t share it with anyone.": "",
"password for this account": "",
"I want to export this account": "",
"Forget account": "",
"You are about to remove the account. This means that you will not be able to access it via this extension anymore. If you wish to recover it, you would need to use the seed.": "",
"I want to forget this account": "",
"Scan Address Qr": "",
"Add the account with identified address": "",
"Import account": "",
"existing 12 or 24-word mnemonic seed": "",
"Add the account with the supplied seed": "",
"Metadata": "",
"from": "",
"chain": "",
"icon": "",
"decimals": "",
"symbol": "",
"upgrade": "",
"This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.": "",
"Yes, do this metadata update": "",
"Reject": "",
"Restore from JSON": "",
"backup file": "",
"Password for this file": "",
"Restore": "",
"bytes": "",
"method data": "",
"method": "",
"info": "",
"immortal": "",
"mortal, valid from {{birth}} to {{death}}": "",
"genesis": "",
"version": "",
"nonce": "",
"tip": "",
"Transaction": "",
"Sign message": "",
"Sign the transaction": "",
"Sign the message": "",
"Scan signature via camera": "",
"Don't ask me again for the next 15 minutes": "",
"Password for this account": "",
"Welcome": "",
"Before we start, just a couple of notes regarding use:": "",
"We do not send any clicks, pageviews or events to a central server": "",
"We do not use any trackers or analytics": "",
"We don't collect keys, addresses or any information - your information never leaves this machine": "",
"... we are not in the information collection business (even anonymized).": "",
"Understood, let me continue": ""
}

676
yarn.lock

File diff suppressed because it is too large Load Diff