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 }