Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add i18n library to Linkerd dashboard #4803

Merged
merged 5 commits into from
Jul 30, 2020
Merged

Add i18n library to Linkerd dashboard #4803

merged 5 commits into from
Jul 30, 2020

Conversation

scottcarol
Copy link
Contributor

Part of #3651

This PR adds the LinguiJS project to the Linkerd dashboard for i18n and translation. This is an open source project that is well documented and updated, with an active community. After evaluating several libraries this seemed like a lightweight option with easy CLI tools, making it easy for other contributors to learn and use.

This PR ONLY changes 2 React components. The purpose is for reviewers to evaluate the use of the library, adding a language, editing translations, compiling, etc. After this approach is validated a second PR will go up which adds translations for all of the remaining components.

How to test this PR

Viewing the dashboard

  1. Check out this branch and run bin/web dev or bin/web run.

  2. With your browser language set to en open http://localhost:7777/namespaces/linkerd/replicationcontrollers

  3. If you have the standard Linkerd setup you should see:
    Screen Shot 2020-07-27 at 4 24 10 PM

  4. Change your browser language to es. You should see:
    Screen Shot 2020-07-27 at 4 24 43 PM

Adding a translation

  1. Open Namespace.jsx in your code editor.
  2. Apply this diff. Note importing Trans and wrapping text in it.
diff --git a/web/app/js/components/Namespace.jsx b/web/app/js/components/Namespace.jsx
index 65f32e77..c33a33e8 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>No resources detected.</Trans></div> : null}
             {
               _isEmpty(deploymentsWithMetrics) ? null :
               <NetworkGraph namespace={ns} deployments={metrics.deployment} />
  1. From web/app run yarn extract
  2. It should indicate that you have 1 missing translation in each file
    Screen Shot 2020-07-27 at 4 28 51 PM
  3. Open locales/es/messages.json and locales/en/messages.json and fill in the blanks.
  4. Run yarn extract again. It should say you have 0 missing translations.
  5. Run yarn compile.
  6. Load http://localhost:7777/namespaces/blabhablhab from an en browser and an es browser. You should see the translations you put in. If your browser is set to a different language, only the English key will show.

Adding a locale

  1. From web/app run yarn add-locale fr then yarn extract
  2. Open locales/fr/messages.json and add sample values
  3. Add import catalogFr from './locales/fr/messages.js’; to index.js
  4. Run yarn compile
  5. Load dashboard with your browser set to fr

Signed-off-by: Carol Scott carol@buoyant.io

@scottcarol scottcarol requested a review from a team as a code owner July 28, 2020 00:23
@scottcarol scottcarol self-assigned this Jul 28, 2020
.gitignore Show resolved Hide resolved
bin/web Outdated Show resolved Hide resolved
return (
<Card className={classes.card} elevation={3}>
<CardContent>
<Typography>
{content}
<Trans>No data to display</Trans>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example of translating a static text string.

Also, we were never passing in a prop content to this component, so we could remove the propTypes checks.


return (
<Typography variant="body2">
<Trans>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example of translating a block of text containing a dynamic text value.

web/app/js/index.js Outdated Show resolved Hide resolved
@scottcarol scottcarol requested review from alpeb and grampelberg July 28, 2020 00:26
Copy link
Member

@alpeb alpeb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @scottcarol , the explained workflow feels pretty smooth 💃 (you only missed mentioning under the "adding locale section" adding the new locale into the catalogs const).

One very specific edge case I tested was "generic locale fallback" support, that helps overriding specific translations that differ across countries using the same language (that I also mentioned in the previous PR). So I added one catalog for es and another for es-CO; by the way this is the right format that took me a bit to figure:

const catalogs = { en: catalogEn, es: catalogEs, "es-CO": catalogEsCo, fr: catalogFr };

If I browsed the site with a "es-CO" locale it will rightly show the translations. If a message isn't translated it will fallback to the English word, but I was hoping it would fallback to the generic es translation. Maybe that's just a lingui limitation? Anyways, not a deal breaker if so.

@@ -72,9 +75,13 @@ class App extends React.Component {
}

render() {
const catalogs = { en: catalogEn, es: catalogEs };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean every translation will get loaded regardless of the current user language? If so, probably not a big deal, but would be better if they could be loaded dynamically...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I updated it so that we are only passing the necessary catalog into I18nProvider.

I tried taking it one step further and dynamically loading the specific catalog we needed into index.js:

const selectedCatalog = require(`./locales/${selectedLocale}/messages.js`);

but I hit the no-dynamic-require es-lint rule, which seems to be best practices, so I am continuing to load all of the possible catalogs into this file.

Comment on lines 100 to 102
"add-locale": "lingui add-locale",
"extract": "lingui extract",
"compile": "lingui compile"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aliases can be less expressive than the original command. yarn compile in particular sounds like it would do more than compile locales 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a good point. To be more explicit, I updated the commands so they include lingui.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case the scripts section is no longer necessary, is it? Or are you leaving that there just to document the available commands?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're totally right, it's not needed now. Just removed, and the BUILD.md should be the file with the documentation of commands. I can add documentation in the follow-up PR I will make after this.

@scottcarol
Copy link
Contributor Author

scottcarol commented Jul 29, 2020

@alpeb Thanks for taking a look!

Based on your feedback around generic locale fallback support, I took a look in the js-lingui repo and they recommend using the locales-detector library, so I added this.

If my browser locales are es-AR, fr-CA, we create an array of languages: ['es-AR', 'es', 'fr-CA', 'fr'] and return the first match to an existing catalog (and default to the en catalog if there are no matches).

If this updated branch makes sense to you, I'll pull out the logic into its own util file and add tests.

@alpeb
Copy link
Member

alpeb commented Jul 29, 2020

Nice, this fixes the case when say the browser uses es-AR and if that locale is missing it falls back to es 👍

Do you know if there's a chance to have this fallback mechanism operate at the single phrase level?
E.g. I have a complete catalog locales/es/messages.js with all the entries for standard Spanish, and I also have locales/es_AR/messages.js but that only contains overrides for phrases that differ from the standard Spanish. So if an entry is not there, it should fallback to the entry from the es locale.
Maybe if lingui doesn't support that, the right workflow in that case could be having users copy all the entries from es and overwrite what they need in es-AR...

@scottcarol
Copy link
Contributor Author

Good question @alpeb:

Do you know if there's a chance to have this fallback mechanism operate at the single phrase level?

When we run lingui add-locale my-LOCALE and then lingui extract it creates a new JSON file with all of the existing keys and empty values. If we want to use the lingui extract command to call out anything we need to fix/add, we will need to have complete keys/values for each translation file. Otherwise any empty values would be listed as "missing" in the command output. I'm thinking there is a benefit for each translation file to stand independently, since it might be hard to communicate which values are actually missing and which values are intentionally blank, but I'll look back at the docs.

@scottcarol
Copy link
Contributor Author

@alpeb I can't immediately find best practices on individual default fallback values, but if it's ok with you, we could merge this PR, in a follow-up PR add translations for es and en, and then decide on our process for adding locale-SPECIFIC formats.

Copy link
Member

@alpeb alpeb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alpeb I can't immediately find best practices on individual default fallback values, but if it's ok with you, we could merge this PR, in a follow-up PR add translations for es and en, and then decide on our process for adding locale-SPECIFIC formats.

Yeah definitely! I was you wondering about that since I remembering doing that for some project a thousand years ago, but this is shippable as is 👍 🥇

Copy link
Contributor

@grampelberg grampelberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, we probably want to use keys instead of the strings themselves as keys but that's an eventual improvement.

@scottcarol scottcarol merged commit eec8905 into main Jul 30, 2020
@scottcarol scottcarol deleted the cscott/linguijs branch July 30, 2020 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants