Skip to content

Commit 4849f87

Browse files
authored
i18n: Add locale to SharedPreferences (#103133)
1 parent 5e923bb commit 4849f87

File tree

4 files changed

+254
-24
lines changed

4 files changed

+254
-24
lines changed

e2e/various-suite/verify-i18n.spec.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,33 @@ describe('Verify i18n', () => {
3838
});
3939

4040
// map between languages in the language picker and the corresponding translation of the 'Language' label
41-
const languageMap: Record<string, string> = {
42-
Deutsch: 'Sprache',
43-
English: 'Language',
44-
Español: 'Idioma',
45-
Français: 'Langue',
46-
'Português Brasileiro': 'Idioma',
47-
'中文(简体)': '语言',
41+
// const languageMap: Record<string, string> = {
42+
// Deutsch: 'Sprache',
43+
// English: 'Language',
44+
// Español: 'Idioma',
45+
// Français: 'Langue',
46+
// 'Português Brasileiro': 'Idioma',
47+
// '中文(简体)': '语言',
48+
// };
49+
50+
// map between languages in the weekstart picker and the corresponding translation of the 'Week Start' label
51+
const weekStartMap: Record<string, string> = {
52+
Deutsch: 'Wochenbeginn',
53+
English: 'Week start',
54+
Español: 'Inicio de la semana',
55+
Français: 'Début de la semaine',
56+
'Português Brasileiro': 'Início da semana',
57+
'中文(简体)': '每周开始日',
4858
};
4959

5060
// basic test which loops through the defined languages in the picker
5161
// and verifies that the corresponding label is translated correctly
5262
it('loads all the languages correctly', () => {
5363
cy.visit('/profile');
54-
const LANGUAGE_SELECTOR = '[id="locale-select"]';
55-
56-
cy.wrap(Object.entries(languageMap)).each(([language, label]: [string, string]) => {
64+
const LANGUAGE_SELECTOR = '[id="language-preference-select"]';
65+
//TODO ckeck translations using language label when its translations get updated
66+
// Checking the Week start label instead
67+
cy.wrap(Object.entries(weekStartMap)).each(([language, label]: [string, string]) => {
5768
cy.get(LANGUAGE_SELECTOR).should('not.be.disabled');
5869
cy.get(LANGUAGE_SELECTOR).click();
5970
cy.get(LANGUAGE_SELECTOR).clear().type(language).type('{downArrow}{enter}');

public/app/core/components/SharedPreferences/SharedPreferences.tsx

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
2525
import { t, Trans } from 'app/core/internationalization';
2626
import { LANGUAGES, PSEUDO_LOCALE } from 'app/core/internationalization/constants';
27+
import { LOCALES } from 'app/core/internationalization/locales';
2728
import { PreferencesService } from 'app/core/services/PreferencesService';
2829
import { changeTheme } from 'app/core/services/theme';
2930

@@ -39,7 +40,6 @@ export interface Props {
3940
export type State = UserPreferencesDTO & {
4041
isLoading: boolean;
4142
};
42-
4343
function getLanguageOptions(): ComboboxOption[] {
4444
const languageOptions = LANGUAGES.map((v) => ({
4545
value: v.code,
@@ -67,9 +67,29 @@ function getLanguageOptions(): ComboboxOption[] {
6767
return options;
6868
}
6969

70+
function getLocaleOptions(): ComboboxOption[] {
71+
const localeOptions = LOCALES.map((v) => ({
72+
value: v.code,
73+
label: v.name,
74+
})).sort((a, b) => {
75+
return a.label.localeCompare(b.label);
76+
});
77+
78+
const options = [
79+
{
80+
value: '',
81+
label: t('common.locale.default', 'Default'),
82+
},
83+
...localeOptions,
84+
];
85+
return options;
86+
}
87+
7088
export class SharedPreferences extends PureComponent<Props, State> {
7189
service: PreferencesService;
7290
themeOptions: ComboboxOption[];
91+
languageOptions: ComboboxOption[];
92+
localeOptions: ComboboxOption[];
7393

7494
constructor(props: Props) {
7595
super(props);
@@ -81,17 +101,22 @@ export class SharedPreferences extends PureComponent<Props, State> {
81101
timezone: '',
82102
weekStart: '',
83103
language: '',
104+
locale: '',
84105
queryHistory: { homeTab: '' },
85106
navbar: { bookmarkUrls: [] },
86107
};
87108

88109
const themes = getSelectableThemes();
89110

111+
// Options are translated, so must be called after init but call them
112+
// in constructor to avoid memo-break of array changing every render
90113
this.themeOptions = themes.map((theme) => ({
91114
value: theme.id,
92115
label: getTranslatedThemeName(theme),
93116
group: theme.isExtra ? t('shared-preferences.theme.experimental', 'Experimental') : undefined,
94117
}));
118+
this.languageOptions = getLanguageOptions();
119+
this.localeOptions = getLocaleOptions();
95120

96121
// Add default option
97122
this.themeOptions.unshift({ value: '', label: t('shared-preferences.theme.default-label', 'Default') });
@@ -110,6 +135,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
110135
timezone: prefs.timezone,
111136
weekStart: prefs.weekStart,
112137
language: prefs.language,
138+
locale: prefs.locale,
113139
queryHistory: prefs.queryHistory,
114140
navbar: prefs.navbar,
115141
});
@@ -120,13 +146,22 @@ export class SharedPreferences extends PureComponent<Props, State> {
120146
const confirmationResult = this.props.onConfirm ? await this.props.onConfirm() : true;
121147

122148
if (confirmationResult) {
123-
const { homeDashboardUID, theme, timezone, weekStart, language, queryHistory, navbar } = this.state;
149+
const { homeDashboardUID, theme, timezone, weekStart, language, locale, queryHistory, navbar } = this.state;
124150
reportInteraction('grafana_preferences_save_button_clicked', {
125151
preferenceType: this.props.preferenceType,
126152
theme,
127153
language,
128154
});
129-
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, language, queryHistory, navbar });
155+
await this.service.update({
156+
homeDashboardUID,
157+
theme,
158+
timezone,
159+
weekStart,
160+
language,
161+
locale,
162+
queryHistory,
163+
navbar,
164+
});
130165
window.location.reload();
131166
}
132167
};
@@ -167,11 +202,19 @@ export class SharedPreferences extends PureComponent<Props, State> {
167202
});
168203
};
169204

205+
onLocaleChanged = (locale: string) => {
206+
this.setState({ locale });
207+
208+
reportInteraction('grafana_preferences_locale_changed', {
209+
toLocale: locale,
210+
preferenceType: this.props.preferenceType,
211+
});
212+
};
213+
170214
render() {
171-
const { theme, timezone, weekStart, homeDashboardUID, language, isLoading } = this.state;
215+
const { theme, timezone, weekStart, homeDashboardUID, language, isLoading, locale } = this.state;
172216
const { disabled } = this.props;
173217
const styles = getStyles();
174-
const languages = getLanguageOptions();
175218
const currentThemeOption = this.themeOptions.find((x) => x.value === theme) ?? this.themeOptions[0];
176219

177220
return (
@@ -257,23 +300,50 @@ export class SharedPreferences extends PureComponent<Props, State> {
257300
loading={isLoading}
258301
disabled={isLoading}
259302
label={
260-
<Label htmlFor="locale-select">
303+
<Label htmlFor="language-preference-select">
261304
<span className={styles.labelText}>
262-
<Trans i18nKey="shared-preferences.fields.locale-label">Language</Trans>
305+
<Trans i18nKey="shared-preferences.fields.language-preference-label">Language</Trans>
263306
</span>
264-
<FeatureBadge featureState={FeatureState.beta} />
307+
<FeatureBadge featureState={FeatureState.preview} />
265308
</Label>
266309
}
267310
data-testid="User preferences language drop down"
268311
>
269312
<Combobox
270-
value={languages.find((lang) => lang.value === language)?.value || ''}
313+
value={this.languageOptions.find((lang) => lang.value === language)?.value || ''}
271314
onChange={(lang: ComboboxOption | null) => this.onLanguageChanged(lang?.value ?? '')}
272-
options={languages}
273-
placeholder={t('shared-preferences.fields.locale-placeholder', 'Choose language')}
274-
id="locale-select"
315+
options={this.languageOptions}
316+
placeholder={t('shared-preferences.fields.language-preference-placeholder', 'Choose language')}
317+
id="language-preference-select"
275318
/>
276319
</Field>
320+
{config.featureToggles.localeFormatPreference && (
321+
<Field
322+
loading={isLoading}
323+
disabled={isLoading}
324+
label={
325+
<Label htmlFor="locale-preference">
326+
<span className={styles.labelText}>
327+
<Trans i18nKey="shared-preferences.fields.locale-preference-label">Region format</Trans>
328+
</span>
329+
<FeatureBadge featureState={FeatureState.experimental} />
330+
</Label>
331+
}
332+
description={t(
333+
'shared-preferences.fields.locale-preference-description',
334+
'Choose your region to see the corresponding date, time, and number format'
335+
)}
336+
data-testid="User preferences locale drop down"
337+
>
338+
<Combobox
339+
value={this.localeOptions.find((loc) => loc.value === locale)?.value || ''}
340+
onChange={(locale: ComboboxOption | null) => this.onLocaleChanged(locale?.value ?? '')}
341+
options={this.localeOptions}
342+
placeholder={t('shared-preferences.fields.locale-preference-placeholder', 'Choose region')}
343+
id="locale-preference-select"
344+
/>
345+
</Field>
346+
)}
277347
</FieldSet>
278348
<Button type="submit" variant="primary" data-testid={selectors.components.UserProfile.preferencesSaveButton}>
279349
<Trans i18nKey="common.save">Save</Trans>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// List hard-coded locales from https://github.com/moment/moment/tree/develop/locale
2+
3+
interface Locale {
4+
name: string;
5+
code: string;
6+
}
7+
8+
// TODO re-check translations
9+
export const LOCALES: Locale[] = [
10+
{ name: 'Afrikaans', code: 'af' },
11+
{ name: 'العربية', code: 'ar' },
12+
{ name: 'العربية (الجزائر)', code: 'ar-dz' },
13+
{ name: 'العربية (الكويت)', code: 'ar-kw' },
14+
{ name: 'العربية (ليبيا)', code: 'ar-ly' },
15+
{ name: 'العربية (المغرب)', code: 'ar-ma' },
16+
{ name: 'العربية (فلسطين)', code: 'ar-ps' },
17+
{ name: 'العربية (السعودية)', code: 'ar-sa' },
18+
{ name: 'العربية (تونس)', code: 'ar-tn' },
19+
{ name: 'Azərbaycanca', code: 'az' },
20+
{ name: 'Беларуская', code: 'be' },
21+
{ name: 'български език', code: 'bg' },
22+
{ name: 'Bamanankan', code: 'bm' },
23+
{ name: 'Bengali', code: 'bn' }, // TODO translate : বাংলা ???
24+
{ name: 'Bengali (Bangladesh)', code: 'bn-bd' }, // TODO translate
25+
{ name: 'Tibetan', code: 'bo' }, // TODO translate
26+
{ name: 'Brezhoneg', code: 'br' },
27+
{ name: 'Босански', code: 'bs' },
28+
{ name: 'Catalán', code: 'ca' },
29+
{ name: 'Čeština', code: 'cs' },
30+
{ name: 'Cymraeg', code: 'cy' },
31+
{ name: 'Чӑвашла', code: 'cv' },
32+
{ name: 'Dansk', code: 'da' },
33+
{ name: 'Deutsch', code: 'de' },
34+
{ name: 'Deutsch (Österreich)', code: 'de-at' },
35+
{ name: 'Deutsch (Schweiz)', code: 'de-ch' },
36+
{ name: 'ދިވެހި', code: 'dv' },
37+
{ name: 'Ελληνικά', code: 'el' },
38+
{ name: 'English (Australia)', code: 'en-au' },
39+
{ name: 'English (Canada)', code: 'en-ca' },
40+
{ name: 'English (United Kingdom)', code: 'en-gb' },
41+
{ name: 'English (Ireland)', code: 'en-ie' },
42+
{ name: 'English (Israel)', code: 'en-il' },
43+
{ name: 'English (India)', code: 'en-in' },
44+
{ name: 'English (New Zealand)', code: 'en-nz' },
45+
{ name: 'English (Singapore)', code: 'en-sg' },
46+
{ name: 'English (United States)', code: 'en' },
47+
{ name: 'Esperanto', code: 'eo' },
48+
{ name: 'Español', code: 'es' },
49+
{ name: 'Español (República Dominicana)', code: 'es-do' },
50+
{ name: 'Español (México)', code: 'es-mx' },
51+
{ name: 'Español (Estados Unidos)', code: 'es-us' },
52+
{ name: 'Eesti keel', code: 'et' },
53+
{ name: 'Euskara', code: 'eu' },
54+
{ name: 'فارسی', code: 'fa' },
55+
{ name: 'Filipino', code: 'fil' },
56+
{ name: 'Suomi', code: 'fi' },
57+
{ name: 'Føroyskt', code: 'fo' },
58+
{ name: 'Français', code: 'fr' },
59+
{ name: 'Français (Canada)', code: 'fr-ca' },
60+
{ name: 'Français (Suisse)', code: 'fr-ch' },
61+
{ name: 'Frisian', code: 'fy' }, // TODO translate
62+
{ name: 'Gaeilge', code: 'ga' },
63+
{ name: 'Gàidhlig', code: 'gd' },
64+
{ name: 'Galego', code: 'gl' },
65+
{ name: 'Konkani Devanagari', code: 'gom-deva' }, // TODO translate
66+
{ name: 'Konkani Latin', code: 'gom-latn' }, // TODO translate
67+
{ name: 'ગુજરાતી', code: 'gu' },
68+
{ name: 'עברית', code: 'he' },
69+
{ name: 'हिन्दी', code: 'hi' },
70+
{ name: 'Hrvatski', code: 'hr' },
71+
{ name: 'Magyar', code: 'hu' },
72+
{ name: 'Հայերեն', code: 'hy-am' },
73+
{ name: 'Bahasa Indonesia', code: 'id' },
74+
{ name: 'Íslenska', code: 'is' },
75+
{ name: 'Italiano', code: 'it' },
76+
{ name: 'Italiano (Switzerland)', code: 'it-ch' },
77+
{ name: '日本語', code: 'ja' },
78+
{ name: 'ꦧꦱꦗꦮ', code: 'jv' },
79+
{ name: 'ქართული', code: 'ka' },
80+
{ name: 'Қазақ Tілі', code: 'kk' },
81+
{ name: 'Cambodian', code: 'km' }, // TODO translate
82+
{ name: 'ಕನ್ನಡ', code: 'kn' },
83+
{ name: '한국어', code: 'ko' },
84+
{ name: 'Kurdish', code: 'ku' }, // TODO translate
85+
{ name: 'Northern Kurdish', code: 'ku' }, // TODO translate
86+
{ name: 'Кыргыз тили', code: 'ky' },
87+
{ name: 'Lëtzebuergesch', code: 'lb' },
88+
{ name: 'ພາສາລາວ', code: 'lo' },
89+
{ name: 'Lietuvių', code: 'lt' },
90+
{ name: 'latviešu', code: 'lv' },
91+
{ name: 'Mакедонски', code: 'mk' },
92+
{ name: 'മലയാളം', code: 'ml' },
93+
{ name: 'te Reo Māori', code: 'mi' },
94+
{ name: 'crnogorski', code: 'me' },
95+
{ name: 'मराठी', code: 'mr' },
96+
{ name: 'Bahasa Melayu', code: 'ms' },
97+
{ name: 'Malti', code: 'mt' },
98+
{ name: 'Монгол Хэл', code: 'mn' },
99+
{ name: 'Burmese', code: 'my' }, // TODO trasnlate: မြန်မာစာ ??
100+
{ name: 'Norwegian Bokmål', code: 'nb' }, // TODO translate
101+
{ name: 'नेपाली', code: 'ne' },
102+
{ name: 'Nederlands', code: 'nl' },
103+
{ name: 'Nederlands (België)', code: 'nl-be' },
104+
{ name: 'Ninorks', code: 'nn' }, //??
105+
{ name: 'Occitan (Lengadocian)', code: 'oc-lnc' },
106+
{ name: 'पंजाबी (ਭਾਰਤ)', code: 'pa-in' },
107+
{ name: 'Polski', code: 'pl' },
108+
{ name: 'Português', code: 'pt' },
109+
{ name: 'Português (Brasil)', code: 'pt-br' },
110+
{ name: 'Română', code: 'ro' },
111+
{ name: 'Русский', code: 'ru' },
112+
{ name: 'Nothern Sami', code: 'se' }, // TODO translate
113+
{ name: 'سنڌي', code: 'sd' },
114+
{ name: 'සිංහල', code: 'si' },
115+
{ name: 'Slovenčina', code: 'sk' },
116+
{ name: 'Slovenščina', code: 'sl' },
117+
{ name: 'Shqip', code: 'sq' },
118+
{ name: 'Српски', code: 'sr' },
119+
{ name: 'Serbian Cyrillic', code: 'sr-cyrl' }, // TODO translate
120+
{ name: 'siSwati', code: 'ss' },
121+
{ name: 'Kiswahili', code: 'sw' },
122+
{ name: 'Svenska', code: 'sv' },
123+
{ name: 'தமிழ்', code: 'ta' },
124+
{ name: 'తెలుగు', code: 'te' },
125+
{ name: 'Lia-Tetun', code: 'tet' },
126+
{ name: 'Тоҷикӣ', code: 'tg' },
127+
{ name: 'ภาษาไทย', code: 'th' },
128+
{ name: 'Türkmençe', code: 'tk' },
129+
{ name: 'Tagalog (Philippines)', code: 'tl-ph' }, // TODO translate
130+
{ name: 'tlhIngan Hol', code: 'tlh' },
131+
{ name: 'Türkçe', code: 'tr' },
132+
{ name: 'Talossan', code: 'tzl' }, // TODO translate
133+
{ name: 'أمازيغية أطلس الأوسط', code: 'tzm' },
134+
{ name: 'Central Atlas Tamazight Latin', code: 'tzm-latn' }, // TODO translate
135+
{ name: 'ئۇيغۇر تىلى', code: 'ug-cn' },
136+
{ name: 'Українська', code: 'uk' },
137+
{ name: 'اُردُو', code: 'ur' },
138+
{ name: 'Ўзбек', code: 'uz' },
139+
{ name: 'Uzbek (Latin)', code: 'uz-latn' }, // TODO translate
140+
{ name: 'tiếng Việt', code: 'vi' },
141+
{ name: 'Chinese (China)', code: 'zh-cn' }, // TODO translate
142+
{ name: 'Chinese (Hong Kong)', code: 'zh-hk' }, // TODO translate
143+
{ name: 'Chinese (Macau)', code: 'zh-mo' }, // TODO translate
144+
{ name: 'Chinese (Taiwan)', code: 'zh-tw' }, // TODO translate
145+
{ name: 'Èdè Yorùbá', code: 'yo-ng' },
146+
];

public/locales/en-US/grafana.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7427,8 +7427,11 @@
74277427
"fields": {
74287428
"home-dashboard-label": "Home Dashboard",
74297429
"home-dashboard-placeholder": "Default dashboard",
7430-
"locale-label": "Language",
7431-
"locale-placeholder": "Choose language",
7430+
"language-preference-label": "Language",
7431+
"language-preference-placeholder": "Choose language",
7432+
"locale-preference-description": "Choose your region to see the corresponding date, time, and number format",
7433+
"locale-preference-label": "Region format",
7434+
"locale-preference-placeholder": "Choose region",
74327435
"theme-description": "Enjoying the experimental themes? Tell us what you'd like to see <2>here.</2>",
74337436
"theme-label": "Interface theme",
74347437
"week-start-label": "Week start"

0 commit comments

Comments
 (0)