package i18n import ( "regexp" "sort" "strings" "testing" "testing/fstest" ) func testBundle(t *testing.T, hu, en string) *Bundle { t.Helper() b, err := loadFrom(fstest.MapFS{ "locales/hu.json": {Data: []byte(hu)}, "locales/en.json": {Data: []byte(en)}, }) if err != nil { t.Fatal(err) } return b } func TestNormalize(t *testing.T) { for in, want := range map[string]string{"en": "en", " EN ": "en", "hu": "hu", "": "hu", "de": "hu", "en-GB": "hu"} { if got := Normalize(in); got != want { t.Errorf("Normalize(%q) = %q, want %q", in, got, want) } } if IsSupported("EN") || IsSupported("") || !IsSupported("en") { t.Error("IsSupported must be exact") } } // Rule 2 of the package: a missing English line shows the Hungarian one, and says it fell back; a key // absent everywhere is reported as not ok — it never becomes an empty string silently. func TestTextFallback(t *testing.T) { b := testBundle(t, `{"a":"alma","b":"banán"}`, `{"a":"apple"}`) if v, fb, ok := b.Text("en", "a"); v != "apple" || fb || !ok { t.Errorf("translated key: %q fellBack=%v ok=%v", v, fb, ok) } if v, fb, ok := b.Text("en", "b"); v != "banán" || !fb || !ok { t.Errorf("missing English must show Hungarian and flag the fallback: %q fellBack=%v ok=%v", v, fb, ok) } if v, fb, ok := b.Text("hu", "b"); v != "banán" || fb || !ok { t.Errorf("Hungarian is never a fallback of itself: %q fellBack=%v", v, fb) } if _, _, ok := b.Text("en", "zzz"); ok { t.Error("a key absent from Hungarian must not be ok") } if got := b.Msg("en", "zzz"); got != "zzz" { t.Errorf("Msg of an undefined key must be visible, never blank: %q", got) } if got := b.MissingKeys("en"); len(got) != 1 || got[0] != "b" { t.Errorf("MissingKeys(en) = %v, want [b]", got) } } func TestPlural(t *testing.T) { b := testBundle(t, `{"m":"%d perce"}`, `{"m.one":"%d minute ago","m.other":"%d minutes ago"}`) for n, want := range map[int]string{0: "0 minutes ago", 1: "1 minute ago", 5: "5 minutes ago"} { if got := b.Plural("en", "m", n); got != want { t.Errorf("en Plural(%d) = %q, want %q", n, got, want) } } if got := b.Plural("hu", "m", 1); got != "1 perce" { t.Errorf("hu has one form: %q", got) } if !b.Has("en", "m") || len(b.MissingKeys("en")) != 0 { t.Error("a key carried as .one/.other is present, not missing") } } // A marker whose key exists nowhere is LEFT IN PLACE, so html/template refuses the file ("function T // not defined") — loud at startup, never a silently shown marker or an empty string. func TestExpandLeavesUndefinedMarker(t *testing.T) { b := testBundle(t, `{"k":"Szia {{.Name}}"}`, `{}`) out, st := b.Expand("en", `
{{T "k"}}
{{ T "nope" }}
`) if out != `Szia {{.Name}}
{{ T "nope" }}
` { t.Errorf("Expand = %q", out) } if st.Markers != 1 || len(st.FellBack) != 1 || len(st.Undefined) != 1 || st.Undefined[0] != "nope" { t.Errorf("stats = %+v", st) } } // The embedded bundles themselves. func TestEmbeddedBundles(t *testing.T) { b, err := Load() if err != nil { t.Fatal(err) } if len(b.Keys(Default)) < 200 { t.Fatalf("hu bundle has %d keys — the embedded file is not the converted one", len(b.Keys(Default))) } // No orphan: every English key translates a Hungarian key (plural forms count for their base). for _, k := range b.Keys("en") { base := strings.TrimSuffix(strings.TrimSuffix(k, ".one"), ".other") if _, ok := b.msgs[Default][k]; !ok { if _, ok := b.msgs[Default][base]; !ok { t.Errorf("en key %q has no Hungarian source", k) } } } for lang, m := range b.msgs { for k, v := range m { if strings.TrimSpace(v) == "" { t.Errorf("%s %q is empty — a blank box", lang, k) } } } } var fieldRefRe = regexp.MustCompile(`\.[A-Z][A-Za-z0-9_]*(\.[A-Z][A-Za-z0-9_]*)*`) // verbRe counts printf verbs OUTSIDE template actions ({{printf "%.0f" .X}} is a parameter, not a verb // of the message), and without the space flag — „50% szabad" is prose, not „% s". var verbRe = regexp.MustCompile(`%[-+#0-9.]*[sdvq]`) func refs(s string) []string { var out []string for _, a := range regexp.MustCompile(`\{\{.*?\}\}`).FindAllString(s, -1) { out = append(out, fieldRefRe.FindAllString(a, -1)...) } seen := map[string]bool{} var uniq []string for _, r := range out { if !seen[r] { seen[r] = true uniq = append(uniq, r) } } sort.Strings(uniq) return uniq } // A translation carries the same parameters as the Hungarian: the same page fields inside its template // actions, and the same printf verbs. A dropped {{.RecoveryAbandonDate}} would render an English banner // with no date; a dropped %d would print "%!(EXTRA int=5)". func TestBundleParametersMatchAcrossLanguages(t *testing.T) { b, err := Load() if err != nil { t.Fatal(err) } for _, lang := range Supported[1:] { for _, k := range b.Keys(Default) { hu := b.msgs[Default][k] forms := []string{k} if _, ok := b.msgs[lang][k]; !ok { forms = []string{k + ".one", k + ".other"} } for _, f := range forms { v, ok := b.msgs[lang][f] if !ok { continue } if a, c := strings.Join(refs(hu), ","), strings.Join(refs(v), ","); a != c { t.Errorf("%s %q: template fields %q, Hungarian has %q", lang, f, c, a) } if a, c := len(verbRe.FindAllString(stripActions(hu), -1)), len(verbRe.FindAllString(stripActions(v), -1)); a != c { t.Errorf("%s %q: %d printf verbs, Hungarian has %d", lang, f, c, a) } } } } } func stripActions(s string) string { return regexp.MustCompile(`\{\{.*?\}\}`).ReplaceAllString(s, "") }