diff --git a/examples/hello/hello_test.go b/examples/hello/hello_test.go index a019943..1302ea9 100644 --- a/examples/hello/hello_test.go +++ b/examples/hello/hello_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "crypto/sha256" "net/http" "net/http/httptest" @@ -18,6 +19,7 @@ import ( "git.golem15.com/golem15/summercms/phrasebook" "git.golem15.com/golem15/summercms/postcard" "git.golem15.com/golem15/summercms/surf" + "git.golem15.com/golem15/summercms/towel" ) func TestGreeterHelloPrintsLayeredConfig(t *testing.T) { @@ -122,6 +124,13 @@ func TestHelloPluginResolvesLang(t *testing.T) { if !ok { t.Fatal("translator not published") } + ctx := towel.WithLocale(context.Background(), "pl") + if got := tr.Get(ctx, "golem15.hello::lang.greeting", nil); got != "Cześć" { + t.Fatalf("towel locale = %q", got) + } + if got := tr.Get(context.Background(), "golem15.hello::lang.greeting", nil); got != "Hello" { + t.Fatalf("app locale default = %q", got) + } if got := tr.GetIn("en", "golem15.hello::lang.labels.pluginName", nil); got != "Hello plugin" { t.Fatalf("en = %q", got) } diff --git a/party/registry_test.go b/party/registry_test.go index 5273435..2b959fd 100644 --- a/party/registry_test.go +++ b/party/registry_test.go @@ -457,6 +457,26 @@ func TestActivateMalformedLangFailsWithPluginContext(t *testing.T) { } } +func TestActivateMalformedLangLeafNamesFileAndKey(t *testing.T) { + p := &langPlugin{ + id: "golem15.hello", + fsys: fstest.MapFS{ + "lang/en/lang.yaml": {Data: []byte("labels:\n count: 1\n")}, + }, + } + app := backpack.New(&compass.Config{}) + _, err := activate([]Plugin{p}, app, []string{"golem15.hello"}) + if err == nil { + t.Fatal("expected non-string leaf error") + } + msg := err.Error() + for _, want := range []string{"golem15.hello", "lang/en/lang.yaml", "labels.count", "non-string leaf"} { + if !strings.Contains(msg, want) { + t.Fatalf("error %q missing %s", msg, want) + } + } +} + type mailPlugin struct { id string fsys fs.FS diff --git a/phrasebook/translator_test.go b/phrasebook/translator_test.go index 911750b..ea36eba 100644 --- a/phrasebook/translator_test.go +++ b/phrasebook/translator_test.go @@ -5,6 +5,8 @@ import ( "context" "io/fs" "log/slog" + "os" + "path/filepath" "strings" "testing" "testing/fstest" @@ -249,3 +251,95 @@ func TestLocaleFallbackSmoke(t *testing.T) { t.Fatalf("production lookup must be silent, got %q", buf.String()) } } + +func TestCatalogBoundaries(t *testing.T) { + t.Parallel() + + t.Run("invalid yaml names plugin and file", func(t *testing.T) { + err := NewCatalog().Load("golem15.hello", fstest.MapFS{ + "lang/en/lang.yaml": {Data: []byte("greeting: [\n")}, + }) + if err == nil || !strings.Contains(err.Error(), "golem15.hello") || !strings.Contains(err.Error(), "lang/en/lang.yaml") { + t.Fatalf("parse error = %v", err) + } + }) + t.Run("nested group file flattens under group name", func(t *testing.T) { + cat := NewCatalog() + err := cat.Load("golem15.hello", fstest.MapFS{ + "lang/en/validation.yaml": {Data: []byte("fields:\n name: Name is required\n")}, + }) + if err != nil { + t.Fatal(err) + } + tr := NewTranslator(cat, Options{}) + if got := tr.GetIn("en", "golem15.hello::validation.fields.name", nil); got != "Name is required" { + t.Fatalf("nested group = %q", got) + } + }) + t.Run("path extra segments and wrong suffix are malformed", func(t *testing.T) { + for _, file := range []string{"lang/en/nested/lang.yaml", "lang/en/lang.yml", "i18n/en/lang.yaml"} { + err := NewCatalog().Load("golem15.hello", fstest.MapFS{ + file: {Data: []byte("greeting: Hi\n")}, + }) + if err == nil || !strings.Contains(err.Error(), "golem15.hello") || !strings.Contains(err.Error(), "malformed path") { + t.Fatalf("%s error = %v", file, err) + } + } + }) + t.Run("inverted bracket range", func(t *testing.T) { + err := NewCatalog().Load("golem15.hello", fstest.MapFS{ + "lang/en/lang.yaml": {Data: []byte("items: \"[5,2] nope|{1} one\"\n")}, + }) + if err == nil || !strings.Contains(err.Error(), "golem15.hello") || !strings.Contains(err.Error(), "lang/en/lang.yaml") || !strings.Contains(err.Error(), "malformed pipe") { + t.Fatalf("inverted range error = %v", err) + } + }) + t.Run("empty pipe form", func(t *testing.T) { + err := NewCatalog().Load("golem15.hello", fstest.MapFS{ + "lang/en/lang.yaml": {Data: []byte("items: \"one||other\"\n")}, + }) + if err == nil || !strings.Contains(err.Error(), "malformed pipe") { + t.Fatalf("empty form error = %v", err) + } + }) + t.Run("get substitutes name case variants", func(t *testing.T) { + cat := NewCatalog() + if err := cat.Load("golem15.hello", fstest.MapFS{ + "lang/en/lang.yaml": {Data: []byte("helloName: \"hi :name :Name :NAME\"\n")}, + }); err != nil { + t.Fatal(err) + } + tr := NewTranslator(cat, Options{}) + if got := tr.GetIn("en", "golem15.hello::lang.helloName", map[string]string{"name": "alice"}); got != "hi alice Alice ALICE" { + t.Fatalf("Get params = %q", got) + } + }) + t.Run("configured locale and fallback", func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("locale: pl\nfallback_locale: en\n"), 0o644); err != nil { + t.Fatal(err) + } + cfg, err := compass.Load(dir) + if err != nil { + t.Fatal(err) + } + app := backpack.New(cfg) + p := &langPlugin{id: "golem15.hello", fsys: fstest.MapFS{ + "lang/en/lang.yaml": {Data: []byte("greeting: Hello\nonlyEn: English only\n")}, + "lang/pl/lang.yaml": {Data: []byte("greeting: Cześć\n")}, + }} + if err := Activate(app, []*langPlugin{p}); err != nil { + t.Fatal(err) + } + tr, ok := app.Lookup[*Translator]() + if !ok { + t.Fatal("translator not published") + } + if got := tr.Get(context.Background(), "golem15.hello::lang.greeting", nil); got != "Cześć" { + t.Fatalf("configured app.locale = %q", got) + } + if got := tr.GetIn("pl", "golem15.hello::lang.onlyEn", nil); got != "English only" { + t.Fatalf("configured fallback = %q", got) + } + }) +}