Commit 649d6bf0 authored by Luke Schoen's avatar Luke Schoen Committed by Thibaut Sardan
Browse files

feat: Relates to #402. Internationalisation. Base Support (#452)

* chore: Update to latest React 16.8.3 to requirements of react-i18next

* feat: Scaffold basic translation with English and German

* feat: Relates to #402. German translation fully working

* fix: Switch back to English by default

* fix: Allow user to switch between languages in preferences of context menu

* feat: Translate the context menus

* refactor: Remove German language. Add as separate PR

* refactor: Remove blank line

* docs: Update Readme with Internationalisation Add New language instructions

* docs: Update Readme with Known Issues and Usage instructions

* review-fix: Disabled tooltip Please fill out this field. Add High and Low tx speed

* review-fix: Update license headers to be 2019 instead of 2018

* review-fix: Change ns1 to fether-electron and fether-react. Use pino.debug

* fix: Add missing i18n conversion for macOS Edit menu

* review-fix: Remove unused i18next browser languagedetector dependency

* merge latest from master and fix conflicts

* merge latest master and fix conflicts. TODO do not expose remote

* fix: Do not expose remote. Only expose add and remove listener, and reload via bridge

* feat: Convert new release available text to i18n
parent 6184a296
Pipeline #35465 passed with stages
in 10 minutes and 15 seconds
......@@ -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": {
......
......@@ -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",
......
// 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;
{
"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"
}
}
......@@ -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;
......
......@@ -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,
......
......@@ -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",
......
......@@ -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={<h1>Accounts</h1>}
title={<h1>{i18n.t(`${packageNS}:accounts_list.header`)}</h1>}
/>
<div className='window_content'>
......@@ -72,20 +73,25 @@ class AccountsList extends Component {
<AccountCard
address={address}
className='-clickable'
type={accountsInfo[address].type}
name={accountsInfo[address].name || '(no name)'}
i18n={i18n}
name={
accountsInfo[address].name ||
i18n.t(`${packageNS}:account.existing.no_name`)
}
packageNS={packageNS}
screen='accounts'
shortAddress
type={accountsInfo[address].type}
/>
</li>
))}
</ul>
) : (
<p className='create-hint'>
Nothing here yet!
{i18n.t(`${packageNS}:accounts_list.hint.none`)}
<br />
<br />
Click the + icon to add a new account.
{i18n.t(`${packageNS}:accounts_list.hint.exist`)}
</p>
)}
</div>
......
......@@ -5,6 +5,8 @@
import React from 'react';
import i18n, { packageNS } from '../../../i18n';
export const Feedback = ({ accountsListLength }) => (
<a
className='feedback'
......@@ -12,6 +14,6 @@ export const Feedback = ({ accountsListLength }) => (
rel='noopener noreferrer'
target='_blank'
>
Feedback
{i18n.t(`${packageNS}:feedback:title`)}
</a>