Repository: linkerd/linkerd2
Pull request #4881 (2020-08-14 17:24:49)

Remove no longer needed GITCOOKIE_SH in CI
Removed usage of `GITCOOKIE_SH`, which was a script stored in a secret
to authenticate requests against googlesource.com, to avoid hitting
rate limits when pulling go dependencies from that source. Now that we
use go modules, deps are pulled from http://proxy.golang.org/ and this
is no longer needed.


Patch:
diff --git a/.github/workflows/cloud_integration.yml b/.github/workflows/cloud_integration.yml
index 7dc835aef..633e15d68 100644
--- a/.github/workflows/cloud_integration.yml
+++ b/.github/workflows/cloud_integration.yml
@@ -100,11 +100,7 @@ jobs:
         name: testing-${{ env.TAG }}-${{ github.run_id }}
         num_nodes: 2
     - name: Run integration tests
-      env:
-        GITCOOKIE_SH: ${{ secrets.GITCOOKIE_SH }}
-      run: |
-        echo "$GITCOOKIE_SH" | bash
-        bin/tests --skip-kind-create "$CMD"
+      run: bin/tests --skip-kind-create "$CMD"
     - name: CNI tests
       run: |
         export TAG="$($CMD version --client --short)"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c8e191012..d6e51c8d3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -183,11 +183,7 @@ jobs:
         name: testing-${{ steps.install_cli.outputs.tag }}-${{ github.run_id }}
         num_nodes: 2
     - name: Run integration tests
-      env:
-        GITCOOKIE_SH: ${{ secrets.GITCOOKIE_SH }}
-      run: |
-        echo "$GITCOOKIE_SH" | bash
-        bin/tests --skip-kind-create "$CMD"
+      run: bin/tests --skip-kind-create "$CMD"
     - name: CNI tests
       run: |
         export TAG="$($CMD version --client --short)"
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index f4867b521..4d9dee668 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -18,11 +18,7 @@ jobs:
       # actions/checkout@v2
       uses: actions/checkout@722adc6
     - name: Go lint
-      env:
-        GITCOOKIE_SH: ${{ secrets.GITCOOKIE_SH }}
-      run: |
-        echo "$GITCOOKIE_SH" | bash
-        bin/lint --verbose
+      run: bin/lint --verbose
   go_format:
     name: Go format
     runs-on: ubuntu-18.04
@@ -33,11 +29,7 @@ jobs:
       # actions/checkout@v2
       uses: actions/checkout@722adc6
     - name: Format
-      env:
-        GITCOOKIE_SH: ${{ secrets.GITCOOKIE_SH }}
-      run: |
-        echo "$GITCOOKIE_SH" | bash
-        bin/fmt
+      run: bin/fmt
   shellcheck:
     name: shellcheck
     runs-on: ubuntu-18.04
diff --git a/BUILD.md b/BUILD.md
index 57565c725..a01ca399f 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -340,6 +340,24 @@ cd web/app
 yarn add [dep]
 ```
 
+#### Translations
+
+To add a locale:
+
+```bash
+cd web/app
+yarn lingui add-locale [locales...] # will create a messages.json file for new locale(s)
+```
+
+To extract message keys from existing components:
+
+```bash
+cd web/app
+yarn lingui extract
+...
+yarn lingui compile # done automatically in bin/web run
+```
+
 ### Rust
 
 All Rust development happens in the
diff --git a/CHANGES.md b/CHANGES.md
index 16633a5a7..6e25e2492 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,20 @@
 # Changes
 
+## edge-20.8.2
+
+This edge release adds an internationalization framework to the dashboard,
+Spanish translations to the dashboard UI, and a `linkerd multicluster uninstall`
+command for graceful removal of the multicluster components.
+
+* Web UI
+  * Added Spanish translations to the dashboard
+  * Added a framework and documentation to simplify creation of new
+    translations
+* Multicluster
+  * Added a multicluster uninstall command
+  * Added a warning from `linkerd check --multicluster` if the multicluster
+    support is not installed
+
 ## edge-20.8.1
 
 This edge adds multi-arch support to Linkerd! Our docker images and CLI now
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 02f2b3280..ae362f65c 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -23,6 +23,7 @@ import (
 	"github.com/linkerd/linkerd2/pkg/tls"
 	"github.com/linkerd/linkerd2/pkg/version"
 	log "github.com/sirupsen/logrus"
+	admissionRegistration "k8s.io/api/admissionregistration/v1beta1"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
@@ -1208,7 +1209,11 @@ func (hc *HealthChecker) allCategories() []category {
 					hintAnchor:  "l5d-injection-disabled",
 					warning:     true,
 					check: func(context.Context) error {
-						if hc.isHA() {
+						policy, err := hc.getMutatingWebhookFailurePolicy()
+						if err != nil {
+							return err
+						}
+						if policy != nil && *policy == admissionRegistration.Fail {
 							return hc.checkHAMetadataPresentOnKubeSystemNamespace()
 						}
 						return &SkipError{Reason: "not run for non HA installs"}
@@ -1667,6 +1672,17 @@ func (hc *HealthChecker) checkCustomResourceDefinitions(shouldExist bool) error
 	return checkResources("CustomResourceDefinitions", objects, []string{"serviceprofiles.linkerd.io"}, shouldExist)
 }
 
+func (hc *HealthChecker) getMutatingWebhookFailurePolicy() (*admissionRegistration.FailurePolicyType, error) {
+	mwc, err := hc.kubeAPI.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Get(k8s.ProxyInjectorWebhookConfigName, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+	if len(mwc.Webhooks) != 1 {
+		return nil, fmt.Errorf("expected 1 webhooks, found %d", len(mwc.Webhooks))
+	}
+	return mwc.Webhooks[0].FailurePolicy, nil
+}
+
 func (hc *HealthChecker) checkMutatingWebhookConfigurations(shouldExist bool) error {
 	options := metav1.ListOptions{
 		LabelSelector: hc.controlPlaneComponentsSelector(),
@@ -1885,7 +1901,7 @@ func (hc *HealthChecker) checkHAMetadataPresentOnKubeSystemNamespace() error {
 
 	val, ok := ns.Labels[k8s.AdmissionWebhookLabel]
 	if !ok || val != "disabled" {
-		return fmt.Errorf("kube-system namespace needs to have the label %s: disabled if HA mode is enabled", k8s.AdmissionWebhookLabel)
+		return fmt.Errorf("kube-system namespace needs to have the label %s: disabled if injector webhook failure policy is Fail", k8s.AdmissionWebhookLabel)
 	}
 
 	return nil
diff --git a/web/app/js/components/BaseTable.jsx b/web/app/js/components/BaseTable.jsx
index 33573c46e..1418cd943 100644
--- a/web/app/js/components/BaseTable.jsx
+++ b/web/app/js/components/BaseTable.jsx
@@ -261,10 +261,10 @@ BaseTable.propTypes = {
     isNumeric: PropTypes.bool,
     render: PropTypes.func,
     sorter: PropTypes.func,
-    title: PropTypes.string,
+    title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
   })).isRequired,
   tableRows: PropTypes.arrayOf(PropTypes.shape({})),
-  title: PropTypes.string,
+  title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
 };
 
 BaseTable.defaultProps = {
diff --git a/web/app/js/components/CheckModal.jsx b/web/app/js/components/CheckModal.jsx
index 6edadd8ba..d284b2ac1 100644
--- a/web/app/js/components/CheckModal.jsx
+++ b/web/app/js/components/CheckModal.jsx
@@ -14,6 +14,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import SimpleChip from './util/Chip.jsx';
 import Slide from '@material-ui/core/Slide';
+import { Trans } from '@lingui/macro';
 import Typography from '@material-ui/core/Typography';
 import withMobileDialog from '@material-ui/core/withMobileDialog';
 import { withStyles } from '@material-ui/core/styles';
@@ -254,7 +255,7 @@ class CheckModal extends React.Component {
               color="primary"
               disabled={running}
               onClick={this.runCheck}>
-              Run Linkerd Check
+              <Trans>buttonRunLinkerdCheck</Trans>
             </Button>
           </Grid>
         </Grid>
diff --git a/web/app/js/components/EdgesTable.jsx b/web/app/js/components/EdgesTable.jsx
index 6eb0735b7..10c86d493 100644
--- a/web/app/js/components/EdgesTable.jsx
+++ b/web/app/js/components/EdgesTable.jsx
@@ -3,6 +3,7 @@ import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
 import PropTypes from 'prop-types';
 import React from 'react';
 import Tooltip from '@material-ui/core/Tooltip';
+import { Trans } from '@lingui/macro';
 import WarningIcon from '@material-ui/icons/Warning';
 import { directionColumn } from './util/TapUtils.jsx';
 import { processedEdgesPropType } from './util/EdgesUtils.jsx';
@@ -25,7 +26,7 @@ const edgesColumnDefinitions = (PrefixedLink, namespace, type, classes) => {
       render: d => directionColumn(d.direction),
     },
     {
-      title: 'Namespace',
+      title: <Trans>columnTitleNamespace</Trans>,
       dataIndex: 'namespace',
       isNumeric: false,
       filter: d => d.namespace,
@@ -37,7 +38,7 @@ const edgesColumnDefinitions = (PrefixedLink, namespace, type, classes) => {
       sorter: d => d.namespace,
     },
     {
-      title: 'Name',
+      title: <Trans>columnTitleName</Trans>,
       dataIndex: 'name',
       isNumeric: false,
       filter: d => d.name,
@@ -56,7 +57,7 @@ const edgesColumnDefinitions = (PrefixedLink, namespace, type, classes) => {
       sorter: d => d.name,
     },
     {
-      title: 'Identity',
+      title: <Trans>columnTitleIdentity</Trans>,
       dataIndex: 'identity',
       isNumeric: false,
       filter: d => d.identity,
@@ -64,7 +65,7 @@ const edgesColumnDefinitions = (PrefixedLink, namespace, type, classes) => {
       sorter: d => d.identity,
     },
     {
-      title: 'Secured',
+      title: <Trans>columnTitleSecured</Trans>,
       dataIndex: 'message',
       isNumeric: true,
       render: d => {
@@ -83,12 +84,12 @@ const edgesColumnDefinitions = (PrefixedLink, namespace, type, classes) => {
 };
 
 const generateEdgesTableTitle = edges => {
-  let title = 'Edges';
+  let title = <Trans>tableTitleEdgesEmpty</Trans>;
   if (edges.length > 0) {
     let identity = edges[0].direction === 'INBOUND' ? edges[0].serverId : edges[0].clientId;
     if (identity) {
       identity = `${identity.split('.')[0]}.${identity.split('.')[1]}`;
-      title = `${title} (Identity: ${identity})`;
+      title = <Trans>tableTitleEdgesWithIdentity {identity}</Trans>;
     }
   }
   return title;
diff --git a/web/app/js/components/EmptyCard.jsx b/web/app/js/components/EmptyCard.jsx
index 4adeb7062..b3e7b8fe6 100644
--- a/web/app/js/components/EmptyCard.jsx
+++ b/web/app/js/components/EmptyCard.jsx
@@ -17,7 +17,7 @@ const EmptyCard = ({ classes }) => {
     <Card className={classes.card} elevation={3}>
       <CardContent>
         <Typography>
-          <Trans>No data to display</Trans>
+          <Trans>NoDataToDisplayMsg</Trans>
         </Typography>
       </CardContent>
     </Card>
diff --git a/web/app/js/components/ErrorModal.jsx b/web/app/js/components/ErrorModal.jsx
index c649b7d37..bdf223dbd 100644
--- a/web/app/js/components/ErrorModal.jsx
+++ b/web/app/js/components/ErrorModal.jsx
@@ -10,6 +10,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import Switch from '@material-ui/core/Switch';
 import Tooltip from '@material-ui/core/Tooltip';
+import { Trans } from '@lingui/macro';
 import Typography from '@material-ui/core/Typography';
 import _each from 'lodash/each';
 import _get from 'lodash/get';
@@ -150,7 +151,9 @@ class ErrorModal extends React.Component {
 
     if (showInit) {
       return (
-        <Tooltip title="Pods are initializing"><CircularProgress size={20} thickness={4} /></Tooltip>
+        <Tooltip title={<Trans>podsAreInitializingMsg</Trans>}>
+          <CircularProgress size={20} thickness={4} />
+        </Tooltip>
       );
     } else {
       return (
diff --git a/web/app/js/components/ExpandableTable.jsx b/web/app/js/components/ExpandableTable.jsx
index 8244fed82..89af24afe 100644
--- a/web/app/js/components/ExpandableTable.jsx
+++ b/web/app/js/components/ExpandableTable.jsx
@@ -148,7 +148,7 @@ ExpandableTable.propTypes = {
   expandedRowRender: PropTypes.func.isRequired,
   tableClassName: PropTypes.string,
   tableColumns: PropTypes.arrayOf(PropTypes.shape({
-    title: PropTypes.string,
+    title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
     isNumeric: PropTypes.bool,
     render: PropTypes.func,
   })).isRequired,
diff --git a/web/app/js/components/Gateway.jsx b/web/app/js/components/Gateway.jsx
index f41548cb3..ddee3a936 100644
--- a/web/app/js/components/Gateway.jsx
+++ b/web/app/js/components/Gateway.jsx
@@ -4,6 +4,7 @@ import MetricsTable from './MetricsTable.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
 import Spinner from './util/Spinner.jsx';
+import { Trans } from '@lingui/macro';
 import _isEmpty from 'lodash/isEmpty';
 import { processGatewayResults } from './util/MetricUtils.jsx';
 import { withContext } from './util/AppContext.jsx';
@@ -102,7 +103,7 @@ class Gateways extends React.Component {
             {noMetrics ? null : (
               <div className="page-section">
                 <MetricsTable
-                  title="Gateways"
+                  title={<Trans>tableTitleGateways</Trans>}
                   resource="gateway"
                   metrics={metrics} />
               </div>
diff --git a/web/app/js/components/MeshedStatusTable.jsx b/web/app/js/components/MeshedStatusTable.jsx
index 4fb24b4ab..dcff295df 100644
--- a/web/app/js/components/MeshedStatusTable.jsx
+++ b/web/app/js/components/MeshedStatusTable.jsx
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import { StyledProgress } from './util/Progress.jsx';
 import Tooltip from '@material-ui/core/Tooltip';
+import { Trans } from '@lingui/macro';
 import _isEmpty from 'lodash/isEmpty';
 import { withContext } from './util/AppContext.jsx';
 
@@ -20,7 +21,7 @@ const getClassification = (meshedPodCount, failedPodCount) => {
 
 const namespacesColumns = PrefixedLink => [
   {
-    title: 'Namespace',
+    title: <Trans>columnTitleNamespace</Trans>,
     dataIndex: 'namespace',
     sorter: d => d.namespace,
     render: d => {
@@ -37,12 +38,12 @@ const namespacesColumns = PrefixedLink => [
     },
   },
   {
-    title: 'Meshed pods',
+    title: <Trans>columnTitleMeshedPods</Trans>,
     dataIndex: 'meshedPodsStr',
     isNumeric: true,
   },
   {
-    title: 'Meshed Status',
+    title: <Trans>columnTitleMeshedStatus</Trans>,
     key: 'meshification',
     render: row => {
       const percent = row.meshedPercent.get();
diff --git a/web/app/js/components/MetricsTable.jsx b/web/app/js/components/MetricsTable.jsx
index cf2bf7abd..8c8415023 100644
--- a/web/app/js/components/MetricsTable.jsx
+++ b/web/app/js/components/MetricsTable.jsx
@@ -7,6 +7,7 @@ import JaegerLink from './JaegerLink.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
 import SuccessRateMiniChart from './util/SuccessRateMiniChart.jsx';
+import { Trans } from '@lingui/macro';
 import _cloneDeep from 'lodash/cloneDeep';
 import _each from 'lodash/each';
 import _get from 'lodash/get';
@@ -16,21 +17,21 @@ import { withContext } from './util/AppContext.jsx';
 
 const tcpStatColumns = [
   {
-    title: 'Connections',
+    title: <Trans>columnTitleOpenConnections</Trans>,
     dataIndex: 'tcp.openConnections',
     isNumeric: true,
     render: d => metricToFormatter.NO_UNIT(d.tcp.openConnections),
     sorter: d => d.tcp.openConnections,
   },
   {
-    title: 'Read Bytes / sec',
+    title: <Trans>columnTitleReadRate</Trans>,
     dataIndex: 'tcp.readRate',
     isNumeric: true,
     render: d => metricToFormatter.BYTES(d.tcp.readRate),
     sorter: d => d.tcp.readRate,
   },
   {
-    title: 'Write Bytes / sec',
+    title: <Trans>columnTitleWriteRate</Trans>,
     dataIndex: 'tcp.writeRate',
     isNumeric: true,
     render: d => metricToFormatter.BYTES(d.tcp.writeRate),
@@ -40,35 +41,35 @@ const tcpStatColumns = [
 
 const httpStatColumns = [
   {
-    title: 'Success Rate',
+    title: <Trans>columnTitleSuccessRate</Trans>,
     dataIndex: 'successRate',
     isNumeric: true,
     render: d => <SuccessRateMiniChart sr={d.successRate} />,
     sorter: d => d.successRate,
   },
   {
-    title: 'RPS',
+    title: <Trans>columnTitleRPS</Trans>,
     dataIndex: 'requestRate',
     isNumeric: true,
     render: d => metricToFormatter.NO_UNIT(d.requestRate),
     sorter: d => d.requestRate,
   },
   {
-    title: 'P50 Latency',
+    title: <Trans>columnTitleP50Latency</Trans>,
     dataIndex: 'P50',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.P50),
     sorter: d => d.P50,
   },
   {
-    title: 'P95 Latency',
+    title: <Trans>columnTitleP95Latency</Trans>,
     dataIndex: 'P95',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.P95),
     sorter: d => d.P95,
   },
   {
-    title: 'P99 Latency',
+    title: <Trans>columnTitleP99Latency</Trans>,
     dataIndex: 'P99',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.P99),
@@ -79,7 +80,7 @@ const httpStatColumns = [
 
 const trafficSplitDetailColumns = [
   {
-    title: 'Apex Service',
+    title: <Trans>columnTitleApexService</Trans>,
     dataIndex: 'apex',
     isNumeric: false,
     filter: d => !d.tsStats ? null : d.tsStats.apex,
@@ -87,7 +88,7 @@ const trafficSplitDetailColumns = [
     sorter: d => !d.tsStats ? null : d.tsStats.apex,
   },
   {
-    title: 'Leaf Service',
+    title: <Trans>columnTitleLeafService</Trans>,
     dataIndex: 'leaf',
     isNumeric: false,
     filter: d => !d.tsStats ? null : d.tsStats.leaf,
@@ -95,7 +96,7 @@ const trafficSplitDetailColumns = [
     sorter: d => !d.tsStats ? null : d.tsStats.leaf,
   },
   {
-    title: 'Weight',
+    title: <Trans>columnTitleWeight</Trans>,
     dataIndex: 'weight',
     isNumeric: true,
     filter: d => !d.tsStats ? null : d.tsStats.weight,
@@ -113,42 +114,42 @@ const trafficSplitDetailColumns = [
 
 const gatewayColumns = [
   {
-    title: 'ClusterName',
+    title: <Trans>columnTitleClusterName</Trans>,
     dataIndex: 'clusterName',
     isNumeric: false,
     render: d => !d.clusterName ? '---' : d.clusterName,
     sorter: d => !d.clusterName ? '---' : d.clusterName,
   },
   {
-    title: 'Alive',
+    title: <Trans>columnTitleAlive</Trans>,
     dataIndex: 'alive',
     isNumeric: false,
     render: d => !d.alive ? 'FALSE' : 'TRUE',
     sorter: d => d.alive,
   },
   {
-    title: 'Paired Services',
+    title: <Trans>columnTitlePairedServices</Trans>,
     dataIndex: 'pairedServices',
     isNumeric: false,
     render: d => d.pairedServices,
     sorter: d => d.pairedServices,
   },
   {
-    title: 'P50 Latency',
+    title: <Trans>columnTitleP50Latency</Trans>,
     dataIndex: 'P50',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.P50),
     sorter: d => d.P50,
   },
   {
-    title: 'P95 Latency',
+    title: <Trans>columnTitleP95Latency</Trans>,
     dataIndex: 'P95',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.P95),
     sorter: d => d.P95,
   },
   {
-    title: 'P99 Latency',
+    title: <Trans>columnTitleP99Latency</Trans>,
     dataIndex: 'P99',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.P99),
@@ -165,7 +166,7 @@ const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, Prefix
 
   const nsColumn = [
     {
-      title: 'Namespace',
+      title: <Trans>columnTitleNamespace</Trans>,
       dataIndex: 'namespace',
       filter: d => d.namespace,
       isNumeric: false,
@@ -175,7 +176,7 @@ const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, Prefix
   ];
 
   const meshedColumn = {
-    title: 'Meshed',
+    title: <Trans>columnTitleMeshed</Trans>,
     dataIndex: 'pods.totalPods',
     isNumeric: true,
     render: d => !d.pods ? null : `${d.pods.meshedPods}/${d.pods.totalPods}`,
@@ -183,7 +184,7 @@ const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, Prefix
   };
 
   const grafanaColumn = {
-    title: 'Grafana',
+    title: <Trans>columnTitleGrafana</Trans>,
     key: 'grafanaDashboard',
     isNumeric: true,
     render: row => {
@@ -203,7 +204,7 @@ const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, Prefix
 
 
   const jaegerColumn = {
-    title: 'Jaeger',
+    title: <Trans>columnTitleJaeger</Trans>,
     key: 'JaegerDashboard',
     isNumeric: true,
     render: row => {
@@ -327,7 +328,7 @@ MetricsTable.propTypes = {
   selectedNamespace: PropTypes.string.isRequired,
   showName: PropTypes.bool,
   showNamespaceColumn: PropTypes.bool,
-  title: PropTypes.string,
+  title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
   grafana: PropTypes.string,
   jaeger: PropTypes.string,
 };
diff --git a/web/app/js/components/Namespace.jsx b/web/app/js/components/Namespace.jsx
index 65f32e77c..be6392f80 100644
--- a/web/app/js/components/Namespace.jsx
+++ b/web/app/js/components/Namespace.jsx
@@ -7,6 +7,7 @@ import NetworkGraph from './NetworkGraph.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
 import Spinner from './util/Spinner.jsx';
+import { Trans } from '@lingui/macro';
 import _filter from 'lodash/filter';
 import _get from 'lodash/get';
 import _isEmpty from 'lodash/isEmpty';
@@ -147,7 +148,7 @@ class Namespaces extends React.Component {
         {!error ? null : <ErrorBanner message={error} />}
         {!loaded ? <Spinner /> : (
           <div>
-            {noMetrics ? <div>No resources detected.</div> : null}
+            {noMetrics ? <div><Trans>noResourcesDetectedMsg</Trans></div> : null}
             {
               _isEmpty(deploymentsWithMetrics) ? null :
               <NetworkGraph namespace={ns} deployments={metrics.deployment} />
@@ -166,7 +167,7 @@ class Namespaces extends React.Component {
               noMetrics ? null :
               <div className="page-section">
                 <MetricsTable
-                  title="TCP"
+                  title={<Trans>tableTitleTCP</Trans>}
                   resource="pod"
                   metrics={metrics.pod}
                   isTcpTable />
diff --git a/web/app/js/components/Navigation.jsx b/web/app/js/components/Navigation.jsx
index 91e0884cb..d6034d770 100644
--- a/web/app/js/components/Navigation.jsx
+++ b/web/app/js/components/Navigation.jsx
@@ -22,6 +22,7 @@ import React from 'react';
 import ReactRouterPropTypes from 'react-router-prop-types';
 import TextField from '@material-ui/core/TextField';
 import Toolbar from '@material-ui/core/Toolbar';
+import { Trans } from '@lingui/macro';
 import Typography from '@material-ui/core/Typography';
 import Version from './Version.jsx';
 import _maxBy from 'lodash/maxBy';
@@ -450,15 +451,15 @@ class NavigationBase extends React.Component {
         <Divider />
         <MenuList>
           <Typography variant="button" component="div" className={classes.sidebarHeading}>
-            Cluster
+            <Trans>sidebarHeadingCluster</Trans>
           </Typography>
-          { this.menuItem('/namespaces', 'Namespaces', namespaceIcon) }
+          { this.menuItem('/namespaces', <Trans>menuItemNamespaces</Trans>, namespaceIcon) }
 
 
-          { this.menuItem('/controlplane', 'Control Plane',
+          { this.menuItem('/controlplane', <Trans>menuItemControlPlane</Trans>,
             <FontAwesomeIcon icon={faCloud} className={classes.shrinkCloudIcon} />) }
 
-          { this.menuItem('/gateways', 'Gateway',
+          { this.menuItem('/gateways', <Trans>menuItemGateway</Trans>,
             <FontAwesomeIcon icon={faDungeon} className={classes.shrinkIcon} />) }
 
         </MenuList>
@@ -500,48 +501,48 @@ class NavigationBase extends React.Component {
 
         <MenuList>
           <Typography variant="button" component="div" className={classes.sidebarHeading}>
-            Workloads
+            <Trans>sidebarHeadingWorkloads</Trans>
           </Typography>
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/cronjobs`, 'Cron Jobs', cronJobIcon) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/cronjobs`, <Trans>menuItemCronJobs</Trans>, cronJobIcon) }
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/daemonsets`, 'Daemon Sets', daemonsetIcon) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/daemonsets`, <Trans>menuItemDaemonSets</Trans>, daemonsetIcon) }
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/deployments`, 'Deployments', deploymentIcon) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/deployments`, <Trans>menuItemDeployments</Trans>, deploymentIcon) }
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/jobs`, 'Jobs', jobIcon) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/jobs`, <Trans>menuItemJobs</Trans>, jobIcon) }
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/pods`, 'Pods', podIcon) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/pods`, <Trans>menuItemPods</Trans>, podIcon) }
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/replicasets`, 'Replica Sets', replicaSetIcon) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/replicasets`, <Trans>menuItemReplicaSets</Trans>, replicaSetIcon) }
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/replicationcontrollers`, 'Replication Controllers', replicaSetIcon) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/replicationcontrollers`, <Trans>menuItemReplicationControllers</Trans>, replicaSetIcon) }
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/statefulsets`, 'Stateful Sets', statefulSetIcon) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/statefulsets`, <Trans>menuItemStatefulSets</Trans>, statefulSetIcon) }
         </MenuList>
 
         <MenuList>
           <Typography variant="button" component="div" className={classes.sidebarHeading}>
-            Configuration
+            <Trans>sidebarHeadingConfiguration</Trans>
           </Typography>
 
-          { this.menuItem(`/namespaces/${selectedNamespace}/trafficsplits`, 'Traffic Splits', <FontAwesomeIcon icon={faFilter} className={classes.shrinkIcon} />) }
+          { this.menuItem(`/namespaces/${selectedNamespace}/trafficsplits`, <Trans>menuItemTrafficSplits</Trans>, <FontAwesomeIcon icon={faFilter} className={classes.shrinkIcon} />) }
 
         </MenuList>
         <Divider />
         <MenuList>
           <Typography variant="button" component="div" className={classes.sidebarHeading}>
-            Tools
+            <Trans>sidebarHeadingTools</Trans>
           </Typography>
 
-          { this.menuItem('/tap', 'Tap', <FontAwesomeIcon icon={faMicroscope} className={classes.shrinkIcon} />) }
-          { this.menuItem('/top', 'Top', <FontAwesomeIcon icon={faStream} className={classes.shrinkIcon} />) }
-          { this.menuItem('/routes', 'Routes', <FontAwesomeIcon icon={faRandom} className={classes.shrinkIcon} />) }
+          { this.menuItem('/tap', <Trans>menuItemTap</Trans>, <FontAwesomeIcon icon={faMicroscope} className={classes.shrinkIcon} />) }
+          { this.menuItem('/top', <Trans>menuItemTop</Trans>, <FontAwesomeIcon icon={faStream} className={classes.shrinkIcon} />) }
+          { this.menuItem('/routes', <Trans>menuItemRoutes</Trans>, <FontAwesomeIcon icon={faRandom} className={classes.shrinkIcon} />) }
 
         </MenuList>
         <Divider />
         <MenuList>
-          { this.menuItem('/community', 'Community',
+          { this.menuItem('/community', <Trans>menuItemCommunity</Trans>,
             <Badge
               classes={{ badge: classes.badge }}
               invisible={hideUpdateBadge}
@@ -551,25 +552,25 @@ class NavigationBase extends React.Component {
 
           <MenuItem component="a" href="https://linkerd.io/2/overview/" target="_blank" className={classes.navMenuItem}>
             <ListItemIcon><LibraryBooksIcon className={classes.shrinkIcon} /></ListItemIcon>
-            <ListItemText primary="Documentation" />
+            <ListItemText primary={<Trans>menuItemDocumentation</Trans>} />
             <FontAwesomeIcon icon={faExternalLinkAlt} className={classes.externalLinkIcon} size="xs" />
           </MenuItem>
 
           <MenuItem component="a" href="https://github.com/linkerd/linkerd2/issues/new/choose" target="_blank" className={classes.navMenuItem}>
             <ListItemIcon>{githubIcon}</ListItemIcon>
-            <ListItemText primary="GitHub" />
+            <ListItemText primary={<Trans>menuItemGitHub</Trans>} />
             <FontAwesomeIcon icon={faExternalLinkAlt} className={classes.externalLinkIcon} size="xs" />
           </MenuItem>
 
           <MenuItem component="a" href="https://lists.cncf.io/g/cncf-linkerd-users" target="_blank" className={classes.navMenuItem}>
             <ListItemIcon><EmailIcon className={classes.shrinkIcon} /></ListItemIcon>
-            <ListItemText primary="Mailing List" />
+            <ListItemText primary={<Trans>menuItemMailingList</Trans>} />
             <FontAwesomeIcon icon={faExternalLinkAlt} className={classes.externalLinkIcon} size="xs" />
           </MenuItem>
 
           <MenuItem component="a" href="https://slack.linkerd.io" target="_blank" className={classes.navMenuItem}>
             <ListItemIcon>{slackIcon}</ListItemIcon>
-            <ListItemText primary="Slack" />
+            <ListItemText primary={<Trans>menuItemSlack</Trans>} />
             <FontAwesomeIcon icon={faExternalLinkAlt} className={classes.externalLinkIcon} size="xs" />
           </MenuItem>
 
diff --git a/web/app/js/components/NoMatch.jsx b/web/app/js/components/NoMatch.jsx
index b68fec913..1e2d0d19c 100644
--- a/web/app/js/components/NoMatch.jsx
+++ b/web/app/js/components/NoMatch.jsx
@@ -1,9 +1,14 @@
 import React from 'react';
+import { Trans } from '@lingui/macro';
 
 const NoMatch = () => (
   <div>
     <h3>404</h3>
-    <div>Page not found.</div>
+    <div>
+      <Trans>
+        404Msg
+      </Trans>
+    </div>
   </div>
 );
 
diff --git a/web/app/js/components/ResourceDetail.jsx b/web/app/js/components/ResourceDetail.jsx
index 24fe481a4..3b0edeb9e 100644
--- a/web/app/js/components/ResourceDetail.jsx
+++ b/web/app/js/components/ResourceDetail.jsx
@@ -14,6 +14,7 @@ import SimpleChip from './util/Chip.jsx';
 import Spinner from './util/Spinner.jsx';
 import TopRoutesTabs from './TopRoutesTabs.jsx';
 import TrafficSplitDetail from './TrafficSplitDetail.jsx';
+import { Trans } from '@lingui/macro';
 import Typography from '@material-ui/core/Typography';
 import _filter from 'lodash/filter';
 import _get from 'lodash/get';
@@ -402,14 +403,14 @@ export class ResourceDetailBase extends React.Component {
         {_isEmpty(upstreams) ? null :
         <MetricsTable
           resource="multi_resource"
-          title="Inbound"
+          title={<Trans>tableTitleInbound</Trans>}
           metrics={upstreamDisplayMetrics} />
         }
 
         {_isEmpty(downstreamDisplayMetrics) ? null :
         <MetricsTable
           resource="multi_resource"
-          title="Outbound"
+          title={<Trans>tableTitleOutbound</Trans>}
           metrics={downstreamDisplayMetrics} />
         }
 
@@ -417,13 +418,13 @@ export class ResourceDetailBase extends React.Component {
           resourceType === 'pod' || isTcpOnly ? null :
           <MetricsTable
             resource="pod"
-            title="Pods"
+            title={<Trans>tableTitlePods</Trans>}
             metrics={podMetrics} />
         }
 
         <MetricsTable
           resource="pod"
-          title="TCP"
+          title={<Trans>tableTitleTCP</Trans>}
           isTcpTable
           metrics={podMetrics} />
 
diff --git a/web/app/js/components/ResourceList.jsx b/web/app/js/components/ResourceList.jsx
index 89545d7b0..c1e48aacb 100644
--- a/web/app/js/components/ResourceList.jsx
+++ b/web/app/js/components/ResourceList.jsx
@@ -7,6 +7,7 @@ import MetricsTable from './MetricsTable.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
 import Spinner from './util/Spinner.jsx';
+import { Trans } from '@lingui/macro';
 import { apiErrorPropType } from './util/ApiHelpers.jsx';
 import withREST from './util/withREST.jsx';
 
@@ -33,14 +34,14 @@ export class ResourceListBase extends React.Component {
         <MetricsTable
           resource={resource}
           metrics={processedMetrics}
-          title="HTTP metrics" />
+          title={<Trans>tableTitleHTTPMetrics</Trans>} />
 
         {resource !== 'trafficsplit' &&
         <MetricsTable
           resource={resource}
           isTcpTable
           metrics={processedMetrics}
-          title="TCP metrics" />
+          title={<Trans>tableTitleTCPMetrics</Trans>} />
         }
       </React.Fragment>
     );
diff --git a/web/app/js/components/ServiceMesh.jsx b/web/app/js/components/ServiceMesh.jsx
index e3d445f44..92a28d1eb 100644
--- a/web/app/js/components/ServiceMesh.jsx
+++ b/web/app/js/components/ServiceMesh.jsx
@@ -13,6 +13,7 @@ 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';
@@ -35,11 +36,11 @@ const styles = {
 
 const serviceMeshDetailsColumns = [
   {
-    title: 'Name',
+    title: <Trans>columnTitleName</Trans>,
     dataIndex: 'name',
   },
   {
-    title: 'Value',
+    title: <Trans>columnTitleValue</Trans>,
     dataIndex: 'value',
     isNumeric: true,
   },
@@ -107,9 +108,9 @@ class ServiceMesh extends React.Component {
 
     return [
       { key: 1, name: `${productName} version`, value: releaseVersion },
-      { key: 2, name: `${productName} namespace`, value: controllerNamespace },
-      { key: 3, name: 'Control plane components', value: components.length },
-      { key: 4, name: 'Data plane proxies', value: this.proxyCount() },
+      { 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() },
     ];
   }
 
@@ -225,7 +226,7 @@ class ServiceMesh extends React.Component {
             <Typography variant="h6">Control plane</Typography>
           </Grid>
           <Grid item xs={3}>
-            <Typography align="right">Components</Typography>
+            <Typography align="right"><Trans>componentsMsg</Trans></Typography>
             <Typography align="right">{components.length}</Typography>
           </Grid>
         </Grid>
diff --git a/web/app/js/components/StatusTable.jsx b/web/app/js/components/StatusTable.jsx
index 32ed39744..6ba5089db 100644
--- a/web/app/js/components/StatusTable.jsx
+++ b/web/app/js/components/StatusTable.jsx
@@ -2,6 +2,7 @@ import BaseTable from './BaseTable.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
 import Tooltip from '@material-ui/core/Tooltip';
+import { Trans } from '@lingui/macro';
 import _get from 'lodash/get';
 import _merge from 'lodash/merge';
 import classNames from 'classnames';
@@ -75,18 +76,18 @@ StatusDot.propTypes = {
 
 const columns = {
   resourceName: {
-    title: 'Deployment',
+    title: <Trans>columnTitleDeployment</Trans>,
     dataIndex: 'name',
   },
   pods: {
-    title: 'Pods',
+    title: <Trans>columnTitlePods</Trans>,
     key: 'numEntities',
     isNumeric: true,
     render: d => d.pods.length,
   },
   status: (name, classes) => {
     return {
-      title: name,
+      title: <Trans>columnTitlePodStatus</Trans>,
       key: 'status',
       render: d => {
         return d.pods.map(status => (
diff --git a/web/app/js/components/TapEventTable.jsx b/web/app/js/components/TapEventTable.jsx
index 3972fa598..e07c1b852 100644
--- a/web/app/js/components/TapEventTable.jsx
+++ b/web/app/js/components/TapEventTable.jsx
@@ -11,6 +11,7 @@ import ListItem from '@material-ui/core/ListItem';
 import ListItemText from '@material-ui/core/ListItemText';
 import PropTypes from 'prop-types';
 import React from 'react';
+import { Trans } from '@lingui/macro';
 import Typography from '@material-ui/core/Typography';
 import _get from 'lodash/get';
 import _isEmpty from 'lodash/isEmpty';
@@ -53,7 +54,7 @@ const formatTapLatency = str => {
 };
 
 const httpStatusCol = {
-  title: 'HTTP status',
+  title: <Trans>columnTitleHTTPStatus</Trans>,
   key: 'http-status',
   render: datum => {
     const d = _get(datum, 'responseInit.http.responseInit');
@@ -62,7 +63,7 @@ const httpStatusCol = {
 };
 
 const responseInitLatencyCol = {
-  title: 'Latency',
+  title: <Trans>columnTitleLatency</Trans>,
   key: 'rsp-latency',
   isNumeric: true,
   render: datum => {
@@ -72,7 +73,7 @@ const responseInitLatencyCol = {
 };
 
 const grpcStatusCol = {
-  title: 'GRPC status',
+  title: <Trans>columnTitleGRPCStatus</Trans>,
   key: 'grpc-status',
   render: datum => {
     const d = _get(datum, 'responseEnd.http.responseEnd');
@@ -82,7 +83,7 @@ const grpcStatusCol = {
 };
 
 const pathCol = {
-  title: 'Path',
+  title: <Trans>columnTitlePath</Trans>,
   key: 'path',
   render: datum => {
     const d = _get(datum, 'requestInit.http.requestInit');
@@ -91,7 +92,7 @@ const pathCol = {
 };
 
 const methodCol = {
-  title: 'Method',
+  title: <Trans>columnTitleMethod</Trans>,
   key: 'method',
   render: datum => {
     const d = _get(datum, 'requestInit.http.requestInit');
@@ -101,12 +102,12 @@ const methodCol = {
 
 const topLevelColumns = (resourceType, ResourceLink) => [
   {
-    title: 'Direction',
+    title: <Trans>columnTitleDirection</Trans>,
     key: 'direction',
     render: d => directionColumn(d.base.proxyDirection),
   },
   {
-    title: 'Name',
+    title: <Trans>columnTitleName</Trans>,
     key: 'src-dst',
     render: d => {
       const datum = {
diff --git a/web/app/js/components/TapQueryForm.jsx b/web/app/js/components/TapQueryForm.jsx
index 55173e646..e4d5e97dd 100644
--- a/web/app/js/components/TapQueryForm.jsx
+++ b/web/app/js/components/TapQueryForm.jsx
@@ -24,6 +24,7 @@ import QueryToCliCmd from './QueryToCliCmd.jsx';
 import React from 'react';
 import Select from '@material-ui/core/Select';
 import TextField from '@material-ui/core/TextField';
+import { Trans } from '@lingui/macro';
 import Typography from '@material-ui/core/Typography';
 import _flatten from 'lodash/flatten';
 import _isEmpty from 'lodash/isEmpty';
@@ -287,9 +288,17 @@ class TapQueryForm extends React.Component {
     const { handleTapStart, handleTapStop } = this.props;
 
     if (tapIsClosing) {
-      return (<Button variant="outlined" color="primary" className="tap-ctrl tap-stop" disabled>Stop</Button>);
+      return (
+        <Button variant="outlined" color="primary" className="tap-ctrl tap-stop" disabled>
+          <Trans>buttonStop</Trans>
+        </Button>
+      );
     } else if (tapInProgress) {
-      return (<Button variant="outlined" color="primary" className="tap-ctrl tap-stop" onClick={handleTapStop}>Stop</Button>);
+      return (
+        <Button variant="outlined" color="primary" className="tap-ctrl tap-stop" onClick={handleTapStop}>
+          <Trans>buttonStop</Trans>
+        </Button>
+      );
     } else {
       return (
         <Button
@@ -298,7 +307,7 @@ class TapQueryForm extends React.Component {
           className="tap-ctrl tap-start"
           disabled={!query.namespace || !query.resource}
           onClick={handleTapStart}>
-          Start
+          <Trans>buttonStart</Trans>
         </Button>
       );
     }
@@ -330,7 +339,7 @@ class TapQueryForm extends React.Component {
         <Grid container spacing={3}>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
             <FormControl className={classes.formControl}>
-              {this.renderNamespaceSelect('To Namespace', 'toNamespace', 'toResource')}
+              {this.renderNamespaceSelect(<Trans>formToNamespace</Trans>, 'toNamespace', 'toResource')}
             </FormControl>
           </Grid>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
@@ -343,7 +352,7 @@ class TapQueryForm extends React.Component {
         <Grid container spacing={3}>
           <Grid item xs={6} md={3} classes={{ item: classes.formControlWrapper }}>
             <FormControl className={classes.formControl}>
-              <InputLabel htmlFor="authority">Authority</InputLabel>
+              <InputLabel htmlFor="authority"><Trans>formAuthority</Trans></InputLabel>
               <Select
                 value={query.authority}
                 onChange={this.handleFormChange('authority')}
@@ -359,20 +368,20 @@ class TapQueryForm extends React.Component {
             </FormControl>
           </Grid>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
-            { this.renderTextInput('Path', 'path', 'Display requests with paths that start with this prefix') }
+            { this.renderTextInput(<Trans>formPath</Trans>, 'path', 'Display requests with paths that start with this prefix') }
           </Grid>
         </Grid>
 
         <Grid container spacing={3}>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
-            { this.renderTextInput('Scheme', 'scheme', 'Display requests with this scheme') }
+            { this.renderTextInput(<Trans>formScheme</Trans>, 'scheme', 'Display requests with this scheme') }
           </Grid>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
-            { this.renderTextInput('Max RPS', 'maxRps', `Maximum requests per second to tap. Default ${defaultMaxRps}`) }
+            { this.renderTextInput(<Trans>formMaxRPS</Trans>, 'maxRps', `Maximum requests per second to tap. Default ${defaultMaxRps}`) }
           </Grid>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
             <FormControl className={classes.formControl}>
-              <InputLabel htmlFor="method">HTTP method</InputLabel>
+              <InputLabel htmlFor="method"><Trans>formHTTPMethod</Trans></InputLabel>
               <Select
                 value={query.method}
                 onChange={this.handleFormChange('method')}
@@ -400,7 +409,7 @@ class TapQueryForm extends React.Component {
       <ExpansionPanel expanded={advancedFormExpanded} onChange={this.handleAdvancedFormExpandClick} elevation={3}>
         <ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
           <Typography variant="caption" gutterBottom>
-            {advancedFormExpanded ? 'Hide filters' : 'Show more filters'}
+            {advancedFormExpanded ? <Trans>formHideFilters</Trans> : <Trans>formShowFilters</Trans>}
           </Typography>
         </ExpansionPanelSummary>
 
@@ -421,7 +430,7 @@ class TapQueryForm extends React.Component {
           <Grid container spacing={3}>
             <Grid item xs={6} md="auto" className={classes.formControlWrapper}>
               <FormControl className={classes.formControl} fullWidth>
-                {this.renderNamespaceSelect('Namespace', 'namespace', 'resource')}
+                {this.renderNamespaceSelect(<Trans>formNamespace</Trans>, 'namespace', 'resource')}
               </FormControl>
             </Grid>
 
@@ -433,7 +442,9 @@ class TapQueryForm extends React.Component {
 
             <Grid item>
               { this.renderTapButton(tapRequestInProgress, tapIsClosing) }
-              <Button onClick={this.resetTapForm} disabled={tapRequestInProgress} className={classes.resetButton}>Reset</Button>
+              <Button onClick={this.resetTapForm} disabled={tapRequestInProgress} className={classes.resetButton}>
+                <Trans>buttonReset</Trans>
+              </Button>
             </Grid>
           </Grid>
         </CardContent>
diff --git a/web/app/js/components/TopEventTable.jsx b/web/app/js/components/TopEventTable.jsx
index af262e71b..8638f2677 100644
--- a/web/app/js/components/TopEventTable.jsx
+++ b/web/app/js/components/TopEventTable.jsx
@@ -5,6 +5,7 @@ import BaseTable from './BaseTable.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
 import SuccessRateMiniChart from './util/SuccessRateMiniChart.jsx';
+import { Trans } from '@lingui/macro';
 import _isEmpty from 'lodash/isEmpty';
 import _isNil from 'lodash/isNil';
 import { withContext } from './util/AppContext.jsx';
@@ -16,7 +17,7 @@ const topColumns = (resourceType, ResourceLink, PrefixedLink) => [
     render: d => directionColumn(d.direction),
   },
   {
-    title: 'Name',
+    title: <Trans>columnTitleName</Trans>,
     filter: d => {
       const [labels, display] = extractDisplayName(d);
       return _isEmpty(labels[resourceType]) ?
@@ -27,33 +28,33 @@ const topColumns = (resourceType, ResourceLink, PrefixedLink) => [
     render: d => srcDstColumn(d, resourceType, ResourceLink),
   },
   {
-    title: 'Method',
+    title: <Trans>columnTitleMethod</Trans>,
     dataIndex: 'httpMethod',
     filter: d => d.httpMethod,
     sorter: d => d.httpMethod,
   },
   {
-    title: 'Path',
+    title: <Trans>columnTitlePath</Trans>,
     dataIndex: 'path',
     filter: d => d.path,
     sorter: d => d.path,
   },
   {
-    title: 'Count',
+    title: <Trans>columnTitleCount</Trans>,
     dataIndex: 'count',
     isNumeric: true,
     defaultSortOrder: 'desc',
     sorter: d => d.count,
   },
   {
-    title: 'Best',
+    title: <Trans>columnTitleBest</Trans>,
     dataIndex: 'best',
     isNumeric: true,
     render: d => formatLatencySec(d.best),
     sorter: d => d.best,
   },
   {
-    title: 'Worst',
+    title: <Trans>columnTitleWorst</Trans>,
     dataIndex: 'worst',
     isNumeric: true,
     defaultSortOrder: 'desc',
@@ -61,14 +62,14 @@ const topColumns = (resourceType, ResourceLink, PrefixedLink) => [
     sorter: d => d.worst,
   },
   {
-    title: 'Last',
+    title: <Trans>columnTitleLast</Trans>,
     dataIndex: 'last',
     isNumeric: true,
     render: d => formatLatencySec(d.last),
     sorter: d => d.last,
   },
   {
-    title: 'Success Rate',
+    title: <Trans>columnTitleSuccessRate</Trans>,
     dataIndex: 'successRate',
     isNumeric: true,
     render: d => _isNil(d) || _isNil(d.successRate) ? '---' :
@@ -76,7 +77,7 @@ const topColumns = (resourceType, ResourceLink, PrefixedLink) => [
     sorter: d => d.successRate.get(),
   },
   {
-    title: 'Tap',
+    title: <Trans>columnTitleTap</Trans>,
     key: 'tap',
     isNumeric: true,
     render: d => tapLink(d, resourceType, PrefixedLink),
diff --git a/web/app/js/components/TopRoutes.jsx b/web/app/js/components/TopRoutes.jsx
index 82705058b..f3fd5f6ce 100644
--- a/web/app/js/components/TopRoutes.jsx
+++ b/web/app/js/components/TopRoutes.jsx
@@ -17,6 +17,7 @@ import QueryToCliCmd from './QueryToCliCmd.jsx';
 import React from 'react';
 import Select from '@material-ui/core/Select';
 import TopRoutesModule from './TopRoutesModule.jsx';
+import { Trans } from '@lingui/macro';
 import Typography from '@material-ui/core/Typography';
 import _get from 'lodash/get';
 import _isEmpty from 'lodash/isEmpty';
@@ -232,11 +233,11 @@ class TopRoutes extends React.Component {
 
           <Grid item container spacing={4} alignItems="center" justify="flex-start">
             <Grid item>
-              { this.renderNamespaceDropdown('To Namespace', 'to_namespace', 'Namespace of target resource') }
+              { this.renderNamespaceDropdown(<Trans>formToNamespace</Trans>, 'to_namespace', 'Namespace of target resource') }
             </Grid>
 
             <Grid item>
-              { this.renderResourceDropdown('To Resource', 'to_name', 'to_type', 'Target resource') }
+              { this.renderResourceDropdown(<Trans>formToResource</Trans>, 'to_name', 'to_type', 'Target resource') }
             </Grid>
           </Grid>
         </Grid>
diff --git a/web/app/js/components/TrafficSplitDetail.jsx b/web/app/js/components/TrafficSplitDetail.jsx
index a24bd83c7..da9521769 100644
--- a/web/app/js/components/TrafficSplitDetail.jsx
+++ b/web/app/js/components/TrafficSplitDetail.jsx
@@ -3,6 +3,7 @@ import MetricsTable from './MetricsTable.jsx';
 import Octopus from './Octopus.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
+import { Trans } from '@lingui/macro';
 import Typography from '@material-ui/core/Typography';
 import _each from 'lodash/each';
 import _isNil from 'lodash/isNil';
@@ -73,7 +74,7 @@ const TrafficSplitDetail = ({ resourceMetrics, resourceName, resourceRsp, resour
         metrics={resourceMetrics}
         showName={false}
         showNamespaceColumn={false}
-        title="Leaf Services" />
+        title={<Trans>tableTitleLeafServices</Trans>} />
     </div>
   );
 };
diff --git a/web/app/js/components/util/CopyUtils.jsx b/web/app/js/components/util/CopyUtils.jsx
index b62d45e1b..dbb769ff1 100644
--- a/web/app/js/components/util/CopyUtils.jsx
+++ b/web/app/js/components/util/CopyUtils.jsx
@@ -6,7 +6,7 @@ import Typography from '@material-ui/core/Typography';
 * Instructions for adding resources to service mesh
 */
 export const incompleteMeshMessage = name => {
-  const unspecifiedResources = <Trans>one or more resources</Trans>;
+  const unspecifiedResources = <Trans>unspecifiedResourcesMsg</Trans>;
   const inject = <code>linkerd inject k8s.yml | kubectl apply -f -</code>;
 
   return (
diff --git a/web/app/js/locales/en/messages.json b/web/app/js/locales/en/messages.json
index 59ed9efd0..fac755632 100644
--- a/web/app/js/locales/en/messages.json
+++ b/web/app/js/locales/en/messages.json
@@ -1,5 +1,104 @@
 {
+  "404Msg": "Page not found.",
   "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",
-  "No data to display": "No data to display",
-  "one or more resources": "one or more resources"
+  "All namespaces have a {productName} install.": "All namespaces have a {productName} install.",
+  "Control plane components": "Control plane components",
+  "Data plane proxies": "Data plane proxies",
+  "NoDataToDisplayMsg": "No data to display",
+  "buttonReset": "Reset",
+  "buttonRunLinkerdCheck": "Run Linkerd Check",
+  "buttonStart": "Start",
+  "buttonStop": "Stop",
+  "columnTitleAlive": "Alive",
+  "columnTitleApexService": "Apex Service",
+  "columnTitleBest": "Best",
+  "columnTitleClusterName": "Cluster Name",
+  "columnTitleCount": "Count",
+  "columnTitleDeployment": "Deployment",
+  "columnTitleDirection": "Direction",
+  "columnTitleGRPCStatus": "GRPC Status",
+  "columnTitleGrafana": "Grafana",
+  "columnTitleHTTPStatus": "HTTP Status",
+  "columnTitleIdentity": "Identity",
+  "columnTitleJaeger": "Jaeger",
+  "columnTitleLast": "Last",
+  "columnTitleLatency": "Latency",
+  "columnTitleLeafService": "Leaf Service",
+  "columnTitleMeshed": "Meshed",
+  "columnTitleMeshedPods": "Meshed Pods",
+  "columnTitleMeshedStatus": "Meshed Status",
+  "columnTitleMethod": "Method",
+  "columnTitleName": "Name",
+  "columnTitleNamespace": "Namespace",
+  "columnTitleOpenConnections": "Connections",
+  "columnTitleP50Latency": "P50 Latency",
+  "columnTitleP95Latency": "P95 Latency",
+  "columnTitleP99Latency": "P99 Latency",
+  "columnTitlePairedServices": "Paired Services",
+  "columnTitlePath": "Path",
+  "columnTitlePodStatus": "Pod Status",
+  "columnTitlePods": "Pods",
+  "columnTitleRPS": "RPS",
+  "columnTitleReadRate": "Read Bytes / sec",
+  "columnTitleRoute": "Route",
+  "columnTitleSecured": "Secured",
+  "columnTitleService": "Service",
+  "columnTitleSuccessRate": "Success Rate",
+  "columnTitleTap": "Tap",
+  "columnTitleValue": "Value",
+  "columnTitleWeight": "Weight",
+  "columnTitleWorst": "Worst",
+  "columnTitleWriteRate": "Write Bytes / sec",
+  "componentsMsg": "Components",
+  "formAuthority": "Authority",
+  "formHTTPMethod": "HTTP Method",
+  "formHideFilters": "Hide filters",
+  "formMaxRPS": "Max RPS",
+  "formNamespace": "Namespace",
+  "formPath": "Path",
+  "formScheme": "Scheme",
+  "formShowFilters": "Show more filters",
+  "formToNamespace": "To Namespace",
+  "formToResource": "To Resource",
+  "menuItemCommunity": "Community",
+  "menuItemControlPlane": "Control Plane",
+  "menuItemCronJobs": "Cron Jobs",
+  "menuItemDaemonSets": "Daemon Sets",
+  "menuItemDeployments": "Deployments",
+  "menuItemDocumentation": "Documentation",
+  "menuItemGateway": "Gateway",
+  "menuItemGitHub": "GitHub",
+  "menuItemJobs": "Jobs",
+  "menuItemMailingList": "Mailing List",
+  "menuItemNamespaces": "Namespaces",
+  "menuItemPods": "Pods",
+  "menuItemReplicaSets": "Replica Sets",
+  "menuItemReplicationControllers": "Replication Controllers",
+  "menuItemRoutes": "Routes",
+  "menuItemSlack": "Slack",
+  "menuItemStatefulSets": "Stateful Sets",
+  "menuItemTap": "Tap",
+  "menuItemTop": "Top",
+  "menuItemTrafficSplits": "Traffic Splits",
+  "noResourcesDetectedMsg": "No resources detected.",
+  "podsAreInitializingMsg": "Pods are initializing",
+  "sidebarHeadingCluster": "Cluster",
+  "sidebarHeadingConfiguration": "Configuration",
+  "sidebarHeadingTools": "Tools",
+  "sidebarHeadingWorkloads": "Workloads",
+  "tableTitleEdgesEmpty": "Edges",
+  "tableTitleEdgesWithIdentity {identity}": "Edges (Identity: {identity})",
+  "tableTitleGateways": "Gateways",
+  "tableTitleHTTPMetrics": "HTTP Metrics",
+  "tableTitleInbound": "Inbound",
+  "tableTitleLeafServices": "Leaf Services",
+  "tableTitleOutbound": "Outbound",
+  "tableTitlePods": "Pods",
+  "tableTitleTCP": "TCP",
+  "tableTitleTCPMetrics": "TCP Metrics",
+  "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ß",
+  "{productName} namespace": "{productName} namespace",
+  "{productName} version": "{productName} version"
 }
diff --git a/web/app/js/locales/es/messages.json b/web/app/js/locales/es/messages.json
index a390f26d7..698825593 100644
--- a/web/app/js/locales/es/messages.json
+++ b/web/app/js/locales/es/messages.json
@@ -1,5 +1,104 @@
 {
-  "Add {0} to the k8s.yml file<0/><1/>Then run {inject} to add it to the service mesh": "Agregue {0} a la k8s.yml file<0/><1/>Luego ejecuta {inject} para inyectarla en la malla de servicios",
-  "No data to display": "No hay datos que mostrar",
-  "one or more resources": "uno más recursos más"
+  "404Msg": "Página no encontrada.",
+  "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 components": "Componentes del plano de control",
+  "Data plane proxies": "Proxies del plano de datos",
+  "NoDataToDisplayMsg": "No hay datos que mostrar",
+  "buttonReset": "Restablecer",
+  "buttonRunLinkerdCheck": "Ejecutar Linkerd Check",
+  "buttonStart": "Empezar",
+  "buttonStop": "Terminar",
+  "columnTitleAlive": "Vivo",
+  "columnTitleApexService": "Servicio Apice",
+  "columnTitleBest": "Mejor",
+  "columnTitleClusterName": "Nombre del Clúster",
+  "columnTitleCount": "Total",
+  "columnTitleDeployment": "Deployment",
+  "columnTitleDirection": "Dirección",
+  "columnTitleGRPCStatus": "Estado GRPC",
+  "columnTitleGrafana": "Grafana",
+  "columnTitleHTTPStatus": "Estado HTTP",
+  "columnTitleIdentity": "Identidad",
+  "columnTitleJaeger": "Jaeger",
+  "columnTitleLast": "Último",
+  "columnTitleLatency": "Latencia",
+  "columnTitleLeafService": "Servicio Hoja",
+  "columnTitleMeshed": "En la malla de servicios",
+  "columnTitleMeshedPods": "Pods en la malla de servicios",
+  "columnTitleMeshedStatus": "Estado de la malla de servicios",
+  "columnTitleMethod": "Método",
+  "columnTitleName": "Nombre",
+  "columnTitleNamespace": "Namespace",
+  "columnTitleOpenConnections": "Conexiones",
+  "columnTitleP50Latency": "Latencia P50",
+  "columnTitleP95Latency": "Latencia P95",
+  "columnTitleP99Latency": "Latencia P99",
+  "columnTitlePairedServices": "Servicios Emparejados",
+  "columnTitlePath": "Ruta",
+  "columnTitlePodStatus": "Estado del Pod",
+  "columnTitlePods": "Pods",
+  "columnTitleRPS": "PPS",
+  "columnTitleReadRate": "Lectura Bytes / seg",
+  "columnTitleRoute": "Ruta",
+  "columnTitleSecured": "Asegurado",
+  "columnTitleService": "Servicio",
+  "columnTitleSuccessRate": "Tasa de éxito",
+  "columnTitleTap": "Tap",
+  "columnTitleValue": "Valor",
+  "columnTitleWeight": "Peso",
+  "columnTitleWorst": "Peor",
+  "columnTitleWriteRate": "Escritura Bytes / seg",
+  "componentsMsg": "Componentes",
+  "formAuthority": "Authority",
+  "formHTTPMethod": "Método HTTP",
+  "formHideFilters": "Ocultar filtros",
+  "formMaxRPS": "Max PPS",
+  "formNamespace": "Namespace",
+  "formPath": "Ruta",
+  "formScheme": "Esquema",
+  "formShowFilters": "Mostrar más filtros",
+  "formToNamespace": "Al Namespace",
+  "formToResource": "Al Recurso",
+  "menuItemCommunity": "Comunidad",
+  "menuItemControlPlane": "Plano de Control",
+  "menuItemCronJobs": "Cron Jobs",
+  "menuItemDaemonSets": "Daemon Sets",
+  "menuItemDeployments": "Deployments",
+  "menuItemDocumentation": "Documentación",
+  "menuItemGateway": "Gateway",
+  "menuItemGitHub": "GitHub",
+  "menuItemJobs": "Jobs",
+  "menuItemMailingList": "Lista de Correo",
+  "menuItemNamespaces": "Namespaces",
+  "menuItemPods": "Pods",
+  "menuItemReplicaSets": "Replica Sets",
+  "menuItemReplicationControllers": "Replication Controllers",
+  "menuItemRoutes": "Rutas",
+  "menuItemSlack": "Slack",
+  "menuItemStatefulSets": "Stateful Sets",
+  "menuItemTap": "Tap",
+  "menuItemTop": "Top",
+  "menuItemTrafficSplits": "Traffic Splits",
+  "noResourcesDetectedMsg": "No se han encontrado recursos.",
+  "podsAreInitializingMsg": "Los pods se están inicializando",
+  "sidebarHeadingCluster": "Clúster",
+  "sidebarHeadingConfiguration": "Configuración",
+  "sidebarHeadingTools": "Herramientas",
+  "sidebarHeadingWorkloads": "Cargas de trabajo",
+  "tableTitleEdgesEmpty": "Bordes",
+  "tableTitleEdgesWithIdentity {identity}": "Bordes (Identidad: {identity})",
+  "tableTitleGateways": "Gateways",
+  "tableTitleHTTPMetrics": "Métricas HTTP",
+  "tableTitleInbound": "Entrada",
+  "tableTitleLeafServices": "Servicios Hoja",
+  "tableTitleOutbound": "Salida",
+  "tableTitlePods": "Pods",
+  "tableTitleTCP": "TCP",
+  "tableTitleTCPMetrics": "Métricas TCP",
+  "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.",
+  "{productName} namespace": "Namespace de {productName}",
+  "{productName} version": "Versión de {productName}"
 }


Test patch:
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index f8049d22d..261a8f4a5 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -18,10 +18,7 @@ jobs:
       # actions/checkout@v2
       uses: actions/checkout@722adc6
     - name: Go unit tests
-      env:
-        GITCOOKIE_SH: ${{ secrets.GITCOOKIE_SH }}
       run: |
-        echo "$GITCOOKIE_SH" | bash
         go get gotest.tools/gotestsum@v0.4.2
         # TODO: validate bin/protoc-go.sh does not dirty the repo
         gotestsum -- -cover -race -v -mod=readonly ./...
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index a44226cc4..32b0e467b 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -2335,17 +2335,8 @@ metadata:
 	})
 }
 
-func getConfigAndKubeSystemNamespace(ha bool, nsLabel string) []string {
+func getWebhookAndKubeSystemNamespace(nsLabel string, failurePolicy string) []string {
 	return []string{fmt.Sprintf(`
-kind: ConfigMap
-apiVersion: v1
-metadata:
-  name: linkerd-config
-  namespace: linkerd
-data:
-  install: |
-    {"cliVersion":"dev-undefined","flags":[{"name":"ha","value":"%v"}]}`, ha),
-		fmt.Sprintf(`
 apiVersion: v1
 kind: Namespace
 metadata:
@@ -2353,6 +2344,35 @@ metadata:
   labels:
     %s
   name: kube-system`, nsLabel),
+		fmt.Sprintf(`
+apiVersion: admissionregistration.k8s.io/v1beta1
+kind: MutatingWebhookConfiguration
+metadata:
+  name: linkerd-proxy-injector-webhook-config
+  labels:
+    linkerd.io/control-plane-component: proxy-injector
+    linkerd.io/control-plane-ns: linkerd
+webhooks:
+- name: linkerd-proxy-injector.linkerd.io
+  namespaceSelector:
+    matchExpressions:
+    - key: config.linkerd.io/admission-webhooks
+      operator: NotIn
+      values:
+      - disabled
+  clientConfig:
+    service:
+      name: linkerd-proxy-injector
+      namespace: linkerd
+      path: "/"
+    caBundle: cHJveHkgaW5qZWN0b3IgQ0EgYnVuZGxl
+  failurePolicy: %s
+  rules:
+  - operations: [ "CREATE" ]
+    apiGroups: [""]
+    apiVersions: ["v1"]
+    resources: ["pods"]
+  sideEffects: None`, failurePolicy),
 	}
 }
 
@@ -2363,24 +2383,24 @@ func TestKubeSystemNamespaceInHA(t *testing.T) {
 		expectedOutput  string
 	}{
 		{
-			"passes when HA is not enabled",
-			getConfigAndKubeSystemNamespace(false, ""),
+			"passes when webhook policy is Ignore is not enabled",
+			getWebhookAndKubeSystemNamespace("", "Ignore"),
 			"",
 		},
 		{
-			"passes when HA is enabled and namespace has required metadata",
-			getConfigAndKubeSystemNamespace(true, "config.linkerd.io/admission-webhooks: disabled"),
+			"passes when webhook policy is Fail and namespace has required metadata",
+			getWebhookAndKubeSystemNamespace("config.linkerd.io/admission-webhooks: disabled", "Fail"),
 			"l5d-injection-disabled pod injection disabled on kube-system",
 		},
 		{
-			"fails when HA and admission hooks are enabled",
-			getConfigAndKubeSystemNamespace(true, "config.linkerd.io/admission-webhooks: enabled"),
-			"l5d-injection-disabled pod injection disabled on kube-system: kube-system namespace needs to have the label config.linkerd.io/admission-webhooks: disabled if HA mode is enabled",
+			"fails when webhook policy is Fail and admission hooks are enabled",
+			getWebhookAndKubeSystemNamespace("config.linkerd.io/admission-webhooks: enabled", "Fail"),
+			"l5d-injection-disabled pod injection disabled on kube-system: kube-system namespace needs to have the label config.linkerd.io/admission-webhooks: disabled if injector webhook failure policy is Fail",
 		},
 		{
-			"fails when HA is enabled and metadata is missing",
-			getConfigAndKubeSystemNamespace(true, ""),
-			"l5d-injection-disabled pod injection disabled on kube-system: kube-system namespace needs to have the label config.linkerd.io/admission-webhooks: disabled if HA mode is enabled",
+			"fails when webhook policy is Fail and metadata is missing",
+			getWebhookAndKubeSystemNamespace("", "Fail"),
+			"l5d-injection-disabled pod injection disabled on kube-system: kube-system namespace needs to have the label config.linkerd.io/admission-webhooks: disabled if injector webhook failure policy is Fail",
 		},
 	}
 
@@ -2391,13 +2411,7 @@ func TestKubeSystemNamespaceInHA(t *testing.T) {
 			hc := NewHealthChecker([]CategoryID{}, &Options{})
 			hc.ControlPlaneNamespace = "linkerd"
 
-			var err error
 			hc.kubeAPI, _ = k8s.NewFakeAPI(tc.k8sConfigs...)
-			_, hc.linkerdConfig, err = hc.checkLinkerdConfigConfigMap()
-			if err != nil {
-				t.Fatalf("Unexpected error: %q", err)
-			}
-
 			hc.addCheckAsCategory("l5d-injection-disabled", LinkerdHAChecks, "pod injection disabled on kube-system")
 
 			obs := newObserver()
diff --git a/web/app/js/components/TopRoutesTable.jsx b/web/app/js/components/TopRoutesTable.jsx
index 266d9afac..d13f929a8 100644
--- a/web/app/js/components/TopRoutesTable.jsx
+++ b/web/app/js/components/TopRoutesTable.jsx
@@ -2,52 +2,53 @@ import BaseTable from './BaseTable.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
 import SuccessRateMiniChart from './util/SuccessRateMiniChart.jsx';
+import { Trans } from '@lingui/macro';
 import { metricToFormatter } from './util/Utils.js';
 
 const routesColumns = [
   {
-    title: 'Route',
+    title: <Trans>columnTitleRoute</Trans>,
     dataIndex: 'route',
     filter: d => d.route,
     sorter: d => d.route,
   },
   {
-    title: 'Service',
+    title: <Trans>columnTitleService</Trans>,
     tooltip: 'hostname:port used when communicating with this target',
     dataIndex: 'authority',
     filter: d => d.authority,
     sorter: d => d.authority,
   },
   {
-    title: 'Success Rate',
+    title: <Trans>columnTitleSuccessRate</Trans>,
     dataIndex: 'successRate',
     isNumeric: true,
     render: d => <SuccessRateMiniChart sr={d.successRate} />,
     sorter: d => d.successRate,
   },
   {
-    title: 'RPS',
+    title: <Trans>columnTitleRPS</Trans>,
     dataIndex: 'requestRate',
     isNumeric: true,
     render: d => metricToFormatter.NO_UNIT(d.requestRate),
     sorter: d => d.requestRate,
   },
   {
-    title: 'P50 Latency',
+    title: <Trans>columnTitleP50Latency</Trans>,
     dataIndex: 'latency.P50',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.latency.P50),
     sorter: d => d.latency.P50,
   },
   {
-    title: 'P95 Latency',
+    title: <Trans>columnTitleP95Latency</Trans>,
     dataIndex: 'latency.P95',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.latency.P95),
     sorter: d => d.latency.P95,
   },
   {
-    title: 'P99 Latency',
+    title: <Trans>columnTitleP99Latency</Trans>,
     dataIndex: 'latency.P99',
     isNumeric: true,
     render: d => metricToFormatter.LATENCY(d.latency.P99),