From dcf786f3a936df413f3165d955b89f843b46b086 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 21 Jun 2022 11:12:49 +0100 Subject: [PATCH] I18n: Add default locale server config option (#51035) * I18n: Set default locale in server config and expose in grafanaBootData * put default locale behind feature flag * update tests now that default locale is behind feature flag * little bit of PR feedback * update sample.ini --- conf/defaults.ini | 3 + conf/sample.ini | 5 +- pkg/api/index.go | 12 +- pkg/services/preference/prefimpl/pref.go | 27 +++- pkg/services/preference/prefimpl/pref_test.go | 119 +++++++++++++++--- pkg/setting/setting.go | 6 +- 6 files changed, 142 insertions(+), 30 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index b2034acc9ac..92bbcdfd35d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -376,6 +376,9 @@ 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 + # 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 = diff --git a/conf/sample.ini b/conf/sample.ini index 960d84ff9b9..45bc3283a08 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -376,8 +376,11 @@ # Default UI theme ("dark" or "light") ;default_theme = dark +# Default locale (supported IETF language tag, such as en-US) +;default_locale = 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 = +;home_page = # External user management, these options affect the organization users view ;external_manage_link_url = diff --git a/pkg/api/index.go b/pkg/api/index.go index ca072bb5ccc..eedc353334b 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -691,12 +691,16 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat return nil, err } - // Read locale from accept-language - acceptLang := c.Req.Header.Get("Accept-Language") + // 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 + acceptLangHeader := c.Req.Header.Get("Accept-Language") locale := "en-US" - if len(acceptLang) > 0 { - parts := strings.Split(acceptLang, ",") + if hs.Features.IsEnabled(featuremgmt.FlagInternationalization) && prefs.JSONData.Locale != "" { + locale = prefs.JSONData.Locale + } else if len(acceptLangHeader) > 0 { + parts := strings.Split(acceptLangHeader, ",") locale = parts[0] } diff --git a/pkg/services/preference/prefimpl/pref.go b/pkg/services/preference/prefimpl/pref.go index d8fd2937c31..a7703b3b73f 100644 --- a/pkg/services/preference/prefimpl/pref.go +++ b/pkg/services/preference/prefimpl/pref.go @@ -5,22 +5,25 @@ import ( "errors" "time" + "github.com/grafana/grafana/pkg/services/featuremgmt" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/sqlstore/db" "github.com/grafana/grafana/pkg/setting" ) type Service struct { - store store - cfg *setting.Cfg + store store + cfg *setting.Cfg + features *featuremgmt.FeatureManager } -func ProvideService(db db.DB, cfg *setting.Cfg) pref.Service { +func ProvideService(db db.DB, cfg *setting.Cfg, features *featuremgmt.FeatureManager) pref.Service { return &Service{ store: &sqlStore{ db: db, }, - cfg: cfg, + cfg: cfg, + features: features, } } @@ -51,7 +54,17 @@ func (s *Service) GetWithDefaults(ctx context.Context, query *pref.GetPreference res.HomeDashboardID = p.HomeDashboardID } if p.JSONData != nil { - res.JSONData = p.JSONData + if p.JSONData.Locale != "" { + res.JSONData.Locale = p.JSONData.Locale + } + + if len(p.JSONData.Navbar.SavedItems) > 0 { + res.JSONData.Navbar = p.JSONData.Navbar + } + + if p.JSONData.QueryHistory.HomeTab != "" { + res.JSONData.QueryHistory.HomeTab = p.JSONData.QueryHistory.HomeTab + } } } @@ -217,5 +230,9 @@ func (s *Service) GetDefaults() *pref.Preference { JSONData: &pref.PreferenceJSONData{}, } + if s.features.IsEnabled(featuremgmt.FlagInternationalization) { + defaults.JSONData.Locale = s.cfg.DefaultLocale + } + return defaults } diff --git a/pkg/services/preference/prefimpl/pref_test.go b/pkg/services/preference/prefimpl/pref_test.go index 782aa20fc13..28749e715bc 100644 --- a/pkg/services/preference/prefimpl/pref_test.go +++ b/pkg/services/preference/prefimpl/pref_test.go @@ -8,14 +8,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/featuremgmt" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/setting" ) func TestGet_empty(t *testing.T) { prefService := &Service{ - store: newFake(), - cfg: setting.NewCfg(), + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), } preference, err := prefService.Get(context.Background(), &pref.GetPreferenceQuery{}) require.NoError(t, err) @@ -27,9 +29,11 @@ func TestGet_empty(t *testing.T) { func TestGetDefaults(t *testing.T) { prefService := &Service{ - store: newFake(), - cfg: setting.NewCfg(), + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), } + prefService.cfg.DefaultLocale = "en-US" prefService.cfg.DefaultTheme = "light" prefService.cfg.DateFormats.DefaultTimezone = "UTC" @@ -62,11 +66,40 @@ func TestGetDefaults(t *testing.T) { }) } +func TestGetDefaultsWithI18nFeatureFlag(t *testing.T) { + prefService := &Service{ + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(featuremgmt.FlagInternationalization), + } + prefService.cfg.DefaultLocale = "en-US" + prefService.cfg.DefaultTheme = "light" + prefService.cfg.DateFormats.DefaultTimezone = "UTC" + + t.Run("GetDefaults", func(t *testing.T) { + preference := prefService.GetDefaults() + expected := &pref.Preference{ + Theme: "light", + Timezone: "UTC", + HomeDashboardID: 0, + JSONData: &pref.PreferenceJSONData{ + Locale: "en-US", + }, + } + if diff := cmp.Diff(expected, preference); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) +} + func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { prefService := &Service{ - store: newFake(), - cfg: setting.NewCfg(), + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), } + prefService.cfg.DefaultLocale = "en-US" + insertPrefs(t, prefService.store, pref.Preference{ OrgID: 1, @@ -74,6 +107,9 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { Theme: "dark", Timezone: "UTC", WeekStart: "1", + JSONData: &pref.PreferenceJSONData{ + Locale: "en-GB", + }, }, pref.Preference{ OrgID: 1, @@ -82,6 +118,9 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { Theme: "light", Timezone: "browser", WeekStart: "2", + JSONData: &pref.PreferenceJSONData{ + Locale: "en-AU", + }, }, ) @@ -94,7 +133,9 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { Timezone: "browser", WeekStart: "2", HomeDashboardID: 4, - JSONData: &pref.PreferenceJSONData{}, + JSONData: &pref.PreferenceJSONData{ + Locale: "en-AU", + }, } if diff := cmp.Diff(expected, preference); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) @@ -111,7 +152,9 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { Timezone: "UTC", WeekStart: "1", HomeDashboardID: 1, - JSONData: &pref.PreferenceJSONData{}, + JSONData: &pref.PreferenceJSONData{ + Locale: "en-GB", + }, } if diff := cmp.Diff(expected, preference); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) @@ -158,6 +201,10 @@ func TestGetDefaults_JSONData(t *testing.T) { orgPreferencesJsonData := pref.PreferenceJSONData{ Navbar: orgNavbarPreferences, } + orgPreferencesWithLocaleJsonData := pref.PreferenceJSONData{ + Navbar: orgNavbarPreferences, + Locale: "en-GB", + } team2PreferencesJsonData := pref.PreferenceJSONData{ Navbar: team2NavbarPreferences, } @@ -167,8 +214,9 @@ func TestGetDefaults_JSONData(t *testing.T) { t.Run("users have precedence over org", func(t *testing.T) { prefService := &Service{ - store: newFake(), - cfg: setting.NewCfg(), + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), } insertPrefs(t, prefService.store, @@ -191,10 +239,42 @@ func TestGetDefaults_JSONData(t *testing.T) { }, preference) }) + t.Run("user JSONData with missing locale does not override org preference", func(t *testing.T) { + prefService := &Service{ + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), + } + + insertPrefs(t, prefService.store, + pref.Preference{ + OrgID: 1, + JSONData: &orgPreferencesWithLocaleJsonData, + }, + pref.Preference{ + OrgID: 1, + UserID: 1, + JSONData: &userPreferencesJsonData, + }, + ) + + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 1} + preference, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + require.Equal(t, &pref.Preference{ + JSONData: &pref.PreferenceJSONData{ + Locale: "en-GB", + Navbar: userNavbarPreferences, + QueryHistory: queryPreference, + }, + }, preference) + }) + t.Run("teams have precedence over org and are read in ascending order", func(t *testing.T) { prefService := &Service{ - store: newFake(), - cfg: setting.NewCfg(), + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), } insertPrefs(t, prefService.store, @@ -227,8 +307,9 @@ func TestGetDefaults_JSONData(t *testing.T) { func TestGetWithDefaults_teams(t *testing.T) { prefService := &Service{ - store: newFake(), - cfg: setting.NewCfg(), + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), } insertPrefs(t, prefService.store, pref.Preference{ @@ -273,8 +354,9 @@ func TestGetWithDefaults_teams(t *testing.T) { func TestPatch_toCreate(t *testing.T) { prefService := &Service{ - store: newFake(), - cfg: setting.NewCfg(), + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), } themeValue := "light" @@ -293,8 +375,9 @@ func TestPatch_toCreate(t *testing.T) { func TestSave(t *testing.T) { prefService := &Service{ - store: newFake(), - cfg: setting.NewCfg(), + store: newFake(), + cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), } t.Run("insert", func(t *testing.T) { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c6f0b815376..f120054fea4 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -397,8 +397,9 @@ type Cfg struct { Quota QuotaSettings - DefaultTheme string - HomePage string + DefaultTheme string + DefaultLocale string + HomePage string AutoAssignOrg bool AutoAssignOrgId int @@ -1356,6 +1357,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.HomePage = valueAsString(users, "home_page", "") ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "") ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "")