i18n: Add `locale` to SharedPreferences (#103133)

pull/103734/head
Laura Fernández 1 month ago committed by GitHub
parent 5e923bbab8
commit 4849f87e82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 31
      e2e/various-suite/verify-i18n.spec.ts
  2. 94
      public/app/core/components/SharedPreferences/SharedPreferences.tsx
  3. 146
      public/app/core/internationalization/locales.ts
  4. 7
      public/locales/en-US/grafana.json

@ -38,22 +38,33 @@ describe('Verify i18n', () => {
});
// map between languages in the language picker and the corresponding translation of the 'Language' label
const languageMap: Record<string, string> = {
Deutsch: 'Sprache',
English: 'Language',
Español: 'Idioma',
Français: 'Langue',
'Português Brasileiro': 'Idioma',
'中文(简体)': '语言',
// const languageMap: Record<string, string> = {
// Deutsch: 'Sprache',
// English: 'Language',
// Español: 'Idioma',
// Français: 'Langue',
// 'Português Brasileiro': 'Idioma',
// '中文(简体)': '语言',
// };
// map between languages in the weekstart picker and the corresponding translation of the 'Week Start' label
const weekStartMap: Record<string, string> = {
Deutsch: 'Wochenbeginn',
English: 'Week start',
Español: 'Inicio de la semana',
Français: 'Début de la semaine',
'Português Brasileiro': 'Início da semana',
'中文(简体)': '每周开始日',
};
// basic test which loops through the defined languages in the picker
// and verifies that the corresponding label is translated correctly
it('loads all the languages correctly', () => {
cy.visit('/profile');
const LANGUAGE_SELECTOR = '[id="locale-select"]';
cy.wrap(Object.entries(languageMap)).each(([language, label]: [string, string]) => {
const LANGUAGE_SELECTOR = '[id="language-preference-select"]';
//TODO ckeck translations using language label when its translations get updated
// Checking the Week start label instead
cy.wrap(Object.entries(weekStartMap)).each(([language, label]: [string, string]) => {
cy.get(LANGUAGE_SELECTOR).should('not.be.disabled');
cy.get(LANGUAGE_SELECTOR).click();
cy.get(LANGUAGE_SELECTOR).clear().type(language).type('{downArrow}{enter}');

@ -24,6 +24,7 @@ import {
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
import { t, Trans } from 'app/core/internationalization';
import { LANGUAGES, PSEUDO_LOCALE } from 'app/core/internationalization/constants';
import { LOCALES } from 'app/core/internationalization/locales';
import { PreferencesService } from 'app/core/services/PreferencesService';
import { changeTheme } from 'app/core/services/theme';
@ -39,7 +40,6 @@ export interface Props {
export type State = UserPreferencesDTO & {
isLoading: boolean;
};
function getLanguageOptions(): ComboboxOption[] {
const languageOptions = LANGUAGES.map((v) => ({
value: v.code,
@ -67,9 +67,29 @@ function getLanguageOptions(): ComboboxOption[] {
return options;
}
function getLocaleOptions(): ComboboxOption[] {
const localeOptions = LOCALES.map((v) => ({
value: v.code,
label: v.name,
})).sort((a, b) => {
return a.label.localeCompare(b.label);
});
const options = [
{
value: '',
label: t('common.locale.default', 'Default'),
},
...localeOptions,
];
return options;
}
export class SharedPreferences extends PureComponent<Props, State> {
service: PreferencesService;
themeOptions: ComboboxOption[];
languageOptions: ComboboxOption[];
localeOptions: ComboboxOption[];
constructor(props: Props) {
super(props);
@ -81,17 +101,22 @@ export class SharedPreferences extends PureComponent<Props, State> {
timezone: '',
weekStart: '',
language: '',
locale: '',
queryHistory: { homeTab: '' },
navbar: { bookmarkUrls: [] },
};
const themes = getSelectableThemes();
// Options are translated, so must be called after init but call them
// in constructor to avoid memo-break of array changing every render
this.themeOptions = themes.map((theme) => ({
value: theme.id,
label: getTranslatedThemeName(theme),
group: theme.isExtra ? t('shared-preferences.theme.experimental', 'Experimental') : undefined,
}));
this.languageOptions = getLanguageOptions();
this.localeOptions = getLocaleOptions();
// Add default option
this.themeOptions.unshift({ value: '', label: t('shared-preferences.theme.default-label', 'Default') });
@ -110,6 +135,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
timezone: prefs.timezone,
weekStart: prefs.weekStart,
language: prefs.language,
locale: prefs.locale,
queryHistory: prefs.queryHistory,
navbar: prefs.navbar,
});
@ -120,13 +146,22 @@ export class SharedPreferences extends PureComponent<Props, State> {
const confirmationResult = this.props.onConfirm ? await this.props.onConfirm() : true;
if (confirmationResult) {
const { homeDashboardUID, theme, timezone, weekStart, language, queryHistory, navbar } = this.state;
const { homeDashboardUID, theme, timezone, weekStart, language, locale, queryHistory, navbar } = this.state;
reportInteraction('grafana_preferences_save_button_clicked', {
preferenceType: this.props.preferenceType,
theme,
language,
});
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, language, queryHistory, navbar });
await this.service.update({
homeDashboardUID,
theme,
timezone,
weekStart,
language,
locale,
queryHistory,
navbar,
});
window.location.reload();
}
};
@ -167,11 +202,19 @@ export class SharedPreferences extends PureComponent<Props, State> {
});
};
onLocaleChanged = (locale: string) => {
this.setState({ locale });
reportInteraction('grafana_preferences_locale_changed', {
toLocale: locale,
preferenceType: this.props.preferenceType,
});
};
render() {
const { theme, timezone, weekStart, homeDashboardUID, language, isLoading } = this.state;
const { theme, timezone, weekStart, homeDashboardUID, language, isLoading, locale } = this.state;
const { disabled } = this.props;
const styles = getStyles();
const languages = getLanguageOptions();
const currentThemeOption = this.themeOptions.find((x) => x.value === theme) ?? this.themeOptions[0];
return (
@ -257,23 +300,50 @@ export class SharedPreferences extends PureComponent<Props, State> {
loading={isLoading}
disabled={isLoading}
label={
<Label htmlFor="locale-select">
<Label htmlFor="language-preference-select">
<span className={styles.labelText}>
<Trans i18nKey="shared-preferences.fields.locale-label">Language</Trans>
<Trans i18nKey="shared-preferences.fields.language-preference-label">Language</Trans>
</span>
<FeatureBadge featureState={FeatureState.beta} />
<FeatureBadge featureState={FeatureState.preview} />
</Label>
}
data-testid="User preferences language drop down"
>
<Combobox
value={languages.find((lang) => lang.value === language)?.value || ''}
value={this.languageOptions.find((lang) => lang.value === language)?.value || ''}
onChange={(lang: ComboboxOption | null) => this.onLanguageChanged(lang?.value ?? '')}
options={languages}
placeholder={t('shared-preferences.fields.locale-placeholder', 'Choose language')}
id="locale-select"
options={this.languageOptions}
placeholder={t('shared-preferences.fields.language-preference-placeholder', 'Choose language')}
id="language-preference-select"
/>
</Field>
{config.featureToggles.localeFormatPreference && (
<Field
loading={isLoading}
disabled={isLoading}
label={
<Label htmlFor="locale-preference">
<span className={styles.labelText}>
<Trans i18nKey="shared-preferences.fields.locale-preference-label">Region format</Trans>
</span>
<FeatureBadge featureState={FeatureState.experimental} />
</Label>
}
description={t(
'shared-preferences.fields.locale-preference-description',
'Choose your region to see the corresponding date, time, and number format'
)}
data-testid="User preferences locale drop down"
>
<Combobox
value={this.localeOptions.find((loc) => loc.value === locale)?.value || ''}
onChange={(locale: ComboboxOption | null) => this.onLocaleChanged(locale?.value ?? '')}
options={this.localeOptions}
placeholder={t('shared-preferences.fields.locale-preference-placeholder', 'Choose region')}
id="locale-preference-select"
/>
</Field>
)}
</FieldSet>
<Button type="submit" variant="primary" data-testid={selectors.components.UserProfile.preferencesSaveButton}>
<Trans i18nKey="common.save">Save</Trans>

@ -0,0 +1,146 @@
// List hard-coded locales from https://github.com/moment/moment/tree/develop/locale
interface Locale {
name: string;
code: string;
}
// TODO re-check translations
export const LOCALES: Locale[] = [
{ name: 'Afrikaans', code: 'af' },
{ name: 'العربية', code: 'ar' },
{ name: 'العربية (الجزائر)', code: 'ar-dz' },
{ name: 'العربية (الكويت)', code: 'ar-kw' },
{ name: 'العربية (ليبيا)', code: 'ar-ly' },
{ name: 'العربية (المغرب)', code: 'ar-ma' },
{ name: 'العربية (فلسطين)', code: 'ar-ps' },
{ name: 'العربية (السعودية)', code: 'ar-sa' },
{ name: 'العربية (تونس)', code: 'ar-tn' },
{ name: 'Azərbaycanca', code: 'az' },
{ name: 'Беларуская', code: 'be' },
{ name: 'български език', code: 'bg' },
{ name: 'Bamanankan', code: 'bm' },
{ name: 'Bengali', code: 'bn' }, // TODO translate : ব ???
{ name: 'Bengali (Bangladesh)', code: 'bn-bd' }, // TODO translate
{ name: 'Tibetan', code: 'bo' }, // TODO translate
{ name: 'Brezhoneg', code: 'br' },
{ name: 'Босански', code: 'bs' },
{ name: 'Catalán', code: 'ca' },
{ name: 'Čeština', code: 'cs' },
{ name: 'Cymraeg', code: 'cy' },
{ name: 'Чӑвашла', code: 'cv' },
{ name: 'Dansk', code: 'da' },
{ name: 'Deutsch', code: 'de' },
{ name: 'Deutsch (Österreich)', code: 'de-at' },
{ name: 'Deutsch (Schweiz)', code: 'de-ch' },
{ name: ިވެހި', code: 'dv' },
{ name: 'Ελληνικά', code: 'el' },
{ name: 'English (Australia)', code: 'en-au' },
{ name: 'English (Canada)', code: 'en-ca' },
{ name: 'English (United Kingdom)', code: 'en-gb' },
{ name: 'English (Ireland)', code: 'en-ie' },
{ name: 'English (Israel)', code: 'en-il' },
{ name: 'English (India)', code: 'en-in' },
{ name: 'English (New Zealand)', code: 'en-nz' },
{ name: 'English (Singapore)', code: 'en-sg' },
{ name: 'English (United States)', code: 'en' },
{ name: 'Esperanto', code: 'eo' },
{ name: 'Español', code: 'es' },
{ name: 'Español (República Dominicana)', code: 'es-do' },
{ name: 'Español (México)', code: 'es-mx' },
{ name: 'Español (Estados Unidos)', code: 'es-us' },
{ name: 'Eesti keel', code: 'et' },
{ name: 'Euskara', code: 'eu' },
{ name: 'فارسی', code: 'fa' },
{ name: 'Filipino', code: 'fil' },
{ name: 'Suomi', code: 'fi' },
{ name: 'Føroyskt', code: 'fo' },
{ name: 'Français', code: 'fr' },
{ name: 'Français (Canada)', code: 'fr-ca' },
{ name: 'Français (Suisse)', code: 'fr-ch' },
{ name: 'Frisian', code: 'fy' }, // TODO translate
{ name: 'Gaeilge', code: 'ga' },
{ name: 'Gàidhlig', code: 'gd' },
{ name: 'Galego', code: 'gl' },
{ name: 'Konkani Devanagari', code: 'gom-deva' }, // TODO translate
{ name: 'Konkani Latin', code: 'gom-latn' }, // TODO translate
{ name: 'ગજર', code: 'gu' },
{ name: 'עברית', code: 'he' },
{ name: 'हि', code: 'hi' },
{ name: 'Hrvatski', code: 'hr' },
{ name: 'Magyar', code: 'hu' },
{ name: 'Հայերեն', code: 'hy-am' },
{ name: 'Bahasa Indonesia', code: 'id' },
{ name: 'Íslenska', code: 'is' },
{ name: 'Italiano', code: 'it' },
{ name: 'Italiano (Switzerland)', code: 'it-ch' },
{ name: '日本語', code: 'ja' },
{ name: 'ꦧꦱꦗꦮ', code: 'jv' },
{ name: 'ქართული', code: 'ka' },
{ name: 'Қазақ Tілі', code: 'kk' },
{ name: 'Cambodian', code: 'km' }, // TODO translate
{ name: 'ಕನನಡ', code: 'kn' },
{ name: '한국어', code: 'ko' },
{ name: 'Kurdish', code: 'ku' }, // TODO translate
{ name: 'Northern Kurdish', code: 'ku' }, // TODO translate
{ name: 'Кыргыз тили', code: 'ky' },
{ name: 'Lëtzebuergesch', code: 'lb' },
{ name: 'ພາສາລາວ', code: 'lo' },
{ name: 'Lietuvių', code: 'lt' },
{ name: 'latviešu', code: 'lv' },
{ name: 'Mакедонски', code: 'mk' },
{ name: 'മലയ', code: 'ml' },
{ name: 'te Reo Māori', code: 'mi' },
{ name: 'crnogorski', code: 'me' },
{ name: 'मर', code: 'mr' },
{ name: 'Bahasa Melayu', code: 'ms' },
{ name: 'Malti', code: 'mt' },
{ name: 'Монгол Хэл', code: 'mn' },
{ name: 'Burmese', code: 'my' }, // TODO trasnlate: မ ??
{ name: 'Norwegian Bokmål', code: 'nb' }, // TODO translate
{ name: 'न', code: 'ne' },
{ name: 'Nederlands', code: 'nl' },
{ name: 'Nederlands (België)', code: 'nl-be' },
{ name: 'Ninorks', code: 'nn' }, //??
{ name: 'Occitan (Lengadocian)', code: 'oc-lnc' },
{ name: 'प (ਭਰਤ)', code: 'pa-in' },
{ name: 'Polski', code: 'pl' },
{ name: 'Português', code: 'pt' },
{ name: 'Português (Brasil)', code: 'pt-br' },
{ name: 'Română', code: 'ro' },
{ name: 'Русский', code: 'ru' },
{ name: 'Nothern Sami', code: 'se' }, // TODO translate
{ name: 'سنڌي', code: 'sd' },
{ name: 'සහල', code: 'si' },
{ name: 'Slovenčina', code: 'sk' },
{ name: 'Slovenščina', code: 'sl' },
{ name: 'Shqip', code: 'sq' },
{ name: 'Српски', code: 'sr' },
{ name: 'Serbian Cyrillic', code: 'sr-cyrl' }, // TODO translate
{ name: 'siSwati', code: 'ss' },
{ name: 'Kiswahili', code: 'sw' },
{ name: 'Svenska', code: 'sv' },
{ name: 'தமி', code: 'ta' },
{ name: 'త', code: 'te' },
{ name: 'Lia-Tetun', code: 'tet' },
{ name: 'Тоҷикӣ', code: 'tg' },
{ name: 'ภาษาไทย', code: 'th' },
{ name: 'Türkmençe', code: 'tk' },
{ name: 'Tagalog (Philippines)', code: 'tl-ph' }, // TODO translate
{ name: 'tlhIngan Hol', code: 'tlh' },
{ name: 'Türkçe', code: 'tr' },
{ name: 'Talossan', code: 'tzl' }, // TODO translate
{ name: 'أمازيغية أطلس الأوسط', code: 'tzm' },
{ name: 'Central Atlas Tamazight Latin', code: 'tzm-latn' }, // TODO translate
{ name: 'ئۇيغۇر تىلى', code: 'ug-cn' },
{ name: 'Українська', code: 'uk' },
{ name: ُردُو', code: 'ur' },
{ name: 'Ўзбек', code: 'uz' },
{ name: 'Uzbek (Latin)', code: 'uz-latn' }, // TODO translate
{ name: 'tiếng Việt', code: 'vi' },
{ name: 'Chinese (China)', code: 'zh-cn' }, // TODO translate
{ name: 'Chinese (Hong Kong)', code: 'zh-hk' }, // TODO translate
{ name: 'Chinese (Macau)', code: 'zh-mo' }, // TODO translate
{ name: 'Chinese (Taiwan)', code: 'zh-tw' }, // TODO translate
{ name: 'Èdè Yorùbá', code: 'yo-ng' },
];

@ -7427,8 +7427,11 @@
"fields": {
"home-dashboard-label": "Home Dashboard",
"home-dashboard-placeholder": "Default dashboard",
"locale-label": "Language",
"locale-placeholder": "Choose language",
"language-preference-label": "Language",
"language-preference-placeholder": "Choose language",
"locale-preference-description": "Choose your region to see the corresponding date, time, and number format",
"locale-preference-label": "Region format",
"locale-preference-placeholder": "Choose region",
"theme-description": "Enjoying the experimental themes? Tell us what you'd like to see <2>here.</2>",
"theme-label": "Interface theme",
"week-start-label": "Week start"

Loading…
Cancel
Save