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:
36
i18n/i18n.go
36
i18n/i18n.go
@@ -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
5
i18n/internal/aliases.go
Normal 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
149
i18n/internal/catalog.go
Normal 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
195
i18n/internal/locale.go
Normal 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
81
i18n/internal/message.go
Normal 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
261
i18n/internal/plural.go
Normal 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
242
i18n/internal/template.go
Normal 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
182
i18n/internal/var.go
Normal 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
|
||||
}
|
||||
288
i18n/loader.go
288
i18n/loader.go
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user