feat: RegisterCommands accepts locale FS for automatic translation loading
Some checks failed
Deploy / build (push) Failing after 4s
Security Scan / security (push) Successful in 15s

Both WithCommands() and RegisterCommands() now accept an optional
fs.FS for translations. The CLI collects them via RegisteredLocales()
and the i18n service loads them on startup.

Packages just pass their embed.FS — no i18n import needed:
  cli.RegisterCommands(AddDevCommands, locales.FS)

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-17 01:35:04 +00:00
parent bf994fab17
commit ee7e9d1abf

View file

@ -3,6 +3,7 @@ package cli
import ( import (
"context" "context"
"io/fs"
"iter" "iter"
"sync" "sync"
@ -18,15 +19,24 @@ import (
// cli.WithCommands("config", config.AddConfigCommands), // cli.WithCommands("config", config.AddConfigCommands),
// cli.WithCommands("doctor", doctor.AddDoctorCommands), // cli.WithCommands("doctor", doctor.AddDoctorCommands),
// ) // )
func WithCommands(name string, register func(root *Command)) core.Option { // WithCommands creates a framework Option that registers a command group.
// Optionally pass a locale fs.FS as the third argument to provide translations.
//
// cli.WithCommands("dev", dev.AddDevCommands, locales.FS)
func WithCommands(name string, register func(root *Command), localeFS ...fs.FS) core.Option {
return core.WithName("cmd."+name, func(c *core.Core) (any, error) { return core.WithName("cmd."+name, func(c *core.Core) (any, error) {
return &commandService{core: c, register: register}, nil svc := &commandService{core: c, register: register}
if len(localeFS) > 0 {
svc.localeFS = localeFS[0]
}
return svc, nil
}) })
} }
type commandService struct { type commandService struct {
core *core.Core core *core.Core
register func(root *Command) register func(root *Command)
localeFS fs.FS
} }
func (s *commandService) OnStartup(_ context.Context) error { func (s *commandService) OnStartup(_ context.Context) error {
@ -36,6 +46,11 @@ func (s *commandService) OnStartup(_ context.Context) error {
return nil return nil
} }
// Locales implements core.LocaleProvider.
func (s *commandService) Locales() fs.FS {
return s.localeFS
}
// CommandRegistration is a function that adds commands to the root. // CommandRegistration is a function that adds commands to the root.
type CommandRegistration func(root *cobra.Command) type CommandRegistration func(root *cobra.Command)
@ -43,22 +58,24 @@ var (
registeredCommands []CommandRegistration registeredCommands []CommandRegistration
registeredCommandsMu sync.Mutex registeredCommandsMu sync.Mutex
commandsAttached bool commandsAttached bool
registeredLocales []fs.FS
) )
// RegisterCommands registers a function that adds commands to the CLI. // RegisterCommands registers a function that adds commands to the CLI.
// Call this in your package's init() to register commands. // Optionally pass a locale fs.FS to provide translations for the commands.
// //
// func init() { // func init() {
// cli.RegisterCommands(AddCommands) // cli.RegisterCommands(AddCommands, locales.FS)
// } // }
// func RegisterCommands(fn CommandRegistration, localeFS ...fs.FS) {
// func AddCommands(root *cobra.Command) {
// root.AddCommand(myCmd)
// }
func RegisterCommands(fn CommandRegistration) {
registeredCommandsMu.Lock() registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock() defer registeredCommandsMu.Unlock()
registeredCommands = append(registeredCommands, fn) registeredCommands = append(registeredCommands, fn)
for _, lfs := range localeFS {
if lfs != nil {
registeredLocales = append(registeredLocales, lfs)
}
}
// If commands already attached (CLI already running), attach immediately // If commands already attached (CLI already running), attach immediately
if commandsAttached && instance != nil && instance.root != nil { if commandsAttached && instance != nil && instance.root != nil {
@ -66,6 +83,13 @@ func RegisterCommands(fn CommandRegistration) {
} }
} }
// RegisteredLocales returns all locale filesystems registered by command packages.
func RegisteredLocales() []fs.FS {
registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock()
return registeredLocales
}
// RegisteredCommands returns an iterator over the registered command functions. // RegisteredCommands returns an iterator over the registered command functions.
func RegisteredCommands() iter.Seq[CommandRegistration] { func RegisteredCommands() iter.Seq[CommandRegistration] {
return func(yield func(CommandRegistration) bool) { return func(yield func(CommandRegistration) bool) {