diff --git a/bboltStorage/migrate.go b/bboltStorage/migrate.go index 1fd9ed3..c35f0b9 100644 --- a/bboltStorage/migrate.go +++ b/bboltStorage/migrate.go @@ -33,6 +33,7 @@ func (bb *BBolter) Migrate(hooks ...func(state types.State, wantedMigrationPoint } for { l := bb.l.With(). + Str("sub-label", "migration"). Interface("state", state). Int("wanted-migration-point", wantedMigrationPoint). Logger() @@ -93,7 +94,7 @@ func (bb *BBolter) Migrate(hooks ...func(state types.State, wantedMigrationPoint } for i, hook := range hooks { - l.Debug().Int("hook_no", i).Int("total", len(hooks)).Msg("Running hook") + l.Debug().Int("hook-no", i).Int("total-hooks", len(hooks)).Msg("Running migration-hook") err := hook(*state, wantedMigrationPoint) if err != nil { return *state, err diff --git a/bboltStorage/project.go b/bboltStorage/project.go index e3e0bc5..3fad317 100644 --- a/bboltStorage/project.go +++ b/bboltStorage/project.go @@ -166,34 +166,44 @@ func (bb *BBolter) GetProjectByShortName(shortName string) (*types.Project, erro } func (bb *BBolter) GetProjectFilter(filter ...types.Project) (*types.Project, error) { - var u *types.Project - err := bb.Iterate(BucketProject, func(key, b []byte) bool { - var uu types.Project - err := bb.Unmarshal(b, &uu) - if err != nil { - bb.l.Error().Err(err).Msg("failed to unmarshal user") - return false - } + return FindOne(bb, BucketProject, func(t types.Project) bool { for _, f := range filter { - if f.ID != "" && f.ID != uu.ID { - continue + if f.OrganizationID == "" { + bb.l.Warn().Msg("Received a user-filter without organization-id") } - if f.ShortName != "" && f.ShortName != uu.ShortName { - continue + if projectFilter(f, t) { + return true } - if f.Description != "" && f.Description != uu.Description { - continue - } - u = &uu - return true } return false }) - if err != nil { - if err == ErrNotFound { - return nil, nil +} +func (bb *BBolter) FindProjects(max int, filter ...types.Project) (map[string]types.Project, error) { + return Find(bb, BucketProject, max, func(uu types.Project) bool { + if len(filter) == 0 { + return true } - return nil, err + for _, f := range filter { + if projectFilter(f, uu) { + return true + } + } + return false + }) +} + +func projectFilter(f, uu types.Project) bool { + if f.OrganizationID != "" && f.OrganizationID != uu.OrganizationID { + return false + } + if f.ID != "" && f.ID != uu.ID { + return false + } + if f.ShortName != "" && f.ShortName != uu.ShortName { + return false + } + if f.ID != "" && f.ID != uu.ID { + return false } - return u, err + return true } diff --git a/handlers/apiHandler.go b/handlers/apiHandler.go index e5741ec..b43dab0 100644 --- a/handlers/apiHandler.go +++ b/handlers/apiHandler.go @@ -574,6 +574,12 @@ func EndpointsHandler( rc.WriteErr(err, requestContext.CodeErrTranslation) return } + p, err := t.GetProject(ctx.DB) + if err != nil { + ctx.L.Error().Err(err).Msg("Project was not found for translation") + rc.WriteErr(err, requestContext.CodeErrTranslation) + return + } if t == nil { rc.WriteErr(ErrApiNotFound("Translation", tv.TranslationID), "") } @@ -588,11 +594,16 @@ func EndpointsHandler( rc.WriteErr(ErrApiDatabase("translation", err), "translation") return } + o, err := importexport.CreateInterpolationMapForOrganization(ctx.DB, session.Organization.ID) + if err != nil { + ctx.L.Error().Err(err).Msg("Failed during CreateInterpolationMapForOrganization") + } _, err = UpdateTranslationFromInferrence( ctx.DB, et, []AdditionalValue{ {Value: tv.Value, LocaleID: tv.LocaleID}}, + o.ByProject(p.ID), ) if err != nil { ctx.L.Error().Err(err).Msg("Failed in updateTranslationFromInferrence") @@ -640,6 +651,12 @@ func EndpointsHandler( if t == nil { rc.WriteErr(ErrApiNotFound("Translation", exTV.TranslationID), "") } + p, err := t.GetProject(ctx.DB) + if err != nil { + ctx.L.Error().Err(err).Msg("Project was not found for translation") + rc.WriteErr(err, requestContext.CodeErrTranslation) + return + } et, err := t.Extend(ctx.DB) if err != nil { rc.WriteErr(err, requestContext.CodeErrTranslation) @@ -650,10 +667,18 @@ func EndpointsHandler( rc.WriteErr(ErrApiDatabase("translation", err), "translation") return } + o, err := importexport.CreateInterpolationMapForOrganization(ctx.DB, session.Organization.ID) + if err != nil { + ctx.L.Error().Err(err).Msg("Failed during CreateInterpolationMapForOrganization") + } _, err = UpdateTranslationFromInferrence( - ctx.DB, et, []AdditionalValue{ + ctx.DB, + et, + []AdditionalValue{ {Value: tv.Value, LocaleID: exTV.LocaleID, Context: j.ContextKey}, - }) + }, + o.ByProject(p.ID), + ) if err != nil { ctx.L.Error().Err(err).Msg("Failed in updateTranslationFromInferrence") } @@ -718,7 +743,7 @@ type AdditionalValue struct { // The additionalValues are meant to be new values to consider for inferrence. // If the Translation already has a value for the same LocaleID/Context as an additionalValue, // the existing value will not be considered. -func UpdateTranslationFromInferrence(db types.Storage, et types.ExtendedTranslation, additionalValues []AdditionalValue) (*types.Translation, error) { +func UpdateTranslationFromInferrence(db types.Storage, et types.ExtendedTranslation, additionalValues []AdditionalValue, interpolationMaps []map[string]interface{}) (*types.Translation, error) { var allValues []string for _, v := range et.Values { @@ -757,7 +782,7 @@ func UpdateTranslationFromInferrence(db types.Storage, et types.ExtendedTranslat allValues = append(allValues, av.Value) } // Check if we can infer some more variables/refs, and if can, we may need to update the Translation. - _, variables, refs := importexport.InferVariablesFromMultiple(allValues, "???", et.ID) + _, variables, refs := importexport.InferVariablesFromMultiple(allValues, "???", et.ID, interpolationMaps) if len(variables) == 0 && len(refs) == 0 { return nil, nil } diff --git a/importexport/import.go b/importexport/import.go index 09a242a..dca95b7 100644 --- a/importexport/import.go +++ b/importexport/import.go @@ -178,7 +178,7 @@ func translationFromNode(t *types.ExtendedTranslation, key string, base types.Pr tv.CreatedBy = base.CreatedBy tv.OrganizationID = base.OrganizationID - w, variables, refs := InferVariables(value, categoryKey, t.Key) + w, variables, refs := InferVariables(value, categoryKey, t.Key, []map[string]interface{}{interpolator.DefaultInterpolationExamples}) if len(w) != 0 { warnings = append(warnings, w...) } @@ -241,13 +241,110 @@ var ( inferVariablesRegex = regexp.MustCompile(`{{\s*([^\s,}]*)[^}]*}}`) ) -func InferVariablesFromMultiple(translationValues []string, category, translation string) ([]Warning, map[string]interface{}, []string) { +type orgInterpolatorMap struct { + mapsByProject map[string]map[string]interface{} + orgMap map[string]interface{} +} + +func (o orgInterpolatorMap) ForOrganization() []map[string]interface{} { + var m []map[string]interface{} + if len(o.orgMap) > 0 { + m = append(m, o.orgMap) + } + m = append(m, interpolator.DefaultInterpolationExamples) + return m + +} +func (o orgInterpolatorMap) ByProject(id string) []map[string]interface{} { + var m []map[string]interface{} + // If there are zero or one projects, we don't want to use the projectMap, + // since it would be equal to the orgMap. + if len(o.mapsByProject) >= 2 { + projectMap := o.mapsByProject[id] + if len(projectMap) > 0 { + m = append(m, projectMap) + } + } + if len(o.orgMap) > 0 { + m = append(m, o.orgMap) + } + + m = append(m, interpolator.DefaultInterpolationExamples) + return m +} + +// Creates an prioritized interpolationmap from an organization +func CreateInterpolationMapForOrganization(db types.Storage, orgID string) (orgInterpolatorMap, error) { + if orgID == "" { + return orgInterpolatorMap{}, fmt.Errorf("Missing orgID") + } + o := orgInterpolatorMap{ + orgMap: map[string]interface{}{}, + mapsByProject: map[string]map[string]interface{}{}, + } + projectFilter := types.Project{} + projectFilter.OrganizationID = orgID + + projects, err := db.FindProjects(0, projectFilter) + if err != nil { + return o, err + } + hasMultipleProjects := len(projects) >= 0 + + catFilter := types.CategoryFilter{} + catFilter.OrganizationID = orgID + var categories map[string]types.Category + if hasMultipleProjects { + cs, err := db.FindCategories(0, catFilter) + if err != nil { + return o, err + } + categories = cs + + } + whichProject := func(t types.Translation) string { + c, ok := categories[t.CategoryID] + if !ok { + return "" + } + return c.ProjectID + } + + filter := types.Translation{} + filter.OrganizationID = orgID + pt, err := db.GetTranslationsFilter(0, filter) + if err != nil { + return o, err + } + for _, t := range pt { + if t.Variables == nil { + continue + } + pid := whichProject(t) + for k, v := range t.Variables { + if v == "" || v == "???" { + continue + } + o.orgMap[k] = v + if pid != "" { + if _, ok := o.mapsByProject[pid]; ok { + o.mapsByProject[pid][k] = v + } else { + o.mapsByProject[pid] = map[string]interface{}{k: v} + } + } + } + } + return o, nil +} + +func InferVariablesFromMultiple(translationValues []string, category, translation string, interpolationMaps []map[string]interface{}) ([]Warning, map[string]interface{}, []string) { w := []Warning{} variables := make(map[string]interface{}) refs := []string{} for _, v := range translationValues { - wx, vx, rx := InferVariables(v, category, translation) + wx, vx, rx := InferVariables(v, category, translation, interpolationMaps) w = append(w, wx...) loop_rx: for _, r := range rx { @@ -271,7 +368,7 @@ func InferVariablesFromMultiple(translationValues []string, category, translatio return w, variables, refs } -func InferVariables(translationValue, category, translation string) ([]Warning, map[string]interface{}, []string) { +func InferVariables(translationValue, category, translation string, interpolationMaps []map[string]interface{}) ([]Warning, map[string]interface{}, []string) { w := []Warning{} variables := make(map[string]interface{}) refs := []string{} @@ -283,7 +380,7 @@ func InferVariables(translationValue, category, translation string) ([]Warning, if len(v) < 2 { continue } - variables[v[1]] = getValueForVariableKey(v[1]) + variables[v[1]] = getValueForVariableKey(v[1], interpolationMaps) } } @@ -334,7 +431,7 @@ func InferVariables(translationValue, category, translation string) ([]Warning, if _, ok := variables[key]; ok { continue } - variables[key] = getValueForVariableKey(key) + variables[key] = getValueForVariableKey(key, interpolationMaps) } } @@ -391,7 +488,7 @@ func InferVariables(translationValue, category, translation string) ([]Warning, continue } - variables[key] = getValueForVariableKey(key) + variables[key] = getValueForVariableKey(key, interpolationMaps) } } sort.Strings(refs) @@ -550,14 +647,16 @@ func ImportI18NTranslation( } return &imp, w, nil } -func getValueForVariableKey(key string) interface{} { +func getValueForVariableKey(key string, interpolationMaps []map[string]interface{}) interface{} { key = strings.ToLower(key) - if val, ok := interpolator.DefaultInterpolationExamples[key]; ok { - return val - } - for k, v := range interpolator.DefaultInterpolationExamples { - if strings.HasSuffix(key, k) { - return v + for _, m := range interpolationMaps { + if val, ok := m[key]; ok { + return val + } + for k, v := range m { + if strings.HasSuffix(key, k) { + return v + } } } return "???" diff --git a/main.go b/main.go index ad5214d..e97d246 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ import ( cfg "github.com/runar-rkmedia/skiver/config" "github.com/runar-rkmedia/skiver/frontend" "github.com/runar-rkmedia/skiver/handlers" + "github.com/runar-rkmedia/skiver/importexport" "github.com/runar-rkmedia/skiver/localuser" "github.com/runar-rkmedia/skiver/models" "github.com/runar-rkmedia/skiver/requestContext" @@ -380,18 +381,29 @@ func main() { func(state types.State, wantedMigrationPoint int) error { if state.MigrationPoint == 0 { - projects, err := db.GetProjects() + orgs, err := db.GetOrganizations() if err != nil { return err } - for _, p := range projects { - ep, err := p.Extend(&db) + for _, org := range orgs { + + o, err := importexport.CreateInterpolationMapForOrganization(&db, org.ID) + if err != nil { + return err + } + projects, err := db.GetProjects() if err != nil { return err } - for _, ec := range ep.Categories { - for _, et := range ec.Translations { - handlers.UpdateTranslationFromInferrence(&db, et, []handlers.AdditionalValue{}) + for _, p := range projects { + ep, err := p.Extend(&db) + if err != nil { + return err + } + for _, ec := range ep.Categories { + for _, et := range ec.Translations { + handlers.UpdateTranslationFromInferrence(&db, et, []handlers.AdditionalValue{}, o.ByProject(p.ID)) + } } } } diff --git a/types/project.go b/types/project.go index f44dcaf..5921bdf 100644 --- a/types/project.go +++ b/types/project.go @@ -39,3 +39,10 @@ type LocaleSetting struct { // TODO: implement organization-settings AutoTranslation bool `json:"auto_translation"` } + +func (e Project) Namespace() string { + return e.Kind() +} +func (e Project) Kind() string { + return string(PubTypeProject) +} diff --git a/types/storage.go b/types/storage.go index b5e524e..188ccdf 100644 --- a/types/storage.go +++ b/types/storage.go @@ -33,6 +33,7 @@ type Storage interface { UpdateProject(id string, project Project) (Project, error) GetProjects() (map[string]Project, error) GetProjectByIDOrShortName(shortNameOrId string) (*Project, error) + FindProjects(max int, filter ...Project) (map[string]Project, error) GetTranslation(ID string) (*Translation, error) SoftDeleteTranslation(id string, byUser string, deleteDate *time.Time) (Translation, error) @@ -46,6 +47,7 @@ type Storage interface { CreateCategory(category Category) (Category, error) GetCategories() (map[string]Category, error) UpdateCategory(id string, category Category) (Category, error) + FindCategories(max int, filter ...CategoryFilter) (map[string]Category, error) GetTranslationValue(ID string) (*TranslationValue, error) CreateTranslationValue(translationValue TranslationValue) (TranslationValue, error) diff --git a/types/translations.go b/types/translations.go index df4b994..a34e044 100644 --- a/types/translations.go +++ b/types/translations.go @@ -1,5 +1,7 @@ package types +import "fmt" + // # See https://en.wikipedia.org/wiki/Language_code for more information // TODO: consider supporting other standards here, like Windows(?), which seem to have their own thing. type Locale struct { @@ -65,6 +67,24 @@ func (e Locale) Namespace() string { func (e Locale) Kind() string { return string(PubTypeLocale) } +func (e Translation) GetProject(db Storage) (Project, error) { + if e.CategoryID == "" { + return Project{}, fmt.Errorf("Translation unexpectedly does not have a CategoryID") + } + c, err := db.GetCategory(e.CategoryID) + if err != nil { + return Project{}, err + } + p, err := db.GetProject(c.ProjectID) + if err != nil { + return Project{}, err + } + if p == nil { + return Project{}, fmt.Errorf("Project unexpectedly not found") + } + + return *p, nil +} type CreatorSource string