Internationalization: Correctly generate plural forms (#71341)

* user essentials mob! 🔱

lastFile:public/locales/pseudo-LOCALE/grafana.json

* user essentials mob! 🔱

* user essentials mob! 🔱

lastFile:contribute/internationalization.md

* user essentials mob! 🔱

lastFile:contribute/internationalization.md

* move pseudo generation to precommit hook if en-US file is modified

Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com>
Co-authored-by: tskarhed <1438972+tskarhed@users.noreply.github.com>

* fix unit tests

---------

Co-authored-by: Joao Silva <joao.silva@grafana.com>
Co-authored-by: joshhunt <josh@trtr.co>
Co-authored-by: Roxana Turc <anamaria-roxana.turc@grafana.com>
Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com>
Co-authored-by: tskarhed <1438972+tskarhed@users.noreply.github.com>
This commit is contained in:
Ashley Harrison 2023-07-11 16:37:01 +01:00 committed by GitHub
parent 17d8fca289
commit 2650aa5600
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 128 additions and 31 deletions

View File

@ -9,7 +9,7 @@ Grafana uses the [i18next](https://www.i18next.com/) framework for managing tran
- Use `<Trans i18nKey="search-results.panel-link">Go to {{ pageTitle }}</Trans>` 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 English, edit the default phrase in both the component's source and the [English grafana.json message catalogue](../public/locales/en-US/grafana.json), then run `yarn i18n:extract`.
- 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
@ -80,7 +80,7 @@ Grafana uses the [i18next](https://www.i18next.com/) framework for managing tran
- Extracts phrases into messages catalogues for translating in external systems
- 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 `<Trans />` 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.
Grafana will load the message catalogue JSON before the initial render.
### Phrase ID naming convention
@ -162,17 +162,32 @@ import { Trans } from "app/core/internationalization"
### Plurals
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 `<Trans />` component, with the `count` prop.
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 either the `<Trans />` component or the `t` function, with the `count` prop to provide a singular form.
```js
import { Trans } from 'app/core/internationalization';
<Trans i18nKey="newMessages" count={messages.length}>
You got {{ count: messages.length }} messages.
<Trans i18nKey="inbox.heading" count={messages.length}>
You got {{ count: messages.length }} message
</Trans>;
```
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.
```js
import { t } from 'app/core/internationalization';
const translatedString = t('inbox.heading', 'You got {{count}} message', { count: messages.length });
```
Once extracted with `yarn i18n:extract` you will need to manually edit the [English grafana.json message catalogue](../public/locales/en-US/grafana.json) to correct the plural forms. See the [react-i18next docs](https://react.i18next.com/latest/trans-component#plural) for more details.
```json
{
"inbox": {
"heading__one": "You got {{count}} message",
"heading__other": "You got {{count}} messages"
}
}
```
## Documentation

View File

@ -46,8 +46,9 @@
"ci:test-frontend": "yarn run test:ci",
"postinstall": "husky install",
"i18n:clean": "rimraf public/locales/en-US/grafana.json",
"i18n:extract": "yarn i18n:clean && yarn run i18next -c public/locales/i18next-parser.config.js 'public/**/*.{tsx,ts}' 'packages/grafana-ui/**/*.{tsx,ts}' && node ./public/locales/psuedo.js",
"i18n:extract": "yarn run i18next -c public/locales/i18next-parser.config.js 'public/**/*.{tsx,ts}' 'packages/grafana-ui/**/*.{tsx,ts}'",
"i18n:compile": "echo 'no i18n compile yet, all good'",
"i18n:pseudo": "node ./public/locales/pseudo.js",
"betterer": "betterer",
"betterer:merge": "betterer merge",
"betterer:stats": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/reportBettererStats.ts",
@ -76,6 +77,9 @@
],
"*public/app/plugins/**/**/*.cue": [
"make fix-cue"
],
"./public/locales/en-US/grafana.json": [
"yarn i18n:pseudo"
]
},
"devDependencies": {

View File

@ -24,7 +24,7 @@ export const LANGUAGES: LanguageDefinitions[] = [
{
code: ENGLISH_US,
name: 'English',
loader: () => Promise.resolve({}),
loader: () => import('../../../locales/en-US/grafana.json'),
},
{

View File

@ -23,11 +23,8 @@ export function initializeI18n(language: string) {
const validLocale = VALID_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE;
// This is a placeholder so we can put a 'comment' in the message json files.
// Starts with an underscore so it's sorted to the top of the file
t(
'_comment',
'Do not manually edit this file, or update these source phrases in Crowdin. The source of truth for English strings are in the code source'
);
// Starts with an underscore so it's sorted to the top of the file. Even though it is in a comment the following line is still extracted
// t('_comment', 'This file is the source of truth for English strings. Edit this to change plurals and other phrases for the UI.');
return i18n
.use(loadTranslations)

View File

@ -2,17 +2,19 @@ import { buildBreakdownString } from './utils';
describe('browse-dashboards utils', () => {
describe('buildBreakdownString', () => {
// note: pluralisation is handled as part of the i18n framework
// in tests, only the fallback singular message is used
it.each`
folderCount | dashboardCount | libraryPanelCount | alertRuleCount | expected
${0} | ${0} | ${0} | ${0} | ${'0 items'}
${0} | ${0} | ${0} | ${0} | ${'0 item'}
${1} | ${0} | ${0} | ${0} | ${'1 item: 1 folder'}
${2} | ${0} | ${0} | ${0} | ${'2 items: 2 folders'}
${2} | ${0} | ${0} | ${0} | ${'2 item: 2 folder'}
${0} | ${1} | ${0} | ${0} | ${'1 item: 1 dashboard'}
${0} | ${2} | ${0} | ${0} | ${'2 items: 2 dashboards'}
${1} | ${0} | ${1} | ${1} | ${'3 items: 1 folder, 1 library panel, 1 alert rule'}
${2} | ${0} | ${3} | ${4} | ${'9 items: 2 folders, 3 library panels, 4 alert rules'}
${1} | ${1} | ${1} | ${1} | ${'4 items: 1 folder, 1 dashboard, 1 library panel, 1 alert rule'}
${1} | ${2} | ${3} | ${4} | ${'10 items: 1 folder, 2 dashboards, 3 library panels, 4 alert rules'}
${0} | ${2} | ${0} | ${0} | ${'2 item: 2 dashboard'}
${1} | ${0} | ${1} | ${1} | ${'3 item: 1 folder, 1 library panel, 1 alert rule'}
${2} | ${0} | ${3} | ${4} | ${'9 item: 2 folder, 3 library panel, 4 alert rule'}
${1} | ${1} | ${1} | ${1} | ${'4 item: 1 folder, 1 dashboard, 1 library panel, 1 alert rule'}
${1} | ${2} | ${3} | ${4} | ${'10 item: 1 folder, 2 dashboard, 3 library panel, 4 alert rule'}
`(
'returns the correct message for the various inputs',
({ folderCount, dashboardCount, libraryPanelCount, alertRuleCount, expected }) => {

View File

@ -1,3 +1,5 @@
import { t } from 'app/core/internationalization';
export function buildBreakdownString(
folderCount: number,
dashboardCount: number,
@ -7,18 +9,18 @@ export function buildBreakdownString(
const total = folderCount + dashboardCount + libraryPanelCount + alertRuleCount;
const parts = [];
if (folderCount) {
parts.push(`${folderCount} ${folderCount === 1 ? 'folder' : 'folders'}`);
parts.push(t('browse-dashboards.counts.folder', '{{count}} folder', { count: folderCount }));
}
if (dashboardCount) {
parts.push(`${dashboardCount} ${dashboardCount === 1 ? 'dashboard' : 'dashboards'}`);
parts.push(t('browse-dashboards.counts.dashboard', '{{count}} dashboard', { count: dashboardCount }));
}
if (libraryPanelCount) {
parts.push(`${libraryPanelCount} ${libraryPanelCount === 1 ? 'library panel' : 'library panels'}`);
parts.push(t('browse-dashboards.counts.libraryPanel', '{{count}} library panel', { count: libraryPanelCount }));
}
if (alertRuleCount) {
parts.push(`${alertRuleCount} ${alertRuleCount === 1 ? 'alert rule' : 'alert rules'}`);
parts.push(t('browse-dashboards.counts.alertRule', '{{count}} alert rule', { count: alertRuleCount }));
}
let breakdownString = `${total} ${total === 1 ? 'item' : 'items'}`;
let breakdownString = t('browse-dashboards.counts.total', '{{count}} item', { count: total });
if (parts.length > 0) {
breakdownString += `: ${parts.join(', ')}`;
}

View File

@ -1,5 +1,5 @@
{
"_comment": "Do not manually edit this file. Translations must be made in Crowdin which will sync them back into this file",
"_comment": "",
"access-control": {
"add-permission": {
"role-label": "",
@ -39,6 +39,18 @@
"moving": "",
"new-folder-name-required-phrase": ""
},
"counts": {
"alertRule__one": "",
"alertRule__other": "",
"dashboard__one": "",
"dashboard__other": "",
"folder__one": "",
"folder__other": "",
"libraryPanel__one": "",
"libraryPanel__other": "",
"total__one": "",
"total__other": ""
},
"dashboards-tree": {
"name-column": "",
"tags-column": ""

View File

@ -1,5 +1,5 @@
{
"_comment": "Do not manually edit this file, or update these source phrases in Crowdin. The source of truth for English strings are in the code source",
"_comment": "This file is the source of truth for English strings. Edit this to change plurals and other phrases for the UI.",
"access-control": {
"add-permission": {
"role-label": "Role",
@ -39,6 +39,18 @@
"moving": "Moving...",
"new-folder-name-required-phrase": "Folder name is required."
},
"counts": {
"alertRule__one": "{{count}} alert rule",
"alertRule__other": "{{count}} alert rules",
"dashboard__one": "{{count}} dashboard",
"dashboard__other": "{{count}} dashboards",
"folder__one": "{{count}} folder",
"folder__other": "{{count}} folders",
"libraryPanel__one": "{{count}} library panel",
"libraryPanel__other": "{{count}} library panels",
"total__one": "{{count}} item",
"total__other": "{{count}} items"
},
"dashboards-tree": {
"name-column": "Name",
"tags-column": "Tags"

View File

@ -1,5 +1,5 @@
{
"_comment": "Do not manually edit this file, or update these source phrases in Crowdin. The source of truth for English strings are in the code source",
"_comment": "",
"access-control": {
"add-permission": {
"role-label": "",
@ -39,6 +39,23 @@
"moving": "",
"new-folder-name-required-phrase": ""
},
"counts": {
"alertRule__one": "",
"alertRule__many": "",
"alertRule__other": "",
"dashboard__one": "",
"dashboard__many": "",
"dashboard__other": "",
"folder__one": "",
"folder__many": "",
"folder__other": "",
"libraryPanel__one": "",
"libraryPanel__many": "",
"libraryPanel__other": "",
"total__one": "",
"total__many": "",
"total__other": ""
},
"dashboards-tree": {
"name-column": "",
"tags-column": ""

View File

@ -1,5 +1,5 @@
{
"_comment": "Do not manually edit this file. Translations must be made in Crowdin which will sync them back into this file",
"_comment": "",
"access-control": {
"add-permission": {
"role-label": "",
@ -39,6 +39,23 @@
"moving": "",
"new-folder-name-required-phrase": ""
},
"counts": {
"alertRule__one": "",
"alertRule__many": "",
"alertRule__other": "",
"dashboard__one": "",
"dashboard__many": "",
"dashboard__other": "",
"folder__one": "",
"folder__many": "",
"folder__other": "",
"libraryPanel__one": "",
"libraryPanel__many": "",
"libraryPanel__other": "",
"total__one": "",
"total__many": "",
"total__other": ""
},
"dashboards-tree": {
"name-column": "",
"tags-column": ""

View File

@ -1,5 +1,5 @@
{
"_comment": "Đő ʼnőŧ mäʼnūäľľy ęđįŧ ŧĥįş ƒįľę, őř ūpđäŧę ŧĥęşę şőūřčę pĥřäşęş įʼn Cřőŵđįʼn. Ŧĥę şőūřčę őƒ ŧřūŧĥ ƒőř Ēʼnģľįşĥ şŧřįʼnģş äřę įʼn ŧĥę čőđę şőūřčę",
"_comment": "Ŧĥįş ƒįľę įş ŧĥę şőūřčę őƒ ŧřūŧĥ ƒőř Ēʼnģľįşĥ şŧřįʼnģş. Ēđįŧ ŧĥįş ŧő čĥäʼnģę pľūřäľş äʼnđ őŧĥęř pĥřäşęş ƒőř ŧĥę ŮĨ.",
"access-control": {
"add-permission": {
"role-label": "Ŗőľę",
@ -39,6 +39,18 @@
"moving": "Mővįʼnģ...",
"new-folder-name-required-phrase": "Főľđęř ʼnämę įş řęqūįřęđ."
},
"counts": {
"alertRule__one": "{{count}} äľęřŧ řūľę",
"alertRule__other": "{{count}} äľęřŧ řūľęş",
"dashboard__one": "{{count}} đäşĥþőäřđ",
"dashboard__other": "{{count}} đäşĥþőäřđş",
"folder__one": "{{count}} ƒőľđęř",
"folder__other": "{{count}} ƒőľđęřş",
"libraryPanel__one": "{{count}} ľįþřäřy päʼnęľ",
"libraryPanel__other": "{{count}} ľįþřäřy päʼnęľş",
"total__one": "{{count}} įŧęm",
"total__other": "{{count}} įŧęmş"
},
"dashboards-tree": {
"name-column": "Ńämę",
"tags-column": "Ŧäģş"

View File

@ -1,5 +1,5 @@
{
"_comment": "Do not manually edit this file. Translations must be made in Crowdin which will sync them back into this file",
"_comment": "",
"access-control": {
"add-permission": {
"role-label": "",
@ -39,6 +39,13 @@
"moving": "",
"new-folder-name-required-phrase": ""
},
"counts": {
"alertRule__other": "",
"dashboard__other": "",
"folder__other": "",
"libraryPanel__other": "",
"total__other": ""
},
"dashboards-tree": {
"name-column": "",
"tags-column": ""