Skip to content

Commit

Permalink
I18n support for Bob (#440)
Browse files Browse the repository at this point in the history
* add i18n bootstrap files

* add i18n setting

* i18n: account view

* i18n: auction view

* i18n: domain manager, exchange, domain details, get coins, onboarding

* i18n: setting, sign, verify, watchlist, yourbids, transactions

* i18n: persist setting, splash screen

* sort en.json

* add instruction

* add note to maintainers
  • Loading branch information
chikeichan authored Jan 17, 2022
1 parent c3f93b1 commit 2d6b9a5
Show file tree
Hide file tree
Showing 82 changed files with 2,345 additions and 811 deletions.
59 changes: 59 additions & 0 deletions LOCALE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Internationalization (i18n)

This documents describes how i18n works in Bob Wallet, and how to add support for a new language

## Overview

All locale strings are saved as `{locale}.json` based on result return from [electron.app.getLocale()](https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc).

A locale json looks like this:

en.json
```json
{
"hello": "Hello, %s!"
}
```

On app starts, all locale strings are compiled into a JSON object, and injected to the app using [React Context](https://reactjs.org/docs/context.html).

Usage Example:
```js
import {I18nContext} from "../../utils/i18n";

class Example extends Component {
static contextType = I18nContext;

render() {
const {t} = this.context;

// This will render "Hello, World!" based on en.json above
return (
<div>{t('hello', 'World')}</div>
);
}
}
```


When getting string using the injected `this.context.t(localeKey)` function, the app will:
- first check to see if there is a matching string for `localeKey` from the exact locale (e.g. `en-US.json`)
- if a matching string cannot be found, it will check to see if there is matching string for `localeKey` from the root locale (e.g. `en-US.json` -> `en.json`)
- if a match is still not found, it will use `en.json` by default
- if `localeKey` is not found in `en.json`, it will render `this.context.t(localeKey)` in the UI;


## Adding Support for New Language

1. Copy `/locales/en.json` to a new file, and save it as `[locale].json`. For example, if you are adding support for Spanish, the file name should be `es.json`. You can find all valid locale strings [here](https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc).
2. Start translating 📙
3. When finished translating, save your file
4. Go to https://github.com/kyokan/bob-wallet/tree/master/locales
5. Click `Add Files -> Uplaod Files`
6. Drag and drop your file to upload it to GitHub
7. Make sure you select **Create a new branch for this commit and start a pull request.**
8. Click **Propose Change**

## Note to Maintainers

When merging in a new locale json, be sure to update the dropdown list in `app/util/i18n.js` with the new locale.
2 changes: 2 additions & 0 deletions app/background/setting/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ import { makeClient } from '../ipc/ipc';
export const clientStub = ipcRendererInjector => makeClient(ipcRendererInjector, 'Setting', [
'getExplorer',
'setExplorer',
'getLocale',
'setLocale',
]);
18 changes: 18 additions & 0 deletions app/background/setting/service.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { del, get, put } from '../db/service';
import { app } from "electron";

const EXPLORER = 'setting/explorer';
const LOCALE = 'setting/locale';


export async function getExplorer() {
Expand All @@ -12,10 +14,26 @@ export async function setExplorer(explorer) {
return await put(EXPLORER, explorer);
}

export async function getLocale() {
const locale = await get(LOCALE);

if (locale) return locale;

return app.getLocale();
}



export async function setLocale(locale) {
return await put(LOCALE, locale);
}

const sName = 'Setting';
const methods = {
getExplorer,
setExplorer,
getLocale,
setLocale,
};

export async function start(server) {
Expand Down
1 change: 1 addition & 0 deletions app/components/Collapsible/collapsible.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@extend %row-nowrap;
flex: 1 1 auto;
width: 0;
text-transform: capitalize;
}

&__toggle {
Expand Down
9 changes: 6 additions & 3 deletions app/components/Collapsible/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import './collapsible.scss';
import {I18nContext} from "../../utils/i18n";

export default class Collapsible extends Component {
static propTypes = {
Expand All @@ -16,6 +17,8 @@ export default class Collapsible extends Component {
defaultCollapsed: false,
};

static contextType = I18nContext;

constructor(props) {
super(props);
this.state = {
Expand All @@ -41,7 +44,7 @@ export default class Collapsible extends Component {
<div className="collapsible__header">
<div className="collapsible__header__title">
{title}
{!!pillContent &&
{!!pillContent &&
<div className="collapsible__header__pill">
{pillContent}
</div>
Expand All @@ -51,7 +54,7 @@ export default class Collapsible extends Component {
className="collapsible__header__toggle"
onClick={this.toggle}
>
{ isCollapsed ? 'Show': 'Hide' }
{ isCollapsed ? this.context.t('show') : this.context.t('hide') }
</div>
</div>
{ this.renderContent() }
Expand All @@ -63,7 +66,7 @@ export default class Collapsible extends Component {
return (
<div className={cn('collapsible__content', {
'collapsible__content--hidden': this.state.isCollapsed
})} >
})} >
{ this.props.children }
</div>
)
Expand Down
49 changes: 29 additions & 20 deletions app/components/ConnectLedgerStep/defaultSteps.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import React, { Component } from 'react';
import ConnectLedgerStep from './index';
import {I18nContext} from "../../utils/i18n";

export default class DefaultConnectLedgerSteps extends Component {
static contextType = I18nContext;

render() {
const {completedSteps} = this.props;
const {t} = this.props;

return (
<React.Fragment>
<ConnectLedgerStep
stepNumber={1}
stepDescription={t('obLedgerStep1')}
stepCompleted={completedSteps[0]}
/>
<ConnectLedgerStep
stepNumber={2}
stepDescription={t('obLedgerStep2')}
stepCompleted={completedSteps[1]}
/>
<ConnectLedgerStep
stepNumber={3}
stepDescription={t('obLedgerStep3')}
stepCompleted={completedSteps[2]}
/>
</React.Fragment>
)
}

export default function DefaultConnectLedgerSteps(props) {
return (
<React.Fragment>
<ConnectLedgerStep
stepNumber={1}
stepDescription="Connect your Ledger directly to your computer."
stepCompleted={props.completedSteps[0]}
/>
<ConnectLedgerStep
stepNumber={2}
stepDescription="Enter your secret pin on your Ledger device."
stepCompleted={props.completedSteps[1]}
/>
<ConnectLedgerStep
stepNumber={3}
stepDescription="Select the Handshake app on your Ledger."
stepCompleted={props.completedSteps[2]}
/>
</React.Fragment>
)
}
10 changes: 7 additions & 3 deletions app/components/IdleModal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import Modal from '../Modal';
import * as walletAction from '../../ducks/walletActions';
import './idle-modal.scss';
import {I18nContext} from "../../utils/i18n";

class IdleModal extends Component {
static propTypes = {
Expand All @@ -13,6 +14,8 @@ class IdleModal extends Component {
logout: PropTypes.func.isRequired,
};

static contextType = I18nContext;

state = {
isShowing: false,
timeRemaining: 60,
Expand Down Expand Up @@ -80,6 +83,7 @@ class IdleModal extends Component {
};

render() {
const {t} = this.context;
if (!this.state.isShowing) {
return <noscript />;
}
Expand All @@ -88,21 +92,21 @@ class IdleModal extends Component {
<Modal className="idle-modal__wrapper" onClose={() => ({})}>
<div className="idle-modal">
<div className="idle-modal__title">
You will be automatically logged out in:
{t('idleModalTitle')}
</div>
<div className="idle-modal__time">{this.state.timeRemaining}s</div>
<div className="idle-modal__actions">
<button
className="idle-modal__actions__extend"
onClick={this.extend}
>
Extend
{t('extend')}
</button>
<button
className="idle-modal__actions__logout"
onClick={this.logout}
>
Logout
{t('logout')}
</button>
</div>
</div>
Expand Down
18 changes: 12 additions & 6 deletions app/components/LedgerModal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as logger from '../../utils/logClient';
import './ledger-modal.scss';
import DefaultConnectLedgerSteps from '../ConnectLedgerStep/defaultSteps';
import ConnectLedgerStep from '../ConnectLedgerStep';
import {I18nContext} from "../../utils/i18n";

const ipc = require('electron').ipcRenderer;

Expand All @@ -15,6 +16,8 @@ const ipc = require('electron').ipcRenderer;
}),
)
export class LedgerModal extends Component {
static contextType = I18nContext;

constructor(props) {
super(props);

Expand Down Expand Up @@ -67,14 +70,15 @@ export class LedgerModal extends Component {
};

handleError(err) {
const {t} = this.context;
logger.error('failed to connect to ledger', {err});

// Totally confusing
if (err === 'Device was not selected.')
err = 'Could not connect to device.';
err = t('ledgerModalNoConnError');

this.setState({
errorMessage: `Error confirming on Ledger: ${err}`,
errorMessage: t('ledgerModalGenericError', err),
isLoading: false,
});
}
Expand All @@ -89,8 +93,10 @@ export class LedgerModal extends Component {
};

render() {
const {t} = this.context;

if (!this.state.isVisible) {
return null;
return <></>;
}

return (
Expand All @@ -101,7 +107,7 @@ export class LedgerModal extends Component {
<div className="ledger-modal__last-step">
<ConnectLedgerStep
stepNumber={4}
stepDescription="Confirm the transaction info on your ledger device."
stepDescription={t('ledgerModalConfirmText')}
stepCompleted={false}
/>
</div>
Expand All @@ -110,14 +116,14 @@ export class LedgerModal extends Component {
className="ledger-modal__connect" onClick={this.onClickConnect}
disabled={this.state.isLoading}
>
{this.state.isLoading ? 'Connecting...' : 'Connect'}
{this.state.isLoading ? t('obLedgerConnecting') : t('obLedgerConnectLedger')}
</button>
<button
className="ledger-modal__cancel"
onClick={this.cancelLedger}
disabled={this.state.isLoading}
>
Cancel
{t('cancel')}
</button>
</div>
</div>
Expand Down
13 changes: 7 additions & 6 deletions app/components/SplashScreen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dbClient from "../../utils/dbClient";
import Alert from "../Alert";
import {withRouter} from "react-router-dom";
import {connect} from "react-redux";
import {I18nContext} from "../../utils/i18n";


class SplashScreen extends Component {
Expand All @@ -19,6 +20,8 @@ class SplashScreen extends Component {
error: '',
};

static contextType = I18nContext;

state = {
hasMigrated300: false,
};
Expand All @@ -32,6 +35,7 @@ class SplashScreen extends Component {

render() {
const {error} = this.props;
const {t} = this.context;

return (
<div style={wrapperStyle}>
Expand All @@ -44,18 +48,15 @@ class SplashScreen extends Component {
: (
<React.Fragment>
<div style={spinnerStyle} />
<div style={textStyles}>Loading node...</div>
<div style={textStyles}>{t('splashLoading')}</div>
{
!this.state.hasMigrated300 && (
<Alert type="warning" style={alertStyle}>
<div>
Database migration in progress!
{t('splashMigrate3001')}
</div>
<div>
This will take several minutes.
If Bob Wallet is closed during migration, it will resume on the next open.
Migration must be complete before proceeding to login screen.
Once complete, you can not downgrade Bob Wallet to an earlier version.
{t('splashMigrate3002')}
</div>
</Alert>
)
Expand Down
Loading

0 comments on commit 2d6b9a5

Please sign in to comment.