game6 / client / test / i18n.test.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Catalog fidelity — the CLJS replacement for the TS-era MsgKey type check.
// Two gates: every EN key exists in ro/hu with nothing stray (a missing
// translation fails the suite), and every message VALUE is byte-equal to the
// TS catalogs' snapshot in vectors/i18n.json (so the port never quietly
// reworded a string).
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as lib from "../test-dist/testlib.js";

const ts = JSON.parse(readFileSync(new URL("./vectors/i18n.json", import.meta.url), "utf8"));
const en = [...lib.catalogKeys("en")];

test("EN catalog is non-empty and carries the social.* block", () => {
  assert.ok(en.length >= 40);
  assert.ok(en.some((k) => k.startsWith("social.")));
});

for (const lang of ["ro", "hu"]) {
  test(`${lang} covers every EN key, and nothing else`, () => {
    assert.deepEqual([...lib.catalogKeys(lang)], en);
  });
}

for (const lang of ["en", "ro", "hu"]) {
  test(`${lang} messages are byte-identical to the TS catalog`, () => {
    assert.deepEqual([...lib.catalogKeys(lang)], Object.keys(ts[lang]).sort(), `${lang} key set`);
    for (const k of Object.keys(ts[lang])) {
      const got = lib.catalogMsg(lang, k);
      const want = ts[lang][k];
      if (typeof want === "string") {
        assert.equal(got, want, `${lang} ${k}`);
      } else {
        // a plural site: compare the variant map, categories included
        assert.deepEqual(JSON.parse(JSON.stringify(got)), want, `${lang} ${k}`);
      }
    }
  });
}

test("plural sites: ro one/few/other, hu other-only, en one/other", () => {
  const cats = (lang, k) => Object.keys(JSON.parse(JSON.stringify(lib.catalogMsg(lang, k)))).sort();
  for (const k of ["rec.bars"]) {
    assert.deepEqual(cats("en", k), ["one", "other"], `en ${k}`);
    assert.deepEqual(cats("ro", k), ["few", "one", "other"], `ro ${k}`);
    assert.deepEqual(cats("hu", k), ["other"], `hu ${k}`);
  }
});

test("wire-adjacent strings are never translated (brand + protocol)", () => {
  for (const lang of ["en", "ro", "hu"]) {
    for (const k of lib.catalogKeys(lang)) {
      const m = lib.catalogMsg(lang, k);
      if (typeof m === "string") assert.ok(!m.includes("/odeon/"), `${lang} ${k}`);
    }
  }
});

test("music notation never enters a catalog (pad names, note names)", () => {
  // The pad and note names are drawn straight from the code, never through
  // t() — so no catalog may carry one as a key or as a whole message.
  const notation = new Set([...lib.padNames, ...lib.noteNames]);
  for (const lang of ["en", "ro", "hu"]) {
    for (const k of lib.catalogKeys(lang)) {
      assert.ok(!notation.has(k), `${lang} has a notation key ${k}`);
      const m = lib.catalogMsg(lang, k);
      if (typeof m === "string") {
        assert.ok(!notation.has(m.trim()), `${lang} ${k} translates the notation "${m}"`);
      }
    }
  }
});

test("index.html's baked English matches the catalog for every data-i18n key", () => {
  const html = readFileSync(new URL("../public/index.html", import.meta.url), "utf8");
  const seen = [];
  for (const m of html.matchAll(/data-i18n="([^"]+)"[^>]*>([^<]*)</g)) {
    seen.push([m[1], m[2]]);
  }
  assert.ok(seen.length >= 8, "markup carries data-i18n keys");
  for (const [key, text] of seen) {
    assert.equal(lib.catalogMsg("en", key), text, `data-i18n=${key}`);
  }
  // and every data-i18n-attr target resolves too
  for (const m of html.matchAll(/data-i18n-attr="([^"]+)"/g)) {
    for (const pair of m[1].split(",")) {
      const key = pair.split(":")[1];
      assert.ok(lib.catalogMsg("en", key) !== null, `data-i18n-attr key ${key}`);
    }
  }
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/game6.git