diff --git a/hooks.go b/hooks.go index 8566ae8..2d38c60 100644 --- a/hooks.go +++ b/hooks.go @@ -18,6 +18,12 @@ type missingKeyHandlersState struct { 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. @@ -26,10 +32,12 @@ type LocaleProvider interface { } var ( - registeredLocales []localeRegistration - registeredLocaleProviders []LocaleProvider - registeredLocalesMu sync.Mutex - localesLoaded bool + registeredLocales []localeRegistration + registeredLocaleProviders []localeProviderRegistration + registeredLocalesMu sync.Mutex + localesLoaded bool + nextLocaleRegistrationID int + nextLocaleProviderID int ) // RegisterLocales registers a filesystem containing locale files. @@ -42,12 +50,18 @@ var ( // i18n.RegisterLocales(localeFS, "locales") // } func RegisterLocales(fsys fs.FS, dir string) { + reg := localeRegistration{fsys: fsys, dir: dir} registeredLocalesMu.Lock() - defer registeredLocalesMu.Unlock() - registeredLocales = append(registeredLocales, localeRegistration{fsys: fsys, dir: dir}) - if svc := defaultService.Load(); svc != nil { + 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.Printf("i18n: RegisterLocales failed to load %q: %v", dir, err) + } else { + svc.markLocaleRegistrationLoaded(reg.id) } } } @@ -59,26 +73,41 @@ func RegisterLocaleProvider(provider LocaleProvider) { if provider == nil { return } + reg := localeProviderRegistration{provider: provider} registeredLocalesMu.Lock() - registeredLocaleProviders = append(registeredLocaleProviders, provider) + nextLocaleProviderID++ + reg.id = nextLocaleProviderID + registeredLocaleProviders = append(registeredLocaleProviders, reg) + svc := defaultService.Load() registeredLocalesMu.Unlock() - if svc := defaultService.Load(); svc != nil { - loadLocaleProvider(svc, provider) + if svc != nil { + loadLocaleProvider(svc, reg) } } func loadRegisteredLocales(svc *Service) { + if svc == nil { + return + } registeredLocalesMu.Lock() locales := append([]localeRegistration(nil), registeredLocales...) - providers := append([]LocaleProvider(nil), registeredLocaleProviders...) + 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.Printf("i18n: loadRegisteredLocales failed to load %q: %v", reg.dir, err) + continue } + svc.markLocaleRegistrationLoaded(reg.id) } for _, provider := range providers { + if svc != nil && svc.hasLocaleProviderLoaded(provider.id) { + continue + } loadLocaleProvider(svc, provider) } @@ -87,15 +116,16 @@ func loadRegisteredLocales(svc *Service) { registeredLocalesMu.Unlock() } -func loadLocaleProvider(svc *Service, provider LocaleProvider) { - if svc == nil || provider == nil { +func loadLocaleProvider(svc *Service, provider localeProviderRegistration) { + if svc == nil || provider.provider == nil { return } - for _, src := range provider.LocaleSources() { + for _, src := range provider.provider.LocaleSources() { if err := svc.LoadFS(src.FS, src.Dir); err != nil { log.Printf("i18n: loadLocaleProvider failed to load %q: %v", src.Dir, err) } } + svc.markLocaleProviderLoaded(provider.id) } // OnMissingKey registers a handler for missing translation keys. diff --git a/hooks_test.go b/hooks_test.go index 2954f0f..a4f2b65 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -187,6 +187,43 @@ func TestSetDefault_Good_LoadsQueuedRegisteredLocales(t *testing.T) { assert.Equal(t, "loaded via setdefault", got) } +func TestSetDefault_Good_LoadsRegisteredLocalesIntoFreshService(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() + }() + + fs := fstest.MapFS{ + "locales/en.json": &fstest.MapFile{ + Data: []byte(`{"fresh.registration": "fresh value"}`), + }, + } + RegisterLocales(fs, "locales") + + first, err := New() + require.NoError(t, err) + SetDefault(first) + require.Equal(t, "fresh value", first.T("fresh.registration")) + + second, err := New() + require.NoError(t, err) + SetDefault(second) + + got := second.T("fresh.registration") + assert.Equal(t, "fresh value", got) +} + func TestInit_LoadsRegisteredLocales(t *testing.T) { // Save and restore global service state. registeredLocalesMu.Lock() diff --git a/service.go b/service.go index 7900073..28426be 100644 --- a/service.go +++ b/service.go @@ -30,6 +30,8 @@ type Service struct { formality Formality location string handlers []KeyHandler + loadedLocales map[int]struct{} + loadedProviders map[int]struct{} mu sync.RWMutex } @@ -102,10 +104,12 @@ func NewWithFS(fsys fs.FS, dir string, opts ...Option) (*Service, error) { // NewWithLoader creates a new i18n service with a custom loader. func NewWithLoader(loader Loader, opts ...Option) (*Service, error) { s := &Service{ - loader: loader, - messages: make(map[string]map[string]Message), - fallbackLang: "en", - handlers: DefaultHandlers(), + loader: loader, + messages: make(map[string]map[string]Message), + fallbackLang: "en", + handlers: DefaultHandlers(), + loadedLocales: make(map[int]struct{}), + loadedProviders: make(map[int]struct{}), } for _, opt := range opts { opt(s) @@ -192,10 +196,9 @@ func SetDefault(s *Service) { return } registeredLocalesMu.Lock() - loaded := localesLoaded - hasRegistrations := len(registeredLocales) > 0 + hasRegistrations := len(registeredLocales) > 0 || len(registeredLocaleProviders) > 0 registeredLocalesMu.Unlock() - if !loaded && hasRegistrations { + if hasRegistrations { loadRegisteredLocales(s) } } @@ -833,6 +836,50 @@ func (s *Service) AddLoader(loader Loader) error { return nil } +func (s *Service) hasLocaleRegistrationLoaded(id int) bool { + s.mu.RLock() + defer s.mu.RUnlock() + if len(s.loadedLocales) == 0 { + return false + } + _, ok := s.loadedLocales[id] + return ok +} + +func (s *Service) markLocaleRegistrationLoaded(id int) { + if id == 0 || s == nil { + return + } + s.mu.Lock() + if s.loadedLocales == nil { + s.loadedLocales = make(map[int]struct{}) + } + s.loadedLocales[id] = struct{}{} + s.mu.Unlock() +} + +func (s *Service) hasLocaleProviderLoaded(id int) bool { + s.mu.RLock() + defer s.mu.RUnlock() + if len(s.loadedProviders) == 0 { + return false + } + _, ok := s.loadedProviders[id] + return ok +} + +func (s *Service) markLocaleProviderLoaded(id int) { + if id == 0 || s == nil { + return + } + s.mu.Lock() + if s.loadedProviders == nil { + s.loadedProviders = make(map[int]struct{}) + } + s.loadedProviders[id] = struct{}{} + s.mu.Unlock() +} + func translateOK(messageID, value string) bool { if value == "" { return false