diff --git a/app/boot_levels.go b/app/boot_levels.go index 71ae84073..46d6678d3 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -101,7 +101,7 @@ func (app *CortezaApp) Setup() (err error) { localeLog = app.Log } - if languages, err = locale.New(localeLog, strings.Split(app.Opt.Locale.Path, ":")...); err != nil { + if languages, err = locale.New(localeLog, app.Opt.Locale); err != nil { return err } else { locale.SetGlobal(languages) diff --git a/auth/assets/templates/inc_header.html.tpl b/auth/assets/templates/inc_header.html.tpl index 7f9f4320e..b53ca413c 100644 --- a/auth/assets/templates/inc_header.html.tpl +++ b/auth/assets/templates/inc_header.html.tpl @@ -1,5 +1,5 @@ - + @@ -12,9 +12,7 @@ - - Corteza - + Corteza {{ if .user }} diff --git a/auth/handlers/handler.go b/auth/handlers/handler.go index af4b1ab09..def920680 100644 --- a/auth/handlers/handler.go +++ b/auth/handlers/handler.go @@ -232,8 +232,12 @@ func (h *AuthHandlers) handle(fn handlerFn) http.HandlerFunc { var ( // translator template function + // + // we're adding these functions on every request on all templates + // because we need request's context to detect the language from! ttf = func(t *template.Template) *template.Template { return t.Funcs(map[string]interface{}{ + "language": func() string { return loc.Current(req.Context()).String() }, "tr": func(key string, pp ...interface{}) template.HTML { ss := make([]string, len(pp)) for i := range pp { diff --git a/pkg/locale/http.go b/pkg/locale/http.go index a477de100..783276464 100644 --- a/pkg/locale/http.go +++ b/pkg/locale/http.go @@ -3,6 +3,7 @@ package locale import ( "net/http" + "go.uber.org/zap" "golang.org/x/text/language" ) @@ -11,34 +12,56 @@ const AcceptLanguageHeader = "Accept-Language" func DetectLanguage(ll *Languages) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // parse & ignore errors var ( - preferred = ll.Default() - supported = ll.Tags() - - accepted, _, err = language.ParseAcceptLanguage(r.Header.Get(AcceptLanguageHeader)) + rawLanguageTag string ) - if err == nil { - // ignoring index & confidence - preferred, _, _ = language.NewMatcher(supported).Match(accepted...) + // try to detect the language from the request's query string: + if ll.Options().QueryStringParam != "" { + rawLanguageTag = r.URL.Query().Get(ll.Options().QueryStringParam) + } - var match bool - for _, s := range supported { - if s == preferred { - match = true - break + // try to detect the language from the request's headers: + if len(rawLanguageTag) == 0 { + rawLanguageTag = r.Header.Get(AcceptLanguageHeader) + } + + if len(rawLanguageTag) > 0 { + // parse & ignore errors + var ( + preferred = ll.Default() + supported = ll.Tags() + + accepted, _, err = language.ParseAcceptLanguage(rawLanguageTag) + ) + + if err == nil { + // ignoring index & confidence + preferred, _, _ = language.NewMatcher(supported).Match(accepted...) + + var match bool + for _, s := range supported { + if s == preferred { + match = true + break + } + } + + if !match { + base, _ := preferred.Base() + preferred = language.MustParse(base.String()) } } - if !match { - base, _ := preferred.Base() - preferred = language.MustParse(base.String()) - } - } + // new request with new context + r = r.WithContext(SetLanguageToContext(r.Context(), preferred)) - // new request with new context - r = r.WithContext(SetLanguageToContext(r.Context(), preferred)) + ll.log.Debug( + "language detected", + zap.String("preferred", preferred.String()), + zap.String("raw", rawLanguageTag), + ) + } next.ServeHTTP(w, r) }) diff --git a/pkg/locale/locale.go b/pkg/locale/locale.go index 50f9c90eb..c24fccd50 100644 --- a/pkg/locale/locale.go +++ b/pkg/locale/locale.go @@ -8,6 +8,7 @@ import ( "strings" "sync" + "github.com/cortezaproject/corteza-server/pkg/options" "go.uber.org/zap" "golang.org/x/text/language" ) @@ -45,6 +46,8 @@ type ( log *zap.Logger + opt options.LocaleOpt + // sources src []string ll map[language.Tag]*Language @@ -56,8 +59,12 @@ type ( ErrorMetaKey struct{} ) -func New(log *zap.Logger, src ...string) (*Languages, error) { - ll := &Languages{src: src, log: log.Named("locale")} +func New(log *zap.Logger, opt options.LocaleOpt) (*Languages, error) { + ll := &Languages{ + opt: opt, + src: strings.Split(opt.Path, ":"), + log: log.Named("locale"), + } return ll, ll.Reload() } @@ -65,6 +72,10 @@ func (set *Languages) Default() language.Tag { return set.def } +func (set *Languages) Options() options.LocaleOpt { + return set.opt +} + func (set *Languages) Tags() (tt []language.Tag) { tt = make([]language.Tag, 0, len(set.ll)) for t := range set.ll { @@ -144,6 +155,10 @@ func (set *Languages) GetNS(ctx context.Context, ns string) func(key string, rr } } +func (set *Languages) Current(ctx context.Context) language.Tag { + return GetLanguageFromContext(ctx) +} + func (set *Languages) Get(ctx context.Context, ns, key string, rr ...string) string { return set.get(GetLanguageFromContext(ctx), ns, key, rr...) } diff --git a/pkg/options/locale.gen.go b/pkg/options/locale.gen.go index dd3b9862b..60053855a 100644 --- a/pkg/options/locale.gen.go +++ b/pkg/options/locale.gen.go @@ -10,14 +10,17 @@ package options type ( LocaleOpt struct { - Path string `env:"LOCALE_PATH"` - Log bool `env:"LOCALE_LOG"` + Path string `env:"LOCALE_PATH"` + QueryStringParam string `env:"LOCALE_QUERY_STRING_PARAM"` + Log bool `env:"LOCALE_LOG"` } ) // Locale initializes and returns a LocaleOpt with default values func Locale() (o *LocaleOpt) { - o = &LocaleOpt{} + o = &LocaleOpt{ + QueryStringParam: "lng", + } fill(o) diff --git a/pkg/options/locale.yaml b/pkg/options/locale.yaml index 97af87c03..2084de167 100644 --- a/pkg/options/locale.yaml +++ b/pkg/options/locale.yaml @@ -2,6 +2,13 @@ props: - name: path description: Path to locale config and translation files, separated by colon + - name: queryStringParam + description: | + Name of the query string parameter used to pass the language tag (it overrides Accept-Language header). + Set it to empty string to disable detection from the query string. + This parameter is ignored if only one language is enabled + default: "lng" + - name: log type: bool description: Log locale related events and actions