Skip to content

Commit b4c31ec

Browse files
authored
Merge pull request #6 from lightninglabs/feat/i18n
Add i18n support and logging
2 parents e915969 + ee760b4 commit b4c31ec

16 files changed

+299
-18
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,21 @@ Open browser at http://localhost:3000
4949
```sh
5050
yarn test
5151
```
52+
53+
## Logging
54+
55+
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:
56+
57+
```
58+
localStorage.setItem('debug', '*'); localStorage.setItem('debug-level', 'debug');
59+
```
60+
61+
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:
62+
63+
- `main`: logs general application messages
64+
- `action`: logs all actions that modify the internal application state
65+
- `grpc`: logs all GRPC API requests and responses
66+
67+
Example filters: `main,action` will only log main and action messages. `*,-action` will log everything except action messages.
68+
69+
The value for `debug-level` determines the verbosity of the logs. The value can be one of `debug`, `info`, `warn`, or `error`.

app/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,21 @@
1515
"proxy": "https://localhost:8443",
1616
"dependencies": {
1717
"@improbable-eng/grpc-web": "0.12.0",
18+
"debug": "4.1.1",
19+
"i18next": "19.4.1",
20+
"i18next-browser-languagedetector": "4.0.2",
1821
"mobx": "5.15.4",
1922
"mobx-react": "6.2.2",
2023
"react": "^16.13.1",
2124
"react-dom": "^16.13.1",
25+
"react-i18next": "11.3.4",
2226
"react-scripts": "3.4.1"
2327
},
2428
"devDependencies": {
2529
"@testing-library/jest-dom": "^4.2.4",
2630
"@testing-library/react": "^9.3.2",
2731
"@testing-library/user-event": "^7.1.2",
32+
"@types/debug": "4.1.5",
2833
"@types/google-protobuf": "3.7.2",
2934
"@types/jest": "^24.0.0",
3035
"@types/node": "^12.0.0",
@@ -48,6 +53,7 @@
4853
"src/**/*.{js,jsx,ts,tsx}",
4954
"!src/**/*.d.ts",
5055
"!src/types/**/*.{js,ts}",
56+
"!src/i18n/**/*.{js,ts}",
5157
"!src/index.tsx"
5258
]
5359
},

app/src/App.tsx

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, { useEffect } from 'react';
22
import { observer } from 'mobx-react';
33
import './App.css';
4+
import usePrefixedTranslation from 'hooks/usePrefixedTranslation';
45
import { channel, node, swap } from 'action';
56
import store from 'store';
67

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

1416
return (
1517
<div className="App">
16-
<p>Node Info</p>
18+
<p>{l('App.nodeInfo')}</p>
1719
{store.info && (
1820
<>
1921
<table className="App-table">
2022
<tbody>
2123
<tr>
22-
<th>Pubkey</th>
24+
<th>{l('pubkey')}</th>
2325
<td>{store.info.identityPubkey}</td>
2426
</tr>
2527
<tr>
26-
<th>Alias</th>
28+
<th>{l('alias')}</th>
2729
<td>{store.info.alias}</td>
2830
</tr>
2931
<tr>
30-
<th>Version</th>
32+
<th>{l('version')}</th>
3133
<td>{store.info.version}</td>
3234
</tr>
3335
<tr>
34-
<th># Channels</th>
36+
<th>{l('numChannels')}</th>
3537
<td>{store.info.numActiveChannels}</td>
3638
</tr>
3739
</tbody>

app/src/action/channel.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { action } from 'mobx';
1+
import { action, toJS } from 'mobx';
2+
import { actionLog as log } from 'util/log';
23
import LndApi from 'api/lnd';
34
import { Store } from 'store';
45

@@ -19,6 +20,7 @@ class ChannelAction {
1920
* fetch channels from the LND RPC
2021
*/
2122
@action.bound async getChannels() {
23+
log.info('fetching channels');
2224
const channels = await this._lnd.listChannels();
2325
this._store.channels = channels.channelsList.map(c => ({
2426
chanId: c.chanId,
@@ -29,6 +31,7 @@ class ChannelAction {
2931
uptime: c.uptime,
3032
active: c.active,
3133
}));
34+
log.info('updated store.channels', toJS(this._store.channels));
3235
}
3336
}
3437

app/src/action/node.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { action } from 'mobx';
1+
import { action, toJS } from 'mobx';
2+
import { actionLog as log } from 'util/log';
23
import LndApi from 'api/lnd';
34
import { Store } from 'store';
45

@@ -19,7 +20,9 @@ class NodeAction {
1920
* fetch node info from the LND RPC
2021
*/
2122
@action.bound async getInfo() {
23+
log.info('fetching node information');
2224
this._store.info = await this._lnd.getInfo();
25+
log.info('updated store.info', toJS(this._store.info));
2326
}
2427
}
2528

app/src/action/swap.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { action } from 'mobx';
1+
import { action, toJS } from 'mobx';
22
import { SwapState, SwapType } from 'types/generated/loop_pb';
3+
import { actionLog as log } from 'util/log';
34
import LoopApi from 'api/loop';
45
import { Store } from 'store';
56

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

3639
/**

app/src/api/grpc.ts

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ProtobufMessage } from '@improbable-eng/grpc-web/dist/typings/message';
33
import { Metadata } from '@improbable-eng/grpc-web/dist/typings/metadata';
44
import { UnaryMethodDefinition } from '@improbable-eng/grpc-web/dist/typings/service';
55
import { DEV_HOST } from 'config';
6+
import { grpcLog as log } from 'util/log';
67

78
/**
89
* Executes a single GRPC request and returns a promise which will resolve with the response
@@ -16,16 +17,24 @@ export const grpcRequest = <TReq extends ProtobufMessage, TRes extends ProtobufM
1617
metadata?: Metadata.ConstructorArg,
1718
): Promise<TRes> => {
1819
return new Promise((resolve, reject) => {
20+
log.debug(
21+
`Request: ${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`,
22+
);
23+
log.debug(` - req: `, request.toObject());
1924
grpc.unary(methodDescriptor, {
2025
host: DEV_HOST,
2126
request,
2227
metadata,
2328
onEnd: ({ status, statusMessage, headers, message, trailers }) => {
29+
log.debug(' - status', status, statusMessage);
30+
log.debug(' - headers', headers);
2431
if (status === grpc.Code.OK && message) {
32+
log.debug(' - message', message.toObject());
2533
resolve(message as TRes);
2634
} else {
2735
reject(new Error(`${status}: ${statusMessage}`));
2836
}
37+
log.debug(' - trailers', trailers);
2938
},
3039
});
3140
});

app/src/api/lnd.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class LndApi {
1616
* call the LND `GetInfo` RPC and return the response
1717
*/
1818
async getInfo(): Promise<LND.GetInfoResponse.AsObject> {
19-
const req = new LND.GetInfoResponse();
19+
const req = new LND.GetInfoRequest();
2020
const res = await grpcRequest(Lightning.GetInfo, req, this._meta);
2121
return res.toObject();
2222
}

app/src/config.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
// flag to check if the app is running in a local development environment
2+
export const IS_DEV = process.env.NODE_ENV === 'development';
3+
4+
// flag to check if the app is running in a a production environment
5+
export const IS_PROD = process.env.NODE_ENV === 'production';
6+
17
//
28
// temporary placeholder values. these will be supplied via the UI in the future
39
//
410

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

14+
// detect the host currently serving the app files
15+
const { protocol, hostname, port = '' } = window.location;
16+
const host = `${protocol}//${hostname}:${port}`;
817
// the GRPC server to make requests to
9-
export const DEV_HOST = process.env.REACT_APP_DEV_HOST || 'http://localhost:3000';
18+
export const DEV_HOST = process.env.REACT_APP_DEV_HOST || host;
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useCallback } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
4+
/**
5+
* A hook which returns a `t` function that inserts a prefix in each key lookup
6+
* @param prefix the prefix to use for all translation keys
7+
*/
8+
const usePrefixedTranslation = (prefix: string) => {
9+
const { t } = useTranslation();
10+
// the new `t` function that will append the prefix
11+
const translate = useCallback(
12+
(key: string, options?: string | object) => {
13+
// if the key contains a '.', then don't add the prefix
14+
return key.includes('.') ? t(key, options) : t(`${prefix}.${key}`, options);
15+
},
16+
[prefix, t],
17+
);
18+
19+
return {
20+
l: translate,
21+
};
22+
};
23+
24+
export default usePrefixedTranslation;

app/src/i18n/index.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { initReactI18next } from 'react-i18next';
2+
import i18n, { InitOptions } from 'i18next';
3+
import LanguageDetector from 'i18next-browser-languagedetector';
4+
import enUS from './locales/en-US.json';
5+
6+
const defaultLanguage = 'en-US';
7+
8+
export const languages: { [index: string]: string } = {
9+
'en-US': 'English',
10+
};
11+
12+
/**
13+
* create a mapping of locales -> translations
14+
*/
15+
const resources = Object.keys(languages).reduce((acc: { [key: string]: any }, lang) => {
16+
switch (lang) {
17+
case 'en-US':
18+
acc[lang] = { translation: enUS };
19+
break;
20+
}
21+
return acc;
22+
}, {});
23+
24+
/**
25+
* create an array of allowed languages
26+
*/
27+
const whitelist = Object.keys(languages).reduce((acc: string[], lang) => {
28+
acc.push(lang);
29+
30+
if (lang.includes('-')) {
31+
acc.push(lang.substring(0, lang.indexOf('-')));
32+
}
33+
34+
return acc;
35+
}, []);
36+
37+
const config: InitOptions = {
38+
lng: defaultLanguage,
39+
resources,
40+
whitelist,
41+
fallbackLng: defaultLanguage,
42+
keySeparator: false,
43+
interpolation: {
44+
escapeValue: false,
45+
},
46+
detection: {
47+
lookupLocalStorage: 'lang',
48+
},
49+
};
50+
51+
i18n.use(LanguageDetector).use(initReactI18next).init(config);
52+
53+
export default i18n;

app/src/i18n/locales/en-US.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"App.nodeInfo": "Node Info",
3+
"App.pubkey": "Pubkey",
4+
"App.alias": "Alias",
5+
"App.version": "Version",
6+
"App.numChannels": "# Channels"
7+
}

app/src/index.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
33
import 'mobx-react/batchingForReactDom';
4+
import './i18n';
45
import './index.css';
6+
import { log } from 'util/log';
57
import App from './App';
68

9+
log.info('Rendering the App');
10+
711
ReactDOM.render(
812
<React.StrictMode>
913
<App />

app/src/setupTests.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ import 'mobx-react-lite/batchingForReactDom';
55
// expect(element).toHaveTextContent(/react/i)
66
// learn more: https://github.com/testing-library/jest-dom
77
import '@testing-library/jest-dom/extend-expect';
8+
// enable i18n translations in unit tests
9+
import './i18n';

0 commit comments

Comments
 (0)