Internationalization: Change locale preference to language (#58359)

* backend locale -> language

* frontend locale -> language

* sample.ini and tests

* fix few last locale -> language

* fix few last locale -> language
This commit is contained in:
Josh Hunt 2022-11-22 12:18:34 +00:00 committed by GitHub
parent 9926931d40
commit 460be70261
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 85 additions and 77 deletions

View File

@ -412,8 +412,8 @@ password_hint = password
# Default UI theme ("dark" or "light")
default_theme = dark
# Default locale (supported IETF language tag, such as en-US)
default_locale = en-US
# Default UI language (supported IETF language tag, such as en-US)
default_language = en-US
# Path to a custom home page. Users are only redirected to this if the default home dashboard is used. It should match a frontend route and contain a leading slash.
home_page =

View File

@ -412,8 +412,8 @@
# Default UI theme ("dark" or "light")
;default_theme = dark
# Default locale (supported IETF language tag, such as en-US)
;default_locale = en-US
# Default UI language (supported IETF language tag, such as en-US)
;default_language = en-US
# Path to a custom home page. Users are only redirected to this if the default home dashboard is used. It should match a frontend route and contain a leading slash.
;home_page =

View File

@ -127,6 +127,7 @@ export interface CurrentUserDTO {
timezone: string;
weekStart: string;
locale: string;
language: string;
permissions?: Record<string, boolean>;
}

View File

@ -44,6 +44,7 @@ type CurrentUser struct {
Timezone string `json:"timezone"`
WeekStart string `json:"weekStart"`
Locale string `json:"locale"`
Language string `json:"language"`
HelpFlags1 user.HelpFlags1 `json:"helpFlags1"`
HasEditPermissionInFolders bool `json:"hasEditPermissionInFolders"`
Permissions UserPermissionsMap `json:"permissions,omitempty"`

View File

@ -10,7 +10,7 @@ type Prefs struct {
HomeDashboardUID string `json:"homeDashboardUID,omitempty"`
Timezone string `json:"timezone"`
WeekStart string `json:"weekStart"`
Locale string `json:"locale"`
Language string `json:"language"`
Navbar pref.NavbarPreference `json:"navbar,omitempty"`
QueryHistory pref.QueryHistoryPreference `json:"queryHistory,omitempty"`
}
@ -28,7 +28,7 @@ type UpdatePrefsCmd struct {
WeekStart string `json:"weekStart"`
Navbar *pref.NavbarPreference `json:"navbar,omitempty"`
QueryHistory *pref.QueryHistoryPreference `json:"queryHistory,omitempty"`
Locale string `json:"locale"`
Language string `json:"language"`
}
// swagger:model
@ -41,7 +41,7 @@ type PatchPrefsCmd struct {
// Enum: utc,browser
Timezone *string `json:"timezone,omitempty"`
WeekStart *string `json:"weekStart,omitempty"`
Locale *string `json:"locale,omitempty"`
Language *string `json:"language,omitempty"`
Navbar *pref.NavbarPreference `json:"navbar,omitempty"`
QueryHistory *pref.QueryHistoryPreference `json:"queryHistory,omitempty"`
HomeDashboardUID *string `json:"homeDashboardUID,omitempty"`

View File

@ -45,15 +45,17 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
return nil, err
}
// Set locale to the preference, otherwise fall back to the accept language header.
// In practice, because the preference has configuration-backed default, the header
// shouldn't frequently be used
// Locale is used for some number and date/time formatting, whereas language is used just for
// translating words in the interface
acceptLangHeader := c.Req.Header.Get("Accept-Language")
locale := "en-US"
language := "" // frontend will set the default language
if hs.Features.IsEnabled(featuremgmt.FlagInternationalization) && prefs.JSONData.Locale != "" {
locale = prefs.JSONData.Locale
} else if len(acceptLangHeader) > 0 {
if hs.Features.IsEnabled(featuremgmt.FlagInternationalization) && prefs.JSONData.Language != "" {
language = prefs.JSONData.Language
}
if len(acceptLangHeader) > 0 {
parts := strings.Split(acceptLangHeader, ",")
locale = parts[0]
}
@ -100,6 +102,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
Timezone: prefs.Timezone,
WeekStart: weekStart,
Locale: locale,
Language: language,
HelpFlags1: c.HelpFlags1,
HasEditPermissionInFolders: hasEditPerm,
},

View File

@ -96,7 +96,7 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team
}
if preference.JSONData != nil {
dto.Locale = preference.JSONData.Locale
dto.Language = preference.JSONData.Language
dto.Navbar = preference.JSONData.Navbar
dto.QueryHistory = preference.JSONData.QueryHistory
}
@ -149,7 +149,7 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t
OrgID: orgID,
TeamID: teamId,
Theme: dtoCmd.Theme,
Locale: dtoCmd.Locale,
Language: dtoCmd.Language,
Timezone: dtoCmd.Timezone,
WeekStart: dtoCmd.WeekStart,
HomeDashboardID: dtoCmd.HomeDashboardID,
@ -212,7 +212,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te
Timezone: dtoCmd.Timezone,
WeekStart: dtoCmd.WeekStart,
HomeDashboardID: dtoCmd.HomeDashboardID,
Locale: dtoCmd.Locale,
Language: dtoCmd.Language,
Navbar: dtoCmd.Navbar,
QueryHistory: dtoCmd.QueryHistory,
}

View File

@ -49,7 +49,7 @@ type SavePreferenceCommand struct {
Timezone string `json:"timezone,omitempty"`
WeekStart string `json:"weekStart,omitempty"`
Theme string `json:"theme,omitempty"`
Locale string `json:"locale,omitempty"`
Language string `json:"language,omitempty"`
Navbar *NavbarPreference `json:"navbar,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
}
@ -64,7 +64,7 @@ type PatchPreferenceCommand struct {
Timezone *string `json:"timezone,omitempty"`
WeekStart *string `json:"weekStart,omitempty"`
Theme *string `json:"theme,omitempty"`
Locale *string `json:"locale,omitempty"`
Language *string `json:"language,omitempty"`
Navbar *NavbarPreference `json:"navbar,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
}
@ -81,7 +81,7 @@ type NavbarPreference struct {
}
type PreferenceJSONData struct {
Locale string `json:"locale"`
Language string `json:"language"`
Navbar NavbarPreference `json:"navbar"`
QueryHistory QueryHistoryPreference `json:"queryHistory"`
}

View File

@ -61,8 +61,8 @@ func (s *Service) GetWithDefaults(ctx context.Context, query *pref.GetPreference
res.HomeDashboardID = p.HomeDashboardID
}
if p.JSONData != nil {
if p.JSONData.Locale != "" {
res.JSONData.Locale = p.JSONData.Locale
if p.JSONData.Language != "" {
res.JSONData.Language = p.JSONData.Language
}
if len(p.JSONData.Navbar.SavedItems) > 0 {
@ -113,7 +113,7 @@ func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) err
Created: time.Now(),
Updated: time.Now(),
JSONData: &pref.PreferenceJSONData{
Locale: cmd.Locale,
Language: cmd.Language,
},
}
_, err = s.store.Insert(ctx, preference)
@ -131,7 +131,7 @@ func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) err
preference.Version += 1
preference.HomeDashboardID = cmd.HomeDashboardID
preference.JSONData = &pref.PreferenceJSONData{
Locale: cmd.Locale,
Language: cmd.Language,
}
if cmd.Navbar != nil {
@ -166,11 +166,11 @@ func (s *Service) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) e
exists = true
}
if cmd.Locale != nil {
if cmd.Language != nil {
if preference.JSONData == nil {
preference.JSONData = &pref.PreferenceJSONData{}
}
preference.JSONData.Locale = *cmd.Locale
preference.JSONData.Language = *cmd.Language
}
if cmd.Navbar != nil {
@ -238,7 +238,7 @@ func (s *Service) GetDefaults() *pref.Preference {
}
if s.features.IsEnabled(featuremgmt.FlagInternationalization) {
defaults.JSONData.Locale = s.cfg.DefaultLocale
defaults.JSONData.Language = s.cfg.DefaultLanguage
}
return defaults

View File

@ -33,7 +33,7 @@ func TestGetDefaults(t *testing.T) {
cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
}
prefService.cfg.DefaultLocale = "en-US"
prefService.cfg.DefaultLanguage = "en-US"
prefService.cfg.DefaultTheme = "light"
prefService.cfg.DateFormats.DefaultTimezone = "UTC"
weekStart := ""
@ -76,7 +76,7 @@ func TestGetDefaultsWithI18nFeatureFlag(t *testing.T) {
features: featuremgmt.WithFeatures(featuremgmt.FlagInternationalization),
}
weekStart := ""
prefService.cfg.DefaultLocale = "en-US"
prefService.cfg.DefaultLanguage = "en-US"
prefService.cfg.DefaultTheme = "light"
prefService.cfg.DateFormats.DefaultTimezone = "UTC"
@ -88,7 +88,7 @@ func TestGetDefaultsWithI18nFeatureFlag(t *testing.T) {
Timezone: "UTC",
HomeDashboardID: 0,
JSONData: &pref.PreferenceJSONData{
Locale: "en-US",
Language: "en-US",
},
}
if diff := cmp.Diff(expected, preference); diff != "" {
@ -103,7 +103,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
}
prefService.cfg.DefaultLocale = "en-US"
prefService.cfg.DefaultLanguage = "en-US"
weekStartOne := "1"
weekStartTwo := "2"
@ -115,7 +115,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
Timezone: "UTC",
WeekStart: &weekStartOne,
JSONData: &pref.PreferenceJSONData{
Locale: "en-GB",
Language: "en-GB",
},
},
pref.Preference{
@ -126,7 +126,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
Timezone: "browser",
WeekStart: &weekStartTwo,
JSONData: &pref.PreferenceJSONData{
Locale: "en-AU",
Language: "en-AU",
},
},
)
@ -141,7 +141,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
WeekStart: &weekStartTwo,
HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{
Locale: "en-AU",
Language: "en-AU",
},
}
if diff := cmp.Diff(expected, preference); diff != "" {
@ -160,7 +160,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
WeekStart: &weekStartOne,
HomeDashboardID: 1,
JSONData: &pref.PreferenceJSONData{
Locale: "en-GB",
Language: "en-GB",
},
}
if diff := cmp.Diff(expected, preference); diff != "" {
@ -209,9 +209,9 @@ func TestGetDefaults_JSONData(t *testing.T) {
orgPreferencesJsonData := pref.PreferenceJSONData{
Navbar: orgNavbarPreferences,
}
orgPreferencesWithLocaleJsonData := pref.PreferenceJSONData{
Navbar: orgNavbarPreferences,
Locale: "en-GB",
orgPreferencesWithLanguageJsonData := pref.PreferenceJSONData{
Navbar: orgNavbarPreferences,
Language: "en-GB",
}
team2PreferencesJsonData := pref.PreferenceJSONData{
Navbar: team2NavbarPreferences,
@ -248,7 +248,7 @@ func TestGetDefaults_JSONData(t *testing.T) {
}, preference)
})
t.Run("user JSONData with missing locale does not override org preference", func(t *testing.T) {
t.Run("user JSONData with missing language does not override org preference", func(t *testing.T) {
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
@ -258,7 +258,7 @@ func TestGetDefaults_JSONData(t *testing.T) {
insertPrefs(t, prefService.store,
pref.Preference{
OrgID: 1,
JSONData: &orgPreferencesWithLocaleJsonData,
JSONData: &orgPreferencesWithLanguageJsonData,
},
pref.Preference{
OrgID: 1,
@ -273,7 +273,7 @@ func TestGetDefaults_JSONData(t *testing.T) {
require.Equal(t, &pref.Preference{
WeekStart: &weekStart,
JSONData: &pref.PreferenceJSONData{
Locale: "en-GB",
Language: "en-GB",
Navbar: userNavbarPreferences,
QueryHistory: queryPreference,
},

View File

@ -427,9 +427,9 @@ type Cfg struct {
LDAPSkipOrgRoleSync bool
LDAPAllowSignup bool
DefaultTheme string
DefaultLocale string
HomePage string
DefaultTheme string
DefaultLanguage string
HomePage string
Quota QuotaSettings
@ -1455,7 +1455,7 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
LoginHint = valueAsString(users, "login_hint", "")
PasswordHint = valueAsString(users, "password_hint", "")
cfg.DefaultTheme = valueAsString(users, "default_theme", "")
cfg.DefaultLocale = valueAsString(users, "default_locale", "")
cfg.DefaultLanguage = valueAsString(users, "default_language", "")
cfg.HomePage = valueAsString(users, "home_page", "")
ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "")
ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "")

View File

@ -105,7 +105,7 @@ export class GrafanaApp {
// Let iframe container know grafana has started loading
parent.postMessage('GrafanaAppInit', '*');
const loadLocalePromise = initializeI18n(config.bootData.user.locale);
const initI18nPromise = initializeI18n(config.bootData.user.language);
setBackendSrv(backendSrv);
initEchoSrv();
@ -160,7 +160,7 @@ export class GrafanaApp {
modalManager.init();
await Promise.all([
loadLocalePromise,
initI18nPromise,
// Preload selected app plugins
await preloadPlugins(config.pluginsToPreload),

View File

@ -83,7 +83,7 @@ const mockPreferences: UserPreferencesDTO = {
queryHistory: {
homeTab: '',
},
locale: '',
language: '',
};
const mockPrefsPatch = jest.fn();
@ -152,7 +152,7 @@ describe('SharedPreferences', () => {
expect(weekSelect).toHaveTextContent('Monday');
});
it('renders the locale preference', async () => {
it('renders the language preference', async () => {
const weekSelect = getSelectParent(screen.getByLabelText(/language/i));
expect(weekSelect).toHaveTextContent('Default');
});
@ -174,7 +174,7 @@ describe('SharedPreferences', () => {
queryHistory: {
homeTab: '',
},
locale: 'fr-FR',
language: 'fr-FR',
});
});
@ -196,7 +196,7 @@ describe('SharedPreferences', () => {
queryHistory: {
homeTab: '',
},
locale: '',
language: '',
});
});

View File

@ -19,7 +19,7 @@ import {
} from '@grafana/ui';
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
import { t, Trans } from 'app/core/internationalization';
import { LOCALES } from 'app/core/internationalization/constants';
import { LANGUAGES } from 'app/core/internationalization/constants';
import { PreferencesService } from 'app/core/services/PreferencesService';
import { UserPreferencesDTO } from 'app/types';
@ -37,7 +37,7 @@ const themes: SelectableValue[] = [
];
function getLanguageOptions(): Array<SelectableValue<string>> {
const languageOptions = LOCALES.map((v) => ({
const languageOptions = LANGUAGES.map((v) => ({
value: v.code,
label: v.name,
}));
@ -66,7 +66,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
theme: '',
timezone: '',
weekStart: '',
locale: '',
language: '',
queryHistory: { homeTab: '' },
};
}
@ -79,14 +79,14 @@ export class SharedPreferences extends PureComponent<Props, State> {
theme: prefs.theme,
timezone: prefs.timezone,
weekStart: prefs.weekStart,
locale: prefs.locale,
language: prefs.language,
queryHistory: prefs.queryHistory,
});
}
onSubmitForm = async () => {
const { homeDashboardUID, theme, timezone, weekStart, locale, queryHistory } = this.state;
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, locale, queryHistory });
const { homeDashboardUID, theme, timezone, weekStart, language, queryHistory } = this.state;
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, language, queryHistory });
window.location.reload();
};
@ -109,12 +109,12 @@ export class SharedPreferences extends PureComponent<Props, State> {
this.setState({ homeDashboardUID: dashboardUID });
};
onLocaleChanged = (locale: string) => {
this.setState({ locale });
onLanguageChanged = (language: string) => {
this.setState({ language });
};
render() {
const { theme, timezone, weekStart, homeDashboardUID, locale } = this.state;
const { theme, timezone, weekStart, homeDashboardUID, language } = this.state;
const { disabled } = this.props;
const styles = getStyles();
const languages = getLanguageOptions();
@ -188,8 +188,8 @@ export class SharedPreferences extends PureComponent<Props, State> {
data-testid="User preferences language drop down"
>
<Select
value={languages.find((lang) => lang.value === locale)}
onChange={(locale: SelectableValue<string>) => this.onLocaleChanged(locale.value ?? '')}
value={languages.find((lang) => lang.value === language)}
onChange={(lang: SelectableValue<string>) => this.onLanguageChanged(lang.value ?? '')}
options={languages}
placeholder={t('shared-preferences.fields.locale-placeholder', 'Choose language')}
inputId="locale-select"

View File

@ -1,14 +1,14 @@
import { uniqBy } from 'lodash';
import { LOCALES, VALID_LOCALES } from './constants';
import { LANGUAGES, VALID_LANGUAGES } from './constants';
describe('internationalization constants', () => {
it('should not have duplicate languages codes', () => {
const uniqLocales = uniqBy(LOCALES, (v) => v.code);
expect(LOCALES).toHaveLength(uniqLocales.length);
const uniqLocales = uniqBy(LANGUAGES, (v) => v.code);
expect(LANGUAGES).toHaveLength(uniqLocales.length);
});
it('should have a correct list of valid locale codes', () => {
expect(VALID_LOCALES).toEqual(LOCALES.map((v) => v.code));
expect(VALID_LANGUAGES).toEqual(LANGUAGES.map((v) => v.code));
});
});

View File

@ -7,9 +7,9 @@ export const GERMAN_GERMANY = 'de-DE';
export const CHINESE_SIMPLIFIED = 'zh-Hans';
export const PSEUDO_LOCALE = 'pseudo-LOCALE';
export const DEFAULT_LOCALE = ENGLISH_US;
export const DEFAULT_LANGUAGE = ENGLISH_US;
interface LocaleDefinition {
interface LanguageDefinitions {
/** IETF language tag for the language e.g. en-US */
code: string;
@ -20,7 +20,7 @@ interface LocaleDefinition {
loader: () => Promise<ResourceKey>;
}
export const LOCALES: LocaleDefinition[] = [
export const LANGUAGES: LanguageDefinitions[] = [
{
code: ENGLISH_US,
name: 'English',
@ -53,11 +53,11 @@ export const LOCALES: LocaleDefinition[] = [
];
if (process.env.NODE_ENV === 'development') {
LOCALES.push({
LANGUAGES.push({
code: PSEUDO_LOCALE,
name: 'Pseudo-locale',
loader: () => import('../../../locales/pseudo-LOCALE/grafana.json'),
});
}
export const VALID_LOCALES = LOCALES.map((v) => v.code);
export const VALID_LANGUAGES = LANGUAGES.map((v) => v.code);

View File

@ -2,13 +2,13 @@ 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, LOCALES, VALID_LOCALES } from './constants';
import { DEFAULT_LANGUAGE, LANGUAGES, VALID_LANGUAGES } from './constants';
const loadTranslations: BackendModule = {
type: 'backend',
init() {},
async read(language, namespace, callback) {
const localeDef = LOCALES.find((v) => v.code === language);
const localeDef = LANGUAGES.find((v) => v.code === language);
if (!localeDef) {
return callback(new Error('No message loader available for ' + language), null);
@ -19,8 +19,8 @@ const loadTranslations: BackendModule = {
},
};
export function initializeI18n(locale: string) {
const validLocale = VALID_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE;
export function initializeI18n(language: string) {
const validLocale = VALID_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE;
i18n
.use(loadTranslations)
@ -47,7 +47,7 @@ export function initializeI18n(locale: string) {
}
export function changeLanguage(locale: string) {
const validLocale = VALID_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE;
const validLocale = VALID_LANGUAGES.includes(locale) ? locale : DEFAULT_LANGUAGE;
return i18n.changeLanguage(validLocale);
}
@ -63,7 +63,7 @@ export const i18nDate = (value: number | Date | string, format: Intl.DateTimeFor
if (typeof value === 'string') {
return i18nDate(new Date(value), format);
}
const locale = i18n.options.lng ?? DEFAULT_LOCALE;
const locale = i18n.options.lng ?? DEFAULT_LANGUAGE;
const dateFormatter = new Intl.DateTimeFormat(locale, format);
return dateFormatter.format(value);

View File

@ -24,6 +24,7 @@ export class User implements CurrentUserInternal {
timezone: string;
weekStart: string;
locale: string;
language: string;
helpFlags1: number;
hasEditPermissionInFolders: boolean;
permissions?: UserPermission;
@ -47,6 +48,7 @@ export class User implements CurrentUserInternal {
this.email = '';
this.name = '';
this.locale = '';
this.language = '';
this.weekStart = '';
this.gravatarUrl = '';

View File

@ -18,6 +18,7 @@ const baseUser: CurrentUserDTO = {
timezone: 'browser',
weekStart: 'browser',
locale: 'en-AU',
language: 'en-US',
externalUserId: '',
};

View File

@ -3,7 +3,7 @@ import { TimeZone } from '@grafana/data';
export interface UserPreferencesDTO {
timezone: TimeZone;
weekStart: string;
locale: string;
language: string;
// It is undefined when there is not dashboard assigned (default)
homeDashboardUID?: string;
theme: string;