mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
17d8fca289
commit
2650aa5600
@ -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
|
||||
|
||||
|
@ -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": {
|
||||
|
@ -24,7 +24,7 @@ export const LANGUAGES: LanguageDefinitions[] = [
|
||||
{
|
||||
code: ENGLISH_US,
|
||||
name: 'English',
|
||||
loader: () => Promise.resolve({}),
|
||||
loader: () => import('../../../locales/en-US/grafana.json'),
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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 }) => {
|
||||
|
@ -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(', ')}`;
|
||||
}
|
||||
|
@ -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": ""
|
||||
|
@ -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"
|
||||
|
@ -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": ""
|
||||
|
@ -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": ""
|
||||
|
@ -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": "Ŧäģş"
|
||||
|
@ -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": ""
|
||||
|
Loading…
Reference in New Issue
Block a user