- compose.go: remove fmt import, use local stringer interface + core.Sprint - hooks.go: replace stdlib log with dappco.re/go/core/log - localise.go: replace os.Getenv with core.Env - loader.go: replace strings.CutPrefix with core.HasPrefix/TrimPrefix - reversal/tokeniser.go: replace strings.Fields with local splitFields helper - validate.go: rename sb → builder (AX naming) - calibrate.go, classify.go: rename cfg → configuration (AX naming) - numbers.go: rename local fmt variable → numberFormat - All test files: add Good/Bad/Ugly triads per AX test naming convention Co-Authored-By: Virgil <virgil@lethean.io>
317 lines
7.8 KiB
Go
317 lines
7.8 KiB
Go
package i18n
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
func TestFSLoaderLanguages(t *testing.T) {
|
|
loader := NewFSLoader(localeFS, "locales")
|
|
langs := loader.Languages()
|
|
if len(langs) == 0 {
|
|
t.Fatal("FSLoader.Languages() returned empty")
|
|
}
|
|
|
|
found := slices.Contains(langs, "en")
|
|
if !found {
|
|
t.Errorf("Languages() = %v, expected 'en' in list", langs)
|
|
}
|
|
}
|
|
|
|
func TestFSLoaderLoad(t *testing.T) {
|
|
loader := NewFSLoader(localeFS, "locales")
|
|
messages, grammar, err := loader.Load("en")
|
|
if err != nil {
|
|
t.Fatalf("Load(en) error: %v", err)
|
|
}
|
|
|
|
// Should have messages from the JSON
|
|
if len(messages) == 0 {
|
|
t.Error("Load(en) returned 0 messages")
|
|
}
|
|
|
|
// Grammar data should be extracted from nested JSON
|
|
if grammar == nil {
|
|
t.Fatal("Load(en) returned nil grammar")
|
|
}
|
|
|
|
// Verbs from gram.verb.*
|
|
if len(grammar.Verbs) == 0 {
|
|
t.Error("grammar has 0 verbs")
|
|
}
|
|
if v, ok := grammar.Verbs["build"]; !ok {
|
|
t.Error("grammar missing verb 'build'")
|
|
} else {
|
|
if v.Past != "built" {
|
|
t.Errorf("build.past = %q, want 'built'", v.Past)
|
|
}
|
|
if v.Gerund != "building" {
|
|
t.Errorf("build.gerund = %q, want 'building'", v.Gerund)
|
|
}
|
|
}
|
|
|
|
// Nouns from gram.noun.*
|
|
if len(grammar.Nouns) == 0 {
|
|
t.Error("grammar has 0 nouns")
|
|
}
|
|
if n, ok := grammar.Nouns["file"]; !ok {
|
|
t.Error("grammar missing noun 'file'")
|
|
} else {
|
|
if n.One != "file" {
|
|
t.Errorf("file.one = %q, want 'file'", n.One)
|
|
}
|
|
if n.Other != "files" {
|
|
t.Errorf("file.other = %q, want 'files'", n.Other)
|
|
}
|
|
}
|
|
|
|
// Articles from gram.article
|
|
if grammar.Articles.IndefiniteDefault != "a" {
|
|
t.Errorf("article.indefinite.default = %q, want 'a'", grammar.Articles.IndefiniteDefault)
|
|
}
|
|
if grammar.Articles.IndefiniteVowel != "an" {
|
|
t.Errorf("article.indefinite.vowel = %q, want 'an'", grammar.Articles.IndefiniteVowel)
|
|
}
|
|
if grammar.Articles.Definite != "the" {
|
|
t.Errorf("article.definite = %q, want 'the'", grammar.Articles.Definite)
|
|
}
|
|
|
|
// Punctuation from gram.punct
|
|
if grammar.Punct.LabelSuffix != ":" {
|
|
t.Errorf("punct.label = %q, want ':'", grammar.Punct.LabelSuffix)
|
|
}
|
|
if grammar.Punct.ProgressSuffix != "..." {
|
|
t.Errorf("punct.progress = %q, want '...'", grammar.Punct.ProgressSuffix)
|
|
}
|
|
|
|
// Words from gram.word.*
|
|
if len(grammar.Words) == 0 {
|
|
t.Error("grammar has 0 words")
|
|
}
|
|
if grammar.Words["url"] != "URL" {
|
|
t.Errorf("word.url = %q, want 'URL'", grammar.Words["url"])
|
|
}
|
|
if grammar.Words["api"] != "API" {
|
|
t.Errorf("word.api = %q, want 'API'", grammar.Words["api"])
|
|
}
|
|
}
|
|
|
|
func TestFSLoaderLoadMissing(t *testing.T) {
|
|
loader := NewFSLoader(localeFS, "locales")
|
|
_, _, err := loader.Load("xx")
|
|
if err == nil {
|
|
t.Error("Load(xx) should fail for non-existent locale")
|
|
}
|
|
}
|
|
|
|
func TestFlattenWithGrammar(t *testing.T) {
|
|
messages := make(map[string]Message)
|
|
grammar := &GrammarData{
|
|
Verbs: make(map[string]VerbForms),
|
|
Nouns: make(map[string]NounForms),
|
|
Words: make(map[string]string),
|
|
}
|
|
|
|
raw := map[string]any{
|
|
"gram": map[string]any{
|
|
"verb": map[string]any{
|
|
"test": map[string]any{
|
|
"base": "test",
|
|
"past": "tested",
|
|
"gerund": "testing",
|
|
},
|
|
},
|
|
"noun": map[string]any{
|
|
"widget": map[string]any{
|
|
"one": "widget",
|
|
"other": "widgets",
|
|
},
|
|
},
|
|
"word": map[string]any{
|
|
"api": "API",
|
|
},
|
|
"punct": map[string]any{
|
|
"label": ":",
|
|
"progress": "...",
|
|
},
|
|
"article": map[string]any{
|
|
"indefinite": map[string]any{
|
|
"default": "a",
|
|
"vowel": "an",
|
|
},
|
|
"definite": "the",
|
|
},
|
|
},
|
|
"prompt": map[string]any{
|
|
"yes": "y",
|
|
"no": "n",
|
|
},
|
|
}
|
|
|
|
flattenWithGrammar("", raw, messages, grammar)
|
|
|
|
// Verb extracted
|
|
if v, ok := grammar.Verbs["test"]; !ok {
|
|
t.Error("verb 'test' not extracted")
|
|
} else {
|
|
if v.Past != "tested" {
|
|
t.Errorf("test.past = %q, want 'tested'", v.Past)
|
|
}
|
|
}
|
|
|
|
// Noun extracted
|
|
if n, ok := grammar.Nouns["widget"]; !ok {
|
|
t.Error("noun 'widget' not extracted")
|
|
} else {
|
|
if n.Other != "widgets" {
|
|
t.Errorf("widget.other = %q, want 'widgets'", n.Other)
|
|
}
|
|
}
|
|
|
|
// Word extracted
|
|
if grammar.Words["api"] != "API" {
|
|
t.Errorf("word 'api' = %q, want 'API'", grammar.Words["api"])
|
|
}
|
|
|
|
// Punct extracted
|
|
if grammar.Punct.LabelSuffix != ":" {
|
|
t.Errorf("punct.label = %q, want ':'", grammar.Punct.LabelSuffix)
|
|
}
|
|
|
|
// Articles extracted
|
|
if grammar.Articles.IndefiniteDefault != "a" {
|
|
t.Errorf("article.indefinite.default = %q, want 'a'", grammar.Articles.IndefiniteDefault)
|
|
}
|
|
|
|
// Regular keys flattened
|
|
if msg, ok := messages["prompt.yes"]; !ok || msg.Text != "y" {
|
|
t.Errorf("prompt.yes not flattened correctly, got %+v", messages["prompt.yes"])
|
|
}
|
|
}
|
|
|
|
func TestFlattenPluralObject(t *testing.T) {
|
|
messages := make(map[string]Message)
|
|
raw := map[string]any{
|
|
"time": map[string]any{
|
|
"ago": map[string]any{
|
|
"second": map[string]any{
|
|
"one": "{{.Count}} second ago",
|
|
"other": "{{.Count}} seconds ago",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
flattenWithGrammar("", raw, messages, nil)
|
|
|
|
msg, ok := messages["time.ago.second"]
|
|
if !ok {
|
|
t.Fatal("time.ago.second not found")
|
|
}
|
|
if !msg.IsPlural() {
|
|
t.Error("time.ago.second should be plural")
|
|
}
|
|
if msg.One != "{{.Count}} second ago" {
|
|
t.Errorf("time.ago.second.one = %q", msg.One)
|
|
}
|
|
if msg.Other != "{{.Count}} seconds ago" {
|
|
t.Errorf("time.ago.second.other = %q", msg.Other)
|
|
}
|
|
}
|
|
|
|
func TestFSLoaderLanguagesErr_Good(t *testing.T) {
|
|
loader := NewFSLoader(localeFS, "locales")
|
|
if err := loader.LanguagesErr(); err != nil {
|
|
t.Errorf("LanguagesErr() = %v, want nil for valid dir", err)
|
|
}
|
|
}
|
|
|
|
func TestFSLoaderLanguagesErr_Bad(t *testing.T) {
|
|
loader := NewFSLoader(localeFS, "nonexistent")
|
|
langs := loader.Languages()
|
|
if len(langs) != 0 {
|
|
t.Errorf("Languages() = %v, want empty for bad dir", langs)
|
|
}
|
|
if err := loader.LanguagesErr(); err == nil {
|
|
t.Error("LanguagesErr() = nil, want error for bad dir")
|
|
}
|
|
}
|
|
|
|
func TestFlatten_Good(t *testing.T) {
|
|
messages := make(map[string]Message)
|
|
raw := map[string]any{
|
|
"hello": "world",
|
|
"nested": map[string]any{
|
|
"key": "value",
|
|
},
|
|
}
|
|
flatten("", raw, messages)
|
|
if msg, ok := messages["hello"]; !ok || msg.Text != "world" {
|
|
t.Errorf("flatten: hello = %+v, want 'world'", messages["hello"])
|
|
}
|
|
if msg, ok := messages["nested.key"]; !ok || msg.Text != "value" {
|
|
t.Errorf("flatten: nested.key = %+v, want 'value'", messages["nested.key"])
|
|
}
|
|
}
|
|
|
|
func TestCustomFSLoader(t *testing.T) {
|
|
fs := fstest.MapFS{
|
|
"locales/test.json": &fstest.MapFile{
|
|
Data: []byte(`{
|
|
"gram": {
|
|
"verb": {
|
|
"zap": { "base": "zap", "past": "zapped", "gerund": "zapping" }
|
|
},
|
|
"word": {
|
|
"hello": "Hello"
|
|
}
|
|
},
|
|
"greeting": "Hello, world!"
|
|
}`),
|
|
},
|
|
}
|
|
|
|
svc, err := NewWithFS(fs, "locales", WithFallback("test"))
|
|
if err != nil {
|
|
t.Fatalf("NewWithFS failed: %v", err)
|
|
}
|
|
|
|
got := svc.T("greeting")
|
|
if got != "Hello, world!" {
|
|
t.Errorf("T(greeting) = %q, want 'Hello, world!'", got)
|
|
}
|
|
|
|
// Grammar should be loaded
|
|
gd := GetGrammarData("test")
|
|
if gd == nil {
|
|
t.Fatal("grammar data not loaded for 'test'")
|
|
}
|
|
if v, ok := gd.Verbs["zap"]; !ok || v.Past != "zapped" {
|
|
t.Errorf("verb 'zap' not loaded correctly")
|
|
}
|
|
}
|
|
|
|
// TestFSLoader_Ugly verifies that loading a locale with only a gram block (no messages) works.
|
|
//
|
|
// loader.Load("en") // returns empty messages map but populated grammar
|
|
func TestFSLoader_Ugly(t *testing.T) {
|
|
gramOnly := `{"gram":{"verb":{"zap":{"past":"zapped","gerund":"zapping"}}}}`
|
|
fsys := fstest.MapFS{
|
|
"locales/eo.json": &fstest.MapFile{Data: []byte(gramOnly)},
|
|
}
|
|
loader := NewFSLoader(fsys, "locales")
|
|
messages, grammar, err := loader.Load("eo")
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(messages) != 0 {
|
|
t.Errorf("messages = %d, want 0 for gram-only locale", len(messages))
|
|
}
|
|
if grammar == nil {
|
|
t.Fatal("grammar should not be nil")
|
|
}
|
|
if v, ok := grammar.Verbs["zap"]; !ok || v.Past != "zapped" {
|
|
t.Errorf("gram-only verb not loaded: %v", grammar.Verbs)
|
|
}
|
|
}
|