Skip to content
Snippets Groups Projects
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
}