From c6fae794b35daabb6baf97d29af04ab65357dc16 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 03:53:21 +0000 Subject: [PATCH] fix(cli): load locale sources during registration --- pkg/cli/commands.go | 28 ++++++++++++++++++ pkg/cli/commands_test.go | 63 +++++++++++++++++++++++++++++++++++++++- pkg/cli/runtime.go | 2 ++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index e1d6495..6764ca1 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -7,6 +7,7 @@ import ( "sync" "dappco.re/go/core" + "forge.lthn.ai/core/go-i18n" "github.com/spf13/cobra" ) @@ -19,6 +20,7 @@ import ( // ) func WithCommands(name string, register func(root *Command), localeFS ...fs.FS) CommandSetup { return func(c *core.Core) { + loadLocaleSources(localeSourcesFromFS(localeFS...)...) if root, ok := c.App().Runtime.(*cobra.Command); ok { register(root) } @@ -49,6 +51,7 @@ func RegisterCommands(fn CommandRegistration, localeFS ...fs.FS) { root := instance registeredCommandsMu.Unlock() + loadLocaleSources(localeSourcesFromFS(localeFS...)...) appendLocales(localeFS...) // If commands already attached (CLI already running), attach immediately @@ -73,6 +76,31 @@ func appendLocales(localeFS ...fs.FS) { registeredCommandsMu.Unlock() } +func localeSourcesFromFS(localeFS ...fs.FS) []LocaleSource { + sources := make([]LocaleSource, 0, len(localeFS)) + for _, lfs := range localeFS { + if lfs != nil { + sources = append(sources, LocaleSource{FS: lfs, Dir: "."}) + } + } + return sources +} + +func loadLocaleSources(sources ...LocaleSource) { + svc := i18n.Default() + if svc == nil { + return + } + for _, src := range sources { + if src.FS == nil { + continue + } + if err := svc.AddLoader(i18n.NewFSLoader(src.FS, src.Dir)); err != nil { + LogDebug("failed to load locale source", "dir", src.Dir, "err", err) + } + } +} + // RegisteredLocales returns all locale filesystems registered by command packages. func RegisteredLocales() []fs.FS { registeredCommandsMu.Lock() diff --git a/pkg/cli/commands_test.go b/pkg/cli/commands_test.go index a2f6d1f..d1629b6 100644 --- a/pkg/cli/commands_test.go +++ b/pkg/cli/commands_test.go @@ -3,7 +3,9 @@ package cli import ( "sync" "testing" + "testing/fstest" + "forge.lthn.ai/core/go-i18n" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +18,19 @@ func resetGlobals(t *testing.T) { t.Cleanup(doReset) } +func resetI18nDefault(t *testing.T) { + t.Helper() + + prev := i18n.Default() + svc, err := i18n.New() + require.NoError(t, err) + i18n.SetDefault(svc) + + t.Cleanup(func() { + i18n.SetDefault(prev) + }) +} + // doReset clears all package-level state. Only safe from a single goroutine // with no concurrent RegisterCommands calls in flight (i.e. test setup/teardown). func doReset() { @@ -135,6 +150,53 @@ func TestRegisterCommands_Bad(t *testing.T) { }) } +// TestLocaleLoading_Good verifies locale files become available to the active i18n service. +func TestLocaleLoading_Good(t *testing.T) { + t.Run("Init loads I18nSources", func(t *testing.T) { + resetGlobals(t) + resetI18nDefault(t) + + localeFS := fstest.MapFS{ + "en.json": { + Data: []byte(`{"custom":{"hello":"Hello from locale"}}`), + }, + } + + err := Init(Options{ + AppName: "test", + I18nSources: []LocaleSource{WithLocales(localeFS, ".")}, + }) + require.NoError(t, err) + + assert.Equal(t, "Hello from locale", i18n.T("custom.hello")) + }) + + t.Run("WithCommands loads localeFS before registration", func(t *testing.T) { + resetGlobals(t) + resetI18nDefault(t) + + err := Init(Options{AppName: "test"}) + require.NoError(t, err) + + localeFS := fstest.MapFS{ + "en.json": { + Data: []byte(`{"custom":{"immediate":"Loaded eagerly"}}`), + }, + } + + var observed string + setup := WithCommands("test", func(root *cobra.Command) { + _ = root + observed = i18n.T("custom.immediate") + }, localeFS) + + setup(Core()) + + assert.Equal(t, "Loaded eagerly", observed) + assert.Equal(t, "Loaded eagerly", i18n.T("custom.immediate")) + }) +} + // TestWithAppName_Good tests the app name override. func TestWithAppName_Good(t *testing.T) { t.Run("overrides root command use", func(t *testing.T) { @@ -158,4 +220,3 @@ func TestWithAppName_Good(t *testing.T) { assert.Equal(t, "core", RootCmd().Use) }) } - diff --git a/pkg/cli/runtime.go b/pkg/cli/runtime.go index 17cb6f0..b4aab7e 100644 --- a/pkg/cli/runtime.go +++ b/pkg/cli/runtime.go @@ -110,6 +110,8 @@ func Init(opts Options) error { return } + loadLocaleSources(opts.I18nSources...) + // Attach registered commands AFTER Core startup so i18n is available attachRegisteredCommands(rootCmd) })