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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import QueryToCliCmd from './QueryToCliCmd.jsx'; import QueryToCliCmd from './QueryToCliCmd.jsx';
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { i18nWrap } from '../../test/testHelpers.jsx';
describe('QueryToCliCmd', () => { describe('QueryToCliCmd', () => {
it('renders a query as a linkerd CLI command', () => { it('renders a query as a linkerd CLI command', () => {
@ -10,12 +11,12 @@ describe('QueryToCliCmd', () => {
"scheme": "" "scheme": ""
} }
let component = mount( let component = mount(i18nWrap(
<QueryToCliCmd <QueryToCliCmd
cmdName="routes" cmdName="routes"
query={query} query={query}
resource={query.resource} resource={query.resource}
controllerNamespace="linkerd" /> controllerNamespace="linkerd" />)
); );
expect(component).toIncludeText("Current Routes query"); expect(component).toIncludeText("Current Routes query");
@ -28,12 +29,12 @@ describe('QueryToCliCmd', () => {
"namespace": "linkerd" "namespace": "linkerd"
} }
let component = mount( let component = mount(i18nWrap(
<QueryToCliCmd <QueryToCliCmd
cmdName="routes" cmdName="routes"
query={query} query={query}
resource={query.resource} resource={query.resource}
controllerNamespace="my-linkerd-ns" /> controllerNamespace="my-linkerd-ns" />)
); );
expect(component).toIncludeText("Current Routes query"); expect(component).toIncludeText("Current Routes query");
@ -49,12 +50,12 @@ describe('QueryToCliCmd', () => {
"authority": "foo.bar:8080" "authority": "foo.bar:8080"
} }
let component = mount( let component = mount(i18nWrap(
<QueryToCliCmd <QueryToCliCmd
cmdName="tap" cmdName="tap"
query={query} query={query}
resource={query.resource} resource={query.resource}
controllerNamespace="linkerd" /> controllerNamespace="linkerd" />)
); );
expect(component).toIncludeText("Current Tap query"); expect(component).toIncludeText("Current Tap query");
@ -71,12 +72,12 @@ describe('QueryToCliCmd', () => {
"authority": "foo.bar:8080" "authority": "foo.bar:8080"
} }
let component = mount( let component = mount(i18nWrap(
<QueryToCliCmd <QueryToCliCmd
cmdName="tap" cmdName="tap"
query={query} query={query}
resource={query.resource} resource={query.resource}
controllerNamespace="linkerd" /> controllerNamespace="linkerd" />)
); );
expect(component).toIncludeText("Current Tap query"); expect(component).toIncludeText("Current Tap query");
@ -89,12 +90,12 @@ describe('QueryToCliCmd', () => {
"namespace": "linkerd" "namespace": "linkerd"
} }
let component = mount( let component = mount(i18nWrap(
<QueryToCliCmd <QueryToCliCmd
cmdName="top" cmdName="top"
query={query} query={query}
resource={query.resource} resource={query.resource}
controllerNamespace="linkerd" /> controllerNamespace="linkerd" />)
); );
expect(component).toIncludeText("Current Top query"); expect(component).toIncludeText("Current Top query");
@ -109,12 +110,12 @@ describe('QueryToCliCmd', () => {
"theLimitDoesNotExist": 999 "theLimitDoesNotExist": 999
} }
let component = mount( let component = mount(i18nWrap(
<QueryToCliCmd <QueryToCliCmd
cmdName="tap" cmdName="tap"
query={query} query={query}
resource={query.resource} resource={query.resource}
controllerNamespace="linkerd" /> controllerNamespace="linkerd" />)
); );
expect(component).toIncludeText("Current Tap query"); 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><Typography variant="h5">{resourceType}/{resourceName}</Typography></Grid>
<Grid item> <Grid item>
<Grid container spacing={1}> <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> <Grid item>
{resourceIsMeshed ? {resourceIsMeshed ?
<SimpleChip label="meshed" type="good" /> : <SimpleChip label={<Trans>columnTitleMeshed</Trans>} type="good" /> :
<SimpleChip label="unmeshed" type="bad" /> <SimpleChip label={<Trans>columnTitleUnmeshed</Trans>} type="bad" />
} }
</Grid> </Grid>
</Grid> </Grid>

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,6 @@ import _noop from 'lodash/noop';
import _omit from 'lodash/omit'; import _omit from 'lodash/omit';
import _pick from 'lodash/pick'; import _pick from 'lodash/pick';
import _some from 'lodash/some'; import _some from 'lodash/some';
import _startCase from 'lodash/startCase';
import _uniq from 'lodash/uniq'; import _uniq from 'lodash/uniq';
import _values from 'lodash/values'; import _values from 'lodash/values';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
@ -245,7 +244,7 @@ class TapQueryForm extends React.Component {
return ( return (
<React.Fragment> <React.Fragment>
<InputLabel htmlFor={resourceKey}>{_startCase(resourceKey)}</InputLabel> <InputLabel htmlFor={resourceKey}>{resourceKey === 'resource' ? <Trans>formResource</Trans> : <Trans>formToResource</Trans>}</InputLabel>
<Select <Select
value={!nsEmpty && resourceOptions.includes(query[resourceKey]) ? query[resourceKey] : ''} value={!nsEmpty && resourceOptions.includes(query[resourceKey]) ? query[resourceKey] : ''}
onChange={this.handleFormChange(resourceKey)} onChange={this.handleFormChange(resourceKey)}
@ -364,20 +363,20 @@ class TapQueryForm extends React.Component {
)) ))
} }
</Select> </Select>
<FormHelperText>Display requests with this :authority</FormHelperText> <FormHelperText><Trans>formAuthorityHelpText</Trans></FormHelperText>
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item xs={6} md={3} className={classes.formControlWrapper}> <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> </Grid>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={6} md={3} className={classes.formControlWrapper}> <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>
<Grid item xs={6} md={3} className={classes.formControlWrapper}> <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>
<Grid item xs={6} md={3} className={classes.formControlWrapper}> <Grid item xs={6} md={3} className={classes.formControlWrapper}>
<FormControl className={classes.formControl}> <FormControl className={classes.formControl}>
@ -393,7 +392,7 @@ class TapQueryForm extends React.Component {
)) ))
} }
</Select> </Select>
<FormHelperText>Display requests with this HTTP method</FormHelperText> <FormHelperText><Trans>formHTTPMethodHelpText</Trans></FormHelperText>
</FormControl> </FormControl>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -203,11 +203,11 @@ class TopRoutes extends React.Component {
<Grid container direction="column" spacing={2}> <Grid container direction="column" spacing={2}>
<Grid item container spacing={4} alignItems="center" justify="flex-start"> <Grid item container spacing={4} alignItems="center" justify="flex-start">
<Grid item> <Grid item>
{ this.renderNamespaceDropdown('Namespace', 'namespace', 'Namespace to query') } { this.renderNamespaceDropdown(<Trans>formNamespace</Trans>, 'namespace', <Trans>formNamespaceHelpText</Trans>) }
</Grid> </Grid>
<Grid item> <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>
<Grid item> <Grid item>
@ -216,7 +216,7 @@ class TopRoutes extends React.Component {
variant="outlined" variant="outlined"
disabled={requestInProgress || !query.namespace || !query.resource_type} disabled={requestInProgress || !query.namespace || !query.resource_type}
onClick={this.handleBtnClick(true)}> onClick={this.handleBtnClick(true)}>
Start <Trans>buttonStart</Trans>
</Button> </Button>
</Grid> </Grid>
@ -226,23 +226,23 @@ class TopRoutes extends React.Component {
variant="outlined" variant="outlined"
disabled={!requestInProgress} disabled={!requestInProgress}
onClick={this.handleBtnClick(false)}> onClick={this.handleBtnClick(false)}>
Stop <Trans>buttonStop</Trans>
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>
<Grid item container spacing={4} alignItems="center" justify="flex-start"> <Grid item container spacing={4} alignItems="center" justify="flex-start">
<Grid item> <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>
<Grid item> <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> </Grid>
</Grid> </Grid>
<Divider light className={classes.root} /> <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> </CardContent>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,25 @@
{ {
"404Msg": "Page not found.", "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", "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.", "All namespaces have a {productName} install.": "All namespaces have a {productName} install.",
"Control plane": "Control plane",
"Control plane components": "Control plane components", "Control plane components": "Control plane components",
"Current {cmdNameDisplay} query": "Current {cmdNameDisplay} query",
"Data plane proxies": "Data plane proxies", "Data plane proxies": "Data plane proxies",
"LinkerdIsUpToDateMsg": "Linkerd is up to date.",
"New service profile": "New service profile",
"NoDataToDisplayMsg": "No data to display", "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", "buttonReset": "Reset",
"buttonRunLinkerdCheck": "Run Linkerd Check", "buttonRunLinkerdCheck": "Run Linkerd Check",
"buttonStart": "Start", "buttonStart": "Start",
@ -15,7 +30,9 @@
"columnTitleClusterName": "Cluster Name", "columnTitleClusterName": "Cluster Name",
"columnTitleCount": "Count", "columnTitleCount": "Count",
"columnTitleDeployment": "Deployment", "columnTitleDeployment": "Deployment",
"columnTitleDestination": "Destination",
"columnTitleDirection": "Direction", "columnTitleDirection": "Direction",
"columnTitleFrom": "FROM",
"columnTitleGRPCStatus": "GRPC Status", "columnTitleGRPCStatus": "GRPC Status",
"columnTitleGrafana": "Grafana", "columnTitleGrafana": "Grafana",
"columnTitleHTTPStatus": "HTTP Status", "columnTitleHTTPStatus": "HTTP Status",
@ -30,6 +47,7 @@
"columnTitleMethod": "Method", "columnTitleMethod": "Method",
"columnTitleName": "Name", "columnTitleName": "Name",
"columnTitleNamespace": "Namespace", "columnTitleNamespace": "Namespace",
"columnTitleNoTraffic": "No Traffic",
"columnTitleOpenConnections": "Connections", "columnTitleOpenConnections": "Connections",
"columnTitleP50Latency": "P50 Latency", "columnTitleP50Latency": "P50 Latency",
"columnTitleP95Latency": "P95 Latency", "columnTitleP95Latency": "P95 Latency",
@ -43,23 +61,51 @@
"columnTitleRoute": "Route", "columnTitleRoute": "Route",
"columnTitleSecured": "Secured", "columnTitleSecured": "Secured",
"columnTitleService": "Service", "columnTitleService": "Service",
"columnTitleSource": "Source",
"columnTitleSuccessRate": "Success Rate", "columnTitleSuccessRate": "Success Rate",
"columnTitleTap": "Tap", "columnTitleTap": "Tap",
"columnTitleTo": "TO",
"columnTitleUnmeshed": "Unmeshed",
"columnTitleValue": "Value", "columnTitleValue": "Value",
"columnTitleWeight": "Weight", "columnTitleWeight": "Weight",
"columnTitleWorst": "Worst", "columnTitleWorst": "Worst",
"columnTitleWriteRate": "Write Bytes / sec", "columnTitleWriteRate": "Write Bytes / sec",
"componentsMsg": "Components", "componentsMsg": "Components",
"connectResourceMsg {resource}": "Connect your first {resource}",
"controllerInstalledMsg": "Controller successfully installed",
"createNewProfileMsg": "You can also create a new profile",
"formAuthority": "Authority", "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", "formHTTPMethod": "HTTP Method",
"formHTTPMethodHelpText": "Display requests with this HTTP method",
"formHTTPStatus": "HTTP Status",
"formHeaders": "Headers",
"formHideFilters": "Hide filters", "formHideFilters": "Hide filters",
"formLatency": "Latency",
"formMaxRPS": "Max RPS", "formMaxRPS": "Max RPS",
"formMaxRPSHelpText {defaultMaxRps}": "Maximum requests per second to tap. Default {defaultMaxRps}",
"formMethod": "Method",
"formNamespace": "Namespace", "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", "formPath": "Path",
"formPathHelpText": "Display requests with paths that start with this prefix",
"formResource": "Resource",
"formResourceHelpText": "Resource to query",
"formResponseLengthB": "Response Length (B)",
"formScheme": "Scheme", "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", "formShowFilters": "Show more filters",
"formToNamespace": "To Namespace", "formToNamespace": "To Namespace",
"formToNamespaceHelpText": "Namespace of target resource",
"formToResource": "To Resource", "formToResource": "To Resource",
"formToResourceHelpText": "Target resource",
"labelError": "Error",
"labelSuccess": "Success",
"menuItemCommunity": "Community", "menuItemCommunity": "Community",
"menuItemControlPlane": "Control Plane", "menuItemControlPlane": "Control Plane",
"menuItemCronJobs": "Cron Jobs", "menuItemCronJobs": "Cron Jobs",
@ -80,12 +126,20 @@
"menuItemTap": "Tap", "menuItemTap": "Tap",
"menuItemTop": "Top", "menuItemTop": "Top",
"menuItemTrafficSplits": "Traffic Splits", "menuItemTrafficSplits": "Traffic Splits",
"noNamespacesDetectedMsg": "No namespaces detected.",
"noResourcesDetectedMsg": "No resources detected.", "noResourcesDetectedMsg": "No resources detected.",
"podsAreInitializingMsg": "Pods are initializing", "podsAreInitializingMsg": "Pods are initializing",
"serviceMeshInstalledMsg": "The service mesh was successfully installed!",
"sidebarHeadingCluster": "Cluster", "sidebarHeadingCluster": "Cluster",
"sidebarHeadingConfiguration": "Configuration", "sidebarHeadingConfiguration": "Configuration",
"sidebarHeadingTools": "Tools", "sidebarHeadingTools": "Tools",
"sidebarHeadingWorkloads": "Workloads", "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", "tableTitleEdgesEmpty": "Edges",
"tableTitleEdgesWithIdentity {identity}": "Edges (Identity: {identity})", "tableTitleEdgesWithIdentity {identity}": "Edges (Identity: {identity})",
"tableTitleGateways": "Gateways", "tableTitleGateways": "Gateways",
@ -94,11 +148,17 @@
"tableTitleLeafServices": "Leaf Services", "tableTitleLeafServices": "Leaf Services",
"tableTitleOutbound": "Outbound", "tableTitleOutbound": "Outbound",
"tableTitlePods": "Pods", "tableTitlePods": "Pods",
"tableTitleRequestDetails": "Request Details",
"tableTitleRequestInit": "Request Init",
"tableTitleResponseEnd": "Response End",
"tableTitleResponseInit": "Response Init",
"tableTitleTCP": "TCP", "tableTitleTCP": "TCP",
"tableTitleTCPMetrics": "TCP Metrics", "tableTitleTCPMetrics": "TCP Metrics",
"tooltipInbound": "INBOUND",
"tooltipOutbound": "OUTBOUND",
"unspecifiedResourcesMsg": "one or more resources", "unspecifiedResourcesMsg": "one or more resources",
"{numUnadded} namespace has no meshed resources.": "{numUnadded} namespace has 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} namespaces have no meshed resources.": "{numUnadded} namespaces have no meshed resourcesß", "{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} namespace": "{productName} namespace",
"{productName} version": "{productName} version" "{productName} version": "{productName} version"
} }

View File

@ -1,10 +1,25 @@
{ {
"404Msg": "Página no encontrada.", "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", "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}.", "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", "Control plane components": "Componentes del plano de control",
"Current {cmdNameDisplay} query": "Consulta {cmdNameDisplay} actual",
"Data plane proxies": "Proxies del plano de datos", "Data plane proxies": "Proxies del plano de datos",
"LinkerdIsUpToDateMsg": "Linkerd está actualizado.",
"New service profile": "Nuevo service profile",
"NoDataToDisplayMsg": "No hay datos que mostrar", "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", "buttonReset": "Restablecer",
"buttonRunLinkerdCheck": "Ejecutar Linkerd Check", "buttonRunLinkerdCheck": "Ejecutar Linkerd Check",
"buttonStart": "Empezar", "buttonStart": "Empezar",
@ -15,7 +30,9 @@
"columnTitleClusterName": "Nombre del Clúster", "columnTitleClusterName": "Nombre del Clúster",
"columnTitleCount": "Total", "columnTitleCount": "Total",
"columnTitleDeployment": "Deployment", "columnTitleDeployment": "Deployment",
"columnTitleDestination": "Destino",
"columnTitleDirection": "Dirección", "columnTitleDirection": "Dirección",
"columnTitleFrom": "DESDE",
"columnTitleGRPCStatus": "Estado GRPC", "columnTitleGRPCStatus": "Estado GRPC",
"columnTitleGrafana": "Grafana", "columnTitleGrafana": "Grafana",
"columnTitleHTTPStatus": "Estado HTTP", "columnTitleHTTPStatus": "Estado HTTP",
@ -30,6 +47,7 @@
"columnTitleMethod": "Método", "columnTitleMethod": "Método",
"columnTitleName": "Nombre", "columnTitleName": "Nombre",
"columnTitleNamespace": "Namespace", "columnTitleNamespace": "Namespace",
"columnTitleNoTraffic": "No Hay Tráfico",
"columnTitleOpenConnections": "Conexiones", "columnTitleOpenConnections": "Conexiones",
"columnTitleP50Latency": "Latencia P50", "columnTitleP50Latency": "Latencia P50",
"columnTitleP95Latency": "Latencia P95", "columnTitleP95Latency": "Latencia P95",
@ -43,23 +61,51 @@
"columnTitleRoute": "Ruta", "columnTitleRoute": "Ruta",
"columnTitleSecured": "Asegurado", "columnTitleSecured": "Asegurado",
"columnTitleService": "Servicio", "columnTitleService": "Servicio",
"columnTitleSource": "Fuente",
"columnTitleSuccessRate": "Tasa de éxito", "columnTitleSuccessRate": "Tasa de éxito",
"columnTitleTap": "Tap", "columnTitleTap": "Tap",
"columnTitleTo": "HACIA",
"columnTitleUnmeshed": "Ausente en la malla de servicios",
"columnTitleValue": "Valor", "columnTitleValue": "Valor",
"columnTitleWeight": "Peso", "columnTitleWeight": "Peso",
"columnTitleWorst": "Peor", "columnTitleWorst": "Peor",
"columnTitleWriteRate": "Escritura Bytes / seg", "columnTitleWriteRate": "Escritura Bytes / seg",
"componentsMsg": "Componentes", "componentsMsg": "Componentes",
"connectResourceMsg {resource}": "Conecta tu primer {resource}",
"controllerInstalledMsg": "Controller instalado correctamente",
"createNewProfileMsg": "También puede crear un nuevo perfil",
"formAuthority": "Authority", "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", "formHTTPMethod": "Método HTTP",
"formHTTPMethodHelpText": "Mostrar solicitudes con este método HTTP",
"formHTTPStatus": "Estado HTTP",
"formHeaders": "Encabezados",
"formHideFilters": "Ocultar filtros", "formHideFilters": "Ocultar filtros",
"formLatency": "Latencia",
"formMaxRPS": "Max PPS", "formMaxRPS": "Max PPS",
"formMaxRPSHelpText {defaultMaxRps}": "Máximo de solicitudes por segundo para tap. Por defecto {defaultMaxRps}",
"formMethod": "Método",
"formNamespace": "Namespace", "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", "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", "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", "formShowFilters": "Mostrar más filtros",
"formToNamespace": "Al Namespace", "formToNamespace": "Al Namespace",
"formToNamespaceHelpText": "Namespace del recurso de destino",
"formToResource": "Al Recurso", "formToResource": "Al Recurso",
"formToResourceHelpText": "Recurso destino",
"labelError": "Error",
"labelSuccess": "Éxito",
"menuItemCommunity": "Comunidad", "menuItemCommunity": "Comunidad",
"menuItemControlPlane": "Plano de Control", "menuItemControlPlane": "Plano de Control",
"menuItemCronJobs": "Cron Jobs", "menuItemCronJobs": "Cron Jobs",
@ -80,12 +126,20 @@
"menuItemTap": "Tap", "menuItemTap": "Tap",
"menuItemTop": "Top", "menuItemTop": "Top",
"menuItemTrafficSplits": "Traffic Splits", "menuItemTrafficSplits": "Traffic Splits",
"noNamespacesDetectedMsg": "No se han encontrado namespaces.",
"noResourcesDetectedMsg": "No se han encontrado recursos.", "noResourcesDetectedMsg": "No se han encontrado recursos.",
"podsAreInitializingMsg": "Los pods se están inicializando", "podsAreInitializingMsg": "Los pods se están inicializando",
"serviceMeshInstalledMsg": "¡La malla de servicios se instaló correctamente!",
"sidebarHeadingCluster": "Clúster", "sidebarHeadingCluster": "Clúster",
"sidebarHeadingConfiguration": "Configuración", "sidebarHeadingConfiguration": "Configuración",
"sidebarHeadingTools": "Herramientas", "sidebarHeadingTools": "Herramientas",
"sidebarHeadingWorkloads": "Cargas de trabajo", "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", "tableTitleEdgesEmpty": "Bordes",
"tableTitleEdgesWithIdentity {identity}": "Bordes (Identidad: {identity})", "tableTitleEdgesWithIdentity {identity}": "Bordes (Identidad: {identity})",
"tableTitleGateways": "Gateways", "tableTitleGateways": "Gateways",
@ -94,11 +148,17 @@
"tableTitleLeafServices": "Servicios Hoja", "tableTitleLeafServices": "Servicios Hoja",
"tableTitleOutbound": "Salida", "tableTitleOutbound": "Salida",
"tableTitlePods": "Pods", "tableTitlePods": "Pods",
"tableTitleRequestDetails": "Detalle Solicitudes",
"tableTitleRequestInit": "Solicitud Inicial",
"tableTitleResponseEnd": "Fin De Respuesta",
"tableTitleResponseInit": "Respuesta Inicial",
"tableTitleTCP": "TCP", "tableTitleTCP": "TCP",
"tableTitleTCPMetrics": "Métricas TCP", "tableTitleTCPMetrics": "Métricas TCP",
"tooltipInbound": "ENTRADA",
"tooltipOutbound": "SALIDA",
"unspecifiedResourcesMsg": "uno o más recursos", "unspecifiedResourcesMsg": "uno o más recursos",
"{numUnadded} namespace has no meshed resources.": "El namespace {numUnadded} no tiene 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} namespaces have no meshed resources.": "Los namespaces {numUnadded} no tienen recursos en la malla de servicios.", "{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} namespace": "Namespace de {productName}",
"{productName} version": "Versión 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 { createMemoryHistory } from 'history';
import React from 'react'; import React from 'react';
import { Route, Router } from 'react-router'; import { Route, Router } from 'react-router';
import { I18nProvider } from '@lingui/react';
import catalogEn from './../js/locales/en/messages.js';
const componentDefaultProps = { const componentDefaultProps = {
api: ApiHelpers(''), api: ApiHelpers(''),
@ -11,6 +13,9 @@ const componentDefaultProps = {
releaseVersion: '' releaseVersion: ''
}; };
const selectedLocale = 'en';
const selectedCatalog = catalogEn;
export function routerWrap(Component, extraProps={}, route="/", currentLoc="/") { export function routerWrap(Component, extraProps={}, route="/", currentLoc="/") {
const createElement = (ComponentToWrap, props) => ( const createElement = (ComponentToWrap, props) => (
<ComponentToWrap {...(_merge({}, componentDefaultProps, props, extraProps))} /> <ComponentToWrap {...(_merge({}, componentDefaultProps, props, extraProps))} />
@ -21,3 +26,13 @@ export function routerWrap(Component, extraProps={}, route="/", currentLoc="/")
</Router> </Router>
); );
} }
export function i18nWrap(Component) {
return (
<I18nProvider
language={selectedLocale}
catalogs={{ [selectedLocale]: selectedCatalog }}>
{Component}
</I18nProvider>
);
}