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={
- 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`)}