Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-29 10:03:32 +02:00
commit f8ed33df83
206 changed files with 44165 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package i18n
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
func TestNormalize(t *testing.T) {
cases := map[string]string{
"cs": "cs",
"cs-CZ": "cs",
"cs-CZ-x-praha": "cs",
"EN": "en",
"en-GB": "en",
"de": "",
"": "",
}
for in, want := range cases {
if got := Normalize(in); got != want {
t.Errorf("Normalize(%q) = %q, want %q", in, got, want)
}
}
}
func TestSimpleFallback(t *testing.T) {
// English is the key: it always renders as itself.
if got := Admin.T("en", "New post"); got != "New post" {
t.Errorf("en simple = %q", got)
}
// A key the Czech table misses falls back to English, not to noise.
if got := Admin.T("cs", "No such string in the catalogue"); got != "No such string in the catalogue" {
t.Errorf("cs fallback = %q", got)
}
if got := Admin.T("cs", "New post"); got != "Nová publikace" {
t.Errorf("cs New post = %q", got)
}
}
func TestPlural(t *testing.T) {
csCases := []struct {
n int
want string
}{
{1, "Celkem 1 publikace"},
{2, "Celkem 2 publikace"},
{4, "Celkem 4 publikace"},
{5, "Celkem 5 publikací"},
{11, "Celkem 11 publikací"},
{12, "Celkem 12 publikací"},
{14, "Celkem 14 publikací"},
{22, "Celkem 22 publikace"},
{24, "Celkem 24 publikace"},
{25, "Celkem 25 publikací"},
{111, "Celkem 111 publikací"},
{124, "Celkem 124 publikace"},
}
for _, c := range csCases {
if got := Admin.N("cs", "posts.total", c.n); got != c.want {
t.Errorf("cs posts.total(%d) = %q, want %q", c.n, got, c.want)
}
}
if got := Admin.N("en", "posts.total", 1); got != "1 post in total" {
t.Errorf("en posts.total(1) = %q", got)
}
if got := Admin.N("en", "posts.total", 3); got != "3 posts in total" {
t.Errorf("en posts.total(3) = %q", got)
}
if got := Admin.N("cs", "no.such.id", 2); got != "no.such.id" {
t.Errorf("missing plural id = %q", got)
}
}
func TestFormat(t *testing.T) {
if got := Admin.Tf("cs", "Scheduled for %s", "2026-09-19"); got != "Naplánováno na 2026-09-19" {
t.Errorf("cs Tf = %q", got)
}
if got := Admin.Tf("en", "No such string %s", "x"); got != "No such string x" {
t.Errorf("en Tf fallback = %q", got)
}
if got := Admin.Tf2("cs", "Upgrade to %s failed: %s", "v1.1", "boom"); got != "Aktualizace na v1.1 selhala: boom" {
t.Errorf("cs Tf2 = %q", got)
}
}
func TestHTML(t *testing.T) {
en := Admin.TH("en", "Download a <code>.tar.gz</code> archive of your entire site.")
if en != "Download a <code>.tar.gz</code> archive of your entire site." {
t.Errorf("en TH = %q", en)
}
csOut := Admin.TH("cs", "Download a <code>.tar.gz</code> archive of your entire site.")
if !strings.Contains(csOut, "<code>.tar.gz</code>") {
t.Errorf("cs TH lost the markup: %q", csOut)
}
}
// TestTemplatesAreTranslated walks the admin templates and asserts every
// tr, trh and trf key exists in the Czech catalogue, and every trn id is a
// known plural message. A string edited in a template without its Czech
// counterpart fails here rather than silently rendering English.
func TestTemplatesAreTranslated(t *testing.T) {
root := filepath.Join("..", "..", "internal", "web", "templates")
entries, err := os.ReadDir(root)
if err != nil {
t.Fatalf("read templates: %v", err)
}
callRe := regexp.MustCompile(`\b(tr|trh|trf)\s+"((?:[^"\\]|\\.)*)"`)
// trn takes the count first: "trn .Count \"id\"" or
// "trn (len .Posts) \"id\"", so the id is the first quoted string
// after a run of non-quote characters on the same line.
idRe := regexp.MustCompile(`\btrn\s+[^\n"]*"((?:[^"\\]|\\.)*)"`)
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".html") {
continue
}
src, err := os.ReadFile(filepath.Join(root, entry.Name()))
if err != nil {
t.Fatalf("read %s: %v", entry.Name(), err)
}
for _, m := range callRe.FindAllStringSubmatch(string(src), -1) {
fn, key := m[1], m[2]
switch fn {
case "tr", "trf":
if _, ok := cs[key]; !ok {
t.Errorf("%s: tr key %q has no Czech entry", entry.Name(), key)
}
case "trh":
if _, ok := csHTML[key]; !ok {
t.Errorf("%s: trh key %q has no Czech entry", entry.Name(), key)
}
}
}
for _, m := range idRe.FindAllStringSubmatch(string(src), -1) {
if _, ok := pluralForms[m[1]]; !ok {
t.Errorf("%s: trn id %q is not a plural message", entry.Name(), m[1])
}
}
}
}