verstak/internal/i18n/catalog.go

93 lines
1.7 KiB
Go

package i18n
import (
"embed"
"encoding/json"
"fmt"
"strings"
"sync"
)
//go:embed locales/*.json
var localeFS embed.FS
var (
mu sync.RWMutex
cache = map[string]map[string]string{}
defaultLocale = "ru"
)
// T returns a localized string for the given locale and key.
// It supports printf-style formatting via args.
// Falls back: key not found -> ru -> key itself.
func T(locale, key string, args ...any) string {
mu.RLock()
catalog, ok := cache[locale]
mu.RUnlock()
if !ok {
catalog = loadLocale(locale)
}
msg, ok := catalog[key]
if !ok && locale != defaultLocale {
mu.RLock()
ruCatalog, ruOK := cache[defaultLocale]
mu.RUnlock()
if ruOK {
msg = ruCatalog[key]
}
if msg == "" {
msg = key
}
}
if msg == "" {
msg = key
}
if len(args) > 0 {
return fmt.Sprintf(msg, args...)
}
return msg
}
// TF is a shorthand for T with formatting.
func TF(locale, key string, args ...any) string {
return T(locale, key, args...)
}
// SetDefault changes the default locale.
func SetDefault(locale string) {
mu.Lock()
defaultLocale = locale
mu.Unlock()
}
// AvailableLocales returns locale names for which files exist.
func AvailableLocales() []string {
entries, err := localeFS.ReadDir("locales")
if err != nil {
return nil
}
var out []string
for _, e := range entries {
name := e.Name()
name = strings.TrimSuffix(name, ".json")
out = append(out, name)
}
return out
}
func loadLocale(locale string) map[string]string {
data, err := localeFS.ReadFile("locales/" + locale + ".json")
if err != nil {
data = []byte("{}")
}
var m map[string]string
json.Unmarshal(data, &m)
if m == nil {
m = map[string]string{}
}
mu.Lock()
cache[locale] = m
mu.Unlock()
return m
}