mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
Add NIP-46 nostrconnect (QR) login path; move board safety link to footer
NIP-46 now supports both connect directions on the editor and the board: - bunker:// (existing): paste a link the signer app generated. - nostrconnect:// (new): the client generates the link, renders it as a QR (+ a copyable link for one-device use), listens on a rendezvous relay (relay.nsec.app), and the signer app scans it and connects back — proven by echoing the client's random secret. The discovered signer pubkey then drives the durable connection (shared adopt path). Both paths stay session-only with an ephemeral local key; the QR uses the same hand-rolled NIP-44 client (NIP-04 decrypt fallback). qrcode- generator is now loaded on the board too. 3 new localized strings (scan / waiting / copy-link) across all 14 languages in NOSTR_UI and BOARD_UI; i18n parity passes. Also per feedback: the board's "Safety & privacy" link moves out of the topbar (where it ran together with "Back to VoxVera") into a centered footer link matching the home page, and both pages now link /safety.html (works on the static host and Vercel alike). Tests: editor and board each connect via a fake signer that "scans" the nostrconnect QR (reads the on-screen URI, echoes the secret) — all 6 engines. Full suite 372 passing. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
b38dea1294
commit
18fa382632
9 files changed
+494
-74
No files matched your search
@@ -9,7 +9,7 @@ The previous Tor/OnionShare CLI version is preserved on the `legacy-tor` branch.
|
||||
## Features
|
||||
|
||||
- **Author and publish.** Fill in the flyer fields and publish them as a Nostr `30078` parameterized replaceable event. Republishing with the same flyer name replaces the previous version.
|
||||
- **Anonymous by default, your identity if you want it.** The editor signs flyers under one of four identities: an **anonymous** per-device key (default, no setup), a **NIP-07 browser extension** (your secret never touches the page), an **imported `nsec`**, or a **NIP-46 remote signer** (paste a `bunker://` link — your secret stays in your signer app, the browser holds only an ephemeral key; session-only). An imported key signs in-memory for the session; tick "Remember on this device" to keep it, encrypted with a PIN (PBKDF2 → AES-GCM) — the plaintext secret is never stored. A short PIN is a convenience lock, not protection against a targeted attacker (see the [spec](docs/nostr-static-client-spec.md#publishing-identity)); the extension and remote-signer paths are stronger.
|
||||
- **Anonymous by default, your identity if you want it.** The editor signs flyers under one of four identities: an **anonymous** per-device key (default, no setup), a **NIP-07 browser extension** (your secret never touches the page), an **imported `nsec`**, or a **NIP-46 remote signer** (paste a `bunker://` link or scan a `nostrconnect://` QR with your signer app — your secret stays in the app, the browser holds only an ephemeral key; session-only). An imported key signs in-memory for the session; tick "Remember on this device" to keep it, encrypted with a PIN (PBKDF2 → AES-GCM) — the plaintext secret is never stored. A short PIN is a convenience lock, not protection against a targeted attacker (see the [spec](docs/nostr-static-client-spec.md#publishing-identity)); the extension and remote-signer paths are stronger.
|
||||
- **Publish confirmation.** After signing and publishing, a modal shows the new flyer's URL to copy, so authors know they're done and where to find it (localized, like everything else).
|
||||
- **Edit, re-publish, and delete your flyers.** Because flyers are replaceable events, the author can edit one in place (same key + Flyer Name keeps the same URL), re-publish it as a keep-alive so relays don't age it out, or delete it. Delete publishes a replaceable **tombstone** (so VoxVera hides it even where relays ignore deletions) **and** a NIP-09 deletion request — best-effort, author-only.
|
||||
- **Import / export a flyer design.** Save the current flyer as a portable `.json` file and load it back later — or hand it to someone else. The design file holds the content and relay list but **nothing identity-bound** (no npub, address, or signature; those are generated at publish), so a recipient can import it and publish under their own key. Importing asks for confirmation before replacing what's in the editor.
|
||||
|
||||
@@ -143,15 +143,22 @@ identity controls (`activeIdentity` / `signActiveEvent` in `nostr-client.js`):
|
||||
memory** for the session. If the user ticks "Remember on this device", the
|
||||
secret is encrypted with a PIN and stored as `voxvera_identity_nsec_enc`; the
|
||||
plaintext secret is never written to storage.
|
||||
- **Remote signer (NIP-46 / bunker).** The user pastes a `bunker://` link from a
|
||||
signer app (nsec.app, Amber, …). The client mints an **ephemeral** local key,
|
||||
connects to the bunker relay over a raw WebSocket, and exchanges encrypted
|
||||
kind-`24133` JSON-RPC requests (`connect` → `get_public_key` → `sign_event`).
|
||||
The user's real secret never reaches the browser. Requests are encrypted with
|
||||
**NIP-44** (with a NIP-04 decrypt fallback for older signers); the vendored
|
||||
nostr-tools bundle has no nip46 module, so this client is hand-rolled. The
|
||||
connection is **session-only** — the bunker token is never persisted, so a
|
||||
reload returns to anonymous.
|
||||
- **Remote signer (NIP-46).** The user's real secret never reaches the browser;
|
||||
the client mints an **ephemeral** local key and exchanges encrypted
|
||||
kind-`24133` JSON-RPC requests (`connect` → `get_public_key` → `sign_event`)
|
||||
over a raw WebSocket. Two connect paths are offered:
|
||||
- **`bunker://` (paste).** The user pastes a link the signer app generated;
|
||||
the client connects out to that bunker relay.
|
||||
- **`nostrconnect://` (QR).** The client generates the link itself, renders it
|
||||
as a QR (`makeQrSvg`) plus a copyable link, and listens on a rendezvous relay
|
||||
(`relay.nsec.app`); the signer app scans it and connects back, proving the
|
||||
connection by echoing the client's random `secret`. The discovered signer
|
||||
pubkey is then used for the durable connection.
|
||||
|
||||
Requests are encrypted with **NIP-44** (with a NIP-04 decrypt fallback for
|
||||
older signers); the vendored nostr-tools bundle has no nip46 module, so this
|
||||
client is hand-rolled. The connection is **session-only** — nothing is
|
||||
persisted, so a reload returns to anonymous.
|
||||
|
||||
The chosen mode is remembered in `voxvera_identity_mode` (and the npub in
|
||||
`voxvera_identity_npub` for display). On reload: NIP-07 re-fetches the pubkey;
|
||||
@@ -278,10 +285,11 @@ de-duplicated per author+`d` identifier, keeping the latest.
|
||||
- **The board is gated behind a connected Nostr identity.** On load it shows a
|
||||
login gate (`#board-gate`) instead of the table, with four ways to connect:
|
||||
a **NIP-07** extension (`window.nostr.getPublicKey()`), a **NIP-46 remote
|
||||
signer** (paste a `bunker://` link; same hand-rolled client as the editor —
|
||||
it establishes the viewer pubkey via `get_public_key` and keeps the connection
|
||||
so `getBoardSigner` can sign reports/deletes remotely this session, then falls
|
||||
back to read-only after a reload), **pasting an `nsec`** (decoded in-page to
|
||||
signer** (either paste a `bunker://` link or scan a `nostrconnect://` QR; same
|
||||
hand-rolled client as the editor — it establishes the viewer pubkey via
|
||||
`get_public_key` and keeps the connection so `getBoardSigner` can sign
|
||||
reports/deletes remotely this session, then falls back to read-only after a
|
||||
reload), **pasting an `nsec`** (decoded in-page to
|
||||
derive the pubkey, then discarded — the secret is never stored or transmitted),
|
||||
or **creating a new key** (reuses this device's anon key in
|
||||
`voxvera_nostr_anon_secret_hex` if present, else generates one, and reveals the
|
||||
|
||||
+16
-13
@@ -198,19 +198,22 @@ board can show them flyers from people they trust.
|
||||
riskier than a NIP-07 extension that keeps the key isolated; the input is a
|
||||
password field, the value is cleared after use, and nothing is transmitted. The
|
||||
**NIP-46 remote signer** (below) is now the safer mobile path.
|
||||
- **NIP-46 remote signer (shipped — editor *and* board).** Both the editor's
|
||||
publishing identity and the board's login gate can connect a remote signer
|
||||
(bunker) by pasting a `bunker://` link: the user's secret stays in their signer
|
||||
app (nsec.app, Amber, …) and the browser only holds an ephemeral local key. The
|
||||
client is hand-rolled over a raw WebSocket using the vendored NIP-44 (with a
|
||||
NIP-04 decrypt fallback) — the bundle ships no nip46 module. On the board it
|
||||
establishes the viewer pubkey and keeps the connection so reports/deletes are
|
||||
signed remotely (read-only after a reload). Connections are **session-only**
|
||||
(the bunker token is never written to disk, so a reload returns to anonymous /
|
||||
read-only). Covered by the cross-engine e2e suite (editor: connect →
|
||||
get_public_key → sign_event → publish; board: connect → reveal → report
|
||||
signed through the remote signer — both against a fake signer doing real
|
||||
NIP-44 crypto).
|
||||
- **NIP-46 remote signer (shipped — editor *and* board, both connect paths).**
|
||||
Both the editor's publishing identity and the board's login gate can connect a
|
||||
remote signer two ways: **(a) `bunker://`** — paste a link the signer app
|
||||
generated; and **(b) `nostrconnect://` (QR)** — *we* generate the link, show it
|
||||
as a QR (and a copyable link for one-device use), and the signer app scans it
|
||||
and connects back (verified by echoing our secret). Either way the user's
|
||||
secret stays in their signer app (nsec.app, Amber, …) and the browser holds
|
||||
only an ephemeral local key. The client is hand-rolled over a raw WebSocket
|
||||
using the vendored NIP-44 (with a NIP-04 decrypt fallback) — the bundle ships
|
||||
no nip46 module; the QR flow uses `relay.nsec.app` as the rendezvous relay. On
|
||||
the board it establishes the viewer pubkey and keeps the connection so
|
||||
reports/deletes are signed remotely (read-only after a reload). Connections are
|
||||
**session-only** (nothing is written to disk, so a reload returns to anonymous
|
||||
/ read-only). Covered by the cross-engine e2e suite for both paths on both
|
||||
pages (bunker and nostrconnect), against a fake signer doing real NIP-44
|
||||
crypto.
|
||||
- **Editor identity (shipped).** The editor signs under anon / NIP-07 / imported
|
||||
nsec / remote signer. A remembered nsec is PIN-encrypted at rest (PBKDF2 600k →
|
||||
AES-GCM); plaintext is never stored. **Known weakness:** a short numeric PIN is
|
||||
|
||||
+107
-4
@@ -397,7 +397,7 @@ test.describe("VoxVera static client", () => {
|
||||
const link = page.locator('.preview-band > .board-link a[href="board.html"]');
|
||||
await expect(link).toHaveText("Bulletin board");
|
||||
// The safety link sits alongside it, below the preview.
|
||||
await expect(page.locator('.preview-band > .board-link a[href="/safety"]')).toHaveText("Safety & privacy");
|
||||
await expect(page.locator('.preview-band > .board-link a[href="/safety.html"]')).toHaveText("Safety & privacy");
|
||||
// It is app chrome below the preview, not part of the printable sheet.
|
||||
await expect(page.locator(".container .board-link")).toHaveCount(0);
|
||||
// Screen-only — it must not appear in print output.
|
||||
@@ -741,7 +741,93 @@ test.describe("VoxVera static client", () => {
|
||||
expect(errors, errors.join("\n")).toHaveLength(0);
|
||||
});
|
||||
|
||||
// A fake relay that also plays a signer "scanning" the nostrconnect QR: when it
|
||||
// sees the client's listener subscription it reads the on-screen nostrconnect
|
||||
// URI, echoes the secret back (the connect proof), then answers requests.
|
||||
function nostrConnectSignerInit() {
|
||||
function hexToBytes(hex) {
|
||||
const b = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < b.length; i += 1) b[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
class FakeWS {
|
||||
constructor() { this.readyState = 1; setTimeout(() => this.onopen && this.onopen(), 1); }
|
||||
send(data) {
|
||||
let m; try { m = JSON.parse(data); } catch (_) { return; }
|
||||
const NT = window.NostrTools;
|
||||
if (m[0] === "REQ") {
|
||||
this.sub = m[1];
|
||||
const f = m[2] || {};
|
||||
if (f["#p"] && f.kinds && f.kinds.indexOf(24133) !== -1 && window.__nip46SkHex) {
|
||||
const clientPubkey = f["#p"][0];
|
||||
const uriEl = document.getElementById("nip46-uri") || document.getElementById("board-nip46-uri");
|
||||
const uri = uriEl ? uriEl.value : "";
|
||||
const mm = uri.match(/secret=([0-9a-f]+)/i);
|
||||
if (mm) {
|
||||
const sk = hexToBytes(window.__nip46SkHex);
|
||||
const ck = NT.nip44.getConversationKey(sk, clientPubkey);
|
||||
const content = NT.nip44.encrypt(JSON.stringify({ id: "echo", result: mm[1] }), ck);
|
||||
const echo = NT.finalizeEvent({ kind: 24133, created_at: Math.floor(Date.now() / 1000), tags: [["p", clientPubkey]], content }, sk);
|
||||
setTimeout(() => this.onmessage && this.onmessage({ data: JSON.stringify(["EVENT", this.sub, echo]) }), 5);
|
||||
}
|
||||
}
|
||||
setTimeout(() => this.onmessage && this.onmessage({ data: JSON.stringify(["EOSE", this.sub]) }), 1);
|
||||
return;
|
||||
}
|
||||
if (m[0] !== "EVENT") return;
|
||||
const ev = m[1];
|
||||
if (ev.kind === 24133) {
|
||||
const sk = hexToBytes(window.__nip46SkHex);
|
||||
const ck = NT.nip44.getConversationKey(sk, ev.pubkey);
|
||||
let req; try { req = JSON.parse(NT.nip44.decrypt(ev.content, ck)); } catch (_) { return; }
|
||||
let result = "";
|
||||
if (req.method === "get_public_key") result = window.__nip46Pubkey;
|
||||
else if (req.method === "sign_event") result = JSON.stringify(NT.finalizeEvent(JSON.parse(req.params[0]), sk));
|
||||
else if (req.method === "connect") result = "ack";
|
||||
const content = NT.nip44.encrypt(JSON.stringify({ id: req.id, result }), ck);
|
||||
const resp = NT.finalizeEvent({ kind: 24133, created_at: Math.floor(Date.now() / 1000), tags: [["p", ev.pubkey]], content }, sk);
|
||||
const sub = this.sub;
|
||||
setTimeout(() => this.onmessage && this.onmessage({ data: JSON.stringify(["EVENT", sub, resp]) }), 5);
|
||||
return;
|
||||
}
|
||||
window.__published = window.__published || [];
|
||||
window.__published.push(ev);
|
||||
setTimeout(() => this.onmessage && this.onmessage({ data: JSON.stringify(["OK", ev.id, true, ""]) }), 1);
|
||||
}
|
||||
close() {}
|
||||
}
|
||||
window.WebSocket = FakeWS;
|
||||
}
|
||||
|
||||
async function mintSignerKey(page) {
|
||||
return page.evaluate(() => {
|
||||
const sk = window.NostrTools.generateSecretKey();
|
||||
window.__nip46SkHex = Array.from(sk).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
window.__nip46Pubkey = window.NostrTools.getPublicKey(sk);
|
||||
return window.__nip46Pubkey;
|
||||
});
|
||||
}
|
||||
|
||||
test("editor connects via the NIP-46 nostrconnect (QR) flow", async ({ page }) => {
|
||||
const errors = trackErrors(page);
|
||||
await page.addInitScript(nostrConnectSignerInit);
|
||||
await page.goto("/#editor");
|
||||
const signerPubkey = await mintSignerKey(page);
|
||||
const expectedNpub = await page.evaluate((pk) => window.NostrTools.nip19.npubEncode(pk), signerPubkey);
|
||||
await page.locator("#use-nip46-toggle").click();
|
||||
await page.locator("#nip46-scan").click();
|
||||
// A nostrconnect:// link + QR are produced for the signer to scan.
|
||||
await expect(page.locator("#nip46-uri")).toHaveValue(/^nostrconnect:\/\//);
|
||||
await expect(page.locator("#nip46-qr svg")).toHaveCount(1);
|
||||
// The fake signer "scans" it and connects back → identity adopts the signer.
|
||||
await expect(page.locator("#signer-state")).toHaveText("Publishing via remote signer");
|
||||
await expect(page.locator("#author-npub-output")).toHaveText(expectedNpub);
|
||||
expect(errors, errors.join("\n")).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("exports the current flyer design as a portable, identity-free JSON file", async ({ page }) => {
|
||||
await page.goto("/#editor");
|
||||
// Wait for the editor to finish populating defaults before overwriting them
|
||||
await page.goto("/#editor");
|
||||
// Wait for the editor to finish populating defaults before overwriting them
|
||||
// (otherwise setDefaultText can race ahead of fill() on some engines).
|
||||
@@ -1256,6 +1342,22 @@ test.describe("VoxVera static client", () => {
|
||||
expect(reports[0].tags.some((tg) => tg[0] === "p" && tg[1] === AUTHOR)).toBe(true);
|
||||
});
|
||||
|
||||
test("board connects via the NIP-46 nostrconnect (QR) flow", async ({ page }) => {
|
||||
const errors = trackErrors(page);
|
||||
await page.addInitScript(nostrConnectSignerInit);
|
||||
await page.goto("/board.html");
|
||||
const signerPubkey = await mintSignerKey(page);
|
||||
await page.locator("#board-connect-nip46-toggle").click();
|
||||
await page.locator("#board-nip46-scan").click();
|
||||
// A nostrconnect:// link + QR are produced for the signer to scan.
|
||||
await expect(page.locator("#board-nip46-uri")).toHaveValue(/^nostrconnect:\/\//);
|
||||
await expect(page.locator("#board-nip46-qr svg")).toHaveCount(1);
|
||||
// The fake signer "scans" and connects back → the board reveals as that key.
|
||||
await expect(page.locator("#board-content")).toBeVisible();
|
||||
expect(await page.evaluate(() => localStorage.getItem("voxvera_connected_pubkey"))).toBe(signerPubkey);
|
||||
expect(errors, errors.join("\n")).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("safety page renders all sections, localizes, and is linked from editor + board", async ({ page }) => {
|
||||
const errors = trackErrors(page);
|
||||
await page.goto("/safety.html");
|
||||
@@ -1270,11 +1372,12 @@ test.describe("VoxVera static client", () => {
|
||||
await expect(page.locator("html")).toHaveAttribute("dir", "rtl");
|
||||
await expect(page.locator("#safety-title")).toHaveText("الأمان والخصوصية");
|
||||
await expect(page.locator("#safety-exposes li")).toHaveCount(7);
|
||||
// Reachable from both the editor and the board (served at /safety on Vercel).
|
||||
// Reachable from the editor and the board via a footer link.
|
||||
await page.goto("/");
|
||||
await expect(page.locator('.board-link a[href="/safety"]')).toHaveCount(1);
|
||||
await expect(page.locator('.board-link a[href="/safety.html"]')).toHaveCount(1);
|
||||
await page.goto("/board.html");
|
||||
await expect(page.locator("#board-safety")).toHaveAttribute("href", "/safety");
|
||||
await expect(page.locator("#board-safety")).toHaveAttribute("href", "/safety.html");
|
||||
await expect(page.locator(".board-link #board-safety")).toBeVisible();
|
||||
expect(errors, errors.join("\n")).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
||||
+12
-1
@@ -27,7 +27,6 @@
|
||||
</div>
|
||||
<div class="topbar-controls">
|
||||
<a id="board-back" class="board-back" href="/">Back to VoxVera</a>
|
||||
<a id="board-safety" class="board-back" href="/safety">Safety & privacy</a>
|
||||
<span id="board-identity" class="board-identity" hidden>
|
||||
<span id="board-identity-npub" class="board-npub"></span>
|
||||
<button id="board-disconnect" type="button" class="board-disconnect">Disconnect</button>
|
||||
@@ -57,6 +56,15 @@
|
||||
<input id="board-nip46-input" type="text" autocomplete="off" spellcheck="false" placeholder="bunker://…">
|
||||
<button id="board-nip46-submit" type="button" class="board-connect">Connect</button>
|
||||
<p id="board-nip46-note" class="board-key-note"></p>
|
||||
<button id="board-nip46-scan" type="button" class="board-connect-alt">Scan with your signer app</button>
|
||||
<!-- nostrconnect:// QR: this client generates the link; the signer app
|
||||
scans it and connects back. The local key is ephemeral. -->
|
||||
<div id="board-nip46-qr-wrap" hidden>
|
||||
<div id="board-nip46-qr" class="nip46-qr"></div>
|
||||
<input id="board-nip46-uri" readonly>
|
||||
<button id="board-nip46-uri-copy" type="button" class="board-connect-alt">Copy link</button>
|
||||
<p id="board-nip46-scan-status" class="board-key-note" role="status"></p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- nsec import: the key is decoded in-page to derive the pubkey, then
|
||||
discarded. Only the pubkey is persisted (board never signs). -->
|
||||
@@ -104,6 +112,8 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<p class="board-link"><a id="board-safety" href="/safety.html">Safety & privacy</a></p>
|
||||
|
||||
<div id="board-delete-modal" class="modal-overlay" hidden>
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="board-delete-title">
|
||||
<h2 id="board-delete-title">Delete this flyer?</h2>
|
||||
@@ -130,6 +140,7 @@
|
||||
|
||||
<script src="/locales.js"></script>
|
||||
<script src="/vendor/nostr-tools/nostr.bundle.js"></script>
|
||||
<script src="/vendor/qrcode-generator/qrcode.js"></script>
|
||||
<script src="/board.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+129
-24
@@ -38,22 +38,26 @@
|
||||
const NIP46_RELAY_TIMEOUT_MS = 12000;
|
||||
const NIP46_CONNECT_TIMEOUT_MS = 90000;
|
||||
const NIP46_SIGN_TIMEOUT_MS = 90000;
|
||||
// Rendezvous relay for the nostrconnect:// (QR) flow — the client proposes it
|
||||
// and the signer app connects to it. relay.nsec.app is the de-facto NIP-46
|
||||
// relay the common signer apps already speak.
|
||||
const NIP46_NOSTRCONNECT_RELAY = "wss://relay.nsec.app";
|
||||
|
||||
const BOARD_UI = {
|
||||
en: { title: "Bulletin Board", subtitle: "Most recent flyers published to the relays.", col_title: "Title", col_link: "Link", col_author: "Posted by", col_language: "Language", col_date: "Posted", col_event_id: "Event ID", act_edit: "Edit", act_delete: "Delete", act_rebroadcast: "Re-broadcast", act_block: "Block", act_report: "Report", safety_link: "Safety & privacy", connect_nip46: "Remote signer (NIP-46)", nip46_hint: "Paste a bunker:// link from your signer app (e.g. nsec.app, Amber).", nip46_bad_uri: "That doesn't look like a bunker:// link.", report_title: "Report this flyer?", report_message: "This publishes a public report to the relays and hides the flyer for you. Reports can't be undone.", report_reason: "Reason (optional)", report_submit: "Submit report", report_done: "Report submitted.", report_unavailable: "To report from here, connect with a browser extension or a generated key.", report_cat_spam: "Spam", report_cat_illegal: "Illegal content", report_cat_impersonation: "Impersonation", report_cat_other: "Other", delete_title: "Delete this flyer?", delete_message: "This requests deletion from the relays and replaces it with a deletion marker so VoxVera hides it. Deletion can't be guaranteed across every relay.", delete_confirm: "Delete", cancel: "Cancel", deleted_done: "Deletion requested.", rebroadcast_done: "Re-broadcast to the relays.", sign_unavailable: "To delete from here, connect with a browser extension or a generated key — or delete from the editor.", unblock_all: "Clear blocked", mine_only: "My flyers", details_label: "Details", actions_label: "Actions",link_text: "link", loading: "Loading flyers…", empty: "No flyers found yet.", error: "Could not reach the relays.", back: "← Back to VoxVera", gate_title: "Connect to view the board", gate_body: "The bulletin board is visible to people who connect a Nostr identity. Choose how to connect below.", connect: "Connect", connecting: "Connecting…", no_extension: "No Nostr extension found. Install a NIP-07 browser extension to view the board.", connect_failed: "Connection cancelled or failed. Please try again.", disconnect: "Disconnect", filter_following: "Showing flyers from people you follow.", filter_seeded: "You don't follow anyone yet — showing a curated set from VoxVera's trust network.", filter_all: "Showing all flyers on the relays (unfiltered).", filter_unavailable: "Couldn't load a trust network — showing all flyers.", filter_empty: "No flyers from your trust network yet. Turn on “Show all” to see everything.", show_all_label: "Show all flyers", connect_extension: "Browser extension (NIP-07)", connect_nsec: "Use a private key (nsec)", connect_generate: "Create a new key", key_note: "Your key stays in this browser and is only used to read your follow list — it is never sent anywhere.", invalid_nsec: "That doesn't look like a valid nsec key.", gen_warning: "This is your new secret key. Anyone who has it controls this identity — save it somewhere safe, because VoxVera can't recover it.", copy: "Copy", copied: "Copied", continue_label: "Continue" },
|
||||
es: { title: "Tablón de anuncios", subtitle: "Carteles más recientes publicados en los relés.", col_title: "Título", col_link: "Enlace", col_author: "Publicado por", col_language: "Idioma", col_date: "Publicado", col_event_id: "ID del evento", act_edit: "Editar", act_delete: "Eliminar", act_rebroadcast: "Reemitir", act_block: "Bloquear", act_report: "Denunciar", safety_link: "Seguridad y privacidad", connect_nip46: "Firmante remoto (NIP-46)", nip46_hint: "Pega un enlace bunker:// de tu app de firma (p. ej. nsec.app, Amber).", nip46_bad_uri: "Esto no parece un enlace bunker://.", report_title: "¿Denunciar este cartel?", report_message: "Esto publica una denuncia pública en los relés y oculta el cartel para ti. Las denuncias no se pueden deshacer.", report_reason: "Motivo (opcional)", report_submit: "Enviar denuncia", report_done: "Denuncia enviada.", report_unavailable: "Para denunciar desde aquí, conéctate con una extensión de navegador o una clave generada.", report_cat_spam: "Spam", report_cat_illegal: "Contenido ilegal", report_cat_impersonation: "Suplantación", report_cat_other: "Otro", delete_title: "¿Eliminar este cartel?", delete_message: "Esto solicita la eliminación a los relés y lo reemplaza por una marca de eliminación para que VoxVera lo oculte. No se puede garantizar la eliminación en todos los relés.", delete_confirm: "Eliminar", cancel: "Cancelar", deleted_done: "Eliminación solicitada.", rebroadcast_done: "Reemitido a los relés.", sign_unavailable: "Para eliminar desde aquí, conéctate con una extensión del navegador o una clave generada, o elimínalo desde el editor.", unblock_all: "Borrar bloqueados", mine_only: "Mis carteles", details_label: "Detalles", actions_label: "Acciones", link_text: "enlace", loading: "Cargando carteles…", empty: "Aún no se encontraron carteles.", error: "No se pudo conectar con los relés.", back: "← Volver a VoxVera", gate_title: "Conéctate para ver el tablón", gate_body: "El tablón de anuncios es visible para quienes conectan una identidad Nostr. Elige abajo cómo conectarte.", connect: "Conectar", connecting: "Conectando…", no_extension: "No se encontró ninguna extensión Nostr. Instala una extensión de navegador NIP-07 para ver el tablón.", connect_failed: "Conexión cancelada o fallida. Inténtalo de nuevo.", disconnect: "Desconectar", filter_following: "Mostrando carteles de las personas que sigues.", filter_seeded: "Aún no sigues a nadie: mostramos un conjunto curado de la red de confianza de VoxVera.", filter_all: "Mostrando todos los carteles de los relés (sin filtrar).", filter_unavailable: "No se pudo cargar una red de confianza: mostrando todos los carteles.", filter_empty: "Aún no hay carteles de tu red de confianza. Activa «Mostrar todos» para verlo todo.", show_all_label: "Mostrar todos los carteles", connect_extension: "Extensión del navegador (NIP-07)", connect_nsec: "Usar una clave privada (nsec)", connect_generate: "Crear una clave nueva", key_note: "Tu clave permanece en este navegador y solo se usa para leer tu lista de seguidos; nunca se envía a ningún sitio.", invalid_nsec: "Eso no parece una clave nsec válida.", gen_warning: "Esta es tu nueva clave secreta. Cualquiera que la tenga controla esta identidad: guárdala en un lugar seguro, porque VoxVera no puede recuperarla.", copy: "Copiar", copied: "Copiado", continue_label: "Continuar" },
|
||||
de: { title: "Schwarzes Brett", subtitle: "Neueste auf den Relays veröffentlichte Plakate.", col_title: "Titel", col_link: "Link", col_author: "Veröffentlicht von", col_language: "Sprache", col_date: "Veröffentlicht", col_event_id: "Event-ID", act_edit: "Bearbeiten", act_delete: "Löschen", act_rebroadcast: "Erneut senden", act_block: "Blockieren", act_report: "Melden", safety_link: "Sicherheit & Privatsphäre", connect_nip46: "Externer Signierer (NIP-46)", nip46_hint: "Füge einen bunker://-Link aus deiner Signier-App ein (z. B. nsec.app, Amber).", nip46_bad_uri: "Das sieht nicht nach einem bunker://-Link aus.", report_title: "Diesen Flyer melden?", report_message: "Dies veröffentlicht eine öffentliche Meldung auf den Relays und blendet den Flyer für dich aus. Meldungen können nicht rückgängig gemacht werden.", report_reason: "Grund (optional)", report_submit: "Meldung senden", report_done: "Meldung gesendet.", report_unavailable: "Zum Melden von hier verbinde dich mit einer Browser-Erweiterung oder einem generierten Schlüssel.", report_cat_spam: "Spam", report_cat_illegal: "Illegale Inhalte", report_cat_impersonation: "Identitätsdiebstahl", report_cat_other: "Sonstiges", delete_title: "Dieses Plakat löschen?", delete_message: "Dies fordert die Löschung von den Relays an und ersetzt es durch eine Löschmarkierung, sodass VoxVera es ausblendet. Eine Löschung über alle Relays hinweg kann nicht garantiert werden.", delete_confirm: "Löschen", cancel: "Abbrechen", deleted_done: "Löschung angefordert.", rebroadcast_done: "Erneut an die Relays gesendet.", sign_unavailable: "Zum Löschen hier mit einer Browser-Erweiterung oder einem generierten Schlüssel verbinden – oder im Editor löschen.", unblock_all: "Blockierte löschen", mine_only: "Meine Plakate", details_label: "Details", actions_label: "Aktionen",link_text: "Link", loading: "Plakate werden geladen…", empty: "Noch keine Plakate gefunden.", error: "Relays konnten nicht erreicht werden.", back: "← Zurück zu VoxVera", gate_title: "Verbinden, um das Brett zu sehen", gate_body: "Das Schwarze Brett ist für Personen sichtbar, die eine Nostr-Identität verbinden. Wähle unten, wie du dich verbinden möchtest.", connect: "Verbinden", connecting: "Verbindung…", no_extension: "Keine Nostr-Erweiterung gefunden. Installiere eine NIP-07-Browsererweiterung, um das Brett zu sehen.", connect_failed: "Verbindung abgebrochen oder fehlgeschlagen. Bitte erneut versuchen.", disconnect: "Trennen", filter_following: "Plakate von Personen, denen du folgst.", filter_seeded: "Du folgst noch niemandem – wir zeigen eine kuratierte Auswahl aus dem Vertrauensnetz von VoxVera.", filter_all: "Alle Plakate auf den Relays (ungefiltert).", filter_unavailable: "Vertrauensnetz konnte nicht geladen werden – alle Plakate werden angezeigt.", filter_empty: "Noch keine Plakate aus deinem Vertrauensnetz. Aktiviere „Alle anzeigen“, um alles zu sehen.", show_all_label: "Alle Plakate anzeigen", connect_extension: "Browser-Erweiterung (NIP-07)", connect_nsec: "Privaten Schlüssel (nsec) verwenden", connect_generate: "Neuen Schlüssel erstellen", key_note: "Dein Schlüssel bleibt in diesem Browser und wird nur zum Lesen deiner Folgeliste verwendet – er wird nirgendwohin gesendet.", invalid_nsec: "Das sieht nicht nach einem gültigen nsec-Schlüssel aus.", gen_warning: "Das ist dein neuer geheimer Schlüssel. Wer ihn hat, kontrolliert diese Identität – bewahre ihn sicher auf, denn VoxVera kann ihn nicht wiederherstellen.", copy: "Kopieren", copied: "Kopiert", continue_label: "Weiter" },
|
||||
fr: { title: "Tableau d'affichage", subtitle: "Affiches les plus récentes publiées sur les relais.", col_title: "Titre", col_link: "Lien", col_author: "Publié par", col_language: "Langue", col_date: "Publié", col_event_id: "ID de l'événement", act_edit: "Modifier", act_delete: "Supprimer", act_rebroadcast: "Rediffuser", act_block: "Bloquer", act_report: "Signaler", safety_link: "Sécurité et confidentialité", connect_nip46: "Signataire distant (NIP-46)", nip46_hint: "Collez un lien bunker:// depuis votre app de signature (ex. nsec.app, Amber).", nip46_bad_uri: "Cela ne ressemble pas à un lien bunker://.", report_title: "Signaler ce tract ?", report_message: "Ceci publie un signalement public sur les relais et masque le tract pour vous. Les signalements sont irréversibles.", report_reason: "Motif (facultatif)", report_submit: "Envoyer le signalement", report_done: "Signalement envoyé.", report_unavailable: "Pour signaler depuis ici, connectez-vous avec une extension de navigateur ou une clé générée.", report_cat_spam: "Spam", report_cat_illegal: "Contenu illégal", report_cat_impersonation: "Usurpation d'identité", report_cat_other: "Autre", delete_title: "Supprimer cette affiche ?", delete_message: "Cela demande la suppression aux relais et la remplace par un marqueur de suppression pour que VoxVera la masque. La suppression ne peut pas être garantie sur tous les relais.", delete_confirm: "Supprimer", cancel: "Annuler", deleted_done: "Suppression demandée.", rebroadcast_done: "Rediffusée aux relais.", sign_unavailable: "Pour supprimer ici, connectez-vous avec une extension de navigateur ou une clé générée — ou supprimez depuis l'éditeur.", unblock_all: "Effacer les blocages", mine_only: "Mes affiches", details_label: "Détails", actions_label: "Actions",link_text: "lien", loading: "Chargement des affiches…", empty: "Aucune affiche trouvée pour l'instant.", error: "Impossible de joindre les relais.", back: "← Retour à VoxVera", gate_title: "Connectez-vous pour voir le tableau", gate_body: "Le tableau d'affichage est visible par les personnes qui connectent une identité Nostr. Choisissez ci-dessous comment vous connecter.", connect: "Se connecter", connecting: "Connexion…", no_extension: "Aucune extension Nostr trouvée. Installez une extension de navigateur NIP-07 pour voir le tableau.", connect_failed: "Connexion annulée ou échouée. Veuillez réessayer.", disconnect: "Se déconnecter", filter_following: "Affiches des personnes que vous suivez.", filter_seeded: "Vous ne suivez encore personne – voici une sélection issue du réseau de confiance de VoxVera.", filter_all: "Toutes les affiches sur les relais (non filtrées).", filter_unavailable: "Impossible de charger un réseau de confiance – toutes les affiches sont affichées.", filter_empty: "Aucune affiche de votre réseau de confiance pour l'instant. Activez « Tout afficher » pour tout voir.", show_all_label: "Afficher toutes les affiches", connect_extension: "Extension de navigateur (NIP-07)", connect_nsec: "Utiliser une clé privée (nsec)", connect_generate: "Créer une nouvelle clé", key_note: "Votre clé reste dans ce navigateur et ne sert qu'à lire votre liste d'abonnements ; elle n'est jamais envoyée nulle part.", invalid_nsec: "Cela ne ressemble pas à une clé nsec valide.", gen_warning: "Voici votre nouvelle clé secrète. Quiconque la possède contrôle cette identité — conservez-la en lieu sûr, car VoxVera ne peut pas la récupérer.", copy: "Copier", copied: "Copié", continue_label: "Continuer" },
|
||||
ru: { title: "Доска объявлений", subtitle: "Последние листовки, опубликованные на реле.", col_title: "Заголовок", col_link: "Ссылка", col_author: "Опубликовал", col_language: "Язык", col_date: "Опубликовано", col_event_id: "ID события", act_edit: "Изменить", act_delete: "Удалить", act_rebroadcast: "Переслать", act_block: "Заблокировать", act_report: "Пожаловаться", safety_link: "Безопасность и конфиденциальность", connect_nip46: "Удалённый подписант (NIP-46)", nip46_hint: "Вставьте ссылку bunker:// из вашего приложения для подписи (например, nsec.app, Amber).", nip46_bad_uri: "Это не похоже на ссылку bunker://.", report_title: "Пожаловаться на эту листовку?", report_message: "Это публикует публичную жалобу на реле и скрывает листовку для вас. Жалобу нельзя отменить.", report_reason: "Причина (необязательно)", report_submit: "Отправить жалобу", report_done: "Жалоба отправлена.", report_unavailable: "Чтобы пожаловаться отсюда, подключитесь через расширение браузера или сгенерированный ключ.", report_cat_spam: "Спам", report_cat_illegal: "Незаконный контент", report_cat_impersonation: "Выдача себя за другого", report_cat_other: "Другое", delete_title: "Удалить эту листовку?", delete_message: "Это запросит удаление у реле и заменит её маркером удаления, чтобы VoxVera её скрыл. Удаление на всех реле не гарантируется.", delete_confirm: "Удалить", cancel: "Отмена", deleted_done: "Запрошено удаление.", rebroadcast_done: "Переслано на реле.", sign_unavailable: "Чтобы удалить отсюда, подключитесь через расширение браузера или сгенерированный ключ — или удалите в редакторе.", unblock_all: "Очистить заблокированных", mine_only: "Мои листовки", details_label: "Подробности", actions_label: "Действия",link_text: "ссылка", loading: "Загрузка листовок…", empty: "Листовки пока не найдены.", error: "Не удалось подключиться к реле.", back: "← Назад в VoxVera", gate_title: "Подключитесь, чтобы увидеть доску", gate_body: "Доска объявлений видна тем, кто подключил идентификацию Nostr. Выберите способ подключения ниже.", connect: "Подключить", connecting: "Подключение…", no_extension: "Расширение Nostr не найдено. Установите расширение браузера NIP-07, чтобы увидеть доску.", connect_failed: "Подключение отменено или не удалось. Повторите попытку.", disconnect: "Отключить", filter_following: "Показаны листовки от тех, на кого вы подписаны.", filter_seeded: "Вы пока ни на кого не подписаны — показываем подборку из сети доверия VoxVera.", filter_all: "Показаны все листовки на реле (без фильтра).", filter_unavailable: "Не удалось загрузить сеть доверия — показаны все листовки.", filter_empty: "Пока нет листовок из вашей сети доверия. Включите «Показать все», чтобы увидеть всё.", show_all_label: "Показать все листовки", connect_extension: "Расширение браузера (NIP-07)", connect_nsec: "Использовать приватный ключ (nsec)", connect_generate: "Создать новый ключ", key_note: "Ваш ключ остаётся в этом браузере и используется только для чтения вашего списка подписок — он никуда не отправляется.", invalid_nsec: "Это не похоже на действительный ключ nsec.", gen_warning: "Это ваш новый секретный ключ. Любой, у кого он есть, управляет этой личностью — сохраните его в надёжном месте, потому что VoxVera не сможет его воссLine truncated
|
||||
he: { title: "לוח מודעות", subtitle: "הכרזות האחרונות שפורסמו לממסרים.", col_title: "כותרת", col_link: "קישור", col_author: "פורסם על ידי", col_language: "שפה", col_date: "פורסם", col_event_id: "מזהה אירוע", act_edit: "ערוך", act_delete: "מחק", act_rebroadcast: "שדר מחדש", act_block: "חסום", act_report: "דווח", safety_link: "בטיחות ופרטיות", connect_nip46: "חותם מרוחק (NIP-46)", nip46_hint: "הדבק קישור bunker:// מאפליקציית החתימה שלך (למשל nsec.app או Amber).", nip46_bad_uri: "זה לא נראה כמו קישור bunker://.", report_title: "לדווח על הפלייר הזה?", report_message: "פעולה זו מפרסמת דיווח ציבורי לממסרים ומסתירה את הפלייר עבורך. לא ניתן לבטל דיווחים.", report_reason: "סיבה (אופציונלי)", report_submit: "שלח דיווח", report_done: "הדיווח נשלח.", report_unavailable: "כדי לדווח מכאן, התחבר עם תוסף דפדפן או מפתח שנוצר.", report_cat_spam: "ספאם", report_cat_illegal: "תוכן בלתי חוקי", report_cat_impersonation: "התחזות", report_cat_other: "אחר", delete_title: "למחוק את הכרזה הזו?", delete_message: "פעולה זו מבקשת מחיקה מהממסרים ומחליפה אותה בסימן מחיקה כך ש-VoxVera יסתיר אותה. לא ניתן להבטיח מחיקה בכל הממסרים.", delete_confirm: "מחק", cancel: "ביטול", deleted_done: "התבקשה מחיקה.", rebroadcast_done: "שודר מחדש לממסרים.", sign_unavailable: "כדי למחוק מכאן, התחבר עם תוסף דפדפן או מפתח שנוצר — או מחק מהעורך.", unblock_all: "נקה חסומים", mine_only: "הכרזות שלי", details_label: "פרטים", actions_label: "פעולות",link_text: "קישור", loading: "טוען כרזות…", empty: "לא נמצאו כרזות עדיין.", error: "לא ניתן להתחבר לממסרים.", back: "← חזרה ל-VoxVera", gate_title: "התחבר כדי לראות את הלוח", gate_body: "לוח המודעות גלוי למי שמחבר זהות Nostr. בחר למטה כיצד להתחבר.", connect: "התחבר", connecting: "מתחבר…", no_extension: "לא נמצא תוסף Nostr. התקן תוסף דפדפן NIP-07 כדי לראות את הלוח.", connect_failed: "ההתחברות בוטלה או נכשלה. נסה שוב.", disconnect: "התנתק", filter_following: "מציג כרזות מאנשים שאתה עוקב אחריהם.", filter_seeded: "אינך עוקב עדיין אחר אף אחד — מוצגת בחירה מרשת האמון של VoxVera.", filter_all: "מציג את כל הכרזות בממסרים (ללא סינון).", filter_unavailable: "לא ניתן לטעון רשת אמון — מוצגות כל הכרזות.", filter_empty: "אין עדיין כרזות מרשת האמון שלך. הפעל «הצג הכול» כדי לראות הכול.", show_all_label: "הצג את כל הכרזות", connect_extension: "תוסף דפדפן (NIP-07)", connect_nsec: "השתמש במפתח פרטי (nsec)", connect_generate: "צור מפתח חדש", key_note: "המפתח שלך נשאר בדפדפן זה ומשמש רק לקריאת רשימת העוקבים שלך — הוא לעולם לא נשלח לשום מקום.", invalid_nsec: "זה לא נראה כמו מפתח nsec תקין.", gen_warning: "זהו המפתח הסודי החדש שלך. כל מי שיש לו אותו שולט בזהות הזו — שמור אותו במקום בטוח, מכיוון ש-VoxVera אינה יכולה לשחזר אותו.", copy: "העתק", copied: "הועתק", continue_label: "המשך" },
|
||||
ar: { title: "لوحة الإعلانات", subtitle: "أحدث الملصقات المنشورة على المرحلات.", col_title: "العنوان", col_link: "رابط", col_author: "نُشر بواسطة", col_language: "اللغة", col_date: "تاريخ النشر", col_event_id: "معرّف الحدث", act_edit: "تعديل", act_delete: "حذف", act_rebroadcast: "إعادة بث", act_block: "حظر", act_report: "إبلاغ", safety_link: "الأمان والخصوصية", connect_nip46: "موقّع عن بُعد (NIP-46)", nip46_hint: "الصق رابط bunker:// من تطبيق التوقيع لديك (مثل nsec.app أو Amber).", nip46_bad_uri: "لا يبدو هذا رابط bunker://.", report_title: "هل تريد الإبلاغ عن هذا الملصق؟", report_message: "يؤدي هذا إلى نشر بلاغ عام على المرحلات وإخفاء الملصق بالنسبة لك. لا يمكن التراجع عن البلاغات.", report_reason: "السبب (اختياري)", report_submit: "إرسال البلاغ", report_done: "تم إرسال البلاغ.", report_unavailable: "للإبلاغ من هنا، اتصل عبر إضافة متصفح أو مفتاح مُولَّد.", report_cat_spam: "بريد مزعج", report_cat_illegal: "محتوى غير قانوني", report_cat_impersonation: "انتحال هوية", report_cat_other: "أخرى", delete_title: "حذف هذا الملصق؟", delete_message: "يطلب هذا الحذف من المرحلات ويستبدله بعلامة حذف حتى يخفيه VoxVera. لا يمكن ضمان الحذف عبر كل مرحل.", delete_confirm: "حذف", cancel: "إلغاء", deleted_done: "تم طلب الحذف.", rebroadcast_done: "أُعيد بثه إلى المرحلات.", sign_unavailable: "للحذف من هنا، اتصل بإضافة متصفح أو مفتاح مُنشأ — أو احذف من المحرر.", unblock_all: "مسح المحظورين", mine_only: "ملصقاتي", details_label: "تفاصيل", actions_label: "إجراءات",link_text: "رابط", loading: "جارٍ تحميل الملصقات…", empty: "لم يتم العثور على ملصقات بعد.", error: "تعذّر الوصول إلى المرحلات.", back: "← العودة إلى VoxVera", gate_title: "اتصل لعرض اللوحة", gate_body: "لوحة الإعلانات مرئية لمن يربط هوية Nostr. اختر أدناه كيفية الاتصال.", connect: "اتصال", connecting: "جارٍ الاتصال…", no_extension: "لم يتم العثور على إضافة Nostr. ثبّت إضافة متصفح NIP-07 لعرض اللوحة.", connect_failed: "تم إلغاء الاتصال أو فشل. يرجى المحاولة مرة أخرى.", disconnect: "قطع الاتصال", filter_following: "عرض الملصقات من الأشخاص الذين تتابعهم.", filter_seeded: "أنت لا تتابع أحدًا بعد — نعرض مجموعة مختارة من شبكة الثقة في VoxVera.", filter_all: "عرض جميع الملصقات على المرحلات (دون تصفية).", filter_unavailable: "تعذّر تحميل شبكة ثقة — يتم عرض جميع الملصقات.", filter_empty: "لا توجد ملصقات من شبكة الثقة الخاصة بك بعد. فعّل «عرض الكل» لرؤية كل شيء.", show_all_label: "عرض جميع الملصقات", connect_extension: "إضافة المتصفح (NIP-07)", connect_nsec: "استخدام مفتاح خاص (nsec)", connect_generate: "إنشاء مفتاح جديد", key_note: "يبقى مفتاحك في هذا المتصفح ويُستخدم فقط لقراءة قائمة متابَعاتك — ولا يُرسل إلى أي مكان أبدًا.", invalid_nsec: "لا يبدو هذا مفتاح nsec صالحًا.", gen_warning: "هذا مفتاحك السري الجديد. أي شخص يملكه يتحكم في هذه الهوية — احفظه في مكان آمن، لأن VoxVera لا يمكنها استعادته.", copy: "نسخ", copied: "تم النسخ", continue_label: "متابعة" },
|
||||
fa: { title: "تابلوی اعلانات", subtitle: "جدیدترین پوسترهای منتشرشده روی رلهها.", col_title: "عنوان", col_link: "پیوند", col_author: "منتشرشده توسط", col_language: "زبان", col_date: "تاریخ انتشار", col_event_id: "شناسه رویداد", act_edit: "ویرایش", act_delete: "حذف", act_rebroadcast: "بازپخش", act_block: "مسدود کردن", act_report: "گزارش", safety_link: "ایمنی و حریم خصوصی", connect_nip46: "امضاکننده از راه دور (NIP-46)", nip46_hint: "پیوند bunker:// را از برنامه امضای خود وارد کنید (مثل nsec.app یا Amber).", nip46_bad_uri: "این یک پیوند bunker:// به نظر نمیرسد.", report_title: "این پوستر گزارش شود؟", report_message: "این یک گزارش عمومی روی رلهها منتشر میکند و پوستر را برای شما پنهان میکند. گزارشها قابل بازگردانی نیستند.", report_reason: "دلیل (اختیاری)", report_submit: "ارسال گزارش", report_done: "گزارش ارسال شد.", report_unavailable: "برای گزارش از اینجا، با افزونه مرورگر یا کلید ساختهشده متصل شوید.", report_cat_spam: "هرزنامه", report_cat_illegal: "محتوای غیرقانونی", report_cat_impersonation: "جعل هویت", report_cat_other: "سایر", delete_title: "این پوستر حذف شود؟", delete_message: "این از رلهها درخواست حذف میکند و آن را با یک نشان حذف جایگزین میکند تا VoxVera آن را پنهان کند. حذف در همه رلهها تضمین نمیشود.", delete_confirm: "حذف", cancel: "لغو", deleted_done: "درخواست حذف ارسال شد.", rebroadcast_done: "به رلهها بازپخش شد.", sign_unavailable: "برای حذف از اینجا، با افزونه مرورگر یا کلید ساختهشده متصل شوید — یا از ویرایشگر حذف کنید.", unblock_all: "پاک کردن مسدودها", mine_only: "پوسترهای من", details_label: "جزئیات", actions_label: "اقدامات",link_text: "پیوند", loading: "در حال بارگذاری پوسترها…", empty: "هنوز پوستری یافت نشد.", error: "اتصال به رلهها ممکن نشد.", back: "← بازگشت به VoxVera", gate_title: "برای مشاهده تابلو متصل شوید", gate_body: "تابلوی اعلانات برای کسانی که هویت Nostr را متصل میکنند قابل مشاهده است. در زیر نحوه اتصال را انتخاب کنید.", connect: "اتصال", connecting: "در حال اتصال…", no_extension: "هیچ افزونه Nostr یافت نشد. برای مشاهده تابلو یک افزونه مرورگر NIP-07 نصب کنید.", connect_failed: "اتصال لغو شد یا ناموفق بود. لطفاً دوباره تلاش کنید.", disconnect: "قطع اتصال", filter_following: "نمایش پوسترها از افرادی که دنبال میکنید.", filter_seeded: "هنوز کسی را دنبال نمیکنید — مجموعهای منتخب از شبکه اعتماد VoxVera نمایش داده میشود.", filter_all: "نمایش همه پوسترها روی رلهها (بدون فیلتر).", filter_unavailable: "بارگذاری شبکه اعتماد ممکن نشد — همه پوسترها نمایش داده میشوند.", filter_empty: "هنوز پوستری از شبکه اعتماد شما نیست. «نمایش همه» را روشن کنید تا همه را ببینید.", show_all_label: "نمایش همه پوسترها", connect_extension: "افزونه مرورگر (NIP-07)", connect_nsec: "استفاده از کلید خصوصی (nsec)", connect_generate: "ایجاد کلید جدید", key_note: "کلید شما در همین مرورگر میماند و فقط برای خواندن فهرست دنبالشدههای شما استفاده میشود — هرگز به جایی ارسال نمیشود.", invalid_nsec: "این یک کلید nsec معتبر به نظر نمیرسد.", gen_warning: "این کلید مخفی جدید شماست. هر کسی آن را داشته باشد این هویت را کنترل میکند — آن را جایی امن ذخیره کنید، زیرا VoxVera نمیتواند آن را بازیابی کند.", copy: "کپی", copied: "کپی شد", continue_label: "ادامه" },
|
||||
hi: { title: "बुलेटिन बोर्ड", subtitle: "रिले पर प्रकाशित सबसे हाल के पोस्टर।", col_title: "शीर्षक", col_link: "लिंक", col_author: "द्वारा पोस्ट किया गया", col_language: "भाषा", col_date: "पोस्ट किया गया", col_event_id: "इवेंट ID", act_edit: "संपादित करें", act_delete: "हटाएँ", act_rebroadcast: "पुनः प्रसारित करें", act_block: "ब्लॉक करें", act_report: "रिपोर्ट करें", safety_link: "सुरक्षा और निजता", connect_nip46: "रिमोट साइनर (NIP-46)", nip46_hint: "अपने साइनर ऐप से bunker:// लिंक पेस्ट करें (जैसे nsec.app, Amber)।", nip46_bad_uri: "यह bunker:// लिंक जैसा नहीं लगता।", report_title: "इस फ़्लायर की रिपोर्ट करें?", report_message: "यह रिले पर एक सार्वजनिक रिपोर्ट प्रकाशित करता है और फ़्लायर को आपके लिए छिपा देता है। रिपोर्ट पूर्ववत नहीं की जा सकती।", report_reason: "कारण (वैकल्पिक)", report_submit: "रिपोर्ट भेजें", report_done: "रिपोर्ट भेजी गई।", report_unavailable: "यहाँ से रिपोर्ट करने के लिए, ब्राउज़र एक्सटेंशन या जनरेट की गई कुंजी से कनेक्ट करें।", report_cat_spam: "स्पैम", report_cat_illegal: "अवैध सामग्री", report_cat_impersonation: "पहचान की नकल", report_cat_other: "अन्य", delete_title: "इस पोस्टर को हटाएँ?", delete_message: "यह रिले से हटाने का अनुरोध करता है और इसे एक विलोपन चिह्न से बदल देता है ताकि VoxVera इसे छिपा दे। हर रिले पर विलोपन की गारंटी नहीं दी जा सकती।", delete_confirm: "हटाएँ", cancel: "रद्द करें", deleted_done: "विलोपन का अनुरोध किया गया।", rebroadcast_done: "रिले पर पुनः प्रसारित किया गया।", sign_unavailable: "यहाँ से हटाने के लिए, ब्राउज़र एक्सटेंशन या जेनरेट की गई कुंजी से कनेक्ट करें — या संपादक से हटाएँ।", unblock_all: "ब्लॉक किए गए साफ़ करें", mine_only: "मेरे पोस्टर", details_label: "विवरण", actions_label: "क्रियाएँ",link_text: "लिंक", loading: "पोस्टर लोड हो रहे हैं…", empty: "अभी तक कोई पोस्टर नहीं मिला।", error: "रिले तक नहीं पहुँच सके।", back: "← VoxVera पर वापस", gate_title: "बोर्ड देखने के लिए कनेक्ट करें", gate_body: "बुलेटिन बोर्ड उन लोगों को दिखता है जो Nostr पहचान कनेक्ट करते हैं। नीचे चुनें कि कैसे कनेक्ट करना है।", connect: "कनेक्ट करें", connecting: "कनेक्ट हो रहा है…", no_extension: "कोई Nostr एक्सटेंशन नहीं मिला। बोर्ड देखने के लिए NIP-07 ब्राउज़र एक्सटेंशन इंस्टॉल करें।", connect_failed: "कनेक्शन रद्द या विफल। कृपया पुनः प्रयास करें।", disconnect: "डिस्कनेक्ट करें", filter_following: "उन लोगों के पोस्टर दिखा रहे हैं जिन्हें आप फ़ॉलो करते हैं।", filter_seeded: "आप अभी किसी को फ़ॉलो नहीं करते — VoxVera के विश्वास नेटवर्क से चुनिंदा पोस्टर दिखा रहे हैं।", filter_all: "रिले पर सभी पोस्टर दिखा रहे हैं (बिना फ़िल्टर)।", filter_unavailabLine truncated
|
||||
ja: { title: "掲示板", subtitle: "リレーに公開された最新のちらし。", col_title: "タイトル", col_link: "リンク", col_author: "投稿者", col_language: "言語", col_date: "投稿日", col_event_id: "イベントID", act_edit: "編集", act_delete: "削除", act_rebroadcast: "再配信", act_block: "ブロック", act_report: "報告", safety_link: "安全とプライバシー", connect_nip46: "リモート署名(NIP-46)", nip46_hint: "署名アプリの bunker:// リンクを貼り付けてください(例:nsec.app、Amber)。", nip46_bad_uri: "これは bunker:// リンクではないようです。", report_title: "このちらしを報告しますか?", report_message: "リレーに公開報告を投稿し、このちらしをあなたの表示から隠します。報告は取り消せません。", report_reason: "理由(任意)", report_submit: "報告を送信", report_done: "報告を送信しました。", report_unavailable: "ここから報告するには、ブラウザ拡張機能または生成した鍵で接続してください。", report_cat_spam: "スパム", report_cat_illegal: "違法なコンテンツ", report_cat_impersonation: "なりすまし", report_cat_other: "その他", delete_title: "このちらしを削除しますか?", delete_message: "リレーに削除を要求し、削除マーカーに置き換えて VoxVera で非表示にします。すべてのリレーでの削除は保証できません。", delete_confirm: "削除", cancel: "キャンセル", deleted_done: "削除を要求しました。", rebroadcast_done: "リレーに再配信しました。", sign_unavailable: "ここから削除するには、ブラウザ拡張機能または生成した鍵で接続してください。またはエディターから削除してください。", unblock_all: "ブロックを消去", mine_only: "自分のちらし", details_label: "詳細", actions_label: "操作",link_text: "リンク", loading: "ちらしを読み込んでいます…", empty: "まだちらしが見つかりません。", error: "リレーに接続できませんでした。", back: "← VoxVera に戻る", gate_title: "掲示板を見るには接続してください", gate_body: "掲示板はNostr IDを接続した人に表示されます。下で接続方法を選んでください。", connect: "接続", connecting: "接続中…", no_extension: "Nostr拡張機能が見つかりません。掲示板を見るにはNIP-07ブラウザ拡張機能をインストールしてください。", connect_failed: "接続がキャンセルされたか失敗しました。もう一度お試しください。", disconnect: "切断", filter_following: "フォローしている人のちらしを表示しています。", filter_seeded: "まだ誰もフォローしていません。VoxVera の信頼ネットワークから厳選したものを表示しています。", filter_all: "リレー上のすべてのちらしを表示しています(フィルターなし)。", filter_unavailable: "信頼ネットワークを読み込めませんでした。すべてのちらしを表示しています。", filter_empty: "あなたの信頼ネットワークのちらしはまだありません。「すべて表示」をオンにするとすべて表示されます。", show_all_label: "すべてのちらしを表示", connect_extension: "ブラウザ拡張機能 (NIP-07)", connect_nsec: "秘密鍵 (nsec) を使う", connect_generate: "新しい鍵を作成", key_note: "鍵はこのブラウザ内に留まり、フォローリストの読み取りにのみ使用されます。どこにも送信されません。", invalid_nsec: "有効な nsec 鍵ではないようです。", gen_warning: "これはあなたの新しい秘密鍵です。これを持つ人は誰でもこの ID を操作できます。VoxVera は復元できないので、安全な場所に保存してください。", copy: "コピー", copied: "コピーしました", continue_label: "続行" },
|
||||
pt: { title: "Quadro de avisos", subtitle: "Panfletos mais recentes publicados nos relés.", col_title: "Título", col_link: "Link", col_author: "Publicado por", col_language: "Idioma", col_date: "Publicado", col_event_id: "ID do evento", act_edit: "Editar", act_delete: "Excluir", act_rebroadcast: "Retransmitir", act_block: "Bloquear", act_report: "Denunciar", safety_link: "Segurança e privacidade", connect_nip46: "Assinador remoto (NIP-46)", nip46_hint: "Cole um link bunker:// do seu app de assinatura (ex.: nsec.app, Amber).", nip46_bad_uri: "Isso não parece um link bunker://.", report_title: "Denunciar este panfleto?", report_message: "Isto publica uma denúncia pública nos relés e oculta o panfleto para você. As denúncias não podem ser desfeitas.", report_reason: "Motivo (opcional)", report_submit: "Enviar denúncia", report_done: "Denúncia enviada.", report_unavailable: "Para denunciar daqui, conecte-se com uma extensão de navegador ou uma chave gerada.", report_cat_spam: "Spam", report_cat_illegal: "Conteúdo ilegal", report_cat_impersonation: "Falsidade ideológica", report_cat_other: "Outro", delete_title: "Excluir este panfleto?", delete_message: "Isso solicita a exclusão aos relés e o substitui por um marcador de exclusão para que o VoxVera o oculte. A exclusão não pode ser garantida em todos os relés.", delete_confirm: "Excluir", cancel: "Cancelar", deleted_done: "Exclusão solicitada.", rebroadcast_done: "Retransmitido aos relés.", sign_unavailable: "Para excluir aqui, conecte-se com uma extensão de navegador ou uma chave gerada — ou exclua no editor.", unblock_all: "Limpar bloqueados", mine_only: "Meus panfletos", details_label: "Detalhes", actions_label: "Ações", link_text: "link", loading: "Carregando panfletos…", empty: "Nenhum panfleto encontrado ainda.", error: "Não foi possível acessar os relés.", back: "← Voltar ao VoxVera", gate_title: "Conecte-se para ver o quadro", gate_body: "O quadro de avisos é visível para quem conecta uma identidade Nostr. Escolha abaixo como se conectar.", connect: "Conectar", connecting: "Conectando…", no_extension: "Nenhuma extensão Nostr encontrada. Instale uma extensão de navegador NIP-07 para ver o quadro.", connect_failed: "Conexão cancelada ou falhou. Tente novamente.", disconnect: "Desconectar", filter_following: "Mostrando panfletos de pessoas que você segue.", filter_seeded: "Você ainda não segue ninguém — mostrando uma seleção da rede de confiança da VoxVera.", filter_all: "Mostrando todos os panfletos nos relés (sem filtro).", filter_unavailable: "Não foi possível carregar uma rede de confiança — mostrando todos os panfletos.", filter_empty: "Ainda não há panfletos da sua rede de confiança. Ative “Mostrar todos” para ver tudo.", show_all_label: "Mostrar todos os panfletos", connect_extension: "Extensão do navegador (NIP-07)", connect_nsec: "Usar uma chave privada (nsec)", connect_generate: "Criar uma nova chave", key_note: "Sua chave permanece neste navegador e é usada apenas para ler sua lista de seguidos — nunca é enviada para lugar algum.", invalid_nsec: "Isso não parece uma chave nsec válida.", gen_warning: "Esta é sua nova chave secreta. Qualquer pessoa que a tenha controla esta identidade — guarde-a em local seguro, pois o VoxVera não pode recuperá-la.", copy: "Copiar", copied: "Copiado", continue_label: "Continuar" },
|
||||
sw: { title: "Ubao wa matangazo", subtitle: "Mabango ya hivi karibuni yaliyochapishwa kwenye relay.", col_title: "Kichwa", col_link: "Kiungo", col_author: "Imechapishwa na", col_language: "Lugha", col_date: "Imechapishwa", col_event_id: "Kitambulisho cha tukio", act_edit: "Hariri", act_delete: "Futa", act_rebroadcast: "Tangaza tena", act_block: "Zuia", act_report: "Ripoti", safety_link: "Usalama na faragha", connect_nip46: "Mtia-saini wa mbali (NIP-46)", nip46_hint: "Bandika kiungo cha bunker:// kutoka programu yako ya kutia saini (k.m. nsec.app, Amber).", nip46_bad_uri: "Hiki hakionekani kama kiungo cha bunker://.", report_title: "Ripoti kipeperushi hiki?", report_message: "Hii huchapisha ripoti ya umma kwenye relay na kuficha kipeperushi kwako. Ripoti haziwezi kutenduliwa.", report_reason: "Sababu (hiari)", report_submit: "Tuma ripoti", report_done: "Ripoti imetumwa.", report_unavailable: "Ili kuripoti kutoka hapa, unganisha kwa kiendelezi cha kivinjari au ufunguo ulioundwa.", report_cat_spam: "Taka", report_cat_illegal: "Maudhui haramu", report_cat_impersonation: "Kujifanya mtu mwingine", report_cat_other: "Nyingine", delete_title: "Futa bango hili?", delete_message: "Hii inaomba kufuta kutoka relay na kulibadilisha na alama ya kufuta ili VoxVera ilifiche. Kufuta hakuwezi kuhakikishwa kwenye kila relay.", delete_confirm: "Futa", cancel: "Ghairi", deleted_done: "Kufuta kumeombwa.", rebroadcast_done: "Limetangazwa tena kwenye relay.", sign_unavailable: "Ili kufuta hapa, unganisha kwa kiendelezi cha kivinjari au ufunguo uliotengenezwa — au futa kutoka kwa kihariri.", unblock_all: "Futa walizuiliwa", mine_only: "Mabango yangu", details_label: "Maelezo", actions_label: "Vitendo",link_text: "kiungo", loading: "Inapakia mabango…", empty: "Hakuna mabango yaliyopatikana bado.", error: "Imeshindwa kufikia relay.", back: "← Rudi VoxVera", gate_title: "Unganisha ili kuona ubao", gate_body: "Ubao wa matangazo unaonekana kwa wale wanaounganisha kitambulisho cha Nostr. Chagua hapa chini jinsi ya kuunganisha.", connect: "Unganisha", connecting: "Inaunganisha…", no_extension: "Hakuna kiendelezi cha Nostr kilichopatikana. Sakinisha kiendelezi cha kivinjari cha NIP-07 ili kuona ubao.", connect_failed: "Muunganisho umeghairiwa au umeshindwa. Tafadhali jaribu tena.", disconnect: "Tenganisha", filter_following: "Inaonyesha mabango kutoka kwa watu unaowafuata.", filter_seeded: "Bado hufuati mtu yeyote — tunaonyesha mkusanyiko uliochaguliwa kutoka mtandao wa kuaminiana wa VoxVera.", filter_all: "Inaonyesha mabango yote kwenye relay (bila kuchuja).", filter_unavailable: "Imeshindwa kupakia mtandao wa kuaminiana — inaonyesha mabango yote.", filter_empty: "Bado hakuna mabango kutoka mtandao wako wa kuaminiana. Washa “Onyesha yote” kuona yote.", show_all_label: "Onyesha mabango yote", connect_extension: "Kiendelezi cha kivinjari (NIP-07)", connect_nsec: "Tumia ufunguo wa faragha (nsec)", connect_generate: "Tengeneza ufunguo mpya", key_note: "Ufunguo wako unabaki katika kivinjari hiki na hutumika tu kusoma orodha yako ya unaowafuata — haupelekwi popote.", invalid_nsec: "Huu hauonekani kama ufunguo halali wa nsec.", gen_warning: "Huu ndio ufunguo wako mpya wa siri. Yeyote aliye nao anadhibiti utambulisho huu — uhifadhi mahali salama, kwa sababu VoxVera haiwezi kuurejesha.", copy: "Nakili", copied: "Imenakiliwa", continue_label: "Endelea" },
|
||||
tr: { title: "İlan panosu", subtitle: "Rölelere yayımlanan en yeni el ilanları.", col_title: "Başlık", col_link: "Bağlantı", col_author: "Yayımlayan", col_language: "Dil", col_date: "Yayımlandı", col_event_id: "Olay ID", act_edit: "Düzenle", act_delete: "Sil", act_rebroadcast: "Yeniden yayınla", act_block: "Engelle", act_report: "Bildir", safety_link: "Güvenlik ve gizlilik", connect_nip46: "Uzak imzalayıcı (NIP-46)", nip46_hint: "İmzalama uygulamanızdan bir bunker:// bağlantısı yapıştırın (ör. nsec.app, Amber).", nip46_bad_uri: "Bu bir bunker:// bağlantısına benzemiyor.", report_title: "Bu el ilanı bildirilsin mi?", report_message: "Bu, rölelere herkese açık bir bildirim yayımlar ve el ilanını sizin için gizler. Bildirimler geri alınamaz.", report_reason: "Neden (isteğe bağlı)", report_submit: "Bildirim gönder", report_done: "Bildirim gönderildi.", report_unavailable: "Buradan bildirmek için bir tarayıcı uzantısı veya oluşturulmuş bir anahtarla bağlanın.", report_cat_spam: "İstenmeyen", report_cat_illegal: "Yasa dışı içerik", report_cat_impersonation: "Kimlik taklidi", report_cat_other: "Diğer", delete_title: "Bu el ilanı silinsin mi?", delete_message: "Bu, rölelerden silmeyi ister ve VoxVera'nın gizlemesi için bir silme işaretiyle değiştirir. Silme her rölede garanti edilemez.", delete_confirm: "Sil", cancel: "İptal", deleted_done: "Silme istendi.", rebroadcast_done: "Rölelere yeniden yayınlandı.", sign_unavailable: "Buradan silmek için bir tarayıcı uzantısı veya oluşturulmuş bir anahtarla bağlan — ya da düzenleyiciden sil.", unblock_all: "Engellenenleri temizle", mine_only: "El ilanlarım", details_label: "Ayrıntılar", actions_label: "İşlemler",link_text: "bağlantı", loading: "El ilanları yükleniyor…", empty: "Henüz el ilanı bulunamadı.", error: "Rölelere ulaşılamadı.", back: "← VoxVera'ya dön", gate_title: "Panoyu görmek için bağlanın", gate_body: "İlan panosu, bir Nostr kimliği bağlayanlara görünür. Aşağıdan nasıl bağlanacağını seç.", connect: "Bağlan", connecting: "Bağlanıyor…", no_extension: "Nostr uzantısı bulunamadı. Panoyu görmek için bir NIP-07 tarayıcı uzantısı kurun.", connect_failed: "Bağlantı iptal edildi veya başarısız oldu. Lütfen tekrar deneyin.", disconnect: "Bağlantıyı kes", filter_following: "Takip ettiğin kişilerin el ilanları gösteriliyor.", filter_seeded: "Henüz kimseyi takip etmiyorsun — VoxVera'nın güven ağından seçilmiş bir küme gösteriliyor.", filter_all: "Rölelerdeki tüm el ilanları gösteriliyor (filtresiz).", filter_unavailable: "Güven ağı yüklenemedi — tüm el ilanları gösteriliyor.", filter_empty: "Güven ağından henüz el ilanı yok. Her şeyi görmek için “Tümünü göster”i aç.", show_all_label: "Tüm el ilanlarını göster", connect_extension: "Tarayıcı uzantısı (NIP-07)", connect_nsec: "Özel anahtar (nsec) kullan", connect_generate: "Yeni anahtar oluştur", key_note: "Anahtarın bu tarayıcıda kalır ve yalnızca takip listeni okumak için kullanılır — hiçbir yere gönderilmez.", invalid_nsec: "Bu geçerli bir nsec anahtarı gibi görünmüyor.", gen_warning: "Bu senin yeni gizli anahtarın. Ona sahip olan herkes bu kimliği kontrol eder — güvenli bir yerde sakla, çünkü VoxVera onu kurtaramaz.", copy: "Kopyala", copied: "Kopyalandı", continue_label: "Devam et" },
|
||||
zh: { title: "公告栏", subtitle: "发布到中继的最新传单。", col_title: "标题", col_link: "链接", col_author: "发布者", col_language: "语言", col_date: "发布时间", col_event_id: "事件 ID", act_edit: "编辑", act_delete: "删除", act_rebroadcast: "重新广播", act_block: "屏蔽", act_report: "举报", safety_link: "安全与隐私", connect_nip46: "远程签名器(NIP-46)", nip46_hint: "粘贴来自签名应用的 bunker:// 链接(如 nsec.app、Amber)。", nip46_bad_uri: "这看起来不是 bunker:// 链接。", report_title: "举报此传单?", report_message: "这将向中继发布一条公开举报,并为你隐藏该传单。举报无法撤销。", report_reason: "原因(可选)", report_submit: "提交举报", report_done: "举报已提交。", report_unavailable: "要从此处举报,请使用浏览器扩展或生成的密钥进行连接。", report_cat_spam: "垃圾信息", report_cat_illegal: "非法内容", report_cat_impersonation: "冒充他人", report_cat_other: "其他", delete_title: "删除此传单?", delete_message: "这将请求中继删除它,并用删除标记替换它,使 VoxVera 将其隐藏。无法保证在每个中继上都删除。", delete_confirm: "删除", cancel: "取消", deleted_done: "已请求删除。", rebroadcast_done: "已重新广播到中继。", sign_unavailable: "若要在此删除,请使用浏览器扩展或生成的密钥连接——或在编辑器中删除。", unblock_all: "清除屏蔽", mine_only: "我的传单", details_label: "详情", actions_label: "操作",link_text: "链接", loading: "正在加载传单…", empty: "尚未找到传单。", error: "无法连接到中继。", back: "← 返回 VoxVera", gate_title: "连接以查看公告栏", gate_body: "公告栏对连接了 Nostr 身份的人可见。请在下方选择连接方式。", connect: "连接", connecting: "正在连接…", no_extension: "未找到 Nostr 扩展。请安装 NIP-07 浏览器扩展以查看公告栏。", connect_failed: "连接已取消或失败。请重试。", disconnect: "断开连接", filter_following: "正在显示你关注的人的传单。", filter_seeded: "你还没有关注任何人——正在显示来自 VoxVera 信任网络的精选内容。", filter_all: "正在显示中继上的所有传单(未过滤)。", filter_unavailable: "无法加载信任网络——正在显示所有传单。", filter_empty: "你的信任网络暂时没有传单。开启“显示全部”以查看所有内容。", show_all_label: "显示所有传单", connect_extension: "浏览器扩展 (NIP-07)", connect_nsec: "使用私钥 (nsec)", connect_generate: "创建新密钥", key_note: "你的密钥保留在此浏览器中,仅用于读取你的关注列表——绝不会发送到任何地方。", invalid_nsec: "这看起来不是有效的 nsec 密钥。", gen_warning: "这是你的新私钥。拥有它的任何人都能控制此身份——请妥善保存,因为 VoxVera 无法恢复它。", copy: "复制", copied: "已复制", continue_label: "继续" }
|
||||
en: { title: "Bulletin Board", subtitle: "Most recent flyers published to the relays.", col_title: "Title", col_link: "Link", col_author: "Posted by", col_language: "Language", col_date: "Posted", col_event_id: "Event ID", act_edit: "Edit", act_delete: "Delete", act_rebroadcast: "Re-broadcast", act_block: "Block", act_report: "Report", safety_link: "Safety & privacy", connect_nip46: "Remote signer (NIP-46)", nip46_hint: "Paste a bunker:// link from your signer app (e.g. nsec.app, Amber).", nip46_bad_uri: "That doesn't look like a bunker:// link.", nip46_scan: "Scan with your signer app", nip46_waiting: "Waiting for your signer app to connect…", nip46_copy: "Copy link", report_title: "Report this flyer?", report_message: "This publishes a public report to the relays and hides the flyer for you. Reports can't be undone.", report_reason: "Reason (optional)", report_submit: "Submit report", report_done: "Report submitted.", report_unavailable: "To report from here, connect with a browser extension or a generated key.", report_cat_spam: "Spam", report_cat_illegal: "Illegal content", report_cat_impersonation: "Impersonation", report_cat_other: "Other", delete_title: "Delete this flyer?", delete_message: "This requests deletion from the relays and replaces it with a deletion marker so VoxVera hides it. Deletion can't be guaranteed across every relay.", delete_confirm: "Delete", cancel: "Cancel", deleted_done: "Deletion requested.", rebroadcast_done: "Re-broadcast to the relays.", sign_unavailable: "To delete from here, connect with a browser extension or a generated key — or delete from the editor.", unblock_all: "Clear blocked", mine_only: "My flyers", details_label: "Details", actions_label: "Actions",link_text: "link", loading: "Loading flyers…", empty: "No flyers found yet.", error: "Could not reach the relays.", back: "← Back to VoxVera", gate_title: "Connect to view the board", gate_body: "The bulletin board is visible to people who connect a Nostr identity. Choose how to connect below.", connect: "Connect", connecting: "Connecting…", no_extension: "No Nostr extension found. Install a NIP-07 browser extension to view the board.", connect_failed: "Connection cancelled or failed. Please try again.", disconnect: "Disconnect", filter_following: "Showing flyers from people you follow.", filter_seeded: "You don't follow anyone yet — showing a curated set from VoxVera's trust network.", filter_all: "Showing all flyers on the relays (unfiltered).", filter_unavailable: "Couldn't load a trust network — showing all flyers.", filter_empty: "No flyers from your trust network yet. Turn on “Show all” to see everything.", show_all_label: "Show all flyers", connect_extension: "Browser extension (NIP-07)", connect_nsec: "Use a private key (nsec)", connect_generate: "Create a new key", key_note: "Your key stays in this browser and is only used to read your follow list — it is never sent anywhere.", invalid_nsec: "That doesn't look like a valid nsec key.", gen_warning: "This is your new secret key. Anyone who has it controls this identity — save it somewhere safe, because VoxVera can't recover it.", copy: "Copy", copied: "Copied", continue_label: "Continue" },
|
||||
es: { title: "Tablón de anuncios", subtitle: "Carteles más recientes publicados en los relés.", col_title: "Título", col_link: "Enlace", col_author: "Publicado por", col_language: "Idioma", col_date: "Publicado", col_event_id: "ID del evento", act_edit: "Editar", act_delete: "Eliminar", act_rebroadcast: "Reemitir", act_block: "Bloquear", act_report: "Denunciar", safety_link: "Seguridad y privacidad", connect_nip46: "Firmante remoto (NIP-46)", nip46_hint: "Pega un enlace bunker:// de tu app de firma (p. ej. nsec.app, Amber).", nip46_bad_uri: "Esto no parece un enlace bunker://.", nip46_scan: "Escanear con tu app de firma", nip46_waiting: "Esperando a que tu app de firma se conecte…", nip46_copy: "Copiar enlace", report_title: "¿Denunciar este cartel?", report_message: "Esto publica una denuncia pública en los relés y oculta el cartel para ti. Las denuncias no se pueden deshacer.", report_reason: "Motivo (opcional)", report_submit: "Enviar denuncia", report_done: "Denuncia enviada.", report_unavailable: "Para denunciar desde aquí, conéctate con una extensión de navegador o una clave generada.", report_cat_spam: "Spam", report_cat_illegal: "Contenido ilegal", report_cat_impersonation: "Suplantación", report_cat_other: "Otro", delete_title: "¿Eliminar este cartel?", delete_message: "Esto solicita la eliminación a los relés y lo reemplaza por una marca de eliminación para que VoxVera lo oculte. No se puede garantizar la eliminación en todos los relés.", delete_confirm: "Eliminar", cancel: "Cancelar", deleted_done: "Eliminación solicitada.", rebroadcast_done: "Reemitido a los relés.", sign_unavailable: "Para eliminar desde aquí, conéctate con una extensión del navegador o una clave generada, o elimínalo desde el editor.", unblock_all: "Borrar bloqueados", mine_only: "Mis carteles", details_label: "Detalles", actions_label: "Acciones", link_text: "enlace", loading: "Cargando carteles…", empty: "Aún no se encontraron carteles.", error: "No se pudo conectar con los relés.", back: "← Volver a VoxVera", gate_title: "Conéctate para ver el tablón", gate_body: "El tablón de anuncios es visible para quienes conectan una identidad Nostr. Elige abajo cómo conectarte.", connect: "Conectar", connecting: "Conectando…", no_extension: "No se encontró ninguna extensión Nostr. Instala una extensión de navegador NIP-07 para ver el tablón.", connect_failed: "Conexión cancelada o fallida. Inténtalo de nuevo.", disconnect: "Desconectar", filter_following: "Mostrando carteles de las personas que sigues.", filter_seeded: "Aún no sigues a nadie: mostramos un conjunto curado de la red de confianza de VoxVera.", filter_all: "Mostrando todos los carteles de los relés (sin filtrar).", filter_unavailable: "No se pudo cargar una red de confianza: mostrando todos los carteles.", filter_empty: "Aún no hay carteles de tu red de confianza. Activa «Mostrar todos» para verlo todo.", show_all_label: "Mostrar todos los carteles", connect_extension: "Extensión del navegador (NIP-07)", connect_nsec: "Usar una clave privada (nsec)", connect_generate: "Crear una clave nueva", key_note: "Tu clave permanece en este navegador y solo se usa para leer tu lista de seguidos; nunca se envía a ningún sitio.", invalid_nsec: "Eso no parece una clave nsec válida.", gen_warning: "Esta es tu nueva clave secreta. Cualquiera que la tenga controla esta identidad: guárdala en un lugar seguro, porque VoxVera no puede recuperarla.", copy: "Copiar", copied: "Copiado", continue_label: "Continuar" },
|
||||
de: { title: "Schwarzes Brett", subtitle: "Neueste auf den Relays veröffentlichte Plakate.", col_title: "Titel", col_link: "Link", col_author: "Veröffentlicht von", col_language: "Sprache", col_date: "Veröffentlicht", col_event_id: "Event-ID", act_edit: "Bearbeiten", act_delete: "Löschen", act_rebroadcast: "Erneut senden", act_block: "Blockieren", act_report: "Melden", safety_link: "Sicherheit & Privatsphäre", connect_nip46: "Externer Signierer (NIP-46)", nip46_hint: "Füge einen bunker://-Link aus deiner Signier-App ein (z. B. nsec.app, Amber).", nip46_bad_uri: "Das sieht nicht nach einem bunker://-Link aus.", nip46_scan: "Mit deiner Signier-App scannen", nip46_waiting: "Warte auf die Verbindung deiner Signier-App…", nip46_copy: "Link kopieren", report_title: "Diesen Flyer melden?", report_message: "Dies veröffentlicht eine öffentliche Meldung auf den Relays und blendet den Flyer für dich aus. Meldungen können nicht rückgängig gemacht werden.", report_reason: "Grund (optional)", report_submit: "Meldung senden", report_done: "Meldung gesendet.", report_unavailable: "Zum Melden von hier verbinde dich mit einer Browser-Erweiterung oder einem generierten Schlüssel.", report_cat_spam: "Spam", report_cat_illegal: "Illegale Inhalte", report_cat_impersonation: "Identitätsdiebstahl", report_cat_other: "Sonstiges", delete_title: "Dieses Plakat löschen?", delete_message: "Dies fordert die Löschung von den Relays an und ersetzt es durch eine Löschmarkierung, sodass VoxVera es ausblendet. Eine Löschung über alle Relays hinweg kann nicht garantiert werden.", delete_confirm: "Löschen", cancel: "Abbrechen", deleted_done: "Löschung angefordert.", rebroadcast_done: "Erneut an die Relays gesendet.", sign_unavailable: "Zum Löschen hier mit einer Browser-Erweiterung oder einem generierten Schlüssel verbinden – oder im Editor löschen.", unblock_all: "Blockierte löschen", mine_only: "Meine Plakate", details_label: "Details", actions_label: "Aktionen",link_text: "Link", loading: "Plakate werden geladen…", empty: "Noch keine Plakate gefunden.", error: "Relays konnten nicht erreicht werden.", back: "← Zurück zu VoxVera", gate_title: "Verbinden, um das Brett zu sehen", gate_body: "Das Schwarze Brett ist für Personen sichtbar, die eine Nostr-Identität verbinden. Wähle unten, wie du dich verbinden möchtest.", connect: "Verbinden", connecting: "Verbindung…", no_extension: "Keine Nostr-Erweiterung gefunden. Installiere eine NIP-07-Browsererweiterung, um das Brett zu sehen.", connect_failed: "Verbindung abgebrochen oder fehlgeschlagen. Bitte erneut versuchen.", disconnect: "Trennen", filter_following: "Plakate von Personen, denen du folgst.", filter_seeded: "Du folgst noch niemandem – wir zeigen eine kuratierte Auswahl aus dem Vertrauensnetz von VoxVera.", filter_all: "Alle Plakate auf den Relays (ungefiltert).", filter_unavailable: "Vertrauensnetz konnte nicht geladen werden – alle Plakate werden angezeigt.", filter_empty: "Noch keine Plakate aus deinem Vertrauensnetz. Aktiviere „Alle anzeigen“, um alles zu sehen.", show_all_label: "Alle Plakate anzeigen", connect_extension: "Browser-Erweiterung (NIP-07)", connect_nsec: "Privaten Schlüssel (nsec) verwenden", connect_generate: "Neuen Schlüssel erstellen", key_note: "Dein Schlüssel bleibt in diesem Browser und wird nur zum Lesen deiner Folgeliste verwendet – er wird nirgendwohin gesendet.", invalid_nsec: "Das sieht nicht nach einem gültigen nsec-Schlüssel aus.", gen_warning: "Das ist dein neuer geheimer Schlüssel. Wer ihn hat, kontrolliert diese Identität – bewahre ihn sicher auf, denn VoxVera kann ihn nicht wiederherstellen.", copy: "Kopieren", copied: "Kopiert", continue_label: "Weiter" },
|
||||
fr: { title: "Tableau d'affichage", subtitle: "Affiches les plus récentes publiées sur les relais.", col_title: "Titre", col_link: "Lien", col_author: "Publié par", col_language: "Langue", col_date: "Publié", col_event_id: "ID de l'événement", act_edit: "Modifier", act_delete: "Supprimer", act_rebroadcast: "Rediffuser", act_block: "Bloquer", act_report: "Signaler", safety_link: "Sécurité et confidentialité", connect_nip46: "Signataire distant (NIP-46)", nip46_hint: "Collez un lien bunker:// depuis votre app de signature (ex. nsec.app, Amber).", nip46_bad_uri: "Cela ne ressemble pas à un lien bunker://.", nip46_scan: "Scanner avec votre app de signature", nip46_waiting: "En attente de la connexion de votre app de signature…", nip46_copy: "Copier le lien", report_title: "Signaler ce tract ?", report_message: "Ceci publie un signalement public sur les relais et masque le tract pour vous. Les signalements sont irréversibles.", report_reason: "Motif (facultatif)", report_submit: "Envoyer le signalement", report_done: "Signalement envoyé.", report_unavailable: "Pour signaler depuis ici, connectez-vous avec une extension de navigateur ou une clé générée.", report_cat_spam: "Spam", report_cat_illegal: "Contenu illégal", report_cat_impersonation: "Usurpation d'identité", report_cat_other: "Autre", delete_title: "Supprimer cette affiche ?", delete_message: "Cela demande la suppression aux relais et la remplace par un marqueur de suppression pour que VoxVera la masque. La suppression ne peut pas être garantie sur tous les relais.", delete_confirm: "Supprimer", cancel: "Annuler", deleted_done: "Suppression demandée.", rebroadcast_done: "Rediffusée aux relais.", sign_unavailable: "Pour supprimer ici, connectez-vous avec une extension de navigateur ou une clé générée — ou supprimez depuis l'éditeur.", unblock_all: "Effacer les blocages", mine_only: "Mes affiches", details_label: "Détails", actions_label: "Actions",link_text: "lien", loading: "Chargement des affiches…", empty: "Aucune affiche trouvée pour l'instant.", error: "Impossible de joindre les relais.", back: "← Retour à VoxVera", gate_title: "Connectez-vous pour voir le tableau", gate_body: "Le tableau d'affichage est visible par les personnes qui connectent une identité Nostr. Choisissez ci-dessous comment vous connecter.", connect: "Se connecter", connecting: "Connexion…", no_extension: "Aucune extension Nostr trouvée. Installez une extension de navigateur NIP-07 pour voir le tableau.", connect_failed: "Connexion annulée ou échouée. Veuillez réessayer.", disconnect: "Se déconnecter", filter_following: "Affiches des personnes que vous suivez.", filter_seeded: "Vous ne suivez encore personne – voici une sélection issue du réseau de confiance de VoxVera.", filter_all: "Toutes les affiches sur les relais (non filtrées).", filter_unavailable: "Impossible de charger un réseau de confiance – toutes les affiches sont affichées.", filter_empty: "Aucune affiche de votre réseau de confiance pour l'instant. Activez « Tout afficher » pour tout voir.", show_all_label: "Afficher toutes les affiches", connect_extension: "Extension de navigateur (NIP-07)", connect_nsec: "Utiliser une clé privée (nsec)", connect_generate: "Créer une nouvelle clé", key_note: "Votre clé reste dans ce navigateur et ne sert qu'à lire votre liste d'abonnements ; elle n'est jamais envoyée nulle part.", invalid_nsec: "Cela ne ressemble pas à une clé nsec valide.", gen_warning: "Voici votre nouvelle clé secrète. Quiconque la possède contrôle cette identité — conservez-la en lieu sûr, car VoxVera ne peut pas la récupérer.", copy: "Copier", copied: "Copié", continue_label: "Continuer" },
|
||||
ru: { title: "Доска объявлений", subtitle: "Последние листовки, опубликованные на реле.", col_title: "Заголовок", col_link: "Ссылка", col_author: "Опубликовал", col_language: "Язык", col_date: "Опубликовано", col_event_id: "ID события", act_edit: "Изменить", act_delete: "Удалить", act_rebroadcast: "Переслать", act_block: "Заблокировать", act_report: "Пожаловаться", safety_link: "Безопасность и конфиденциальность", connect_nip46: "Удалённый подписант (NIP-46)", nip46_hint: "Вставьте ссылку bunker:// из вашего приложения для подписи (например, nsec.app, Amber).", nip46_bad_uri: "Это не похоже на ссылку bunker://.", nip46_scan: "Сканировать приложением для подписи", nip46_waiting: "Ожидание подключения приложения для подписи…", nip46_copy: "Скопировать ссылку", report_title: "Пожаловаться на эту листовку?", report_message: "Это публикует публичную жалобу на реле и скрывает листовку для вас. Жалобу нельзя отменить.", report_reason: "Причина (необязательно)", report_submit: "Отправить жалобу", report_done: "Жалоба отправлена.", report_unavailable: "Чтобы пожаловаться отсюда, подключитесь через расширение браузера или сгенерированный ключ.", report_cat_spam: "Спам", report_cat_illegal: "Незаконный контент", report_cat_impersonation: "Выдача себя за другого", report_cat_other: "Другое", delete_title: "Удалить эту листовку?", delete_message: "Это запросит удаление у реле и заменит её маркером удаления, чтобы VoxVera её скрыл. Удаление на всех реле не гарантируется.", delete_confirm: "Удалить", cancel: "Отмена", deleted_done: "Запрошено удаление.", rebroadcast_done: "Переслано на реле.", sign_unavailable: "Чтобы удалить отсюда, подключитесь через расширение браузера или сгенерированный ключ — или удалите в редакторе.", unblock_all: "Очистить заблокированных", mine_only: "Мои листовки", details_label: "Подробности", actions_label: "Действия",link_text: "ссылка", loading: "Загрузка листовок…", empty: "Листовки пока не найдены.", error: "Не удалось подключиться к реле.", back: "← Назад в VoxVera", gate_title: "Подключитесь, чтобы увидеть доску", gate_body: "Доска объявлений видна тем, кто подключил идентификацию Nostr. Выберите способ подключения ниже.", connect: "Подключить", connecting: "Подключение…", no_extension: "Расширение Nostr не найдено. Установите расширение браузера NIP-07, чтобы увидеть доску.", connect_failed: "Подключение отменено или не удалось. Повторите попытку.", disconnect: "Отключить", filter_following: "Показаны листовки от тех, на кого вы подписаны.", filter_seeded: "Вы пока ни на кого не подписаны — показываем подборку из сети доверия VoxVera.", filter_all: "Показаны все листовки на реле (без фильтра).", filter_unavailable: "Не удалось загрузить сеть доверия — показаны все листовки.", filter_empty: "Пока нет листовок из вашей сети доверия. Включите «Показать все», чтобы увидеть всё.", show_all_label: "Показать все листовки", connect_extension: "Расширение браузера (NIP-07)", connect_nsec: "Использовать приватный ключ (nsec)", connect_generate: "Создать новый ключ", key_note: "Ваш ключ остаётся в этом браузере и используется только для чтения вашего списка подписок — он никуда не отправляется.", invalid_nsec: "Это не похоже на действительный ключ nsec.", gen_warning: "Это ваш новый
Line truncated
|
||||
he: { title: "לוח מודעות", subtitle: "הכרזות האחרונות שפורסמו לממסרים.", col_title: "כותרת", col_link: "קישור", col_author: "פורסם על ידי", col_language: "שפה", col_date: "פורסם", col_event_id: "מזהה אירוע", act_edit: "ערוך", act_delete: "מחק", act_rebroadcast: "שדר מחדש", act_block: "חסום", act_report: "דווח", safety_link: "בטיחות ופרטיות", connect_nip46: "חותם מרוחק (NIP-46)", nip46_hint: "הדבק קישור bunker:// מאפליקציית החתימה שלך (למשל nsec.app או Amber).", nip46_bad_uri: "זה לא נראה כמו קישור bunker://.", nip46_scan: "סרוק עם אפליקציית החתימה", nip46_waiting: "ממתין לחיבור אפליקציית החתימה…", nip46_copy: "העתק קישור", report_title: "לדווח על הפלייר הזה?", report_message: "פעולה זו מפרסמת דיווח ציבורי לממסרים ומסתירה את הפלייר עבורך. לא ניתן לבטל דיווחים.", report_reason: "סיבה (אופציונלי)", report_submit: "שלח דיווח", report_done: "הדיווח נשלח.", report_unavailable: "כדי לדווח מכאן, התחבר עם תוסף דפדפן או מפתח שנוצר.", report_cat_spam: "ספאם", report_cat_illegal: "תוכן בלתי חוקי", report_cat_impersonation: "התחזות", report_cat_other: "אחר", delete_title: "למחוק את הכרזה הזו?", delete_message: "פעולה זו מבקשת מחיקה מהממסרים ומחליפה אותה בסימן מחיקה כך ש-VoxVera יסתיר אותה. לא ניתן להבטיח מחיקה בכל הממסרים.", delete_confirm: "מחק", cancel: "ביטול", deleted_done: "התבקשה מחיקה.", rebroadcast_done: "שודר מחדש לממסרים.", sign_unavailable: "כדי למחוק מכאן, התחבר עם תוסף דפדפן או מפתח שנוצר — או מחק מהעורך.", unblock_all: "נקה חסומים", mine_only: "הכרזות שלי", details_label: "פרטים", actions_label: "פעולות",link_text: "קישור", loading: "טוען כרזות…", empty: "לא נמצאו כרזות עדיין.", error: "לא ניתן להתחבר לממסרים.", back: "← חזרה ל-VoxVera", gate_title: "התחבר כדי לראות את הלוח", gate_body: "לוח המודעות גלוי למי שמחבר זהות Nostr. בחר למטה כיצד להתחבר.", connect: "התחבר", connecting: "מתחבר…", no_extension: "לא נמצא תוסף Nostr. התקן תוסף דפדפן NIP-07 כדי לראות את הלוח.", connect_failed: "ההתחברות בוטלה או נכשלה. נסה שוב.", disconnect: "התנתק", filter_following: "מציג כרזות מאנשים שאתה עוקב אחריהם.", filter_seeded: "אינך עוקב עדיין אחר אף אחד — מוצגת בחירה מרשת האמון של VoxVera.", filter_all: "מציג את כל הכרזות בממסרים (ללא סינון).", filter_unavailable: "לא ניתן לטעון רשת אמון — מוצגות כל הכרזות.", filter_empty: "אין עדיין כרזות מרשת האמון שלך. הפעל «הצג הכול» כדי לראות הכול.", show_all_label: "הצג את כל הכרזות", connect_extension: "תוסף דפדפן (NIP-07)", connect_nsec: "השתמש במפתח פרטי (nsec)", connect_generate: "צור מפתח חדש", key_note: "המפתח שלך נשאר בדפדפן זה ומשמש רק לקריאת רשימת העוקבים שלך — הוא לעולם לא נשלח לשום מקום.", invalid_nsec: "זה לא נראה כמו מפתח nsec תקין.", gen_warning: "זהו המפתח הסודי החדש שלך. כל מי שיש לו אותו שולט בזהות הזו — שמור אותו במקום בטוח, מכיוון ש-VoxVera אינה יכולה לשחזר אותו.", copy: "העתק", copied: "הועתק", continue_label: "המשך" },
|
||||
ar: { title: "لوحة الإعلانات", subtitle: "أحدث الملصقات المنشورة على المرحلات.", col_title: "العنوان", col_link: "رابط", col_author: "نُشر بواسطة", col_language: "اللغة", col_date: "تاريخ النشر", col_event_id: "معرّف الحدث", act_edit: "تعديل", act_delete: "حذف", act_rebroadcast: "إعادة بث", act_block: "حظر", act_report: "إبلاغ", safety_link: "الأمان والخصوصية", connect_nip46: "موقّع عن بُعد (NIP-46)", nip46_hint: "الصق رابط bunker:// من تطبيق التوقيع لديك (مثل nsec.app أو Amber).", nip46_bad_uri: "لا يبدو هذا رابط bunker://.", nip46_scan: "امسح باستخدام تطبيق التوقيع", nip46_waiting: "بانتظار اتصال تطبيق التوقيع…", nip46_copy: "نسخ الرابط", report_title: "هل تريد الإبلاغ عن هذا الملصق؟", report_message: "يؤدي هذا إلى نشر بلاغ عام على المرحلات وإخفاء الملصق بالنسبة لك. لا يمكن التراجع عن البلاغات.", report_reason: "السبب (اختياري)", report_submit: "إرسال البلاغ", report_done: "تم إرسال البلاغ.", report_unavailable: "للإبلاغ من هنا، اتصل عبر إضافة متصفح أو مفتاح مُولَّد.", report_cat_spam: "بريد مزعج", report_cat_illegal: "محتوى غير قانوني", report_cat_impersonation: "انتحال هوية", report_cat_other: "أخرى", delete_title: "حذف هذا الملصق؟", delete_message: "يطلب هذا الحذف من المرحلات ويستبدله بعلامة حذف حتى يخفيه VoxVera. لا يمكن ضمان الحذف عبر كل مرحل.", delete_confirm: "حذف", cancel: "إلغاء", deleted_done: "تم طلب الحذف.", rebroadcast_done: "أُعيد بثه إلى المرحلات.", sign_unavailable: "للحذف من هنا، اتصل بإضافة متصفح أو مفتاح مُنشأ — أو احذف من المحرر.", unblock_all: "مسح المحظورين", mine_only: "ملصقاتي", details_label: "تفاصيل", actions_label: "إجراءات",link_text: "رابط", loading: "جارٍ تحميل الملصقات…", empty: "لم يتم العثور على ملصقات بعد.", error: "تعذّر الوصول إلى المرحلات.", back: "← العودة إلى VoxVera", gate_title: "اتصل لعرض اللوحة", gate_body: "لوحة الإعلانات مرئية لمن يربط هوية Nostr. اختر أدناه كيفية الاتصال.", connect: "اتصال", connecting: "جارٍ الاتصال…", no_extension: "لم يتم العثور على إضافة Nostr. ثبّت إضافة متصفح NIP-07 لعرض اللوحة.", connect_failed: "تم إلغاء الاتصال أو فشل. يرجى المحاولة مرة أخرى.", disconnect: "قطع الاتصال", filter_following: "عرض الملصقات من الأشخاص الذين تتابعهم.", filter_seeded: "أنت لا تتابع أحدًا بعد — نعرض مجموعة مختارة من شبكة الثقة في VoxVera.", filter_all: "عرض جميع الملصقات على المرحلات (دون تصفية).", filter_unavailable: "تعذّر تحميل شبكة ثقة — يتم عرض جميع الملصقات.", filter_empty: "لا توجد ملصقات من شبكة الثقة الخاصة بك بعد. فعّل «عرض الكل» لرؤية كل شيء.", show_all_label: "عرض جميع الملصقات", connect_extension: "إضافة المتصفح (NIP-07)", connect_nsec: "استخدام مفتاح خاص (nsec)", connect_generate: "إنشاء مفتاح جديد", key_note: "يبقى مفتاحك في هذا المتصفح ويُستخدم فقط لقراءة قائمة متابَعاتك — ولا يُرسل إلى أي مكان أبدًا.", invalid_nsec: "لا يبدو هذا مفتاح nsec صالحًا.", gen_warning: "هذا مفتاحك السري الجديد. أي شخص يملكه يتحكم في هذه الهوية — احفظه في مكان آمن، لأن VoxVera لا يمكنها استعادته.", copy: "نسخ", copied: "تم النسخ", continue_label: "متابعة" },
|
||||
fa: { title: "تابلوی اعلانات", subtitle: "جدیدترین پوسترهای منتشرشده روی رلهها.", col_title: "عنوان", col_link: "پیوند", col_author: "منتشرشده توسط", col_language: "زبان", col_date: "تاریخ انتشار", col_event_id: "شناسه رویداد", act_edit: "ویرایش", act_delete: "حذف", act_rebroadcast: "بازپخش", act_block: "مسدود کردن", act_report: "گزارش", safety_link: "ایمنی و حریم خصوصی", connect_nip46: "امضاکننده از راه دور (NIP-46)", nip46_hint: "پیوند bunker:// را از برنامه امضای خود وارد کنید (مثل nsec.app یا Amber).", nip46_bad_uri: "این یک پیوند bunker:// به نظر نمیرسد.", nip46_scan: "با برنامه امضای خود اسکن کنید", nip46_waiting: "در انتظار اتصال برنامه امضای شما…", nip46_copy: "کپی پیوند", report_title: "این پوستر گزارش شود؟", report_message: "این یک گزارش عمومی روی رلهها منتشر میکند و پوستر را برای شما پنهان میکند. گزارشها قابل بازگردانی نیستند.", report_reason: "دلیل (اختیاری)", report_submit: "ارسال گزارش", report_done: "گزارش ارسال شد.", report_unavailable: "برای گزارش از اینجا، با افزونه مرورگر یا کلید ساختهشده متصل شوید.", report_cat_spam: "هرزنامه", report_cat_illegal: "محتوای غیرقانونی", report_cat_impersonation: "جعل هویت", report_cat_other: "سایر", delete_title: "این پوستر حذف شود؟", delete_message: "این از رلهها درخواست حذف میکند و آن را با یک نشان حذف جایگزین میکند تا VoxVera آن را پنهان کند. حذف در همه رلهها تضمین نمیشود.", delete_confirm: "حذف", cancel: "لغو", deleted_done: "درخواست حذف ارسال شد.", rebroadcast_done: "به رلهها بازپخش شد.", sign_unavailable: "برای حذف از اینجا، با افزونه مرورگر یا کلید ساختهشده متصل شوید — یا از ویرایشگر حذف کنید.", unblock_all: "پاک کردن مسدودها", mine_only: "پوسترهای من", details_label: "جزئیات", actions_label: "اقدامات",link_text: "پیوند", loading: "در حال بارگذاری پوسترها…", empty: "هنوز پوستری یافت نشد.", error: "اتصال به رلهها ممکن نشد.", back: "← بازگشت به VoxVera", gate_title: "برای مشاهده تابلو متصل شوید", gate_body: "تابلوی اعلانات برای کسانی که هویت Nostr را متصل میکنند قابل مشاهده است. در زیر نحوه اتصال را انتخاب کنید.", connect: "اتصال", connecting: "در حال اتصال…", no_extension: "هیچ افزونه Nostr یافت نشد. برای مشاهده تابلو یک افزونه مرورگر NIP-07 نصب کنید.", connect_failed: "اتصال لغو شد یا ناموفق بود. لطفاً دوباره تلاش کنید.", disconnect: "قطع اتصال", filter_following: "نمایش پوسترها از افرادی که دنبال میکنید.", filter_seeded: "هنوز کسی را دنبال نمیکنید — مجموعهای منتخب از شبکه اعتماد VoxVera نمایش داده میشود.", filter_all: "نمایش همه پوسترها روی رلهها (بدون فیلتر).", filter_unavailable: "بارگذاری شبکه اعتماد ممکن نشد — همه پوسترها نمایش داده میشوند.", filter_empty: "هنوز پوستری از شبکه اعتماد شما نیست. «نمایش همه» را روشن کنید تا همه را ببینید.", show_all_label: "نمایش همه پوسترها", connect_extension: "افزونه مرورگر (NIP-07)", connect_nsec: "استفاده از کلید خصوصی (nsec)", connect_generate: "ایجاد کلید جدید", key_note: "کلید شما در همین مرورگر میماند و فقط برای خواندن فهرست دنبالشدههای شما استفاده میشود — هرگز به جایی ارسال نمیشود.", invalid_nsec: "این یک کلید nsec معتبر به نظر نمیرسد.", gen_warning: "این کلید مخفی جدید شماست. هر کسی آن را داشته باشد این هویت را کنترل میکند — آن را جایی امن ذخیره کنید، زیرا VoxVera نمیتواند آن را بازیابی کند.", copy: "کپی", copied: "کپی شد", continue_label: "ادامه" },
|
||||
hi: { title: "बुलेटिन बोर्ड", subtitle: "रिले पर प्रकाशित सबसे हाल के पोस्टर।", col_title: "शीर्षक", col_link: "लिंक", col_author: "द्वारा पोस्ट किया गया", col_language: "भाषा", col_date: "पोस्ट किया गया", col_event_id: "इवेंट ID", act_edit: "संपादित करें", act_delete: "हटाएँ", act_rebroadcast: "पुनः प्रसारित करें", act_block: "ब्लॉक करें", act_report: "रिपोर्ट करें", safety_link: "सुरक्षा और निजता", connect_nip46: "रिमोट साइनर (NIP-46)", nip46_hint: "अपने साइनर ऐप से bunker:// लिंक पेस्ट करें (जैसे nsec.app, Amber)।", nip46_bad_uri: "यह bunker:// लिंक जैसा नहीं लगता।", nip46_scan: "अपने साइनर ऐप से स्कैन करें", nip46_waiting: "आपके साइनर ऐप के कनेक्ट होने की प्रतीक्षा…", nip46_copy: "लिंक कॉपी करें", report_title: "इस फ़्लायर की रिपोर्ट करें?", report_message: "यह रिले पर एक सार्वजनिक रिपोर्ट प्रकाशित करता है और फ़्लायर को आपके लिए छिपा देता है। रिपोर्ट पूर्ववत नहीं की जा सकती।", report_reason: "कारण (वैकल्पिक)", report_submit: "रिपोर्ट भेजें", report_done: "रिपोर्ट भेजी गई।", report_unavailable: "यहाँ से रिपोर्ट करने के लिए, ब्राउज़र एक्सटेंशन या जनरेट की गई कुंजी से कनेक्ट करें।", report_cat_spam: "स्पैम", report_cat_illegal: "अवैध सामग्री", report_cat_impersonation: "पहचान की नकल", report_cat_other: "अन्य", delete_title: "इस पोस्टर को हटाएँ?", delete_message: "यह रिले से हटाने का अनुरोध करता है और इसे एक विलोपन चिह्न से बदल देता है ताकि VoxVera इसे छिपा दे। हर रिले पर विलोपन की गारंटी नहीं दी जा सकती।", delete_confirm: "हटाएँ", cancel: "रद्द करें", deleted_done: "विलोपन का अनुरोध किया गया।", rebroadcast_done: "रिले पर पुनः प्रसारित किया गया।", sign_unavailable: "यहाँ से हटाने के लिए, ब्राउज़र एक्सटेंशन या जेनरेट की गई कुंजी से कनेक्ट करें — या संपादक से हटाएँ।", unblock_all: "ब्लॉक किए गए साफ़ करें", mine_only: "मेरे पोस्टर", details_label: "विवरण", actions_label: "क्रियाएँ",link_text: "लिंक", loading: "पोस्टर लोड हो रहे हैं…", empty: "अभी तक कोई पोस्टर नहीं मिला।", error: "रिले तक नहीं पहुँच सके।", back: "← VoxVera पर वापस", gate_title: "बोर्ड देखने के लिए कनेक्ट करें", gate_body: "बुलेटिन बोर्ड उन लोगों को दिखता है जो Nostr पहचान कनेक्ट करते हैं। नीचे चुनें कि कैसे कनेक्ट करना है।", connect: "कनेक्ट करें", connecting: "कनेक्ट हो रहा है…", no_extension: "कोई Nostr एक्सटेंशन नहीं मिला। बोर्ड देखने के लिए NIP-07 ब्राउज़र एक्सटेंशन इंस्टॉल करें।", connect_failed: "कनेक्शन रद्द या विफल। कृपया पुनः प्रयास करें।", disconnect: "डिस्कनेक्ट करें", filter_following: "उन लोगों के पोस्टर दिखा रहे हैं जिन्हें आप फ़ॉलो करते हैं।", filter_seeded: "आप अभी किसी को फ़ॉलो नहीं करते — VoxVera के विशLine truncated
|
||||
ja: { title: "掲示板", subtitle: "リレーに公開された最新のちらし。", col_title: "タイトル", col_link: "リンク", col_author: "投稿者", col_language: "言語", col_date: "投稿日", col_event_id: "イベントID", act_edit: "編集", act_delete: "削除", act_rebroadcast: "再配信", act_block: "ブロック", act_report: "報告", safety_link: "安全とプライバシー", connect_nip46: "リモート署名(NIP-46)", nip46_hint: "署名アプリの bunker:// リンクを貼り付けてください(例:nsec.app、Amber)。", nip46_bad_uri: "これは bunker:// リンクではないようです。", nip46_scan: "署名アプリでスキャン", nip46_waiting: "署名アプリの接続を待っています…", nip46_copy: "リンクをコピー", report_title: "このちらしを報告しますか?", report_message: "リレーに公開報告を投稿し、このちらしをあなたの表示から隠します。報告は取り消せません。", report_reason: "理由(任意)", report_submit: "報告を送信", report_done: "報告を送信しました。", report_unavailable: "ここから報告するには、ブラウザ拡張機能または生成した鍵で接続してください。", report_cat_spam: "スパム", report_cat_illegal: "違法なコンテンツ", report_cat_impersonation: "なりすまし", report_cat_other: "その他", delete_title: "このちらしを削除しますか?", delete_message: "リレーに削除を要求し、削除マーカーに置き換えて VoxVera で非表示にします。すべてのリレーでの削除は保証できません。", delete_confirm: "削除", cancel: "キャンセル", deleted_done: "削除を要求しました。", rebroadcast_done: "リレーに再配信しました。", sign_unavailable: "ここから削除するには、ブラウザ拡張機能または生成した鍵で接続してください。またはエディターから削除してください。", unblock_all: "ブロックを消去", mine_only: "自分のちらし", details_label: "詳細", actions_label: "操作",link_text: "リンク", loading: "ちらしを読み込んでいます…", empty: "まだちらしが見つかりません。", error: "リレーに接続できませんでした。", back: "← VoxVera に戻る", gate_title: "掲示板を見るには接続してください", gate_body: "掲示板はNostr IDを接続した人に表示されます。下で接続方法を選んでください。", connect: "接続", connecting: "接続中…", no_extension: "Nostr拡張機能が見つかりません。掲示板を見るにはNIP-07ブラウザ拡張機能をインストールしてください。", connect_failed: "接続がキャンセルされたか失敗しました。もう一度お試しください。", disconnect: "切断", filter_following: "フォローしている人のちらしを表示しています。", filter_seeded: "まだ誰もフォローしていません。VoxVera の信頼ネットワークから厳選したものを表示しています。", filter_all: "リレー上のすべてのちらしを表示しています(フィルターなし)。", filter_unavailable: "信頼ネットワークを読み込めませんでした。すべてのちらしを表示しています。", filter_empty: "あなたの信頼ネットワークのちらしはまだありません。「すべて表示」をオンにするとすべて表示されます。", show_all_label: "すべてのちらしを表示", connect_extension: "ブラウザ拡張機能 (NIP-07)", connect_nsec: "秘密鍵 (nsec) を使う", connect_generate: "新しい鍵を作成", key_note: "鍵はこのブラウザ内に留まり、フォローリストの読み取りにのみ使用されます。どこにも送信されません。", invalid_nsec: "有効な nsec 鍵ではないようです。", gen_warning: "これはあなたの新しい秘密鍵です。これを持つ人は誰でもこの ID を操作できます。VoxVera は復元できないので、安全な場所に保存してください。", copy: "コピー", copied: "コピーしました", continue_label: "続行" },
|
||||
pt: { title: "Quadro de avisos", subtitle: "Panfletos mais recentes publicados nos relés.", col_title: "Título", col_link: "Link", col_author: "Publicado por", col_language: "Idioma", col_date: "Publicado", col_event_id: "ID do evento", act_edit: "Editar", act_delete: "Excluir", act_rebroadcast: "Retransmitir", act_block: "Bloquear", act_report: "Denunciar", safety_link: "Segurança e privacidade", connect_nip46: "Assinador remoto (NIP-46)", nip46_hint: "Cole um link bunker:// do seu app de assinatura (ex.: nsec.app, Amber).", nip46_bad_uri: "Isso não parece um link bunker://.", nip46_scan: "Escanear com seu app de assinatura", nip46_waiting: "Aguardando seu app de assinatura conectar…", nip46_copy: "Copiar link", report_title: "Denunciar este panfleto?", report_message: "Isto publica uma denúncia pública nos relés e oculta o panfleto para você. As denúncias não podem ser desfeitas.", report_reason: "Motivo (opcional)", report_submit: "Enviar denúncia", report_done: "Denúncia enviada.", report_unavailable: "Para denunciar daqui, conecte-se com uma extensão de navegador ou uma chave gerada.", report_cat_spam: "Spam", report_cat_illegal: "Conteúdo ilegal", report_cat_impersonation: "Falsidade ideológica", report_cat_other: "Outro", delete_title: "Excluir este panfleto?", delete_message: "Isso solicita a exclusão aos relés e o substitui por um marcador de exclusão para que o VoxVera o oculte. A exclusão não pode ser garantida em todos os relés.", delete_confirm: "Excluir", cancel: "Cancelar", deleted_done: "Exclusão solicitada.", rebroadcast_done: "Retransmitido aos relés.", sign_unavailable: "Para excluir aqui, conecte-se com uma extensão de navegador ou uma chave gerada — ou exclua no editor.", unblock_all: "Limpar bloqueados", mine_only: "Meus panfletos", details_label: "Detalhes", actions_label: "Ações", link_text: "link", loading: "Carregando panfletos…", empty: "Nenhum panfleto encontrado ainda.", error: "Não foi possível acessar os relés.", back: "← Voltar ao VoxVera", gate_title: "Conecte-se para ver o quadro", gate_body: "O quadro de avisos é visível para quem conecta uma identidade Nostr. Escolha abaixo como se conectar.", connect: "Conectar", connecting: "Conectando…", no_extension: "Nenhuma extensão Nostr encontrada. Instale uma extensão de navegador NIP-07 para ver o quadro.", connect_failed: "Conexão cancelada ou falhou. Tente novamente.", disconnect: "Desconectar", filter_following: "Mostrando panfletos de pessoas que você segue.", filter_seeded: "Você ainda não segue ninguém — mostrando uma seleção da rede de confiança da VoxVera.", filter_all: "Mostrando todos os panfletos nos relés (sem filtro).", filter_unavailable: "Não foi possível carregar uma rede de confiança — mostrando todos os panfletos.", filter_empty: "Ainda não há panfletos da sua rede de confiança. Ative “Mostrar todos” para ver tudo.", show_all_label: "Mostrar todos os panfletos", connect_extension: "Extensão do navegador (NIP-07)", connect_nsec: "Usar uma chave privada (nsec)", connect_generate: "Criar uma nova chave", key_note: "Sua chave permanece neste navegador e é usada apenas para ler sua lista de seguidos — nunca é enviada para lugar algum.", invalid_nsec: "Isso não parece uma chave nsec válida.", gen_warning: "Esta é sua nova chave secreta. Qualquer pessoa que a tenha controla esta identidade — guarde-a em local seguro, pois o VoxVera não pode recuperá-la.", copy: "Copiar", copied: "Copiado", continue_label: "Continuar" },
|
||||
sw: { title: "Ubao wa matangazo", subtitle: "Mabango ya hivi karibuni yaliyochapishwa kwenye relay.", col_title: "Kichwa", col_link: "Kiungo", col_author: "Imechapishwa na", col_language: "Lugha", col_date: "Imechapishwa", col_event_id: "Kitambulisho cha tukio", act_edit: "Hariri", act_delete: "Futa", act_rebroadcast: "Tangaza tena", act_block: "Zuia", act_report: "Ripoti", safety_link: "Usalama na faragha", connect_nip46: "Mtia-saini wa mbali (NIP-46)", nip46_hint: "Bandika kiungo cha bunker:// kutoka programu yako ya kutia saini (k.m. nsec.app, Amber).", nip46_bad_uri: "Hiki hakionekani kama kiungo cha bunker://.", nip46_scan: "Changanua kwa programu yako ya kutia saini", nip46_waiting: "Inasubiri programu yako ya kutia saini iunganishe…", nip46_copy: "Nakili kiungo", report_title: "Ripoti kipeperushi hiki?", report_message: "Hii huchapisha ripoti ya umma kwenye relay na kuficha kipeperushi kwako. Ripoti haziwezi kutenduliwa.", report_reason: "Sababu (hiari)", report_submit: "Tuma ripoti", report_done: "Ripoti imetumwa.", report_unavailable: "Ili kuripoti kutoka hapa, unganisha kwa kiendelezi cha kivinjari au ufunguo ulioundwa.", report_cat_spam: "Taka", report_cat_illegal: "Maudhui haramu", report_cat_impersonation: "Kujifanya mtu mwingine", report_cat_other: "Nyingine", delete_title: "Futa bango hili?", delete_message: "Hii inaomba kufuta kutoka relay na kulibadilisha na alama ya kufuta ili VoxVera ilifiche. Kufuta hakuwezi kuhakikishwa kwenye kila relay.", delete_confirm: "Futa", cancel: "Ghairi", deleted_done: "Kufuta kumeombwa.", rebroadcast_done: "Limetangazwa tena kwenye relay.", sign_unavailable: "Ili kufuta hapa, unganisha kwa kiendelezi cha kivinjari au ufunguo uliotengenezwa — au futa kutoka kwa kihariri.", unblock_all: "Futa walizuiliwa", mine_only: "Mabango yangu", details_label: "Maelezo", actions_label: "Vitendo",link_text: "kiungo", loading: "Inapakia mabango…", empty: "Hakuna mabango yaliyopatikana bado.", error: "Imeshindwa kufikia relay.", back: "← Rudi VoxVera", gate_title: "Unganisha ili kuona ubao", gate_body: "Ubao wa matangazo unaonekana kwa wale wanaounganisha kitambulisho cha Nostr. Chagua hapa chini jinsi ya kuunganisha.", connect: "Unganisha", connecting: "Inaunganisha…", no_extension: "Hakuna kiendelezi cha Nostr kilichopatikana. Sakinisha kiendelezi cha kivinjari cha NIP-07 ili kuona ubao.", connect_failed: "Muunganisho umeghairiwa au umeshindwa. Tafadhali jaribu tena.", disconnect: "Tenganisha", filter_following: "Inaonyesha mabango kutoka kwa watu unaowafuata.", filter_seeded: "Bado hufuati mtu yeyote — tunaonyesha mkusanyiko uliochaguliwa kutoka mtandao wa kuaminiana wa VoxVera.", filter_all: "Inaonyesha mabango yote kwenye relay (bila kuchuja).", filter_unavailable: "Imeshindwa kupakia mtandao wa kuaminiana — inaonyesha mabango yote.", filter_empty: "Bado hakuna mabango kutoka mtandao wako wa kuaminiana. Washa “Onyesha yote” kuona yote.", show_all_label: "Onyesha mabango yote", connect_extension: "Kiendelezi cha kivinjari (NIP-07)", connect_nsec: "Tumia ufunguo wa faragha (nsec)", connect_generate: "Tengeneza ufunguo mpya", key_note: "Ufunguo wako unabaki katika kivinjari hiki na hutumika tu kusoma orodha yako ya unaowafuata — haupelekwi popote.", invalid_nsec: "Huu hauonekani kama ufunguo halali wa nsec.", gen_warning: "Huu ndio ufunguo wako mpya wa siri. Yeyote aliye nao anadhibiti utambulisho huu — uhifadhi mahali salama, kwa sababu VoxVera haiwezi kuurejesha.", copy: "Nakili", copied: "Imenakiliwa", continue_label: "Endelea" },
|
||||
tr: { title: "İlan panosu", subtitle: "Rölelere yayımlanan en yeni el ilanları.", col_title: "Başlık", col_link: "Bağlantı", col_author: "Yayımlayan", col_language: "Dil", col_date: "Yayımlandı", col_event_id: "Olay ID", act_edit: "Düzenle", act_delete: "Sil", act_rebroadcast: "Yeniden yayınla", act_block: "Engelle", act_report: "Bildir", safety_link: "Güvenlik ve gizlilik", connect_nip46: "Uzak imzalayıcı (NIP-46)", nip46_hint: "İmzalama uygulamanızdan bir bunker:// bağlantısı yapıştırın (ör. nsec.app, Amber).", nip46_bad_uri: "Bu bir bunker:// bağlantısına benzemiyor.", nip46_scan: "İmzalama uygulamanızla tarayın", nip46_waiting: "İmzalama uygulamanızın bağlanması bekleniyor…", nip46_copy: "Bağlantıyı kopyala", report_title: "Bu el ilanı bildirilsin mi?", report_message: "Bu, rölelere herkese açık bir bildirim yayımlar ve el ilanını sizin için gizler. Bildirimler geri alınamaz.", report_reason: "Neden (isteğe bağlı)", report_submit: "Bildirim gönder", report_done: "Bildirim gönderildi.", report_unavailable: "Buradan bildirmek için bir tarayıcı uzantısı veya oluşturulmuş bir anahtarla bağlanın.", report_cat_spam: "İstenmeyen", report_cat_illegal: "Yasa dışı içerik", report_cat_impersonation: "Kimlik taklidi", report_cat_other: "Diğer", delete_title: "Bu el ilanı silinsin mi?", delete_message: "Bu, rölelerden silmeyi ister ve VoxVera'nın gizlemesi için bir silme işaretiyle değiştirir. Silme her rölede garanti edilemez.", delete_confirm: "Sil", cancel: "İptal", deleted_done: "Silme istendi.", rebroadcast_done: "Rölelere yeniden yayınlandı.", sign_unavailable: "Buradan silmek için bir tarayıcı uzantısı veya oluşturulmuş bir anahtarla bağlan — ya da düzenleyiciden sil.", unblock_all: "Engellenenleri temizle", mine_only: "El ilanlarım", details_label: "Ayrıntılar", actions_label: "İşlemler",link_text: "bağlantı", loading: "El ilanları yükleniyor…", empty: "Henüz el ilanı bulunamadı.", error: "Rölelere ulaşılamadı.", back: "← VoxVera'ya dön", gate_title: "Panoyu görmek için bağlanın", gate_body: "İlan panosu, bir Nostr kimliği bağlayanlara görünür. Aşağıdan nasıl bağlanacağını seç.", connect: "Bağlan", connecting: "Bağlanıyor…", no_extension: "Nostr uzantısı bulunamadı. Panoyu görmek için bir NIP-07 tarayıcı uzantısı kurun.", connect_failed: "Bağlantı iptal edildi veya başarısız oldu. Lütfen tekrar deneyin.", disconnect: "Bağlantıyı kes", filter_following: "Takip ettiğin kişilerin el ilanları gösteriliyor.", filter_seeded: "Henüz kimseyi takip etmiyorsun — VoxVera'nın güven ağından seçilmiş bir küme gösteriliyor.", filter_all: "Rölelerdeki tüm el ilanları gösteriliyor (filtresiz).", filter_unavailable: "Güven ağı yüklenemedi — tüm el ilanları gösteriliyor.", filter_empty: "Güven ağından henüz el ilanı yok. Her şeyi görmek için “Tümünü göster”i aç.", show_all_label: "Tüm el ilanlarını göster", connect_extension: "Tarayıcı uzantısı (NIP-07)", connect_nsec: "Özel anahtar (nsec) kullan", connect_generate: "Yeni anahtar oluştur", key_note: "Anahtarın bu tarayıcıda kalır ve yalnızca takip listeni okumak için kullanılır — hiçbir yere gönderilmez.", invalid_nsec: "Bu geçerli bir nsec anahtarı gibi görünmüyor.", gen_warning: "Bu senin yeni gizli anahtarın. Ona sahip olan herkes bu kimliği kontrol eder — güvenli bir yerde sakla, çünkü VoxVera onu kurtaramaz.", copy: "Kopyala", copied: "Kopyalandı", continue_label: "Devam et" },
|
||||
zh: { title: "公告栏", subtitle: "发布到中继的最新传单。", col_title: "标题", col_link: "链接", col_author: "发布者", col_language: "语言", col_date: "发布时间", col_event_id: "事件 ID", act_edit: "编辑", act_delete: "删除", act_rebroadcast: "重新广播", act_block: "屏蔽", act_report: "举报", safety_link: "安全与隐私", connect_nip46: "远程签名器(NIP-46)", nip46_hint: "粘贴来自签名应用的 bunker:// 链接(如 nsec.app、Amber)。", nip46_bad_uri: "这看起来不是 bunker:// 链接。", nip46_scan: "用你的签名应用扫描", nip46_waiting: "正在等待你的签名应用连接…", nip46_copy: "复制链接", report_title: "举报此传单?", report_message: "这将向中继发布一条公开举报,并为你隐藏该传单。举报无法撤销。", report_reason: "原因(可选)", report_submit: "提交举报", report_done: "举报已提交。", report_unavailable: "要从此处举报,请使用浏览器扩展或生成的密钥进行连接。", report_cat_spam: "垃圾信息", report_cat_illegal: "非法内容", report_cat_impersonation: "冒充他人", report_cat_other: "其他", delete_title: "删除此传单?", delete_message: "这将请求中继删除它,并用删除标记替换它,使 VoxVera 将其隐藏。无法保证在每个中继上都删除。", delete_confirm: "删除", cancel: "取消", deleted_done: "已请求删除。", rebroadcast_done: "已重新广播到中继。", sign_unavailable: "若要在此删除,请使用浏览器扩展或生成的密钥连接——或在编辑器中删除。", unblock_all: "清除屏蔽", mine_only: "我的传单", details_label: "详情", actions_label: "操作",link_text: "链接", loading: "正在加载传单…", empty: "尚未找到传单。", error: "无法连接到中继。", back: "← 返回 VoxVera", gate_title: "连接以查看公告栏", gate_body: "公告栏对连接了 Nostr 身份的人可见。请在下方选择连接方式。", connect: "连接", connecting: "正在连接…", no_extension: "未找到 Nostr 扩展。请安装 NIP-07 浏览器扩展以查看公告栏。", connect_failed: "连接已取消或失败。请重试。", disconnect: "断开连接", filter_following: "正在显示你关注的人的传单。", filter_seeded: "你还没有关注任何人——正在显示来自 VoxVera 信任网络的精选内容。", filter_all: "正在显示中继上的所有传单(未过滤)。", filter_unavailable: "无法加载信任网络——正在显示所有传单。", filter_empty: "你的信任网络暂时没有传单。开启“显示全部”以查看所有内容。", show_all_label: "显示所有传单", connect_extension: "浏览器扩展 (NIP-07)", connect_nsec: "使用私钥 (nsec)", connect_generate: "创建新密钥", key_note: "你的密钥保留在此浏览器中,仅用于读取你的关注列表——绝不会发送到任何地方。", invalid_nsec: "这看起来不是有效的 nsec 密钥。", gen_warning: "这是你的新私钥。拥有它的任何人都能控制此身份——请妥善保存,因为 VoxVera 无法恢复它。", copy: "复制", copied: "已复制", continue_label: "继续" }
|
||||
};
|
||||
|
||||
function pickLang() {
|
||||
@@ -403,6 +407,8 @@
|
||||
setText("board-connect-nip46-toggle", t("connect_nip46"));
|
||||
setText("board-nip46-submit", t("connect"));
|
||||
setText("board-nip46-note", t("nip46_hint"));
|
||||
setText("board-nip46-scan", t("nip46_scan"));
|
||||
setText("board-nip46-uri-copy", t("nip46_copy"));
|
||||
setText("board-connect-nsec-toggle", t("connect_nsec"));
|
||||
setText("board-generate", t("connect_generate"));
|
||||
setText("board-nsec-submit", t("connect"));
|
||||
@@ -496,6 +502,7 @@
|
||||
if (nip46Row) nip46Row.hidden = true;
|
||||
const nip46Input = document.getElementById("board-nip46-input");
|
||||
if (nip46Input) nip46Input.value = "";
|
||||
cancelNip46Scan();
|
||||
const gen = document.getElementById("board-generated");
|
||||
if (gen) gen.hidden = true;
|
||||
pendingGeneratedPubkey = null;
|
||||
@@ -703,7 +710,20 @@
|
||||
return { open, request, close };
|
||||
}
|
||||
|
||||
let nip46Scan = null; // { ws, aborted } — the live nostrconnect (QR) listener
|
||||
|
||||
function cancelNip46Scan() {
|
||||
if (nip46Scan) {
|
||||
nip46Scan.aborted = true;
|
||||
if (nip46Scan.ws) { try { nip46Scan.ws.close(); } catch (_) {} }
|
||||
}
|
||||
nip46Scan = null;
|
||||
const wrap = document.getElementById("board-nip46-qr-wrap");
|
||||
if (wrap) wrap.hidden = true;
|
||||
}
|
||||
|
||||
function disconnectNip46() {
|
||||
cancelNip46Scan();
|
||||
if (nip46Signer && nip46Signer.conn) { try { nip46Signer.conn.close(); } catch (_) {} }
|
||||
nip46Signer = null;
|
||||
}
|
||||
@@ -718,12 +738,23 @@
|
||||
const row = document.getElementById("board-nip46-row");
|
||||
if (!row) return;
|
||||
row.hidden = !row.hidden;
|
||||
if (!row.hidden) {
|
||||
const input = document.getElementById("board-nip46-input");
|
||||
if (input) input.focus();
|
||||
}
|
||||
if (row.hidden) { cancelNip46Scan(); return; }
|
||||
const input = document.getElementById("board-nip46-input");
|
||||
if (input) input.focus();
|
||||
}
|
||||
|
||||
// Adopt a connected NIP-46 transport (shared by bunker + nostrconnect paths):
|
||||
// learn the user pubkey, keep the signer for reports/deletes, reveal the board.
|
||||
async function adoptNip46(conn) {
|
||||
const userPubkey = await conn.request("get_public_key", [], NIP46_CONNECT_TIMEOUT_MS);
|
||||
if (!/^[0-9a-f]{64}$/i.test(String(userPubkey || ""))) throw new Error("bad pubkey");
|
||||
disconnectNip46();
|
||||
nip46Signer = { conn, userPubkey: String(userPubkey).toLowerCase() };
|
||||
setConnecting(false);
|
||||
connectWithPubkey(userPubkey);
|
||||
}
|
||||
|
||||
// Bunker flow: the signer app made the link; we paste it and connect out.
|
||||
async function connectNip46() {
|
||||
gateErrorKey = "";
|
||||
refreshGateError();
|
||||
@@ -739,13 +770,8 @@
|
||||
try {
|
||||
await conn.open();
|
||||
await conn.request("connect", [parsed.remotePubkey, parsed.secret || ""], NIP46_CONNECT_TIMEOUT_MS);
|
||||
const userPubkey = await conn.request("get_public_key", [], NIP46_CONNECT_TIMEOUT_MS);
|
||||
if (!/^[0-9a-f]{64}$/i.test(String(userPubkey || ""))) throw new Error("bad pubkey");
|
||||
disconnectNip46();
|
||||
nip46Signer = { conn, userPubkey: String(userPubkey).toLowerCase() };
|
||||
if (input) input.value = "";
|
||||
setConnecting(false);
|
||||
connectWithPubkey(userPubkey);
|
||||
await adoptNip46(conn);
|
||||
} catch (_) {
|
||||
try { conn.close(); } catch (_) {}
|
||||
setConnecting(false);
|
||||
@@ -754,6 +780,76 @@
|
||||
}
|
||||
}
|
||||
|
||||
function boardQrSvg(value) {
|
||||
if (typeof qrcode !== "function") return "";
|
||||
try {
|
||||
const qr = qrcode(0, "M");
|
||||
qr.addData(value);
|
||||
qr.make();
|
||||
return qr.createSvgTag({ cellSize: 2, margin: 2, scalable: true });
|
||||
} catch (_) { return ""; }
|
||||
}
|
||||
|
||||
function randomHex(bytes) {
|
||||
const buf = new Uint8Array(bytes || 16);
|
||||
window.crypto.getRandomValues(buf);
|
||||
return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
// nostrconnect (QR) flow: WE generate the link + QR; the signer app scans it
|
||||
// and connects back, echoing our secret. We then bind a durable connection.
|
||||
function startNip46Scan() {
|
||||
cancelNip46Scan();
|
||||
gateErrorKey = "";
|
||||
refreshGateError();
|
||||
const tools = window.NostrTools;
|
||||
const skBytes = tools.generateSecretKey();
|
||||
const clientSkHex = Array.from(skBytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
const clientPubkey = tools.getPublicKey(skBytes);
|
||||
const secret = randomHex(16);
|
||||
const relay = NIP46_NOSTRCONNECT_RELAY;
|
||||
const uri = `nostrconnect://${clientPubkey}?relay=${encodeURIComponent(relay)}&secret=${secret}&name=VoxVera`;
|
||||
const qr = document.getElementById("board-nip46-qr"); if (qr) qr.innerHTML = boardQrSvg(uri);
|
||||
const uriEl = document.getElementById("board-nip46-uri"); if (uriEl) uriEl.value = uri;
|
||||
const wrap = document.getElementById("board-nip46-qr-wrap"); if (wrap) wrap.hidden = false;
|
||||
const status = document.getElementById("board-nip46-scan-status"); if (status) status.textContent = t("nip46_waiting");
|
||||
|
||||
const state = { ws: null, aborted: false };
|
||||
nip46Scan = state;
|
||||
const subId = "voxveranc-" + Math.random().toString(36).slice(2);
|
||||
let ws;
|
||||
try { ws = new WebSocket(relay); } catch (_) { gateErrorKey = "connect_failed"; refreshGateError(); return; }
|
||||
state.ws = ws;
|
||||
const timer = setTimeout(() => {
|
||||
if (state.aborted) return;
|
||||
cancelNip46Scan();
|
||||
gateErrorKey = "connect_failed";
|
||||
refreshGateError();
|
||||
}, NIP46_CONNECT_TIMEOUT_MS);
|
||||
ws.onopen = () => {
|
||||
const since = Math.floor(Date.now() / 1000) - 10;
|
||||
try { ws.send(JSON.stringify(["REQ", subId, { kinds: [NIP46_KIND], "#p": [clientPubkey], since }])); } catch (_) {}
|
||||
};
|
||||
ws.onmessage = (m) => {
|
||||
if (state.aborted) return;
|
||||
let data; try { data = JSON.parse(m.data); } catch (_) { return; }
|
||||
if (data[0] !== "EVENT" || data[1] !== subId) return;
|
||||
const ev = data[2];
|
||||
if (!ev || ev.kind !== NIP46_KIND) return;
|
||||
let text; try { text = nip46Codec(clientSkHex, ev.pubkey).decrypt(ev.content); } catch (_) { return; }
|
||||
let msg; try { msg = JSON.parse(text); } catch (_) { return; }
|
||||
if (!msg || (msg.result !== secret && msg.result !== "ack")) return;
|
||||
clearTimeout(timer);
|
||||
state.aborted = true;
|
||||
try { ws.close(); } catch (_) {}
|
||||
const conn = openNip46Connection({ relay, remotePubkey: ev.pubkey, localSkHex: clientSkHex, localPubkey: clientPubkey });
|
||||
conn.open()
|
||||
.then(() => adoptNip46(conn))
|
||||
.catch(() => { try { conn.close(); } catch (_) {} gateErrorKey = "connect_failed"; refreshGateError(); });
|
||||
};
|
||||
ws.onerror = () => {};
|
||||
}
|
||||
|
||||
function hexToBytes(hex) {
|
||||
const out = new Uint8Array(32);
|
||||
for (let i = 0; i < 32; i += 1) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
@@ -795,6 +891,7 @@
|
||||
if (nsecRow) nsecRow.hidden = true;
|
||||
const nip46Row = document.getElementById("board-nip46-row");
|
||||
if (nip46Row) nip46Row.hidden = true;
|
||||
cancelNip46Scan();
|
||||
const gen = document.getElementById("board-generated");
|
||||
if (gen) gen.hidden = false;
|
||||
}
|
||||
@@ -810,6 +907,7 @@
|
||||
if (gen) gen.hidden = true;
|
||||
const nip46Row = document.getElementById("board-nip46-row");
|
||||
if (nip46Row) nip46Row.hidden = true;
|
||||
cancelNip46Scan();
|
||||
const row = document.getElementById("board-nsec-row");
|
||||
if (!row) return;
|
||||
row.hidden = !row.hidden;
|
||||
@@ -1222,6 +1320,13 @@
|
||||
if (nip46Input) nip46Input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") { e.preventDefault(); connectNip46().catch(() => {}); }
|
||||
});
|
||||
const nip46ScanBtn = document.getElementById("board-nip46-scan");
|
||||
if (nip46ScanBtn) nip46ScanBtn.addEventListener("click", startNip46Scan);
|
||||
const nip46CopyBtn = document.getElementById("board-nip46-uri-copy");
|
||||
if (nip46CopyBtn) nip46CopyBtn.addEventListener("click", () => {
|
||||
const u = document.getElementById("board-nip46-uri");
|
||||
if (u && u.value) navigator.clipboard.writeText(u.value).catch(() => u.select());
|
||||
});
|
||||
const nsecToggle = document.getElementById("board-connect-nsec-toggle");
|
||||
if (nsecToggle) nsecToggle.addEventListener("click", toggleNsecRow);
|
||||
const nsecSubmit = document.getElementById("board-nsec-submit");
|
||||
|
||||
+10
-1
@@ -81,6 +81,15 @@
|
||||
<input id="nip46-input" type="text" autocomplete="off" spellcheck="false" placeholder="bunker://…">
|
||||
<button type="button" id="nip46-submit" class="secondary-button">Connect</button>
|
||||
<span id="nip46-status" class="board-key-note" role="status"></span>
|
||||
<button type="button" id="nip46-scan" class="secondary-button">Scan with your signer app</button>
|
||||
<!-- nostrconnect:// QR: the client generates the link; the signer app
|
||||
scans it and connects back. The local key is ephemeral. -->
|
||||
<div id="nip46-qr-wrap" hidden>
|
||||
<div id="nip46-qr" class="nip46-qr"></div>
|
||||
<input id="nip46-uri" readonly>
|
||||
<button type="button" id="nip46-uri-copy" class="secondary-button">Copy link</button>
|
||||
<span id="nip46-scan-status" class="board-key-note" role="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Shown on load when a PIN-encrypted identity is stored. -->
|
||||
<div id="identity-locked-row" class="identity-import" hidden>
|
||||
@@ -220,7 +229,7 @@
|
||||
<button type="button" id="print-preview">Print preview</button>
|
||||
</div>
|
||||
<div id="flyer-preview" class="flyer-preview"></div>
|
||||
<p class="board-link"><a href="board.html" data-i18n-key="bulletin_board">Bulletin board</a> · <a href="/safety" data-i18n-key="safety_link">Safety & privacy</a></p>
|
||||
<p class="board-link"><a href="board.html" data-i18n-key="bulletin_board">Bulletin board</a> · <a href="/safety.html" data-i18n-key="safety_link">Safety & privacy</a></p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -256,6 +256,43 @@ main {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#nip46-qr-wrap[hidden],
|
||||
#board-nip46-qr-wrap[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#nip46-qr-wrap,
|
||||
#board-nip46-qr-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.nip46-qr {
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
align-self: center;
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nip46-qr svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#nip46-uri,
|
||||
#board-nip46-uri {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* The locked row keeps the note on its own line, then PIN + buttons below. */
|
||||
#identity-locked-row {
|
||||
flex-direction: row;
|
||||
@@ -765,6 +802,13 @@ hr {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Footer "Safety & privacy" link on the board page (mirrors the home page). */
|
||||
.board-page .board-link {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
margin: 16px 0 28px;
|
||||
}
|
||||
|
||||
body.mode-viewer {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
+154
-17
@@ -31,6 +31,10 @@
|
||||
const NIP46_RELAY_TIMEOUT_MS = 12000;
|
||||
const NIP46_CONNECT_TIMEOUT_MS = 90000;
|
||||
const NIP46_SIGN_TIMEOUT_MS = 90000;
|
||||
// Rendezvous relay for the nostrconnect:// (QR) flow — the client proposes it
|
||||
// and the signer app connects to it. relay.nsec.app is the de-facto NIP-46
|
||||
// relay that the common signer apps (nsec.app, Amber) already speak.
|
||||
const NIP46_NOSTRCONNECT_RELAY = "wss://relay.nsec.app";
|
||||
const UI_LANG_STORAGE_KEY = "voxvera_nostr_lang";
|
||||
const LOCALES = window.VoxVeraLocales || {};
|
||||
const FALLBACK_LANG = "en";
|
||||
@@ -134,6 +138,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
"use-nip46-toggle": "use_nip46",
|
||||
"nsec-submit": "use_key",
|
||||
"nip46-submit": "connect_remote",
|
||||
"nip46-scan": "nip46_scan",
|
||||
"nip46-uri-copy": "nip46_copy",
|
||||
"unlock-key": "unlock",
|
||||
"forget-key": "forget_key",
|
||||
"viewer-fetch-render": "fetch_render",
|
||||
@@ -262,6 +268,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "لا يبدو هذا رابط bunker://.",
|
||||
nip46_timeout: "لم يستجب الموقّع عن بُعد. افتح التطبيق وحاول مرة أخرى.",
|
||||
nip46_failed: "تعذّر الاتصال بالموقّع عن بُعد.",
|
||||
nip46_scan: "امسح باستخدام تطبيق التوقيع",
|
||||
nip46_waiting: "بانتظار اتصال تطبيق التوقيع…",
|
||||
nip46_copy: "نسخ الرابط",
|
||||
import_design: "استيراد التصميم",
|
||||
export_design: "تصدير التصميم",
|
||||
import_confirm_title: "هل تريد استيراد هذا التصميم؟",
|
||||
@@ -373,6 +382,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "Das sieht nicht nach einem bunker://-Link aus.",
|
||||
nip46_timeout: "Der externe Signierer hat nicht geantwortet. Öffne die App und versuche es erneut.",
|
||||
nip46_failed: "Verbindung zum externen Signierer fehlgeschlagen.",
|
||||
nip46_scan: "Mit deiner Signier-App scannen",
|
||||
nip46_waiting: "Warte auf die Verbindung deiner Signier-App…",
|
||||
nip46_copy: "Link kopieren",
|
||||
import_design: "Design importieren",
|
||||
export_design: "Design exportieren",
|
||||
import_confirm_title: "Dieses Design importieren?",
|
||||
@@ -507,6 +519,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "That doesn't look like a bunker:// link.",
|
||||
nip46_timeout: "The remote signer didn't respond. Open your signer app and try again.",
|
||||
nip46_failed: "Couldn't connect to the remote signer.",
|
||||
nip46_scan: "Scan with your signer app",
|
||||
nip46_waiting: "Waiting for your signer app to connect…",
|
||||
nip46_copy: "Copy link",
|
||||
import_design: "Import design",
|
||||
export_design: "Export design",
|
||||
import_confirm_title: "Import this design?",
|
||||
@@ -595,6 +610,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "Esto no parece un enlace bunker://.",
|
||||
nip46_timeout: "El firmante remoto no respondió. Abre la app e inténtalo de nuevo.",
|
||||
nip46_failed: "No se pudo conectar con el firmante remoto.",
|
||||
nip46_scan: "Escanear con tu app de firma",
|
||||
nip46_waiting: "Esperando a que tu app de firma se conecte…",
|
||||
nip46_copy: "Copiar enlace",
|
||||
import_design: "Importar diseño",
|
||||
export_design: "Exportar diseño",
|
||||
import_confirm_title: "¿Importar este diseño?",
|
||||
@@ -706,6 +724,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "این یک پیوند bunker:// به نظر نمیرسد.",
|
||||
nip46_timeout: "امضاکننده از راه دور پاسخ نداد. برنامه را باز کنید و دوباره تلاش کنید.",
|
||||
nip46_failed: "اتصال به امضاکننده از راه دور ممکن نشد.",
|
||||
nip46_scan: "با برنامه امضای خود اسکن کنید",
|
||||
nip46_waiting: "در انتظار اتصال برنامه امضای شما…",
|
||||
nip46_copy: "کپی پیوند",
|
||||
import_design: "وارد کردن طرح",
|
||||
export_design: "خروجی گرفتن از طرح",
|
||||
import_confirm_title: "این طرح وارد شود؟",
|
||||
@@ -817,6 +838,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "Cela ne ressemble pas à un lien bunker://.",
|
||||
nip46_timeout: "Le signataire distant n'a pas répondu. Ouvrez l'app et réessayez.",
|
||||
nip46_failed: "Impossible de se connecter au signataire distant.",
|
||||
nip46_scan: "Scanner avec votre app de signature",
|
||||
nip46_waiting: "En attente de la connexion de votre app de signature…",
|
||||
nip46_copy: "Copier le lien",
|
||||
import_design: "Importer le modèle",
|
||||
export_design: "Exporter le modèle",
|
||||
import_confirm_title: "Importer ce modèle ?",
|
||||
@@ -928,6 +952,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "זה לא נראה כמו קישור bunker://.",
|
||||
nip46_timeout: "החותם המרוחק לא הגיב. פתח את האפליקציה ונסה שוב.",
|
||||
nip46_failed: "לא ניתן להתחבר לחותם המרוחק.",
|
||||
nip46_scan: "סרוק עם אפליקציית החתימה",
|
||||
nip46_waiting: "ממתין לחיבור אפליקציית החתימה…",
|
||||
nip46_copy: "העתק קישור",
|
||||
import_design: "ייבוא עיצוב",
|
||||
export_design: "ייצוא עיצוב",
|
||||
import_confirm_title: "לייבא את העיצוב הזה?",
|
||||
@@ -1039,6 +1066,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "यह bunker:// लिंक जैसा नहीं लगता।",
|
||||
nip46_timeout: "रिमोट साइनर ने उत्तर नहीं दिया। ऐप खोलें और पुनः प्रयास करें।",
|
||||
nip46_failed: "रिमोट साइनर से कनेक्ट नहीं हो सका।",
|
||||
nip46_scan: "अपने साइनर ऐप से स्कैन करें",
|
||||
nip46_waiting: "आपके साइनर ऐप के कनेक्ट होने की प्रतीक्षा…",
|
||||
nip46_copy: "लिंक कॉपी करें",
|
||||
import_design: "डिज़ाइन आयात करें",
|
||||
export_design: "डिज़ाइन निर्यात करें",
|
||||
import_confirm_title: "यह डिज़ाइन आयात करें?",
|
||||
@@ -1150,6 +1180,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "これは bunker:// リンクではないようです。",
|
||||
nip46_timeout: "リモート署名が応答しませんでした。アプリを開いて再試行してください。",
|
||||
nip46_failed: "リモート署名に接続できませんでした。",
|
||||
nip46_scan: "署名アプリでスキャン",
|
||||
nip46_waiting: "署名アプリの接続を待っています…",
|
||||
nip46_copy: "リンクをコピー",
|
||||
import_design: "デザインを読み込む",
|
||||
export_design: "デザインを書き出す",
|
||||
import_confirm_title: "このデザインを読み込みますか?",
|
||||
@@ -1261,6 +1294,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "Isso não parece um link bunker://.",
|
||||
nip46_timeout: "O assinador remoto não respondeu. Abra o app e tente novamente.",
|
||||
nip46_failed: "Não foi possível conectar ao assinador remoto.",
|
||||
nip46_scan: "Escanear com seu app de assinatura",
|
||||
nip46_waiting: "Aguardando seu app de assinatura conectar…",
|
||||
nip46_copy: "Copiar link",
|
||||
import_design: "Importar design",
|
||||
export_design: "Exportar design",
|
||||
import_confirm_title: "Importar este design?",
|
||||
@@ -1372,6 +1408,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "Это не похоже на ссылку bunker://.",
|
||||
nip46_timeout: "Удалённый подписант не ответил. Откройте приложение и попробуйте снова.",
|
||||
nip46_failed: "Не удалось подключиться к удалённому подписанту.",
|
||||
nip46_scan: "Сканировать приложением для подписи",
|
||||
nip46_waiting: "Ожидание подключения приложения для подписи…",
|
||||
nip46_copy: "Скопировать ссылку",
|
||||
import_design: "Импорт дизайна",
|
||||
export_design: "Экспорт дизайна",
|
||||
import_confirm_title: "Импортировать этот дизайн?",
|
||||
@@ -1483,6 +1522,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "Hiki hakionekani kama kiungo cha bunker://.",
|
||||
nip46_timeout: "Mtia-saini wa mbali hakujibu. Fungua programu na ujaribu tena.",
|
||||
nip46_failed: "Imeshindwa kuunganisha na mtia-saini wa mbali.",
|
||||
nip46_scan: "Changanua kwa programu yako ya kutia saini",
|
||||
nip46_waiting: "Inasubiri programu yako ya kutia saini iunganishe…",
|
||||
nip46_copy: "Nakili kiungo",
|
||||
import_design: "Leta muundo",
|
||||
export_design: "Hamisha muundo",
|
||||
import_confirm_title: "Ulete muundo huu?",
|
||||
@@ -1594,6 +1636,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "Bu bir bunker:// bağlantısına benzemiyor.",
|
||||
nip46_timeout: "Uzak imzalayıcı yanıt vermedi. Uygulamayı açıp tekrar deneyin.",
|
||||
nip46_failed: "Uzak imzalayıcıya bağlanılamadı.",
|
||||
nip46_scan: "İmzalama uygulamanızla tarayın",
|
||||
nip46_waiting: "İmzalama uygulamanızın bağlanması bekleniyor…",
|
||||
nip46_copy: "Bağlantıyı kopyala",
|
||||
import_design: "Tasarımı içe aktar",
|
||||
export_design: "Tasarımı dışa aktar",
|
||||
import_confirm_title: "Bu tasarım içe aktarılsın mı?",
|
||||
@@ -1705,6 +1750,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
nip46_bad_uri: "这看起来不是 bunker:// 链接。",
|
||||
nip46_timeout: "远程签名器无响应。请打开应用并重试。",
|
||||
nip46_failed: "无法连接到远程签名器。",
|
||||
nip46_scan: "用你的签名应用扫描",
|
||||
nip46_waiting: "正在等待你的签名应用连接…",
|
||||
nip46_copy: "复制链接",
|
||||
import_design: "导入设计",
|
||||
export_design: "导出设计",
|
||||
import_confirm_title: "导入此设计?",
|
||||
@@ -2527,7 +2575,20 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
return { open, request, close };
|
||||
}
|
||||
|
||||
let nostrConnect = null; // { ws, aborted } — the live nostrconnect (QR) listener
|
||||
|
||||
function cancelNostrConnect() {
|
||||
if (nostrConnect) {
|
||||
nostrConnect.aborted = true;
|
||||
if (nostrConnect.ws) { try { nostrConnect.ws.close(); } catch (_) {} }
|
||||
}
|
||||
nostrConnect = null;
|
||||
const wrap = el("nip46-qr-wrap");
|
||||
if (wrap) wrap.hidden = true;
|
||||
}
|
||||
|
||||
function disconnectNip46() {
|
||||
cancelNostrConnect();
|
||||
if (nip46Signer) { try { nip46Signer.close(); } catch (_) {} }
|
||||
nip46Signer = null;
|
||||
nip46Pubkey = null;
|
||||
@@ -2541,9 +2602,33 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
const row = el("nip46-import");
|
||||
if (!row) return;
|
||||
row.hidden = !row.hidden;
|
||||
if (!row.hidden) { const i = el("nip46-input"); if (i) i.focus(); }
|
||||
if (row.hidden) { cancelNostrConnect(); return; }
|
||||
const i = el("nip46-input"); if (i) i.focus();
|
||||
}
|
||||
|
||||
// Adopt a connected NIP-46 transport as the active identity (shared by the
|
||||
// bunker and nostrconnect paths). The signer is already reachable on `conn`.
|
||||
async function adoptNip46Connection(conn) {
|
||||
const userPubkey = await conn.request("get_public_key", [], NIP46_CONNECT_TIMEOUT_MS);
|
||||
if (!/^[0-9a-f]{64}$/i.test(String(userPubkey || ""))) throw new Error("bad pubkey");
|
||||
disconnectNip46();
|
||||
nip46Signer = conn;
|
||||
nip46Pubkey = String(userPubkey).toLowerCase();
|
||||
identityMode = "nip46";
|
||||
const nip46Input = el("nip46-input");
|
||||
if (nip46Input) nip46Input.value = "";
|
||||
const status = el("nip46-status");
|
||||
if (status) status.textContent = "";
|
||||
const row = el("nip46-import");
|
||||
if (row) row.hidden = true;
|
||||
// Session-only: nothing is written to disk, so a reload returns to anonymous.
|
||||
persistMode("anon");
|
||||
syncConnectedPubkey(nip46Pubkey);
|
||||
refreshSignerState();
|
||||
refreshIdentityState();
|
||||
}
|
||||
|
||||
// Bunker flow: the signer app made the link; we paste it and connect out.
|
||||
async function submitNip46Import() {
|
||||
setIdentityError(null);
|
||||
const input = el("nip46-input");
|
||||
@@ -2561,22 +2646,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
try {
|
||||
await conn.open();
|
||||
await conn.request("connect", [parsed.remotePubkey, parsed.secret || ""], NIP46_CONNECT_TIMEOUT_MS);
|
||||
const userPubkey = await conn.request("get_public_key", [], NIP46_CONNECT_TIMEOUT_MS);
|
||||
if (!/^[0-9a-f]{64}$/i.test(String(userPubkey || ""))) throw new Error("bad pubkey");
|
||||
disconnectNip46();
|
||||
nip46Signer = conn;
|
||||
nip46Pubkey = String(userPubkey).toLowerCase();
|
||||
identityMode = "nip46";
|
||||
if (input) input.value = "";
|
||||
if (status) status.textContent = "";
|
||||
const row = el("nip46-import");
|
||||
if (row) row.hidden = true;
|
||||
// Session-only: the bunker token is never written to disk, so a reload
|
||||
// returns cleanly to anonymous rather than a dead connection.
|
||||
persistMode("anon");
|
||||
syncConnectedPubkey(nip46Pubkey);
|
||||
refreshSignerState();
|
||||
refreshIdentityState();
|
||||
await adoptNip46Connection(conn);
|
||||
} catch (error) {
|
||||
try { conn.close(); } catch (_) {}
|
||||
if (status) status.textContent = "";
|
||||
@@ -2585,6 +2655,67 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
}
|
||||
}
|
||||
|
||||
function randomSecretHex(bytes) {
|
||||
const buf = new Uint8Array(bytes || 16);
|
||||
window.crypto.getRandomValues(buf);
|
||||
return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
// nostrconnect (QR) flow: WE generate the link + QR; the signer app scans it
|
||||
// and connects back. We listen for the signer's reply (which echoes our
|
||||
// secret), then hand the discovered signer pubkey to a durable connection.
|
||||
function startNostrConnect() {
|
||||
cancelNostrConnect();
|
||||
setIdentityError(null);
|
||||
const tools = nostrTools();
|
||||
const skBytes = tools.generateSecretKey();
|
||||
const clientSkHex = bytesToHex(skBytes);
|
||||
const clientPubkey = tools.getPublicKey(skBytes);
|
||||
const secret = randomSecretHex(16);
|
||||
const relay = NIP46_NOSTRCONNECT_RELAY;
|
||||
const uri = `nostrconnect://${clientPubkey}?relay=${encodeURIComponent(relay)}&secret=${secret}&name=VoxVera`;
|
||||
const qr = el("nip46-qr"); if (qr) qr.innerHTML = makeQrSvg(uri);
|
||||
const uriInput = el("nip46-uri"); if (uriInput) uriInput.value = uri;
|
||||
const wrap = el("nip46-qr-wrap"); if (wrap) wrap.hidden = false;
|
||||
const status = el("nip46-scan-status");
|
||||
if (status) status.textContent = nostrLabel("nip46_waiting", currentUiLang());
|
||||
|
||||
const state = { ws: null, aborted: false };
|
||||
nostrConnect = state;
|
||||
const subId = "voxveranc-" + Math.random().toString(36).slice(2);
|
||||
let ws;
|
||||
try { ws = new WebSocket(relay); } catch (_) { setIdentityError("nip46_failed"); return; }
|
||||
state.ws = ws;
|
||||
const timer = setTimeout(() => {
|
||||
if (state.aborted) return;
|
||||
cancelNostrConnect();
|
||||
setIdentityError("nip46_timeout");
|
||||
}, NIP46_CONNECT_TIMEOUT_MS);
|
||||
ws.onopen = () => {
|
||||
const since = Math.floor(Date.now() / 1000) - 10;
|
||||
try { ws.send(JSON.stringify(["REQ", subId, { kinds: [NIP46_KIND], "#p": [clientPubkey], since }])); } catch (_) {}
|
||||
};
|
||||
ws.onmessage = (m) => {
|
||||
if (state.aborted) return;
|
||||
let data; try { data = JSON.parse(m.data); } catch (_) { return; }
|
||||
if (data[0] !== "EVENT" || data[1] !== subId) return;
|
||||
const ev = data[2];
|
||||
if (!ev || ev.kind !== NIP46_KIND) return;
|
||||
let text; try { text = nip46Codec(clientSkHex, ev.pubkey).decrypt(ev.content); } catch (_) { return; }
|
||||
let msg; try { msg = JSON.parse(text); } catch (_) { return; }
|
||||
// The signer proves it scanned our code by echoing the secret back.
|
||||
if (!msg || (msg.result !== secret && msg.result !== "ack")) return;
|
||||
clearTimeout(timer);
|
||||
state.aborted = true;
|
||||
try { ws.close(); } catch (_) {}
|
||||
const conn = openNip46Connection({ relay, remotePubkey: ev.pubkey, localSkHex: clientSkHex, localPubkey: clientPubkey });
|
||||
conn.open()
|
||||
.then(() => adoptNip46Connection(conn))
|
||||
.catch(() => { try { conn.close(); } catch (_) {} setIdentityError("nip46_failed"); });
|
||||
};
|
||||
ws.onerror = () => {};
|
||||
}
|
||||
|
||||
// --- Identity actions wired to the controls ---
|
||||
function chooseAnonIdentity() {
|
||||
disconnectNip46();
|
||||
@@ -2627,6 +2758,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
function toggleNsecImport() {
|
||||
setIdentityError(null);
|
||||
showLockedRow(false);
|
||||
cancelNostrConnect();
|
||||
const remote = el("nip46-import");
|
||||
if (remote) remote.hidden = true;
|
||||
const row = el("nsec-import");
|
||||
@@ -3889,6 +4021,11 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
el("nip46-input").addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") { event.preventDefault(); el("nip46-submit").click(); }
|
||||
});
|
||||
el("nip46-scan").addEventListener("click", startNostrConnect);
|
||||
el("nip46-uri-copy").addEventListener("click", () => {
|
||||
const uri = el("nip46-uri");
|
||||
if (uri && uri.value) navigator.clipboard.writeText(uri.value).catch(() => { uri.select(); });
|
||||
});
|
||||
el("unlock-key").addEventListener("click", () => {
|
||||
unlockIdentity().catch(() => setIdentityError("pin_wrong"));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user