mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
New standalone /safety page (safety.html / safety.js), localized across all 14 languages, in plain language: what VoxVera protects, what it does NOT (relays see your IP; the anonymous key is stored unencrypted; a short PIN won't stop a targeted attacker; published events are public and permanent; nsec-paste risk; reports are public; the host/relays still see requests), and operational tips (Tor/VPN, disposable keys, signer apps). Linked from the editor and the board. Aimed at the activist audience the aesthetic invites — being honest about limits matters more than features. Also adds static site-wide Open Graph / Twitter-card meta to the home, board, and safety pages so a shared voxvera.org link renders a real title/description instead of nothing. Per-flyer previews remain a deliberate non-goal: the naddr lives in the URL fragment (invisible to servers) and per-flyer meta would require server-side rendering, which is out of scope for a purely static site. i18n guard extended to SAFETY_UI (keys + equal bullet-list lengths per language). New cross-engine test covers the page's sections, localization, RTL, and the editor/board links. Full suite 354 passing. Co-Authored-By: Claude Opus 4.8 <[email protected]>
98 lines
3.7 KiB
JavaScript
98 lines
3.7 KiB
JavaScript
// @ts-check
|
|
// Pure-Node guard (no browser): fails if any localized dictionary is missing a
|
|
// key in some language, or has an extra one. We add user-facing strings across
|
|
// 14 languages by hand, so this catches drift before it ships as a fallback to
|
|
// English (or a raw key) for some users.
|
|
const { test, expect } = require("@playwright/test");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
function readSite(file) {
|
|
return fs.readFileSync(path.join(__dirname, "..", "site", file), "utf8");
|
|
}
|
|
|
|
// Extract the first balanced { ... } object literal after a marker, string-aware
|
|
// so braces inside strings don't throw off the depth count.
|
|
function extractObject(src, marker) {
|
|
const start = src.indexOf(marker);
|
|
if (start === -1) throw new Error(`marker not found: ${marker}`);
|
|
const open = src.indexOf("{", start);
|
|
let depth = 0;
|
|
let str = null;
|
|
let esc = false;
|
|
for (let j = open; j < src.length; j += 1) {
|
|
const c = src[j];
|
|
if (str) {
|
|
if (esc) esc = false;
|
|
else if (c === "\\") esc = true;
|
|
else if (c === str) str = null;
|
|
} else if (c === '"' || c === "'" || c === "`") {
|
|
str = c;
|
|
} else if (c === "{") {
|
|
depth += 1;
|
|
} else if (c === "}") {
|
|
depth -= 1;
|
|
if (depth === 0) return src.slice(open, j + 1);
|
|
}
|
|
}
|
|
throw new Error(`unbalanced braces for ${marker}`);
|
|
}
|
|
|
|
function evalObject(objectText) {
|
|
// The literals are pure data (label strings); safe to evaluate.
|
|
return Function(`return (${objectText});`)();
|
|
}
|
|
|
|
function checkParity(label, dict, refLang = "en") {
|
|
const langs = Object.keys(dict);
|
|
expect(langs, `${label}: no ${refLang} reference`).toContain(refLang);
|
|
const ref = Object.keys(dict[refLang]).sort();
|
|
for (const lang of langs) {
|
|
const keys = Object.keys(dict[lang]).sort();
|
|
const missing = ref.filter((k) => !keys.includes(k));
|
|
const extra = keys.filter((k) => !ref.includes(k));
|
|
expect(missing, `${label}[${lang}] is missing: ${missing.join(", ")}`).toEqual([]);
|
|
expect(extra, `${label}[${lang}] has unexpected keys: ${extra.join(", ")}`).toEqual([]);
|
|
}
|
|
}
|
|
|
|
test.describe("i18n key parity", () => {
|
|
test("NOSTR_UI defines every key in all languages", () => {
|
|
const dict = evalObject(extractObject(readSite("nostr-client.js"), "const NOSTR_UI ="));
|
|
expect(Object.keys(dict).length).toBe(14);
|
|
checkParity("NOSTR_UI", dict);
|
|
});
|
|
|
|
test("BOARD_UI defines every key in all languages", () => {
|
|
const dict = evalObject(extractObject(readSite("board.js"), "const BOARD_UI ="));
|
|
expect(Object.keys(dict).length).toBe(14);
|
|
checkParity("BOARD_UI", dict);
|
|
});
|
|
|
|
test("SAFETY_UI defines every key (and list length) in all languages", () => {
|
|
const dict = evalObject(extractObject(readSite("safety.js"), "const SAFETY_UI ="));
|
|
expect(Object.keys(dict).length).toBe(14);
|
|
checkParity("SAFETY_UI", dict);
|
|
// The bullet lists must have the same number of items in every language, or
|
|
// some readers silently get fewer warnings than others.
|
|
for (const listKey of ["protects", "exposes", "tips"]) {
|
|
const refLen = dict.en[listKey].length;
|
|
for (const lang of Object.keys(dict)) {
|
|
expect(dict[lang][listKey].length, `SAFETY_UI[${lang}].${listKey} length`).toBe(refLen);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("locales labels and landing match across all languages", () => {
|
|
const locales = Function(`var window={}; ${readSite("locales.js")}; return window.VoxVeraLocales;`)();
|
|
const labels = {};
|
|
const landing = {};
|
|
for (const [lang, v] of Object.entries(locales)) {
|
|
labels[lang] = v.labels || {};
|
|
landing[lang] = v.landing || {};
|
|
}
|
|
checkParity("locales.labels", labels);
|
|
checkParity("locales.landing", landing);
|
|
});
|
|
});
|