From 8619261c7853452619a298e70491aa5850b5b260 Mon Sep 17 00:00:00 2001
From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
Date: Fri, 5 Jun 2026 08:51:18 -0400
Subject: [PATCH] Localize Nostr language controls
---
site/nostr/index.html | 11 +-
site/nostr/nostr-client.css | 14 ++
site/nostr/nostr-client.js | 403 ++++++++++++++++++++++++++++++++----
3 files changed, 389 insertions(+), 39 deletions(-)
diff --git a/site/nostr/index.html b/site/nostr/index.html
index 901a98e..f395690 100644
--- a/site/nostr/index.html
+++ b/site/nostr/index.html
@@ -12,10 +12,13 @@
VoxVera Nostr Client
Static editor and viewer for decentralized flyer source events.
-
+
+
+
+
diff --git a/site/nostr/nostr-client.css b/site/nostr/nostr-client.css
index 99f4521..0d9900e 100644
--- a/site/nostr/nostr-client.css
+++ b/site/nostr/nostr-client.css
@@ -79,6 +79,20 @@ button:hover {
border: 1px solid #000;
}
+.topbar-controls {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+}
+
+.topbar-controls select {
+ width: auto;
+ min-height: 40px;
+ max-width: 220px;
+}
+
.tab {
border: 0;
border-right: 1px solid #000;
diff --git a/site/nostr/nostr-client.js b/site/nostr/nostr-client.js
index a5871f3..fed15af 100644
--- a/site/nostr/nostr-client.js
+++ b/site/nostr/nostr-client.js
@@ -10,6 +10,7 @@
const UI_LANG_STORAGE_KEY = "voxvera_nostr_lang";
const LOCALES = window.VoxVeraLocales || {};
const FALLBACK_LANG = "en";
+ const LANGUAGE_SELECT_IDS = ["topbar-lang", "field-lang", "viewer-lang"];
const EVENT_KIND = 30078;
const FIELD_LIMITS = {
folder_name: 64,
@@ -45,36 +46,326 @@ Join us in a revolution that values truth and transparency. Together, we can bui
};
const UI_LABELS = {
"label-relays": "cli.init_links",
- "label-flyer-name": "cli.init_folder",
- "label-page-title": "cli.init_name",
"label-title": "cli.init_title",
"label-subtitle": "cli.init_subtitle",
"label-headline": "cli.init_headline",
"label-content": "cli.init_body",
"label-url-message": "cli.url_message_label",
"label-poster-url": "cli.url_label",
- "label-footer-message": "cli.footer_message_label"
+ "label-footer-message": "cli.footer_message_label",
+ "label-viewer-relays": "cli.init_links"
+ };
+ const NOSTR_TEXT_LABELS = {
+ "label-flyer-name": "flyer_name",
+ "label-language": "language",
+ "label-page-title": "page_title",
+ "label-event-id": "event_id"
};
const BUTTON_LABELS = {
- "preview-editor": "Preview",
- "export-event": "Export event JSON",
- "publish-event": "Sign and publish",
- "copy-viewer-config": "Copy config JSON",
- "print-preview": "Print preview",
- "open-viewer-controls": "Load Event",
- "viewer-print": "Print",
- "viewer-editor": "Editor",
- "close-viewer-controls": "Close",
- "generate-anon-identity": "Generate anonymous npub",
- "viewer-fetch-render": "Fetch and render"
+ "preview-editor": "preview",
+ "export-event": "export_event",
+ "publish-event": "publish_event",
+ "copy-viewer-config": "copy_config",
+ "print-preview": "print_preview",
+ "open-viewer-controls": "load_event",
+ "viewer-print": "print",
+ "viewer-editor": "editor",
+ "close-viewer-controls": "back",
+ "generate-anon-identity": "generate_npub",
+ "viewer-fetch-render": "fetch_render",
+ "editor-tab": "editor",
+ "viewer-tab": "viewer"
};
const HEADING_LABELS = {
- "editor-heading": "Editor",
- "viewer-heading": "Viewer",
- "published-event-heading": "Published Event",
- "normalized-config-heading": "Normalized Config",
- "preview-heading": "Preview"
+ "editor-heading": "editor",
+ "viewer-heading": "viewer",
+ "published-event-heading": "published_event",
+ "normalized-config-heading": "normalized_config",
+ "preview-heading": "preview"
};
+ const NOSTR_UI = {
+ ar: {
+ back: "رجوع",
+ copy_config: "نسخ JSON للإعداد",
+ editor: "المحرر",
+ event_id: "معرّف الحدث",
+ export_event: "تصدير JSON الحدث",
+ fetch_render: "جلب وعرض",
+ flyer_name: "اسم المنشور",
+ generate_npub: "إنشاء npub مجهول",
+ language: "اللغة",
+ load_event: "تحميل الحدث",
+ normalized_config: "الإعداد الموحد",
+ page_title: "عنوان الصفحة",
+ preview: "معاينة",
+ print: "اطبع المنشور",
+ print_preview: "اطبع المعاينة",
+ publish_event: "توقيع ونشر",
+ published_event: "الحدث المنشور",
+ viewer: "العارض"
+ },
+ de: {
+ back: "Zurück",
+ copy_config: "Konfig-JSON kopieren",
+ editor: "Editor",
+ event_id: "Event-ID",
+ export_event: "Event-JSON exportieren",
+ fetch_render: "Abrufen und anzeigen",
+ flyer_name: "Flyername",
+ generate_npub: "Anonymes npub erzeugen",
+ language: "Sprache",
+ load_event: "Event laden",
+ normalized_config: "Normalisierte Konfiguration",
+ page_title: "Seitentitel",
+ preview: "Vorschau",
+ print: "Flyer drucken",
+ print_preview: "Vorschau drucken",
+ publish_event: "Signieren und veröffentlichen",
+ published_event: "Veröffentlichtes Event",
+ viewer: "Viewer"
+ },
+ en: {
+ back: "Back",
+ copy_config: "Copy config JSON",
+ editor: "Editor",
+ event_id: "Event ID",
+ export_event: "Export event JSON",
+ fetch_render: "Fetch and render",
+ flyer_name: "Flyer Name",
+ generate_npub: "Generate anonymous npub",
+ language: "Language",
+ load_event: "Load Event",
+ normalized_config: "Normalized Config",
+ page_title: "Page title",
+ preview: "Preview",
+ print: "Print this page",
+ print_preview: "Print preview",
+ publish_event: "Sign and publish",
+ published_event: "Published Event",
+ viewer: "Viewer"
+ },
+ es: {
+ back: "Volver",
+ copy_config: "Copiar JSON de config.",
+ editor: "Editor",
+ event_id: "ID del evento",
+ export_event: "Exportar JSON del evento",
+ fetch_render: "Buscar y renderizar",
+ flyer_name: "Nombre del volante",
+ generate_npub: "Generar npub anónimo",
+ language: "Idioma",
+ load_event: "Cargar evento",
+ normalized_config: "Configuración normalizada",
+ page_title: "Título de la página",
+ preview: "Vista previa",
+ print: "Imprimir volante",
+ print_preview: "Imprimir vista previa",
+ publish_event: "Firmar y publicar",
+ published_event: "Evento publicado",
+ viewer: "Visor"
+ },
+ fa: {
+ back: "بازگشت",
+ copy_config: "کپی JSON پیکربندی",
+ editor: "ویرایشگر",
+ event_id: "شناسه رویداد",
+ export_event: "خروجی JSON رویداد",
+ fetch_render: "دریافت و نمایش",
+ flyer_name: "نام اعلامیه",
+ generate_npub: "ساخت npub ناشناس",
+ language: "زبان",
+ load_event: "بارگذاری رویداد",
+ normalized_config: "پیکربندی عادیشده",
+ page_title: "عنوان صفحه",
+ preview: "پیشنمایش",
+ print: "چاپ اعلامیه",
+ print_preview: "چاپ پیشنمایش",
+ publish_event: "امضا و انتشار",
+ published_event: "رویداد منتشرشده",
+ viewer: "نمایشگر"
+ },
+ fr: {
+ back: "Retour",
+ copy_config: "Copier le JSON config",
+ editor: "Editeur",
+ event_id: "ID d'evenement",
+ export_event: "Exporter le JSON de l'evenement",
+ fetch_render: "Charger et afficher",
+ flyer_name: "Nom du flyer",
+ generate_npub: "Generer un npub anonyme",
+ language: "Langue",
+ load_event: "Charger l'evenement",
+ normalized_config: "Configuration normalisee",
+ page_title: "Titre de page",
+ preview: "Apercu",
+ print: "Imprimer le flyer",
+ print_preview: "Imprimer l'apercu",
+ publish_event: "Signer et publier",
+ published_event: "Evenement publie",
+ viewer: "Visionneuse"
+ },
+ he: {
+ back: "חזרה",
+ copy_config: "העתק JSON תצורה",
+ editor: "עורך",
+ event_id: "מזהה אירוע",
+ export_event: "ייצוא JSON אירוע",
+ fetch_render: "טען והצג",
+ flyer_name: "שם הפלייר",
+ generate_npub: "צור npub אנונימי",
+ language: "שפה",
+ load_event: "טען אירוע",
+ normalized_config: "תצורה מנורמלת",
+ page_title: "כותרת עמוד",
+ preview: "תצוגה מקדימה",
+ print: "הדפס פלייר",
+ print_preview: "הדפס תצוגה מקדימה",
+ publish_event: "חתום ופרסם",
+ published_event: "אירוע שפורסם",
+ viewer: "מציג"
+ },
+ hi: {
+ back: "पीछे",
+ copy_config: "कॉन्फिग JSON कॉपी करें",
+ editor: "संपादक",
+ event_id: "इवेंट ID",
+ export_event: "इवेंट JSON निर्यात करें",
+ fetch_render: "लाएं और दिखाएं",
+ flyer_name: "फ्लायर नाम",
+ generate_npub: "अनाम npub बनाएं",
+ language: "भाषा",
+ load_event: "इवेंट लोड करें",
+ normalized_config: "सामान्यीकृत कॉन्फिग",
+ page_title: "पृष्ठ शीर्षक",
+ preview: "पूर्वावलोकन",
+ print: "फ्लायर प्रिंट करें",
+ print_preview: "पूर्वावलोकन प्रिंट करें",
+ publish_event: "हस्ताक्षर कर प्रकाशित करें",
+ published_event: "प्रकाशित इवेंट",
+ viewer: "दर्शक"
+ },
+ ja: {
+ back: "戻る",
+ copy_config: "設定JSONをコピー",
+ editor: "エディター",
+ event_id: "イベントID",
+ export_event: "イベントJSONを書き出す",
+ fetch_render: "取得して表示",
+ flyer_name: "フライヤー名",
+ generate_npub: "匿名npubを生成",
+ language: "言語",
+ load_event: "イベントを読み込む",
+ normalized_config: "正規化設定",
+ page_title: "ページタイトル",
+ preview: "プレビュー",
+ print: "フライヤーを印刷",
+ print_preview: "プレビューを印刷",
+ publish_event: "署名して公開",
+ published_event: "公開済みイベント",
+ viewer: "ビューア"
+ },
+ pt: {
+ back: "Voltar",
+ copy_config: "Copiar JSON de config.",
+ editor: "Editor",
+ event_id: "ID do evento",
+ export_event: "Exportar JSON do evento",
+ fetch_render: "Buscar e renderizar",
+ flyer_name: "Nome do panfleto",
+ generate_npub: "Gerar npub anonimo",
+ language: "Idioma",
+ load_event: "Carregar evento",
+ normalized_config: "Configuracao normalizada",
+ page_title: "Titulo da pagina",
+ preview: "Previa",
+ print: "Imprimir Panfleto",
+ print_preview: "Imprimir previa",
+ publish_event: "Assinar e publicar",
+ published_event: "Evento publicado",
+ viewer: "Visualizador"
+ },
+ ru: {
+ back: "Назад",
+ copy_config: "Копировать JSON конфигурации",
+ editor: "Редактор",
+ event_id: "ID события",
+ export_event: "Экспорт JSON события",
+ fetch_render: "Загрузить и отобразить",
+ flyer_name: "Название листовки",
+ generate_npub: "Создать анонимный npub",
+ language: "Язык",
+ load_event: "Загрузить событие",
+ normalized_config: "Нормализованная конфигурация",
+ page_title: "Заголовок страницы",
+ preview: "Предпросмотр",
+ print: "Печать листовки",
+ print_preview: "Печать предпросмотра",
+ publish_event: "Подписать и опубликовать",
+ published_event: "Опубликованное событие",
+ viewer: "Просмотр"
+ },
+ sw: {
+ back: "Rudi",
+ copy_config: "Nakili JSON ya usanidi",
+ editor: "Kihariri",
+ event_id: "Kitambulisho cha tukio",
+ export_event: "Hamisha JSON ya tukio",
+ fetch_render: "Pakua na onyesha",
+ flyer_name: "Jina la kipeperushi",
+ generate_npub: "Tengeneza npub isiyojulikana",
+ language: "Lugha",
+ load_event: "Pakia tukio",
+ normalized_config: "Usanidi uliorekebishwa",
+ page_title: "Kichwa cha ukurasa",
+ preview: "Hakiki",
+ print: "Chapa Kipeperushi",
+ print_preview: "Chapa hakiki",
+ publish_event: "Saini na chapisha",
+ published_event: "Tukio lililochapishwa",
+ viewer: "Kitazamaji"
+ },
+ tr: {
+ back: "Geri",
+ copy_config: "Yapılandırma JSON'unu kopyala",
+ editor: "Düzenleyici",
+ event_id: "Olay ID",
+ export_event: "Olay JSON'unu dışa aktar",
+ fetch_render: "Getir ve göster",
+ flyer_name: "İlan adı",
+ generate_npub: "Anonim npub oluştur",
+ language: "Dil",
+ load_event: "Olay yükle",
+ normalized_config: "Normalleştirilmiş yapılandırma",
+ page_title: "Sayfa başlığı",
+ preview: "Önizleme",
+ print: "İlanı Yazdır",
+ print_preview: "Önizlemeyi yazdır",
+ publish_event: "İmzala ve yayınla",
+ published_event: "Yayınlanan olay",
+ viewer: "Görüntüleyici"
+ },
+ zh: {
+ back: "返回",
+ copy_config: "复制配置 JSON",
+ editor: "编辑器",
+ event_id: "事件 ID",
+ export_event: "导出事件 JSON",
+ fetch_render: "获取并渲染",
+ flyer_name: "传单名称",
+ generate_npub: "生成匿名 npub",
+ language: "语言",
+ load_event: "加载事件",
+ normalized_config: "标准化配置",
+ page_title: "页面标题",
+ preview: "预览",
+ print: "打印传单",
+ print_preview: "打印预览",
+ publish_event: "签名并发布",
+ published_event: "已发布事件",
+ viewer: "查看器"
+ }
+ };
+ let flyerSource = "default";
const el = (id) => document.getElementById(id);
@@ -91,6 +382,17 @@ Join us in a revolution that values truth and transparency. Together, we can bui
return path.split(".").reduce((value, key) => (value && value[key] !== undefined ? value[key] : undefined), data);
}
+ function htmlBreaksToNewlines(value) {
+ return String(value || "").replace(/
/gi, "\n");
+ }
+
+ function nostrLabel(key, lang) {
+ const selected = supportedLang(lang);
+ return (NOSTR_UI[selected] && NOSTR_UI[selected][key])
+ || (NOSTR_UI[FALLBACK_LANG] && NOSTR_UI[FALLBACK_LANG][key])
+ || key;
+ }
+
function localeDefaults(lang) {
const data = localeData(lang);
const landing = data.landing || {};
@@ -102,7 +404,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
title: landing.title || CONFIG_DEFAULTS.title,
subtitle: landing.subtitle || CONFIG_DEFAULTS.subtitle,
headline: landing.headline || CONFIG_DEFAULTS.headline,
- content: landing.content || CONFIG_DEFAULTS.content,
+ content: htmlBreaksToNewlines(landing.content || CONFIG_DEFAULTS.content),
url_message: landing.url_message || CONFIG_DEFAULTS.url_message,
url: CONFIG_DEFAULTS.url,
tear_off_link: "",
@@ -128,7 +430,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
const bName = (b[1] && b[1].meta && b[1].meta.language_name) || b[0];
return aName.localeCompare(bName);
});
- ["field-lang", "viewer-lang"].forEach((selectId) => {
+ LANGUAGE_SELECT_IDS.forEach((selectId) => {
const select = el(selectId);
if (!select) return;
select.innerHTML = "";
@@ -156,14 +458,15 @@ Join us in a revolution that values truth and transparency. Together, we can bui
const translated = translate(path, selected);
if (translated) setText(id, translated);
});
- Object.entries(HEADING_LABELS).forEach(([id, value]) => setText(id, value));
- Object.entries(BUTTON_LABELS).forEach(([id, value]) => setButtonText(id, value));
+ Object.entries(NOSTR_TEXT_LABELS).forEach(([id, key]) => setText(id, nostrLabel(key, selected)));
+ Object.entries(HEADING_LABELS).forEach(([id, key]) => setText(id, nostrLabel(key, selected)));
+ Object.entries(BUTTON_LABELS).forEach(([id, key]) => setButtonText(id, nostrLabel(key, selected)));
setButtonText("print-preview", translate("web.print_button", selected) || BUTTON_LABELS["print-preview"]);
setButtonText("viewer-print", translate("web.print_button", selected) || BUTTON_LABELS["viewer-print"]);
setButtonText("viewer-editor", translate("cli.manage_action_edit", selected) || BUTTON_LABELS["viewer-editor"]);
setButtonText("close-viewer-controls", translate("cli.manage_action_back", selected) || BUTTON_LABELS["close-viewer-controls"]);
- ["field-lang", "viewer-lang"].forEach((selectId) => {
+ LANGUAGE_SELECT_IDS.forEach((selectId) => {
const select = el(selectId);
if (select) select.value = selected;
});
@@ -192,6 +495,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
function applyFlyerDefaults(lang) {
const defaults = localeDefaults(lang);
+ flyerSource = "default";
el("field-lang").value = defaults.lang;
el("field-folder-name").value = defaults.folder_name;
el("field-name").value = defaults.name;
@@ -205,6 +509,19 @@ Join us in a revolution that values truth and transparency. Together, we can bui
syncUiLanguage(defaults.lang);
}
+ function applyLanguageChange(lang) {
+ const selected = supportedLang(lang);
+ if (flyerSource === "default") {
+ applyFlyerDefaults(selected);
+ renderPreview(buildPayloadFromForm());
+ return;
+ }
+
+ el("field-lang").value = selected;
+ syncUiLanguage(selected);
+ renderPreview(normalizePayload(buildPayloadFromForm()));
+ }
+
function refreshSignerState() {
const state = el("signer-state");
if (window.nostr && typeof window.nostr.signEvent === "function") {
@@ -316,7 +633,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
}
function redactionToHtml(value) {
- return escapeHtml(value).replace(/~~(.*?)~~/g, '$1');
+ return escapeHtml(value)
+ .replace(/~~(.*?)~~/g, '$1')
+ .replace(/\r?\n/g, "
");
}
function buildPayloadFromForm() {
@@ -351,7 +670,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
}
});
Object.entries(payload).forEach(([key, value]) => {
- if (typeof value === "string" && key !== "url" && /<\s*\/?\s*[a-z][^>]*>|on[a-z]+\s*=/i.test(value)) {
+ const htmlCheckValue = typeof value === "string" ? value.replace(/
/gi, "") : value;
+ if (typeof value === "string" && key !== "url" && /<\s*\/?\s*[a-z][^>]*>|on[a-z]+\s*=/i.test(htmlCheckValue)) {
throw new Error(`${key} contains raw HTML.`);
}
});
@@ -374,7 +694,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
title: payload.title || CONFIG_DEFAULTS.title,
subtitle: payload.subtitle || CONFIG_DEFAULTS.subtitle,
headline: payload.headline || CONFIG_DEFAULTS.headline,
- content: payload.content || CONFIG_DEFAULTS.content,
+ content: htmlBreaksToNewlines(payload.content || CONFIG_DEFAULTS.content),
url_message: payload.url_message || CONFIG_DEFAULTS.url_message,
url: payload.url || CONFIG_DEFAULTS.url,
footer_message: payload.footer_message || CONFIG_DEFAULTS.footer_message,
@@ -793,6 +1113,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
: await fetchEvent(parsed.id, relays);
const payload = payloadFromEvent(nostrEvent);
const config = normalizePayload(payload);
+ flyerSource = "event";
syncUiLanguage(config.lang);
el("field-lang").value = config.lang;
el("viewer-config-output").value = JSON.stringify(config, null, 2);
@@ -819,15 +1140,27 @@ Join us in a revolution that values truth and transparency. Together, we can bui
button.addEventListener("click", () => setMode(button.dataset.mode));
});
- el("field-lang").addEventListener("change", () => {
- const lang = supportedLang(el("field-lang").value || FALLBACK_LANG);
- applyFlyerDefaults(lang);
- renderPreview(buildPayloadFromForm());
+ LANGUAGE_SELECT_IDS.forEach((selectId) => {
+ const select = el(selectId);
+ if (!select) return;
+ select.addEventListener("change", () => applyLanguageChange(select.value || FALLBACK_LANG));
});
- el("viewer-lang").addEventListener("change", () => {
- const lang = supportedLang(el("viewer-lang").value || FALLBACK_LANG);
- applyFlyerDefaults(lang);
- renderPreview(buildPayloadFromForm());
+
+ [
+ "field-folder-name",
+ "field-name",
+ "field-title",
+ "field-subtitle",
+ "field-headline",
+ "field-content",
+ "field-url-message",
+ "field-footer-message"
+ ].forEach((fieldId) => {
+ const field = el(fieldId);
+ if (!field) return;
+ field.addEventListener("input", () => {
+ flyerSource = "custom";
+ });
});
el("open-viewer-controls").addEventListener("click", openViewerDrawer);