Skip to content

Add i18n support and logging #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,21 @@ Open browser at http://localhost:3000
```sh
yarn test
```

## Logging

Client-side logs are disabled by default in production builds and enabled by default in a development environment. In production, logging can be turned on by adding a couple keys to your browser's `localStorage`. Simply run these two JS statements in you browser's DevTools console:

```
localStorage.setItem('debug', '*'); localStorage.setItem('debug-level', 'debug');
```

The value for `debug` is a namespace filter which determines which portions of the app to display logs for. The namespaces currently used by the app are as follows:

- `main`: logs general application messages
- `action`: logs all actions that modify the internal application state
- `grpc`: logs all GRPC API requests and responses

Example filters: `main,action` will only log main and action messages. `*,-action` will log everything except action messages.

The value for `debug-level` determines the verbosity of the logs. The value can be one of `debug`, `info`, `warn`, or `error`.
6 changes: 6 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,21 @@
"proxy": "https://localhost:8443",
"dependencies": {
"@improbable-eng/grpc-web": "0.12.0",
"debug": "4.1.1",
"i18next": "19.4.1",
"i18next-browser-languagedetector": "4.0.2",
"mobx": "5.15.4",
"mobx-react": "6.2.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-i18next": "11.3.4",
"react-scripts": "3.4.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/debug": "4.1.5",
"@types/google-protobuf": "3.7.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
Expand All @@ -48,6 +53,7 @@
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts",
"!src/types/**/*.{js,ts}",
"!src/i18n/**/*.{js,ts}",
"!src/index.tsx"
]
},
Expand Down
12 changes: 7 additions & 5 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import './App.css';
import usePrefixedTranslation from 'hooks/usePrefixedTranslation';
import { channel, node, swap } from 'action';
import store from 'store';

const App = () => {
const { l } = usePrefixedTranslation('App');
useEffect(() => {
// fetch node info when the component is mounted
const fetchInfo = async () => await node.getInfo();
Expand All @@ -13,25 +15,25 @@ const App = () => {

return (
<div className="App">
<p>Node Info</p>
<p>{l('App.nodeInfo')}</p>
{store.info && (
<>
<table className="App-table">
<tbody>
<tr>
<th>Pubkey</th>
<th>{l('pubkey')}</th>
<td>{store.info.identityPubkey}</td>
</tr>
<tr>
<th>Alias</th>
<th>{l('alias')}</th>
<td>{store.info.alias}</td>
</tr>
<tr>
<th>Version</th>
<th>{l('version')}</th>
<td>{store.info.version}</td>
</tr>
<tr>
<th># Channels</th>
<th>{l('numChannels')}</th>
<td>{store.info.numActiveChannels}</td>
</tr>
</tbody>
Expand Down
5 changes: 4 additions & 1 deletion app/src/action/channel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { action } from 'mobx';
import { action, toJS } from 'mobx';
import { actionLog as log } from 'util/log';
import LndApi from 'api/lnd';
import { Store } from 'store';

Expand All @@ -19,6 +20,7 @@ class ChannelAction {
* fetch channels from the LND RPC
*/
@action.bound async getChannels() {
log.info('fetching channels');
const channels = await this._lnd.listChannels();
this._store.channels = channels.channelsList.map(c => ({
chanId: c.chanId,
Expand All @@ -29,6 +31,7 @@ class ChannelAction {
uptime: c.uptime,
active: c.active,
}));
log.info('updated store.channels', toJS(this._store.channels));
}
}

Expand Down
5 changes: 4 additions & 1 deletion app/src/action/node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { action } from 'mobx';
import { action, toJS } from 'mobx';
import { actionLog as log } from 'util/log';
import LndApi from 'api/lnd';
import { Store } from 'store';

Expand All @@ -19,7 +20,9 @@ class NodeAction {
* fetch node info from the LND RPC
*/
@action.bound async getInfo() {
log.info('fetching node information');
this._store.info = await this._lnd.getInfo();
log.info('updated store.info', toJS(this._store.info));
}
}

Expand Down
5 changes: 4 additions & 1 deletion app/src/action/swap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { action } from 'mobx';
import { action, toJS } from 'mobx';
import { SwapState, SwapType } from 'types/generated/loop_pb';
import { actionLog as log } from 'util/log';
import LoopApi from 'api/loop';
import { Store } from 'store';

Expand All @@ -20,6 +21,7 @@ class SwapAction {
* fetch swaps from the Loop RPC
*/
@action.bound async listSwaps() {
log.info('fetching Loop history');
const loopSwaps = await this._loop.listSwaps();
this._store.swaps = loopSwaps.swapsList
// sort the list with newest first as the API returns them out of order
Expand All @@ -31,6 +33,7 @@ class SwapAction {
createdOn: new Date(s.initiationTime / 1000 / 1000),
status: this._stateToString(s.state),
}));
log.info('updated store.swaps', toJS(this._store.swaps));
}

/**
Expand Down
9 changes: 9 additions & 0 deletions app/src/api/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ProtobufMessage } from '@improbable-eng/grpc-web/dist/typings/message';
import { Metadata } from '@improbable-eng/grpc-web/dist/typings/metadata';
import { UnaryMethodDefinition } from '@improbable-eng/grpc-web/dist/typings/service';
import { DEV_HOST } from 'config';
import { grpcLog as log } from 'util/log';

/**
* Executes a single GRPC request and returns a promise which will resolve with the response
Expand All @@ -16,16 +17,24 @@ export const grpcRequest = <TReq extends ProtobufMessage, TRes extends ProtobufM
metadata?: Metadata.ConstructorArg,
): Promise<TRes> => {
return new Promise((resolve, reject) => {
log.debug(
`Request: ${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`,
);
log.debug(` - req: `, request.toObject());
grpc.unary(methodDescriptor, {
host: DEV_HOST,
request,
metadata,
onEnd: ({ status, statusMessage, headers, message, trailers }) => {
log.debug(' - status', status, statusMessage);
log.debug(' - headers', headers);
if (status === grpc.Code.OK && message) {
log.debug(' - message', message.toObject());
resolve(message as TRes);
} else {
reject(new Error(`${status}: ${statusMessage}`));
}
log.debug(' - trailers', trailers);
},
});
});
Expand Down
2 changes: 1 addition & 1 deletion app/src/api/lnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class LndApi {
* call the LND `GetInfo` RPC and return the response
*/
async getInfo(): Promise<LND.GetInfoResponse.AsObject> {
const req = new LND.GetInfoResponse();
const req = new LND.GetInfoRequest();
const res = await grpcRequest(Lightning.GetInfo, req, this._meta);
return res.toObject();
}
Expand Down
11 changes: 10 additions & 1 deletion app/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
// flag to check if the app is running in a local development environment
export const IS_DEV = process.env.NODE_ENV === 'development';

// flag to check if the app is running in a a production environment
export const IS_PROD = process.env.NODE_ENV === 'production';

//
// temporary placeholder values. these will be supplied via the UI in the future
//

// macaroon to use for LND auth
export const DEV_MACAROON = process.env.REACT_APP_DEV_MACAROON || '';

// detect the host currently serving the app files
const { protocol, hostname, port = '' } = window.location;
const host = `${protocol}//${hostname}:${port}`;
// the GRPC server to make requests to
export const DEV_HOST = process.env.REACT_APP_DEV_HOST || 'http://localhost:3000';
export const DEV_HOST = process.env.REACT_APP_DEV_HOST || host;
24 changes: 24 additions & 0 deletions app/src/hooks/usePrefixedTranslation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';

/**
* A hook which returns a `t` function that inserts a prefix in each key lookup
* @param prefix the prefix to use for all translation keys
*/
const usePrefixedTranslation = (prefix: string) => {
const { t } = useTranslation();
// the new `t` function that will append the prefix
const translate = useCallback(
(key: string, options?: string | object) => {
// if the key contains a '.', then don't add the prefix
return key.includes('.') ? t(key, options) : t(`${prefix}.${key}`, options);
},
[prefix, t],
);

return {
l: translate,
};
};

export default usePrefixedTranslation;
53 changes: 53 additions & 0 deletions app/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { initReactI18next } from 'react-i18next';
import i18n, { InitOptions } from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import enUS from './locales/en-US.json';

const defaultLanguage = 'en-US';

export const languages: { [index: string]: string } = {
'en-US': 'English',
};

/**
* create a mapping of locales -> translations
*/
const resources = Object.keys(languages).reduce((acc: { [key: string]: any }, lang) => {
switch (lang) {
case 'en-US':
acc[lang] = { translation: enUS };
break;
}
return acc;
}, {});

/**
* create an array of allowed languages
*/
const whitelist = Object.keys(languages).reduce((acc: string[], lang) => {
acc.push(lang);

if (lang.includes('-')) {
acc.push(lang.substring(0, lang.indexOf('-')));
}

return acc;
}, []);

const config: InitOptions = {
lng: defaultLanguage,
resources,
whitelist,
fallbackLng: defaultLanguage,
keySeparator: false,
interpolation: {
escapeValue: false,
},
detection: {
lookupLocalStorage: 'lang',
},
};

i18n.use(LanguageDetector).use(initReactI18next).init(config);

export default i18n;
7 changes: 7 additions & 0 deletions app/src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"App.nodeInfo": "Node Info",
"App.pubkey": "Pubkey",
"App.alias": "Alias",
"App.version": "Version",
"App.numChannels": "# Channels"
}
4 changes: 4 additions & 0 deletions app/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import 'mobx-react/batchingForReactDom';
import './i18n';
import './index.css';
import { log } from 'util/log';
import App from './App';

log.info('Rendering the App');

ReactDOM.render(
<React.StrictMode>
<App />
Expand Down
2 changes: 2 additions & 0 deletions app/src/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ import 'mobx-react-lite/batchingForReactDom';
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
// enable i18n translations in unit tests
import './i18n';
Loading