From f2dd3473b363fa5f72e44d3ed0bb21141418e354 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 05:36:42 +0000 Subject: [PATCH] feat(i18n): add locale provider registry Co-Authored-By: Virgil --- hooks.go | 51 +++++++++++++++++++++++++++++++++++++---- hooks_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/hooks.go b/hooks.go index bbb1fc9..8566ae8 100644 --- a/hooks.go +++ b/hooks.go @@ -20,10 +20,16 @@ type localeRegistration struct { dir string } +// LocaleProvider supplies one or more locale filesystems to the default service. +type LocaleProvider interface { + LocaleSources() []FSSource +} + var ( - registeredLocales []localeRegistration - registeredLocalesMu sync.Mutex - localesLoaded bool + registeredLocales []localeRegistration + registeredLocaleProviders []LocaleProvider + registeredLocalesMu sync.Mutex + localesLoaded bool ) // RegisterLocales registers a filesystem containing locale files. @@ -46,15 +52,50 @@ func RegisterLocales(fsys fs.FS, dir string) { } } +// 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 + } + registeredLocalesMu.Lock() + registeredLocaleProviders = append(registeredLocaleProviders, provider) + registeredLocalesMu.Unlock() + if svc := defaultService.Load(); svc != nil { + loadLocaleProvider(svc, provider) + } +} + func loadRegisteredLocales(svc *Service) { registeredLocalesMu.Lock() - defer registeredLocalesMu.Unlock() - for _, reg := range registeredLocales { + locales := append([]localeRegistration(nil), registeredLocales...) + providers := append([]LocaleProvider(nil), registeredLocaleProviders...) + registeredLocalesMu.Unlock() + + for _, reg := range locales { if err := svc.LoadFS(reg.fsys, reg.dir); err != nil { log.Printf("i18n: loadRegisteredLocales failed to load %q: %v", reg.dir, err) } } + for _, provider := range providers { + loadLocaleProvider(svc, provider) + } + + registeredLocalesMu.Lock() localesLoaded = true + registeredLocalesMu.Unlock() +} + +func loadLocaleProvider(svc *Service, provider LocaleProvider) { + if svc == nil || provider == nil { + return + } + for _, src := range provider.LocaleSources() { + if err := svc.LoadFS(src.FS, src.Dir); err != nil { + log.Printf("i18n: loadLocaleProvider failed to load %q: %v", src.Dir, err) + } + } } // OnMissingKey registers a handler for missing translation keys. diff --git a/hooks_test.go b/hooks_test.go index 7d628f3..42e5046 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -10,17 +10,28 @@ import ( "github.com/stretchr/testify/require" ) +type testLocaleProvider struct { + sources []FSSource +} + +func (p testLocaleProvider) LocaleSources() []FSSource { + return p.sources +} + func TestRegisterLocales_Good(t *testing.T) { // Save and restore registered locales state registeredLocalesMu.Lock() savedLocales := registeredLocales + savedProviders := registeredLocaleProviders savedLoaded := localesLoaded registeredLocales = nil + registeredLocaleProviders = nil localesLoaded = false registeredLocalesMu.Unlock() defer func() { registeredLocalesMu.Lock() registeredLocales = savedLocales + registeredLocaleProviders = savedProviders localesLoaded = savedLoaded registeredLocalesMu.Unlock() }() @@ -39,6 +50,41 @@ func TestRegisterLocales_Good(t *testing.T) { assert.Equal(t, 1, count, "should have 1 registered locale") } +func TestRegisterLocaleProvider_Good(t *testing.T) { + registeredLocalesMu.Lock() + savedLocales := registeredLocales + savedProviders := registeredLocaleProviders + savedLoaded := localesLoaded + registeredLocales = nil + registeredLocaleProviders = nil + localesLoaded = false + registeredLocalesMu.Unlock() + defer func() { + registeredLocalesMu.Lock() + registeredLocales = savedLocales + registeredLocaleProviders = savedProviders + localesLoaded = savedLoaded + registeredLocalesMu.Unlock() + }() + + svc, err := New() + require.NoError(t, err) + SetDefault(svc) + + fs := fstest.MapFS{ + "locales/en.json": &fstest.MapFile{ + Data: []byte(`{"provider.loaded": "loaded from provider"}`), + }, + } + + RegisterLocaleProvider(testLocaleProvider{ + sources: []FSSource{{FS: fs, Dir: "locales"}}, + }) + + got := svc.T("provider.loaded") + assert.Equal(t, "loaded from provider", got) +} + func TestRegisterLocales_Good_AfterLocalesLoaded(t *testing.T) { // When localesLoaded is true, RegisterLocales should also call LoadFS immediately svc, err := New() @@ -49,13 +95,16 @@ func TestRegisterLocales_Good_AfterLocalesLoaded(t *testing.T) { // Save and restore state registeredLocalesMu.Lock() savedLocales := registeredLocales + savedProviders := registeredLocaleProviders savedLoaded := localesLoaded registeredLocales = nil + registeredLocaleProviders = nil localesLoaded = true // Simulate already loaded registeredLocalesMu.Unlock() defer func() { registeredLocalesMu.Lock() registeredLocales = savedLocales + registeredLocaleProviders = savedProviders localesLoaded = savedLoaded registeredLocalesMu.Unlock() }() @@ -81,13 +130,16 @@ func TestRegisterLocales_Good_WithInitializedDefaultService(t *testing.T) { registeredLocalesMu.Lock() savedLocales := registeredLocales + savedProviders := registeredLocaleProviders savedLoaded := localesLoaded registeredLocales = nil + registeredLocaleProviders = nil localesLoaded = false registeredLocalesMu.Unlock() defer func() { registeredLocalesMu.Lock() registeredLocales = savedLocales + registeredLocaleProviders = savedProviders localesLoaded = savedLoaded registeredLocalesMu.Unlock() }() @@ -107,13 +159,16 @@ func TestRegisterLocales_Good_WithInitializedDefaultService(t *testing.T) { func TestSetDefault_Good_LoadsQueuedRegisteredLocales(t *testing.T) { registeredLocalesMu.Lock() savedLocales := registeredLocales + savedProviders := registeredLocaleProviders savedLoaded := localesLoaded registeredLocales = nil + registeredLocaleProviders = nil localesLoaded = false registeredLocalesMu.Unlock() defer func() { registeredLocalesMu.Lock() registeredLocales = savedLocales + registeredLocaleProviders = savedProviders localesLoaded = savedLoaded registeredLocalesMu.Unlock() }() @@ -137,8 +192,10 @@ func TestInit_LoadsRegisteredLocales(t *testing.T) { // Save and restore global service state. registeredLocalesMu.Lock() savedLocales := registeredLocales + savedProviders := registeredLocaleProviders savedLoaded := localesLoaded registeredLocales = nil + registeredLocaleProviders = nil localesLoaded = false registeredLocalesMu.Unlock() @@ -148,6 +205,7 @@ func TestInit_LoadsRegisteredLocales(t *testing.T) { defer func() { registeredLocalesMu.Lock() registeredLocales = savedLocales + registeredLocaleProviders = savedProviders localesLoaded = savedLoaded registeredLocalesMu.Unlock() defaultService.Store(nil) @@ -211,6 +269,7 @@ func TestInit_ReDetectsRegisteredLocales(t *testing.T) { registeredLocalesMu.Lock() savedLocales := registeredLocales + savedProviders := registeredLocaleProviders savedLoaded := localesLoaded registeredLocales = nil localesLoaded = false @@ -222,6 +281,7 @@ func TestInit_ReDetectsRegisteredLocales(t *testing.T) { defer func() { registeredLocalesMu.Lock() registeredLocales = savedLocales + registeredLocaleProviders = savedProviders localesLoaded = savedLoaded registeredLocalesMu.Unlock() defaultService.Store(nil) @@ -250,6 +310,7 @@ func TestLoadRegisteredLocales_Good(t *testing.T) { // Save and restore state registeredLocalesMu.Lock() savedLocales := registeredLocales + savedProviders := registeredLocaleProviders savedLoaded := localesLoaded registeredLocales = []localeRegistration{ { @@ -261,11 +322,13 @@ func TestLoadRegisteredLocales_Good(t *testing.T) { dir: "loc", }, } + registeredLocaleProviders = nil localesLoaded = false registeredLocalesMu.Unlock() defer func() { registeredLocalesMu.Lock() registeredLocales = savedLocales + registeredLocaleProviders = savedProviders localesLoaded = savedLoaded registeredLocalesMu.Unlock() }() -- 2.45.3