- Add i18n.RegisterLocales(fsys, dir) for packages to register translations - Locales are automatically loaded when i18n.Init() is called - Fix gram.word.* loading bug (strings were in wrong switch case) - Fix loadJSON to merge messages instead of replacing - Add common.* keys to base locale (labels, flags, progress, etc.) - Add pkg/php/locales with PHP-specific translations - pkg/php/i18n.go registers locales via init() This enables the idiomatic pattern where packages register their locale files and they're automatically loaded by the i18n system. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
// Package i18n provides internationalization for the CLI.
|
|
package i18n
|
|
|
|
import (
|
|
"io/fs"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
var missingKeyHandler atomic.Value // stores MissingKeyHandler
|
|
|
|
// localeRegistration holds a filesystem and directory for locale loading.
|
|
type localeRegistration struct {
|
|
fsys fs.FS
|
|
dir string
|
|
}
|
|
|
|
var (
|
|
registeredLocales []localeRegistration
|
|
registeredLocalesMu sync.Mutex
|
|
localesLoaded bool
|
|
)
|
|
|
|
// RegisterLocales registers a filesystem containing locale files to be loaded.
|
|
// Call this in your package's init() to register translations.
|
|
// Locales are loaded when the i18n service initialises.
|
|
//
|
|
// //go:embed locales/*.json
|
|
// var localeFS embed.FS
|
|
//
|
|
// func init() {
|
|
// i18n.RegisterLocales(localeFS, "locales")
|
|
// }
|
|
func RegisterLocales(fsys fs.FS, dir string) {
|
|
registeredLocalesMu.Lock()
|
|
defer registeredLocalesMu.Unlock()
|
|
registeredLocales = append(registeredLocales, localeRegistration{fsys: fsys, dir: dir})
|
|
|
|
// If locales already loaded (service already running), load immediately
|
|
if localesLoaded {
|
|
if svc := Default(); svc != nil {
|
|
_ = svc.LoadFS(fsys, dir)
|
|
}
|
|
}
|
|
}
|
|
|
|
// loadRegisteredLocales loads all registered locale filesystems into the service.
|
|
// Called by the service during initialisation.
|
|
func loadRegisteredLocales(svc *Service) {
|
|
registeredLocalesMu.Lock()
|
|
defer registeredLocalesMu.Unlock()
|
|
|
|
for _, reg := range registeredLocales {
|
|
_ = svc.LoadFS(reg.fsys, reg.dir)
|
|
}
|
|
localesLoaded = true
|
|
}
|
|
|
|
// OnMissingKey registers a handler for missing translation keys.
|
|
// Called when T() can't find a key in ModeCollect.
|
|
// Thread-safe: can be called concurrently with translations.
|
|
//
|
|
// i18n.SetMode(i18n.ModeCollect)
|
|
// i18n.OnMissingKey(func(m i18n.MissingKey) {
|
|
// log.Printf("MISSING: %s at %s:%d", m.Key, m.CallerFile, m.CallerLine)
|
|
// })
|
|
func OnMissingKey(h MissingKeyHandler) {
|
|
missingKeyHandler.Store(h)
|
|
}
|
|
|
|
// dispatchMissingKey creates and dispatches a MissingKey event.
|
|
// Called internally when a key is missing in ModeCollect.
|
|
func dispatchMissingKey(key string, args map[string]any) {
|
|
v := missingKeyHandler.Load()
|
|
if v == nil {
|
|
return
|
|
}
|
|
h, ok := v.(MissingKeyHandler)
|
|
if !ok || h == nil {
|
|
return
|
|
}
|
|
|
|
_, file, line, ok := runtime.Caller(2) // Skip dispatchMissingKey and handleMissingKey
|
|
if !ok {
|
|
file = "unknown"
|
|
line = 0
|
|
}
|
|
|
|
h(MissingKey{
|
|
Key: key,
|
|
Args: args,
|
|
CallerFile: file,
|
|
CallerLine: line,
|
|
})
|
|
}
|