From 20fcfb4110e3bf6d46a6175493be86266ff246c1 Mon Sep 17 00:00:00 2001
From: "Gerasimos (Makis) Maropoulos" <kataras2006@hotmail.com>
Date: Mon, 4 May 2020 19:13:06 +0300
Subject: [PATCH] add a 'Context.SetLanguage', similar to pp.I18n.ExtractFunc
 but it can be used per-handler

relative: https://github.com/kataras/iris/issues/1502#issuecomment-623553034

Former-commit-id: 7e2fadeb60149f43edbb9e220b0274ce1160881b
---
 HISTORY.md               |  3 ++-
 _examples/i18n/main.go   |  1 +
 configuration.go         | 23 ++++++++++++++++++++++-
 context/configuration.go |  3 +++
 context/context.go       | 20 ++++++++++++++++++++
 i18n/i18n.go             | 24 +++++++++++++++++-------
 6 files changed, 65 insertions(+), 9 deletions(-)

diff --git a/HISTORY.md b/HISTORY.md
index 6b81232e..36b386fd 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -394,7 +394,8 @@ Other Improvements:
 
 New Context Methods:
 
-- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client.
+- `Context.SetLanguage(langCode string)` force-sets a language code from inside a middleare, similar to the `app.I18n.ExtractFunc`
+- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client
 - `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
 - `Context.IsGRPC() bool` reports whether the request came from a gRPC client
 - `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
diff --git a/_examples/i18n/main.go b/_examples/i18n/main.go
index 438e5acb..11b15d4c 100644
--- a/_examples/i18n/main.go
+++ b/_examples/i18n/main.go
@@ -23,6 +23,7 @@ func newApp() *iris.Application {
 	// Set to false to disallow path (local) redirects,
 	// see https://github.com/kataras/iris/issues/1369.
 	// app.I18n.PathRedirect = true
+	// See app.I18n.ExtractFunc to change the way a language is extracted from a request.
 
 	app.Get("/", func(ctx iris.Context) {
 		hi := ctx.Tr("hi", "iris")
diff --git a/configuration.go b/configuration.go
index a0248296..985974ce 100644
--- a/configuration.go
+++ b/configuration.go
@@ -845,7 +845,17 @@ type Configuration struct {
 	//
 	// Defaults to "iris.locale".
 	LocaleContextKey string `json:"localeContextKey,omitempty" yaml:"LocaleContextKey" toml:"LocaleContextKey"`
-
+	// LanguageContextKey is the context key which a language can be modified by a middleware.
+	// It has the highest priority over the rest and if it is empty then it is ignored,
+	// if it set to a static string of "default" or to the default language's code
+	// then the rest of the language extractors will not be called at all and
+	// the default language will be set instead.
+	//
+	// Use with `Context.SetLanguage("el-GR")`.
+	//
+	// See `i18n.ExtractFunc` for a more organised way of the same feature.
+	// Defaults to "iris.locale.language".
+	LanguageContextKey string `json:"languageContextKey,omitempty" yaml:"LanguageContextKey" toml:"LanguageContextKey"`
 	// GetViewLayoutContextKey is the key of the context's user values' key
 	// which is being used to set the template
 	// layout from a middleware or the main handler.
@@ -998,6 +1008,12 @@ func (c Configuration) GetLocaleContextKey() string {
 	return c.LocaleContextKey
 }
 
+// GetLanguageContextKey returns the configuration's LanguageContextKey value,
+// used for i18n.
+func (c Configuration) GetLanguageContextKey() string {
+	return c.LanguageContextKey
+}
+
 // GetViewLayoutContextKey returns the key of the context's user values' key
 // which is being used to set the template
 // layout from a middleware or the main handler.
@@ -1132,6 +1148,10 @@ func WithConfiguration(c Configuration) Configurator {
 			main.LocaleContextKey = v
 		}
 
+		if v := c.LanguageContextKey; v != "" {
+			main.LanguageContextKey = v
+		}
+
 		if v := c.ViewLayoutContextKey; v != "" {
 			main.ViewLayoutContextKey = v
 		}
@@ -1185,6 +1205,7 @@ func DefaultConfiguration() Configuration {
 		// or `context#SetMaxRequestBodySize`.
 		PostMaxMemory:            32 << 20, // 32MB
 		LocaleContextKey:         "iris.locale",
+		LanguageContextKey:       "iris.locale.language",
 		ViewLayoutContextKey:     "iris.viewLayout",
 		ViewDataContextKey:       "iris.viewData",
 		RemoteAddrHeaders:        make(map[string]bool),
diff --git a/context/configuration.go b/context/configuration.go
index c920730b..2d9e8f1a 100644
--- a/context/configuration.go
+++ b/context/configuration.go
@@ -78,6 +78,9 @@ type ConfigurationReadOnly interface {
 	// GetTranslateLanguageContextKey returns the configuration's LocaleContextKey value,
 	// used for i18n. Defaults to "iris.locale".
 	GetLocaleContextKey() string
+	// GetLanguageContextKey returns the configuration's LanguageContextKey value,
+	// used for i18n. Defaults to "iris.locale.language".
+	GetLanguageContextKey() string
 
 	// GetViewLayoutContextKey returns the key of the context's user values' key
 	// which is being used to set the template
diff --git a/context/context.go b/context/context.go
index ab92b9f2..9db59000 100644
--- a/context/context.go
+++ b/context/context.go
@@ -409,6 +409,14 @@ type Context interface {
 	// in https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
 	// or by the URL query parameter "referer".
 	GetReferrer() Referrer
+	// SetLanguage force-sets the language for i18n, can be used inside a middleare.
+	// It has the highest priority over the rest and if it is empty then it is ignored,
+	// if it set to a static string of "default" or to the default language's code
+	// then the rest of the language extractors will not be called at all and
+	// the default language will be set instead.
+	//
+	// See `app.I18n.ExtractFunc` for a more organised way of the same feature.
+	SetLanguage(langCode string)
 	// GetLocale returns the current request's `Locale` found by i18n middleware.
 	// See `Tr` too.
 	GetLocale() Locale
@@ -1975,9 +1983,21 @@ func (ctx *context) GetReferrer() Referrer {
 	return emptyReferrer
 }
 
+// SetLanguage force-sets the language for i18n, can be used inside a middleare.
+// It has the highest priority over the rest and if it is empty then it is ignored,
+// if it set to a static string of "default" or to the default language's code
+// then the rest of the language extractors will not be called at all and
+// the default language will be set instead.
+//
+// See `i18n.ExtractFunc` for a more organised way of the same feature.
+func (ctx *context) SetLanguage(langCode string) {
+	ctx.Values().Set(ctx.app.ConfigurationReadOnly().GetLanguageContextKey(), langCode)
+}
+
 // GetLocale returns the current request's `Locale` found by i18n middleware.
 // See `Tr` too.
 func (ctx *context) GetLocale() Locale {
+	// Cache the Locale itself for multiple calls of `Tr` method.
 	contextKey := ctx.app.ConfigurationReadOnly().GetLocaleContextKey()
 	if v := ctx.Values().Get(contextKey); v != nil {
 		if locale, ok := v.(Locale); ok {
diff --git a/i18n/i18n.go b/i18n/i18n.go
index fceec970..410a7a5c 100644
--- a/i18n/i18n.go
+++ b/i18n/i18n.go
@@ -320,21 +320,31 @@ const acceptLanguageHeaderKey = "Accept-Language"
 // GetLocale returns the found locale of a request.
 // It will return the first registered language if nothing else matched.
 func (i *I18n) GetLocale(ctx context.Context) context.Locale {
-	// if v := ctx.Values().Get(ctx.Application().ConfigurationReadOnly().GetLocaleContextKey()); v != nil {
-	// 	if locale, ok := v.(context.Locale); ok {
-	// 		return locale
-	// 	}
-	// }
-
 	var (
 		index int
 		ok    bool
 	)
 
+	if contextKey := ctx.Application().ConfigurationReadOnly().GetLanguageContextKey(); contextKey != "" {
+		if v := ctx.Values().GetString(contextKey); v != "" {
+			if v == "default" {
+				index = 0 // no need to call `TryMatchString` and spend time.
+			} else {
+				_, index, _ = i.TryMatchString(v)
+			}
+
+			locale := i.localizer.GetLocale(index)
+			if locale == nil {
+				return nil
+			}
+
+			return locale
+		}
+	}
+
 	if !ok && i.ExtractFunc != nil {
 		if v := i.ExtractFunc(ctx); v != "" {
 			_, index, ok = i.TryMatchString(v)
-
 		}
 	}
 
