Files
thePR0M3TH3ANandClaude Opus 4.8 29ed45533d Add localized Safety & privacy page + static link-preview meta
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]>
2026-06-11 14:53:48 -04:00

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);
});
});