mirror of
https://github.com/grafana/grafana.git
synced 2024-12-27 09:21:35 -06:00
I18n: Show languages in local names (#57367)
* I18n: Show languages in local names * fixed test
This commit is contained in:
parent
5c710a5590
commit
fc75076b72
@ -68,11 +68,9 @@ While the `t` function can technically be used outside of React functions (e.g,
|
||||
1. Grafana OSS Crowdin project -> "dot dot dot" menu in top right -> Target languages
|
||||
2. Grafana OSS Crowdin project -> Integrations -> Github -> Sync Now
|
||||
3. If Crowdin's locale code is different from our IETF language tag, add a custom mapping in Project Settings -> Language mapping
|
||||
2. Update `public/app/core/internationalization/constants.ts` (add new constant, and add to `VALID_LOCALES`)
|
||||
3. Update `public/app/core/internationalization/index.tsx` to add the message loader for the new locale
|
||||
4. Update `public/app/core/components/SharedPreferences/SharedPreferences.tsx` to add the new locale to the options.
|
||||
5. Update `public/locales/i18next-parser.config.js` to add the new locale to `locales`
|
||||
6. Run `yarn i18n:extract` and commit the result
|
||||
2. Update `public/app/core/internationalization/constants.ts` (add new constant, and add to `LOCALES`)
|
||||
3. Update `public/locales/i18next-parser.config.js` to add the new locale to `locales`
|
||||
4. Run `yarn i18n:extract` and commit the result
|
||||
|
||||
## How translations work in Grafana
|
||||
|
||||
|
@ -163,7 +163,7 @@ describe('SharedPreferences', () => {
|
||||
|
||||
await selectOptionInTest(screen.getByLabelText('Timezone'), 'Australia/Sydney');
|
||||
await selectOptionInTest(screen.getByLabelText('Week start'), 'Saturday');
|
||||
await selectOptionInTest(screen.getByLabelText(/language/i), 'French');
|
||||
await selectOptionInTest(screen.getByLabelText(/language/i), 'Français');
|
||||
|
||||
await userEvent.click(screen.getByText('Save'));
|
||||
expect(mockPrefsUpdate).toHaveBeenCalledWith({
|
||||
|
@ -19,13 +19,7 @@ import {
|
||||
} from '@grafana/ui';
|
||||
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import {
|
||||
CHINESE_SIMPLIFIED,
|
||||
ENGLISH_US,
|
||||
FRENCH_FRANCE,
|
||||
PSEUDO_LOCALE,
|
||||
SPANISH_SPAIN,
|
||||
} from 'app/core/internationalization/constants';
|
||||
import { LOCALES } from 'app/core/internationalization/constants';
|
||||
import { PreferencesService } from 'app/core/services/PreferencesService';
|
||||
import { UserPreferencesDTO } from 'app/types';
|
||||
|
||||
@ -43,36 +37,19 @@ const themes: SelectableValue[] = [
|
||||
];
|
||||
|
||||
function getLanguageOptions(): Array<SelectableValue<string>> {
|
||||
const languageOptions = LOCALES.map((v) => ({
|
||||
value: v.code,
|
||||
label: v.name,
|
||||
}));
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: '',
|
||||
label: t('common.locale.default', 'Default'),
|
||||
},
|
||||
{
|
||||
value: ENGLISH_US,
|
||||
label: t('common.locale.en-US', 'English'),
|
||||
},
|
||||
{
|
||||
value: SPANISH_SPAIN,
|
||||
label: t('common.locale.es-ES', 'Spanish'),
|
||||
},
|
||||
{
|
||||
value: FRENCH_FRANCE,
|
||||
label: t('common.locale.fr-FR', 'French'),
|
||||
},
|
||||
{
|
||||
value: CHINESE_SIMPLIFIED,
|
||||
label: t('common.locale.zh-Hans', 'Chinese (Simplified)'),
|
||||
},
|
||||
...languageOptions,
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
options.push({
|
||||
value: PSEUDO_LOCALE,
|
||||
label: 'Pseudo-locale', // no need to translate this key
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
14
public/app/core/internationalization/constants.test.ts
Normal file
14
public/app/core/internationalization/constants.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import { LOCALES, VALID_LOCALES } from './constants';
|
||||
|
||||
describe('internationalization constants', () => {
|
||||
it('should not have duplicate languages codes', () => {
|
||||
const uniqLocales = uniqBy(LOCALES, (v) => v.code);
|
||||
expect(LOCALES).toHaveLength(uniqLocales.length);
|
||||
});
|
||||
|
||||
it('should have a correct list of valid locale codes', () => {
|
||||
expect(VALID_LOCALES).toEqual(LOCALES.map((v) => v.code));
|
||||
});
|
||||
});
|
@ -1,3 +1,5 @@
|
||||
import { ResourceKey } from 'i18next';
|
||||
|
||||
export const ENGLISH_US = 'en-US';
|
||||
export const FRENCH_FRANCE = 'fr-FR';
|
||||
export const SPANISH_SPAIN = 'es-ES';
|
||||
@ -6,4 +8,49 @@ export const PSEUDO_LOCALE = 'pseudo-LOCALE';
|
||||
|
||||
export const DEFAULT_LOCALE = ENGLISH_US;
|
||||
|
||||
export const VALID_LOCALES: string[] = [ENGLISH_US, FRENCH_FRANCE, SPANISH_SPAIN, CHINESE_SIMPLIFIED, PSEUDO_LOCALE];
|
||||
interface LocaleDefinition {
|
||||
/** IETF language tag for the language e.g. en-US */
|
||||
code: string;
|
||||
|
||||
/** Language name to show in the UI. Should be formatted local to that language e.g. Français for French */
|
||||
name: string;
|
||||
|
||||
/** Function to load translations */
|
||||
loader: () => Promise<ResourceKey>;
|
||||
}
|
||||
|
||||
export const LOCALES: LocaleDefinition[] = [
|
||||
{
|
||||
code: ENGLISH_US,
|
||||
name: 'English',
|
||||
loader: () => Promise.resolve({}),
|
||||
},
|
||||
|
||||
{
|
||||
code: FRENCH_FRANCE,
|
||||
name: 'Français',
|
||||
loader: () => import('../../../locales/fr-FR/grafana.json'),
|
||||
},
|
||||
|
||||
{
|
||||
code: SPANISH_SPAIN,
|
||||
name: 'Español',
|
||||
loader: () => import('../../../locales/es-ES/grafana.json'),
|
||||
},
|
||||
|
||||
{
|
||||
code: CHINESE_SIMPLIFIED,
|
||||
name: '中文(简体)',
|
||||
loader: () => import('../../../locales/zh-Hans/grafana.json'),
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
LOCALES.push({
|
||||
code: PSEUDO_LOCALE,
|
||||
name: 'Pseudo-locale',
|
||||
loader: () => import('../../../locales/pseudo-LOCALE/grafana.json'),
|
||||
});
|
||||
}
|
||||
|
||||
export const VALID_LOCALES = LOCALES.map((v) => v.code);
|
||||
|
@ -1,36 +1,20 @@
|
||||
import i18n, { BackendModule, ResourceKey } from 'i18next';
|
||||
import i18n, { BackendModule } from 'i18next';
|
||||
import React from 'react';
|
||||
import { Trans as I18NextTrans, initReactI18next } from 'react-i18next'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
ENGLISH_US,
|
||||
FRENCH_FRANCE,
|
||||
SPANISH_SPAIN,
|
||||
PSEUDO_LOCALE,
|
||||
VALID_LOCALES,
|
||||
CHINESE_SIMPLIFIED,
|
||||
} from './constants';
|
||||
|
||||
const messageLoaders: Record<string, () => Promise<ResourceKey>> = {
|
||||
// English phrases are the default fallback string in the source, so we don't need to load the catalogue
|
||||
[ENGLISH_US]: () => Promise.resolve({}),
|
||||
[FRENCH_FRANCE]: () => import('../../../locales/fr-FR/grafana.json'),
|
||||
[SPANISH_SPAIN]: () => import('../../../locales/es-ES/grafana.json'),
|
||||
[CHINESE_SIMPLIFIED]: () => import('../../../locales/zh-Hans/grafana.json'),
|
||||
[PSEUDO_LOCALE]: () => import('../../../locales/pseudo-LOCALE/grafana.json'),
|
||||
};
|
||||
import { DEFAULT_LOCALE, LOCALES, VALID_LOCALES } from './constants';
|
||||
|
||||
const loadTranslations: BackendModule = {
|
||||
type: 'backend',
|
||||
init() {},
|
||||
async read(language, namespace, callback) {
|
||||
const loader = messageLoaders[language];
|
||||
if (!loader) {
|
||||
const localeDef = LOCALES.find((v) => v.code === language);
|
||||
|
||||
if (!localeDef) {
|
||||
return callback(new Error('No message loader available for ' + language), null);
|
||||
}
|
||||
|
||||
const messages = await loader();
|
||||
const messages = await localeDef.loader();
|
||||
callback(null, messages);
|
||||
},
|
||||
};
|
||||
|
@ -2,11 +2,7 @@
|
||||
"_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",
|
||||
"common": {
|
||||
"locale": {
|
||||
"default": "Default",
|
||||
"en-US": "English",
|
||||
"es-ES": "Spanish",
|
||||
"fr-FR": "French",
|
||||
"zh-Hans": "Chinese (Simplified)"
|
||||
"default": "Default"
|
||||
},
|
||||
"save": "Save"
|
||||
},
|
||||
|
@ -2,11 +2,7 @@
|
||||
"_comment": "Do not manually edit this file. Translations must be made in Crowdin which will sync them back into this file",
|
||||
"common": {
|
||||
"locale": {
|
||||
"default": "Por defecto",
|
||||
"en-US": "Inglés",
|
||||
"es-ES": "Español",
|
||||
"fr-FR": "Francés",
|
||||
"zh-Hans": "Chino (simplificado)"
|
||||
"default": "Por defecto"
|
||||
},
|
||||
"save": "Guardar"
|
||||
},
|
||||
|
@ -2,11 +2,7 @@
|
||||
"_comment": "Do not manually edit this file. Translations must be made in Crowdin which will sync them back into this file",
|
||||
"common": {
|
||||
"locale": {
|
||||
"default": "Par défaut",
|
||||
"en-US": "Anglais",
|
||||
"es-ES": "Espagnol",
|
||||
"fr-FR": "Français",
|
||||
"zh-Hans": "Chinois (simplifié)"
|
||||
"default": "Par défaut"
|
||||
},
|
||||
"save": "Enregistrer"
|
||||
},
|
||||
|
@ -2,11 +2,7 @@
|
||||
"_comment": "Đő ʼnőŧ mäʼnūäľľy ęđįŧ ŧĥįş ƒįľę, őř ūpđäŧę ŧĥęşę şőūřčę pĥřäşęş įʼn Cřőŵđįʼn. Ŧĥę şőūřčę őƒ ŧřūŧĥ ƒőř Ēʼnģľįşĥ şŧřįʼnģş äřę įʼn ŧĥę čőđę şőūřčę",
|
||||
"common": {
|
||||
"locale": {
|
||||
"default": "Đęƒäūľŧ",
|
||||
"en-US": "Ēʼnģľįşĥ",
|
||||
"es-ES": "Ŝpäʼnįşĥ",
|
||||
"fr-FR": "Fřęʼnčĥ",
|
||||
"zh-Hans": "Cĥįʼnęşę (Ŝįmpľįƒįęđ)"
|
||||
"default": "Đęƒäūľŧ"
|
||||
},
|
||||
"save": "Ŝävę"
|
||||
},
|
||||
|
@ -2,11 +2,7 @@
|
||||
"_comment": "Do not manually edit this file. Translations must be made in Crowdin which will sync them back into this file",
|
||||
"common": {
|
||||
"locale": {
|
||||
"default": "默认",
|
||||
"en-US": "英语",
|
||||
"es-ES": "西班牙语",
|
||||
"fr-FR": "法语",
|
||||
"zh-Hans": "中文(简体)"
|
||||
"default": "默认"
|
||||
},
|
||||
"save": "保存"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user