Every template and handler message in internal/web goes through a JSON
catalogue under internal/web/locales/<code>/, with English as the fallback.
The language is the person's choice (a cookie set by /_lang/{code}), else
Accept-Language, else landing.language. Tests refuse a key some language
lacks, a translation with other arguments than English, and template text
left in English.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
184 lines
7.1 KiB
Go
184 lines
7.1 KiB
Go
package web
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"gitea.mynodes.xyz/ilker/llmgw/internal/promq"
|
||
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
||
)
|
||
|
||
var clusterTR = map[string]string{"Accept-Language": "tr-TR,tr;q=0.9"}
|
||
|
||
func clusterTurkish(t *testing.T) *Lang {
|
||
t.Helper()
|
||
l := langByCode("tr")
|
||
if l == nil {
|
||
t.Fatal("the portal does not speak Turkish")
|
||
}
|
||
return l
|
||
}
|
||
|
||
func TestTheDashboardSpeaksTurkishWhenAsked(t *testing.T) {
|
||
now := time.Now().Truncate(time.Hour)
|
||
usage := fakeUsage{buckets: []store.Bucket{
|
||
{Start: now.Add(-time.Hour), Requests: 1500, Prompt: 4000, Completion: 900, LatencyMs: 120},
|
||
{Start: now, Requests: 95, Prompt: 9000, Completion: 2100, LatencyMs: 310},
|
||
}}
|
||
_, body := get(t, Dashboard(mapFleet{}, usage, "v1", 5*time.Second, true, NavExtras{}, nil),
|
||
"http://127.0.0.1:9090/", clusterTR)
|
||
for _, want := range []string{
|
||
`<html lang="tr">`, "<h1>Küme</h1>", "Kapasite", "<h2>Düğümler</h2>",
|
||
// A state is still its class, and a Turkish word.
|
||
`<td class="state state-ready">hazır</td>`,
|
||
// The GPU map: its modes, and what a card says under the pointer.
|
||
"<strong>meşguliyet</strong>", "gpu07 GPU 2 · code (code-1) · 93% meşgul · 39,1 / 80 GB",
|
||
// How long a replica has sat idle, in Turkish units.
|
||
"30dk0sn önce",
|
||
// The traffic chart's peak, with Turkish thousands.
|
||
"en yüksek 1,5 bin istek/sa",
|
||
"5 saniyede bir yenilenir",
|
||
} {
|
||
if !strings.Contains(body, want) {
|
||
t.Errorf("the Turkish dashboard lacks %q", want)
|
||
}
|
||
}
|
||
for _, english := range []string{"Capacity", "Holding hardware", "peak ", "requests/h", "No requests recorded"} {
|
||
if strings.Contains(body, english) {
|
||
t.Errorf("the Turkish dashboard still says %q", english)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTheNodePageWritesTurkishNumbers(t *testing.T) {
|
||
q := func(expr string) string { return strings.Replace(expr, "%s", `"llmgw-node01"`, 1) }
|
||
h := &fakeHistory{byExpr: map[string][]promq.Series{
|
||
q(`llmgw_gpu_utilization_percent{node=%s} / 100`): {ts("0", 0.9, 0.97), ts("1", 0, 0)},
|
||
q(`llmgw_gpu_temperature_celsius{node=%s}`): {ts("0", 65, 71)},
|
||
}}
|
||
r := httptest.NewRequest(http.MethodGet, "http://x/admin/nodes/llmgw-node01", nil)
|
||
r.AddCookie(&http.Cookie{Name: langCookie, Value: "tr"})
|
||
w := httptest.NewRecorder()
|
||
nodeMux(gpuSource{}, h).ServeHTTP(w, r)
|
||
body := w.Body.String()
|
||
for _, want := range []string{
|
||
"64 çekirdek", "bellek 64%", "71,2 <span class=\"note\">/ 80 GB", "1.980 MHz",
|
||
"3 düzeltilemeyen ECC hatası", "<h2>GPU'lar</h2>",
|
||
"Meşgul GPU, karta göre", "GPU sıcaklığı, °C", "Son bir saat",
|
||
// The chart's axis and peak in Turkish figures.
|
||
"en yüksek 100%",
|
||
} {
|
||
if !strings.Contains(body, want) {
|
||
t.Errorf("the Turkish node page lacks %q", want)
|
||
}
|
||
}
|
||
if strings.Contains(body, "uncorrected") || strings.Contains(body, "GPU busy, by card") {
|
||
t.Error("the Turkish node page still speaks English")
|
||
}
|
||
}
|
||
|
||
func TestChartTooltipsNameTheDayInTurkish(t *testing.T) {
|
||
tr := clusterTurkish(t)
|
||
// 5 October 2026 is a Monday.
|
||
at := time.Date(2026, 10, 5, 15, 4, 0, 0, time.Local)
|
||
if got := tr.hourLabel(at, 1500, tr.T("dash.unit.requests")); got != "Pzt 15:04 — 1,5 bin istek" {
|
||
t.Errorf("a Turkish tooltip reads %q", got)
|
||
}
|
||
if got := hourLabel(at, 1500, "requests"); got != "Mon 15:04 — 1.5k requests" {
|
||
t.Errorf("the English tooltip changed: %q", got)
|
||
}
|
||
bars := string(tr.BarChart(Series{Values: []float64{1, 2500}, Labels: []string{tr.hourLabel(at, 1, "istek")}}, "istek/sa"))
|
||
if !strings.Contains(bars, "<title>Pzt 15:04 — 1 istek</title>") || !strings.Contains(bars, "en yüksek 2,5 bin istek/sa") {
|
||
t.Errorf("the Turkish bar chart:\n%s", bars)
|
||
}
|
||
if got := string(tr.BarChart(Series{}, "istek")); !strings.Contains(got, "kayıt yok") {
|
||
t.Errorf("an empty Turkish chart: %s", got)
|
||
}
|
||
// The axis of a line chart, and its legend's fold.
|
||
c := tr.LineChart([]promq.Series{ts("a", 0.001, 0.004), ts("b", 0, 0.001), ts("c", 0, 0.001),
|
||
ts("d", 0, 0.001), ts("e", 0, 0.001)}, "%", 4)
|
||
if !strings.Contains(string(c.SVG), ">0,5%<") || c.Peak != "0,5%" {
|
||
t.Errorf("a Turkish percent axis: peak %q\n%s", c.Peak, c.SVG)
|
||
}
|
||
if last := c.Legend[len(c.Legend)-1]; last.Name != "diğer" {
|
||
t.Errorf("the folded line is called %q", last.Name)
|
||
}
|
||
// The coordinates are written the C way whatever the language.
|
||
if regexp.MustCompile(`points="[^"]*\d,\d+,`).MatchString(string(c.SVG)) {
|
||
t.Error("a Turkish decimal comma reached an SVG coordinate")
|
||
}
|
||
if got := tr.windowWord(6 * time.Hour); got != "6 saat" {
|
||
t.Errorf("six hours in Turkish: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTheFabricMapIsHandedItsWordsInThePagesLanguage(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "http://x/admin/fabrics", nil)
|
||
r.Header.Set("Accept-Language", "tr")
|
||
w := httptest.NewRecorder()
|
||
FabricsPage(topoSource{}, false, "v1")(w, r)
|
||
body := w.Body.String()
|
||
m := regexp.MustCompile(`(?s)<script type="application/json" id="fabric-i18n">(.*?)</script>`).FindStringSubmatch(body)
|
||
if m == nil {
|
||
t.Fatalf("the page hands the map no words:\n%s", body)
|
||
}
|
||
var words map[string]string
|
||
if err := json.Unmarshal([]byte(m[1]), &words); err != nil {
|
||
t.Fatalf("the words are not JSON: %v\n%s", err, m[1])
|
||
}
|
||
for k, want := range map[string]string{
|
||
"decimal": ",", "stat.switches": "anahtarlar", "ports_used": "{0}/{1} port",
|
||
"map_failed": "Harita çıkarılamadı: {0}",
|
||
} {
|
||
if words[k] != want {
|
||
t.Errorf("%s is %q, want %q", k, words[k], want)
|
||
}
|
||
}
|
||
for _, want := range []string{"<h1>Fabric'ler</h1>", "Görünümü sıfırla", `placeholder="Düğüm bul"`} {
|
||
if !strings.Contains(body, want) {
|
||
t.Errorf("the Turkish fabrics page lacks %q", want)
|
||
}
|
||
}
|
||
// Every sentence the script falls back to has a Turkish text.
|
||
for _, s := range fabricJSStrings {
|
||
if words[s.key] == "" || strings.Contains(words[s.key], "%!") {
|
||
t.Errorf("the map's %q is %q", s.key, words[s.key])
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTheNVLinkAndLogPagesSpeakTurkish(t *testing.T) {
|
||
_, body := get(t, NVLinkPage(nvlSource{}, false, "v1"), "http://x/admin/nvlink", clusterTR)
|
||
for _, want := range []string{"<h1>NVLink alanları</h1>", "tepsi kullanılabilir", `class="tag bad">hata</span>`, "30 saniyede bir yenilenir"} {
|
||
if !strings.Contains(body, want) {
|
||
t.Errorf("the Turkish NVLink page lacks %q", want)
|
||
}
|
||
}
|
||
|
||
req := httptest.NewRequest("GET", "/admin/nodes/n6/logs?source=agent", nil)
|
||
req.SetPathValue("id", "n6")
|
||
req.Header.Set("Accept-Language", "tr")
|
||
rec := httptest.NewRecorder()
|
||
NodeLogsPage(logsFleet{}, false, "test")(rec, req)
|
||
for _, want := range []string{"ajanın günlüğü", "son 300 satır", "izle"} {
|
||
if !strings.Contains(rec.Body.String(), want) {
|
||
t.Errorf("the Turkish log page lacks %q", want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestAChosenBuildIsAnnouncedInTheChoosersLanguage(t *testing.T) {
|
||
tr := clusterTurkish(t)
|
||
if got := buildNotice(tr, "gpu07", "vllm", "cu129"); !strings.Contains(got, "CUDA 12.9 için derlenmiş") {
|
||
t.Errorf("in Turkish: %q", got)
|
||
}
|
||
if got := buildNotice(english, "gpu07", "vllm", "none"); got != "gpu07 installs vllm no longer; the node is told now, and installs it in the background" {
|
||
t.Errorf("in English: %q", got)
|
||
}
|
||
}
|