localise chrome, smooth prose, in English and Bengali
The engine now owns the words it puts on a page that the author did not write (ADR-0034). `internal/render/chrome.go` holds the phrase table, Gregorian month names and decimal digits per language, keyed phrase-then-language so both forms sit side by side and a half-translated row is visible while reading. Templates reach it through `t`, `num` and `day`, registered before parsing so a site override's blocks may call them too. The reference theme stops hardcoding English: "Newer", "Page 2 of 2" and every date now come from the table, while `datetime` attributes stay ASCII because a parser reads them. Authored text gets goldmark's typographer and nothing else — quotes, dashes and ellipses smoothed, code spans untouched because it works on the parsed tree. It is a parser option rather than a function over a page, so the transforms counter does not move; state.md now says why, so the next reader does not miscount. Deliberately absent: relative dates, which need a validity window that only the cache entry will have, and body widow prevention, which cannot be done safely by a pass over rendered HTML.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"khosra/internal/content"
|
||||
)
|
||||
|
||||
func TestChromeTextIsLocalisedAndNeverEmpty(t *testing.T) {
|
||||
cases := []struct{ lang, key, want string }{
|
||||
{"en", "newer", "Newer"},
|
||||
{"bn", "newer", "নতুন"},
|
||||
{"fr", "newer", "Newer"}, // unknown language falls back to the default locale
|
||||
{"bn", "nope", "nope"}, // unknown phrase returns its key rather than nothing
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := text(c.lang, c.key); got != c.want {
|
||||
t.Errorf("text(%q, %q) = %q, want %q", c.lang, c.key, got, c.want)
|
||||
}
|
||||
}
|
||||
if got := text("bn", "page-of", numerals("bn", 2), numerals("bn", 11)); got != "পৃষ্ঠা ২ / ১১" {
|
||||
t.Errorf("page-of in Bengali = %q", got)
|
||||
}
|
||||
if got := text("en", "page-of", "2", "11"); got != "Page 2 of 11" {
|
||||
t.Errorf("page-of in English = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryPhraseCarriesEveryLanguage(t *testing.T) {
|
||||
// A phrase missing a language falls back silently, so nothing else would catch a half-translated table.
|
||||
for key, forms := range chrome {
|
||||
for lang := range months {
|
||||
if forms[lang] == "" {
|
||||
t.Errorf("phrase %q has no %s form", key, lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
for lang, names := range months {
|
||||
if len(names) != 12 {
|
||||
t.Errorf("%s has %d month names, want 12", lang, len(names))
|
||||
}
|
||||
}
|
||||
if len(digits["bn"]) != 10 {
|
||||
t.Errorf("Bengali digits = %d, want 10", len(digits["bn"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatesReadInTheirOwnScript(t *testing.T) {
|
||||
when := time.Date(2026, time.March, 8, 0, 0, 0, 0, time.UTC)
|
||||
if got := day(content.DefaultLang, when); got != "8 March 2026" {
|
||||
t.Errorf("English date = %q", got)
|
||||
}
|
||||
if got := day("bn", when); got != "৮ মার্চ ২০২৬" {
|
||||
t.Errorf("Bengali date = %q, want Bengali digits and month", got)
|
||||
}
|
||||
if got := day("en", time.Time{}); got != "" {
|
||||
t.Errorf("an undated bundle should render nothing, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypographerSmoothsProseAndLeavesCodeAlone(t *testing.T) {
|
||||
r, err := New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := content.Parse("posts/x.md", []byte(
|
||||
"---\ntitle: X\n---\nShe said \"wait\" -- then left... `\"raw\" -- code`\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := r.Bundle(b, "en", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(out)
|
||||
for _, want := range []string{"“wait”", "–", "…"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("prose missing %s:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(got, `<code>"raw" -- code</code>`) {
|
||||
t.Errorf("a code span must survive untouched:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineReadableOutputStaysASCII(t *testing.T) {
|
||||
r, err := New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := content.Parse("posts/x.bn.md", []byte("---\ntitle: এক\ndate: 2026-03-08\n---\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := r.Listing("posts", "bn", []content.Bundle{b}, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(out)
|
||||
if !strings.Contains(got, `datetime="2026-03-08"`) {
|
||||
t.Errorf("datetime must stay ASCII for parsers (ADR-0034):\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "৮ মার্চ ২০২৬") {
|
||||
t.Errorf("the visible date should read in Bengali:\n%s", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user