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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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_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 =

View File

@ -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 =

View File

@ -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]
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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", "")