1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-17 18:07:01 +00:00

implement #1593 - Read HISTORY.md

updated example: https://github.com/kataras/iris/blob/master/_examples/i18n/main.go#L28-L50
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-08-18 08:05:51 +03:00
parent 1192e6f787
commit a491cdf7ef
9 changed files with 170 additions and 40 deletions

View File

@@ -359,6 +359,8 @@ Response:
Other Improvements:
- Implement feature request [Log when I18n Translation Fails?](https://github.com/kataras/iris/issues/1593) by using the new `Application.I18n.DefaultMessageFunc` field **before** `I18n.Load`. [Example of usage](https://github.com/kataras/iris/blob/master/_examples/i18n/main.go#L28-L50).
- Fix [#1594](https://github.com/kataras/iris/issues/1594) and add a new `PathAfterHandler` which can be set to true to enable the old behavior (not recommended though).
- New [apps](https://github.com/kataras/iris/tree/master/apps) subpackage. [Example of usage](https://github.com/kataras/iris/tree/master/_examples/routing/subdomains/redirect/multi-instances).

View File

@@ -1,6 +1,8 @@
package main
import (
"fmt"
"github.com/kataras/iris/v12"
)
@@ -8,12 +10,7 @@ func newApp() *iris.Application {
app := iris.New()
// Configure i18n.
// First parameter: Glob filpath patern,
// Second variadic parameter: Optional language tags, the first one is the default/fallback one.
err := app.I18n.Load("./locales/*/*.ini", "en-US", "el-GR", "zh-CN")
if err != nil {
panic(err)
}
//
// app.I18n.Subdomain = false to disable resolve lang code from subdomain.
// app.I18n.LoadAssets for go-bindata.
@@ -27,6 +24,31 @@ func newApp() *iris.Application {
//
// See `app.I18n.ExtractFunc = func(ctx iris.Context) string` or
// `ctx.SetLanguage(langCode string)` to change the extracted language from a request.
//
// Use DefaultMessageFunc to customize the return value of a not found key or lang.
// All language inputs fallback to the default locale if not matched.
// This is why this one accepts both input and matched languages,
// so the caller can be more expressful knowing those.
// Defaults to nil.
app.I18n.DefaultMessageFunc = func(langInput, langMatched, key string, args ...interface{}) string {
msg := fmt.Sprintf("user language input: %s: matched as: %s: not found key: %s: args: %v", langInput, langMatched, key, args)
app.Logger().Warn(msg)
return msg
}
// Load i18n when customizations are set in place.
//
// First parameter: Glob filpath patern,
// Second variadic parameter: Optional language tags, the first one is the default/fallback one.
err := app.I18n.Load("./locales/*/*.ini", "en-US", "el-GR", "zh-CN")
if err != nil {
panic(err)
}
app.Get("/not-matched", func(ctx iris.Context) {
text := ctx.Tr("not_found_key", "some", "values", 42)
ctx.WriteString(text)
// user language input: en-gb: matched as: en-US: not found key: not_found_key: args: [some values 42]
})
app.Get("/", func(ctx iris.Context) {
hi := ctx.Tr("hi", "iris")

View File

@@ -83,4 +83,6 @@ func TestI18n(t *testing.T) {
e.GET("/el-templates").Expect().Status(httptest.StatusNotFound)
e.GET("/el/templates").Expect().Status(httptest.StatusOK).Body().Contains(elGR).Contains(zhCN)
e.GET("/not-matched").WithQuery("lang", "en-gb").Expect().Status(httptest.StatusOK).Body().Equal("user language input: en-gb: matched as: en-US: not found key: not_found_key: args: [some values 42]")
}

View File

@@ -752,6 +752,11 @@ type Configuration struct {
// 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"`
// LanguageInputContextKey is the context key of a language that is given by the end-user.
// It's the real user input of the language string, matched or not.
//
// Defaults to "iris.locale.language.input".
LanguageInputContextKey string `json:"languageInputContextKey,omitempty" yaml:"LanguageInputContextKey" toml:"LanguageInputContextKey"`
// VersionContextKey is the context key which an API Version can be modified
// via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`.
// Defaults to "iris.api.version".
@@ -952,6 +957,11 @@ func (c Configuration) GetLanguageContextKey() string {
return c.LanguageContextKey
}
// GetLanguageInputContextKey returns the LanguageInputContextKey field.
func (c Configuration) GetLanguageInputContextKey() string {
return c.LanguageInputContextKey
}
// GetVersionContextKey returns the VersionContextKey field.
func (c Configuration) GetVersionContextKey() string {
return c.VersionContextKey
@@ -1107,6 +1117,10 @@ func WithConfiguration(c Configuration) Configurator {
main.LanguageContextKey = v
}
if v := c.LanguageInputContextKey; v != "" {
main.LanguageInputContextKey = v
}
if v := c.VersionContextKey; v != "" {
main.VersionContextKey = v
}
@@ -1187,6 +1201,7 @@ func DefaultConfiguration() Configuration {
PostMaxMemory: 32 << 20, // 32MB
LocaleContextKey: "iris.locale",
LanguageContextKey: "iris.locale.language",
LanguageInputContextKey: "iris.locale.language.input",
VersionContextKey: "iris.api.version",
ViewEngineContextKey: "iris.view.engine",
ViewLayoutContextKey: "iris.view.layout",

View File

@@ -49,6 +49,8 @@ type ConfigurationReadOnly interface {
GetLocaleContextKey() string
// GetLanguageContextKey returns the LanguageContextKey field.
GetLanguageContextKey() string
// GetLanguageInputContextKey returns the LanguageInputContextKey field.
GetLanguageInputContextKey() string
// GetVersionContextKey returns the VersionContextKey field.
GetVersionContextKey() string

View File

@@ -1203,6 +1203,7 @@ func (ctx *Context) SetLanguage(langCode string) {
}
// GetLocale returns the current request's `Locale` found by i18n middleware.
// It always fallbacks to the default one.
// See `Tr` too.
func (ctx *Context) GetLocale() Locale {
// Cache the Locale itself for multiple calls of `Tr` method.
@@ -1225,11 +1226,13 @@ func (ctx *Context) GetLocale() Locale {
// See `GetLocale` too.
//
// Example: https://github.com/kataras/iris/tree/master/_examples/i18n
func (ctx *Context) Tr(message string, values ...interface{}) string { // other name could be: Localize.
if locale := ctx.GetLocale(); locale != nil { // TODO: here... I need to change the logic, if not found then call the i18n's get locale and set the value in order to be fastest on routes that are not using (no need to reigster a middleware.)
return locale.GetMessage(message, values...)
func (ctx *Context) Tr(message string, values ...interface{}) string {
if locale := ctx.GetLocale(); locale != nil {
return locale.GetMessageContext(ctx, message, values...)
}
// This should never happen as the locale fallbacks to
// the default.
return message
}

View File

@@ -10,8 +10,8 @@ type I18nReadOnly interface {
Tr(lang string, format string, args ...interface{}) string
}
// Locale is the interface which returns from a `Localizer.GetLocale` metod.
// It serves the transltions based on "key" or format. See `GetMessage`.
// Locale is the interface which returns from a `Localizer.GetLocale` method.
// It serves the translations based on "key" or format. See `GetMessage`.
type Locale interface {
// Index returns the current locale index from the languages list.
Index() int
@@ -23,6 +23,12 @@ type Locale interface {
//
// Same as `Tag().String()` but it's static.
Language() string
// GetMessage should return translated text based n the given "key".
// GetMessage should return translated text based on the given "key".
GetMessage(key string, args ...interface{}) string
// GetMessageContext same as GetMessage
// but it accepts the Context as its first input.
// If DefaultMessageFunc was not nil then this Context
// will provide the real language input instead of the locale's which
// may be the default language one.
GetMessageContext(ctx *Context, key string, args ...interface{}) string
}

View File

@@ -29,6 +29,19 @@ type (
// It may return the default language if nothing else matches based on custom localizer's criteria.
GetLocale(index int) context.Locale
}
// MessageFunc is the function type to modify the behavior when a key or language was not found.
// All language inputs fallback to the default locale if not matched.
// This is why this signature accepts both input and matched languages, so caller
// can provide better messages.
//
// The first parameter is set to the client real input of the language,
// the second one is set to the matched language (default one if input wasn't matched)
// and the third and forth are the translation format/key and its optional arguments.
//
// Note: we don't accept the Context here because Tr method and template func {{ tr }}
// have no direct access to it.
MessageFunc func(langInput, langMatched, key string, args ...interface{}) string
)
// I18n is the structure which keeps the i18n configuration and implements localization and internationalization features.
@@ -43,6 +56,15 @@ type I18n struct {
// to extract the language tag name.
// Defaults to nil.
ExtractFunc func(ctx *context.Context) string
// DefaultMessageFunc is the field which can be used
// to modify the behavior when a key or language was not found.
// All language inputs fallback to the default locale if not matched.
// This is why this one accepts both input and matched languages,
// so the caller can be more expressful knowing those.
//
// Defaults to nil.
DefaultMessageFunc MessageFunc
// If not empty, it is language identifier by url query.
//
// Defaults to "lang".
@@ -118,6 +140,7 @@ func (i *I18n) Reset(loader Loader, languages ...string) error {
strict: len(tags) > 0,
Languages: tags,
matcher: language.NewMatcher(tags),
defaultMessageFunc: i.DefaultMessageFunc,
}
return i.reload()
@@ -193,6 +216,8 @@ type Matcher struct {
strict bool
Languages []language.Tag
matcher language.Matcher
// defaultMessageFunc passed by the i18n structure.
defaultMessageFunc MessageFunc
}
var _ language.Matcher = (*Matcher)(nil)
@@ -295,24 +320,32 @@ func (i *I18n) TryMatchString(s string) (language.Tag, int, bool) {
// Tr returns a translated message based on the "lang" language code
// and its key(format) with any optional arguments attached to it.
//
// It returns an empty string if "format" not matched.
func (i *I18n) Tr(lang, format string, args ...interface{}) string {
// It returns an empty string if "lang" not matched, unless DefaultMessageFunc.
// It returns the default language's translation if "key" not matched, unless DefaultMessageFunc.
func (i *I18n) Tr(lang, format string, args ...interface{}) (msg string) {
_, index, ok := i.TryMatchString(lang)
if !ok {
index = 0
}
langMatched := ""
loc := i.localizer.GetLocale(index)
if loc != nil {
msg := loc.GetMessage(format, args...)
if msg == "" && !i.Strict && index > 0 {
langMatched = loc.Language()
msg = loc.GetMessage(format, args...)
if msg == "" && i.DefaultMessageFunc == nil && !i.Strict && index > 0 {
// it's not the default/fallback language and not message found for that lang:key.
return i.localizer.GetLocale(0).GetMessage(format, args...)
msg = i.localizer.GetLocale(0).GetMessage(format, args...)
}
return msg
}
return ""
if msg == "" && i.DefaultMessageFunc != nil {
msg = i.DefaultMessageFunc(lang, langMatched, format, args)
}
return
}
const acceptLanguageHeaderKey = "Accept-Language"
@@ -323,10 +356,17 @@ func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
var (
index int
ok bool
extractedLang string
)
languageInputKey := ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey()
if contextKey := ctx.Application().ConfigurationReadOnly().GetLanguageContextKey(); contextKey != "" {
if v := ctx.Values().GetString(contextKey); v != "" {
if languageInputKey != "" {
ctx.Values().Set(languageInputKey, v)
}
if v == "default" {
index = 0 // no need to call `TryMatchString` and spend time.
} else {
@@ -344,30 +384,35 @@ func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
if !ok && i.ExtractFunc != nil {
if v := i.ExtractFunc(ctx); v != "" {
extractedLang = v
_, index, ok = i.TryMatchString(v)
}
}
if !ok && i.URLParameter != "" {
if v := ctx.URLParam(i.URLParameter); v != "" {
extractedLang = v
_, index, ok = i.TryMatchString(v)
}
}
if !ok && i.Cookie != "" {
if v := ctx.GetCookie(i.Cookie); v != "" {
extractedLang = v
_, index, ok = i.TryMatchString(v) // url.QueryUnescape(cookie.Value)
}
}
if !ok && i.Subdomain {
if v := ctx.Subdomain(); v != "" {
extractedLang = v
_, index, ok = i.TryMatchString(v)
}
}
if !ok {
if v := ctx.GetHeader(acceptLanguageHeaderKey); v != "" {
extractedLang = v // note.
desired, _, err := language.ParseAcceptLanguage(v)
if err == nil {
if _, idx, conf := i.matcher.Match(desired...); conf > language.Low {
@@ -380,8 +425,14 @@ func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
// locale := i.localizer.GetLocale(index)
// ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetLocaleContextKey(), locale)
// // if 0 then it defaults to the first language.
// return locale
if languageInputKey != "" {
// Set the user input we wanna use it on DefaultMessageFunc.
// Even if matched because it may be en-gb or en but if there is a language registered
// as en-us it will be successfully matched ( see TrymatchString and Low conf).
ctx.Values().Set(languageInputKey, extractedLang)
}
// if index == 0 then it defaults to the first language.
locale := i.localizer.GetLocale(index)
if locale == nil {
return nil
@@ -391,18 +442,27 @@ func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
}
// GetMessage returns the localized text message for this "r" request based on the key "format".
// It returns an empty string if locale or format not found.
func (i *I18n) GetMessage(ctx *context.Context, format string, args ...interface{}) string {
// It returns an empty string if context's locale not matched, unless DefaultMessageFunc.
// It returns the default language's translation if "key" not matched, unless DefaultMessageFunc.
func (i *I18n) GetMessage(ctx *context.Context, format string, args ...interface{}) (msg string) {
loc := i.GetLocale(ctx)
langMatched := ""
if loc != nil {
langMatched = loc.Language()
// it's not the default/fallback language and not message found for that lang:key.
msg := loc.GetMessage(format, args...)
if msg == "" && !i.Strict && loc.Index() > 0 {
msg = loc.GetMessage(format, args...)
if msg == "" && i.DefaultMessageFunc == nil && !i.Strict && loc.Index() > 0 {
return i.localizer.GetLocale(0).GetMessage(format, args...)
}
}
return ""
if msg == "" && i.DefaultMessageFunc != nil {
langInput := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey())
msg = i.DefaultMessageFunc(langInput, langMatched, format, args...)
}
return
}
func (i *I18n) setLangWithoutContext(w http.ResponseWriter, r *http.Request, lang string) {

View File

@@ -149,6 +149,8 @@ func load(assetNames []string, asset func(string) ([]byte, error), options ...Lo
templateKeys: templateKeys,
lineKeys: lineKeys,
other: other,
defaultMessageFunc: m.defaultMessageFunc,
}
}
@@ -217,6 +219,8 @@ type defaultLocale struct {
templateKeys map[string]*template.Template
lineKeys map[string]string
other map[string]interface{}
defaultMessageFunc MessageFunc
}
func (l *defaultLocale) Index() int {
@@ -232,6 +236,15 @@ func (l *defaultLocale) Language() string {
}
func (l *defaultLocale) GetMessage(key string, args ...interface{}) string {
return l.getMessage(l.id, key, args...)
}
func (l *defaultLocale) GetMessageContext(ctx *context.Context, key string, args ...interface{}) string {
langInput := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey())
return l.getMessage(langInput, key, args...)
}
func (l *defaultLocale) getMessage(langInput, key string, args ...interface{}) string {
n := len(args)
if n > 0 {
// search on templates.
@@ -254,6 +267,11 @@ func (l *defaultLocale) GetMessage(key string, args ...interface{}) string {
return fmt.Sprintf("%v", v)
}
if l.defaultMessageFunc != nil {
// let langInput to be empty if that's the case.
return l.defaultMessageFunc(langInput, l.id, key, args...)
}
return ""
}