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 }