mirror of
https://github.com/retailcrm/mg-transport-core.git
synced 2024-11-24 22:26:04 +03:00
[feature] Individual localizer for each context
* proposal with context localizer (engine.GetLocalizedMessage shouldn't be used for localizing for individual requests data) * normal test for SetLocale * use sync.Map * method to preload languages into localizer * DefaultLanguages variable * PreloadLanguages field in core.Engine * don't copy sync.Map * patch for goroutine safety usage of Localizer * fix for engine constructor * additional test for localizer middleware, which is more close to real environment * filter language.Und * prevent root placeholder tag usage * remove reduntant bundle storage, deprecated (*core.Localizer).FetchLanguage()
This commit is contained in:
parent
6a6726377d
commit
e6dd1643cf
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
.idea
|
.idea
|
||||||
coverage.txt
|
coverage.*
|
||||||
|
22
README.md
22
README.md
@ -20,22 +20,36 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Create new core.Engine instance
|
||||||
app := core.New()
|
app := core.New()
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
app.Config = core.NewConfig("config.yml")
|
app.Config = core.NewConfig("config.yml")
|
||||||
|
|
||||||
|
// Set default error translation key (will be returned if something goes wrong)
|
||||||
app.DefaultError = "unknown_error"
|
app.DefaultError = "unknown_error"
|
||||||
|
|
||||||
|
// Set translations path
|
||||||
app.TranslationsPath = "./translations"
|
app.TranslationsPath = "./translations"
|
||||||
|
|
||||||
|
// Preload some translations so they will not be loaded for every request
|
||||||
|
app.PreloadLanguages = core.DefaultLanguages
|
||||||
|
|
||||||
|
// Configure gin.Engine inside core.Engine
|
||||||
app.ConfigureRouter(func(engine *gin.Engine) {
|
app.ConfigureRouter(func(engine *gin.Engine) {
|
||||||
engine.Static("/static", "./static")
|
engine.Static("/static", "./static")
|
||||||
engine.HTMLRender = app.CreateRenderer(
|
engine.HTMLRender = app.CreateRenderer(
|
||||||
|
] // Insert templates here. Custom functions also can be provided.
|
||||||
|
// Default transl function will be injected automatically
|
||||||
func(renderer *core.Renderer) {
|
func(renderer *core.Renderer) {
|
||||||
// insert templates here. Example:
|
// Push method will load template from FS or from binary
|
||||||
r.Push("home", "templates/layout.html", "templates/home.html")
|
r.Push("home", "templates/layout.html", "templates/home.html")
|
||||||
},
|
},
|
||||||
template.FuncMap{},
|
template.FuncMap{},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Start application or fail if something gone wrong (e.g. port is already in use)
|
||||||
if err := app.Prepare().Run(); err != nil {
|
if err := app.Prepare().Run(); err != nil {
|
||||||
fmt.Printf("Fatal error: %s", err.Error())
|
fmt.Printf("Fatal error: %s", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -68,14 +82,18 @@ func main() {
|
|||||||
app := core.New()
|
app := core.New()
|
||||||
app.Config = core.NewConfig("config.yml")
|
app.Config = core.NewConfig("config.yml")
|
||||||
app.DefaultError = "unknown_error"
|
app.DefaultError = "unknown_error"
|
||||||
|
|
||||||
|
// Now translations will be loaded from packr.Box
|
||||||
app.TranslationsBox = translations
|
app.TranslationsBox = translations
|
||||||
|
app.PreloadLanguages = core.DefaultLanguages
|
||||||
|
|
||||||
app.ConfigureRouter(func(engine *gin.Engine) {
|
app.ConfigureRouter(func(engine *gin.Engine) {
|
||||||
|
// gin.Engine can use packr.Box as http.FileSystem
|
||||||
engine.StaticFS("/static", static)
|
engine.StaticFS("/static", static)
|
||||||
engine.HTMLRender = app.CreateRendererFS(
|
engine.HTMLRender = app.CreateRendererFS(
|
||||||
templates,
|
templates,
|
||||||
func(renderer *core.Renderer) {
|
func(renderer *core.Renderer) {
|
||||||
// insert templates here. Example:
|
// Same Push method here, but without relative directory.
|
||||||
r.Push("home", "layout.html", "home.html")
|
r.Push("home", "layout.html", "home.html")
|
||||||
},
|
},
|
||||||
template.FuncMap{},
|
template.FuncMap{},
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/gorilla/securecookie"
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Engine struct
|
// Engine struct
|
||||||
@ -24,6 +25,7 @@ type Engine struct {
|
|||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
csrf *CSRF
|
csrf *CSRF
|
||||||
jobManager *JobManager
|
jobManager *JobManager
|
||||||
|
PreloadLanguages []language.Tag
|
||||||
Sessions sessions.Store
|
Sessions sessions.Store
|
||||||
Config ConfigInterface
|
Config ConfigInterface
|
||||||
LogFormatter logging.Formatter
|
LogFormatter logging.Formatter
|
||||||
@ -34,7 +36,11 @@ type Engine struct {
|
|||||||
func New() *Engine {
|
func New() *Engine {
|
||||||
return &Engine{
|
return &Engine{
|
||||||
Config: nil,
|
Config: nil,
|
||||||
Localizer: Localizer{},
|
Localizer: Localizer{
|
||||||
|
i18nStorage: &sync.Map{},
|
||||||
|
loadMutex: &sync.RWMutex{},
|
||||||
|
},
|
||||||
|
PreloadLanguages: []language.Tag{},
|
||||||
ORM: ORM{},
|
ORM: ORM{},
|
||||||
Sentry: Sentry{},
|
Sentry: Sentry{},
|
||||||
Utils: Utils{},
|
Utils: Utils{},
|
||||||
@ -76,14 +82,20 @@ func (e *Engine) Prepare() *Engine {
|
|||||||
if e.LogFormatter == nil {
|
if e.LogFormatter == nil {
|
||||||
e.LogFormatter = DefaultLogFormatter()
|
e.LogFormatter = DefaultLogFormatter()
|
||||||
}
|
}
|
||||||
if e.LocaleBundle == nil {
|
|
||||||
e.LocaleBundle = DefaultLocalizerBundle()
|
|
||||||
}
|
|
||||||
if e.LocaleMatcher == nil {
|
if e.LocaleMatcher == nil {
|
||||||
e.LocaleMatcher = DefaultLocalizerMatcher()
|
e.LocaleMatcher = DefaultLocalizerMatcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.isUnd(e.Localizer.LanguageTag) {
|
||||||
|
e.Localizer.LanguageTag = DefaultLanguage
|
||||||
|
}
|
||||||
|
|
||||||
e.LoadTranslations()
|
e.LoadTranslations()
|
||||||
|
|
||||||
|
if len(e.PreloadLanguages) > 0 {
|
||||||
|
e.Localizer.Preload(e.PreloadLanguages)
|
||||||
|
}
|
||||||
|
|
||||||
e.createDB(e.Config.GetDBConfig())
|
e.createDB(e.Config.GetDBConfig())
|
||||||
e.createRavenClient(e.Config.GetSentryDSN())
|
e.createRavenClient(e.Config.GetSentryDSN())
|
||||||
e.resetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0)
|
e.resetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0)
|
||||||
|
@ -34,13 +34,7 @@ func (e *EngineTest) SetupTest() {
|
|||||||
db, _, err = sqlmock.New()
|
db, _, err = sqlmock.New()
|
||||||
require.NoError(e.T(), err)
|
require.NoError(e.T(), err)
|
||||||
|
|
||||||
if _, err := os.Stat(testTranslationsDir); err != nil && os.IsNotExist(err) {
|
createTestLangFiles(e.T())
|
||||||
err := os.Mkdir(testTranslationsDir, os.ModePerm)
|
|
||||||
require.Nil(e.T(), err)
|
|
||||||
data := []byte("message: Test message\nmessage_template: Test message with {{.data}}")
|
|
||||||
err = ioutil.WriteFile(testLangFile, data, os.ModePerm)
|
|
||||||
require.Nil(e.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
e.engine.Config = Config{
|
e.engine.Config = Config{
|
||||||
Version: "1",
|
Version: "1",
|
||||||
@ -102,6 +96,17 @@ func (e *EngineTest) Test_Prepare() {
|
|||||||
e.engine.TranslationsPath = testTranslationsDir
|
e.engine.TranslationsPath = testTranslationsDir
|
||||||
e.engine.Prepare()
|
e.engine.Prepare()
|
||||||
assert.True(e.T(), e.engine.prepared)
|
assert.True(e.T(), e.engine.prepared)
|
||||||
|
assert.NotNil(e.T(), e.engine.Config)
|
||||||
|
assert.NotEmpty(e.T(), e.engine.DefaultError)
|
||||||
|
assert.NotEmpty(e.T(), e.engine.LogFormatter)
|
||||||
|
assert.NotEmpty(e.T(), e.engine.LocaleMatcher)
|
||||||
|
assert.False(e.T(), e.engine.isUnd(e.engine.Localizer.LanguageTag))
|
||||||
|
assert.NotNil(e.T(), e.engine.DB)
|
||||||
|
assert.NotNil(e.T(), e.engine.Client)
|
||||||
|
assert.NotNil(e.T(), e.engine.logger)
|
||||||
|
assert.NotNil(e.T(), e.engine.Sentry.Localizer)
|
||||||
|
assert.NotNil(e.T(), e.engine.Sentry.Logger)
|
||||||
|
assert.NotNil(e.T(), e.engine.Utils.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EngineTest) Test_initGin_Release() {
|
func (e *EngineTest) Test_initGin_Release() {
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path"
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gobuffalo/packd"
|
"github.com/gobuffalo/packd"
|
||||||
@ -13,11 +14,24 @@ import (
|
|||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultLanguages for transports
|
||||||
|
var DefaultLanguages = []language.Tag{
|
||||||
|
language.English,
|
||||||
|
language.Russian,
|
||||||
|
language.Spanish,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultLanguage is a base language which will be chosen if current language is unspecified
|
||||||
|
var DefaultLanguage = language.English
|
||||||
|
|
||||||
|
// LocalizerContextKey is a key which is used to store localizer in gin.Context key-value storage
|
||||||
|
const LocalizerContextKey = "localizer"
|
||||||
|
|
||||||
// Localizer struct
|
// Localizer struct
|
||||||
type Localizer struct {
|
type Localizer struct {
|
||||||
i18n *i18n.Localizer
|
i18nStorage *sync.Map
|
||||||
TranslationsBox *packr.Box
|
TranslationsBox *packr.Box
|
||||||
LocaleBundle *i18n.Bundle
|
loadMutex *sync.RWMutex
|
||||||
LocaleMatcher language.Matcher
|
LocaleMatcher language.Matcher
|
||||||
LanguageTag language.Tag
|
LanguageTag language.Tag
|
||||||
TranslationsPath string
|
TranslationsPath string
|
||||||
@ -25,13 +39,13 @@ type Localizer struct {
|
|||||||
|
|
||||||
// NewLocalizer returns localizer instance with specified parameters.
|
// NewLocalizer returns localizer instance with specified parameters.
|
||||||
// Usage:
|
// Usage:
|
||||||
// NewLocalizer(language.English, DefaultLocalizerBundle(), DefaultLocalizerMatcher(), "translations")
|
// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations")
|
||||||
func NewLocalizer(locale language.Tag, bundle *i18n.Bundle, matcher language.Matcher, translationsPath string) *Localizer {
|
func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) *Localizer {
|
||||||
localizer := &Localizer{
|
localizer := &Localizer{
|
||||||
i18n: nil,
|
i18nStorage: &sync.Map{},
|
||||||
LocaleBundle: bundle,
|
|
||||||
LocaleMatcher: matcher,
|
LocaleMatcher: matcher,
|
||||||
TranslationsPath: translationsPath,
|
TranslationsPath: translationsPath,
|
||||||
|
loadMutex: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
localizer.SetLanguage(locale)
|
localizer.SetLanguage(locale)
|
||||||
localizer.LoadTranslations()
|
localizer.LoadTranslations()
|
||||||
@ -41,14 +55,14 @@ func NewLocalizer(locale language.Tag, bundle *i18n.Bundle, matcher language.Mat
|
|||||||
|
|
||||||
// NewLocalizerFS returns localizer instance with specified parameters. *packr.Box should be used instead of directory.
|
// NewLocalizerFS returns localizer instance with specified parameters. *packr.Box should be used instead of directory.
|
||||||
// Usage:
|
// Usage:
|
||||||
// NewLocalizerFS(language.English, DefaultLocalizerBundle(), DefaultLocalizerMatcher(), translationsBox)
|
// NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsBox)
|
||||||
// TODO This code should be covered with tests.
|
// TODO This code should be covered with tests.
|
||||||
func NewLocalizerFS(locale language.Tag, bundle *i18n.Bundle, matcher language.Matcher, translationsBox *packr.Box) *Localizer {
|
func NewLocalizerFS(locale language.Tag, matcher language.Matcher, translationsBox *packr.Box) *Localizer {
|
||||||
localizer := &Localizer{
|
localizer := &Localizer{
|
||||||
i18n: nil,
|
i18nStorage: &sync.Map{},
|
||||||
LocaleBundle: bundle,
|
|
||||||
LocaleMatcher: matcher,
|
LocaleMatcher: matcher,
|
||||||
TranslationsBox: translationsBox,
|
TranslationsBox: translationsBox,
|
||||||
|
loadMutex: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
localizer.SetLanguage(locale)
|
localizer.SetLanguage(locale)
|
||||||
localizer.LoadTranslations()
|
localizer.LoadTranslations()
|
||||||
@ -58,26 +72,40 @@ func NewLocalizerFS(locale language.Tag, bundle *i18n.Bundle, matcher language.M
|
|||||||
|
|
||||||
// DefaultLocalizerBundle returns new localizer bundle with English as default language
|
// DefaultLocalizerBundle returns new localizer bundle with English as default language
|
||||||
func DefaultLocalizerBundle() *i18n.Bundle {
|
func DefaultLocalizerBundle() *i18n.Bundle {
|
||||||
return i18n.NewBundle(language.English)
|
return i18n.NewBundle(DefaultLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalizerBundle returns new localizer bundle provided language as default
|
||||||
|
func LocalizerBundle(tag language.Tag) *i18n.Bundle {
|
||||||
|
return i18n.NewBundle(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultLocalizerMatcher returns matcher with English, Russian and Spanish tags
|
// DefaultLocalizerMatcher returns matcher with English, Russian and Spanish tags
|
||||||
func DefaultLocalizerMatcher() language.Matcher {
|
func DefaultLocalizerMatcher() language.Matcher {
|
||||||
return language.NewMatcher([]language.Tag{
|
return language.NewMatcher(DefaultLanguages)
|
||||||
language.English,
|
|
||||||
language.Russian,
|
|
||||||
language.Spanish,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalizationMiddleware returns gin.HandlerFunc which will set localizer language by Accept-Language header
|
// LocalizationMiddleware returns gin.HandlerFunc which will set localizer language by Accept-Language header
|
||||||
|
// Result Localizer instance will share it's internal data (translations, bundles, etc) with instance which was used
|
||||||
|
// to append middleware to gin.
|
||||||
|
// Because of that all Localizer instances from this middleware will share *same* mutex. This mutex is used to wrap
|
||||||
|
// i18n.Bundle methods (those aren't goroutine-safe to use).
|
||||||
// Usage:
|
// Usage:
|
||||||
// engine := gin.New()
|
// engine := gin.New()
|
||||||
// localizer := NewLocalizer("en", DefaultLocalizerBundle(), DefaultLocalizerMatcher(), "translations")
|
// localizer := NewLocalizer("en", DefaultLocalizerBundle(), DefaultLocalizerMatcher(), "translations")
|
||||||
// engine.Use(localizer.LocalizationMiddleware())
|
// engine.Use(localizer.LocalizationMiddleware())
|
||||||
func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
|
func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
l.SetLocale(c.GetHeader("Accept-Language"))
|
clone := &Localizer{
|
||||||
|
i18nStorage: l.i18nStorage,
|
||||||
|
TranslationsBox: l.TranslationsBox,
|
||||||
|
LocaleMatcher: l.LocaleMatcher,
|
||||||
|
LanguageTag: l.LanguageTag,
|
||||||
|
TranslationsPath: l.TranslationsPath,
|
||||||
|
loadMutex: l.loadMutex,
|
||||||
|
}
|
||||||
|
clone.SetLocale(c.GetHeader("Accept-Language"))
|
||||||
|
c.Set(LocalizerContextKey, clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,24 +127,36 @@ func (l *Localizer) LocalizationFuncMap() template.FuncMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getLocaleBundle returns current locale bundle and creates it if needed
|
||||||
func (l *Localizer) getLocaleBundle() *i18n.Bundle {
|
func (l *Localizer) getLocaleBundle() *i18n.Bundle {
|
||||||
if l.LocaleBundle == nil {
|
return l.createLocaleBundleByTag(l.LanguageTag)
|
||||||
l.LocaleBundle = DefaultLocalizerBundle()
|
|
||||||
}
|
}
|
||||||
return l.LocaleBundle
|
|
||||||
|
// createLocaleBundleByTag creates locale bundle by language tag
|
||||||
|
func (l *Localizer) createLocaleBundleByTag(tag language.Tag) *i18n.Bundle {
|
||||||
|
bundle := i18n.NewBundle(tag)
|
||||||
|
bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal)
|
||||||
|
l.loadTranslationsToBundle(bundle)
|
||||||
|
|
||||||
|
return bundle
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadTranslations will load all translation files from translations directory or from embedded box
|
// LoadTranslations will load all translation files from translations directory or from embedded box
|
||||||
func (l *Localizer) LoadTranslations() {
|
func (l *Localizer) LoadTranslations() {
|
||||||
l.getLocaleBundle().RegisterUnmarshalFunc("yml", yaml.Unmarshal)
|
defer l.loadMutex.Unlock()
|
||||||
|
l.loadMutex.Lock()
|
||||||
|
l.getCurrentLocalizer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadTranslationsToBundle loads translations to provided bundle
|
||||||
|
func (l *Localizer) loadTranslationsToBundle(i18nBundle *i18n.Bundle) {
|
||||||
switch {
|
switch {
|
||||||
case l.TranslationsPath != "":
|
case l.TranslationsPath != "":
|
||||||
if err := l.loadFromDirectory(); err != nil {
|
if err := l.loadFromDirectory(i18nBundle); err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
case l.TranslationsBox != nil:
|
case l.TranslationsBox != nil:
|
||||||
if err := l.loadFromFS(); err != nil {
|
if err := l.loadFromFS(i18nBundle); err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -125,7 +165,7 @@ func (l *Localizer) LoadTranslations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadTranslations will load all translation files from translations directory
|
// LoadTranslations will load all translation files from translations directory
|
||||||
func (l *Localizer) loadFromDirectory() error {
|
func (l *Localizer) loadFromDirectory(i18nBundle *i18n.Bundle) error {
|
||||||
files, err := ioutil.ReadDir(l.TranslationsPath)
|
files, err := ioutil.ReadDir(l.TranslationsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -133,7 +173,7 @@ func (l *Localizer) loadFromDirectory() error {
|
|||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if !f.IsDir() {
|
if !f.IsDir() {
|
||||||
l.getLocaleBundle().MustLoadMessageFile(path.Join(l.TranslationsPath, f.Name()))
|
i18nBundle.MustLoadMessageFile(path.Join(l.TranslationsPath, f.Name()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,12 +181,12 @@ func (l *Localizer) loadFromDirectory() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadTranslations will load all translation files from embedded box
|
// LoadTranslations will load all translation files from embedded box
|
||||||
func (l *Localizer) loadFromFS() error {
|
func (l *Localizer) loadFromFS(i18nBundle *i18n.Bundle) error {
|
||||||
err := l.TranslationsBox.Walk(func(s string, file packd.File) error {
|
err := l.TranslationsBox.Walk(func(s string, file packd.File) error {
|
||||||
if fileInfo, err := file.FileInfo(); err == nil {
|
if fileInfo, err := file.FileInfo(); err == nil {
|
||||||
if !fileInfo.IsDir() {
|
if !fileInfo.IsDir() {
|
||||||
if data, err := ioutil.ReadAll(file); err == nil {
|
if data, err := ioutil.ReadAll(file); err == nil {
|
||||||
if _, err := l.getLocaleBundle().ParseMessageFileBytes(data, fileInfo.Name()); err != nil {
|
if _, err := i18nBundle.ParseMessageFileBytes(data, fileInfo.Name()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -167,32 +207,93 @@ func (l *Localizer) loadFromFS() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getLocalizer returns *i18n.Localizer with provided language tag. It will be created if not exist
|
||||||
|
func (l *Localizer) getLocalizer(tag language.Tag) *i18n.Localizer {
|
||||||
|
var localizer *i18n.Localizer
|
||||||
|
|
||||||
|
if l.isUnd(tag) {
|
||||||
|
tag = DefaultLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
if item, ok := l.i18nStorage.Load(tag); !ok {
|
||||||
|
l.i18nStorage.Store(tag, i18n.NewLocalizer(l.createLocaleBundleByTag(tag), tag.String()))
|
||||||
|
} else {
|
||||||
|
localizer = item.(*i18n.Localizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return localizer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Localizer) matchByString(al string) language.Tag {
|
||||||
|
tag, _ := language.MatchStrings(l.LocaleMatcher, al)
|
||||||
|
if l.isUnd(tag) {
|
||||||
|
return DefaultLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Localizer) isUnd(tag language.Tag) bool {
|
||||||
|
return tag == language.Und || tag.IsRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCurrentLocalizer returns *i18n.Localizer with current language tag
|
||||||
|
func (l *Localizer) getCurrentLocalizer() *i18n.Localizer {
|
||||||
|
return l.getLocalizer(l.LanguageTag)
|
||||||
|
}
|
||||||
|
|
||||||
// SetLocale will change language for current localizer
|
// SetLocale will change language for current localizer
|
||||||
func (l *Localizer) SetLocale(al string) {
|
func (l *Localizer) SetLocale(al string) {
|
||||||
tag, _ := language.MatchStrings(l.LocaleMatcher, al)
|
l.SetLanguage(l.matchByString(al))
|
||||||
l.SetLanguage(tag)
|
}
|
||||||
|
|
||||||
|
// Preload provided languages (so they will not be loaded every time in middleware)
|
||||||
|
func (l *Localizer) Preload(tags []language.Tag) {
|
||||||
|
for _, tag := range tags {
|
||||||
|
l.getLocalizer(tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLanguage will change language using language tag
|
// SetLanguage will change language using language tag
|
||||||
func (l *Localizer) SetLanguage(tag language.Tag) {
|
func (l *Localizer) SetLanguage(tag language.Tag) {
|
||||||
|
if l.isUnd(tag) {
|
||||||
|
tag = DefaultLanguage
|
||||||
|
}
|
||||||
|
|
||||||
l.LanguageTag = tag
|
l.LanguageTag = tag
|
||||||
l.FetchLanguage()
|
l.LoadTranslations()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchLanguage will load language from tag
|
// FetchLanguage will load language from tag
|
||||||
|
//
|
||||||
|
// Deprecated: Use `(*core.Localizer).LoadTranslations()` instead
|
||||||
func (l *Localizer) FetchLanguage() {
|
func (l *Localizer) FetchLanguage() {
|
||||||
l.i18n = i18n.NewLocalizer(l.getLocaleBundle(), l.LanguageTag.String())
|
l.LoadTranslations()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLocalizedMessage will return localized message by it's ID
|
// GetLocalizedMessage will return localized message by it's ID. It doesn't use `Must` prefix in order to keep BC.
|
||||||
func (l *Localizer) GetLocalizedMessage(messageID string) string {
|
func (l *Localizer) GetLocalizedMessage(messageID string) string {
|
||||||
return l.i18n.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
|
return l.getCurrentLocalizer().MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLocalizedTemplateMessage will return localized message with specified data
|
// GetLocalizedTemplateMessage will return localized message with specified data. It doesn't use `Must` prefix in order to keep BC.
|
||||||
// It uses text/template syntax: https://golang.org/pkg/text/template/
|
// It uses text/template syntax: https://golang.org/pkg/text/template/
|
||||||
func (l *Localizer) GetLocalizedTemplateMessage(messageID string, templateData map[string]interface{}) string {
|
func (l *Localizer) GetLocalizedTemplateMessage(messageID string, templateData map[string]interface{}) string {
|
||||||
return l.i18n.MustLocalize(&i18n.LocalizeConfig{
|
return l.getCurrentLocalizer().MustLocalize(&i18n.LocalizeConfig{
|
||||||
|
MessageID: messageID,
|
||||||
|
TemplateData: templateData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Localize will return localized message by it's ID, or error if message wasn't found
|
||||||
|
func (l *Localizer) Localize(messageID string) (string, error) {
|
||||||
|
return l.getCurrentLocalizer().Localize(&i18n.LocalizeConfig{MessageID: messageID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalizedTemplateMessage will return localized message with specified data, or error if message wasn't found
|
||||||
|
// It uses text/template syntax: https://golang.org/pkg/text/template/
|
||||||
|
func (l *Localizer) LocalizeTemplateMessage(messageID string, templateData map[string]interface{}) (string, error) {
|
||||||
|
return l.getCurrentLocalizer().Localize(&i18n.LocalizeConfig{
|
||||||
MessageID: messageID,
|
MessageID: messageID,
|
||||||
TemplateData: templateData,
|
TemplateData: templateData,
|
||||||
})
|
})
|
||||||
@ -202,3 +303,28 @@ func (l *Localizer) GetLocalizedTemplateMessage(messageID string, templateData m
|
|||||||
func (l *Localizer) BadRequestLocalized(err string) (int, interface{}) {
|
func (l *Localizer) BadRequestLocalized(err string) (int, interface{}) {
|
||||||
return BadRequest(l.GetLocalizedMessage(err))
|
return BadRequest(l.GetLocalizedMessage(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContextLocalizer returns localizer from context if it exist there
|
||||||
|
func GetContextLocalizer(c *gin.Context) (*Localizer, bool) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if item, ok := c.Get(LocalizerContextKey); ok {
|
||||||
|
if localizer, ok := item.(*Localizer); ok {
|
||||||
|
return localizer, true
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGetContextLocalizer returns Localizer instance if it exists in provided context. Panics otherwise.
|
||||||
|
func MustGetContextLocalizer(c *gin.Context) *Localizer {
|
||||||
|
if localizer, ok := GetContextLocalizer(c); ok {
|
||||||
|
return localizer
|
||||||
|
}
|
||||||
|
panic("localizer is not present in provided context")
|
||||||
|
}
|
||||||
|
@ -2,11 +2,17 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@ -15,24 +21,37 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
testTranslationsDir = path.Join(os.TempDir(), "translations_test_dir")
|
testTranslationsDir = path.Join(os.TempDir(), "translations_test_dir")
|
||||||
testLangFile = path.Join(testTranslationsDir, "translate.en.yml")
|
testLangFiles = map[string][]byte{
|
||||||
|
"translate.en.yml": []byte("message: Test message\nmessage_template: Test message with {{.data}}"),
|
||||||
|
"translate.es.yml": []byte("message: Mensaje de prueba\nmessage_template: Mensaje de prueba con {{.data}}"),
|
||||||
|
"translate.ru.yml": []byte("message: Тестовое сообщение\nmessage_template: Тестовое сообщение с {{.data}}"),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func createTestLangFiles(t *testing.T) {
|
||||||
|
for name, data := range testLangFiles {
|
||||||
|
fileName := path.Join(testTranslationsDir, name)
|
||||||
|
|
||||||
|
if _, err := os.Stat(testTranslationsDir); err != nil && os.IsNotExist(err) {
|
||||||
|
err := os.Mkdir(testTranslationsDir, os.ModePerm)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(fileName); err != nil && os.IsNotExist(err) {
|
||||||
|
err = ioutil.WriteFile(fileName, data, os.ModePerm)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type LocalizerTest struct {
|
type LocalizerTest struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
localizer *Localizer
|
localizer *Localizer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalizerTest) SetupSuite() {
|
func (l *LocalizerTest) SetupSuite() {
|
||||||
if _, err := os.Stat(testTranslationsDir); err != nil && os.IsNotExist(err) {
|
createTestLangFiles(l.T())
|
||||||
err := os.Mkdir(testTranslationsDir, os.ModePerm)
|
l.localizer = NewLocalizer(language.English, DefaultLocalizerMatcher(), testTranslationsDir)
|
||||||
require.Nil(l.T(), err)
|
|
||||||
data := []byte("message: Test message\nmessage_template: Test message with {{.data}}")
|
|
||||||
err = ioutil.WriteFile(testLangFile, data, os.ModePerm)
|
|
||||||
require.Nil(l.T(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.localizer = NewLocalizer(language.English, DefaultLocalizerBundle(), DefaultLocalizerMatcher(), testTranslationsDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalizerTest) Test_SetLocale() {
|
func (l *LocalizerTest) Test_SetLocale() {
|
||||||
@ -40,11 +59,87 @@ func (l *LocalizerTest) Test_SetLocale() {
|
|||||||
require.Nil(l.T(), recover())
|
require.Nil(l.T(), recover())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
l.localizer.SetLocale("es")
|
||||||
|
assert.Equal(l.T(), "Mensaje de prueba", l.localizer.GetLocalizedMessage("message"))
|
||||||
l.localizer.SetLocale("en")
|
l.localizer.SetLocale("en")
|
||||||
|
assert.Equal(l.T(), "Test message", l.localizer.GetLocalizedMessage("message"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalizerTest) Test_LocalizationMiddleware() {
|
func (l *LocalizerTest) Test_LocalizationMiddleware_Context() {
|
||||||
assert.NotNil(l.T(), l.localizer.LocalizationMiddleware())
|
l.localizer.Preload(DefaultLanguages)
|
||||||
|
middlewareFunc := l.localizer.LocalizationMiddleware()
|
||||||
|
require.NotNil(l.T(), middlewareFunc)
|
||||||
|
|
||||||
|
enContext := l.getContextWithLang(language.English)
|
||||||
|
esContext := l.getContextWithLang(language.Spanish)
|
||||||
|
ruContext := l.getContextWithLang(language.Russian)
|
||||||
|
|
||||||
|
middlewareFunc(enContext)
|
||||||
|
middlewareFunc(esContext)
|
||||||
|
middlewareFunc(ruContext)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
assert.Nil(l.T(), recover())
|
||||||
|
}()
|
||||||
|
|
||||||
|
enLocalizer := MustGetContextLocalizer(enContext)
|
||||||
|
esLocalizer := MustGetContextLocalizer(esContext)
|
||||||
|
ruLocalizer := MustGetContextLocalizer(ruContext)
|
||||||
|
|
||||||
|
assert.NotNil(l.T(), enLocalizer)
|
||||||
|
assert.NotNil(l.T(), esLocalizer)
|
||||||
|
assert.NotNil(l.T(), ruLocalizer)
|
||||||
|
|
||||||
|
assert.Equal(l.T(), language.English, enLocalizer.LanguageTag)
|
||||||
|
assert.Equal(l.T(), language.Spanish, esLocalizer.LanguageTag)
|
||||||
|
assert.Equal(l.T(), language.Russian, ruLocalizer.LanguageTag)
|
||||||
|
|
||||||
|
assert.Equal(l.T(), "Test message", enLocalizer.GetLocalizedMessage("message"))
|
||||||
|
assert.Equal(l.T(), "Mensaje de prueba", esLocalizer.GetLocalizedMessage("message"))
|
||||||
|
assert.Equal(l.T(), "Тестовое сообщение", ruLocalizer.GetLocalizedMessage("message"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalizerTest) Test_LocalizationMiddleware_Httptest() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
l.localizer.Preload(DefaultLanguages)
|
||||||
|
langMsgMap := map[language.Tag]string{
|
||||||
|
language.English: "Test message",
|
||||||
|
language.Russian: "Тестовое сообщение",
|
||||||
|
language.Spanish: "Mensaje de prueba",
|
||||||
|
}
|
||||||
|
|
||||||
|
fw := gin.New()
|
||||||
|
fw.Use(l.localizer.LocalizationMiddleware())
|
||||||
|
fw.GET("/test", func(c *gin.Context) {
|
||||||
|
loc := MustGetContextLocalizer(c)
|
||||||
|
c.String(http.StatusOK, loc.GetLocalizedMessage("message"))
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(m map[language.Tag]string, wg *sync.WaitGroup) {
|
||||||
|
var tag language.Tag
|
||||||
|
switch rand.Intn(3-1) + 1 {
|
||||||
|
case 1:
|
||||||
|
tag = language.English
|
||||||
|
case 2:
|
||||||
|
tag = language.Russian
|
||||||
|
case 3:
|
||||||
|
tag = language.Spanish
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
require.NoError(l.T(), err)
|
||||||
|
req.Header.Add("Accept-Language", tag.String())
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
fw.ServeHTTP(rr, req)
|
||||||
|
assert.Equal(l.T(), m[tag], rr.Body.String())
|
||||||
|
wg.Done()
|
||||||
|
}(langMsgMap, &wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalizerTest) Test_LocalizationFuncMap() {
|
func (l *LocalizerTest) Test_LocalizationFuncMap() {
|
||||||
@ -78,6 +173,23 @@ func (l *LocalizerTest) Test_BadRequestLocalized() {
|
|||||||
assert.Equal(l.T(), "Test message", resp.(ErrorResponse).Error)
|
assert.Equal(l.T(), "Test message", resp.(ErrorResponse).Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getContextWithLang generates context with Accept-Language header
|
||||||
|
func (l *LocalizerTest) getContextWithLang(tag language.Tag) *gin.Context {
|
||||||
|
urlInstance, _ := url.Parse("https://example.com")
|
||||||
|
headers := http.Header{}
|
||||||
|
headers.Add("Accept-Language", tag.String())
|
||||||
|
return &gin.Context{
|
||||||
|
Request: &http.Request{
|
||||||
|
Method: "GET",
|
||||||
|
URL: urlInstance,
|
||||||
|
Proto: "https",
|
||||||
|
Header: headers,
|
||||||
|
Host: "example.com",
|
||||||
|
},
|
||||||
|
Keys: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (l *LocalizerTest) TearDownSuite() {
|
func (l *LocalizerTest) TearDownSuite() {
|
||||||
err := os.RemoveAll(testTranslationsDir)
|
err := os.RemoveAll(testTranslationsDir)
|
||||||
require.Nil(l.T(), err)
|
require.Nil(l.T(), err)
|
||||||
@ -87,6 +199,18 @@ func TestLocalizer_Suite(t *testing.T) {
|
|||||||
suite.Run(t, new(LocalizerTest))
|
suite.Run(t, new(LocalizerTest))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocalizer_DefaultLocalizerMatcher(t *testing.T) {
|
||||||
|
assert.NotNil(t, DefaultLocalizerMatcher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalizer_DefaultLocalizerBundle(t *testing.T) {
|
||||||
|
assert.NotNil(t, DefaultLocalizerBundle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalizer_LocalizerBundle(t *testing.T) {
|
||||||
|
assert.NotNil(t, LocalizerBundle(language.Russian))
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocalizer_NoDirectory(t *testing.T) {
|
func TestLocalizer_NoDirectory(t *testing.T) {
|
||||||
defer func() {
|
defer func() {
|
||||||
assert.NotNil(t, recover())
|
assert.NotNil(t, recover())
|
||||||
@ -94,7 +218,6 @@ func TestLocalizer_NoDirectory(t *testing.T) {
|
|||||||
|
|
||||||
_ = NewLocalizer(
|
_ = NewLocalizer(
|
||||||
language.English,
|
language.English,
|
||||||
DefaultLocalizerBundle(),
|
|
||||||
DefaultLocalizerMatcher(),
|
DefaultLocalizerMatcher(),
|
||||||
path.Join(os.TempDir(), "this directory should not exist"),
|
path.Join(os.TempDir(), "this directory should not exist"),
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user