go-i18n/hooks_test.go
Virgil 24eaadda8a
All checks were successful
Security Scan / security (push) Successful in 14s
Test / test (push) Successful in 2m29s
feat(i18n): add current prompt alias
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 12:31:00 +00:00

793 lines
20 KiB
Go

package i18n
import (
"path/filepath"
"sync"
"testing"
"testing/fstest"
"github.com/stretchr/testify/assert"
"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()
}()
fs := fstest.MapFS{
"locales/test.json": &fstest.MapFile{
Data: []byte(`{"custom.hook": "hooked"}`),
},
}
RegisterLocales(fs, "locales")
registeredLocalesMu.Lock()
count := len(registeredLocales)
registeredLocalesMu.Unlock()
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()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
// 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()
}()
// Use "en.json" as filename so language matches fallback
fs := fstest.MapFS{
"i18n/en.json": &fstest.MapFile{
Data: []byte(`{"late.registration": "arrived late"}`),
},
}
RegisterLocales(fs, "i18n")
// Should be able to resolve the newly registered key
got := svc.T("late.registration")
assert.Equal(t, "arrived late", got)
}
func TestRegisterLocales_Good_WithInitializedDefaultService(t *testing.T) {
svc, err := New()
require.NoError(t, err)
SetDefault(svc)
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(`{"eager.registration": "loaded immediately"}`),
},
}
RegisterLocales(fs, "locales")
got := svc.T("eager.registration")
assert.Equal(t, "loaded immediately", got)
}
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()
}()
fs := fstest.MapFS{
"locales/en.json": &fstest.MapFile{
Data: []byte(`{"queued.registration": "loaded via setdefault"}`),
},
}
RegisterLocales(fs, "locales")
svc, err := New()
require.NoError(t, err)
SetDefault(svc)
got := svc.T("queued.registration")
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()
savedLocales := registeredLocales
savedProviders := registeredLocaleProviders
savedLoaded := localesLoaded
registeredLocales = nil
registeredLocaleProviders = nil
localesLoaded = false
registeredLocalesMu.Unlock()
defaultService.Store(nil)
defer func() {
registeredLocalesMu.Lock()
registeredLocales = savedLocales
registeredLocaleProviders = savedProviders
localesLoaded = savedLoaded
registeredLocalesMu.Unlock()
defaultService.Store(nil)
}()
fs := fstest.MapFS{
"locales/en.json": &fstest.MapFile{
Data: []byte(`{"init.registered": "loaded on init"}`),
},
}
RegisterLocales(fs, "locales")
require.NoError(t, Init())
svc := Default()
require.NotNil(t, svc)
got := svc.T("init.registered")
assert.Equal(t, "loaded on init", got)
}
func TestNewCoreService_LoadsRegisteredLocales(t *testing.T) {
registeredLocalesMu.Lock()
savedLocales := registeredLocales
savedLoaded := localesLoaded
registeredLocales = nil
localesLoaded = false
registeredLocalesMu.Unlock()
prev := defaultService.Load()
SetDefault(nil)
defer func() {
registeredLocalesMu.Lock()
registeredLocales = savedLocales
localesLoaded = savedLoaded
registeredLocalesMu.Unlock()
SetDefault(prev)
}()
fs := fstest.MapFS{
"locales/en.json": &fstest.MapFile{
Data: []byte(`{"core.registered": "loaded on core bootstrap"}`),
},
}
RegisterLocales(fs, "locales")
factory := NewCoreService(ServiceOptions{})
_, err := factory(nil)
require.NoError(t, err)
svc := Default()
require.NotNil(t, svc)
got := svc.T("core.registered")
assert.Equal(t, "loaded on core bootstrap", got)
}
func TestNewCoreService_InvalidLanguagePreservesSetLanguageError(t *testing.T) {
factory := NewCoreService(ServiceOptions{Language: "es"})
_, err := factory(nil)
require.Error(t, err)
msg := err.Error()
assert.Contains(t, msg, "unsupported language: es")
assert.Contains(t, msg, "available:")
assert.NotContains(t, msg, "invalid language")
}
func TestNewCoreService_AppliesOptions(t *testing.T) {
prev := Default()
SetDefault(nil)
t.Cleanup(func() {
SetDefault(prev)
})
factory := NewCoreService(ServiceOptions{
Language: "en",
Fallback: "fr",
Formality: FormalityFormal,
Location: "workspace",
Mode: ModeCollect,
Debug: true,
})
_, err := factory(nil)
require.NoError(t, err)
svc := Default()
require.NotNil(t, svc)
assert.Equal(t, "en", svc.Language())
assert.Equal(t, "fr", svc.Fallback())
assert.Equal(t, FormalityFormal, svc.Formality())
assert.Equal(t, "workspace", svc.Location())
assert.Equal(t, ModeCollect, svc.Mode())
assert.True(t, svc.Debug())
}
func TestCoreService_DelegatesToWrappedService(t *testing.T) {
svc, err := New()
require.NoError(t, err)
coreSvc := &CoreService{svc: svc}
assert.Equal(t, svc.T("i18n.label.status"), coreSvc.T("i18n.label.status"))
assert.Equal(t, svc.Raw("i18n.label.status"), coreSvc.Raw("i18n.label.status"))
assert.Equal(t, svc.Translate("i18n.label.status"), coreSvc.Translate("i18n.label.status"))
assert.Equal(t, svc.AvailableLanguages(), coreSvc.AvailableLanguages())
assert.Equal(t, svc.AvailableLanguages(), coreSvc.CurrentAvailableLanguages())
assert.Equal(t, svc.Direction(), coreSvc.Direction())
assert.Equal(t, svc.Direction(), coreSvc.CurrentDirection())
assert.Equal(t, svc.Direction(), coreSvc.CurrentTextDirection())
assert.Equal(t, svc.IsRTL(), coreSvc.IsRTL())
assert.Equal(t, svc.IsRTL(), coreSvc.CurrentIsRTL())
assert.Equal(t, svc.IsRTL(), coreSvc.RTL())
assert.Equal(t, svc.IsRTL(), coreSvc.CurrentRTL())
assert.Equal(t, svc.PluralCategory(2), coreSvc.PluralCategory(2))
assert.Equal(t, svc.PluralCategory(2), coreSvc.CurrentPluralCategory(2))
assert.Equal(t, svc.PluralCategory(2), coreSvc.PluralCategoryOf(2))
assert.Equal(t, svc.Mode(), coreSvc.CurrentMode())
assert.Equal(t, svc.Language(), coreSvc.CurrentLanguage())
assert.Equal(t, svc.Language(), coreSvc.CurrentLang())
assert.Equal(t, svc.Prompt("confirm"), coreSvc.Prompt("confirm"))
assert.Equal(t, svc.Prompt("confirm"), coreSvc.CurrentPrompt("confirm"))
assert.Equal(t, svc.Lang("fr"), coreSvc.Lang("fr"))
assert.Equal(t, svc.Fallback(), coreSvc.CurrentFallback())
assert.Equal(t, svc.Formality(), coreSvc.CurrentFormality())
assert.Equal(t, svc.Location(), coreSvc.CurrentLocation())
assert.Equal(t, svc.Debug(), coreSvc.CurrentDebug())
require.NoError(t, coreSvc.SetLanguage("en"))
assert.Equal(t, "en", coreSvc.Language())
coreSvc.SetFallback("fr")
assert.Equal(t, "fr", coreSvc.Fallback())
coreSvc.SetFormality(FormalityFormal)
assert.Equal(t, FormalityFormal, coreSvc.Formality())
coreSvc.SetLocation("workspace")
assert.Equal(t, "workspace", coreSvc.Location())
coreSvc.SetDebug(true)
assert.True(t, coreSvc.Debug())
coreSvc.SetDebug(false)
assert.False(t, coreSvc.Debug())
handlers := coreSvc.Handlers()
assert.Equal(t, svc.Handlers(), handlers)
assert.Equal(t, svc.Handlers(), coreSvc.CurrentHandlers())
coreSvc.SetHandlers(LabelHandler{})
require.Len(t, coreSvc.Handlers(), 1)
assert.IsType(t, LabelHandler{}, coreSvc.Handlers()[0])
coreSvc.AddHandler(ProgressHandler{})
require.Len(t, coreSvc.Handlers(), 2)
assert.IsType(t, ProgressHandler{}, coreSvc.Handlers()[1])
coreSvc.PrependHandler(CountHandler{})
require.Len(t, coreSvc.Handlers(), 3)
assert.IsType(t, CountHandler{}, coreSvc.Handlers()[0])
coreSvc.ClearHandlers()
assert.Empty(t, coreSvc.Handlers())
coreSvc.ResetHandlers()
require.NotEmpty(t, coreSvc.Handlers())
assert.IsType(t, LabelHandler{}, coreSvc.Handlers()[0])
require.NoError(t, coreSvc.AddLoader(NewFSLoader(fstest.MapFS{
"locales/en.json": &fstest.MapFile{Data: []byte(`{"core.service.loaded": "loaded"}`)},
}, "locales")))
assert.Equal(t, "loaded", coreSvc.T("core.service.loaded"))
require.NoError(t, coreSvc.LoadFS(fstest.MapFS{
"locales/en.json": &fstest.MapFile{Data: []byte(`{"core.service.loaded.fs": "loaded via fs"}`)},
}, "locales"))
assert.Equal(t, "loaded via fs", coreSvc.T("core.service.loaded.fs"))
coreSvc.AddMessages("en", map[string]string{
"core.service.add.messages": "loaded via add messages",
})
assert.Equal(t, "loaded via add messages", coreSvc.T("core.service.add.messages"))
}
func TestInit_ReDetectsRegisteredLocales(t *testing.T) {
t.Setenv("LANG", "de_DE.UTF-8")
registeredLocalesMu.Lock()
savedLocales := registeredLocales
savedProviders := registeredLocaleProviders
savedLoaded := localesLoaded
registeredLocales = nil
localesLoaded = false
registeredLocalesMu.Unlock()
defaultService.Store(nil)
defer func() {
registeredLocalesMu.Lock()
registeredLocales = savedLocales
registeredLocaleProviders = savedProviders
localesLoaded = savedLoaded
registeredLocalesMu.Unlock()
defaultService.Store(nil)
}()
fs := fstest.MapFS{
"locales/de.json": &fstest.MapFile{
Data: []byte(`{"hello": "hallo"}`),
},
}
RegisterLocales(fs, "locales")
require.NoError(t, Init())
svc := Default()
require.NotNil(t, svc)
assert.Contains(t, svc.Language(), "de")
assert.Equal(t, "hallo", svc.T("hello"))
}
func TestDefault_ReinitialisesAfterClear(t *testing.T) {
prev := Default()
t.Cleanup(func() {
SetDefault(prev)
})
SetDefault(nil)
require.NoError(t, Init())
svc := Default()
require.NotNil(t, svc)
assert.Equal(t, "y", svc.T("prompt.yes"))
}
func TestLoadRegisteredLocales_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
// Save and restore state
registeredLocalesMu.Lock()
savedLocales := registeredLocales
savedProviders := registeredLocaleProviders
savedLoaded := localesLoaded
registeredLocales = []localeRegistration{
{
fsys: fstest.MapFS{
"loc/en.json": &fstest.MapFile{
Data: []byte(`{"extra.key": "extra value"}`),
},
},
dir: "loc",
},
}
registeredLocaleProviders = nil
localesLoaded = false
registeredLocalesMu.Unlock()
defer func() {
registeredLocalesMu.Lock()
registeredLocales = savedLocales
registeredLocaleProviders = savedProviders
localesLoaded = savedLoaded
registeredLocalesMu.Unlock()
}()
loadRegisteredLocales(svc)
registeredLocalesMu.Lock()
loaded := localesLoaded
registeredLocalesMu.Unlock()
assert.True(t, loaded, "localesLoaded should be true after loadRegisteredLocales")
got := svc.T("extra.key")
assert.Equal(t, "extra value", got)
}
func TestOnMissingKey_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
svc.SetMode(ModeCollect)
var captured MissingKey
OnMissingKey(func(m MissingKey) {
captured = m
})
_ = T("missing.test.key", map[string]any{"foo": "bar"})
assert.Equal(t, "missing.test.key", captured.Key)
assert.Equal(t, "bar", captured.Args["foo"])
assert.Equal(t, "hooks_test.go", filepath.Base(captured.CallerFile))
}
func TestAddMissingKeyHandler_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
prevHandlers := missingKeyHandlers()
t.Cleanup(func() {
missingKeyHandler.Store(prevHandlers)
SetDefault(prev)
})
svc.SetMode(ModeCollect)
ClearMissingKeyHandlers()
t.Cleanup(func() {
ClearMissingKeyHandlers()
})
var first, second int
AddMissingKeyHandler(func(MissingKey) {
first++
})
AddMissingKeyHandler(func(MissingKey) {
second++
})
_ = T("missing.multiple.handlers")
assert.Equal(t, 1, first)
assert.Equal(t, 1, second)
}
func TestSetMissingKeyHandlers_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
prevHandlers := missingKeyHandlers()
t.Cleanup(func() {
missingKeyHandler.Store(prevHandlers)
SetDefault(prev)
})
svc.SetMode(ModeCollect)
var first, second int
SetMissingKeyHandlers(
nil,
func(MissingKey) { first++ },
func(MissingKey) { second++ },
)
_ = T("missing.set.handlers")
assert.Equal(t, 1, first)
assert.Equal(t, 1, second)
assert.Len(t, missingKeyHandlers().handlers, 2)
}
func TestSetMissingKeyHandlers_Good_Clear(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
prevHandlers := missingKeyHandlers()
t.Cleanup(func() {
missingKeyHandler.Store(prevHandlers)
SetDefault(prev)
})
svc.SetMode(ModeCollect)
var called int
SetMissingKeyHandlers(func(MissingKey) { called++ })
SetMissingKeyHandlers(nil)
_ = T("missing.set.handlers.clear")
assert.Equal(t, 0, called)
assert.Empty(t, missingKeyHandlers().handlers)
}
func TestAddMissingKeyHandler_Good_Concurrent(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
prevHandlers := missingKeyHandlers()
t.Cleanup(func() {
missingKeyHandler.Store(prevHandlers)
SetDefault(prev)
})
svc.SetMode(ModeCollect)
ClearMissingKeyHandlers()
t.Cleanup(func() {
ClearMissingKeyHandlers()
})
const handlers = 32
var wg sync.WaitGroup
wg.Add(handlers)
for i := 0; i < handlers; i++ {
go func() {
defer wg.Done()
AddMissingKeyHandler(func(MissingKey) {})
}()
}
wg.Wait()
state := missingKeyHandlers()
assert.Len(t, state.handlers, handlers)
}
func TestClearMissingKeyHandlers_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
prevHandlers := missingKeyHandlers()
t.Cleanup(func() {
missingKeyHandler.Store(prevHandlers)
SetDefault(prev)
})
svc.SetMode(ModeCollect)
var called int
AddMissingKeyHandler(func(MissingKey) {
called++
})
ClearMissingKeyHandlers()
_ = T("missing.after.clear")
assert.Equal(t, 0, called)
}
func TestOnMissingKey_Good_SubjectArgs(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
svc.SetMode(ModeCollect)
var captured MissingKey
OnMissingKey(func(m MissingKey) {
captured = m
})
_ = T("missing.subject.key", S("file", "config.yaml").Count(3).In("workspace").Formal())
assert.Equal(t, "missing.subject.key", captured.Key)
assert.Equal(t, "config.yaml", captured.Args["Subject"])
assert.Equal(t, "file", captured.Args["Noun"])
assert.Equal(t, 3, captured.Args["Count"])
assert.Equal(t, "workspace", captured.Args["Location"])
assert.Equal(t, FormalityFormal, captured.Args["Formality"])
}
func TestOnMissingKey_Good_TranslationContextArgs(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
svc.SetMode(ModeCollect)
var captured MissingKey
OnMissingKey(func(m MissingKey) {
captured = m
})
_ = T("missing.context.key", C("navigation").WithGender("feminine").In("workspace").Formal())
assert.Equal(t, "missing.context.key", captured.Key)
assert.Equal(t, "navigation", captured.Args["Context"])
assert.Equal(t, "feminine", captured.Args["Gender"])
assert.Equal(t, "workspace", captured.Args["Location"])
assert.Equal(t, FormalityFormal, captured.Args["Formality"])
}
func TestOnMissingKey_Good_MergesAdditionalArgs(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
svc.SetMode(ModeCollect)
var captured MissingKey
OnMissingKey(func(m MissingKey) {
captured = m
})
_ = T("missing.extra.args", S("file", "config.yaml"), map[string]any{"trace": "abc123"})
assert.Equal(t, "missing.extra.args", captured.Key)
assert.Equal(t, "config.yaml", captured.Args["Subject"])
assert.Equal(t, "abc123", captured.Args["trace"])
}
func TestDispatchMissingKey_Good_NoHandler(t *testing.T) {
// Reset to the empty handler set.
OnMissingKey(nil)
// Should not panic when dispatching with nil handler
dispatchMissingKey("test.key", nil)
}
func TestCoreServiceSetMode_Good_PreservesMissingKeyHandlers(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := missingKeyHandlers()
t.Cleanup(func() {
missingKeyHandler.Store(prev)
})
var observed int
OnMissingKey(func(MissingKey) {
observed++
})
t.Cleanup(func() {
OnMissingKey(nil)
})
coreSvc := &CoreService{svc: svc}
coreSvc.SetMode(ModeCollect)
_ = svc.T("missing.core.service.key")
if observed != 1 {
t.Fatalf("custom missing key handler called %d times, want 1", observed)
}
missing := coreSvc.MissingKeys()
if len(missing) != 1 {
t.Fatalf("CoreService captured %d missing keys, want 1", len(missing))
}
if missing[0].Key != "missing.core.service.key" {
t.Fatalf("captured missing key = %q, want %q", missing[0].Key, "missing.core.service.key")
}
}