From 8e512de30832598c6d7c389b137a59e59690779c Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 14 Oct 2022 11:55:15 +0100 Subject: [PATCH] I18n: update contributing docs (#56860) * update contrib i18n docs * update contrib i18n docs * remove lingui references, rearrage plain js usage --- contribute/internationalization.md | 171 ++++++++---------- .../app/core/internationalization/index.tsx | 2 + 2 files changed, 73 insertions(+), 100 deletions(-) diff --git a/contribute/internationalization.md b/contribute/internationalization.md index 88ebb57b759..06e2cfdb4f7 100644 --- a/contribute/internationalization.md +++ b/contribute/internationalization.md @@ -1,42 +1,66 @@ # Internationalization -Grafana uses the [LinguiJS](https://github.com/lingui/js-lingui) framework for managing translating phrases in the Grafana frontend. +Grafana uses the [i18next](https://www.i18next.com/) framework for managing translating phrases in the Grafana frontend. ## tl;dr -- Use `Go to {panel.title}` in code to add a translatable phrase -- Translations are stored in .po files in `public/locales/{locale}/messages.po` -- If a particular phrase is not available in the a language then it will fall back to English -- To update phrases in English, edit the `en/messages.po` file, not the source JSX -- To update phrases in any translated language, edit the phrase in Crowdin +**Please note:** We do not currently accept contributions for translations. Please do not submit pull requests for grafana.json files - they will be rejected. -**Please note:** We do not currently accept contributions for translations. Please do not submit pull requests for messages.po files - they will be rejected. +- Use `Go to {{ pageTitle }}` in code to add a translatable phrase +- Translations are stored in JSON files in `public/locales/{locale}/grafana.json` +- If a particular phrase is not available in the a language then it will fall back to English +- To update phrases in English, edit the default phrase in the component's source, then run `yarn i18n:extract`. Do not edit the `en-ES/grafana.json` or update the english phrase in Crowdin +- To update phrases in any translated language, edit the phrase in Crowdin. Do not edit the `{locale}/grafana.json` ## How to add a new translation phrase -1. Use one of `@lingui/macro`'s React components with the `id`, ensuring it conforms to the guidelines below, with the default english translation. e.g. +### JSX + +1. For JSX children, use the `` component from `app/core/internationalization` with the `i18nKey`, ensuring it conforms to the guidelines below, with the default english translation. e.g. ```jsx -import { Trans } from @lingui/macro +import { Trans } from 'app/core/internationalization'; -const SearchTitle = ({term}) => ( - - Results for {term} +const SearchTitle = ({ term }) => ( + + Results for {{ term }} ); ``` -Prefer using the JSX components (compared to the plain javascript functions, see below) where possible for phrases. Many props can (and probably should) be changed to accept the `React.ReactNode` instead of `string` for phrases put into the DOM. +Prefer using `` for JSX children, and `t()` for props and other javascript usage. -Note that Lingui must be able to statically analyse the code to extract the phrase, so the `id` can not be dynamic. e.g. the following will not work: +When translating in grafana-ui, import `` and `t()` from `src/utils/i18n`. + +Note that our tooling must be able to statically analyse the code to extract the phrase, so the `i18nKey` can not be dynamic. e.g. the following will not work: ```jsx -const ErrorMessage = ({ id, message }) => There was an error: {message}; +const ErrorMessage = ({ id, message }) => There was an error: {{ message }}; ``` 2. Upon reload, the default English phrase will appear on the page. -3. Before submitting your PR, run the `yarn i18n:extract` command to extract the messages you added into the `messages.po` file and make them available for translation. +3. Before submitting your PR, run the `yarn i18n:extract` command to extract the messages you added into the `grafana.json` file and make them available for translation. + +### Plain JS usage + +Sometimes you may need to translate a string cannot be represented in JSX, such as `placeholder` props. Use the `t` macro for this. + +```jsx +import { t } from "app/core/internationalization" + +const placeholder = t('form.username-placeholder','Username'); + +return +``` + +Interpolating phrases is a bit more verbose. Make sure the placeholders in the string match the values passed in the object - there's no type safety here! + +```jsx +const placeholder = t('page.greeting', 'Hello {{ username }}', { username }); +``` + +While the `t` function can technically be used outside of React functions (e.g, in actions/reducers), aim to keep all UI phrases within the React UI functions. ## How to add a new language @@ -52,13 +76,14 @@ const ErrorMessage = ({ id, message }) => There was a ## How translations work in Grafana -Grafana uses the [LinguiJS](https://github.com/lingui/js-lingui) framework for managing translating phrases in the Grafana frontend. It: +Grafana uses the [i18next](https://www.i18next.com/) framework for managing translating phrases in the Grafana frontend. It: - Marks up phrases within our code for extraction - Extracts phrases into messages catalogues for translating in external systems -- "Compiles" the catalogues to a format that can be used in the website - Manages the user's locale and putting the translated phrases in the UI +English phrases remain in our Javascript bundle in the source components (as the `` or `t()` default phrase). At runtime, we don't need to load any messages for en-US. If the user's language preference is set to another language, Grafana will load that translations's messages JSON before the initial render. + ### Phrase ID naming convention We set explicit IDs for phrases to make it easier to identify phrases out of context, and to track where they're used. IDs follow a naming scheme that includes _where_ the phrase is used. The exception is the rare case of single reoccuring words like "Cancel", but default to using a feature/phrase specific phrase. @@ -73,137 +98,83 @@ For components used all over the site, use just two segments: - `footer.update` - `navigation.home` -### Top-level provider +### I18next context -In [AppWrapper.tsx](/public/app/AppWrapper.tsx) the app is wrapped with `I18nProvider` from `public/app/core/internationalization/index.tsx` where the Lingui instance is created with the user's preferred locale. This sets the appropriate context and allows any component from `@lingui/macro` to use the translations for the user's preferred locale. - -### Message format - -Lingui uses the [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/) for the phrases in the .po catalogues. ICU has special syntax especially for describing plurals across multiple languages. For more details see the [Lingui docs](https://lingui.js.org/ref/message-format.html). - -### Plain JS usage - -See [Lingui Docs](https://lingui.js.org/ref/macro.html#t) for more details. - -Sometimes you may need to translate a string cannot be represented in JSX, such as `placeholder` props. Use the `t` macro for this. - -```jsx -import { t } from "@lingui/macro" - -const placeholder = t({ - id: 'form.username-placeholder', - message: `Username` -}); - -return -``` - -While the `t` macro can technically be used outside of React functions (e.g, in actions/reducers), aim to keep all UI phrases within the React UI functions. +We rely on a global i18next singleton (that lives inside the i18next) for storing the i18next config/context. ## Examples -See the [Lingui docs](https://lingui.js.org/ref/macro.html#usage) for more details. +See [i18next](https://www.i18next.com/) and [react-i18next](https://react.i18next.com/) documentation for more details. ### Basic usage For fixed phrases: ```jsx -import { Trans } from '@lingui/macro'; +import { Trans } from 'app/core/internationalization'; -Hello user!; +Hello user!; ``` -You can include variables, just like regular JSX. Prefer using "simple" variables to make the extracted phrase easier to read for translators +To interpolate variables, include it as an object child. It's weird syntax, but Trans will do it's magic to make it work: ```jsx -import { Trans } from '@lingui/macro'; +import { Trans } from 'app/core/internationalization'; -// Bad - translators will see: Hello {0} -Hello {user.name}!; +Hello {{ name: user.name }}!; -// Good - translators will see: Hello {userName} const userName = user.name; -Hello {userName}!; +Hello {{ userName }}!; ``` Variables must be strings (or, must support calling `.toString()`, which we almost never want). ```jsx -import { Trans } from '@lingui/macro'; +import { Trans } from 'app/core/internationalization'; // This will not work const userName = user.name; -Hello {userName}!; +Hello {{ userName }}!; // Instead, put the JSX inside the phrase directly const userName = user.name; - - Hello {userName}! + + Hello {{ userName }}! ; ``` ### React components and HTML tags -Both HTML tags and React components can be included in a phase. The Lingui macro will replace them with placeholder tags for the translators +Both HTML tags and React components can be included in a phase. The Trans function will handle interpolating it's children properly ```js -import { Trans } from "@lingui/macro" +import { Trans } from "app/core/internationalization" -const randomVariable = "variable" - - + Click to learn more. -// ↓ is transformed by macros into ↓ -, - - ]} -/> - -// ↓ is in the messages.po file like ↓ -msgid "page.explainer" -msgstr "Click <0>here to <1>learn more" +// ↓ is in the grafana.json file like ↓ +{ + "page": { + "explainer": "Click <0>here to <1>learn more" + } +} ``` ### Plurals -See the [Lingui docs](https://lingui.js.org/ref/macro.html#id1) for more details. - -Plurals require special handling to make sure they can be translating according to the rules of each locale (which may be more complex that you think!). Use the `` component and specify the plural forms for the default language (English). The message will be extracted into a form where translators can extend it with rules for other locales. +Plurals require special handling to make sure they can be translating according to the rules of each locale (which may be more complex that you think!). Use the `` component, with the `count` prop. ```js -import { Plural } from "@lingui/macro" +import { Trans } from 'app/core/internationalization'; - - -// ↓ is transformed by macros into ↓ - - - -// sharedCount = 0 -> Not shared with anyone -// sharedCount = 1 -> Shared with one person -// sharedCount = 3 -> Shared with # people + + You got {{ count: messages.length }} messages. +; ``` -### Date and time - -[Lingui has functions](https://lingui.js.org/ref/core.html#I18n.date) to format dates and times according to the convention to the user's preferred locale, based on the browser [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) API. However, as displaying dates and times is fundamental to Grafana, guidelines have not been established for this yet. +Once extracted with `yarn i18n:extract` you will need to manually fill in the grafana.json message catalogues with the additional plural forms. See the [react-i18next docs](https://react.i18next.com/latest/trans-component#plural) for more details. ## Documentation diff --git a/public/app/core/internationalization/index.tsx b/public/app/core/internationalization/index.tsx index a99fa2acfb4..824ee4b8584 100644 --- a/public/app/core/internationalization/index.tsx +++ b/public/app/core/internationalization/index.tsx @@ -50,6 +50,8 @@ export function initializeI18n(locale: string) { // If translations are empty strings (no translation), fall back to the default value in source code returnEmptyString: false, + + pluralSeparator: '__', }); // This is a placeholder so we can put a 'comment' in the message json files.