package i18n import ( "errors" "fmt" "html/template" "strings" "sync" "git.ma-al.com/goc_daniel/b2b/app/model" "github.com/gofiber/fiber/v3" ) type I18nTranslation string type TranslationResponse map[uint]map[string]map[string]map[string]string var TransStore = newTranslationsStore() var ( ErrLangIsoEmpty = errors.New("lang_id_empty") ErrScopeEmpty = errors.New("scope_empty") ErrComponentEmpty = errors.New("component_empty") ErrKeyEmpty = errors.New("key_empty") ErrLangIsoNotFoundInCache = errors.New("lang_id_not_in_cache") ErrScopeNotFoundInCache = errors.New("scope_not_in_cache") ErrComponentNotFoundInCache = errors.New("component_not_in_cache") ErrKeyNotFoundInCache = errors.New("key_invalid_in_cache") ) type TranslationsStore struct { mutex sync.RWMutex cache TranslationResponse } func newTranslationsStore() *TranslationsStore { service := &TranslationsStore{ cache: make(TranslationResponse), } return service } func (s *TranslationsStore) Get(langID uint, scope string, component string, key string) (string, error) { s.mutex.RLock() defer s.mutex.RUnlock() if langID == 0 { return "lang_id_empty", ErrLangIsoEmpty } if _, ok := s.cache[langID]; !ok { return fmt.Sprintf("lang_id_not_in_cache: %d", langID), ErrLangIsoNotFoundInCache } if scope == "" { return "scope_empty", ErrScopeEmpty } if _, ok := s.cache[langID][scope]; !ok { return fmt.Sprintf("scope_not_in_cache: %s", scope), ErrScopeNotFoundInCache } if component == "" { return "component_empty", ErrComponentEmpty } if _, ok := s.cache[langID][scope][component]; !ok { return fmt.Sprintf("component_not_in_cache: %s", component), ErrComponentNotFoundInCache } if key == "" { return "key_empty", ErrKeyEmpty } if _, ok := s.cache[langID][scope][component][key]; !ok { return fmt.Sprintf("key_invalid_in_cache: %s", key), ErrKeyNotFoundInCache } return s.cache[langID][scope][component][key], nil } func (s *TranslationsStore) GetTranslations(langID uint, scope string, components []string) (*TranslationResponse, error) { s.mutex.RLock() defer s.mutex.RUnlock() if langID == 0 { resp := make(TranslationResponse) for k, v := range s.cache { resp[k] = v } return &resp, nil } if _, ok := s.cache[langID]; !ok { return nil, fmt.Errorf("invalid translation arguments") } tr := make(TranslationResponse) if scope == "" { tr[langID] = s.cache[langID] return &tr, nil } if _, ok := s.cache[langID][scope]; !ok { return nil, fmt.Errorf("invalid translation arguments") } tr[langID] = make(map[string]map[string]map[string]string) if len(components) <= 0 { tr[langID][scope] = s.cache[langID][scope] return &tr, nil } tr[langID][scope] = make(map[string]map[string]string) var invalidComponents []string for _, component := range components { if _, ok := s.cache[langID][scope][component]; !ok { invalidComponents = append(invalidComponents, component) continue } tr[langID][scope][component] = s.cache[langID][scope][component] } if len(invalidComponents) > 0 { return &tr, fmt.Errorf("invalid translation arguments") } return &tr, nil } // GetAllTranslations returns all translations from the cache func (s *TranslationsStore) GetAllTranslations() *TranslationResponse { s.mutex.RLock() defer s.mutex.RUnlock() resp := make(TranslationResponse) for k, v := range s.cache { resp[k] = v } return &resp } func (s *TranslationsStore) LoadTranslations(translations []model.Translation) error { s.mutex.Lock() defer s.mutex.Unlock() s.cache = make(TranslationResponse) for _, t := range translations { lang := uint(t.LangID) scp := t.Scope.Name cmp := t.Component.Name data := "" if t.Data != nil { data = *t.Data } if _, ok := s.cache[lang]; !ok { s.cache[lang] = make(map[string]map[string]map[string]string) } if _, ok := s.cache[lang][scp]; !ok { s.cache[lang][scp] = make(map[string]map[string]string) } if _, ok := s.cache[lang][scp][cmp]; !ok { s.cache[lang][scp][cmp] = make(map[string]string) } s.cache[lang][scp][cmp][t.Key] = data } return nil } // ReloadTranslations reloads translations from the database func (s *TranslationsStore) ReloadTranslations(translations []model.Translation) error { return s.LoadTranslations(translations) } // T_ is meant to be used to translate error messages and other system communicates. func T_[T ~string](c fiber.Ctx, key T, params ...interface{}) string { if langID, ok := c.Locals("langID").(uint); ok { parts := strings.Split(string(key), ".") if len(parts) >= 2 { if trans, err := TransStore.Get(langID, "Backend", parts[0], strings.Join(parts[1:], ".")); err == nil { return Format(trans, params...) } } else { if trans, err := TransStore.Get(langID, "Backend", "be", parts[0]); err == nil { return Format(trans, params...) } } } return string(key) } // T___ works exactly the same as T_ but uses just language ID instead of the whole context func T___[T ~string](langId uint, key T, params ...interface{}) string { parts := strings.Split(string(key), ".") if len(parts) >= 2 { if trans, err := TransStore.Get(langId, "Backend", parts[0], strings.Join(parts[1:], ".")); err == nil { return Format(trans, params...) } } else { if trans, err := TransStore.Get(langId, "Backend", "be", parts[0]); err == nil { return Format(trans, params...) } } return string(key) } func Format(text string, params ...interface{}) string { text = fmt.Sprintf(text, params...) return text } // T__ wraps T_ adding a conversion from string to template.HTML func T__(c fiber.Ctx, key string, params ...interface{}) template.HTML { return template.HTML(T_(c, key, params...)) } // MapKeyOnTranslationMap is a helper function to map keys on translation map // this is used to map keys on translation map // // example: // map := map[T]string{} // MapKeyOnTranslationMap(ctx, map, key1, key2, key3) func MapKeyOnTranslationMap[T ~string](c fiber.Ctx, m *map[T]string, key ...T) { if *m == nil { *m = make(map[T]string) } for _, k := range key { (*m)[k] = T_(c, string(k)) } }