Skip to content

Commit

Permalink
implement #1593 - Read HISTORY.md
Browse files Browse the repository at this point in the history
  • Loading branch information
kataras committed Aug 18, 2020
1 parent 1192e6f commit a491cdf
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 40 deletions.
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
34 changes: 28 additions & 6 deletions _examples/i18n/main.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package main

import (
"fmt"

"github.com/kataras/iris/v12"
)

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.

Expand All @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions _examples/i18n/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]")
}
33 changes: 24 additions & 9 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -1184,15 +1198,16 @@ func DefaultConfiguration() Configuration {
// The request body the size limit
// can be set by the middleware `LimitRequestBodySize`
// or `context#SetMaxRequestBodySize`.
PostMaxMemory: 32 << 20, // 32MB
LocaleContextKey: "iris.locale",
LanguageContextKey: "iris.locale.language",
VersionContextKey: "iris.api.version",
ViewEngineContextKey: "iris.view.engine",
ViewLayoutContextKey: "iris.view.layout",
ViewDataContextKey: "iris.view.data",
RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false,
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",
ViewDataContextKey: "iris.view.data",
RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{
{
Start: net.ParseIP("10.0.0.0"),
Expand Down
2 changes: 2 additions & 0 deletions context/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 6 additions & 3 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

Expand Down
12 changes: 9 additions & 3 deletions context/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
98 changes: 79 additions & 19 deletions i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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".
Expand Down Expand Up @@ -115,9 +137,10 @@ func (i *I18n) Reset(loader Loader, languages ...string) error {

i.loader = loader
i.matcher = &Matcher{
strict: len(tags) > 0,
Languages: tags,
matcher: language.NewMatcher(tags),
strict: len(tags) > 0,
Languages: tags,
matcher: language.NewMatcher(tags),
defaultMessageFunc: i.DefaultMessageFunc,
}

return i.reload()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand All @@ -321,12 +354,19 @@ const acceptLanguageHeaderKey = "Accept-Language"
// It will return the first registered language if nothing else matched.
func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
var (
index int
ok bool
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 {
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit a491cdf

Please sign in to comment.