i18n: Add locale to SharedPreferences (#103133)
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user