package render import ( "fmt" "html/template" "strconv" "strings" "time" "khosra/internal/content" ) // Chrome is every word the engine puts on a page that the author did not write, in each language it // knows (ADR-0034). Keyed by phrase then language, so both forms sit side by side and a missing one is // visible while reading rather than at request time. // // A phrase may carry %s placeholders, filled in order by the caller. Templates reach these through the // `t`, `num` and `day` functions; nothing here is content, and content never comes from here. var chrome = map[string]map[string]string{ "newer": {"en": "Newer", "bn": "নতুন"}, "older": {"en": "Older", "bn": "পুরোনো"}, "empty": {"en": "Nothing here yet.", "bn": "এখনও কিছু নেই।"}, "page-of": {"en": "Page %s of %s", "bn": "পৃষ্ঠা %s / %s"}, "position": {"en": "%s of %s", "bn": "%s / %s"}, "extras": {"en": "Extras", "bn": "অতিরিক্ত"}, "back-to-page": {"en": "Back to the page", "bn": "পৃষ্ঠায় ফিরুন"}, "everything": {"en": "Everything", "bn": "সবকিছু"}, "first": {"en": "First", "bn": "প্রথম"}, "last": {"en": "Last", "bn": "শেষ"}, } // months are Gregorian month names per language, indexed by [time.Month]-1. var months = map[string][]string{ "en": {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}, "bn": {"জানুয়ারি", "ফেব্রুয়ারি", "মার্চ", "এপ্রিল", "মে", "জুন", "জুলাই", "আগস্ট", "সেপ্টেম্বর", "অক্টোবর", "নভেম্বর", "ডিসেম্বর"}, } // digits are the decimal digits of a script, indexed 0-9. A language absent here uses ASCII. var digits = map[string][]rune{ "bn": []rune("০১২৩৪৫৬৭৮৯"), } // funcs are the chrome helpers a template may call. Registered before parsing, so a site root's override // blocks may use them too. var funcs = template.FuncMap{"t": text, "num": numerals, "day": day} // text is one chrome phrase in a language, with any %s placeholders filled. // // It cannot fail: an unknown language falls back to the default locale and an unknown phrase returns its // own key, because a missing translation must never blank a page or break a render (invariant 1). func text(lang, key string, args ...string) string { forms := chrome[key] phrase, ok := forms[lang] if !ok { phrase = forms[content.DefaultLang] } if phrase == "" { return key } if len(args) == 0 { return phrase } filled := make([]any, len(args)) for i, a := range args { filled[i] = a } return fmt.Sprintf(phrase, filled...) } // numerals is an integer in the script of a language: 12 in English, ১২ in Bengali. func numerals(lang string, n int) string { return localiseDigits(lang, strconv.Itoa(n)) } // day is a date as a reader of that language would read it — "8 March 2026", "৮ মার্চ ২০২৬". // // One path for every language rather than [time.Time.Format] for the default locale and a hand-built // string for the rest, so English cannot drift from the others. The machine-readable form belongs in a // datetime attribute and stays ASCII (ADR-0034). func day(lang string, t time.Time) string { if t.IsZero() { return "" } names, ok := months[lang] if !ok { names = months[content.DefaultLang] } return strings.Join([]string{ numerals(lang, t.Day()), names[t.Month()-1], numerals(lang, t.Year()), }, " ") } // localiseDigits swaps ASCII digits for a script's own, leaving everything else alone. func localiseDigits(lang, s string) string { set, ok := digits[lang] if !ok { return s } out := []rune(s) for i, r := range out { if r >= '0' && r <= '9' { out[i] = set[r-'0'] } } return string(out) }