go-i18n/hooks.go
Virgil 6ca01b37a5
All checks were successful
Security Scan / security (push) Successful in 17s
Test / test (push) Successful in 1m35s
fix(i18n): append missing-key handlers
2026-04-02 14:36:14 +00:00

251 lines
6.1 KiB
Go

package i18n
import (
"io/fs"
"runtime"
"sync"
"sync/atomic"
"dappco.re/go/core"
log "dappco.re/go/core/log"
)
var missingKeyHandler atomic.Value
var missingKeyHandlerMu sync.Mutex
type missingKeyHandlersState struct {
handlers []MissingKeyHandler
}
type localeRegistration struct {
fsys fs.FS
dir string
id int
}
type localeProviderRegistration struct {
provider LocaleProvider
id int
}
// LocaleProvider supplies one or more locale filesystems to the default service.
type LocaleProvider interface {
LocaleSources() []FSSource
}
var (
registeredLocales []localeRegistration
registeredLocaleProviders []localeProviderRegistration
registeredLocalesMu sync.Mutex
localesLoaded bool
nextLocaleRegistrationID int
nextLocaleProviderID int
)
// RegisterLocales registers a filesystem containing locale files.
// Call this in your package's init() to register translations.
//
// //go:embed locales/*.json
// var localeFS embed.FS
//
// func init() {
// i18n.RegisterLocales(localeFS, "locales")
// }
func RegisterLocales(fsys fs.FS, dir string) {
reg := localeRegistration{fsys: fsys, dir: dir}
registeredLocalesMu.Lock()
nextLocaleRegistrationID++
reg.id = nextLocaleRegistrationID
registeredLocales = append(registeredLocales, reg)
svc := defaultService.Load()
registeredLocalesMu.Unlock()
if svc != nil {
if err := svc.LoadFS(fsys, dir); err != nil {
log.Error("i18n: RegisterLocales failed to load", "dir", dir, "err", err)
} else {
svc.markLocaleRegistrationLoaded(reg.id)
markLocalesLoaded()
}
}
}
// RegisterLocaleProvider registers a provider that can contribute locale files.
// This is useful for packages that need to expose multiple locale sources as a
// single unit.
func RegisterLocaleProvider(provider LocaleProvider) {
if provider == nil {
return
}
reg := localeProviderRegistration{provider: provider}
registeredLocalesMu.Lock()
nextLocaleProviderID++
reg.id = nextLocaleProviderID
registeredLocaleProviders = append(registeredLocaleProviders, reg)
svc := defaultService.Load()
registeredLocalesMu.Unlock()
if svc != nil {
loadLocaleProvider(svc, reg)
}
}
func loadRegisteredLocales(svc *Service) {
if svc == nil {
return
}
registeredLocalesMu.Lock()
locales := append([]localeRegistration(nil), registeredLocales...)
providers := append([]localeProviderRegistration(nil), registeredLocaleProviders...)
registeredLocalesMu.Unlock()
for _, reg := range locales {
if svc != nil && svc.hasLocaleRegistrationLoaded(reg.id) {
continue
}
if err := svc.LoadFS(reg.fsys, reg.dir); err != nil {
log.Error("i18n: loadRegisteredLocales failed to load", "dir", reg.dir, "err", err)
continue
}
svc.markLocaleRegistrationLoaded(reg.id)
}
for _, provider := range providers {
if svc != nil && svc.hasLocaleProviderLoaded(provider.id) {
continue
}
loadLocaleProvider(svc, provider)
}
markLocalesLoaded()
}
func loadLocaleProvider(svc *Service, provider localeProviderRegistration) {
if svc == nil || provider.provider == nil {
return
}
for _, src := range provider.provider.LocaleSources() {
if err := svc.LoadFS(src.FS, src.Dir); err != nil {
log.Error("i18n: loadLocaleProvider failed to load", "dir", src.Dir, "err", err)
}
}
svc.markLocaleProviderLoaded(provider.id)
markLocalesLoaded()
}
func markLocalesLoaded() {
registeredLocalesMu.Lock()
localesLoaded = true
registeredLocalesMu.Unlock()
}
// OnMissingKey registers a handler for missing translation keys.
func OnMissingKey(h MissingKeyHandler) {
if h == nil {
ClearMissingKeyHandlers()
return
}
AddMissingKeyHandler(h)
}
// SetMissingKeyHandlers replaces the full missing-key handler chain.
func SetMissingKeyHandlers(handlers ...MissingKeyHandler) {
missingKeyHandlerMu.Lock()
defer missingKeyHandlerMu.Unlock()
handlers = filterNilMissingKeyHandlers(handlers)
if len(handlers) == 0 {
missingKeyHandler.Store(missingKeyHandlersState{})
return
}
missingKeyHandler.Store(missingKeyHandlersState{handlers: handlers})
}
// ClearMissingKeyHandlers removes all registered missing-key handlers.
func ClearMissingKeyHandlers() {
missingKeyHandlerMu.Lock()
defer missingKeyHandlerMu.Unlock()
missingKeyHandler.Store(missingKeyHandlersState{})
}
// AddMissingKeyHandler appends a missing-key handler without replacing any
// existing handlers.
func AddMissingKeyHandler(h MissingKeyHandler) {
if h == nil {
return
}
missingKeyHandlerMu.Lock()
defer missingKeyHandlerMu.Unlock()
current := missingKeyHandlers()
current.handlers = append(current.handlers, h)
missingKeyHandler.Store(current)
}
func filterNilMissingKeyHandlers(handlers []MissingKeyHandler) []MissingKeyHandler {
if len(handlers) == 0 {
return nil
}
filtered := make([]MissingKeyHandler, 0, len(handlers))
for _, h := range handlers {
if h != nil {
filtered = append(filtered, h)
}
}
if len(filtered) == 0 {
return nil
}
return filtered
}
func missingKeyHandlers() missingKeyHandlersState {
v := missingKeyHandler.Load()
if v == nil {
return missingKeyHandlersState{}
}
state, ok := v.(missingKeyHandlersState)
if !ok {
return missingKeyHandlersState{}
}
return state
}
func dispatchMissingKey(key string, args map[string]any) {
state := missingKeyHandlers()
if len(state.handlers) == 0 {
return
}
file, line := missingKeyCaller()
mk := cloneMissingKey(MissingKey{Key: key, Args: args, CallerFile: file, CallerLine: line})
for _, h := range state.handlers {
if h != nil {
h(mk)
}
}
}
func cloneMissingKey(mk MissingKey) MissingKey {
if len(mk.Args) == 0 {
mk.Args = nil
return mk
}
args := make(map[string]any, len(mk.Args))
for key, value := range mk.Args {
args[key] = value
}
mk.Args = args
return mk
}
func missingKeyCaller() (string, int) {
const packagePrefix = "dappco.re/go/core/i18n."
pcs := make([]uintptr, 16)
n := runtime.Callers(2, pcs)
frames := runtime.CallersFrames(pcs[:n])
for {
frame, more := frames.Next()
if !core.HasPrefix(frame.Function, packagePrefix) || core.HasSuffix(frame.File, "_test.go") {
return frame.File, frame.Line
}
if !more {
break
}
}
return "unknown", 0
}