- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
346 lines
13 KiB
Go
346 lines
13 KiB
Go
package phrasebook
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io/fs"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
"git.golem15.com/golem15/summercms/modules/towel"
|
|
)
|
|
|
|
func TestTranslationSmoke(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("greeting: Hello\nlabels:\n pluginName: Hello plugin\n")},
|
|
"lang/pl/lang.yaml": {Data: []byte("greeting: Cześć\nlabels:\n pluginName: Wtyczka Hello\n")},
|
|
}
|
|
cat := NewCatalog()
|
|
if err := cat.Load("golem15.hello", fsys); err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
tr := NewTranslator(cat, Options{})
|
|
if got := tr.GetIn("en", "golem15.hello::lang.labels.pluginName", nil); got != "Hello plugin" {
|
|
t.Fatalf("en nested = %q", got)
|
|
}
|
|
if got := tr.GetIn("pl", "golem15.hello::lang.labels.pluginName", nil); got != "Wtyczka Hello" {
|
|
t.Fatalf("pl nested = %q", got)
|
|
}
|
|
if got := tr.GetIn("en", "golem15.hello::lang.greeting", nil); got != "Hello" {
|
|
t.Fatalf("en greeting = %q", got)
|
|
}
|
|
if got := tr.GetIn("pl", "golem15.hello::lang.greeting", nil); got != "Cześć" {
|
|
t.Fatalf("pl greeting = %q", got)
|
|
}
|
|
|
|
t.Run("malformed path", func(t *testing.T) {
|
|
err := NewCatalog().Load("golem15.hello", fstest.MapFS{
|
|
"lang/en.yaml": {Data: []byte("greeting: Hello\n")},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "golem15.hello") || !strings.Contains(err.Error(), "malformed path") {
|
|
t.Fatalf("malformed path error = %v", err)
|
|
}
|
|
})
|
|
t.Run("non-string leaf", func(t *testing.T) {
|
|
err := NewCatalog().Load("golem15.hello", fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("count: 1\n")},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "golem15.hello") || !strings.Contains(err.Error(), "lang/en/lang.yaml") || !strings.Contains(err.Error(), "non-string leaf") {
|
|
t.Fatalf("non-string leaf error = %v", err)
|
|
}
|
|
})
|
|
t.Run("duplicate key", func(t *testing.T) {
|
|
err := NewCatalog().Load("golem15.hello", fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("labels:\n name: a\nlabels.name: b\n")},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "golem15.hello") || !strings.Contains(err.Error(), "duplicate key") {
|
|
t.Fatalf("duplicate key error = %v", err)
|
|
}
|
|
})
|
|
t.Run("duplicate namespace owner", func(t *testing.T) {
|
|
cat := NewCatalog()
|
|
fsys := fstest.MapFS{"lang/en/lang.yaml": {Data: []byte("greeting: Hello\n")}}
|
|
if err := cat.Load("golem15.hello", fsys); err != nil {
|
|
t.Fatalf("first Load: %v", err)
|
|
}
|
|
err := cat.Load("golem15.hello", fsys)
|
|
if err == nil || !strings.Contains(err.Error(), "golem15.hello") || !strings.Contains(err.Error(), "duplicate namespace owner") {
|
|
t.Fatalf("duplicate namespace error = %v", err)
|
|
}
|
|
})
|
|
t.Run("activate publishes translator", func(t *testing.T) {
|
|
app := backpack.New(&compass.Config{})
|
|
p := &langPlugin{id: "golem15.hello", fsys: fsys}
|
|
if err := Activate(app, []*langPlugin{p}); err != nil {
|
|
t.Fatalf("Activate: %v", err)
|
|
}
|
|
tr, ok := app.Lookup[*Translator]()
|
|
if !ok {
|
|
t.Fatal("translator not published")
|
|
}
|
|
if got := tr.GetIn("pl", "golem15.hello::lang.greeting", nil); got != "Cześć" {
|
|
t.Fatalf("activated pl = %q", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
type langPlugin struct {
|
|
id string
|
|
fsys fstest.MapFS
|
|
}
|
|
|
|
func (p *langPlugin) ID() string { return p.id }
|
|
func (p *langPlugin) Requires() []string { return nil }
|
|
func (p *langPlugin) Register(*backpack.App) error { return nil }
|
|
func (p *langPlugin) Boot(*backpack.App) error { return nil }
|
|
func (p *langPlugin) LangFS() fs.FS { return p.fsys }
|
|
|
|
func TestPluralSmoke(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("" +
|
|
"albums:\n one: \":count album\"\n other: \":count albums\"\n" +
|
|
"items: \"{0} No items|{1} One item|[2,*] :count items\"\n" +
|
|
"helloName: \"hi :name :Name :NAME\"\n" +
|
|
"raw:\n other: \"{{.Count}} bottles\"\n")},
|
|
"lang/pl/lang.yaml": {Data: []byte("" +
|
|
"albums:\n one: \":count plik\"\n few: \":count pliki\"\n many: \":count plików\"\n other: \":count pliku\"\n" +
|
|
"posts: \":count wpis|:count wpisy|:count wpisów|:count wpisu\"\n")},
|
|
}
|
|
cat := NewCatalog()
|
|
if err := cat.Load("golem15.hello", fsys); err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
tr := NewTranslator(cat, Options{})
|
|
|
|
pl := map[any]string{1: "1 plik", 2: "2 pliki", 5: "5 plików", 22: "22 pliki", 0: "0 plików", 1.5: "1.5 pliku"}
|
|
for n, want := range pl {
|
|
if got := tr.ChoiceIn("pl", "golem15.hello::lang.albums", n, nil); got != want {
|
|
t.Fatalf("pl albums %v = %q, want %q", n, got, want)
|
|
}
|
|
}
|
|
en := map[any]string{1: "1 album", 2: "2 albums", 0: "0 albums", 1.5: "1.5 albums"}
|
|
for n, want := range en {
|
|
if got := tr.ChoiceIn("en", "golem15.hello::lang.albums", n, nil); got != want {
|
|
t.Fatalf("en albums %v = %q, want %q", n, got, want)
|
|
}
|
|
}
|
|
if got := tr.ChoiceIn("pl", "golem15.hello::lang.posts", 1, nil); got != "1 wpis" {
|
|
t.Fatalf("pl posts 1 = %q", got)
|
|
}
|
|
if got := tr.ChoiceIn("pl", "golem15.hello::lang.posts", 2, nil); got != "2 wpisy" {
|
|
t.Fatalf("pl posts 2 = %q", got)
|
|
}
|
|
if got := tr.ChoiceIn("pl", "golem15.hello::lang.posts", 5, nil); got != "5 wpisów" {
|
|
t.Fatalf("pl posts 5 = %q", got)
|
|
}
|
|
if got := tr.ChoiceIn("pl", "golem15.hello::lang.posts", 22, nil); got != "22 wpisy" {
|
|
t.Fatalf("pl posts 22 = %q", got)
|
|
}
|
|
if got := tr.ChoiceIn("en", "golem15.hello::lang.items", 0, nil); got != "No items" {
|
|
t.Fatalf("items 0 = %q", got)
|
|
}
|
|
if got := tr.ChoiceIn("en", "golem15.hello::lang.items", 1, nil); got != "One item" {
|
|
t.Fatalf("items 1 = %q", got)
|
|
}
|
|
if got := tr.ChoiceIn("en", "golem15.hello::lang.items", 5, nil); got != "5 items" {
|
|
t.Fatalf("items 5 = %q", got)
|
|
}
|
|
if got := tr.ChoiceIn("en", "golem15.hello::lang.helloName", 1, map[string]string{"name": "alice"}); got != "hi alice Alice ALICE" {
|
|
t.Fatalf("params = %q", got)
|
|
}
|
|
if got := tr.ChoiceIn("en", "golem15.hello::lang.raw", 2, nil); got != "{{.Count}} bottles" {
|
|
t.Fatalf("go-i18n must not evaluate YAML as a template, got %q", got)
|
|
}
|
|
|
|
t.Run("invalid category for locale", func(t *testing.T) {
|
|
err := NewCatalog().Load("golem15.hello", fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("albums:\n few: x\n other: y\n")},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid plural category") {
|
|
t.Fatalf("want invalid category, got %v", err)
|
|
}
|
|
})
|
|
t.Run("missing other", func(t *testing.T) {
|
|
err := NewCatalog().Load("golem15.hello", fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("albums:\n one: x\n")},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "requires other") {
|
|
t.Fatalf("want missing other, got %v", err)
|
|
}
|
|
})
|
|
t.Run("malformed pipe", func(t *testing.T) {
|
|
err := NewCatalog().Load("golem15.hello", fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("items: \"{x} nope|{1} one\"\n")},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "malformed pipe") {
|
|
t.Fatalf("want malformed pipe, got %v", err)
|
|
}
|
|
})
|
|
t.Run("incomplete pipe", func(t *testing.T) {
|
|
err := NewCatalog().Load("golem15.hello", fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("items: \":count post|:count posts|:count extra\"\n")},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "incomplete pipe") {
|
|
t.Fatalf("want incomplete pipe, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLocaleFallbackSmoke(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("greeting: Hello\nonlyEn: English only\n")},
|
|
"lang/pl/lang.yaml": {Data: []byte("greeting: Cześć\n")},
|
|
}
|
|
cat := NewCatalog()
|
|
if err := cat.Load("golem15.hello", fsys); err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
tr := NewTranslator(cat, Options{})
|
|
if tr.locale != "en" || tr.fallback != "en" {
|
|
t.Fatalf("defaults locale=%q fallback=%q, want en/en", tr.locale, tr.fallback)
|
|
}
|
|
if got := tr.GetIn("pl-PL", "golem15.hello::lang.greeting", nil); got != "Cześć" {
|
|
t.Fatalf("pl-PL parent = %q", got)
|
|
}
|
|
if got := tr.GetIn("pl", "golem15.hello::lang.onlyEn", nil); got != "English only" {
|
|
t.Fatalf("fallback en = %q", got)
|
|
}
|
|
const missing = "golem15.hello::lang.missing"
|
|
if got := tr.GetIn("pl-PL", missing, nil); got != missing {
|
|
t.Fatalf("raw key = %q", got)
|
|
}
|
|
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)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
prev := slog.Default()
|
|
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn})))
|
|
defer slog.SetDefault(prev)
|
|
|
|
logged := "golem15.hello::lang.loggedOnce"
|
|
params := map[string]string{"secret": "nope"}
|
|
tr.GetIn("en", logged, params)
|
|
tr.GetIn("en", logged, params)
|
|
log := buf.String()
|
|
if strings.Count(log, logged) != 1 {
|
|
t.Fatalf("missing key should log once, got %q", log)
|
|
}
|
|
if strings.Contains(log, "secret") || strings.Contains(log, "nope") {
|
|
t.Fatalf("must not log parameters: %q", log)
|
|
}
|
|
|
|
buf.Reset()
|
|
prod := NewTranslator(cat, Options{Production: true})
|
|
prod.GetIn("en", "golem15.hello::lang.otherMissing", nil)
|
|
if buf.Len() != 0 {
|
|
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)
|
|
}
|
|
})
|
|
}
|