Something went wrong on our end
-
ce58d1f0
localizer.go 6.70 KiB
package localizer
import (
"context"
"reflect"
"git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/locales"
"git.perx.ru/perxis/perxis-go/pkg/schema"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
"git.perx.ru/perxis/perxis-go/pkg/schema/walk"
)
var (
ErrLocaleDisabled = errors.New("cannot translate to disabled locale")
ErrLocaleNoPublish = errors.New("cannot translate to locale with disabled publication")
)
type Localizer struct {
schema *schema.Schema
localesKV map[string]*locales.Locale
localeID string
locales []*locales.Locale
allowNoPublished bool
allowDisabled bool
}
type Config struct {
Schema *schema.Schema
LocaleID string
Locales []*locales.Locale
AllowNoPublished bool
AllowDisabled bool
}
// NewLocalizer создает экземпляр локализатора. Требуется указать "загруженную" схему
func NewLocalizer(cfg Config) *Localizer {
if cfg.LocaleID == "" {
cfg.LocaleID = locales.DefaultID
}
loc := &Localizer{
schema: cfg.Schema,
localesKV: make(map[string]*locales.Locale, len(cfg.Locales)),
locales: cfg.Locales,
localeID: cfg.LocaleID,
allowDisabled: cfg.AllowDisabled,
allowNoPublished: cfg.AllowNoPublished,
}
for _, l := range cfg.Locales {
loc.localesKV[l.ID] = l
}
return loc
}
// Localize Получить полные локализованные данные для локали `localeID`. Входные параметры:
// - `data map[string]interface{}` - данные основной локали
// - `translations map[string]map[string]interface{}` переводы
//
// При отсутствии каких-либо полей в переводе на `localeID` данные берутся сначала из fallback-локали,
// если перевод отсутствует то из `data`
func (l *Localizer) Localize(data map[string]interface{}, translations map[string]map[string]interface{}) (localized map[string]interface{}, err error) {
target, fallback, err := l.getTargetAndFallBackLocales()
if err != nil {
return nil, err
}
if target.IsDefault() {
return data, nil
}
// localize fallback -> target
fallbackData := data
var exist bool
if !fallback.IsDefault() {
if fd, exist := translations[fallback.ID]; exist {
// localize default -> fallback
fallbackData, err = l.localize(fd, data)
if err != nil {
return nil, err
}
}
}
if localized, exist = translations[target.ID]; !exist {
localized = make(map[string]interface{})
}
return l.localize(localized, fallbackData)
}
// ExtractTranslation Получить "просеянные" данные для локали localeID: все поля, значения которых совпадают
// с переводом на fallback-локаль или основными данными, удаляются из перевода
func (l *Localizer) ExtractTranslation(data map[string]interface{}, translations map[string]map[string]interface{}) (translation map[string]interface{}, err error) {
target, fallback, err := l.getTargetAndFallBackLocales()
if err != nil {
return nil, err
}
if target.IsDefault() {
return data, nil
}
var exist bool
if translation, exist = translations[target.ID]; !exist {
return make(map[string]interface{}), nil
}
var fallbackData map[string]interface{}
if fallbackData, exist = translations[fallback.ID]; !exist {
return l.extractTranslation(translation, data)
}
// localize default -> fallback - нужно для корректного сравнения нельзя делать просто extract из прореженных данных fallback
if fallbackData, err = l.localize(fallbackData, data); err != nil {
return nil, err
}
// extract translation default -> target
return l.extractTranslation(translation, fallbackData)
}
func (l *Localizer) locale(localeID string) (loc *locales.Locale, err error) {
if localeID == "" {
return nil, locales.ErrLocaleIDRequired
}
var exist bool
if loc, exist = l.localesKV[localeID]; !exist {
return nil, locales.ErrNotFound
}
if loc == nil {
return nil, locales.ErrNotFound
}
if !l.allowDisabled && loc.Disabled {
return nil, ErrLocaleDisabled
}
if !l.allowNoPublished && localeID == l.localeID && loc.NoPublish { // can use non-publishing locale for fallback
return nil, ErrLocaleNoPublish
}
return
}
func (l *Localizer) Locale() (loc *locales.Locale, err error) {
return l.locale(l.localeID)
}
func (l *Localizer) Locales() []*locales.Locale {
return l.locales
}
func (l *Localizer) LocaleID() string {
return l.localeID
}
func (l *Localizer) getTargetAndFallBackLocales() (target, fallback *locales.Locale, err error) {
if target, err = l.locale(l.localeID); err != nil {
return nil, nil, err
}
if fallback, err = l.locale(target.Fallback); err != nil {
if fallback, err = l.locale(locales.DefaultID); err != nil {
return nil, nil, errors.Wrap(err, "get default locale")
}
}
return
}
func (l *Localizer) localize(target, fallback map[string]interface{}) (map[string]interface{}, error) {
if target == nil && fallback == nil {
return nil, nil
}
single := l.schema.GetFields(func(f *field.Field, p string) bool {
return f.SingleLocale
})
cfg := &walk.WalkConfig{Fields: make(map[string]walk.FieldConfig, len(single))}
for _, sn := range single {
cfg.Fields[sn.Path] = walk.FieldConfig{Fn: walk.KeepSrc}
}
w := walk.NewWalker(l.schema, cfg)
w.DefaultFn = localize
res, _, err := w.DataWalk(context.Background(), target, fallback)
if err != nil {
return nil, err
}
if res != nil {
return res.(map[string]interface{}), err
}
return nil, nil
}
func (l *Localizer) extractTranslation(target, fallback map[string]interface{}) (map[string]interface{}, error) {
if target == nil && fallback == nil {
return nil, nil
}
single := l.schema.GetFields(func(f *field.Field, p string) bool {
return f.SingleLocale
})
cfg := &walk.WalkConfig{Fields: make(map[string]walk.FieldConfig, len(single))}
for _, sn := range single {
cfg.Fields[sn.Path] = walk.FieldConfig{Fn: walk.RemoveValue}
}
w := walk.NewWalker(l.schema, cfg)
w.DefaultFn = extractTranslation
res, _, err := w.DataWalk(context.Background(), target, fallback)
if err != nil {
return nil, err
}
if res == nil {
return map[string]interface{}{}, nil
}
return res.(map[string]interface{}), nil
}
func localize(c *walk.WalkContext) (err error) {
if c.Dst != nil {
return
}
c.Dst = c.Src
c.Changed = true
return
}
func extractTranslation(c *walk.WalkContext) (err error) {
if c.Dst == nil {
return
}
if reflect.DeepEqual(c.Src, c.Dst) {
c.Dst = nil
c.Changed = true
}
return
}