mg-transport-core/core/localizer.go

332 lines
10 KiB
Go
Raw Normal View History

2019-09-04 15:22:27 +03:00
package core
import (
"html/template"
2021-07-29 11:29:31 +03:00
"io/fs"
2019-09-04 15:22:27 +03:00
"io/ioutil"
"path"
"sync"
2019-09-04 15:22:27 +03:00
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
"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.
2019-09-04 15:22:27 +03:00
type Localizer struct {
i18nStorage *sync.Map
2021-07-29 11:29:31 +03:00
TranslationsFS fs.FS
loadMutex *sync.RWMutex
2019-09-04 15:22:27 +03:00
LocaleMatcher language.Matcher
LanguageTag language.Tag
TranslationsPath string
}
// NewLocalizer returns localizer instance with specified parameters.
// Usage:
// NewLocalizer(language.English, DefaultLocalizerMatcher(), "translations")
func NewLocalizer(locale language.Tag, matcher language.Matcher, translationsPath string) *Localizer {
2019-09-04 15:22:27 +03:00
localizer := &Localizer{
i18nStorage: &sync.Map{},
2019-09-04 15:22:27 +03:00
LocaleMatcher: matcher,
TranslationsPath: translationsPath,
loadMutex: &sync.RWMutex{},
2019-09-04 15:22:27 +03:00
}
localizer.SetLanguage(locale)
localizer.LoadTranslations()
return localizer
}
2021-07-29 11:29:31 +03:00
// NewLocalizerFS returns localizer instance with specified parameters.
// Usage:
2021-07-29 11:29:31 +03:00
// NewLocalizerFS(language.English, DefaultLocalizerMatcher(), translationsFS)
// TODO This code should be covered with tests.
2021-07-29 11:29:31 +03:00
func NewLocalizerFS(
locale language.Tag, matcher language.Matcher, translationsFS fs.FS,
) *Localizer {
localizer := &Localizer{
2021-07-29 11:29:31 +03:00
i18nStorage: &sync.Map{},
LocaleMatcher: matcher,
TranslationsFS: translationsFS,
loadMutex: &sync.RWMutex{},
}
localizer.SetLanguage(locale)
localizer.LoadTranslations()
return localizer
}
// DefaultLocalizerBundle returns new localizer bundle with English as default language.
2019-09-04 15:22:27 +03:00
func DefaultLocalizerBundle() *i18n.Bundle {
return i18n.NewBundle(DefaultLanguage)
}
// LocalizerBundle returns new localizer bundle provided language as default.
func LocalizerBundle(tag language.Tag) *i18n.Bundle {
return i18n.NewBundle(tag)
2019-09-04 15:22:27 +03:00
}
// DefaultLocalizerMatcher returns matcher with English, Russian and Spanish tags.
2019-09-04 15:22:27 +03:00
func DefaultLocalizerMatcher() language.Matcher {
return language.NewMatcher(DefaultLanguages)
2019-09-04 15:22:27 +03:00
}
// Clone *core.Localizer. Clone shares it's translations with the parent localizer. Language tag will not be shared.
// Because of that you can change clone's language without affecting parent localizer.
// This method should be used when LocalizationMiddleware is not feasible (outside of *gin.HandlerFunc).
func (l *Localizer) Clone() *Localizer {
clone := &Localizer{
i18nStorage: l.i18nStorage,
2021-07-29 11:29:31 +03:00
TranslationsFS: l.TranslationsFS,
LocaleMatcher: l.LocaleMatcher,
LanguageTag: l.LanguageTag,
TranslationsPath: l.TranslationsPath,
loadMutex: l.loadMutex,
}
clone.SetLanguage(DefaultLanguage)
return clone
}
2019-09-04 15:22:27 +03:00
// 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).
2019-09-04 15:22:27 +03:00
// Usage:
// engine := gin.New()
// localizer := NewLocalizer("en", DefaultLocalizerMatcher(), "translations")
2019-09-04 15:22:27 +03:00
// engine.Use(localizer.LocalizationMiddleware())
func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
clone := l.Clone()
clone.SetLocale(c.GetHeader("Accept-Language"))
c.Set(LocalizerContextKey, clone)
2019-09-04 15:22:27 +03:00
}
}
// LocalizationFuncMap returns template.FuncMap (html template is used) with one method - trans
// Usage in code:
// engine := gin.New()
// engine.FuncMap = localizer.LocalizationFuncMap()
// or (with multitemplate)
// renderer := multitemplate.NewRenderer()
// funcMap := localizer.LocalizationFuncMap()
// renderer.AddFromFilesFuncs("index", funcMap, "template/index.html")
// funcMap must be passed for every .AddFromFilesFuncs call
// Usage in templates:
// <p class="info">{{"need_login_msg" | trans}}
// You can borrow FuncMap from this method and add your functions to it.
func (l *Localizer) LocalizationFuncMap() template.FuncMap {
return template.FuncMap{
"trans": l.GetLocalizedMessage,
}
}
// getLocaleBundle returns current locale bundle and creates it if needed.
2019-09-04 15:22:27 +03:00
func (l *Localizer) getLocaleBundle() *i18n.Bundle {
return l.createLocaleBundleByTag(l.LanguageTag)
}
// 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
2019-09-04 15:22:27 +03:00
}
// LoadTranslations will load all translation files from translations directory or from embedded box.
2019-09-04 15:22:27 +03:00
func (l *Localizer) LoadTranslations() {
defer l.loadMutex.Unlock()
l.loadMutex.Lock()
l.getCurrentLocalizer()
}
2019-09-18 10:28:55 +03:00
// loadTranslationsToBundle loads translations to provided bundle.
func (l *Localizer) loadTranslationsToBundle(i18nBundle *i18n.Bundle) {
2019-09-18 10:28:55 +03:00
switch {
case l.TranslationsPath != "":
if err := l.loadFromDirectory(i18nBundle); err != nil {
2019-09-18 10:28:55 +03:00
panic(err.Error())
}
2021-07-29 11:29:31 +03:00
default:
if err := l.loadFromFS(i18nBundle); err != nil {
2019-09-18 10:28:55 +03:00
panic(err.Error())
}
}
}
// LoadTranslations will load all translation files from translations directory.
func (l *Localizer) loadFromDirectory(i18nBundle *i18n.Bundle) error {
2019-09-04 15:22:27 +03:00
files, err := ioutil.ReadDir(l.TranslationsPath)
if err != nil {
2019-09-18 10:28:55 +03:00
return err
2019-09-04 15:22:27 +03:00
}
2019-09-18 10:28:55 +03:00
2019-09-04 15:22:27 +03:00
for _, f := range files {
if !f.IsDir() {
i18nBundle.MustLoadMessageFile(path.Join(l.TranslationsPath, f.Name()))
2019-09-04 15:22:27 +03:00
}
}
2019-09-18 10:28:55 +03:00
return nil
}
2021-07-29 11:29:31 +03:00
// LoadTranslations will load all translation files from embedding in binary.
func (l *Localizer) loadFromFS(i18nBundle *i18n.Bundle) error {
2021-07-29 11:29:31 +03:00
translationFiles, err := fs.ReadDir(l.TranslationsFS, string('.'))
2019-09-18 10:28:55 +03:00
if err != nil {
return err
}
2021-07-29 11:29:31 +03:00
for _, file := range translationFiles {
if !file.IsDir() {
data, err := fs.ReadFile(l.TranslationsFS, file.Name())
if err != nil {
return err
}
if _, err := i18nBundle.ParseMessageFileBytes(data, file.Name()); err != nil {
return err
}
}
}
2019-12-12 09:35:05 +03:00
return nil
2019-09-04 15:22:27 +03:00
}
// 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.
2019-09-04 15:22:27 +03:00
func (l *Localizer) SetLocale(al string) {
l.SetLanguage(l.matchByString(al))
}
// 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)
}
2019-09-04 15:22:27 +03:00
}
// SetLanguage will change language using language tag.
2019-09-04 15:22:27 +03:00
func (l *Localizer) SetLanguage(tag language.Tag) {
if l.isUnd(tag) {
tag = DefaultLanguage
}
2019-09-04 15:22:27 +03:00
l.LanguageTag = tag
l.LoadTranslations()
2019-09-04 15:22:27 +03:00
}
// FetchLanguage will load language from tag
//
// Deprecated: Use `(*core.Localizer).LoadTranslations()` instead.
2019-09-04 15:22:27 +03:00
func (l *Localizer) FetchLanguage() {
l.LoadTranslations()
2019-09-04 15:22:27 +03:00
}
// GetLocalizedMessage will return localized message by it's ID. It doesn't use `Must` prefix in order to keep BC.
2019-09-04 15:22:27 +03:00
func (l *Localizer) GetLocalizedMessage(messageID string) string {
return l.getCurrentLocalizer().MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
2019-09-04 15:22:27 +03:00
}
// GetLocalizedTemplateMessage will return localized message with specified data. It doesn't use `Must` prefix in order to keep BC.
2019-09-04 15:22:27 +03:00
// It uses text/template syntax: https://golang.org/pkg/text/template/
func (l *Localizer) GetLocalizedTemplateMessage(messageID string, templateData map[string]interface{}) string {
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})
}
// LocalizeTemplateMessage 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{
2019-09-04 15:22:27 +03:00
MessageID: messageID,
TemplateData: templateData,
})
}
// BadRequestLocalized is same as BadRequest(string), but passed string will be localized.
2019-09-04 15:22:27 +03:00
func (l *Localizer) BadRequestLocalized(err string) (int, interface{}) {
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
}
}
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")
}