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

New: i18n pluralization and variables support and more...

fixes: #1649, #1648, #1641, #1650

relative to: #1597
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-09-29 19:19:19 +03:00
parent f224ded740
commit 4065819688
63 changed files with 2054 additions and 684 deletions

View File

@@ -11,11 +11,25 @@ import (
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/router"
"github.com/kataras/iris/v12/i18n/internal"
"golang.org/x/text/language"
)
type (
// 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 = internal.MessageFunc
// Loader accepts a `Matcher` and should return a `Localizer`.
// Functions that implement this type should load locale files.
Loader func(m *Matcher) (Localizer, error)
@@ -29,19 +43,6 @@ 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.
@@ -49,7 +50,7 @@ type I18n struct {
localizer Localizer
matcher *Matcher
Loader *LoaderConfig
Loader LoaderConfig
loader Loader
mu sync.Mutex
@@ -106,13 +107,10 @@ func makeTags(languages ...string) (tags []language.Tag) {
}
// New returns a new `I18n` instance. Use its `Load` or `LoadAssets` to load languages.
// Examples at: https://github.com/kataras/iris/tree/master/_examples/i18n.
func New() *I18n {
i := &I18n{
Loader: &LoaderConfig{
Left: "{{",
Right: "}}",
Strict: false,
},
Loader: DefaultLoaderConfig,
URLParameter: "lang",
Subdomain: true,
PathRedirect: true,

5
i18n/internal/aliases.go Normal file
View File

@@ -0,0 +1,5 @@
package internal
// Map is just an alias of the map[string]interface{} type.
// Just like the iris.Map one.
type Map = map[string]interface{}

149
i18n/internal/catalog.go Normal file
View File

@@ -0,0 +1,149 @@
package internal
import (
"fmt"
"text/template"
"github.com/kataras/iris/v12/context"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/message/catalog"
)
// 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.
type MessageFunc func(langInput, langMatched, key string, args ...interface{}) string
// Catalog holds the locales and the variables message storage.
type Catalog struct {
builder *catalog.Builder
Locales []*Locale
}
// The Options of the Catalog and its Locales.
type Options struct {
// Left delimiter for template messages.
Left string
// Right delimeter for template messages.
Right string
// Enable strict mode.
Strict bool
// Optional functions for template messages per locale.
Funcs func(context.Locale) template.FuncMap
// Optional function to be called when no message was found.
DefaultMessageFunc MessageFunc
// Customize the overall behavior of the plurazation feature.
PluralFormDecoder PluralFormDecoder
}
// NewCatalog returns a new Catalog based on the registered languages and the loader options.
func NewCatalog(languages []language.Tag, opts Options) (*Catalog, error) { // ordered languages, the first should be the default one.
if len(languages) == 0 {
return nil, fmt.Errorf("catalog: empty languages")
}
if opts.Left == "" {
opts.Left = "{{"
}
if opts.Right == "" {
opts.Right = "}}"
}
if opts.PluralFormDecoder == nil {
opts.PluralFormDecoder = DefaultPluralFormDecoder
}
builder := catalog.NewBuilder(catalog.Fallback(languages[0]))
locales := make([]*Locale, 0, len(languages))
for idx, tag := range languages {
locale := &Locale{
tag: tag,
index: idx,
ID: tag.String(),
Options: opts,
Printer: message.NewPrinter(tag, message.Catalog(builder)),
Messages: make(map[string]Renderer),
}
locale.FuncMap = getFuncs(locale)
locales = append(locales, locale)
}
c := &Catalog{
builder: builder,
Locales: locales,
}
return c, nil
}
// Set sets a simple translation message.
func (c *Catalog) Set(tag language.Tag, key string, msgs ...catalog.Message) error {
// fmt.Printf("Catalog.Set[%s] %s:\n", tag.String(), key)
// for _, msg := range msgs {
// fmt.Printf("%#+v\n", msg)
// }
return c.builder.Set(tag, key, msgs...)
}
// Store stores the a map of values to the locale derives from the given "langIndex".
func (c *Catalog) Store(langIndex int, kv Map) error {
loc := c.getLocale(langIndex)
if loc == nil {
return fmt.Errorf("expected language index to be lower or equal than %d but got %d", len(c.Locales), langIndex)
}
return loc.Load(c, kv)
}
/* Localizer interface. */
// SetDefault changes the default language based on the "index".
// See `I18n#SetDefault` method for more.
func (c *Catalog) SetDefault(index int) bool {
if index < 0 {
index = 0
}
if maxIdx := len(c.Locales) - 1; index > maxIdx {
return false
}
// callers should protect with mutex if called at serve-time.
loc := c.Locales[index]
loc.index = 0
f := c.Locales[0]
c.Locales[0] = loc
f.index = index
c.Locales[index] = f
return true
}
// GetLocale returns a valid `Locale` based on the "index".
func (c *Catalog) GetLocale(index int) context.Locale {
return c.getLocale(index)
}
func (c *Catalog) getLocale(index int) *Locale {
if index < 0 {
index = 0
}
if maxIdx := len(c.Locales) - 1; index > maxIdx {
// panic("expected language index to be lower or equal than %d but got %d", maxIdx, langIndex)
return nil
}
loc := c.Locales[index]
return loc
}

195
i18n/internal/locale.go Normal file
View File

@@ -0,0 +1,195 @@
package internal
import (
"fmt"
"text/template"
"github.com/kataras/iris/v12/context"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/message/catalog"
)
// Locale is the default Locale.
// Created by Catalog.
// One Locale maps to one registered and loaded language.
// Stores the translation variables and most importantly, the Messages (keys and their renderers).
type Locale struct {
// The index of the language registered by the user, starting from zero.
index int
tag language.Tag
// ID is the tag.String().
ID string
// Options given by the Catalog
Options Options
// Fields set by Catalog.
FuncMap template.FuncMap
Printer *message.Printer
//
// Fields set by this Load method.
Messages map[string]Renderer
Vars []Var // shared per-locale variables.
}
// Ensures that the Locale completes the context.Locale interface.
var _ context.Locale = (*Locale)(nil)
// Load sets the translation messages based on the Catalog's key values.
func (loc *Locale) Load(c *Catalog, keyValues Map) error {
return loc.setMap(c, "", keyValues)
}
func (loc *Locale) setMap(c *Catalog, key string, keyValues Map) error {
// unique locals or the shared ones.
isRoot := key == ""
vars := getVars(loc, VarsKey, keyValues)
if isRoot {
loc.Vars = vars
} else {
vars = removeVarsDuplicates(append(vars, loc.Vars...))
}
for k, v := range keyValues {
form, isPlural := loc.Options.PluralFormDecoder(loc, k)
if isPlural {
k = key
} else if !isRoot {
k = key + "." + k
}
switch value := v.(type) {
case string:
if err := loc.setString(c, k, value, vars, form); err != nil {
return fmt.Errorf("%s:%s parse string: %w", loc.ID, key, err)
}
case Map:
// fmt.Printf("%s is map\n", fullKey)
if err := loc.setMap(c, k, value); err != nil {
return fmt.Errorf("%s:%s parse map: %w", loc.ID, key, err)
}
default:
return fmt.Errorf("%s:%s unexpected type of %T as value", loc.ID, key, value)
}
}
return nil
}
func (loc *Locale) setString(c *Catalog, key string, value string, vars []Var, form PluralForm) (err error) {
isPlural := form != nil
// fmt.Printf("setStringVars: %s=%s\n", key, value)
msgs, vars := makeSelectfVars(value, vars, isPlural)
msgs = append(msgs, catalog.String(value))
m := &Message{
Locale: loc,
Key: key,
Value: value,
Vars: vars,
Plural: isPlural,
}
var (
renderer, pluralRenderer Renderer = m, m
)
if stringIsTemplateValue(value, loc.Options.Left, loc.Options.Right) {
t, err := NewTemplate(c, m)
if err != nil {
return err
}
pluralRenderer = t
if !isPlural {
renderer = t
}
} else {
if isPlural {
pluralRenderer, err = newIndependentPluralRenderer(c, loc, key, msgs...)
if err != nil {
return fmt.Errorf("<%s = %s>: %w", key, value, err)
}
} else {
// let's make normal keys direct fire:
// renderer = &simpleRenderer{key, loc.Printer}
if err = c.Set(loc.tag, key, msgs...); err != nil {
return fmt.Errorf("<%s = %s>: %w", key, value, err)
}
}
}
if isPlural {
if existingMsg, ok := loc.Messages[key]; ok {
if msg, ok := existingMsg.(*Message); ok {
msg.AddPlural(form, pluralRenderer)
return
}
}
m.AddPlural(form, pluralRenderer)
}
loc.Messages[key] = renderer
return
}
/* context.Locale interface */
// Index returns the current locale index from the languages list.
func (loc *Locale) Index() int {
return loc.index
}
// Tag returns the full language Tag attached to this Locale,
// it should be unique across different Locales.
func (loc *Locale) Tag() *language.Tag {
return &loc.tag
}
// Language should return the exact languagecode of this `Locale`
//that the user provided on `New` function.
//
// Same as `Tag().String()` but it's static.
func (loc *Locale) Language() string {
return loc.ID
}
// GetMessage should return translated text based on the given "key".
func (loc *Locale) GetMessage(key string, args ...interface{}) string {
return loc.getMessage(loc.ID, key, args...)
}
// 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.
func (loc *Locale) GetMessageContext(ctx *context.Context, key string, args ...interface{}) string {
langInput := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey())
return loc.getMessage(langInput, key, args...)
}
func (loc *Locale) getMessage(langInput, key string, args ...interface{}) string {
if msg, ok := loc.Messages[key]; ok {
result, err := msg.Render(args...)
if err != nil {
result = err.Error()
}
return result
}
if fn := loc.Options.DefaultMessageFunc; fn != nil {
// let langInput to be empty if that's the case.
return fn(langInput, loc.ID, key, args...)
}
return ""
}

81
i18n/internal/message.go Normal file
View File

@@ -0,0 +1,81 @@
package internal
import (
"fmt"
"sort"
)
// Renderer is responsible to render a translation based
// on the given "args".
type Renderer interface {
Render(args ...interface{}) (string, error)
}
// Message is the default Renderer for translation messages.
// Holds the variables and the plurals of this key.
// Each Locale has its own list of messages.
type Message struct {
Locale *Locale
Key string
Value string
Plural bool
Plurals []*PluralMessage // plural forms by order.
Vars []Var
}
// AddPlural adds a plural message to the Plurals list.
func (m *Message) AddPlural(form PluralForm, r Renderer) {
msg := &PluralMessage{
Form: form,
Renderer: r,
}
if len(m.Plurals) == 0 {
m.Plural = true
m.Plurals = append(m.Plurals, msg)
return
}
for i, p := range m.Plurals {
if p.Form.String() == form.String() {
// replace
m.Plurals[i] = msg
return
}
}
m.Plurals = append(m.Plurals, msg)
sort.SliceStable(m.Plurals, func(i, j int) bool {
return m.Plurals[i].Form.Less(m.Plurals[j].Form)
})
}
// Render completes the Renderer interface.
// It accepts arguments, which can resolve the pluralization type of the message
// and its variables. If the Message is wrapped by a Template then the
// first argument should be a map. The map key resolves to the pluralization
// of the message is the "PluralCount". And for variables the user
// should set a message key which looks like: %VAR_NAME%Count, e.g. "DogsCount"
// to set plural count for the "Dogs" variable, case-sensitive.
func (m *Message) Render(args ...interface{}) (string, error) {
if m.Plural {
if len(args) > 0 {
if pluralCount, ok := findPluralCount(args[0]); ok {
for _, plural := range m.Plurals {
if plural.Form.MatchPlural(pluralCount) {
return plural.Renderer.Render(args...)
}
}
return "", fmt.Errorf("key: %q: no registered plurals for <%d>", m.Key, pluralCount)
}
}
return "", fmt.Errorf("key: %q: missing plural count argument", m.Key)
}
return m.Locale.Printer.Sprintf(m.Key, args...), nil
}

261
i18n/internal/plural.go Normal file
View File

@@ -0,0 +1,261 @@
package internal
import (
"strconv"
"github.com/kataras/iris/v12/context"
"golang.org/x/text/feature/plural"
"golang.org/x/text/message"
"golang.org/x/text/message/catalog"
)
// PluralCounter if completes by an input argument of a message to render,
// then the plural renderer will resolve the plural count
// and any variables' counts. This is useful when the data is not a type of Map or integers.
type PluralCounter interface {
// PluralCount returns the plural count of the message.
// If returns -1 then this is not a valid plural message.
PluralCount() int
// VarCount should return the variable count, based on the variable name.
VarCount(name string) int
}
// PluralMessage holds the registered Form and the corresponding Renderer.
// It is used on the `Message.AddPlural` method.
type PluralMessage struct {
Form PluralForm
Renderer Renderer
}
type independentPluralRenderer struct {
key string
printer *message.Printer
}
func newIndependentPluralRenderer(c *Catalog, loc *Locale, key string, msgs ...catalog.Message) (Renderer, error) {
builder := catalog.NewBuilder(catalog.Fallback(c.Locales[0].tag))
if err := builder.Set(loc.tag, key, msgs...); err != nil {
return nil, err
}
printer := message.NewPrinter(loc.tag, message.Catalog(builder))
return &independentPluralRenderer{key, printer}, nil
}
func (m *independentPluralRenderer) Render(args ...interface{}) (string, error) {
return m.printer.Sprintf(m.key, args...), nil
}
// A PluralFormDecoder should report and return whether
// a specific "key" is a plural one. This function
// can be implemented and set on the `Options` to customize
// the plural forms and their behavior in general.
//
// See the `DefaultPluralFormDecoder` package-level
// variable for the default implementation one.
type PluralFormDecoder func(loc context.Locale, key string) (PluralForm, bool)
// DefaultPluralFormDecoder is the default `PluralFormDecoder`.
// Supprots "zero", "one", "two", "other", "=x", "<x", ">x".
var DefaultPluralFormDecoder = func(_ context.Locale, key string) (PluralForm, bool) {
if isDefaultPluralForm(key) {
return pluralForm(key), true
}
return nil, false
}
func isDefaultPluralForm(s string) bool {
switch s {
case "zero", "one", "two", "other":
return true
default:
if len(s) > 1 {
ch := s[0]
if ch == '=' || ch == '<' || ch == '>' {
if isDigit(s[1]) {
return true
}
}
}
return false
}
}
// A PluralForm is responsible to decode
// locale keys to plural forms and match plural forms
// based on the given pluralCount.
//
// See `pluralForm` package-level type for a default implementation.
type PluralForm interface {
String() string
// the string is a verified plural case's raw string value.
// Field for priority on which order to register the plural cases.
Less(next PluralForm) bool
MatchPlural(pluralCount int) bool
}
type pluralForm string
func (f pluralForm) String() string {
return string(f)
}
func (f pluralForm) Less(next PluralForm) bool {
form1 := f.String()
form2 := next.String()
// Order by
// - equals,
// - less than
// - greater than
// - "zero", "one", "two"
// - rest is last "other".
dig1, typ1, hasDig1 := formAtoi(form1)
if typ1 == eq {
return true
}
dig2, typ2, hasDig2 := formAtoi(form2)
if typ2 == eq {
return false
}
// digits smaller, number.
if hasDig1 {
return !hasDig2 || dig1 < dig2
}
if hasDig2 {
return false
}
if form1 == "other" {
return false // other go to last.
}
if form2 == "other" {
return true
}
if form1 == "zero" {
return true
}
if form2 == "zero" {
return false
}
if form1 == "one" {
return true
}
if form2 == "one" {
return false
}
if form1 == "two" {
return true
}
if form2 == "two" {
return false
}
return false
}
func (f pluralForm) MatchPlural(pluralCount int) bool {
switch f {
case "other":
return true
case "=0", "zero":
return pluralCount == 0
case "=1", "one":
return pluralCount == 1
case "=2", "two":
return pluralCount == 2
default:
// <5 or =5
n, typ, ok := formAtoi(string(f))
if !ok {
return false
}
switch typ {
case eq:
return n == pluralCount
case lt:
return pluralCount < n
case gt:
return pluralCount > n
default:
return false
}
}
}
func makeSelectfVars(text string, vars []Var, insidePlural bool) ([]catalog.Message, []Var) {
newVars := sortVars(text, vars)
newVars = removeVarsDuplicates(newVars)
msgs := selectfVars(newVars, insidePlural)
return msgs, newVars
}
func selectfVars(vars []Var, insidePlural bool) []catalog.Message {
msgs := make([]catalog.Message, 0, len(vars))
for _, variable := range vars {
argth := variable.Argth
if insidePlural {
argth++
}
msg := catalog.Var(variable.Name, plural.Selectf(argth, variable.Format, variable.Cases...))
// fmt.Printf("%s:%d | cases | %#+v\n", variable.Name, variable.Argth, variable.Cases)
msgs = append(msgs, msg)
}
return msgs
}
const (
eq uint8 = iota + 1
lt
gt
)
func formType(ch byte) uint8 {
switch ch {
case '=':
return eq
case '<':
return lt
case '>':
return gt
}
return 0
}
func formAtoi(form string) (int, uint8, bool) {
if len(form) < 2 {
return -1, 0, false
}
typ := formType(form[0])
if typ == 0 {
return -1, 0, false
}
dig, err := strconv.Atoi(form[1:])
if err != nil {
return -1, 0, false
}
return dig, typ, true
}
func isDigit(ch byte) bool {
return '0' <= ch && ch <= '9'
}

242
i18n/internal/template.go Normal file
View File

@@ -0,0 +1,242 @@
package internal
import (
"bytes"
"fmt"
"strconv"
"strings"
"sync"
"text/template"
"golang.org/x/text/message/catalog"
)
const (
// VarsKey is the key for the message's variables, per locale(global) or per key (local).
VarsKey = "Vars"
// PluralCountKey is the key for the template's message pluralization.
PluralCountKey = "PluralCount"
// VarCountKeySuffix is the key suffix for the template's variable's pluralization,
// e.g. HousesCount for ${Houses}.
VarCountKeySuffix = "Count"
// VarsKeySuffix is the key which the template message's variables
// are stored with,
// e.g. welcome.human.other_vars
VarsKeySuffix = "_vars"
)
// Template is a Renderer which renders template messages.
type Template struct {
*Message
tmpl *template.Template
bufPool *sync.Pool
}
// NewTemplate returns a new Template message based on the
// catalog and the base translation Message. See `Locale.Load` method.
func NewTemplate(c *Catalog, m *Message) (*Template, error) {
tmpl, err := template.New(m.Key).
Delims(m.Locale.Options.Left, m.Locale.Options.Right).
Funcs(m.Locale.FuncMap).
Parse(m.Value)
if err != nil {
return nil, err
}
if err := registerTemplateVars(c, m); err != nil {
return nil, fmt.Errorf("template vars: <%s = %s>: %w", m.Key, m.Value, err)
}
bufPool := &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
t := &Template{
Message: m,
tmpl: tmpl,
bufPool: bufPool,
}
return t, nil
}
func registerTemplateVars(c *Catalog, m *Message) error {
if len(m.Vars) == 0 {
return nil
}
msgs := selectfVars(m.Vars, false)
variableText := ""
for _, variable := range m.Vars {
variableText += variable.Literal + " "
}
variableText = variableText[0 : len(variableText)-1]
fullKey := m.Key + "." + VarsKeySuffix
return c.Set(m.Locale.tag, fullKey, append(msgs, catalog.String(variableText))...)
}
// Render completes the Renderer interface.
// It renders a template message.
// Each key has its own Template, plurals too.
func (t *Template) Render(args ...interface{}) (string, error) {
var (
data interface{}
result string
)
argsLength := len(args)
if argsLength > 0 {
data = args[0]
}
buf := t.bufPool.Get().(*bytes.Buffer)
buf.Reset()
if err := t.tmpl.Execute(buf, data); err != nil {
t.bufPool.Put(buf)
return "", err
}
result = buf.String()
t.bufPool.Put(buf)
if len(t.Vars) > 0 {
// get the variables plurals.
if argsLength > 1 {
// if has more than the map/struct
// then let's assume the user passes variable counts by raw integer arguments.
args = args[1:]
} else if data != nil {
// otherwise try to resolve them by the map(%var_name%Count)/struct(PlrualCounter).
args = findVarsCount(data, t.Vars)
}
result = t.replaceTmplVars(result, args...)
}
return result, nil
}
func findVarsCount(data interface{}, vars []Var) (args []interface{}) {
if data == nil {
return nil
}
switch dataValue := data.(type) {
case PluralCounter:
for _, v := range vars {
if count := dataValue.VarCount(v.Name); count >= 0 {
args = append(args, count)
}
}
case Map:
for _, v := range vars {
varCountKey := v.Name + VarCountKeySuffix
if value, ok := dataValue[varCountKey]; ok {
args = append(args, value)
}
}
case map[string]string:
for _, v := range vars {
varCountKey := v.Name + VarCountKeySuffix
if value, ok := dataValue[varCountKey]; ok {
if count, err := strconv.Atoi(value); err == nil {
args = append(args, count)
}
}
}
case map[string]int:
for _, v := range vars {
varCountKey := v.Name + VarCountKeySuffix
if value, ok := dataValue[varCountKey]; ok {
args = append(args, value)
}
}
default:
return nil
}
return
}
func findPluralCount(data interface{}) (int, bool) {
if data == nil {
return -1, false
}
switch dataValue := data.(type) {
case PluralCounter:
if count := dataValue.PluralCount(); count >= 0 {
return count, true
}
case Map:
if v, ok := dataValue[PluralCountKey]; ok {
if count, ok := v.(int); ok {
return count, true
}
}
case map[string]string:
if v, ok := dataValue[PluralCountKey]; ok {
count, err := strconv.Atoi(v)
if err != nil {
return -1, false
}
return count, true
}
case map[string]int:
if count, ok := dataValue[PluralCountKey]; ok {
return count, true
}
case int:
return dataValue, true // when this is not a template data, the caller's argument should be args[1:] now.
case int64:
count := int(dataValue)
return count, true
}
return -1, false
}
func (t *Template) replaceTmplVars(result string, args ...interface{}) string {
varsKey := t.Key + "." + VarsKeySuffix
translationVarsText := t.Locale.Printer.Sprintf(varsKey, args...)
if translationVarsText != "" {
translatioVars := strings.Split(translationVarsText, " ")
for i, variable := range t.Vars {
result = strings.Replace(result, variable.Literal, translatioVars[i], 1)
}
}
return result
}
func stringIsTemplateValue(value, left, right string) bool {
leftIdx, rightIdx := strings.Index(value, left), strings.Index(value, right)
return leftIdx != -1 && rightIdx > leftIdx
}
func getFuncs(loc *Locale) template.FuncMap {
// set the template funcs for this locale.
funcs := template.FuncMap{
"tr": loc.GetMessage,
}
if getFuncs := loc.Options.Funcs; getFuncs != nil {
// set current locale's template's funcs.
for k, v := range getFuncs(loc) {
funcs[k] = v
}
}
return funcs
}

182
i18n/internal/var.go Normal file
View File

@@ -0,0 +1,182 @@
package internal
import (
"reflect"
"regexp"
"sort"
"golang.org/x/text/message/catalog"
)
// Var represents a message variable.
// The variables, like the sub messages are sorted.
// First: plurals (which again, are sorted)
// and then any custom keys.
// In variables, the sorting depends on the exact
// order the associated message uses the variables.
// This is extremely handy.
// This package requires the golang.org/x/text/message capabilities
// only for the variables feature, the message itself's pluralization is managed by the package.
type Var struct {
Name string // Variable name, e.g. Name
Literal string // Its literal is ${Name}
Cases []interface{} // one:...,few:...,...
Format string // defaults to "%d".
Argth int // 1, 2, 3...
}
func getVars(loc *Locale, key string, src map[string]interface{}) []Var {
if len(src) == 0 {
return nil
}
varsKey, ok := src[key]
if !ok {
return nil
}
varValue, ok := varsKey.([]interface{})
if !ok {
return nil
}
vars := make([]Var, 0, len(varValue))
for _, v := range varValue {
m, ok := v.(map[string]interface{})
if !ok {
continue
}
for k, inner := range m {
varFormat := "%d"
innerMap, ok := inner.(map[string]interface{})
if !ok {
continue
}
for kk, vv := range innerMap {
if kk == "format" {
if format, ok := vv.(string); ok {
varFormat = format
}
break
}
}
cases := getCases(loc, innerMap)
if len(cases) > 0 {
// cases = sortCases(cases)
vars = append(vars, Var{
Name: k,
Literal: "${" + k + "}",
Cases: cases,
Format: varFormat,
Argth: 1,
})
}
}
}
delete(src, key) // delete the key after.
return vars
}
var unescapeVariableRegex = regexp.MustCompile("\\$\\{(.*?)}")
func sortVars(text string, vars []Var) (newVars []Var) {
argth := 1
for _, submatches := range unescapeVariableRegex.FindAllStringSubmatch(text, -1) {
name := submatches[1]
for _, variable := range vars {
if variable.Name == name {
variable.Argth = argth
newVars = append(newVars, variable)
argth++
break
}
}
}
sort.SliceStable(newVars, func(i, j int) bool {
return newVars[i].Argth < newVars[j].Argth
})
return
}
// it will panic if the incoming "elements" are not catmsg.Var (internal text package).
func removeVarsDuplicates(elements []Var) (result []Var) {
seen := make(map[string]struct{})
for v := range elements {
variable := elements[v]
name := variable.Name
if _, ok := seen[name]; !ok {
seen[name] = struct{}{}
result = append(result, variable)
}
}
return result
}
func removeMsgVarsDuplicates(elements []catalog.Message) (result []catalog.Message) {
seen := make(map[string]struct{})
for _, elem := range elements {
val := reflect.Indirect(reflect.ValueOf(elem))
if val.Type().String() != "catmsg.Var" {
// keep.
result = append(result, elem)
continue // it's not a var.
}
name := val.FieldByName("Name").Interface().(string)
if _, ok := seen[name]; !ok {
seen[name] = struct{}{}
result = append(result, elem)
}
}
return
}
func getCases(loc *Locale, src map[string]interface{}) []interface{} {
type PluralCase struct {
Form PluralForm
Value interface{}
}
pluralCases := make([]PluralCase, 0, len(src))
for key, value := range src {
form, ok := loc.Options.PluralFormDecoder(loc, key)
if !ok {
continue
}
pluralCases = append(pluralCases, PluralCase{
Form: form,
Value: value,
})
}
if len(pluralCases) == 0 {
return nil
}
sort.SliceStable(pluralCases, func(i, j int) bool {
left, right := pluralCases[i].Form, pluralCases[j].Form
return left.Less(right)
})
cases := make([]interface{}, 0, len(pluralCases)*2)
for _, pluralCase := range pluralCases {
// fmt.Printf("%s=%v\n", pluralCase.Form, pluralCase.Value)
cases = append(cases, pluralCase.Form.String())
cases = append(cases, pluralCase.Value)
}
return cases
}

View File

@@ -1,64 +1,24 @@
package i18n
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"sync"
"text/template"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/i18n/internal"
"github.com/BurntSushi/toml"
"golang.org/x/text/language"
"gopkg.in/ini.v1"
"gopkg.in/yaml.v3"
)
// LoaderConfig is an optional configuration structure which contains
// LoaderConfig the configuration structure which contains
// some options about how the template loader should act.
//
// See `Glob` and `Assets` package-level functions.
type (
LoaderConfig struct {
// Template delimiters, defaults to {{ }}.
Left, Right string
// Template functions map per locale, defaults to nil.
Funcs func(context.Locale) template.FuncMap
// If true then it will return error on invalid templates instead of moving them to simple string-line keys.
// Also it will report whether the registered languages matched the loaded ones.
// Defaults to false.
Strict bool
}
// LoaderOption is a type which accepts a pointer to `LoaderConfig`
// and can be optionally passed to the second
// variadic input argument of the `Glob` and `Assets` functions.
LoaderOption interface {
Apply(*LoaderConfig)
}
)
// Apply implements the `LoaderOption` interface.
func (c *LoaderConfig) Apply(cfg *LoaderConfig) {
if c.Left != "" {
cfg.Left = c.Left
}
if c.Right != "" {
cfg.Right = c.Right
}
if c.Funcs != nil {
cfg.Funcs = c.Funcs
}
if c.Strict {
cfg.Strict = true
}
}
type LoaderConfig = internal.Options
// Glob accepts a glob pattern (see: https://golang.org/pkg/path/filepath/#Glob)
// and loads the locale files based on any "options".
@@ -67,13 +27,13 @@ func (c *LoaderConfig) Apply(cfg *LoaderConfig) {
// search and load for locale files.
//
// See `New` and `LoaderConfig` too.
func Glob(globPattern string, options ...LoaderOption) Loader {
func Glob(globPattern string, options LoaderConfig) Loader {
assetNames, err := filepath.Glob(globPattern)
if err != nil {
panic(err)
}
return load(assetNames, ioutil.ReadFile, options...)
return load(assetNames, ioutil.ReadFile, options)
}
// Assets accepts a function that returns a list of filenames (physical or virtual),
@@ -82,8 +42,18 @@ func Glob(globPattern string, options ...LoaderOption) Loader {
// It returns a valid `Loader` which loads and maps the locale files.
//
// See `Glob`, `Assets`, `New` and `LoaderConfig` too.
func Assets(assetNames func() []string, asset func(string) ([]byte, error), options ...LoaderOption) Loader {
return load(assetNames(), asset, options...)
func Assets(assetNames func() []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
return load(assetNames(), asset, options)
}
// DefaultLoaderConfig represents the default loader configuration.
var DefaultLoaderConfig = LoaderConfig{
Left: "{{",
Right: "}}",
Strict: false,
DefaultMessageFunc: nil,
PluralFormDecoder: internal.DefaultPluralFormDecoder,
Funcs: nil,
}
// load accepts a list of filenames (physical or virtual),
@@ -92,24 +62,21 @@ func Assets(assetNames func() []string, asset func(string) ([]byte, error), opti
// It returns a valid `Loader` which loads and maps the locale files.
//
// See `Glob`, `Assets` and `LoaderConfig` too.
func load(assetNames []string, asset func(string) ([]byte, error), options ...LoaderOption) Loader {
var c = LoaderConfig{
Left: "{{",
Right: "}}",
Strict: false,
}
for _, opt := range options {
opt.Apply(&c)
}
func load(assetNames []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
return func(m *Matcher) (Localizer, error) {
languageFiles, err := m.ParseLanguageFiles(assetNames)
if err != nil {
return nil, err
}
locales := make(MemoryLocalizer)
if options.DefaultMessageFunc == nil {
options.DefaultMessageFunc = m.defaultMessageFunc
}
cat, err := internal.NewCatalog(m.Languages, options)
if err != nil {
return nil, err
}
for langIndex, langFiles := range languageFiles {
keyValues := make(map[string]interface{})
@@ -137,213 +104,22 @@ func load(assetNames []string, asset func(string) ([]byte, error), options ...Lo
}
}
var (
templateKeys = make(map[string]*template.Template)
lineKeys = make(map[string]string)
other = make(map[string]interface{})
)
t := m.Languages[langIndex]
locale := &defaultLocale{
index: langIndex,
id: t.String(),
tag: &t,
templateKeys: templateKeys,
lineKeys: lineKeys,
other: other,
defaultMessageFunc: m.defaultMessageFunc,
err = cat.Store(langIndex, keyValues)
if err != nil {
return nil, err
}
var longestValueLength int
for k, v := range keyValues {
// fmt.Printf("[%d] %s = %v of type: [%T]\n", langIndex, k, v, v)
switch value := v.(type) {
case string:
if leftIdx, rightIdx := strings.Index(value, c.Left), strings.Index(value, c.Right); leftIdx != -1 && rightIdx > leftIdx {
// we assume it's template?
// each file:line has its own template funcs so,
// just map it.
// builtin funcs.
funcs := template.FuncMap{
"tr": locale.GetMessage,
}
if c.Funcs != nil {
// set current locale's template's funcs.
for k, v := range c.Funcs(locale) {
funcs[k] = v
}
}
t, err := template.New(k).Delims(c.Left, c.Right).Funcs(funcs).Parse(value)
if err == nil {
templateKeys[k] = t
} else if c.Strict {
return nil, err
}
if valueLength := len(value); valueLength > longestValueLength {
longestValueLength = valueLength
}
}
lineKeys[k] = value
default:
other[k] = v
}
}
// pre-allocate the initial internal buffer.
// Note that Reset should be called immediately.
initBuf := []byte(strings.Repeat("x", longestValueLength))
locale.tmplBufPool = &sync.Pool{
New: func() interface{} {
// try to eliminate the internal "grow" method as much as possible.
return bytes.NewBuffer(initBuf)
},
}
locales[langIndex] = locale
}
if n := len(locales); n == 0 {
if n := len(cat.Locales); n == 0 {
return nil, fmt.Errorf("locales not found in %s", strings.Join(assetNames, ", "))
} else if c.Strict && n < len(m.Languages) {
} else if options.Strict && n < len(m.Languages) {
return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n)
}
return locales, nil
return cat, nil
}
}
// MemoryLocalizer is a map which implements the `Localizer`.
type MemoryLocalizer map[int]context.Locale
// GetLocale returns a valid `Locale` based on the "index".
func (l MemoryLocalizer) GetLocale(index int) context.Locale {
// loc, ok := l[index]
// if !ok {
// panic(fmt.Sprintf("locale of index [%d] not found", index))
// }
// return loc
/* Note(@kataras): the following is allowed as a language index can be higher
than the length of the locale files.
if index >= len(l) || index < 0 {
// 1. language exists in the caller but was not found in files.
// 2. language exists in both files and caller but the actual
// languages are two, while the registered are 4 (when missing files),
// that happens when Strict option is false.
// force to the default language but what is the default language if the language index is greater than this length?
// That's why it's allowed.
index = 0
}*/
if index < 0 {
index = 0
}
if locale, ok := l[index]; ok {
return locale
}
return l[0]
}
// SetDefault changes the default language based on the "index".
// See `I18n#SetDefault` method for more.
func (l MemoryLocalizer) SetDefault(index int) bool {
// callers should protect with mutex if called at serve-time.
if loc, ok := l[index]; ok {
f := l[0]
l[0] = loc
l[index] = f
return true
}
return false
}
type defaultLocale struct {
index int
id string
tag *language.Tag
// templates *template.Template // we could use the ExecuteTemplate too.
templateKeys map[string]*template.Template
lineKeys map[string]string
other map[string]interface{}
defaultMessageFunc MessageFunc
tmplBufPool *sync.Pool
}
func (l *defaultLocale) Index() int {
return l.index
}
func (l *defaultLocale) Tag() *language.Tag {
return l.tag
}
func (l *defaultLocale) Language() string {
return l.id
}
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 {
// search on templates.
if tmpl, ok := l.templateKeys[key]; ok {
var (
data interface{}
text string
)
if len(args) > 0 {
data = args[0]
}
buf := l.tmplBufPool.Get().(*bytes.Buffer)
buf.Reset()
err := tmpl.Execute(buf, data)
if err != nil {
text = err.Error()
} else {
text = buf.String()
}
l.tmplBufPool.Put(buf)
return text
}
if text, ok := l.lineKeys[key]; ok {
return fmt.Sprintf(text, args...)
}
n := len(args)
if v, ok := l.other[key]; ok {
if n > 0 {
return fmt.Sprintf("%v [%v]", v, args)
}
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 ""
}
func unmarshalINI(data []byte, v interface{}) error {
f, err := ini.Load(data)
if err != nil {