diff --git a/package.json b/package.json index 43de4fba5..b4c07d296 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "preelectron": "yarn build", "electron": "cd packages/fether-electron && yarn electron", "fetch-parity": "cd scripts && node ./fetch-latest-parity.js", - "lint-files": "./scripts/lint-files.sh '**/*.js'", + "lint-files": "FILES='**/*.js' ./scripts/lint-files.sh $FILES", "lint": "yarn lint-files", "prepackage": "yarn build", "package": "cd packages/fether-electron && yarn package", @@ -61,7 +61,7 @@ }, "husky": { "hooks": { - "pre-commit": "FILES=`git diff --staged --name-only --diff-filter=d HEAD | grep -E '.js$|.ts$'`; [ -z \"$FILES\" ] && exit 0; yarn lint-files $FILES; git add $FILES" + "pre-commit": "FILES=`git diff --staged --name-only --diff-filter=d HEAD | grep -E '.js$'`; [ -z \"$FILES\" ] && exit 0; yarn lint-files $FILES; git add $FILES" } }, "devDependencies": { diff --git a/packages/fether-electron/package.json b/packages/fether-electron/package.json index c516696f8..d658f4f63 100644 --- a/packages/fether-electron/package.json +++ b/packages/fether-electron/package.json @@ -48,6 +48,8 @@ "electron-positioner": "^4.1.0", "electron-settings": "^3.2.0", "fether-react": "^0.4.0", + "i18next": "^15.0.4", + "i18next-node-fs-backend": "^2.0.0", "pino": "^4.16.1", "pino-multi-stream": "^3.1.2", "source-map-support": "^0.5.10", diff --git a/packages/fether-electron/src/main/app/menu/i18n/index.js b/packages/fether-electron/src/main/app/menu/i18n/index.js new file mode 100644 index 000000000..83c21c516 --- /dev/null +++ b/packages/fether-electron/src/main/app/menu/i18n/index.js @@ -0,0 +1,82 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import i18next from 'i18next'; +import Backend from 'i18next-node-fs-backend'; +import electron from 'electron'; +import settings from 'electron-settings'; + +import { name } from '../../../../../package.json'; +import Pino from '../../utils/pino'; +import { en } from './locales'; + +let { app } = electron; +const pino = Pino(); +let resourceEnglishNS = {}; +resourceEnglishNS[name] = en; +const packageNS = Object.keys(resourceEnglishNS)[0].toString(); +const moduleNS = 'i18n'; +const menuNS = `${packageNS}-${moduleNS}`; + +const i18n = i18next; +i18n + .use(Backend) + .init({ + debug: true, + defaultNS: packageNS, + fallbackLng: ['en-US', 'en'], + interpolation: { + escapeValue: false + }, + lng: settings.get('fether-language') || 'en', + ns: [packageNS], + resources: { + en: resourceEnglishNS + }, + saveMissing: true + }) + .then(() => pino.info(`${menuNS}: success`)) + .catch(error => pino.info(`${menuNS}: failure`, error)); + +// FIXME i18n - convert all text below to i18n + +// https://www.i18next.com/overview/api#changelanguage +i18n.changeLanguage(app.getLocale(), (err, t) => { + if (err) { + pino.info(`${menuNS}: Error loading language ${app.getLocale()}`, err); + } +}); + +i18next.on('initialized', options => { + pino.debug(`${menuNS}: Detected initialisation of i18n`); +}); + +i18next.on('loaded', loaded => { + pino.info(`${menuNS}: Detected success loading resources: `, loaded); +}); + +i18next.on('failedLoading', (lng, ns, msg) => { + pino.info(`${menuNS}: Detected failure loading resources: `, lng, ns, msg); +}); + +// saveMissing must be configured to `true` +i18next.on('missingKey', (lngs, namespace, key, res) => { + pino.info(`${menuNS}: Detected missing key: `, lngs, namespace, key, res); +}); + +i18next.store.on('added', (lng, ns) => { + pino.debug(`${menuNS}: Detected resources added: `, lng, ns); +}); + +i18next.store.on('removed', (lng, ns) => { + pino.debug(`${menuNS}: Detected resources removed: `, lng, ns); +}); + +// https://www.i18next.com/overview/api#changelanguage +i18next.on('languageChanged', lng => { + pino.info(`${menuNS}: Detected language change to: `, lng); +}); + +export default i18n; diff --git a/packages/fether-electron/src/main/app/menu/i18n/locales/en.json b/packages/fether-electron/src/main/app/menu/i18n/locales/en.json new file mode 100644 index 000000000..77a62e0f1 --- /dev/null +++ b/packages/fether-electron/src/main/app/menu/i18n/locales/en.json @@ -0,0 +1,55 @@ +{ + "menu": { + "file": { + "submenu_name": "File", + "about": "About", + "preferences": { + "submenu_name": "Preferences", + "languages": { + "submenu_name": "Languages", + "language": "Language", + "english": "English", + "german": "German" + } + }, + "services": "Services", + "hide": "Hide", + "hide_others": "Hide Others", + "unhide": "Unhide", + "quit": "Quit" + }, + "edit": { + "submenu_name": "Edit", + "undo": "Undo", + "redo": "Redo", + "cut": "Cut", + "copy": "Copy", + "paste": "Paste", + "delete": "Delete", + "select_all": "Select All", + "speech": { + "submenu_name": "Speech", + "start_speaking": "Start Speaking", + "stop_speaking": "Stop Speaking" + } + }, + "view": { + "submenu_name": "View", + "reload": "Reload", + "toggle_developer_tools": "Toggle Developer Tools" + }, + "window": { + "submenu_name": "Window", + "close": "Close", + "minimize": "Minimize", + "zoom": "Zoom", + "bring_all_to_front": "Bring All to Front" + }, + "help": { + "submenu_name": "Help", + "search": "Search", + "learn_more": "Learn More" + }, + "show_hide_fether": "Show/Hide Fether" + } +} diff --git a/packages/fether-electron/src/main/app/menu/i18n/locales/index.js b/packages/fether-electron/src/main/app/menu/i18n/locales/index.js new file mode 100644 index 000000000..8ae0b0f9b --- /dev/null +++ b/packages/fether-electron/src/main/app/menu/i18n/locales/index.js @@ -0,0 +1,3 @@ +import en from './en.json'; + +export { en }; diff --git a/packages/fether-electron/src/main/app/menu/template/index.js b/packages/fether-electron/src/main/app/menu/template/index.js index 9377470cf..ed0f945ff 100644 --- a/packages/fether-electron/src/main/app/menu/template/index.js +++ b/packages/fether-electron/src/main/app/menu/template/index.js @@ -4,10 +4,35 @@ // SPDX-License-Identifier: BSD-3-Clause import electron from 'electron'; +import settings from 'electron-settings'; + import { IS_PROD } from '../../constants'; +import i18n from '../i18n'; const { shell } = electron; +// Preferences menu +const getPreferences = fetherApp => { + return { + label: i18n.t('menu.file.preferences.languages.submenu_name'), + submenu: [ + { + label: i18n.t('menu.file.preferences.languages.english'), + type: 'radio', + checked: settings.get('fether-language') === 'en', + click () { + // Backend menu change language + i18n.changeLanguage('en'); + settings.set('fether-language', 'en'); + fetherApp.setupMenu(fetherApp); + // Frontend change language + fetherApp.win.webContents.emit('set-language', 'en'); + } + } + ] + }; +}; + // Create the Application's main menu // https://github.com/electron/electron/blob/master/docs/api/menu.md#examples const getMenubarMenuTemplate = fetherApp => { @@ -15,37 +40,46 @@ const getMenubarMenuTemplate = fetherApp => { const fileTab = process.platform === 'darwin' ? { - label: 'File', + label: i18n.t('menu.file.submenu_name'), submenu: [ - { role: 'about' }, + { role: 'about', label: i18n.t('menu.file.about') }, { type: 'separator' }, - { role: 'services', submenu: [] }, + { + label: i18n.t('menu.file.preferences.submenu_name'), + submenu: [getPreferences(fetherApp)] + }, { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { role: 'unhide' }, + { + role: 'services', + label: i18n.t('menu.file.services'), + submenu: [] + }, { type: 'separator' }, - { role: 'quit' } + { role: 'hide', label: i18n.t('menu.file.hide') }, + { role: 'hideothers', label: i18n.t('menu.file.hide_others') }, + { role: 'unhide', label: i18n.t('menu.file.unhide') }, + { type: 'separator' }, + { role: 'quit', label: i18n.t('menu.file.quit') } ] } : { - label: 'File', - submenu: [{ role: 'quit' }] + label: i18n.t('menu.file.submenu_name'), + submenu: [{ role: 'quit', label: i18n.t('menu.file.quit') }] }; /* eslint-disable no-sparse-arrays */ const editTabMacOS = { - label: 'Edit', + label: i18n.t('menu.edit.submenu_name'), submenu: [ - { role: 'undo' }, - { role: 'redo' }, + { role: 'undo', label: i18n.t('menu.edit.undo') }, + { role: 'redo', label: i18n.t('menu.edit.redo') }, { type: 'separator' }, - { role: 'cut' },, - { role: 'copy' }, - { role: 'paste' }, + { role: 'cut', label: i18n.t('menu.edit.cut') }, + { role: 'copy', label: i18n.t('menu.edit.copy') }, + { role: 'paste', label: i18n.t('menu.edit.paste') }, { type: 'separator' }, - { role: 'delete' }, - { role: 'selectall' } + { role: 'delete', label: i18n.t('menu.edit.delete') }, + { role: 'selectall', label: i18n.t('menu.edit.select_all') } ] }; /* eslint-enable no-sparse-arrays */ @@ -60,26 +94,50 @@ const getMenubarMenuTemplate = fetherApp => { * it to prevent code duplication */ const editTab = { - label: 'Edit', + label: i18n.t('menu.edit.submenu_name'), submenu: [ - { label: 'Undo', click: () => fetherApp.win.webContents.undo() }, - { label: 'Redo', click: () => fetherApp.win.webContents.redo() }, + { + label: i18n.t('menu.edit.undo'), + click: () => fetherApp.win.webContents.undo() + }, + { + label: i18n.t('menu.edit.redo'), + click: () => fetherApp.win.webContents.redo() + }, { type: 'separator' }, - { label: 'Cut', click: () => fetherApp.win.webContents.cut() }, - { label: 'Copy', click: () => fetherApp.win.webContents.copy() }, - { label: 'Paste', click: () => fetherApp.win.webContents.paste() }, + { + label: i18n.t('menu.edit.cut'), + click: () => fetherApp.win.webContents.cut() + }, + { + label: i18n.t('menu.edit.copy'), + click: () => fetherApp.win.webContents.copy() + }, + { + label: i18n.t('menu.edit.paste'), + click: () => fetherApp.win.webContents.paste() + }, { type: 'separator' }, - { label: 'Delete', click: () => fetherApp.win.webContents.delete() }, { - label: 'Select All', + label: i18n.t('menu.edit.delete'), + click: () => fetherApp.win.webContents.delete() + }, + { + label: i18n.t('menu.edit.select_all'), click: () => fetherApp.win.webContents.selectAll() } ] }; const viewTab = { - label: 'View', - submenu: [{ role: 'reload' }, { role: 'toggledevtools' }] + label: i18n.t('menu.view.submenu_name'), + submenu: [ + { role: 'reload', label: i18n.t('menu.view.reload') }, + { + role: 'toggledevtools', + label: i18n.t('menu.view.toggle_developer_tools') + } + ] }; /** @@ -90,11 +148,14 @@ const getMenubarMenuTemplate = fetherApp => { * add no benefit to users anyway */ const viewTabWindowsOS = { - label: 'View', + label: i18n.t('menu.view.submenu_name'), submenu: [ - { label: 'Reload', click: () => fetherApp.win.webContents.reload() }, { - label: 'Toggle Developer Tools', + label: i18n.t('menu.view.reload'), + click: () => fetherApp.win.webContents.reload() + }, + { + label: i18n.t('menu.view.toggle_developer_tools'), click: () => fetherApp.win.webContents.toggleDevTools() } ] @@ -102,14 +163,19 @@ const getMenubarMenuTemplate = fetherApp => { const windowTab = { role: 'window', - submenu: [{ role: 'minimize' }, { role: 'close' }] + label: i18n.t('menu.window.submenu_name'), + submenu: [ + { role: 'minimize', label: i18n.t('menu.window.minimize') }, + { role: 'close', label: i18n.t('menu.window.close') } + ] }; const helpTab = { role: 'help', + label: i18n.t('menu.help.submenu_name'), submenu: [ { - label: 'Learn More', + label: i18n.t('menu.help.learn_more'), click () { shell.openExternal('https://parity.io'); } @@ -130,8 +196,17 @@ const getMenubarMenuTemplate = fetherApp => { template[1].submenu.push( { type: 'separator' }, { - label: 'Speech', - submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }] + label: i18n.t('menu.edit.speech.submenu_name'), + submenu: [ + { + role: 'startspeaking', + label: i18n.t('menu.edit.speech.start_speaking') + }, + { + role: 'stopspeaking', + label: i18n.t('menu.edit.speech.stop_speaking') + } + ] } ); } @@ -139,11 +214,11 @@ const getMenubarMenuTemplate = fetherApp => { if (process.platform === 'darwin') { // Window menu template[3].submenu = [ - { role: 'close' }, - { role: 'minimize' }, - { role: 'zoom' }, + { role: 'close', label: i18n.t('menu.window.close') }, + { role: 'minimize', label: i18n.t('menu.window.minimize') }, + { role: 'zoom', label: i18n.t('menu.window.zoom') }, { type: 'separator' }, - { role: 'front' } + { role: 'front', label: i18n.t('menu.window.bring_all_to_front') } ]; } @@ -159,7 +234,7 @@ const getContextTrayMenuTemplate = fetherApp => { if (fetherApp.options.withTaskbar) { const template = [ { - label: 'Show/Hide Fether', + label: i18n.t('menu.show_hide_fether'), click () { if (fetherApp.win.isVisible() && fetherApp.win.isFocused()) { fetherApp.win.hide(); @@ -173,12 +248,12 @@ const getContextTrayMenuTemplate = fetherApp => { if (!IS_PROD) { template.push({ - label: 'Reload', + label: i18n.t('menu.view.reload'), click: () => fetherApp.win.webContents.reload() }); } - template.push({ label: 'Quit', role: 'quit' }); + template.push({ role: 'quit', label: i18n.t('menu.file.quit') }); return template; } @@ -191,11 +266,16 @@ const getContextWindowMenuTemplate = fetherApp => { // Remove File and Help menus in taskbar mode for window context menu template.shift(); template.pop(); + template.push({ + label: i18n.t('menu.file.preferences.submenu_name'), + submenu: [getPreferences(fetherApp)] + }); template.push({ role: 'help', + label: i18n.t('menu.help.submenu_name'), submenu: [ { - label: 'Learn More', + label: i18n.t('menu.help.learn_more'), click () { shell.openExternal('https://parity.io'); } @@ -204,10 +284,13 @@ const getContextWindowMenuTemplate = fetherApp => { }); if (process.platform === 'darwin') { - template[2].submenu.push({ role: 'about' }); + template[3].submenu.push({ + role: 'about', + label: i18n.t('menu.file.about') + }); } - template.push({ label: 'Quit', role: 'quit' }); + template.push({ role: 'quit', label: i18n.t('menu.file.quit') }); } return template; diff --git a/packages/fether-electron/static/preload.js b/packages/fether-electron/static/preload.js index e32bb90be..4c6c00a81 100644 --- a/packages/fether-electron/static/preload.js +++ b/packages/fether-electron/static/preload.js @@ -41,6 +41,12 @@ function init () { * Example 2: `require` should not be defined in Chrome Developer Tools Console. */ window.bridge = { + currentWindowWebContentsAddListener: remote.getCurrentWindow().webContents + .addListener, + currentWindowWebContentsRemoveListener: remote.getCurrentWindow() + .webContents.removeListener, + currentWindowWebContentsReload: remote.getCurrentWindow().webContents + .reload, defaultWsInterface: remote.getGlobal('defaultWsInterface'), defaultWsPort: remote.getGlobal('defaultWsPort'), ipcRenderer, diff --git a/packages/fether-react/package.json b/packages/fether-react/package.json index 931d137c3..109b32d1e 100644 --- a/packages/fether-react/package.json +++ b/packages/fether-react/package.json @@ -54,16 +54,18 @@ "file-saver": "^2.0.0", "final-form": "^4.8.3", "final-form-calculate": "^1.2.1", + "i18next": "^15.0.4", "localforage": "^1.7.2", "localforage-observable": "^1.4.0", "lodash": "^4.17.10", "mobx": "^5.0.2", "mobx-react": "^5.2.3", - "react": "^16.3.2", + "react": "^16.8.3", "react-blockies": "^1.3.0", - "react-dom": "^16.3.2", + "react-dom": "^16.8.3", "react-final-form": "^3.6.4", "react-final-form-listeners": "^1.0.1", + "react-i18next": "^10.2.0", "react-markdown": "^3.3.4", "react-resize-detector": "^3.0.1", "react-router-dom": "^4.2.2", diff --git a/packages/fether-react/src/Accounts/AccountsList/AccountsList.js b/packages/fether-react/src/Accounts/AccountsList/AccountsList.js index 3c3fb2872..ec8f2c4e6 100644 --- a/packages/fether-react/src/Accounts/AccountsList/AccountsList.js +++ b/packages/fether-react/src/Accounts/AccountsList/AccountsList.js @@ -9,10 +9,11 @@ import { chainId$ } from '@parity/light.js'; import { inject, observer } from 'mobx-react'; import light from '@parity/light.js-react'; +import i18n, { packageNS } from '../../i18n'; import RequireHealthOverlay from '../../RequireHealthOverlay'; import Health from '../../Health'; -import Feedback from './Feedback'; import withAccountsInfo from '../../utils/withAccountsInfo'; +import Feedback from './Feedback'; @withAccountsInfo @inject('createAccountStore', 'parityStore') @@ -56,7 +57,7 @@ class AccountsList extends Component { onClick={this.handleCreateAccount} /> } - title={

Accounts

} + title={

{i18n.t(`${packageNS}:accounts_list.header`)}

} />
@@ -72,20 +73,25 @@ class AccountsList extends Component { ))} ) : (

- Nothing here yet! + {i18n.t(`${packageNS}:accounts_list.hint.none`)}

- Click the + icon to add a new account. + {i18n.t(`${packageNS}:accounts_list.hint.exist`)}

)}
diff --git a/packages/fether-react/src/Accounts/AccountsList/Feedback/Feedback.js b/packages/fether-react/src/Accounts/AccountsList/Feedback/Feedback.js index 84f6a3d7a..60f1beb8f 100644 --- a/packages/fether-react/src/Accounts/AccountsList/Feedback/Feedback.js +++ b/packages/fether-react/src/Accounts/AccountsList/Feedback/Feedback.js @@ -5,6 +5,8 @@ import React from 'react'; +import i18n, { packageNS } from '../../../i18n'; + export const Feedback = ({ accountsListLength }) => ( ( rel='noopener noreferrer' target='_blank' > - Feedback + {i18n.t(`${packageNS}:feedback:title`)} ); diff --git a/packages/fether-react/src/Accounts/CreateAccount/AccountCopyPhrase/AccountCopyPhrase.js b/packages/fether-react/src/Accounts/CreateAccount/AccountCopyPhrase/AccountCopyPhrase.js index 781e70932..2792b0074 100644 --- a/packages/fether-react/src/Accounts/CreateAccount/AccountCopyPhrase/AccountCopyPhrase.js +++ b/packages/fether-react/src/Accounts/CreateAccount/AccountCopyPhrase/AccountCopyPhrase.js @@ -7,6 +7,7 @@ import React, { Component } from 'react'; import { AccountCard } from 'fether-ui'; import { inject, observer } from 'mobx-react'; +import i18n, { packageNS } from '../../../i18n'; import RequireHealthOverlay from '../../../RequireHealthOverlay'; @inject('createAccountStore') @@ -39,20 +40,18 @@ class AccountCopyPhrase extends Component { drawers={[
-

Please write your secret phrase on a piece of paper:

+

{i18n.t(`${packageNS}:account.create.copy_phrase.msg1`)}

{bip39Phrase}
- Keep it secure and secret. + {i18n.t(`${packageNS}:account.create.copy_phrase.msg2`)}
  • - If you lose your secret phrase, your wallet cannot be - recovered. + {i18n.t(`${packageNS}:account.create.copy_phrase.msg3`)}
  • - If someone gets hold of your secret phrase, they will be - able to drain your account. + {i18n.t(`${packageNS}:account.create.copy_phrase.msg4`)}
@@ -64,15 +63,17 @@ class AccountCopyPhrase extends Component { onClick={history.goBack} type='button' > - Back + {i18n.t(`${packageNS}:navigation.back`)} )} ]} + i18n={i18n} + packageNS={packageNS} /> ); diff --git a/packages/fether-react/src/Accounts/CreateAccount/AccountImportOptions/AccountImportOptions.js b/packages/fether-react/src/Accounts/CreateAccount/AccountImportOptions/AccountImportOptions.js index b55663450..9885a21da 100644 --- a/packages/fether-react/src/Accounts/CreateAccount/AccountImportOptions/AccountImportOptions.js +++ b/packages/fether-react/src/Accounts/CreateAccount/AccountImportOptions/AccountImportOptions.js @@ -10,6 +10,7 @@ import { inject, observer } from 'mobx-react'; import RequireHealthOverlay from '../../../RequireHealthOverlay'; import Scanner from '../../../Scanner'; import withAccountsInfo from '../../../utils/withAccountsInfo'; +import i18n, { packageNS } from '../../../i18n'; @withAccountsInfo @inject('createAccountStore') @@ -55,8 +56,9 @@ class AccountImportOptions extends Component { } catch (error) { this.setState({ isLoading: false, - error: - 'The passphrase was not recognized. Please verify that you entered your passphrase correctly.' + error: i18n.t( + `${packageNS}:account.import.phrase.error_msg_submit_phrase` + ) }); console.error(error); } @@ -81,8 +83,7 @@ class AccountImportOptions extends Component { } catch (error) { this.setState({ isLoading: false, - error: - 'Invalid file. Please check this is your actual Parity backup JSON keyfile and try again.' + error: i18n.t(`${packageNS}:account.import.error_msg_change_json_file`) }); console.error(error); } @@ -94,7 +95,11 @@ class AccountImportOptions extends Component { } = this.props; if (!address || !chainIdString) { - this.setState({ error: 'Invalid QR code.' }); + this.setState({ + error: i18n.t( + `${packageNS}:account.import.signer.error_msg_signer_imported` + ) + }); return; } @@ -128,7 +133,12 @@ class AccountImportOptions extends Component { if (isExistingAddress) { this.setState({ isLoading: false, - error: `Account ${addressShort(addressForImport)} already listed` + error: i18n.t( + `${packageNS}:account.import.error_msg_existing_address`, + { + address: addressShort(addressForImport) + } + ) }); } @@ -147,11 +157,19 @@ class AccountImportOptions extends Component {
-

Recover from JSON Keyfile

+

+ {i18n.t( + `${packageNS}:account.import.json.label_msg_recover_json` + )} +

@@ -163,19 +181,27 @@ class AccountImportOptions extends Component {
-

Recover from Parity Signer

+

+ {i18n.t( + `${packageNS}:account.import.signer.label_msg_recover_signer` + )} +

{importingFromSigner ? ( ) : ( )}
@@ -187,11 +213,17 @@ class AccountImportOptions extends Component {
-

Recover from Seed Phrase

+

+ {i18n.t( + `${packageNS}:account.import.phrase.label_msg_recover_phrase` + )} +

{currentStep > 1 && ( )} @@ -236,7 +268,7 @@ class AccountImportOptions extends Component { disabled={(!json && !phrase) || isLoading} onClick={this.handleSubmitPhrase} > - Next + {i18n.t(`${packageNS}:navigation.next`)} ); }; diff --git a/packages/fether-react/src/Accounts/CreateAccount/AccountName/AccountName.js b/packages/fether-react/src/Accounts/CreateAccount/AccountName/AccountName.js index 1f0291d02..9aca7f30b 100644 --- a/packages/fether-react/src/Accounts/CreateAccount/AccountName/AccountName.js +++ b/packages/fether-react/src/Accounts/CreateAccount/AccountName/AccountName.js @@ -8,6 +8,7 @@ import { AccountCard, Card, Form as FetherForm } from 'fether-ui'; import Blockies from 'react-blockies'; import { inject, observer } from 'mobx-react'; +import i18n, { packageNS } from '../../../i18n'; import RequireHealthOverlay from '../../../RequireHealthOverlay'; import loading from '../../../assets/img/icons/loading.svg'; @@ -77,7 +78,9 @@ class AccountName extends Component { address={address} type={noPrivateKey ? 'signer' : 'node'} drawers={[this.renderDrawer()]} - name={name || '(no name)'} + name={name || i18n.t(`${packageNS}:account.existing.no_name`)} + i18n={i18n} + packageNS={packageNS} /> ); }; @@ -103,7 +106,7 @@ class AccountName extends Component {
@@ -121,13 +124,13 @@ class AccountName extends Component { const currentStep = pathname.slice(-1); return ( -
+
-

Please give this account a name:

+

{i18n.t(`${packageNS}:account.create.label_name_msg`)}

- Back + {i18n.t(`${packageNS}:navigation.back`)} )} {name && address ? ( - + ) : ( )} diff --git a/packages/fether-react/src/Accounts/CreateAccount/AccountPassword/AccountPassword.js b/packages/fether-react/src/Accounts/CreateAccount/AccountPassword/AccountPassword.js index 149dc16de..ff73988a3 100644 --- a/packages/fether-react/src/Accounts/CreateAccount/AccountPassword/AccountPassword.js +++ b/packages/fether-react/src/Accounts/CreateAccount/AccountPassword/AccountPassword.js @@ -7,6 +7,7 @@ import React, { Component } from 'react'; import { AccountCard, Form as FetherForm } from 'fether-ui'; import { inject, observer } from 'mobx-react'; +import i18n, { packageNS } from '../../../i18n'; import RequireHealthOverlay from '../../../RequireHealthOverlay'; @inject('createAccountStore') @@ -35,7 +36,9 @@ class AccountPassword extends Component { if (!createAccountStore.jsonString && confirm !== password) { this.setState({ - error: 'Password confirmation does not match.' + error: `${i18n.t( + `${packageNS}:account.password.create.error_msg_password_confirmation_no_match` + )}` }); return; } @@ -74,19 +77,25 @@ class AccountPassword extends Component { address={address} name={name} drawers={[ - +

{' '} {jsonString - ? 'Unlock your account to decrypt your JSON keystore file: ' - : 'Secure your account with a password:'} + ? i18n.t( + `${packageNS}:account.password.import.label_msg_unlock_json` + ) + : i18n.t( + `${packageNS}:account.password.create.label_msg_password` + )}

- {error && error + ' Please check your password and try again.'} + {error && + error + + ' ' + + i18n.t( + `${packageNS}:account.password.common.error_msg_password_incorrect` + )}

]} + i18n={i18n} + packageNS={packageNS} /> ); diff --git a/packages/fether-react/src/Accounts/CreateAccount/AccountRewritePhrase/AccountRewritePhrase.js b/packages/fether-react/src/Accounts/CreateAccount/AccountRewritePhrase/AccountRewritePhrase.js index 90fe20b67..90be26721 100644 --- a/packages/fether-react/src/Accounts/CreateAccount/AccountRewritePhrase/AccountRewritePhrase.js +++ b/packages/fether-react/src/Accounts/CreateAccount/AccountRewritePhrase/AccountRewritePhrase.js @@ -7,6 +7,7 @@ import React, { Component } from 'react'; import { AccountCard, Card, Form as FetherForm } from 'fether-ui'; import { inject, observer } from 'mobx-react'; +import i18n, { packageNS } from '../../../i18n'; import RequireHealthOverlay from '../../../RequireHealthOverlay'; import AccountImportOptions from '../AccountImportOptions'; @@ -55,8 +56,9 @@ class AccountRewritePhrase extends Component { ) : (

- Type your secret phrase to confirm that you wrote it down - correctly: + {i18n.t( + `${packageNS}:account.phrase_rewrite.label_msg_rewrite_phrase` + )}

)}
@@ -64,7 +66,9 @@ class AccountRewritePhrase extends Component { - Back + {i18n.t(`${packageNS}:navigation.back`)} )} {this.renderButton()} @@ -92,8 +96,14 @@ class AccountRewritePhrase extends Component { ) : ( )} @@ -111,7 +121,7 @@ class AccountRewritePhrase extends Component { if (!isImport) { return ( ); } @@ -119,7 +129,7 @@ class AccountRewritePhrase extends Component { // If we are importing an existing account, the button goes to the next step return ( ); }; diff --git a/packages/fether-react/src/Accounts/CreateAccount/CreateAccount.js b/packages/fether-react/src/Accounts/CreateAccount/CreateAccount.js index cc9b6890d..46046a811 100644 --- a/packages/fether-react/src/Accounts/CreateAccount/CreateAccount.js +++ b/packages/fether-react/src/Accounts/CreateAccount/CreateAccount.js @@ -14,6 +14,7 @@ import AccountRewritePhrase from './AccountRewritePhrase'; import AccountName from './AccountName'; import AccountPassword from './AccountPassword'; import Health from '../../Health'; +import i18n, { packageNS } from '../../i18n'; import withAccountsInfo from '../../utils/withAccountsInfo'; @inject('createAccountStore') @@ -75,7 +76,11 @@ class CreateAccount extends Component { ) } title={ -

{isImport ? 'Import account' : 'Create a new account'}

+

+ {isImport + ? i18n.t(`${packageNS}:account.import.title`) + : i18n.t(`${packageNS}:account.create.title`)} +

} /> @@ -100,12 +105,12 @@ class CreateAccount extends Component {
{isImport ? null : (

- Already have an account? + {i18n.t(`${packageNS}:account.import.question`)}

)} diff --git a/packages/fether-react/src/App/App.js b/packages/fether-react/src/App/App.js index fa98fe21b..f0ab50e1f 100644 --- a/packages/fether-react/src/App/App.js +++ b/packages/fether-react/src/App/App.js @@ -12,10 +12,12 @@ import { Switch } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; +import store from 'store'; import { Modal } from 'fether-ui'; import semver from 'semver'; import { version } from '../../package.json'; +import i18n, { packageNS } from '../i18n'; import Accounts from '../Accounts'; import BackupAccount from '../BackupAccount'; import Onboarding from '../Onboarding'; @@ -25,10 +27,17 @@ import Send from '../Send'; import Tokens from '../Tokens'; import Whitelist from '../Whitelist'; +const LANG_LS_KEY = 'fether-language'; const currentVersion = version; // The preload scripts injects `ipcRenderer` into `window.bridge` -const { ipcRenderer, IS_PROD } = window.bridge; +const { + currentWindowWebContentsAddListener, + currentWindowWebContentsReload, + currentWindowWebContentsRemoveListener, + ipcRenderer, + IS_PROD +} = window.bridge; // Use MemoryRouter for production viewing in file:// protocol // https://github.com/facebook/create-react-app/issues/3591 @@ -42,6 +51,16 @@ class App extends Component { }; componentDidMount () { + if (store.get(LANG_LS_KEY) && i18n.language !== store.get(LANG_LS_KEY)) { + i18n.changeLanguage(store.get(LANG_LS_KEY)); + } + + currentWindowWebContentsAddListener('set-language', newLanguage => { + i18n.changeLanguage(newLanguage); + store.set(LANG_LS_KEY, newLanguage); + currentWindowWebContentsReload(); + }); + window.addEventListener('contextmenu', this.handleRightClick); window @@ -66,6 +85,7 @@ class App extends Component { componentWillUnmount () { window.removeEventListener('contextmenu', this.handleRightClick); + currentWindowWebContentsRemoveListener('set-language'); } renderModalLinks = () => { @@ -139,8 +159,14 @@ class App extends Component {
diff --git a/packages/fether-react/src/BackupAccount/BackupAccount.js b/packages/fether-react/src/BackupAccount/BackupAccount.js index c79cabc41..a24fbd420 100644 --- a/packages/fether-react/src/BackupAccount/BackupAccount.js +++ b/packages/fether-react/src/BackupAccount/BackupAccount.js @@ -8,6 +8,7 @@ import { AccountHeader, Card, Form as FetherForm } from 'fether-ui'; import { observer } from 'mobx-react'; import { Link, withRouter } from 'react-router-dom'; +import i18n, { packageNS } from '../i18n'; import RequireHealthOverlay from '../RequireHealthOverlay'; import backupAccount from '../utils/backupAccount'; import withAccount from '../utils/withAccount'; @@ -68,11 +69,13 @@ class BackupAccount extends Component { - Back + {i18n.t(`${packageNS}:navigation.back`)} } /> @@ -80,11 +83,17 @@ class BackupAccount extends Component {
-

Unlock your account to encrypt the JSON keystore file:

+

+ {i18n.t( + `${packageNS}:account.backup.label_msg_password_unlock` + )} +

- Back + {i18n.t(`${packageNS}:navigation.back`)} diff --git a/packages/fether-react/src/Health/Health.js b/packages/fether-react/src/Health/Health.js index 03946ae9e..b9e73d9c2 100644 --- a/packages/fether-react/src/Health/Health.js +++ b/packages/fether-react/src/Health/Health.js @@ -8,6 +8,7 @@ import { branch } from 'recompose'; import { chainName$ } from '@parity/light.js'; import light from '@parity/light.js-react'; import withHealth from '../utils/withHealth'; +import i18n, { packageNS } from '../i18n'; @withHealth @branch( @@ -58,21 +59,25 @@ class Health extends Component { } = this.props; if (status.launching) { - return 'Launching the node...'; + return i18n.t(`${packageNS}:health.status.title.launching`); } else if (!status.nodeConnected && !status.internet) { - return 'No internet. No node connected'; + return i18n.t( + `${packageNS}:health.status.title.no_internet_no_node_connected` + ); } else if (!status.nodeConnected && status.internet) { - return 'Connecting to node...'; + return i18n.t( + `${packageNS}:health.status.title.internet_no_node_connected` + ); } else if (status.nodeConnected && !status.internet) { - return 'No internet. Connected to node'; - } else if (status.launching) { - return 'Launching the node...'; + return i18n.t( + `${packageNS}:health.status.title.no_internet_node_connected` + ); } else if (!status.clockSync) { - return 'Clock of host not in sync'; + return i18n.t(`${packageNS}:health.status.title.no_clock_sync`); } else if (!status.peers) { - return 'Connecting to peers...'; + return i18n.t(`${packageNS}:health.status.title.no_peers`); } else if (status.syncing) { - return `Syncing...${ + return `${i18n.t(`${packageNS}:health.status.title.syncing`)} ${ payload && payload.syncing && payload.syncing.syncPercentage && @@ -81,7 +86,9 @@ class Health extends Component { : '' } ${chainName}`; } else if (status.good) { - return `Synced ${chainName}`; + return `${i18n.t( + `${packageNS}:health.status.title.synced` + )} ${chainName}`; } else { return JSON.stringify(payload) || ''; // Just in case payload is an object } diff --git a/packages/fether-react/src/Onboarding/Onboarding.js b/packages/fether-react/src/Onboarding/Onboarding.js index d7da1264e..243166eba 100644 --- a/packages/fether-react/src/Onboarding/Onboarding.js +++ b/packages/fether-react/src/Onboarding/Onboarding.js @@ -8,6 +8,7 @@ import { Form as FetherForm, Header } from 'fether-ui'; import { inject, observer } from 'mobx-react'; import ReactMarkdown from 'react-markdown'; +import i18n, { packageNS } from '../i18n'; import Health from '../Health'; import termsAndConditions from './termsAndConditions.md'; @@ -50,7 +51,7 @@ class Onboarding extends Component { render () { return ( -
Terms of Use} /> +
{i18n.t(`${packageNS}:onboarding.header`)}} />
@@ -60,7 +61,7 @@ class Onboarding extends Component { className='terms-and-conditions' renderers={reactMarkdownOptions} source={this.state.markdown} - label='Please read carefully' + label={i18n.t(`${packageNS}:onboarding.instructions`)} />
@@ -72,7 +73,7 @@ class Onboarding extends Component {
diff --git a/packages/fether-react/src/RequireHealthOverlay/HealthModal/HealthModal.js b/packages/fether-react/src/RequireHealthOverlay/HealthModal/HealthModal.js index ef25e65b1..493cd1e25 100644 --- a/packages/fether-react/src/RequireHealthOverlay/HealthModal/HealthModal.js +++ b/packages/fether-react/src/RequireHealthOverlay/HealthModal/HealthModal.js @@ -10,6 +10,7 @@ import { chainName$ } from '@parity/light.js'; import light from '@parity/light.js-react'; import { Modal } from 'fether-ui'; +import i18n, { packageNS } from '../../i18n'; import withHealth from '../../utils/withHealth'; import loading from '../../assets/img/icons/loading.svg'; @@ -56,19 +57,25 @@ class HealthModal extends Component { } = this.props; if (status.launching) { - return 'Launching the node...'; + return i18n.t(`${packageNS}:health.status.title.launching`); } else if (!status.nodeConnected && !status.internet) { - return 'No internet. No node connected'; + return i18n.t( + `${packageNS}:health.status.title.no_internet_no_node_connected` + ); } else if (!status.nodeConnected && status.internet) { - return 'Connecting to node...'; + return i18n.t( + `${packageNS}:health.status.title.internet_no_node_connected` + ); } else if (status.nodeConnected && !status.internet) { - return 'No internet. Connected to node'; + return i18n.t( + `${packageNS}:health.status.title.no_internet_node_connected` + ); } else if (!status.clockSync) { - return 'Clock of host not in sync'; + return i18n.t(`${packageNS}:health.status.title.no_clock_sync`); } else if (!status.peers) { - return 'Connecting to peers...'; + return i18n.t(`${packageNS}:health.status.title.no_peers`); } else if (status.syncing) { - return `Syncing...`; + return i18n.t(`${packageNS}:health.status.title.syncing`); } else { return ''; } @@ -81,14 +88,13 @@ class HealthModal extends Component { } = this.props; if (!status.internet) { - return 'Please connect to the Internet'; + return i18n.t(`${packageNS}:health.status.description.no_internet`); } else if (!status.clockSync) { - return `Mac: System Preferences -> Date & Time -> Uncheck and recheck "Set date and time automatically" - Windows: Control Panel -> "Clock, Language, and Region" -> "Date and Time" -> Uncheck and recheck "Set date and time automatically"`; + return i18n.t(`${packageNS}:health.status.description.no_clock_sync`); } else if (!status.peers) { - return 'Searching for peers'; + return i18n.t(`${packageNS}:health.status.description.no_peers`); } else if (status.syncing) { - return `Syncing...${ + return `${i18n.t(`${packageNS}:health.status.description.syncing`)} ${ payload && payload.syncing && payload.syncing.syncPercentage && diff --git a/packages/fether-react/src/RequireHealthOverlay/RequireHealthOverlay.js b/packages/fether-react/src/RequireHealthOverlay/RequireHealthOverlay.js index e68cd7a26..001239356 100644 --- a/packages/fether-react/src/RequireHealthOverlay/RequireHealthOverlay.js +++ b/packages/fether-react/src/RequireHealthOverlay/RequireHealthOverlay.js @@ -6,6 +6,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import i18n, { packageNS } from '../i18n'; import withHealth from '../utils/withHealth'; import { HealthModal } from './HealthModal'; @@ -22,7 +23,7 @@ function statusMatches (status, require) { return status.good; default: throw new Error( - `Status '${status}' must be one of 'node-internet|node|sync'.` + i18n.t(`${packageNS}:health.error_status_invalid`, { status }) ); } } diff --git a/packages/fether-react/src/Scanner/Scanner.js b/packages/fether-react/src/Scanner/Scanner.js index edbb801b6..2e42aaa9d 100644 --- a/packages/fether-react/src/Scanner/Scanner.js +++ b/packages/fether-react/src/Scanner/Scanner.js @@ -7,6 +7,7 @@ import React from 'react'; import QrSigner from '@parity/qr-signer'; import { ExternalLink } from 'fether-ui'; +import i18n, { packageNS } from '../i18n'; import loading from '../assets/img/icons/loading.svg'; export default class Scanner extends React.PureComponent { @@ -47,17 +48,17 @@ export default class Scanner extends React.PureComponent { switch (e.name) { case 'NotAllowedError': case 'SecurityError': - errorMessage = 'Webcam access was refused.'; + errorMessage = i18n.t(`${packageNS}:scanner.error_security`); break; case 'NotFoundError': case 'OverconstrainedError': - errorMessage = 'Webcam not found on the device.'; + errorMessage = i18n.t(`${packageNS}:scanner.error_overconstrained`); break; case 'NotReadableError': - errorMessage = 'Webcam permissions or hardware error.'; + errorMessage = i18n.t(`${packageNS}:scanner.error_not_readable`); break; default: - errorMessage = 'Webcam unknown error.'; + errorMessage = i18n.t(`${packageNS}:scanner.error_unknown`); } this.setState({ webcamError: errorMessage, diff --git a/packages/fether-react/src/Send/ScanSignedTx/ScanSignedTx.js b/packages/fether-react/src/Send/ScanSignedTx/ScanSignedTx.js index 423a05125..33fc908f9 100644 --- a/packages/fether-react/src/Send/ScanSignedTx/ScanSignedTx.js +++ b/packages/fether-react/src/Send/ScanSignedTx/ScanSignedTx.js @@ -10,6 +10,7 @@ import { Link, Redirect } from 'react-router-dom'; import Scanner from '../../Scanner'; import { withProps } from 'recompose'; +import i18n, { packageNS } from '../../i18n'; import RequireHealthOverlay from '../../RequireHealthOverlay'; import withAccount from '../../utils/withAccount.js'; import withTokens from '../../utils/withTokens'; @@ -41,7 +42,7 @@ class ScanSignedTx extends Component { ); } catch (e) { this.setState({ - error: "The QR code doesn't seem to be a valid transaction." + error: i18n.t(`${packageNS}:tx.scan_signed.error_qr_tx_invalid`) }); } }; @@ -64,10 +65,18 @@ class ScanSignedTx extends Component {
- Close + {i18n.t(`${packageNS}:navigation.close`)} } - title={token &&

Send {token.name}

} + title={ + token && ( +

+ {i18n.t(`${packageNS}:tx.header_send_prefix`, { + token: token.name + })} +

+ ) + } /> @@ -76,7 +85,7 @@ class ScanSignedTx extends Component { {error &&

{error}

} @@ -87,7 +96,7 @@ class ScanSignedTx extends Component { onClick={history.goBack} type='button' > - Back + {i18n.t(`${packageNS}:navigation.back`)}
diff --git a/packages/fether-react/src/Send/Sent/Sent.js b/packages/fether-react/src/Send/Sent/Sent.js index 6f6fa9dfa..1257e885f 100644 --- a/packages/fether-react/src/Send/Sent/Sent.js +++ b/packages/fether-react/src/Send/Sent/Sent.js @@ -11,6 +11,7 @@ import light from '@parity/light.js-react'; import { withProps } from 'recompose'; import { Modal } from 'fether-ui'; +import i18n, { packageNS } from '../../i18n'; import RequireHealthOverlay from '../../RequireHealthOverlay'; import check from '../../assets/img/icons/check.svg'; import loading from '../../assets/img/icons/loading.svg'; @@ -81,11 +82,13 @@ class Sent extends Component { } if (confirmations > 0) { - return `Waiting ${confirmations}/${MIN_CONFIRMATIONS} confirmations`; + return i18n.t(`${packageNS}:tx.sent.waiting_confirmations_receiving`, { + progress: `${confirmations}/${MIN_CONFIRMATIONS}` + }); } if (txStatus.confirmed) { - return 'Waiting for confirmations...'; + return i18n.t(`${packageNS}:tx.sent.waiting_confirmed`); } if (txStatus.failed) { @@ -120,17 +123,17 @@ class Sent extends Component { return ( {confirmations >= MIN_CONFIRMATIONS - ? 'Transaction confirmed' - : 'Submitted'} + ? i18n.t(`${packageNS}:tx.sent.confirmed`) + : i18n.t(`${packageNS}:tx.sent.submitted`)} ); } if (txStatus.failed) { - return 'Error'; + return i18n.t(`${packageNS}:tx.sent.error`); } - return 'Sending your transaction...'; + return i18n.t(`${packageNS}:tx.header_sending`); }; renderGoHomepage = () => { @@ -149,7 +152,7 @@ class Sent extends Component { disabled={confirmations < MIN_CONFIRMATIONS} onClick={this.handleGoToHomepage} > - Go back + {i18n.t(`${packageNS}:navigation.go_back`)} ); @@ -173,7 +176,9 @@ class Sent extends Component { target='_blank' rel='noopener noreferrer' > - + ); } diff --git a/packages/fether-react/src/Send/SignedTxSummary/SignedTxSummary.js b/packages/fether-react/src/Send/SignedTxSummary/SignedTxSummary.js index b82165a39..9d21432d7 100644 --- a/packages/fether-react/src/Send/SignedTxSummary/SignedTxSummary.js +++ b/packages/fether-react/src/Send/SignedTxSummary/SignedTxSummary.js @@ -10,6 +10,7 @@ import { inject, observer } from 'mobx-react'; import { Link } from 'react-router-dom'; import { withProps } from 'recompose'; +import i18n, { packageNS } from '../../i18n'; import RequireHealthOverlay from '../../RequireHealthOverlay'; import TokenAddress from '../../Tokens/TokensList/TokenAddress'; import withAccount from '../../utils/withAccount'; @@ -54,10 +55,18 @@ class SignedTxSummary extends Component {
- Close + {i18n.t(`${packageNS}:navigation.close`)} } - title={token &&

Send {token.name}

} + title={ + token && ( +

+ {i18n.t(`${packageNS}:tx.header_send_prefix`, { + token: token.name + })} +

+ ) + } /> @@ -82,7 +91,7 @@ class SignedTxSummary extends Component { as='textarea' className='form_field_value' disabled - label='To' + label={i18n.t(`${packageNS}:tx.form.field.to`)} name='to' render={FetherForm.Field} /> @@ -90,22 +99,30 @@ class SignedTxSummary extends Component { {values.to === values.from && ( -

WARNING:

+

+ {i18n.t( + `${packageNS}:tx.form.warning.title_same_sender_receiver` + )} +

- The sender and receiver addresses are the same. + {i18n.t( + `${packageNS}:tx.form.warning.body_same_sender_receiver` + )}

)} )} diff --git a/packages/fether-react/src/Send/TxForm/TxDetails/TxDetails.js b/packages/fether-react/src/Send/TxForm/TxDetails/TxDetails.js index eda454caf..86bb4dcf9 100644 --- a/packages/fether-react/src/Send/TxForm/TxDetails/TxDetails.js +++ b/packages/fether-react/src/Send/TxForm/TxDetails/TxDetails.js @@ -7,6 +7,8 @@ import React, { Component } from 'react'; import BigNumber from 'bignumber.js'; import { fromWei, toWei } from '@parity/api/lib/util/wei'; +import i18n, { packageNS } from '../../../i18n'; + class TxDetails extends Component { renderDetails = () => { const { estimatedTxFee, token, values } = this.props; @@ -24,7 +26,7 @@ class TxDetails extends Component { ) { // Keep line break so message is centered return ` -Missing input fields...`; +${i18n.t(`${packageNS}:tx.form.details.missing_fields`)}`; } return `${this.renderCalculation()} @@ -46,7 +48,9 @@ ${this.renderTotalAmount()}`; .toFixed(0) .toString(); - return `Gas Limit: ${gasLimitBn}`; + return i18n.t(`${packageNS}:tx.form.details.gas_limit`, { + gas_limit: gasLimitBn + }); }; renderFee = () => { @@ -56,9 +60,11 @@ ${this.renderTotalAmount()}`; return; } - return `Fee: ${fromWei(estimatedTxFee, 'ether') + const fee = `${fromWei(estimatedTxFee, 'ether') .toFixed(9) - .toString()} ETH (gas limit * gas price)`; + .toString()}`; + + return i18n.t(`${packageNS}:tx.form.details.fee`, { fee }); }; renderTotalAmount = () => { @@ -68,12 +74,16 @@ ${this.renderTotalAmount()}`; return; } - return `Total Amount: ${fromWei( + const totalAmount = `${fromWei( estimatedTxFee.plus( token.address === 'ETH' ? toWei(values.amount.toString()) : 0 ), 'ether' - ).toString()} ETH`; + ).toString()}`; + + return i18n.t(`${packageNS}:tx.form.details.total_amount`, { + total_amount: totalAmount + }); }; render () { @@ -83,7 +93,9 @@ ${this.renderTotalAmount()}`;