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))
}
}