Flag flyer language in Nostr events

This commit is contained in:
thePR0M3TH3AN committed 2026-06-05 13:33:18 -04:00
1 parent 5afda54a21
commit 1bda451604
4 files changed
+63 -2

No files matched your search

+11
View File
@@ -61,6 +61,14 @@ Required tags:
- `["t", "voxvera"]` - `["t", "voxvera"]`
- `["t", "flyer"]` - `["t", "flyer"]`
Recommended language tags:
- `["language", "<lang>"]`
- `["L", "ISO-639-1"]`
- `["l", "<lang>", "ISO-639-1"]`
The payload `lang` field is authoritative. The language tags exist for relay/search metadata and as a fallback for older events whose payload omitted `lang`.
Payload: Payload:
```json ```json
@@ -118,6 +126,9 @@ Nostr editor/viewer controls and Nostr-specific flyer labels come from `NOSTR_UI
Language changes should: Language changes should:
- publish the selected flyer language in payload `lang`
- publish matching Nostr language tags
- infer language from event tags if a fetched payload omits `lang`
- update default flyer content while the flyer is still using default content - update default flyer content while the flyer is still using default content
- preserve user-authored or fetched event content - preserve user-authored or fetched event content
- update editor/viewer tool labels - update editor/viewer tool labels
+15 -1
View File
@@ -751,6 +751,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
function buildUnsignedEvent(payload) { function buildUnsignedEvent(payload) {
const folderName = slugify(payload.folder_name); const folderName = slugify(payload.folder_name);
const lang = supportedLang(payload.lang || FALLBACK_LANG);
return { return {
kind: EVENT_KIND, kind: EVENT_KIND,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
@@ -759,7 +760,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
["t", "voxvera"], ["t", "voxvera"],
["t", "flyer"], ["t", "flyer"],
["title", payload.headline || payload.title || folderName], ["title", payload.headline || payload.title || folderName],
["language", payload.lang || "en"] ["language", lang],
["L", "ISO-639-1"],
["l", lang, "ISO-639-1"]
], ],
content: JSON.stringify(payload) content: JSON.stringify(payload)
}; };
@@ -1011,10 +1014,21 @@ Join us in a revolution that values truth and transparency. Together, we can bui
const hasTag = (name, value) => Array.isArray(event.tags) && event.tags.some((tag) => tag[0] === name && tag[1] === value); const hasTag = (name, value) => Array.isArray(event.tags) && event.tags.some((tag) => tag[0] === name && tag[1] === value);
if (!hasTag("t", "voxvera") || !hasTag("t", "flyer")) throw new Error("Event is missing VoxVera flyer tags."); if (!hasTag("t", "voxvera") || !hasTag("t", "flyer")) throw new Error("Event is missing VoxVera flyer tags.");
const payload = JSON.parse(event.content); const payload = JSON.parse(event.content);
const taggedLang = languageFromTags(event.tags);
if (!payload.lang && taggedLang) {
payload.lang = taggedLang;
}
validatePayload(payload); validatePayload(payload);
return payload; return payload;
} }
function languageFromTags(tags) {
if (!Array.isArray(tags)) return "";
const languageTag = tags.find((tag) => Array.isArray(tag) && tag[0] === "language" && tag[1]);
const labelTag = tags.find((tag) => Array.isArray(tag) && tag[0] === "l" && tag[1]);
return supportedLang((languageTag && languageTag[1]) || (labelTag && labelTag[1]) || "");
}
function makeQrSvg(value) { function makeQrSvg(value) {
if (!value) return ""; if (!value) return "";
if (typeof qrcode !== "function") { if (typeof qrcode !== "function") {
+23 -1
View File
@@ -29,7 +29,14 @@ def _event(payload=None, **overrides):
event = { event = {
"kind": 30078, "kind": 30078,
"content": json.dumps(payload or _payload()), "content": json.dumps(payload or _payload()),
"tags": [["d", "voxvera:nostr-flyer"], ["t", "voxvera"], ["t", "flyer"]], "tags": [
["d", "voxvera:nostr-flyer"],
["t", "voxvera"],
["t", "flyer"],
["language", "en"],
["L", "ISO-639-1"],
["l", "en", "ISO-639-1"],
],
} }
event.update(overrides) event.update(overrides)
return event return event
@@ -43,6 +50,21 @@ def test_validate_accepts_nostr_event_payload():
assert validate_event_source(_event())["content"] == "This flyer came from a Nostr event." assert validate_event_source(_event())["content"] == "This flyer came from a Nostr event."
def test_validate_uses_language_tag_when_payload_lang_is_missing():
payload = _payload()
payload.pop("lang")
event = _event(payload, tags=[
["d", "voxvera:nostr-flyer"],
["t", "voxvera"],
["t", "flyer"],
["language", "es"],
["L", "ISO-639-1"],
["l", "es", "ISO-639-1"],
])
assert validate_event_source(event)["lang"] == "es"
def test_normalize_maps_to_flyer_config_defaults(): def test_normalize_maps_to_flyer_config_defaults():
defaults = { defaults = {
"folder_name": "voxvera", "folder_name": "voxvera",
+14
View File
@@ -137,6 +137,10 @@ def _extract_payload(source: dict[str, Any]) -> dict[str, Any]:
raise NostrValidationError(f"Nostr event content is not valid JSON: {exc.msg}") from exc raise NostrValidationError(f"Nostr event content is not valid JSON: {exc.msg}") from exc
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise NostrValidationError("Nostr event content must decode to a JSON object.") raise NostrValidationError("Nostr event content must decode to a JSON object.")
if not payload.get("lang"):
tagged_lang = _language_from_tags(source.get("tags", []))
if tagged_lang:
payload["lang"] = tagged_lang
return payload return payload
@@ -182,6 +186,16 @@ def _has_tag(source: dict[str, Any], name: str, value: str) -> bool:
return any(isinstance(tag, list) and len(tag) >= 2 and tag[0] == name and tag[1] == value for tag in tags) return any(isinstance(tag, list) and len(tag) >= 2 and tag[0] == name and tag[1] == value for tag in tags)
def _language_from_tags(tags: Any) -> str:
if not isinstance(tags, list):
return ""
for tag_name in ("language", "l"):
for tag in tags:
if isinstance(tag, list) and len(tag) >= 2 and tag[0] == tag_name:
return _normalize_lang(str(tag[1]))
return ""
def _normalize_lang(lang: str) -> str: def _normalize_lang(lang: str) -> str:
return lang if lang in SUPPORTED_LANGS else "en" return lang if lang in SUPPORTED_LANGS else "en"