Compare commits

...
6 Commits
Author SHA1 Message Date
thePR0M3TH3AN 2b29d71ac5 chore: release v0.3.4 2026-03-17 14:18:42 -04:00
thePR0M3TH3ANandClaude Opus 4.6 46360ca6ea fix(install): clean stale generated files from site dirs before rebuild
Install script now removes index.html, QR codes, and download bundles
from existing site directories before rebuild-all regenerates them.
Preserves config.json, .onionshare-session, server.pid, and logs.
Also auto-rebuilds on serve if index.html is missing.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-16 21:46:14 -04:00
thePR0M3TH3ANandClaude Opus 4.6 03b1c51e27 feat(rebuild): add rebuild-all command and auto-rebuild on install
Existing sites kept stale index.html from old templates after upgrades.
New `voxvera rebuild-all` regenerates HTML for all sites from the
current master template while preserving keys, config, and content.
The install script now calls rebuild-all automatically after install.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-16 21:39:34 -04:00
thePR0M3TH3ANandClaude Opus 4.6 fc628fb9a0 fix(templates): sync bundled index.html copies with master template
These files are not read by the build pipeline (which always generates
from index-master.html), but keeping them in sync avoids confusion.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-16 21:33:47 -04:00
thePR0M3TH3ANandClaude Opus 4.6 91391b1dd7 fix(lint): add missing Console() instances in export/import key functions
Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-16 21:12:35 -04:00
thePR0M3TH3ANandClaude Opus 4.6 d61a7b4c92 feat(keys): add Export Keys / Import Keys to per-site menu with 14-language support
Allows users to export onion service session keys as base64 and import
them on another instance, enabling domain migration without re-hosting.
Bump version to 0.3.3.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-16 21:04:34 -04:00
32 changed files with 634 additions and 98 deletions

No files matched your search

+1
View File
@@ -37,6 +37,7 @@ Run the following commands to verify your changes:
- **Dependencies**: if you add a new Python library to `requirements.txt`, you MUST run `voxvera vendorize` to include it in the portable distribution.
- **Layout Integrity**: VoxVera uses visual width validation. If you add new UI elements, ensure they respect the physical boundaries of an 8.5"x11" flyer.
- **Tests**: Ensure all tests pass: `pytest tests/`. Run the integrity suite specifically when modifying locales: `pytest tests/test_integrity.py`.
- **Recommended local check**: Run `bash scripts/check.sh` before opening a PR. It mirrors the main lint/test/build checks in one command.
- **Style**: Follow the existing coding style (PEP 8 for Python).
- **Documentation**: If adding a new feature, update the master templates in `docs/templates/` and run `voxvera build-docs`.
+7
View File
@@ -30,6 +30,13 @@ pip install .
voxvera vendorize # Optional: pull dependencies locally for offline use
```
### 5. Local Validation
Run the full local validation sweep with:
```bash
bash scripts/check.sh
```
This runs linting, Python tests, Electron smoke tests, dependency checks, and the docs/site rebuild steps used during development.
---
## Key Features
+13 -4
View File
@@ -15,9 +15,12 @@ echo "Starting E2E verification at $(date)"
if [ -n "${VOXVERA_BIN:-}" ] && [ -f "$VOXVERA_BIN" ]; then
voxvera_cmd="$VOXVERA_BIN"
echo "Using provided VoxVera binary: $voxvera_cmd" >&2
elif command -v voxvera >/dev/null 2>&1; then
voxvera_cmd="$(command -v voxvera)"
echo "Using existing VoxVera on PATH: $voxvera_cmd" >&2
else
echo "Installing VoxVera in editable mode..."
python3 -m pip install --break-system-packages -e ".[e2e]"
python3 -m pip install --break-system-packages --no-build-isolation -e ".[e2e]"
voxvera_cmd="$(command -v voxvera || echo "$HOME/.local/bin/voxvera")"
fi
@@ -30,16 +33,22 @@ DemoUser
voxvera
EOI
config_path="$HOME/host/voxvera/config.json"
if [ ! -f "$config_path" ]; then
echo "CRITICAL: Expected config file not found after init: $config_path" >&2
exit 1
fi
echo "Building demo flyer..."
"$voxvera_cmd" --lang en build
"$voxvera_cmd" --lang en --config "$config_path" build
# Verify build output exists in host/
folder_name="voxvera"
host_dir="voxvera/host/$folder_name"
host_dir="$HOME/host/$folder_name"
if [ ! -d "$host_dir" ]; then
echo "CRITICAL: Host directory $host_dir not found!" >&2
ls -R voxvera/host/ || true
ls -R "$HOME/host/" || true
exit 1
fi
+10 -5
View File
@@ -4,6 +4,7 @@ const path = require('path');
const which = require('which');
const fs = require('fs');
const { launchTor } = require('./tor.js');
const { extractOnionUrl } = require('./runtime-utils');
let mainWindow;
let onionProc;
@@ -74,8 +75,12 @@ async function runServe (retry = false) {
onionProc.stdout.on('data', buf => {
const line = buf.toString();
const m = line.match(/OnionShare is hosting at (http.*\.onion)/);
if (m) { gotURL = true; clearTimeout(softTimeout); }
const onionUrl = extractOnionUrl(line);
if (onionUrl) {
gotURL = true;
clearTimeout(softTimeout);
mainWindow.webContents.send('onion-url', onionUrl);
}
mainWindow.webContents.send('log', { text: line });
});
@@ -134,9 +139,9 @@ ipcMain.handle('run-quickstart', async (_, config) => {
if (mainWindow) {
mainWindow.webContents.send('log', { text: line, isError: false });
}
const m = line.match(/Onion URL:\s*(https?:\/\/[a-z0-9.-]+\.onion)/i);
if (m && mainWindow) {
mainWindow.webContents.send('onion-url', m[1]);
const onionUrl = extractOnionUrl(line);
if (onionUrl && mainWindow) {
mainWindow.webContents.send('onion-url', onionUrl);
}
});
proc.stderr.on('data', data => {
+2 -1
View File
@@ -4,7 +4,8 @@
"main": "main.js",
"scripts": {
"start": "electron .",
"lint": "echo 'lint pass'"
"lint": "echo 'lint pass'",
"test": "node --test runtime-utils.test.js"
},
"devDependencies": {
"electron": "^29.0.0",
+16
View File
@@ -0,0 +1,16 @@
function extractOnionUrl(line) {
const match = line.match(/Onion URL:\s*(https?:\/\/[a-z0-9.-]+\.onion)/i)
|| line.match(/OnionShare is hosting at\s*(https?:\/\/[a-z0-9.-]+\.onion)/i);
return match ? match[1] : null;
}
function platformDirName(platform = process.platform) {
if (platform === 'win32') return 'win';
if (platform === 'darwin') return 'mac';
return 'linux';
}
module.exports = {
extractOnionUrl,
platformDirName,
};
+28
View File
@@ -0,0 +1,28 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { extractOnionUrl, platformDirName } = require('./runtime-utils');
test('extractOnionUrl handles current CLI output', () => {
assert.equal(
extractOnionUrl('[voxvera] Onion URL: http://test123.onion'),
'http://test123.onion'
);
});
test('extractOnionUrl handles legacy OnionShare output', () => {
assert.equal(
extractOnionUrl('OnionShare is hosting at http://legacy456.onion'),
'http://legacy456.onion'
);
});
test('extractOnionUrl returns null when no onion URL is present', () => {
assert.equal(extractOnionUrl('still bootstrapping'), null);
});
test('platformDirName maps bundled resource folders correctly', () => {
assert.equal(platformDirName('linux'), 'linux');
assert.equal(platformDirName('darwin'), 'mac');
assert.equal(platformDirName('win32'), 'win');
});
+4 -2
View File
@@ -3,14 +3,16 @@ const path = require('path');
const fs = require('fs');
const which = require('which');
const getPort = (...args) => import('get-port').then(m => m.default(...args));
const { platformDirName } = require('./runtime-utils');
async function launchTor() {
const socks = await getPort();
const control = await getPort();
const torBase = path.join(__dirname, '..', '..', 'voxvera', 'resources', 'tor');
let exe = path.join(torBase, process.platform,
const platformDir = platformDirName(process.platform);
let exe = path.join(torBase, platformDir,
process.platform === 'win32' ? 'tor.exe' : 'tor');
let obfs4 = path.join(torBase, process.platform,
let obfs4 = path.join(torBase, platformDir,
process.platform === 'win32' ? 'obfs4proxy.exe' : 'obfs4proxy');
const missing = p => {
+24
View File
@@ -182,6 +182,23 @@ if [ -d "$HOME/imports" ] && ls "$HOME/imports"/*.json &>/dev/null; then
fi
fi
# Clean generated files from existing site directories so rebuild-all
# regenerates them from the current template. Preserves:
# config.json — site content and settings
# .onionshare-session — Tor domain identity key
# server.pid — running process marker
# onionshare.log — current log (may be in use)
# Everything else is regenerated by build_assets().
if [ -d "$HOME/host" ]; then
for site_dir in "$HOME"/host/*/; do
[ -f "${site_dir}config.json" ] || continue
msg "Cleaning generated files in ${site_dir}..."
rm -f "${site_dir}index.html" 2>/dev/null || true
rm -f "${site_dir}qrcode-content.png" "${site_dir}qrcode-tear-offs.png" 2>/dev/null || true
rm -rf "${site_dir}download" 2>/dev/null || true
done
fi
# --- Helper: build pip flags for this distro ---
pip_flags() {
local flags="--user --upgrade"
@@ -364,6 +381,13 @@ for strategy in install_tarball install_shallow install_pipx install_pip_git ins
fi
# Clear bash's cached command path so the new binary is found immediately
hash -r 2>/dev/null || true
# Rebuild all existing sites so they pick up template/CSS changes
if command_exists voxvera; then
msg "Rebuilding existing sites with updated template..."
voxvera rebuild-all 2>/dev/null || true
fi
msg "\nVoxVera installed/updated successfully. Run 'voxvera check' to verify."
# Detect if the parent shell will still have a stale hash
msg "If 'voxvera' is not found, run: hash -r (or open a new terminal)"
+1 -1
View File
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
[project]
name = "voxvera"
version = "0.3.2"
version = "0.3.4"
description = "VoxVera CLI utilities"
readme = "README.md"
authors = [{name = "VoxVera"}]
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
echo "==> flake8"
python3 -m flake8 voxvera tests \
--jobs=1 \
--exclude=voxvera/vendor \
--ignore=E501,W503,W293,E402,E306,E704,E261,F841,E302,F401,E401,F541
echo "==> pytest"
python3 -m pytest tests/ -v
echo "==> electron smoke tests"
(cd gui/electron && npm test)
echo "==> dependency check"
python3 -m voxvera.cli --lang en check
echo "==> build docs"
python3 -m voxvera.cli build-docs
echo "==> build site"
python3 -m voxvera.cli build-site
echo "All local checks passed."
Binary file not shown.
Binary file not shown.
+15
View File
@@ -126,6 +126,21 @@ def test_import_preserves_session(tmp_path, monkeypatch):
assert (host_dir / "index.html").exists()
def test_import_rejects_zip_path_traversal(tmp_path, monkeypatch, capsys):
_setup_tmp(monkeypatch, tmp_path)
malicious_zip = tmp_path / "malicious.zip"
with zipfile.ZipFile(malicious_zip, "w") as zf:
zf.writestr("../escape.txt", "owned")
zf.writestr("config.json", json.dumps({"folder_name": "evil"}))
cli.import_site(str(malicious_zip))
captured = capsys.readouterr()
assert "Refusing to extract path outside destination" in captured.out
assert not (tmp_path / "escape.txt").exists()
assert not ((tmp_path / "data" / "host" / "evil").exists())
def test_check_all_present(capsys, monkeypatch):
monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/bin/" + cmd)
cli.main(["check"])
+1 -1
View File
@@ -1,7 +1,7 @@
import sys
from pathlib import Path
__version__ = "0.3.2"
__version__ = "0.3.4"
# Add the vendor directory to sys.path ONLY for PyInstaller (frozen) builds.
# For pip/pipx installs the system/venv packages are correct and the vendored
+114 -2
View File
@@ -28,6 +28,8 @@ from InquirerPy import prompt, inquirer
from rich.console import Console
from voxvera import __version__
GITHUB_RECOMMENDED_BINARY_LIMIT = 95 * 1024 * 1024
# package root (contains bundled templates and src/)
if getattr(sys, 'frozen', False):
ROOT = Path(sys._MEIPASS).joinpath("voxvera")
@@ -731,6 +733,12 @@ def serve(config_path: str) -> str | None:
if not dir_path.is_dir():
print(f"Directory {dir_path} not found", file=sys.stderr)
sys.exit(1)
# Auto-rebuild if index.html is missing (e.g. after install cleanup)
if not (dir_path / "index.html").exists():
print(f"Rebuilding {folder_name} from current template...")
build_assets(config_path)
logfile = dir_path / "onionshare.log"
# Set environment variables for OnionShare to use the detected Tor ports
@@ -847,10 +855,24 @@ def _clear_host_dir(dir_path: Path):
session.write_bytes(saved)
import base64
import zipfile
import tempfile
def _safe_extract_zip(zipf: zipfile.ZipFile, dest_dir: Path) -> None:
"""Extract a ZIP archive without allowing path traversal."""
dest_dir = dest_dir.resolve()
for member in zipf.infolist():
member_path = Path(member.filename)
if member_path.is_absolute():
raise ValueError(f"Refusing to extract absolute path from archive: {member.filename}")
target_path = (dest_dir / member_path).resolve()
if not str(target_path).startswith(str(dest_dir) + os.sep) and target_path != dest_dir:
raise ValueError(f"Refusing to extract path outside destination: {member.filename}")
zipf.extractall(dest_dir)
def get_export_dir() -> Path:
"""Get the default export directory (~/voxvera-exports)."""
export_dir = Path.home() / "voxvera-exports"
@@ -904,7 +926,11 @@ def import_site(zip_path: str):
with tempfile.TemporaryDirectory() as tmpdir:
with zipfile.ZipFile(zip_path, 'r') as zipf:
zipf.extractall(tmpdir)
try:
_safe_extract_zip(zipf, Path(tmpdir))
except ValueError as e:
print(f"Error: Invalid export file. {e}")
return
tmp_path = Path(tmpdir)
config_file = tmp_path / "config.json"
@@ -954,6 +980,58 @@ def import_multiple_sites(source_dir: str = None):
print("\nImport multiple complete.")
def export_keys(folder_name: str):
"""Export the onion service session key as base64 for easy copy/paste."""
console = Console()
session_file = DATA_DIR / "host" / folder_name / ".onionshare-session"
if not session_file.exists():
console.print(f"[red]{t('cli.keys_not_found', name=folder_name)}[/red]")
return
raw = session_file.read_bytes()
encoded = base64.b64encode(raw).decode("ascii")
console.print(f"\n[bold]{t('cli.keys_export_header', name=folder_name)}[/bold]")
console.print("[yellow]" + "─" * 60 + "[/yellow]")
console.print(encoded)
console.print("[yellow]" + "─" * 60 + "[/yellow]")
console.print(f"[dim]{t('cli.keys_export_hint')}[/dim]\n")
def import_keys(folder_name: str):
"""Import an onion service session key from base64 pasted by the user."""
console = Console()
site_dir = DATA_DIR / "host" / folder_name
if not site_dir.exists():
console.print(f"[red]{t('cli.keys_site_not_found', name=folder_name)}[/red]")
return
session_file = site_dir / ".onionshare-session"
if session_file.exists():
overwrite = inquirer.confirm(message=t("cli.keys_overwrite_confirm", name=folder_name), default=False).execute()
if not overwrite:
console.print(f"[yellow]{t('cli.keys_import_cancelled')}[/yellow]")
return
console.print(f"\n[bold]{t('cli.keys_import_prompt')}[/bold]")
try:
raw_input = input("> ").strip()
except (KeyboardInterrupt, EOFError):
console.print(f"\n[yellow]{t('cli.keys_import_cancelled')}[/yellow]")
return
if not raw_input:
console.print(f"[red]{t('cli.keys_import_empty')}[/red]")
return
try:
decoded = base64.b64decode(raw_input)
except Exception:
console.print(f"[red]{t('cli.keys_import_invalid')}[/red]")
return
session_file.write_bytes(decoded)
console.print(f"[green]{t('cli.keys_import_success', name=folder_name)}[/green]")
def build_docs():
"""Generate localized documentation from templates and locale data."""
if getattr(sys, 'frozen', False):
@@ -1126,11 +1204,15 @@ def build_site():
# Bundle source version
bundle_source(download_dir / "voxvera-source.zip")
# Copy standalone binaries if they exist
# Copy standalone binaries if they exist, but avoid writing oversized
# artifacts into the tracked site tree where GitHub rejects >100 MB files.
bin_dir = ROOT / "resources" / "bin"
if bin_dir.exists():
for bin_file in bin_dir.iterdir():
if bin_file.is_file():
if bin_file.stat().st_size > GITHUB_RECOMMENDED_BINARY_LIMIT:
print(f"Skipping oversized site download artifact: {bin_file.name}")
continue
shutil.copy(bin_file, download_dir / bin_file.name)
print("site/ directory successfully synchronized.")
@@ -1259,6 +1341,27 @@ def _seed_bundled_sites():
shutil.copytree(src, dest)
def rebuild_all_sites():
"""Rebuild index.html for all existing sites from the current master template.
Preserves config.json, .onionshare-session keys, and download/ content.
Only regenerates the HTML and QR codes.
"""
servers = get_servers()
if not servers:
print("No sites found to rebuild.")
return
print(f"Rebuilding {len(servers)} site(s) from current template...")
for name in servers:
config_path = DATA_DIR / "host" / name / "config.json"
try:
build_assets(str(config_path))
print(f" Rebuilt: {name}")
except Exception as e:
print(f" Failed to rebuild {name}: {e}")
print("Rebuild complete.")
def get_servers() -> list[str]:
"""Return server names found in DATA_DIR/host only."""
host_dir = DATA_DIR / "host"
@@ -1597,6 +1700,8 @@ def manage_servers():
Choice("toggle", t("cli.manage_action_toggle_stop") if running else t("cli.manage_action_toggle_start")),
Choice("edit", t("cli.manage_action_edit")),
Choice("export", t("cli.manage_action_export")),
Choice("export_keys", t("cli.manage_action_export_keys")),
Choice("import_keys", t("cli.manage_action_import_keys")),
Choice("delete", t("cli.manage_action_delete")),
Choice(None, t("cli.manage_action_back"))
]
@@ -1629,6 +1734,10 @@ def manage_servers():
console.print("\n[yellow]Update cancelled.[/yellow]")
elif action == "export":
export_site(selected)
elif action == "export_keys":
export_keys(selected)
elif action == "import_keys":
import_keys(selected)
elif action == "delete":
confirm = inquirer.confirm(message=t("cli.manage_delete_confirm", name=selected)).execute()
if confirm:
@@ -1733,6 +1842,7 @@ def main(argv=None):
sub.add_parser("start-all", help="Start all configured sites (non-interactive)")
sub.add_parser("stop-all", help="Stop all running sites (non-interactive)")
sub.add_parser("autostart", help="Install/remove a systemd user service to start sites on boot")
sub.add_parser("rebuild-all", help="Rebuild all existing sites from the current template (preserves keys and config)")
sub.add_parser("_internal_onionshare", help=argparse.SUPPRESS)
args, unknown_args = parser.parse_known_args(argv)
@@ -1832,6 +1942,8 @@ def main(argv=None):
build_docs()
elif args.command == "build-site":
build_site()
elif args.command == "rebuild-all":
rebuild_all_sites()
else:
parser.print_help()
+62 -58
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=850">
<title id="site-title">Vox Vera Printable Flyers</title>
<title id="site-title">{{name}}</title>
<style>
body {
font-family: 'Courier New', Courier, 'Nimbus Mono L', 'Liberation Mono', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans Devanagari', 'Noto Sans JP', 'Noto Sans SC', 'Noto Sans Arabic UI', monospace;
@@ -169,20 +169,21 @@
.content {
width: calc(8.5in - 3.75in);
padding: 20px;
padding: 15px 20px;
text-align: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
justify-content: flex-start;
page-break-inside: avoid;
overflow-wrap: break-word;
}
h1 {
margin: 0;
margin-bottom: 10px;
padding: 0;
font-size: 24px;
line-height: 1.2;
letter-spacing: 2px;
white-space: nowrap;
overflow: hidden;
@@ -192,7 +193,10 @@
.distribute {
font-size: 18px;
line-height: 1.2;
letter-spacing: 6px;
margin: 2px 0;
padding: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -204,8 +208,8 @@
}
.message {
height: 6in; /* Fixed height to prevent footer push-out */
margin-top: 10px;
flex: 1;
margin-top: 4px;
font-size: 14px;
line-height: 1.4;
text-align: left;
@@ -265,7 +269,7 @@
border: none;
border-top: 1px solid #000;
width: 100%;
margin: 10px 0;
margin: 4px 0;
}
.footer {
@@ -378,7 +382,7 @@
</style>
<script>
// VoxVera Multi-Language Engine
const locales = {"ar": {"meta": {"language_name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "flag": "\ud83c\uddf8\ud83c\udde6", "direction": "rtl"}, "web": {"tear_off_tor_browser": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0645\u062a\u0635\u0641\u062d Tor", "tear_off_desc": "\u0644\u0644\u0645\u0633\u0627\u0639\u062f\u0629 \u0641\u064a \u0646\u0634\u0631 \u0647\u0630\u0647 \u0627\u0644\u0631\u0633\u0627\u0644\u0629\u060c \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649", "tear_off_footer": "\u0627\u0646\u0642\u0631 \u0641\u0648\u0642 \u0627\u0644\u0632\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0644\u0637\u0628\u0627\u0639\u0629", "built_with": "\u0628\u0646\u064a \u0628\u0648\u0627\u0633\u0637\u0629", "print_button": "\u0627\u0637\u0628\u0639 \u0627\u0644\u0645\u0646\u0634\u0648\u0631", "download_button": "\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0623\u062f\u0627\u0629 \u0648\u0627\u0644\u0643\u0648\u062f", "download_attachment": "Download Attachment"}, "landing": {"title": "\u0633\u0631\u064a \u0644\u0644\u063a\u0627\u064a\u0629", "subtitle": "~~\u0644\u0627~~ \u0644\u0644\u0646\u0634\u0631", "headline": "\u0639\u0645\u0644\u064a\u0629 VOX VERA", "content": "\u062a\u062d\u0631\u0631 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629 \u0645\u0639 VoxVera. \u0623\u062f\u0627\u0629 \u062a\u0633\u0648\u064a\u0642 \u0639\u0635\u0627\u0628\u0627\u062a \u0645\u062c\u0647\u0648\u0644\u0629 \u0648\u0646\u0634\u0631 \u0631\u0633\u0627\u0626\u0644. \u0633\u0648\u0627\u0621 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0623\u0648 \u0641\u064a \u0627\u0644\u0639\u0627\u0644\u0645 \u0627\u0644\u0645\u0627\u062f\u064a\u060c \u064a\u0645\u0643\u0646\u0647\u0627 \u062a\u0645\u0643\u064a\u0646\u0643 \u0645\u0646 \u0646\u0634\u0631 \u0623\u0641\u0643\u0627\u0631\u0643 \u0628\u062c\u0631\u0623\u0629\u060c \u0645\u062d\u0645\u064a\u0629 \u0628\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0647\u0648\u064a\u0629 \u0627\u0644\u0643\u0627\u0645\u0644 (\u0625\u0630\u0627 \u0643\u0646\u062a \u062a\u0633\u062a\u0636\u064a\u0641 \u0639\u0628\u0631 Tor). \u0642\u0645 \u0628\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0643\u0648\u062f\u060c \u0648\u0635\u0645\u0645 \u0645\u0648\u0642\u0639 \u0645\u0646\u0634\u0648\u0631\u060c \u0648\u0627\u0633\u062a\u0636\u064a\u0641\u0647 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0648\u0642\u0645 \u0628\u062a\u0636\u062e\u064a\u0645 \u0631\u0633\u0627\u0644\u062a\u0643.<br><br>\u0627\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0642\u0648\u0629 \u0627\u0644\u0645\u064a\u0645\u064a\u0629 \u0644\u0645\u0634\u0627\u0631\u0643\u0629 \u0623\u0641\u0643\u0627\u0631\u0643 \u0641\u064a \u0645\u062f\u0631\u0633\u062a\u0643\u060c \u0623\u0648 \u0645\u0643\u0627\u0646 \u0639\u0645\u0644\u0643\u060c \u0623\u0648 \u0645\u062c\u062a\u0645\u0639\u0627\u062a\u0643 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a\u060c \u0623\u0648 \u062d\u062a\u0649 \u0639\u0644\u0649 \u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0639\u0627\u0644\u0645. \u062a\u0636\u0645\u0646 VoxVera \u062a\u0631\u062f\u062f \u0635\u062f\u0649 \u0631\u0633\u0627\u0644\u062a\u0643 \u0628\u0639\u064a\u062f\u0627\u064b \u0648\u0639\u0644\u0649 \u0646\u0637\u0627\u0642 \u0648\u0627\u0633\u0639\u060c \u0645\u0639 \u0623\u0642\u0633\u0627\u0645 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u0646\u0632\u0639 \u062a\u062a\u0645\u064a\u0632 \u0628\u0631\u0648\u0627\u0628\u0637 \u0641\u0631\u064a\u062f\u0629 \u0648\u0623\u0643\u0648\u0627\u062f QR \u0644\u0633\u0647\u0648\u0644\u0629 \u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u0637\u0628\u0627\u0639\u0629.<br><br>\u064a\u0645\u0643\u0646 \u0627\u0644\u062d\u0641\u0627\u0638 \u0639\u0644\u0649 \u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629. \u064a\u0645\u0643\u0646 \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u0645\u0646\u0634\u0648\u0631\u0627\u062a \u0639\u0628\u0631 \u0634\u0628\u0643\u0629 Tor\u060c \u0645\u0645\u0627 \u064a\u062d\u0645\u064a \u0627\u0644\u0645\u0636\u064a\u0641 \u0648\u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a\u0646 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629.<br><br>\u0627\u0646\u0636\u0645 \u0625\u0644\u064a\u0646\u0627 \u0641\u064a \u062b\u0648\u0631\u0629 \u062a\u0642\u062f\u0631 \u0627\u0644\u062d\u0642\u064a\u0642\u0629 \u0648\u0627\u0644\u0634\u0641\u0627\u0641\u064a\u0629. \u0645\u0639\u0627\u064b\u060c \u064a\u0645\u0643\u0646\u0646\u0627 \u0628\u0646\u0627\u0621 \u0634\u0628\u0643\u0629 \u0645\u0646 \u0627\u0644\u0645\u0648\u0627\u0637\u0646\u064a\u0646 \u0627\u0644\u0645\u0633\u062a\u0646\u064a\u0631\u064a\u0646 \u0627\u0644\u0630\u064a\u0646 \u0644\u0627 \u064a\u062e\u0634\u0648\u0646 \u0627\u0644\u062a\u062d\u062f\u062b.", "url_message": "\u0627\u062d\u0635\u0644 \u0639\u0644\u0649 VoxVera \u0648\u0627\u0628\u062f\u0623 \u0628\u0646\u0634\u0631 \u0631\u0633\u0627\u0644\u062a\u0643.", "name": "\u0645\u0646\u0634\u0648\u0631\u0627\u062a Vox Vera \u0627\u0644Line truncated
const locales = {{locales}}; // Injected at build time
function t(path, lang) {
const parts = path.split('.');
@@ -441,10 +445,10 @@
<div class="left-tear-offs">
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -453,10 +457,10 @@
<!-- Repeat for other tear-offs -->
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -464,10 +468,10 @@
</div>
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -475,10 +479,10 @@
</div>
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -486,10 +490,10 @@
</div>
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -497,10 +501,10 @@
</div>
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -508,10 +512,10 @@
</div>
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -519,10 +523,10 @@
</div>
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -530,10 +534,10 @@
</div>
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -541,10 +545,10 @@
</div>
<div class="tear-off">
<div class="tear-off-text">
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
<span data-i18n="web.tear_off_footer">click the button below to print</span>
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
</div>
<div class="qr-code">
<img src="qrcode-tear-offs.png" alt="QR Code">
@@ -552,32 +556,32 @@
</div>
</div>
<div class="content">
<h1 id="top-secret-title" data-i18n="landing.title">TOP SECRET</h1>
<div class="distribute" id="do-not-distribute" data-i18n="landing.subtitle">DO <span class="redacted">NOT</span> DISTRIBUTE</div>
<h1 id="operation-title-2" data-i18n="landing.headline">OPERATION VOX VERA</h1>
<h1 id="top-secret-title" data-i18n="landing.title">{{t_landing_title}}</h1>
<div class="distribute" id="do-not-distribute" data-i18n="landing.subtitle">{{t_landing_subtitle}}</div>
<h1 id="operation-title-2" data-i18n="landing.headline">{{t_landing_headline}}</h1>
<hr>
<div class="message">
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">Break free from censorship with VoxVera. An anonymous guerrilla marketing and message-spreading tool. Whether online or in the physical world, it can empower you to spread your ideas boldly, shielded by complete anonymity (if you host over Tor). Download the code, design a flyer site, host it online and amplify your message.<br><br>Use memetic power to share your ideas in your school, workplace, online communities, or even globally. VoxVera ensures your message resonates far and wide, with tear-off sections featuring unique URLs and QR codes for easy reprinting.<br><br>Privacy can be maintained. Flyers can be shared via the Tor network, protecting hoster and users from censorship.<br><br>Join us in a revolution that values truth and transparency. Together, we can build a network of informed citizens who are unafraid to speak out.</div>
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">{{t_landing_content}}</div>
<div class="qr-code-body">
<div class="qr-code-url">
<span class="url-message" data-i18n="landing.url_message">Get VoxVera and start spreading your message.</span><br><br>
<a id="qr-link-2" href="https://github.com/PR0M3TH3AN/VoxVera" target="_blank">https://github.com/PR0M3TH3AN/VoxVera</a>
<span class="url-message" data-i18n="landing.url_message">{{t_landing_url_message}}</span><br><br>
<a id="qr-link-2" href="{{url}}" target="_blank">{{url}}</a>
</div>
<img src="qrcode-content.png" alt="QR Code">
</div>
</div>
<hr>
<div class="footer">
<p class="credit"><span data-i18n="web.built_with">built with</span> <a href="https://github.com/PR0M3TH3AN/VoxVera">voxvera</a></p>
<p class="binary" id="binary-message">0110010 0101011 0110010 0111101 0110100</p>
<p class="credit"><span data-i18n="web.built_with">{{t_web_built_with}}</span> <a href="https://github.com/PR0M3TH3AN/VoxVera">voxvera</a></p>
<p class="binary" id="binary-message">{{binary_message}}</p>
</div>
</div>
</div>
<div class="button-bar">
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">Print this page</button>
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">{{t_web_print_button}}</button>
<a id="download-link" href="download/voxvera-source.zip" download>
<button class="download-button" data-i18n="web.download_button">Download Source Code</button>
<button class="download-button" data-i18n="web.download_button">{{t_web_download_button}}</button>
</a>
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
</div>
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- إنشاء من قالب ---",
"template_select": "اختر قالباً:",
"body_over_limit_warning": "تحذير: نص المحتوى يتجاوز 1000 حرف. قد يتم قطعه في المنشور."
"body_over_limit_warning": "تحذير: نص المحتوى يتجاوز 1000 حرف. قد يتم قطعه في المنشور.",
"manage_action_export_keys": "تصدير المفاتيح",
"manage_action_import_keys": "استيراد المفاتيح",
"keys_not_found": "لم يتم العثور على مفتاح جلسة لـ '{name}'. قم بتشغيل الموقع أولاً لتوليد المفاتيح.",
"keys_export_header": "مفتاح الجلسة لـ '{name}':",
"keys_export_hint": "انسخ هذا المفتاح واحفظه بأمان. استخدم 'استيراد المفاتيح' في نسخة أخرى للاستعادة.",
"keys_site_not_found": "الموقع '{name}' غير موجود.",
"keys_overwrite_confirm": "الموقع '{name}' لديه مفتاح بالفعل. هل تريد الكتابة فوقه؟",
"keys_import_cancelled": "تم إلغاء الاستيراد.",
"keys_import_prompt": "الصق مفتاح الجلسة base64 أدناه:",
"keys_import_empty": "لم يتم تقديم مفتاح.",
"keys_import_invalid": "تنسيق المفتاح غير صالح. يجب أن يكون سلسلة base64 صالحة.",
"keys_import_success": "تم استيراد مفتاح الجلسة لـ '{name}'. أعد تشغيل الموقع لاستخدام المفتاح الجديد."
},
"web": {
"tear_off_tor_browser": "استخدم متصفح Tor",
+13 -1
View File
@@ -76,7 +76,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- Aus Vorlage erstellen ---",
"template_select": "Wählen Sie eine Vorlage:",
"body_over_limit_warning": "Warnung: Der Textkörper überschreitet 1000 Zeichen. Er könnte auf dem Flyer abgeschnitten werden."
"body_over_limit_warning": "Warnung: Der Textkörper überschreitet 1000 Zeichen. Er könnte auf dem Flyer abgeschnitten werden.",
"manage_action_export_keys": "Schlüssel exportieren",
"manage_action_import_keys": "Schlüssel importieren",
"keys_not_found": "Kein Sitzungsschlüssel für '{name}' gefunden. Starten Sie die Seite zuerst, um Schlüssel zu generieren.",
"keys_export_header": "Sitzungsschlüssel für '{name}':",
"keys_export_hint": "Kopieren Sie diesen Schlüssel und bewahren Sie ihn sicher auf. Verwenden Sie 'Schlüssel importieren' auf einer anderen Instanz zur Wiederherstellung.",
"keys_site_not_found": "Seite '{name}' nicht gefunden.",
"keys_overwrite_confirm": "Seite '{name}' hat bereits einen Schlüssel. Überschreiben?",
"keys_import_cancelled": "Import abgebrochen.",
"keys_import_prompt": "Fügen Sie den base64-Sitzungsschlüssel unten ein:",
"keys_import_empty": "Kein Schlüssel angegeben.",
"keys_import_invalid": "Ungültiges Schlüsselformat. Muss eine gültige base64-Zeichenkette sein.",
"keys_import_success": "Sitzungsschlüssel für '{name}' importiert. Starten Sie die Seite neu, um den neuen Schlüssel zu verwenden."
},
"web": {
"tear_off_tor_browser": "Tor-Browser verwenden",
+13 -1
View File
@@ -76,7 +76,19 @@
"input_file_not_found": "Input file {path} does not exist.",
"manage_templates": "--- Create From Template ---",
"template_select": "Select a template:",
"body_over_limit_warning": "Warning: Body text exceeds 1000 characters. It may be cut off on the flyer."
"body_over_limit_warning": "Warning: Body text exceeds 1000 characters. It may be cut off on the flyer.",
"manage_action_export_keys": "Export Keys",
"manage_action_import_keys": "Import Keys",
"keys_not_found": "No session key found for '{name}'. Start the site first to generate keys.",
"keys_export_header": "Session key for '{name}':",
"keys_export_hint": "Copy this key and save it securely. Use 'Import Keys' on another instance to restore.",
"keys_site_not_found": "Site '{name}' not found.",
"keys_overwrite_confirm": "Site '{name}' already has a key. Overwrite?",
"keys_import_cancelled": "Import cancelled.",
"keys_import_prompt": "Paste the base64 session key below:",
"keys_import_empty": "No key provided.",
"keys_import_invalid": "Invalid key format. Must be a valid base64 string.",
"keys_import_success": "Session key imported for '{name}'. Restart the site to use the new key."
},
"web": {
"tear_off_tor_browser": "Use Tor Browser",
+13 -1
View File
@@ -76,7 +76,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- Crear Desde Plantilla ---",
"template_select": "Seleccione una plantilla:",
"body_over_limit_warning": "Advertencia: El texto del cuerpo supera los 1000 caracteres. Puede cortarse en el volante."
"body_over_limit_warning": "Advertencia: El texto del cuerpo supera los 1000 caracteres. Puede cortarse en el volante.",
"manage_action_export_keys": "Exportar Claves",
"manage_action_import_keys": "Importar Claves",
"keys_not_found": "No se encontró clave de sesión para '{name}'. Inicie el sitio primero para generar claves.",
"keys_export_header": "Clave de sesión para '{name}':",
"keys_export_hint": "Copie esta clave y guárdela de forma segura. Use 'Importar Claves' en otra instancia para restaurar.",
"keys_site_not_found": "Sitio '{name}' no encontrado.",
"keys_overwrite_confirm": "El sitio '{name}' ya tiene una clave. ¿Sobrescribir?",
"keys_import_cancelled": "Importación cancelada.",
"keys_import_prompt": "Pegue la clave de sesión base64 a continuación:",
"keys_import_empty": "No se proporcionó clave.",
"keys_import_invalid": "Formato de clave no válido. Debe ser una cadena base64 válida.",
"keys_import_success": "Clave de sesión importada para '{name}'. Reinicie el sitio para usar la nueva clave."
},
"web": {
"tear_off_tor_browser": "Use el Navegador Tor",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- ایجاد از قالب ---",
"template_select": "یک قالب انتخاب کنید:",
"body_over_limit_warning": "هشدار: متن بدنه از 1000 کاراکتر بیشتر است. ممکن است در بروشور بریده شود."
"body_over_limit_warning": "هشدار: متن بدنه از 1000 کاراکتر بیشتر است. ممکن است در بروشور بریده شود.",
"manage_action_export_keys": "صادرات کلیدها",
"manage_action_import_keys": "واردات کلیدها",
"keys_not_found": "کلید جلسه برای '{name}' یافت نشد. ابتدا سایت را اجرا کنید تا کلیدها تولید شوند.",
"keys_export_header": "کلید جلسه برای '{name}':",
"keys_export_hint": "این کلید را کپی کرده و به صورت امن ذخیره کنید. از 'واردات کلیدها' در نمونه دیگر برای بازیابی استفاده کنید.",
"keys_site_not_found": "سایت '{name}' یافت نشد.",
"keys_overwrite_confirm": "سایت '{name}' از قبل کلید دارد. رونویسی شود؟",
"keys_import_cancelled": "واردات لغو شد.",
"keys_import_prompt": "کلید جلسه base64 را در زیر جایگذاری کنید:",
"keys_import_empty": "کلیدی ارائه نشد.",
"keys_import_invalid": "فرمت کلید نامعتبر است. باید یک رشته base64 معتبر باشد.",
"keys_import_success": "کلید جلسه برای '{name}' وارد شد. سایت را مجدداً راه‌اندازی کنید تا از کلید جدید استفاده شود."
},
"web": {
"tear_off_tor_browser": "از مرورگر Tor استفاده کنید",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- Créer à partir d'un modèle ---",
"template_select": "Sélectionnez un modèle :",
"body_over_limit_warning": "Avertissement : Le texte dépasse 1000 caractères. Il pourrait être coupé sur le tract."
"body_over_limit_warning": "Avertissement : Le texte dépasse 1000 caractères. Il pourrait être coupé sur le tract.",
"manage_action_export_keys": "Exporter les clés",
"manage_action_import_keys": "Importer les clés",
"keys_not_found": "Aucune clé de session trouvée pour '{name}'. Démarrez le site d'abord pour générer les clés.",
"keys_export_header": "Clé de session pour '{name}' :",
"keys_export_hint": "Copiez cette clé et conservez-la en sécurité. Utilisez 'Importer les clés' sur une autre instance pour restaurer.",
"keys_site_not_found": "Site '{name}' introuvable.",
"keys_overwrite_confirm": "Le site '{name}' a déjà une clé. Écraser ?",
"keys_import_cancelled": "Importation annulée.",
"keys_import_prompt": "Collez la clé de session base64 ci-dessous :",
"keys_import_empty": "Aucune clé fournie.",
"keys_import_invalid": "Format de clé invalide. Doit être une chaîne base64 valide.",
"keys_import_success": "Clé de session importée pour '{name}'. Redémarrez le site pour utiliser la nouvelle clé."
},
"web": {
"tear_off_tor_browser": "Utilisez le navigateur Tor",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- יצירה מתבנית ---",
"template_select": "בחר תבנית:",
"body_over_limit_warning": "אזהרה: טקסט הגוף חורג מ-1000 תווים. הוא עלול להיחתך בעלון."
"body_over_limit_warning": "אזהרה: טקסט הגוף חורג מ-1000 תווים. הוא עלול להיחתך בעלון.",
"manage_action_export_keys": "ייצוא מפתחות",
"manage_action_import_keys": "ייבוא מפתחות",
"keys_not_found": "לא נמצא מפתח הפעלה עבור '{name}'. הפעל את האתר תחילה ליצירת מפתחות.",
"keys_export_header": "מפתח הפעלה עבור '{name}':",
"keys_export_hint": "העתק מפתח זה ושמור אותו בבטחה. השתמש ב'ייבוא מפתחות' במופע אחר לשחזור.",
"keys_site_not_found": "האתר '{name}' לא נמצא.",
"keys_overwrite_confirm": "לאתר '{name}' כבר יש מפתח. לדרוס?",
"keys_import_cancelled": "הייבוא בוטל.",
"keys_import_prompt": "הדבק את מפתח ההפעלה base64 למטה:",
"keys_import_empty": "לא סופק מפתח.",
"keys_import_invalid": "פורמט מפתח לא חוקי. חייב להיות מחרוזת base64 חוקית.",
"keys_import_success": "מפתח הפעלה יובא עבור '{name}'. הפעל מחדש את האתר כדי להשתמש במפתח החדש."
},
"web": {
"tear_off_tor_browser": "השתמש בדפדפן Tor",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- टेम्पलेट से बनाएं ---",
"template_select": "एक टेम्पलेट चुनें:",
"body_over_limit_warning": "चेतावनी: मुख्य पाठ 1000 अक्षरों से अधिक है। यह पर्चे पर कटा हुआ दिख सकता है।"
"body_over_limit_warning": "चेतावनी: मुख्य पाठ 1000 अक्षरों से अधिक है। यह पर्चे पर कटा हुआ दिख सकता है।",
"manage_action_export_keys": "कुंजियाँ निर्यात करें",
"manage_action_import_keys": "कुंजियाँ आयात करें",
"keys_not_found": "'{name}' के लिए सत्र कुंजी नहीं मिली। कुंजियाँ उत्पन्न करने के लिए पहले साइट शुरू करें।",
"keys_export_header": "'{name}' के लिए सत्र कुंजी:",
"keys_export_hint": "इस कुंजी को कॉपी करें और सुरक्षित रूप से सहेजें। पुनर्स्थापित करने के लिए किसी अन्य इंस्टेंस पर 'कुंजियाँ आयात करें' का उपयोग करें।",
"keys_site_not_found": "साइट '{name}' नहीं मिली।",
"keys_overwrite_confirm": "साइट '{name}' में पहले से कुंजी है। अधिलेखित करें?",
"keys_import_cancelled": "आयात रद्द किया गया।",
"keys_import_prompt": "नीचे base64 सत्र कुंजी चिपकाएँ:",
"keys_import_empty": "कोई कुंजी प्रदान नहीं की गई।",
"keys_import_invalid": "अमान्य कुंजी प्रारूप। एक वैध base64 स्ट्रिंग होनी चाहिए।",
"keys_import_success": "'{name}' के लिए सत्र कुंजी आयात की गई। नई कुंजी का उपयोग करने के लिए साइट को पुनः प्रारंभ करें।"
},
"web": {
"tear_off_tor_browser": "Tor Browser का उपयोग करें",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- テンプレートから作成 ---",
"template_select": "テンプレートを選択してください:",
"body_over_limit_warning": "警告:本文が1000文字を超えています。チラシ上で途切れる可能性があります。"
"body_over_limit_warning": "警告:本文が1000文字を超えています。チラシ上で途切れる可能性があります。",
"manage_action_export_keys": "鍵をエクスポート",
"manage_action_import_keys": "鍵をインポート",
"keys_not_found": "'{name}' のセッションキーが見つかりません。まずサイトを起動してキーを生成してください。",
"keys_export_header": "'{name}' のセッションキー:",
"keys_export_hint": "このキーをコピーして安全に保管してください。別のインスタンスで「鍵をインポート」を使用して復元できます。",
"keys_site_not_found": "サイト '{name}' が見つかりません。",
"keys_overwrite_confirm": "サイト '{name}' にはすでにキーがあります。上書きしますか?",
"keys_import_cancelled": "インポートがキャンセルされました。",
"keys_import_prompt": "base64セッションキーを以下に貼り付けてください:",
"keys_import_empty": "キーが提供されていません。",
"keys_import_invalid": "無効なキー形式です。有効なbase64文字列である必要があります。",
"keys_import_success": "'{name}' のセッションキーがインポートされました。新しいキーを使用するにはサイトを再起動してください。"
},
"web": {
"tear_off_tor_browser": "Tor Browserを使用してください",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- Criar a partir de Modelo ---",
"template_select": "Selecione um modelo:",
"body_over_limit_warning": "Aviso: O texto do corpo excede 1000 caracteres. Pode ser cortado no panfleto."
"body_over_limit_warning": "Aviso: O texto do corpo excede 1000 caracteres. Pode ser cortado no panfleto.",
"manage_action_export_keys": "Exportar Chaves",
"manage_action_import_keys": "Importar Chaves",
"keys_not_found": "Nenhuma chave de sessão encontrada para '{name}'. Inicie o site primeiro para gerar as chaves.",
"keys_export_header": "Chave de sessão para '{name}':",
"keys_export_hint": "Copie esta chave e guarde-a com segurança. Use 'Importar Chaves' em outra instância para restaurar.",
"keys_site_not_found": "Site '{name}' não encontrado.",
"keys_overwrite_confirm": "O site '{name}' já possui uma chave. Sobrescrever?",
"keys_import_cancelled": "Importação cancelada.",
"keys_import_prompt": "Cole a chave de sessão base64 abaixo:",
"keys_import_empty": "Nenhuma chave fornecida.",
"keys_import_invalid": "Formato de chave inválido. Deve ser uma string base64 válida.",
"keys_import_success": "Chave de sessão importada para '{name}'. Reinicie o site para usar a nova chave."
},
"web": {
"tear_off_tor_browser": "Use o Tor Browser",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- Создать из шаблона ---",
"template_select": "Выберите шаблон:",
"body_over_limit_warning": "Предупреждение: Текст превышает 1000 символов. Он может быть обрезан на листовке."
"body_over_limit_warning": "Предупреждение: Текст превышает 1000 символов. Он может быть обрезан на листовке.",
"manage_action_export_keys": "Экспорт ключей",
"manage_action_import_keys": "Импорт ключей",
"keys_not_found": "Сессионный ключ для '{name}' не найден. Сначала запустите сайт для генерации ключей.",
"keys_export_header": "Сессионный ключ для '{name}':",
"keys_export_hint": "Скопируйте этот ключ и сохраните его в безопасности. Используйте 'Импорт ключей' на другом экземпляре для восстановления.",
"keys_site_not_found": "Сайт '{name}' не найден.",
"keys_overwrite_confirm": "У сайта '{name}' уже есть ключ. Перезаписать?",
"keys_import_cancelled": "Импорт отменён.",
"keys_import_prompt": "Вставьте сессионный ключ base64 ниже:",
"keys_import_empty": "Ключ не предоставлен.",
"keys_import_invalid": "Неверный формат ключа. Должна быть допустимая строка base64.",
"keys_import_success": "Сессионный ключ импортирован для '{name}'. Перезапустите сайт для использования нового ключа."
},
"web": {
"tear_off_tor_browser": "Используйте Tor Browser",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- Tengeneza Kutoka Kiolezo ---",
"template_select": "Chagua kiolezo:",
"body_over_limit_warning": "Onyo: Maandishi ya mwili yanazidi herufi 1000. Yanaweza kukatwa kwenye kipeperushi."
"body_over_limit_warning": "Onyo: Maandishi ya mwili yanazidi herufi 1000. Yanaweza kukatwa kwenye kipeperushi.",
"manage_action_export_keys": "Hamisha Funguo",
"manage_action_import_keys": "Ingiza Funguo",
"keys_not_found": "Hakuna funguo ya kipindi iliyopatikana kwa '{name}'. Anzisha tovuti kwanza ili kuzalisha funguo.",
"keys_export_header": "Funguo ya kipindi kwa '{name}':",
"keys_export_hint": "Nakili funguo hii na uihifadhi kwa usalama. Tumia 'Ingiza Funguo' kwenye mfano mwingine kurejesha.",
"keys_site_not_found": "Tovuti '{name}' haijapatikana.",
"keys_overwrite_confirm": "Tovuti '{name}' tayari ina funguo. Andika juu yake?",
"keys_import_cancelled": "Uingizaji umeghairiwa.",
"keys_import_prompt": "Bandika funguo ya kipindi ya base64 hapa chini:",
"keys_import_empty": "Hakuna funguo iliyotolewa.",
"keys_import_invalid": "Muundo wa funguo si sahihi. Lazima iwe mfuatano halali wa base64.",
"keys_import_success": "Funguo ya kipindi imeingizwa kwa '{name}'. Anzisha upya tovuti ili kutumia funguo mpya."
},
"web": {
"tear_off_tor_browser": "Tumia Kivinjari cha Tor",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- Şablondan Oluştur ---",
"template_select": "Bir şablon seçin:",
"body_over_limit_warning": "Uyarı: Gövde metni 1000 karakteri aşıyor. El ilanında kesilebilir."
"body_over_limit_warning": "Uyarı: Gövde metni 1000 karakteri aşıyor. El ilanında kesilebilir.",
"manage_action_export_keys": "Anahtarları Dışa Aktar",
"manage_action_import_keys": "Anahtarları İçe Aktar",
"keys_not_found": "'{name}' için oturum anahtarı bulunamadı. Anahtarları oluşturmak için önce siteyi başlatın.",
"keys_export_header": "'{name}' için oturum anahtarı:",
"keys_export_hint": "Bu anahtarı kopyalayın ve güvenli bir şekilde saklayın. Geri yüklemek için başka bir örnekte 'Anahtarları İçe Aktar' kullanın.",
"keys_site_not_found": "Site '{name}' bulunamadı.",
"keys_overwrite_confirm": "Site '{name}' zaten bir anahtara sahip. Üzerine yazılsın mı?",
"keys_import_cancelled": "İçe aktarma iptal edildi.",
"keys_import_prompt": "Base64 oturum anahtarını aşağıya yapıştırın:",
"keys_import_empty": "Anahtar sağlanmadı.",
"keys_import_invalid": "Geçersiz anahtar biçimi. Geçerli bir base64 dizesi olmalıdır.",
"keys_import_success": "'{name}' için oturum anahtarı içe aktarıldı. Yeni anahtarı kullanmak için siteyi yeniden başlatın."
},
"web": {
"tear_off_tor_browser": "Tor Tarayıcı Kullanın",
+13 -1
View File
@@ -77,7 +77,19 @@
"manage_action_edit": "Edit",
"manage_templates": "--- 从模板创建 ---",
"template_select": "选择一个模板:",
"body_over_limit_warning": "警告:正文超过1000个字符。传单上可能会被截断。"
"body_over_limit_warning": "警告:正文超过1000个字符。传单上可能会被截断。",
"manage_action_export_keys": "导出密钥",
"manage_action_import_keys": "导入密钥",
"keys_not_found": "未找到 '{name}' 的会话密钥。请先启动站点以生成密钥。",
"keys_export_header": "'{name}' 的会话密钥:",
"keys_export_hint": "复制此密钥并安全保存。在另一个实例上使用\u201c导入密钥\u201d来恢复。",
"keys_site_not_found": "未找到站点 '{name}'。",
"keys_overwrite_confirm": "站点 '{name}' 已有密钥。是否覆盖?",
"keys_import_cancelled": "导入已取消。",
"keys_import_prompt": "请在下方粘贴 base64 会话密钥:",
"keys_import_empty": "未提供密钥。",
"keys_import_invalid": "密钥格式无效。必须是有效的 base64 字符串。",
"keys_import_success": "已导入 '{name}' 的会话密钥。请重启站点以使用新密钥。"
},
"web": {
"tear_off_tor_browser": "使用 Tor 浏览器",
+127 -10
View File
@@ -7,7 +7,7 @@
<title id="site-title">{{name}}</title>
<style>
body {
font-family: 'Courier New', Courier, monospace;
font-family: 'Courier New', Courier, 'Nimbus Mono L', 'Liberation Mono', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans Devanagari', 'Noto Sans JP', 'Noto Sans SC', 'Noto Sans Arabic UI', monospace;
margin: 0;
padding: 20px 0;
display: flex;
@@ -19,6 +19,45 @@
background-color: #ffffff;
}
/* RTL Layout Overrides */
[dir="rtl"] .container {
flex-direction: row-reverse;
}
[dir="rtl"] .left-tear-offs {
border-right: none;
border-left: 1px dashed #000;
}
[dir="rtl"] .tear-off {
flex-direction: row-reverse;
}
[dir="rtl"] .qr-code {
margin-left: 0;
margin-right: 10px;
}
[dir="rtl"] .qr-code-body {
flex-direction: row-reverse;
}
[dir="rtl"] .qr-code-url {
text-align: right;
margin-right: 0;
margin-left: 10px;
}
[dir="rtl"] .message {
text-align: right;
}
[dir="rtl"] .text-container {
text-align: right;
}
/* ... existing styles ... */
a {
color: #b80000;
text-decoration: none;
@@ -87,12 +126,14 @@
.tear-off {
display: flex;
height: 1in; /* Fixed height for consistency */
padding: 5px;
border: 1px dashed #000;
font-size: 10px;
box-sizing: border-box;
page-break-inside: avoid;
align-items: flex-start;
overflow: hidden;
}
.tear-off-text {
@@ -128,26 +169,38 @@
.content {
width: calc(8.5in - 3.75in);
padding: 20px;
padding: 15px 20px;
text-align: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
justify-content: flex-start;
page-break-inside: avoid;
overflow-wrap: break-word;
}
h1 {
margin: 0;
margin-bottom: 10px;
padding: 0;
font-size: 24px;
line-height: 1.2;
letter-spacing: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.distribute {
font-size: 18px;
line-height: 1.2;
letter-spacing: 6px;
margin: 2px 0;
padding: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.redacted {
@@ -156,14 +209,14 @@
.message {
flex: 1;
margin-top: 10px;
margin-top: 4px;
font-size: 14px;
line-height: 1.4;
text-align: left;
overflow-wrap: break-word;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-end;
justify-content: flex-start;
white-space: pre-wrap;
box-sizing: border-box;
position: relative;
@@ -216,7 +269,7 @@
border: none;
border-top: 1px solid #000;
width: 100%;
margin: 10px 0;
margin: 4px 0;
}
.footer {
@@ -281,6 +334,51 @@
padding: 20px 0 30px;
}
/* Dropdown styles */
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
bottom: 100%;
background-color: #f9f9f9;
min-width: 200px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
border: 1px solid #000;
margin-bottom: 5px;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
border-bottom: 1px solid #eee;
}
.dropdown-content a:last-child {
border-bottom: none;
}
.dropdown-content a:hover {
background-color: #eee;
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropdown:hover .download-button {
background-color: #333;
color: #fff;
}
</style>
<script>
// VoxVera Multi-Language Engine
@@ -299,9 +397,25 @@
function setLanguage(lang) {
if (!locales[lang]) lang = 'en';
localStorage.setItem('voxvera_lang', lang);
// Set text direction
const direction = locales[lang].meta.direction || 'ltr';
document.documentElement.dir = direction;
document.documentElement.lang = lang;
document.querySelectorAll('[data-i18n]').forEach(el => {
const path = el.getAttribute('data-i18n');
el.innerText = t(path, lang);
let translation = t(path, lang);
if (translation) {
// Support Markdown-style redaction ~~text~~ -> <span class="redacted">text</span>
translation = translation.replace(/~~(.*?)~~/g, '<span class="redacted">$1</span>');
if (path.startsWith('landing.') || path.includes('subtitle') || path.includes('content')) {
el.innerHTML = translation;
} else {
el.innerText = translation;
}
}
});
document.getElementById('lang-selector').value = lang;
document.documentElement.lang = lang;
@@ -449,6 +563,7 @@
<div class="message">
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">{{t_landing_content}}</div>
<div class="qr-code-body">
<div class="qr-code-url">
<span class="url-message" data-i18n="landing.url_message">{{t_landing_url_message}}</span><br><br>
<a id="qr-link-2" href="{{url}}" target="_blank">{{url}}</a>
@@ -465,7 +580,9 @@
</div>
<div class="button-bar">
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">{{t_web_print_button}}</button>
<a class="download-button" href="download/download.zip" download data-i18n="web.download_button">{{t_web_download_button}}</a>
<a id="download-link" href="download/voxvera-source.zip" download>
<button class="download-button" data-i18n="web.download_button">{{t_web_download_button}}</button>
</a>
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
</div>
</body>