238 lines
6.0 KiB
Go
238 lines
6.0 KiB
Go
package i18n
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"strings"
|
|
"sync"
|
|
|
|
"git.ma-al.com/goc_marek/timetracker/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))
|
|
}
|
|
}
|