Add more translations to dashboard and introduce i18n test wrapper (#5082)

Add more translations to the dashboard; introduce i18n test wrapper
This commit is contained in:
Carol A. Scott 2020-10-21 21:25:52 -07:00 committed by GitHub
parent 15bd95ee1d
commit 79893c9ad6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 271 additions and 116 deletions

View File

@ -3,17 +3,18 @@ import { friendlyTitle, isResource, singularResource } from './util/Utils.js';
import PropTypes from 'prop-types';
import React from 'react';
import ReactRouterPropTypes from 'react-router-prop-types';
import { Trans } from '@lingui/macro';
import _chunk from 'lodash/chunk';
import _takeWhile from 'lodash/takeWhile';
import { withContext } from './util/AppContext.jsx';
const routeToCrumbTitle = {
controlplane: 'Control Plane',
overview: 'Overview',
tap: 'Tap',
top: 'Top',
routes: 'Top Routes',
community: 'Community',
controlplane: <Trans>menuItemControlPlane</Trans>,
tap: <Trans>menuItemTap</Trans>,
top: <Trans>menuItemTop</Trans>,
routes: <Trans>menuItemRoutes</Trans>,
community: <Trans>menuItemCommunity</Trans>,
gateways: <Trans>menuItemGateway</Trans>,
};
class BreadcrumbHeader extends React.Component {

View File

@ -2,6 +2,7 @@ import { BrowserRouter } from 'react-router-dom';
import BreadcrumbHeader from './BreadcrumbHeader.jsx';
import React from 'react';
import { mount } from 'enzyme';
import { i18nWrap } from '../../test/testHelpers.jsx';
const loc = {
pathname: '',
@ -48,13 +49,13 @@ describe('Tests for <BreadcrumbHeader>', () => {
it("renders correct breadcrumb text for top-level pages [Control Plane]",
() => {
loc.pathname = "/controlplane";
const component = mount(
const component = mount(i18nWrap(
<BrowserRouter>
<BreadcrumbHeader
location={loc}
pathPrefix="" />
</BrowserRouter>
);
));
const crumbs = component.find("span");
expect(crumbs).toHaveLength(1);
const crumbText = crumbs.reduce((acc, crumb) => {

View File

@ -1,3 +1,4 @@
import { Plural, Trans } from '@lingui/macro';
import PropTypes from 'prop-types';
import React from 'react';
import Step from '@material-ui/core/Step';
@ -17,9 +18,9 @@ const styles = theme => ({
function getSteps(numResources, resource) {
return [
{ label: 'Controller successfully installed' },
{ label: `${numResources || 'No'} ${resource}s detected` },
{ label: `Connect your first ${resource}`, content: incompleteMeshMessage() },
{ label: <Trans>controllerInstalledMsg</Trans>, key: 'installedMsg' },
{ label: <Plural value={numResources} zero={resource} one={resource} other={resource} />, key: 'resourceMsg' },
{ label: <Trans>connectResourceMsg {resource}</Trans>, content: incompleteMeshMessage(), key: 'connectMsg' },
];
}
@ -29,7 +30,7 @@ const CallToAction = ({ resource, numResources, classes }) => {
return (
<React.Fragment>
<Typography>The service mesh was successfully installed!</Typography>
<Typography><Trans>serviceMeshInstalledMsg</Trans></Typography>
<Stepper
activeStep={lastStep}
className={classes.instructions}
@ -40,7 +41,7 @@ const CallToAction = ({ resource, numResources, classes }) => {
props.completed = i < lastStep; // select the last step as the currently active one
return (
<Step key={step.label} {...props}>
<Step key={step.key} {...props}>
<StepLabel>{step.label}</StepLabel>
<StepContent>{step.content}</StepContent>
</Step>

View File

@ -285,7 +285,7 @@ class CheckModal extends React.Component {
{success !== undefined &&
<Grid item>
<SimpleChip
label={success ? 'Success' : 'Error'}
label={success ? <Trans>labelSuccess</Trans> : <Trans>labelError</Trans>}
type={success ? 'good' : 'bad'} />
</Grid>
}
@ -316,11 +316,11 @@ class CheckModal extends React.Component {
<DialogActions>
<Button onClick={this.runCheck} color="primary">
Re-Run Check
<Trans>buttonReRunCheck</Trans>
</Button>
<Button onClick={this.handleOpenChange} color="primary">
Close
<Trans>buttonClose</Trans>
</Button>
</DialogActions>
</Dialog>

View File

@ -13,6 +13,7 @@ import InputLabel from '@material-ui/core/InputLabel';
import NoteAddIcon from '@material-ui/icons/NoteAdd';
import PropTypes from 'prop-types';
import React from 'react';
import { Trans } from '@lingui/macro';
import Typography from '@material-ui/core/Typography';
import _isEmpty from 'lodash/isEmpty';
import { withContext } from './util/AppContext.jsx';
@ -131,7 +132,7 @@ class ConfigureProfilesMsg extends React.Component {
color="primary"
size="small"
onClick={this.handleClickOpen}>
Create Service Profile
<Trans>buttonCreateServiceProfile</Trans>
</Button>
);
}
@ -144,7 +145,7 @@ class ConfigureProfilesMsg extends React.Component {
disabled={disableDownloadButton}
onClick={() => this.handleClose(downloadUrl)}
color="primary">
Download
<Trans>buttonDownload</Trans>
</Button>
);
@ -155,11 +156,12 @@ class ConfigureProfilesMsg extends React.Component {
open={open}
onClose={this.handleClose}
aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">New service profile</DialogTitle>
<DialogTitle id="form-dialog-title">
<Trans>New service profile</Trans>
</DialogTitle>
<DialogContent>
<DialogContentText>
To create a service profile, download a profile and then apply it
with `kubectl apply`.
<Trans>formCreateServiceProfileHelpText</Trans>
</DialogContentText>
<FormControl
className={classes.textField}
@ -173,8 +175,7 @@ class ConfigureProfilesMsg extends React.Component {
aria-describedby="component-error-text" />
{error.service && (
<FormHelperText id="component-error-text">
Service name must consist of lower case alphanumeric characters or &#45;
start with an alphabetic character, and end with an alphanumeric character
<Trans>formServiceNameErrorText</Trans>
</FormHelperText>
)}
</FormControl>
@ -190,15 +191,14 @@ class ConfigureProfilesMsg extends React.Component {
aria-describedby="component-error-text" />
{error.namespace && (
<FormHelperText id="component-error-text">
Namespace must consist of lower case alphanumeric characters or &#45;
and must start and end with an alphanumeric character
<Trans>formNamespaceErrorText</Trans>
</FormHelperText>
)}
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary">
Cancel
<Trans>buttonCancel</Trans>
</Button>
{disableDownloadButton ?
downloadButton :
@ -221,9 +221,7 @@ class ConfigureProfilesMsg extends React.Component {
return (
<CardContent>
<Typography component="div">
No named route traffic found. This could be because the service is
not receiving any traffic, or because there is no service profile
configured. Does the service have a service profile?
<Trans>formNoNamedRouteTrafficFound</Trans>
{this.renderDownloadProfileForm()}
</Typography>
</CardContent>

View File

@ -14,6 +14,7 @@ import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import { Trans } from '@lingui/macro';
import { withStyles } from '@material-ui/core/styles';
const styles = theme => ({
@ -131,7 +132,7 @@ class ExpandableTable extends React.Component {
open={open}
onClose={this.handleDialogClose}
aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Request Details</DialogTitle>
<DialogTitle id="form-dialog-title"><Trans>tableTitleRequestDetails</Trans></DialogTitle>
<DialogContent>
{expandedRowRender(datum, classes.expandedWrap)}
</DialogContent>

View File

@ -99,7 +99,7 @@ class Gateways extends React.Component {
<Spinner />
) : (
<div>
{noMetrics ? <div>No resources detected.</div> : null}
{noMetrics ? <div><Trans>noResourcesDetectedMsg</Trans></div> : null}
{noMetrics ? null : (
<div className="page-section">
<MetricsTable

View File

@ -356,7 +356,7 @@ class Octopus extends React.Component {
Octopus.propTypes = {
api: PropTypes.shape({
ResourceLink: PropTypes.element,
ResourceLink: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
}),
neighbors: PropTypes.shape({}),
resource: PropTypes.shape({}),

View File

@ -1,6 +1,7 @@
import CardContent from '@material-ui/core/CardContent';
import PropTypes from 'prop-types';
import React from 'react';
import { Trans } from '@lingui/macro';
import Typography from '@material-ui/core/Typography';
import _isEmpty from 'lodash/isEmpty';
import _startCase from 'lodash/startCase';
@ -32,11 +33,13 @@ class QueryToCliCmd extends React.Component {
render = () => {
const { cmdName, query, resource, controllerNamespace } = this.props;
const cmdNameDisplay = _startCase(cmdName);
return (
_isEmpty(resource) ? null :
<CardContent>
<Typography variant="caption" gutterBottom>
Current {_startCase(cmdName)} query
<Trans>Current {cmdNameDisplay} query</Trans>
</Typography>
<br />

View File

@ -1,6 +1,7 @@
import QueryToCliCmd from './QueryToCliCmd.jsx';
import React from 'react';
import { mount } from 'enzyme';
import { i18nWrap } from '../../test/testHelpers.jsx';
describe('QueryToCliCmd', () => {
it('renders a query as a linkerd CLI command', () => {
@ -10,12 +11,12 @@ describe('QueryToCliCmd', () => {
"scheme": ""
}
let component = mount(
let component = mount(i18nWrap(
<QueryToCliCmd
cmdName="routes"
query={query}
resource={query.resource}
controllerNamespace="linkerd" />
controllerNamespace="linkerd" />)
);
expect(component).toIncludeText("Current Routes query");
@ -28,12 +29,12 @@ describe('QueryToCliCmd', () => {
"namespace": "linkerd"
}
let component = mount(
let component = mount(i18nWrap(
<QueryToCliCmd
cmdName="routes"
query={query}
resource={query.resource}
controllerNamespace="my-linkerd-ns" />
controllerNamespace="my-linkerd-ns" />)
);
expect(component).toIncludeText("Current Routes query");
@ -49,12 +50,12 @@ describe('QueryToCliCmd', () => {
"authority": "foo.bar:8080"
}
let component = mount(
let component = mount(i18nWrap(
<QueryToCliCmd
cmdName="tap"
query={query}
resource={query.resource}
controllerNamespace="linkerd" />
controllerNamespace="linkerd" />)
);
expect(component).toIncludeText("Current Tap query");
@ -71,12 +72,12 @@ describe('QueryToCliCmd', () => {
"authority": "foo.bar:8080"
}
let component = mount(
let component = mount(i18nWrap(
<QueryToCliCmd
cmdName="tap"
query={query}
resource={query.resource}
controllerNamespace="linkerd" />
controllerNamespace="linkerd" />)
);
expect(component).toIncludeText("Current Tap query");
@ -89,12 +90,12 @@ describe('QueryToCliCmd', () => {
"namespace": "linkerd"
}
let component = mount(
let component = mount(i18nWrap(
<QueryToCliCmd
cmdName="top"
query={query}
resource={query.resource}
controllerNamespace="linkerd" />
controllerNamespace="linkerd" />)
);
expect(component).toIncludeText("Current Top query");
@ -109,12 +110,12 @@ describe('QueryToCliCmd', () => {
"theLimitDoesNotExist": 999
}
let component = mount(
let component = mount(i18nWrap(
<QueryToCliCmd
cmdName="tap"
query={query}
resource={query.resource}
controllerNamespace="linkerd" />
controllerNamespace="linkerd" />)
);
expect(component).toIncludeText("Current Tap query");

View File

@ -367,11 +367,11 @@ export class ResourceDetailBase extends React.Component {
<Grid item><Typography variant="h5">{resourceType}/{resourceName}</Typography></Grid>
<Grid item>
<Grid container spacing={1}>
{showNoTrafficMsg ? <Grid item><SimpleChip label="no traffic" type="warning" /></Grid> : null}
{showNoTrafficMsg ? <Grid item><SimpleChip label={<Trans>columnTitleNoTraffic</Trans>} type="warning" /></Grid> : null}
<Grid item>
{resourceIsMeshed ?
<SimpleChip label="meshed" type="good" /> :
<SimpleChip label="unmeshed" type="bad" />
<SimpleChip label={<Trans>columnTitleMeshed</Trans>} type="good" /> :
<SimpleChip label={<Trans>columnTitleUnmeshed</Trans>} type="bad" />
}
</Grid>
</Grid>

View File

@ -1,3 +1,4 @@
import { Plural, Trans } from '@lingui/macro';
import { formatDistanceToNow, subSeconds } from 'date-fns';
import { handlePageVisibility, withPageVisibility } from './util/PageVisibility.jsx';
import BaseTable from './BaseTable.jsx';
@ -13,7 +14,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import Spinner from './util/Spinner.jsx';
import StatusTable from './StatusTable.jsx';
import { Trans } from '@lingui/macro';
import Typography from '@material-ui/core/Typography';
import _compact from 'lodash/compact';
import _countBy from 'lodash/countBy';
@ -107,7 +107,7 @@ class ServiceMesh extends React.Component {
const { productName, releaseVersion, controllerNamespace } = this.props;
return [
{ key: 1, name: `${productName} version`, value: releaseVersion },
{ key: 1, name: <Trans>{productName} version</Trans>, value: releaseVersion },
{ key: 2, name: <Trans>{productName} namespace</Trans>, value: controllerNamespace },
{ key: 3, name: <Trans>Control plane components</Trans>, value: components.length },
{ key: 4, name: <Trans>Data plane proxies</Trans>, value: this.proxyCount() },
@ -223,7 +223,7 @@ class ServiceMesh extends React.Component {
<React.Fragment>
<Grid container justify="space-between">
<Grid item xs={3}>
<Typography variant="h6">Control plane</Typography>
<Typography variant="h6"><Trans>Control plane</Trans></Typography>
</Grid>
<Grid item xs={3}>
<Typography align="right"><Trans>componentsMsg</Trans></Typography>
@ -243,7 +243,7 @@ class ServiceMesh extends React.Component {
renderServiceMeshDetails() {
return (
<React.Fragment>
<Typography variant="h6">Service mesh details</Typography>
<Typography variant="h6"><Trans>Service mesh details</Trans></Typography>
<BaseTable
tableClassName="metric-table"
@ -263,14 +263,18 @@ class ServiceMesh extends React.Component {
let numUnadded = 0;
if (_isEmpty(nsStatuses)) {
message = 'No resources detected.';
message = <Trans>noNamespacesDetectedMsg</Trans>;
} else {
const meshedCount = _countBy(nsStatuses, pod => {
return pod.meshedPercent.get() > 0;
});
numUnadded = meshedCount.false || 0;
message = numUnadded === 0 ? `All namespaces have a ${productName} install.` :
`${numUnadded} ${numUnadded === 1 ? 'namespace has' : 'namespaces have'} no meshed resources.`;
message = numUnadded === 0 ? <Trans>All namespaces have a {productName} install.</Trans> : (
<Plural
value={numUnadded}
one="# namespace has no meshed resources"
other="# namespaces have no meshed resources" />
);
}
return (

View File

@ -8,6 +8,7 @@ import sinon from 'sinon';
import sinonStubPromise from 'sinon-stub-promise';
import Spinner from './util/Spinner.jsx';
import { mount } from 'enzyme';
import { i18nWrap } from '../../test/testHelpers.jsx';
sinonStubPromise(sinon);
@ -91,7 +92,7 @@ describe('ServiceMesh', () => {
ok: true,
json: () => Promise.resolve({ metrics: [] })
});
component = mount(routerWrap(ServiceMesh));
component = mount(i18nWrap(routerWrap(ServiceMesh)));
return withPromise(() => {
component.update();
@ -107,7 +108,7 @@ describe('ServiceMesh', () => {
ok: true,
json: () => Promise.resolve({ metrics: [] })
});
component = mount(routerWrap(ServiceMesh));
component = mount(i18nWrap(routerWrap(ServiceMesh)));
return withPromise(() => {
component.update();
@ -122,7 +123,7 @@ describe('ServiceMesh', () => {
ok: true,
json: () => Promise.resolve({ metrics: [] })
});
component = mount(routerWrap(ServiceMesh));
component = mount(i18nWrap(routerWrap(ServiceMesh)));
return withPromise(() => {
component.update();
@ -138,7 +139,7 @@ describe('ServiceMesh', () => {
ok: true,
json: () => Promise.resolve({})
});
component = mount(routerWrap(ServiceMesh));
component = mount(i18nWrap(routerWrap(ServiceMesh)));
return withPromise(() => {
expect(component).toIncludeText("No namespaces detected");
@ -163,7 +164,7 @@ describe('ServiceMesh', () => {
ok: true,
json: () => Promise.resolve(nsAllResourcesAdded)
});
component = mount(routerWrap(ServiceMesh));
component = mount(i18nWrap(routerWrap(ServiceMesh)));
return withPromise(() => {
expect(component).toIncludeText("4 namespaces have no meshed resources.");
@ -183,7 +184,7 @@ describe('ServiceMesh', () => {
ok: true,
json: () => Promise.resolve(nsOneResourceNotAdded)
});
component = mount(routerWrap(ServiceMesh));
component = mount(i18nWrap(routerWrap(ServiceMesh)));
return withPromise(() => {
expect(component).toIncludeText("1 namespace has no meshed resources.");
@ -200,7 +201,7 @@ describe('ServiceMesh', () => {
ok: true,
json: () => Promise.resolve(nsAllResourcesAdded)
});
component = mount(routerWrap(ServiceMesh));
component = mount(i18nWrap(routerWrap(ServiceMesh)));
return withPromise(() => {
expect(component).toIncludeText("All namespaces have a ShinyProductName install.");

View File

@ -25,18 +25,18 @@ const columnConfig = {
width: 200,
wrapDotsAt: 7, // dots take up more than one line in the table; space them out
dotExplanation: status => {
return status.value === 'good' ? 'is up and running' : 'has not been started';
return status.value === 'good' ? <Trans>statusExplanationGood</Trans> : <Trans>statusExplanationNotStarted</Trans>;
},
},
'Proxy Status': {
width: 250,
wrapDotsAt: 9,
dotExplanation: pod => {
const addedStatus = !pod.added ? 'Not in mesh' : 'Added to mesh';
const addedStatus = !pod.added ? <Trans>statusExplanationNotInMesh</Trans> : <Trans>statusExplanationInMesh</Trans>;
return (
<React.Fragment>
<div>Pod status: {pod.status}</div>
<div><Trans>Pod status: {pod.status}</Trans></div>
<div>{addedStatus}</div>
</React.Fragment>
);

View File

@ -138,39 +138,39 @@ const itemDisplay = (title, value) => {
const requestInitSection = d => (
<React.Fragment>
<Typography variant="subtitle2">Request Init</Typography>
<Typography variant="subtitle2"><Trans>tableTitleRequestInit</Trans></Typography>
<br />
<List dense>
{itemDisplay('Authority', _get(d, 'requestInit.http.requestInit.authority'))}
{itemDisplay('Path', _get(d, 'requestInit.http.requestInit.path'))}
{itemDisplay('Scheme', _get(d, 'requestInit.http.requestInit.scheme.registered'))}
{itemDisplay('Method', _get(d, 'requestInit.http.requestInit.method.registered'))}
{headersDisplay('Headers', _get(d, 'requestInit.http.requestInit.headers'))}
{itemDisplay(<Trans>formAuthority</Trans>, _get(d, 'requestInit.http.requestInit.authority'))}
{itemDisplay(<Trans>formPath</Trans>, _get(d, 'requestInit.http.requestInit.path'))}
{itemDisplay(<Trans>formScheme</Trans>, _get(d, 'requestInit.http.requestInit.scheme.registered'))}
{itemDisplay(<Trans>formMethod</Trans>, _get(d, 'requestInit.http.requestInit.method.registered'))}
{headersDisplay(<Trans>formHeaders</Trans>, _get(d, 'requestInit.http.requestInit.headers'))}
</List>
</React.Fragment>
);
const responseInitSection = d => _isEmpty(d.responseInit) ? null : (
<React.Fragment>
<Typography variant="subtitle2">Response Init</Typography>
<Typography variant="subtitle2"><Trans>tableTitleResponseInit</Trans></Typography>
<br />
<List dense>
{itemDisplay('HTTP Status', _get(d, 'responseInit.http.responseInit.httpStatus'))}
{itemDisplay('Latency', formatTapLatency(_get(d, 'responseInit.http.responseInit.sinceRequestInit')))}
{headersDisplay('Headers', _get(d, 'responseInit.http.responseInit.headers'))}
{itemDisplay(<Trans>formHTTPStatus</Trans>, _get(d, 'responseInit.http.responseInit.httpStatus'))}
{itemDisplay(<Trans>formLatency</Trans>, formatTapLatency(_get(d, 'responseInit.http.responseInit.sinceRequestInit')))}
{headersDisplay(<Trans>formHeaders</Trans>, _get(d, 'responseInit.http.responseInit.headers'))}
</List>
</React.Fragment>
);
const responseEndSection = d => _isEmpty(d.responseEnd) ? null : (
<React.Fragment>
<Typography variant="subtitle2">Response End</Typography>
<Typography variant="subtitle2"><Trans>tableTitleResponseEnd</Trans></Typography>
<br />
<List dense>
{itemDisplay('GRPC Status', _isNull(_get(d, 'responseEnd.http.responseEnd.eos')) ? 'N/A' : grpcStatusCodes[_get(d, 'responseEnd.http.responseEnd.eos.grpcStatusCode')])}
{itemDisplay('Latency', formatTapLatency(_get(d, 'responseEnd.http.responseEnd.sinceResponseInit')))}
{itemDisplay('Response Length (B)', formatWithComma(_get(d, 'responseEnd.http.responseEnd.responseBytes')))}
{itemDisplay(<Trans>formGRPCStatus</Trans>, _isNull(_get(d, 'responseEnd.http.responseEnd.eos')) ? 'N/A' : grpcStatusCodes[_get(d, 'responseEnd.http.responseEnd.eos.grpcStatusCode')])}
{itemDisplay(<Trans>formLatency</Trans>, formatTapLatency(_get(d, 'responseEnd.http.responseEnd.sinceResponseInit')))}
{itemDisplay(<Trans>formResponseLengthB</Trans>, formatWithComma(_get(d, 'responseEnd.http.responseEnd.responseBytes')))}
</List>
</React.Fragment>
);

View File

@ -36,7 +36,6 @@ import _noop from 'lodash/noop';
import _omit from 'lodash/omit';
import _pick from 'lodash/pick';
import _some from 'lodash/some';
import _startCase from 'lodash/startCase';
import _uniq from 'lodash/uniq';
import _values from 'lodash/values';
import { withStyles } from '@material-ui/core/styles';
@ -245,7 +244,7 @@ class TapQueryForm extends React.Component {
return (
<React.Fragment>
<InputLabel htmlFor={resourceKey}>{_startCase(resourceKey)}</InputLabel>
<InputLabel htmlFor={resourceKey}>{resourceKey === 'resource' ? <Trans>formResource</Trans> : <Trans>formToResource</Trans>}</InputLabel>
<Select
value={!nsEmpty && resourceOptions.includes(query[resourceKey]) ? query[resourceKey] : ''}
onChange={this.handleFormChange(resourceKey)}
@ -364,20 +363,20 @@ class TapQueryForm extends React.Component {
))
}
</Select>
<FormHelperText>Display requests with this :authority</FormHelperText>
<FormHelperText><Trans>formAuthorityHelpText</Trans></FormHelperText>
</FormControl>
</Grid>
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
{ this.renderTextInput(<Trans>formPath</Trans>, 'path', 'Display requests with paths that start with this prefix') }
{ this.renderTextInput(<Trans>formPath</Trans>, 'path', <Trans>formPathHelpText</Trans>) }
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
{ this.renderTextInput(<Trans>formScheme</Trans>, 'scheme', 'Display requests with this scheme') }
{ this.renderTextInput(<Trans>formScheme</Trans>, 'scheme', <Trans>formSchemeHelpText</Trans>) }
</Grid>
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
{ this.renderTextInput(<Trans>formMaxRPS</Trans>, 'maxRps', `Maximum requests per second to tap. Default ${defaultMaxRps}`) }
{ this.renderTextInput(<Trans>formMaxRPS</Trans>, 'maxRps', <Trans>formMaxRPSHelpText {defaultMaxRps}</Trans>) }
</Grid>
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
<FormControl className={classes.formControl}>
@ -393,7 +392,7 @@ class TapQueryForm extends React.Component {
))
}
</Select>
<FormHelperText>Display requests with this HTTP method</FormHelperText>
<FormHelperText><Trans>formHTTPMethodHelpText</Trans></FormHelperText>
</FormControl>
</Grid>
</Grid>

View File

@ -203,11 +203,11 @@ class TopRoutes extends React.Component {
<Grid container direction="column" spacing={2}>
<Grid item container spacing={4} alignItems="center" justify="flex-start">
<Grid item>
{ this.renderNamespaceDropdown('Namespace', 'namespace', 'Namespace to query') }
{ this.renderNamespaceDropdown(<Trans>formNamespace</Trans>, 'namespace', <Trans>formNamespaceHelpText</Trans>) }
</Grid>
<Grid item>
{ this.renderResourceDropdown('Resource', 'resource_name', 'resource_type', 'Resource to query') }
{ this.renderResourceDropdown(<Trans>formResource</Trans>, 'resource_name', 'resource_type', <Trans>formResourceHelpText</Trans>) }
</Grid>
<Grid item>
@ -216,7 +216,7 @@ class TopRoutes extends React.Component {
variant="outlined"
disabled={requestInProgress || !query.namespace || !query.resource_type}
onClick={this.handleBtnClick(true)}>
Start
<Trans>buttonStart</Trans>
</Button>
</Grid>
@ -226,23 +226,23 @@ class TopRoutes extends React.Component {
variant="outlined"
disabled={!requestInProgress}
onClick={this.handleBtnClick(false)}>
Stop
<Trans>buttonStop</Trans>
</Button>
</Grid>
</Grid>
<Grid item container spacing={4} alignItems="center" justify="flex-start">
<Grid item>
{ this.renderNamespaceDropdown(<Trans>formToNamespace</Trans>, 'to_namespace', 'Namespace of target resource') }
{ this.renderNamespaceDropdown(<Trans>formToNamespace</Trans>, 'to_namespace', <Trans>formToNamespaceHelpText</Trans>) }
</Grid>
<Grid item>
{ this.renderResourceDropdown(<Trans>formToResource</Trans>, 'to_name', 'to_type', 'Target resource') }
{ this.renderResourceDropdown(<Trans>formToResource</Trans>, 'to_name', 'to_type', <Trans>formToResourceHelpText</Trans>) }
</Grid>
</Grid>
</Grid>
<Divider light className={classes.root} />
<Typography variant="caption">You can also create a new profile <ConfigureProfilesMsg showAsIcon /></Typography>
<Typography variant="caption"><Trans>createNewProfileMsg</Trans> <ConfigureProfilesMsg showAsIcon /></Typography>
</CardContent>
);
}

View File

@ -8,6 +8,7 @@ import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import TopModule from './TopModule.jsx';
import TopRoutesModule from './TopRoutesModule.jsx';
import { Trans } from '@lingui/macro';
import _isEmpty from 'lodash/isEmpty';
import _noop from 'lodash/noop';
import { withStyles } from '@material-ui/core/styles';
@ -91,8 +92,8 @@ class TopRoutesTabs extends React.Component {
onChange={this.handleChange}
indicatorColor="primary"
textColor="primary">
<Tab label="Live Calls" />
<Tab label="Route Metrics" />
<Tab label={<Trans>tabLiveCalls</Trans>} />
<Tab label={<Trans>tabRouteMetrics</Trans>} />
</Tabs>
</AppBar>
{value === 0 && this.renderTopComponent()}

View File

@ -1,6 +1,7 @@
import Button from '@material-ui/core/Button';
import PropTypes from 'prop-types';
import React from 'react';
import { Trans } from '@lingui/macro';
import Typography from '@material-ui/core/Typography';
import { apiErrorPropType } from './util/ApiHelpers.jsx';
import { withContext } from './util/AppContext.jsx';
@ -39,25 +40,31 @@ class Version extends React.Component {
if (!latestVersion) {
return (
<Typography className={classes.versionMsg}>
Version check failed{error ? `: ${error.statusText}` : ''}.
<Trans>Version check failed{error ? `: ${error.statusText}` : ''}.</Trans>
</Typography>
);
}
if (isLatest) {
return <Typography className={classes.versionMsg}>Linkerd is up to date.</Typography>;
return <Typography className={classes.versionMsg}><Trans>LinkerdIsUpToDateMsg</Trans></Typography>;
}
const versionText = this.numericVersion(latestVersion);
return (
<div>
<Typography className={classes.versionMsg}>A new version ({this.numericVersion(latestVersion)}) is available.</Typography>
<Typography className={classes.versionMsg}>
<Trans>
A new version ({versionText}) is available.
</Trans>
</Typography>
<Button
className={classes.updateBtn}
variant="contained"
color="primary"
target="_blank"
href="https://versioncheck.linkerd.io/update">
Update Now
<Trans>Update Now</Trans>
</Button>
</div>
);
@ -66,7 +73,7 @@ class Version extends React.Component {
render() {
const { classes, releaseVersion, productName } = this.props;
const channel = this.versionChannel(releaseVersion);
let message = `Running ${productName || 'controller'}`;
let message = ` ${productName || 'controller'}`;
message += ` ${this.numericVersion(releaseVersion)}`;
if (channel) {
message += ` (${channel})`;
@ -75,7 +82,7 @@ class Version extends React.Component {
return (
<div className={classes.version}>
<Typography className={classes.versionMsg}>{message}</Typography>
<Typography className={classes.versionMsg}><Trans>Running</Trans>{message}</Typography>
{this.renderVersionCheck()}
</div>
);

View File

@ -1,30 +1,31 @@
import React from 'react';
import Version from './Version.jsx';
import { mount } from 'enzyme';
import { i18nWrap } from '../../test/testHelpers.jsx';
describe('Version', () => {
let curVer = "edge-1.2.3";
let newVer = "edge-2.3.4";
it('renders up to date message when versions match', () => {
const component = mount(
const component = mount(i18nWrap(
<Version
classes={{}}
isLatest
latestVersion={curVer}
releaseVersion={curVer} />
releaseVersion={curVer} />)
);
expect(component).toIncludeText("Linkerd is up to date");
});
it('renders update message when versions do not match', () => {
const component = mount(
const component = mount(i18nWrap(
<Version
classes={{}}
isLatest={false}
latestVersion={newVer}
releaseVersion={curVer} />
releaseVersion={curVer} />)
);
expect(component).toIncludeText("A new version (2.3.4) is available.");
@ -33,14 +34,14 @@ describe('Version', () => {
it('renders error when version check fails', () => {
let errMsg = "Fake error";
const component = mount(
const component = mount(i18nWrap(
<Version
classes={{}}
error={{
statusText: errMsg,
}}
isLatest={false}
releaseVersion={curVer} />
releaseVersion={curVer} />)
);
expect(component).toIncludeText("Version check failed: Fake error.");

View File

@ -30,7 +30,7 @@ function SimpleChip(props) {
}
SimpleChip.propTypes = {
label: PropTypes.string.isRequired,
label: PropTypes.shape({}).isRequired,
type: PropTypes.string.isRequired,
};

View File

@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import TapLink from '../TapLink.jsx';
import Tooltip from '@material-ui/core/Tooltip';
import { Trans } from '@lingui/macro';
import _each from 'lodash/each';
import _get from 'lodash/get';
import _has from 'lodash/has';
@ -228,9 +229,9 @@ export const processTapEvent = jsonString => {
const displayLimit = 3; // how many upstreams/downstreams to display in the popover table
const popoverSrcDstColumns = [
{ title: 'Source', dataIndex: 'source' },
{ title: <Trans>columnTitleSource</Trans>, dataIndex: 'source' },
{ title: '', key: 'arrow', render: () => <FontAwesomeIcon icon={faLongArrowAltRight} /> },
{ title: 'Destination', dataIndex: 'destination' },
{ title: <Trans>columnTitleDestination</Trans>, dataIndex: 'destination' },
];
const getPodOwner = (labels, ResourceLink) => {
@ -310,8 +311,8 @@ const popoverResourceTable = (d, ResourceLink) => { // eslint-disable-line no-un
};
export const directionColumn = d => (
<Tooltip title={d} placement="right">
<span>{d === 'INBOUND' ? 'FROM' : 'TO'}</span>
<Tooltip title={d === 'INBOUND' ? <Trans>tooltipInbound</Trans> : <Trans>tooltipOutbound</Trans>} placement="right">
<span>{d === 'INBOUND' ? <Trans>columnTitleFrom</Trans> : <Trans>columnTitleTo</Trans>}</span>
</Tooltip>
);

View File

@ -1,10 +1,25 @@
{
"404Msg": "Page not found.",
"A new version ({versionText}) is available.": "A new version ({versionText}) is available.",
"Add {0} to the k8s.yml file<0/><1/>Then run {inject} to add it to the service mesh": "Add {0} to the k8s.yml file<0/><1/>Then run {inject} to add it to the service mesh",
"All namespaces have a {productName} install.": "All namespaces have a {productName} install.",
"Control plane": "Control plane",
"Control plane components": "Control plane components",
"Current {cmdNameDisplay} query": "Current {cmdNameDisplay} query",
"Data plane proxies": "Data plane proxies",
"LinkerdIsUpToDateMsg": "Linkerd is up to date.",
"New service profile": "New service profile",
"NoDataToDisplayMsg": "No data to display",
"Pod status: {0}": "Pod status: {0}",
"Running": "Running",
"Service mesh details": "Service mesh details",
"Update Now": "Update Now",
"Version check failed{0}.": "Version check failed{0}.",
"buttonCancel": "Cancel",
"buttonClose": "Close",
"buttonCreateServiceProfile": "Create Service Profile",
"buttonDownload": "Download",
"buttonReRunCheck": "Re-Run Check",
"buttonReset": "Reset",
"buttonRunLinkerdCheck": "Run Linkerd Check",
"buttonStart": "Start",
@ -15,7 +30,9 @@
"columnTitleClusterName": "Cluster Name",
"columnTitleCount": "Count",
"columnTitleDeployment": "Deployment",
"columnTitleDestination": "Destination",
"columnTitleDirection": "Direction",
"columnTitleFrom": "FROM",
"columnTitleGRPCStatus": "GRPC Status",
"columnTitleGrafana": "Grafana",
"columnTitleHTTPStatus": "HTTP Status",
@ -30,6 +47,7 @@
"columnTitleMethod": "Method",
"columnTitleName": "Name",
"columnTitleNamespace": "Namespace",
"columnTitleNoTraffic": "No Traffic",
"columnTitleOpenConnections": "Connections",
"columnTitleP50Latency": "P50 Latency",
"columnTitleP95Latency": "P95 Latency",
@ -43,23 +61,51 @@
"columnTitleRoute": "Route",
"columnTitleSecured": "Secured",
"columnTitleService": "Service",
"columnTitleSource": "Source",
"columnTitleSuccessRate": "Success Rate",
"columnTitleTap": "Tap",
"columnTitleTo": "TO",
"columnTitleUnmeshed": "Unmeshed",
"columnTitleValue": "Value",
"columnTitleWeight": "Weight",
"columnTitleWorst": "Worst",
"columnTitleWriteRate": "Write Bytes / sec",
"componentsMsg": "Components",
"connectResourceMsg {resource}": "Connect your first {resource}",
"controllerInstalledMsg": "Controller successfully installed",
"createNewProfileMsg": "You can also create a new profile",
"formAuthority": "Authority",
"formAuthorityHelpText": "Display requests with this :authority",
"formCreateServiceProfileHelpText": "To create a service profile, download a profile and then apply it with `kubectl apply`.",
"formGRPCStatus": "GRPC Status",
"formHTTPMethod": "HTTP Method",
"formHTTPMethodHelpText": "Display requests with this HTTP method",
"formHTTPStatus": "HTTP Status",
"formHeaders": "Headers",
"formHideFilters": "Hide filters",
"formLatency": "Latency",
"formMaxRPS": "Max RPS",
"formMaxRPSHelpText {defaultMaxRps}": "Maximum requests per second to tap. Default {defaultMaxRps}",
"formMethod": "Method",
"formNamespace": "Namespace",
"formNamespaceErrorText": "Namespace must consist of lower case alphanumeric characters or '-' and must start and end with an alphanumeric character",
"formNamespaceHelpText": "Namespace to query",
"formNoNamedRouteTrafficFound": "No named route traffic found. This could be because the service is not receiving any traffic, or because there is no service profile configured. Does the service have a service profile?",
"formPath": "Path",
"formPathHelpText": "Display requests with paths that start with this prefix",
"formResource": "Resource",
"formResourceHelpText": "Resource to query",
"formResponseLengthB": "Response Length (B)",
"formScheme": "Scheme",
"formSchemeHelpText": "Display requests with this scheme",
"formServiceNameErrorText": "Service name must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character",
"formShowFilters": "Show more filters",
"formToNamespace": "To Namespace",
"formToNamespaceHelpText": "Namespace of target resource",
"formToResource": "To Resource",
"formToResourceHelpText": "Target resource",
"labelError": "Error",
"labelSuccess": "Success",
"menuItemCommunity": "Community",
"menuItemControlPlane": "Control Plane",
"menuItemCronJobs": "Cron Jobs",
@ -80,12 +126,20 @@
"menuItemTap": "Tap",
"menuItemTop": "Top",
"menuItemTrafficSplits": "Traffic Splits",
"noNamespacesDetectedMsg": "No namespaces detected.",
"noResourcesDetectedMsg": "No resources detected.",
"podsAreInitializingMsg": "Pods are initializing",
"serviceMeshInstalledMsg": "The service mesh was successfully installed!",
"sidebarHeadingCluster": "Cluster",
"sidebarHeadingConfiguration": "Configuration",
"sidebarHeadingTools": "Tools",
"sidebarHeadingWorkloads": "Workloads",
"statusExplanationGood": "is up and running",
"statusExplanationInMesh": "Added to mesh",
"statusExplanationNotInMesh": "Not in mesh",
"statusExplanationNotStarted": "has not been started",
"tabLiveCalls": "Live Calls",
"tabRouteMetrics": "Route Metrics",
"tableTitleEdgesEmpty": "Edges",
"tableTitleEdgesWithIdentity {identity}": "Edges (Identity: {identity})",
"tableTitleGateways": "Gateways",
@ -94,11 +148,17 @@
"tableTitleLeafServices": "Leaf Services",
"tableTitleOutbound": "Outbound",
"tableTitlePods": "Pods",
"tableTitleRequestDetails": "Request Details",
"tableTitleRequestInit": "Request Init",
"tableTitleResponseEnd": "Response End",
"tableTitleResponseInit": "Response Init",
"tableTitleTCP": "TCP",
"tableTitleTCPMetrics": "TCP Metrics",
"tooltipInbound": "INBOUND",
"tooltipOutbound": "OUTBOUND",
"unspecifiedResourcesMsg": "one or more resources",
"{numUnadded} namespace has no meshed resources.": "{numUnadded} namespace has no meshed resources.",
"{numUnadded} namespaces have no meshed resources.": "{numUnadded} namespaces have no meshed resourcesß",
"{numResources, plural, zero {{resource}} one {{resource}} other {{resource}}}": "{numResources, plural, zero {No {resource}s detected} one {# {resource} detected} other {# {resource}s detected}}",
"{numUnadded, plural, one {# namespace has no meshed resources} other {# namespaces have no meshed resources}}": "{numUnadded, plural, one {# namespace has no meshed resources.} other {# namespaces have no meshed resources.}}",
"{productName} namespace": "{productName} namespace",
"{productName} version": "{productName} version"
}

View File

@ -1,10 +1,25 @@
{
"404Msg": "Página no encontrada.",
"A new version ({versionText}) is available.": "Una nueva version ({versionText}) está disponible",
"Add {0} to the k8s.yml file<0/><1/>Then run {inject} to add it to the service mesh": "Agrega {0} al archivo k8s.yml<0/><1/>Luego ejecuta {inject} para inyectarlo en la malla de servicios",
"All namespaces have a {productName} install.": "Todos los namespaces tienen una instalación de {productName}.",
"Control plane": "Plano de control",
"Control plane components": "Componentes del plano de control",
"Current {cmdNameDisplay} query": "Consulta {cmdNameDisplay} actual",
"Data plane proxies": "Proxies del plano de datos",
"LinkerdIsUpToDateMsg": "Linkerd está actualizado.",
"New service profile": "Nuevo service profile",
"NoDataToDisplayMsg": "No hay datos que mostrar",
"Pod status: {0}": "Estado pod: {0}",
"Running": "Ejecutando",
"Service mesh details": "Detalles de la malla de servicios",
"Update Now": "Actualizar Ahora",
"Version check failed{0}.": "Error al comprobar la versión{0}",
"buttonCancel": "Cancelar",
"buttonClose": "Cerrar",
"buttonCreateServiceProfile": "Crear Service Profile",
"buttonDownload": "Descargar",
"buttonReRunCheck": "Volver a Check",
"buttonReset": "Restablecer",
"buttonRunLinkerdCheck": "Ejecutar Linkerd Check",
"buttonStart": "Empezar",
@ -15,7 +30,9 @@
"columnTitleClusterName": "Nombre del Clúster",
"columnTitleCount": "Total",
"columnTitleDeployment": "Deployment",
"columnTitleDestination": "Destino",
"columnTitleDirection": "Dirección",
"columnTitleFrom": "DESDE",
"columnTitleGRPCStatus": "Estado GRPC",
"columnTitleGrafana": "Grafana",
"columnTitleHTTPStatus": "Estado HTTP",
@ -30,6 +47,7 @@
"columnTitleMethod": "Método",
"columnTitleName": "Nombre",
"columnTitleNamespace": "Namespace",
"columnTitleNoTraffic": "No Hay Tráfico",
"columnTitleOpenConnections": "Conexiones",
"columnTitleP50Latency": "Latencia P50",
"columnTitleP95Latency": "Latencia P95",
@ -43,23 +61,51 @@
"columnTitleRoute": "Ruta",
"columnTitleSecured": "Asegurado",
"columnTitleService": "Servicio",
"columnTitleSource": "Fuente",
"columnTitleSuccessRate": "Tasa de éxito",
"columnTitleTap": "Tap",
"columnTitleTo": "HACIA",
"columnTitleUnmeshed": "Ausente en la malla de servicios",
"columnTitleValue": "Valor",
"columnTitleWeight": "Peso",
"columnTitleWorst": "Peor",
"columnTitleWriteRate": "Escritura Bytes / seg",
"componentsMsg": "Componentes",
"connectResourceMsg {resource}": "Conecta tu primer {resource}",
"controllerInstalledMsg": "Controller instalado correctamente",
"createNewProfileMsg": "También puede crear un nuevo perfil",
"formAuthority": "Authority",
"formAuthorityHelpText": "Mostrar solicitudes con este :authority",
"formCreateServiceProfileHelpText": "Para crear un service profile, descargue un perfil y luego aplíquelo con `kubectl apply`.",
"formGRPCStatus": "Estado GRPC",
"formHTTPMethod": "Método HTTP",
"formHTTPMethodHelpText": "Mostrar solicitudes con este método HTTP",
"formHTTPStatus": "Estado HTTP",
"formHeaders": "Encabezados",
"formHideFilters": "Ocultar filtros",
"formLatency": "Latencia",
"formMaxRPS": "Max PPS",
"formMaxRPSHelpText {defaultMaxRps}": "Máximo de solicitudes por segundo para tap. Por defecto {defaultMaxRps}",
"formMethod": "Método",
"formNamespace": "Namespace",
"formNamespaceErrorText": "El namespace debe constar de caracteres alfanuméricos en minúscula o '-' y debe comenzar y terminar con un carácter alfanumérico",
"formNamespaceHelpText": "Namespace para consultar",
"formNoNamedRouteTrafficFound": "No se encontró tráfico de ruta con nombre. Esto podría deberse a que el servicio no está recibiendo tráfico o porque no hay ningún service profile configurado. ¿El servicio tiene un service profile?",
"formPath": "Ruta",
"formPathHelpText": "Mostrar solicitudes con rutas que comienzan con este prefijo",
"formResource": "Recurso",
"formResourceHelpText": "Recurso para consultar",
"formResponseLengthB": "Longitud De Respuesta (B)",
"formScheme": "Esquema",
"formSchemeHelpText": "Mostrar solicitudes con este esquema",
"formServiceNameErrorText": "El nombre del servicio debe constar de caracteres alfanuméricos en minúscula o '-' comenzar con un carácter alfabético y terminar con un carácter alfanumérico",
"formShowFilters": "Mostrar más filtros",
"formToNamespace": "Al Namespace",
"formToNamespaceHelpText": "Namespace del recurso de destino",
"formToResource": "Al Recurso",
"formToResourceHelpText": "Recurso destino",
"labelError": "Error",
"labelSuccess": "Éxito",
"menuItemCommunity": "Comunidad",
"menuItemControlPlane": "Plano de Control",
"menuItemCronJobs": "Cron Jobs",
@ -80,12 +126,20 @@
"menuItemTap": "Tap",
"menuItemTop": "Top",
"menuItemTrafficSplits": "Traffic Splits",
"noNamespacesDetectedMsg": "No se han encontrado namespaces.",
"noResourcesDetectedMsg": "No se han encontrado recursos.",
"podsAreInitializingMsg": "Los pods se están inicializando",
"serviceMeshInstalledMsg": "¡La malla de servicios se instaló correctamente!",
"sidebarHeadingCluster": "Clúster",
"sidebarHeadingConfiguration": "Configuración",
"sidebarHeadingTools": "Herramientas",
"sidebarHeadingWorkloads": "Cargas de trabajo",
"statusExplanationGood": "está en funcionamiento",
"statusExplanationInMesh": "Añadido a la malla",
"statusExplanationNotInMesh": "No en malla",
"statusExplanationNotStarted": "no se ha iniciado",
"tabLiveCalls": "Llamadas En Vivo",
"tabRouteMetrics": "Métricas De Ruta",
"tableTitleEdgesEmpty": "Bordes",
"tableTitleEdgesWithIdentity {identity}": "Bordes (Identidad: {identity})",
"tableTitleGateways": "Gateways",
@ -94,11 +148,17 @@
"tableTitleLeafServices": "Servicios Hoja",
"tableTitleOutbound": "Salida",
"tableTitlePods": "Pods",
"tableTitleRequestDetails": "Detalle Solicitudes",
"tableTitleRequestInit": "Solicitud Inicial",
"tableTitleResponseEnd": "Fin De Respuesta",
"tableTitleResponseInit": "Respuesta Inicial",
"tableTitleTCP": "TCP",
"tableTitleTCPMetrics": "Métricas TCP",
"tooltipInbound": "ENTRADA",
"tooltipOutbound": "SALIDA",
"unspecifiedResourcesMsg": "uno o más recursos",
"{numUnadded} namespace has no meshed resources.": "El namespace {numUnadded} no tiene recursos en la malla de servicios.",
"{numUnadded} namespaces have no meshed resources.": "Los namespaces {numUnadded} no tienen recursos en la malla de servicios.",
"{numResources, plural, zero {{resource}} one {{resource}} other {{resource}}}": "{numResources, plural, zero {No {resource}s detectados} one {# {resource} detectado} other {# {resource}s detectados}}",
"{numUnadded, plural, one {# namespace has no meshed resources} other {# namespaces have no meshed resources}}": "{numUnadded, plural, one {# namespace no tiene recursos en la malla de servicios.} other {# namespaces no tienen recursos en la malla de servicios.}}",
"{productName} namespace": "Namespace de {productName}",
"{productName} version": "Versión de {productName}"
}

View File

@ -3,6 +3,8 @@ import ApiHelpers from '../js/components/util/ApiHelpers.jsx';
import { createMemoryHistory } from 'history';
import React from 'react';
import { Route, Router } from 'react-router';
import { I18nProvider } from '@lingui/react';
import catalogEn from './../js/locales/en/messages.js';
const componentDefaultProps = {
api: ApiHelpers(''),
@ -11,6 +13,9 @@ const componentDefaultProps = {
releaseVersion: ''
};
const selectedLocale = 'en';
const selectedCatalog = catalogEn;
export function routerWrap(Component, extraProps={}, route="/", currentLoc="/") {
const createElement = (ComponentToWrap, props) => (
<ComponentToWrap {...(_merge({}, componentDefaultProps, props, extraProps))} />
@ -21,3 +26,13 @@ export function routerWrap(Component, extraProps={}, route="/", currentLoc="/")
</Router>
);
}
export function i18nWrap(Component) {
return (
<I18nProvider
language={selectedLocale}
catalogs={{ [selectedLocale]: selectedCatalog }}>
{Component}
</I18nProvider>
);
}