go-i18n/i18n_test.go

773 lines
17 KiB
Go
Raw Normal View History

package i18n
import (
"testing"
"testing/fstest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Package-level T() ---
func TestT_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
got := T("prompt.yes")
assert.Equal(t, "y", got)
}
func TestT_Good_WithHandlers(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
got := T("i18n.label.status")
assert.Equal(t, "Status:", got)
}
func TestT_Good_MissingKey(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
got := T("nonexistent.key.test")
assert.Equal(t, "nonexistent.key.test", got)
}
// --- Package-level Translate() ---
func TestTranslate_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
result := Translate("prompt.yes")
require.True(t, result.OK)
assert.Equal(t, "y", result.Value)
}
func TestTranslate_Good_MissingKey(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
result := Translate("nonexistent.translation.key")
require.False(t, result.OK)
assert.Equal(t, "nonexistent.translation.key", result.Value)
}
func TestTranslate_Good_SameTextAsKey(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
AddMessages("en", map[string]string{
"exact.same.key": "exact.same.key",
})
result := Translate("exact.same.key")
require.True(t, result.OK)
assert.Equal(t, "exact.same.key", result.Value)
}
// --- Package-level Raw() ---
func TestRaw_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
got := Raw("prompt.yes")
assert.Equal(t, "y", got)
}
func TestRaw_Good_BypassesHandlers(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
// i18n.label.status is not in messages, handlers don't apply in Raw
got := Raw("i18n.label.status")
assert.Equal(t, "i18n.label.status", got)
}
func TestLoadFS_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
fsys := fstest.MapFS{
"locales/en.json": &fstest.MapFile{
Data: []byte(`{"loadfs.key": "loaded via package helper"}`),
},
}
LoadFS(fsys, "locales")
got := T("loadfs.key")
assert.Equal(t, "loaded via package helper", got)
}
func TestAddMessages_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
AddMessages("en", map[string]string{
"add.messages.key": "loaded via package helper",
})
got := T("add.messages.key")
assert.Equal(t, "loaded via package helper", got)
}
// --- SetLanguage / CurrentLanguage ---
func TestSetLanguage_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
err = SetLanguage("en")
assert.NoError(t, err)
assert.Contains(t, CurrentLanguage(), "en")
}
func TestSetLanguage_Good_UnderscoreTag(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
err = SetLanguage("fr_CA")
assert.NoError(t, err)
assert.True(t, len(CurrentLanguage()) >= 2)
assert.Equal(t, "fr", CurrentLanguage()[:2])
}
func TestLanguage_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
assert.Equal(t, CurrentLanguage(), Language())
}
func TestSetLanguage_Bad_Unsupported(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
_ = SetLanguage("xx")
}
func TestCurrentLanguage_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
lang := CurrentLanguage()
assert.NotEmpty(t, lang)
assert.Equal(t, lang, CurrentLang())
}
func TestAvailableLanguages_Good(t *testing.T) {
prev := Default()
t.Cleanup(func() {
SetDefault(prev)
})
svc, err := New()
require.NoError(t, err)
SetDefault(svc)
langs := AvailableLanguages()
require.NotEmpty(t, langs)
assert.Equal(t, svc.AvailableLanguages(), langs)
langs[0] = "zz"
assert.NotEqual(t, "zz", svc.AvailableLanguages()[0])
}
func TestCurrentAvailableLanguages_Good(t *testing.T) {
prev := Default()
t.Cleanup(func() {
SetDefault(prev)
})
svc, err := New()
require.NoError(t, err)
SetDefault(svc)
langs := CurrentAvailableLanguages()
require.NotEmpty(t, langs)
assert.Equal(t, svc.AvailableLanguages(), langs)
}
func TestFallback_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
SetDefault(svc)
assert.Equal(t, "en", Fallback())
SetFallback("fr")
assert.Equal(t, "fr", Fallback())
}
func TestDebug_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
SetDefault(svc)
assert.False(t, Debug())
SetDebug(true)
assert.True(t, Debug())
}
func TestCurrentState_Good(t *testing.T) {
svc, err := NewWithLoader(messageBaseFallbackLoader{})
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
state := CurrentState()
assert.Equal(t, svc.Language(), state.Language)
assert.Equal(t, svc.AvailableLanguages(), state.AvailableLanguages)
assert.Equal(t, svc.Mode(), state.Mode)
assert.Equal(t, svc.Fallback(), state.Fallback)
assert.Equal(t, svc.Formality(), state.Formality)
assert.Equal(t, svc.Location(), state.Location)
assert.Equal(t, svc.Direction(), state.Direction)
assert.Equal(t, svc.IsRTL(), state.IsRTL)
assert.Equal(t, svc.Debug(), state.Debug)
assert.Len(t, state.Handlers, len(svc.Handlers()))
state.AvailableLanguages[0] = "zz"
assert.NotEqual(t, "zz", CurrentState().AvailableLanguages[0])
state.Handlers[0] = nil
assert.NotNil(t, CurrentState().Handlers[0])
}
func TestState_Good_WithoutDefaultService(t *testing.T) {
var svc *Service
state := svc.State()
assert.Equal(t, defaultServiceStateSnapshot(), state)
}
// --- SetMode / CurrentMode ---
func TestSetMode_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
SetMode(ModeCollect)
assert.Equal(t, ModeCollect, CurrentMode())
SetMode(ModeNormal)
assert.Equal(t, ModeNormal, CurrentMode())
}
func TestCurrentMode_Good_Default(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
assert.Equal(t, ModeNormal, CurrentMode())
}
// --- N() numeric shorthand ---
func TestN_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
tests := []struct {
name string
format string
value any
args []any
want string
}{
{"number", "number", int64(1234567), nil, "1,234,567"},
{"percent", "percent", 0.85, nil, "85%"},
{"bytes", "bytes", int64(1536000), nil, "1.46 MB"},
{"ordinal", "ordinal", 1, nil, "1st"},
{"ago", "ago", 5, []any{"minutes"}, "5 minutes ago"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := N(tt.format, tt.value, tt.args...)
assert.Equal(t, tt.want, got)
})
}
}
func TestN_Good_WithoutDefaultService(t *testing.T) {
prev := Default()
SetDefault(nil)
t.Cleanup(func() {
SetDefault(prev)
})
tests := []struct {
name string
format string
value any
args []any
want string
}{
{"number", "number", int64(1234567), nil, "1,234,567"},
{"percent", "percent", 0.85, nil, "85%"},
{"bytes", "bytes", int64(1536000), nil, "1.46 MB"},
{"ordinal", "ordinal", 1, nil, "1st"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := N(tt.format, tt.value, tt.args...)
assert.Equal(t, tt.want, got)
})
}
}
// --- Prompt() prompt shorthand ---
func TestPrompt_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
tests := []struct {
name string
key string
want string
}{
{"yes", "yes", "y"},
{"yes_trimmed", " yes ", "y"},
{"yes_prefixed", "prompt.yes", "y"},
{"confirm", "confirm", "Are you sure?"},
{"confirm_prefixed", "prompt.confirm", "Are you sure?"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Prompt(tt.key)
assert.Equal(t, tt.want, got)
})
}
}
func TestCurrentPrompt_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
assert.Equal(t, Prompt("confirm"), CurrentPrompt("confirm"))
}
// --- Lang() language label shorthand ---
func TestLang_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
tests := []struct {
name string
key string
want string
}{
{"de", "de", "German"},
{"fr", "fr", "French"},
{"fr_ca", "fr_CA", "French"},
{"fr_prefixed", "lang.fr", "French"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Lang(tt.key)
assert.Equal(t, tt.want, got)
})
}
}
func TestLang_MissingKeyHandler_FiresOnce(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetMissingKeyHandlers()
SetMode(ModeNormal)
SetDefault(prev)
})
SetMode(ModeCollect)
calls := 0
SetMissingKeyHandlers(func(MissingKey) {
calls++
})
got := Lang("zz")
assert.Equal(t, "[lang.zz]", got)
assert.Equal(t, 1, calls)
}
// --- AddHandler / PrependHandler ---
func TestAddHandler_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
SetDefault(svc)
initialCount := len(svc.Handlers())
AddHandler(LabelHandler{})
assert.Equal(t, initialCount+1, len(svc.Handlers()))
}
func TestAddHandler_Good_Variadic(t *testing.T) {
svc, err := New(WithHandlers())
require.NoError(t, err)
_ = Init()
SetDefault(svc)
AddHandler(LabelHandler{}, ProgressHandler{})
handlers := svc.Handlers()
assert.Equal(t, 2, len(handlers))
assert.IsType(t, LabelHandler{}, handlers[0])
assert.IsType(t, ProgressHandler{}, handlers[1])
}
func TestAddHandler_Good_SkipsNil(t *testing.T) {
svc, err := New(WithHandlers())
require.NoError(t, err)
_ = Init()
SetDefault(svc)
var nilHandler KeyHandler
AddHandler(nilHandler, LabelHandler{})
handlers := svc.Handlers()
require.Len(t, handlers, 1)
assert.IsType(t, LabelHandler{}, handlers[0])
}
func TestAddHandler_DoesNotMutateInputSlice(t *testing.T) {
svc, err := New(WithHandlers())
require.NoError(t, err)
_ = Init()
SetDefault(svc)
handlers := []KeyHandler{nil, LabelHandler{}}
AddHandler(handlers...)
assert.Nil(t, handlers[0])
assert.IsType(t, LabelHandler{}, handlers[1])
}
func TestPrependHandler_Good(t *testing.T) {
svc, err := New(WithHandlers()) // start with no handlers
require.NoError(t, err)
_ = Init()
SetDefault(svc)
PrependHandler(LabelHandler{})
assert.Equal(t, 1, len(svc.Handlers()))
// Prepend another
PrependHandler(ProgressHandler{})
handlers := svc.Handlers()
assert.Equal(t, 2, len(handlers))
}
func TestPrependHandler_Good_Variadic(t *testing.T) {
svc, err := New(WithHandlers())
require.NoError(t, err)
_ = Init()
SetDefault(svc)
PrependHandler(LabelHandler{}, ProgressHandler{})
handlers := svc.Handlers()
assert.Equal(t, 2, len(handlers))
assert.IsType(t, LabelHandler{}, handlers[0])
assert.IsType(t, ProgressHandler{}, handlers[1])
}
func TestPrependHandler_Good_SkipsNil(t *testing.T) {
svc, err := New(WithHandlers())
require.NoError(t, err)
_ = Init()
SetDefault(svc)
var nilHandler KeyHandler
PrependHandler(nilHandler, LabelHandler{})
handlers := svc.Handlers()
require.Len(t, handlers, 1)
assert.IsType(t, LabelHandler{}, handlers[0])
}
func TestPrependHandler_DoesNotMutateInputSlice(t *testing.T) {
svc, err := New(WithHandlers())
require.NoError(t, err)
_ = Init()
SetDefault(svc)
handlers := []KeyHandler{nil, ProgressHandler{}}
PrependHandler(handlers...)
assert.Nil(t, handlers[0])
assert.IsType(t, ProgressHandler{}, handlers[1])
}
func TestClearHandlers_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
AddHandler(LabelHandler{})
require.NotEmpty(t, svc.Handlers())
ClearHandlers()
assert.Empty(t, svc.Handlers())
}
func TestResetHandlers_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
ClearHandlers()
require.Empty(t, svc.Handlers())
svc.ResetHandlers()
require.Len(t, svc.Handlers(), len(DefaultHandlers()))
assert.IsType(t, LabelHandler{}, svc.Handlers()[0])
ClearHandlers()
require.Empty(t, svc.Handlers())
ResetHandlers()
handlers := svc.Handlers()
require.Len(t, handlers, len(DefaultHandlers()))
assert.IsType(t, LabelHandler{}, handlers[0])
assert.Equal(t, "Status:", T("i18n.label.status"))
}
func TestSetHandlers_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
_ = Init()
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
SetHandlers(serviceStubHandler{})
handlers := CurrentHandlers()
require.Len(t, handlers, 1)
assert.IsType(t, serviceStubHandler{}, handlers[0])
assert.Equal(t, "stub", T("custom.stub"))
assert.Equal(t, "i18n.label.status", T("i18n.label.status"))
}
func TestHandlers_Good(t *testing.T) {
svc, err := New()
require.NoError(t, err)
prev := Default()
SetDefault(svc)
t.Cleanup(func() {
SetDefault(prev)
})
handlers := Handlers()
require.Len(t, handlers, len(svc.Handlers()))
assert.Equal(t, svc.Handlers(), handlers)
}
func TestNewWithHandlers_SkipsNil(t *testing.T) {
svc, err := New(WithHandlers(nil, LabelHandler{}))
require.NoError(t, err)
handlers := svc.Handlers()
require.Len(t, handlers, 1)
assert.IsType(t, LabelHandler{}, handlers[0])
}
// --- executeIntentTemplate ---
func TestExecuteIntentTemplate_Good(t *testing.T) {
data := templateData{
Subject: "config.yaml",
Noun: "file",
Count: 1,
}
got := executeIntentTemplate("Delete {{.Subject}}?", data)
assert.Equal(t, "Delete config.yaml?", got)
}
func TestExecuteIntentTemplate_Good_Empty(t *testing.T) {
got := executeIntentTemplate("", templateData{})
assert.Equal(t, "", got)
}
func TestExecuteIntentTemplate_Bad_InvalidTemplate(t *testing.T) {
got := executeIntentTemplate("{{.Invalid", templateData{})
assert.Equal(t, "{{.Invalid", got, "should return raw template on parse error")
}
func TestExecuteIntentTemplate_Good_Cached(t *testing.T) {
data := templateData{Subject: "test"}
tmplStr := "Hello {{.Subject}} intent"
// First call caches
got1 := executeIntentTemplate(tmplStr, data)
assert.Equal(t, "Hello test intent", got1)
// Second call hits cache
got2 := executeIntentTemplate(tmplStr, data)
assert.Equal(t, "Hello test intent", got2)
}
func TestExecuteIntentTemplate_Good_WithFuncs(t *testing.T) {
data := templateData{Subject: "build"}
got := executeIntentTemplate("{{past .Subject}}!", data)
assert.Equal(t, "built!", got)
}
func TestComposeIntent_Good(t *testing.T) {
intent := Intent{
Meta: IntentMeta{
Type: "action",
Verb: "delete",
Dangerous: true,
Default: "no",
Supports: []string{"yes", "no"},
},
Question: "Delete {{.Subject}}?",
Confirm: "Really delete {{article .Subject}}?",
Success: "{{title .Subject}} deleted",
Failure: "Failed to delete {{lower .Subject}}",
}
got := ComposeIntent(intent, S("file", "config.yaml"))
assert.Equal(t, "Delete config.yaml?", got.Question)
assert.Equal(t, "Really delete a config.yaml?", got.Confirm)
assert.Equal(t, "Config.yaml deleted", got.Success)
assert.Equal(t, "Failed to delete config.yaml", got.Failure)
assert.Equal(t, intent.Meta, got.Meta)
}
func TestIntentCompose_Good_NilSubject(t *testing.T) {
intent := Intent{
Question: "Proceed?",
}
got := intent.Compose(nil)
assert.Equal(t, "Proceed?", got.Question)
assert.Empty(t, got.Confirm)
assert.Empty(t, got.Success)
assert.Empty(t, got.Failure)
}
// --- applyTemplate ---
func TestApplyTemplate_Good(t *testing.T) {
got := applyTemplate("Hello {{.Name}}", map[string]any{"Name": "World"})
assert.Equal(t, "Hello World", got)
}
func TestApplyTemplate_Good_NoTemplate(t *testing.T) {
got := applyTemplate("plain text", nil)
assert.Equal(t, "plain text", got, "should return text as-is without template markers")
}
func TestApplyTemplate_Bad_InvalidTemplate(t *testing.T) {
got := applyTemplate("{{.Invalid", nil)
assert.Equal(t, "{{.Invalid", got, "should return raw text on parse error")
}
func TestApplyTemplate_Good_Cached(t *testing.T) {
tmpl := "Cached {{.Val}} template test"
data := map[string]any{"Val": "test"}
got1 := applyTemplate(tmpl, data)
assert.Equal(t, "Cached test template test", got1)
// Hit cache
got2 := applyTemplate(tmpl, data)
assert.Equal(t, "Cached test template test", got2)
}
func TestApplyTemplate_Bad_ExecuteError(t *testing.T) {
// A template that references a method on a non-struct type
got := applyTemplate("{{.Missing}}", "not a map")
assert.Equal(t, "{{.Missing}}", got)
}
// --- ErrServiceNotInitialised ---
func TestErrServiceNotInitialised_Good(t *testing.T) {
assert.Equal(t, "i18n: service not initialised", ErrServiceNotInitialised.Error())
}
func TestErrServiceNotInitialized_DeprecatedAlias(t *testing.T) {
assert.Equal(t, ErrServiceNotInitialised, ErrServiceNotInitialized, "deprecated alias must point to the same error")
}