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
This commit is contained in:
Josh Hunt
2022-06-21 11:12:49 +01:00
committed by GitHub
parent 370d6a6f7b
commit dcf786f3a9
6 changed files with 142 additions and 30 deletions

View File

@@ -376,6 +376,9 @@ password_hint = password
# Default UI theme ("dark" or "light") # Default UI theme ("dark" or "light")
default_theme = dark 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. # 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 =

View File

@@ -376,8 +376,11 @@
# Default UI theme ("dark" or "light") # Default UI theme ("dark" or "light")
;default_theme = dark ;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. # 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 user management, these options affect the organization users view
;external_manage_link_url = ;external_manage_link_url =

View File

@@ -691,12 +691,16 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
return nil, err return nil, err
} }
// Read locale from accept-language // Set locale to the preference, otherwise fall back to the accept language header.
acceptLang := c.Req.Header.Get("Accept-Language") // 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" locale := "en-US"
if len(acceptLang) > 0 { if hs.Features.IsEnabled(featuremgmt.FlagInternationalization) && prefs.JSONData.Locale != "" {
parts := strings.Split(acceptLang, ",") locale = prefs.JSONData.Locale
} else if len(acceptLangHeader) > 0 {
parts := strings.Split(acceptLangHeader, ",")
locale = parts[0] locale = parts[0]
} }

View File

@@ -5,22 +5,25 @@ import (
"errors" "errors"
"time" "time"
"github.com/grafana/grafana/pkg/services/featuremgmt"
pref "github.com/grafana/grafana/pkg/services/preference" pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/sqlstore/db" "github.com/grafana/grafana/pkg/services/sqlstore/db"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
type Service struct { type Service struct {
store store store store
cfg *setting.Cfg 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{ return &Service{
store: &sqlStore{ store: &sqlStore{
db: db, 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 res.HomeDashboardID = p.HomeDashboardID
} }
if p.JSONData != nil { 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{}, JSONData: &pref.PreferenceJSONData{},
} }
if s.features.IsEnabled(featuremgmt.FlagInternationalization) {
defaults.JSONData.Locale = s.cfg.DefaultLocale
}
return defaults return defaults
} }

View File

@@ -8,14 +8,16 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
pref "github.com/grafana/grafana/pkg/services/preference" pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func TestGet_empty(t *testing.T) { func TestGet_empty(t *testing.T) {
prefService := &Service{ prefService := &Service{
store: newFake(), store: newFake(),
cfg: setting.NewCfg(), cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
} }
preference, err := prefService.Get(context.Background(), &pref.GetPreferenceQuery{}) preference, err := prefService.Get(context.Background(), &pref.GetPreferenceQuery{})
require.NoError(t, err) require.NoError(t, err)
@@ -27,9 +29,11 @@ func TestGet_empty(t *testing.T) {
func TestGetDefaults(t *testing.T) { func TestGetDefaults(t *testing.T) {
prefService := &Service{ prefService := &Service{
store: newFake(), store: newFake(),
cfg: setting.NewCfg(), cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
} }
prefService.cfg.DefaultLocale = "en-US"
prefService.cfg.DefaultTheme = "light" prefService.cfg.DefaultTheme = "light"
prefService.cfg.DateFormats.DefaultTimezone = "UTC" 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) { func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
prefService := &Service{ prefService := &Service{
store: newFake(), store: newFake(),
cfg: setting.NewCfg(), cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
} }
prefService.cfg.DefaultLocale = "en-US"
insertPrefs(t, prefService.store, insertPrefs(t, prefService.store,
pref.Preference{ pref.Preference{
OrgID: 1, OrgID: 1,
@@ -74,6 +107,9 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
Theme: "dark", Theme: "dark",
Timezone: "UTC", Timezone: "UTC",
WeekStart: "1", WeekStart: "1",
JSONData: &pref.PreferenceJSONData{
Locale: "en-GB",
},
}, },
pref.Preference{ pref.Preference{
OrgID: 1, OrgID: 1,
@@ -82,6 +118,9 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
Theme: "light", Theme: "light",
Timezone: "browser", Timezone: "browser",
WeekStart: "2", WeekStart: "2",
JSONData: &pref.PreferenceJSONData{
Locale: "en-AU",
},
}, },
) )
@@ -94,7 +133,9 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
Timezone: "browser", Timezone: "browser",
WeekStart: "2", WeekStart: "2",
HomeDashboardID: 4, HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{}, JSONData: &pref.PreferenceJSONData{
Locale: "en-AU",
},
} }
if diff := cmp.Diff(expected, preference); diff != "" { if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff) t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@@ -111,7 +152,9 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
Timezone: "UTC", Timezone: "UTC",
WeekStart: "1", WeekStart: "1",
HomeDashboardID: 1, HomeDashboardID: 1,
JSONData: &pref.PreferenceJSONData{}, JSONData: &pref.PreferenceJSONData{
Locale: "en-GB",
},
} }
if diff := cmp.Diff(expected, preference); diff != "" { if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff) t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@@ -158,6 +201,10 @@ func TestGetDefaults_JSONData(t *testing.T) {
orgPreferencesJsonData := pref.PreferenceJSONData{ orgPreferencesJsonData := pref.PreferenceJSONData{
Navbar: orgNavbarPreferences, Navbar: orgNavbarPreferences,
} }
orgPreferencesWithLocaleJsonData := pref.PreferenceJSONData{
Navbar: orgNavbarPreferences,
Locale: "en-GB",
}
team2PreferencesJsonData := pref.PreferenceJSONData{ team2PreferencesJsonData := pref.PreferenceJSONData{
Navbar: team2NavbarPreferences, Navbar: team2NavbarPreferences,
} }
@@ -167,8 +214,9 @@ func TestGetDefaults_JSONData(t *testing.T) {
t.Run("users have precedence over org", func(t *testing.T) { t.Run("users have precedence over org", func(t *testing.T) {
prefService := &Service{ prefService := &Service{
store: newFake(), store: newFake(),
cfg: setting.NewCfg(), cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
} }
insertPrefs(t, prefService.store, insertPrefs(t, prefService.store,
@@ -191,10 +239,42 @@ func TestGetDefaults_JSONData(t *testing.T) {
}, preference) }, 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) { t.Run("teams have precedence over org and are read in ascending order", func(t *testing.T) {
prefService := &Service{ prefService := &Service{
store: newFake(), store: newFake(),
cfg: setting.NewCfg(), cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
} }
insertPrefs(t, prefService.store, insertPrefs(t, prefService.store,
@@ -227,8 +307,9 @@ func TestGetDefaults_JSONData(t *testing.T) {
func TestGetWithDefaults_teams(t *testing.T) { func TestGetWithDefaults_teams(t *testing.T) {
prefService := &Service{ prefService := &Service{
store: newFake(), store: newFake(),
cfg: setting.NewCfg(), cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
} }
insertPrefs(t, prefService.store, insertPrefs(t, prefService.store,
pref.Preference{ pref.Preference{
@@ -273,8 +354,9 @@ func TestGetWithDefaults_teams(t *testing.T) {
func TestPatch_toCreate(t *testing.T) { func TestPatch_toCreate(t *testing.T) {
prefService := &Service{ prefService := &Service{
store: newFake(), store: newFake(),
cfg: setting.NewCfg(), cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
} }
themeValue := "light" themeValue := "light"
@@ -293,8 +375,9 @@ func TestPatch_toCreate(t *testing.T) {
func TestSave(t *testing.T) { func TestSave(t *testing.T) {
prefService := &Service{ prefService := &Service{
store: newFake(), store: newFake(),
cfg: setting.NewCfg(), cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
} }
t.Run("insert", func(t *testing.T) { t.Run("insert", func(t *testing.T) {

View File

@@ -397,8 +397,9 @@ type Cfg struct {
Quota QuotaSettings Quota QuotaSettings
DefaultTheme string DefaultTheme string
HomePage string DefaultLocale string
HomePage string
AutoAssignOrg bool AutoAssignOrg bool
AutoAssignOrgId int AutoAssignOrgId int
@@ -1356,6 +1357,7 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
LoginHint = valueAsString(users, "login_hint", "") LoginHint = valueAsString(users, "login_hint", "")
PasswordHint = valueAsString(users, "password_hint", "") PasswordHint = valueAsString(users, "password_hint", "")
cfg.DefaultTheme = valueAsString(users, "default_theme", "") cfg.DefaultTheme = valueAsString(users, "default_theme", "")
cfg.DefaultLocale = valueAsString(users, "default_locale", "")
cfg.HomePage = valueAsString(users, "home_page", "") cfg.HomePage = valueAsString(users, "home_page", "")
ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "") ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "")
ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "") ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "")