mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
Locale-aware defaults: browser language list + US Letter/A4 paper size
Language inference: - Walk navigator.languages (the full ordered preference list), not just navigator.language, and pick the first base language we support. A user who prefers e.g. Dutch then German now gets German instead of the English fallback. De-duplicates the default-language resolution into setDefaultText. Paper size (US Letter + A4): - Add A4 (210x297mm) alongside US Letter (8.5x11in). Sheet dimensions move to CSS custom properties (--sheet-w/--sheet-h) toggled by a paper-letter/paper-a4 class on <html>; the tear-off column and QR codes stay fixed physical sizes and the content column flexes. - @page size is injected by JS (id voxvera-page-size) since @page cannot read CSS variables across engines; the print block keeps margin:0 and a Letter default for the pre-JS state. - Default paper is chosen from the browser locale's region subtag (en-US -> Letter, en-GB/de-DE -> A4) — "by location" without geolocation, falling back to Letter when no region is present. A manual override selector (topbar + viewer controls) persists in localStorage and wins over the default. - Paper size is a local print/view preference and is never written into the Nostr event, so a flyer reprints on either size unchanged. - Add paper_size/paper_letter/paper_a4 labels for all 14 languages. Docs + tests: - Spec gains a "Print paper size" section; AGENTS.md records the not-in-payload invariant; README adds a feature bullet; the field-limits cheat sheet gains Letter/A4 columns (re-measured per paper size). - Playwright: add tests for navigator.languages inference, region-based paper default (A4 and Letter), and override persistence. 54 tests pass across Chromium, Firefox, WebKit, and Android/iOS mobile emulations. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
c5b2c20fc9
commit
127f9c3134
8 files changed
+310
-32
No files matched your search
@@ -19,6 +19,7 @@
|
||||
- Treat Nostr event content as untrusted. Escape rendered content and reject unsafe schema input.
|
||||
- Preserve print behavior: app controls must not print or shift the flyer sheet in print output.
|
||||
- Preserve the old flyer visual language unless the user explicitly asks for a redesign.
|
||||
- Paper size (US Letter / A4) is a local print/view preference (`localStorage`), defaulted from the browser locale region. Never write it into the Nostr event — a flyer must reprint on either size without changing the source event.
|
||||
|
||||
## Localization
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ The previous Tor/OnionShare CLI version is preserved on the `legacy-tor` branch.
|
||||
- **Tear-off tabs.** The printable flyer has ten tear-off tabs. Each tab is a single hyperlinked call to action (the URL is the link, not printed as text) and prints the full event ID so anyone can re-find the flyer by typing it.
|
||||
- **Loading state on shared links.** Opening a poster/event link shows a localized "Loading content…" placeholder (and a "Could not load this flyer." state on failure) instead of the default flyer; the default sample content appears only on the bare site URL.
|
||||
- **14 languages with RTL support.** Flyer content and UI are localized; right-to-left scripts render correctly. Letter-spacing is applied per script so cursive (Arabic/Persian) and combining (Devanagari) scripts are not broken — see [the spec](docs/nostr-static-client-spec.md#letter-spacing-by-script).
|
||||
- **Fits-the-page editing.** Fields that would overflow the printable sheet are rejected as you type, with an inline error under the affected field. See the [field length cheat sheet](docs/flyer-field-limits.md) for how much text fits each field per language.
|
||||
- **US Letter and A4.** Print size defaults to the right paper for the visitor's country (from the browser locale's region — `en-US` → Letter, `en-GB` → A4, no geolocation), with a manual override. Paper size is a local print preference and is never written into the event. See the [spec](docs/nostr-static-client-spec.md#print-paper-size).
|
||||
- **Fits-the-page editing.** Fields that would overflow the printable sheet are rejected as you type, with an inline error under the affected field. See the [field length cheat sheet](docs/flyer-field-limits.md) for how much text fits each field per language and paper size.
|
||||
- **QR codes.** The tear-off QR points at the poster URL (re-open and reprint); the main flyer QR points at the creator's content URL.
|
||||
- **Self-contained.** No build step and no CDNs — Nostr and QR libraries are vendored under `site/vendor/`.
|
||||
|
||||
|
||||
+18
-14
@@ -9,7 +9,8 @@ So the real limit is enforced for you as you type — the numbers below are
|
||||
practical guidance for drafting copy, sizing translations, or prompting an LLM
|
||||
to write flyer text that will fit on the first try.
|
||||
|
||||
Capacity varies by **script**, not by individual language, for three reasons:
|
||||
Capacity varies by **script** (not by individual language) and by **paper
|
||||
size**, for these reasons:
|
||||
|
||||
1. The flyer body font is monospace (`Courier New`), so within a script every
|
||||
character takes the same width regardless of upper/lowercase.
|
||||
@@ -17,19 +18,22 @@ Capacity varies by **script**, not by individual language, for three reasons:
|
||||
[`nostr-static-client-spec.md`](nostr-static-client-spec.md#letter-spacing-by-script)).
|
||||
3. Glyph width differs by script — CJK glyphs are full-width (≈2× a Latin
|
||||
character), Devanagari fallback glyphs are narrower, etc.
|
||||
4. **Paper size.** A4 (210×297mm) is slightly narrower and taller than US
|
||||
Letter (8.5×11in), so single-line fields hold ~2 fewer characters on A4
|
||||
while the body holds a little more. Both sizes are shown below.
|
||||
|
||||
## Single-line fields (hard caps)
|
||||
|
||||
**Title**, **Subtitle**, and **Headline** are single-line: they never wrap, so
|
||||
these are the limits people actually run into. Counts are characters
|
||||
(codepoints).
|
||||
(codepoints), shown as **Letter / A4**.
|
||||
|
||||
| Languages | Title | Headline | Subtitle |
|
||||
|---|---|---|---|
|
||||
| Latin & Cyrillic & Hebrew — en, es, de, fr, pt, sw, tr, ru, he | ~28 | ~28 | ~32 |
|
||||
| Arabic & Persian — ar, fa | ~28 | ~28 | ~38 |
|
||||
| Devanagari — hi | ~36 | ~36 | ~51 |
|
||||
| CJK — ja, zh | ~17 | ~17 | ~23 |
|
||||
| Languages | Title & Headline | Subtitle |
|
||||
|---|---|---|
|
||||
| Latin & Cyrillic & Hebrew — en, es, de, fr, pt, sw, tr, ru, he | ~28 / ~26 | ~32 / ~30 |
|
||||
| Arabic & Persian — ar, fa | ~28 / ~27 | ~38 / ~36 |
|
||||
| Devanagari — hi | ~36 / ~35 | ~51 / ~48 |
|
||||
| CJK — ja, zh | ~17 / ~16 | ~23 / ~21 |
|
||||
|
||||
## Multi-line fields (generous)
|
||||
|
||||
@@ -37,12 +41,12 @@ these are the limits people actually run into. Counts are characters
|
||||
(`footer_message`) wrap across multiple lines, so they hold far more and are
|
||||
bounded only by the remaining height of the sheet.
|
||||
|
||||
| Languages | Body (content) |
|
||||
| Languages | Body (content) — Letter / A4 |
|
||||
|---|---|
|
||||
| Latin & Cyrillic & Hebrew | ~1,900 (a few hundred words) |
|
||||
| Arabic & Persian | ~1,850 |
|
||||
| Devanagari — hi | ~2,450 |
|
||||
| CJK — ja, zh | ~1,170 |
|
||||
| Latin & Cyrillic & Hebrew | ~1,900 / ~2,000 (a few hundred words) |
|
||||
| Arabic & Persian | ~1,850 / ~1,930 |
|
||||
| Devanagari — hi | ~2,450 / ~2,545 |
|
||||
| CJK — ja, zh | ~1,170 / ~1,220 |
|
||||
|
||||
`url_message` and `footer_message` share the lower portion of the sheet and can
|
||||
hold roughly 1,200–4,800 characters depending on script, but for a readable
|
||||
@@ -55,7 +59,7 @@ URLs scan more reliably).
|
||||
## Notes and caveats
|
||||
|
||||
- **Approximate.** These come from rendering each script in a headless browser
|
||||
at true print size (8.5in × 11in, content column 4.75in). The
|
||||
at true print size for both Letter (8.5×11in) and A4 (210×297mm). The
|
||||
Latin/Cyrillic numbers (`Courier New`) are the most reliable; numbers for
|
||||
Hebrew, Arabic, Persian, Devanagari, and CJK depend on the system fallback
|
||||
font, which can differ slightly between devices and from print output.
|
||||
|
||||
@@ -122,6 +122,36 @@ The client and schema helper must:
|
||||
- keep attachments unsupported in V1
|
||||
- avoid remote frontend dependencies
|
||||
|
||||
## Print paper size
|
||||
|
||||
The flyer supports two print sizes: **US Letter** (8.5×11in) and **A4**
|
||||
(210×297mm), covering every country whose language the client supports (Letter
|
||||
in North America and a few Latin American countries; A4 everywhere else,
|
||||
including the EU, Russia, China, Japan, and the Middle East).
|
||||
|
||||
- Dimensions live in CSS custom properties `--sheet-w` / `--sheet-h`, switched
|
||||
by a `paper-letter` / `paper-a4` class on `<html>` (`applyPaperSize` in
|
||||
`nostr-client.js`). The tear-off column (`3.75in`) and QR codes (`1.25in`)
|
||||
stay fixed physical sizes; the content column flexes to the sheet width.
|
||||
- The print `@page { size: … }` is injected by JS (an element with id
|
||||
`voxvera-page-size`), because `@page size` cannot read CSS custom properties
|
||||
across engines. The static `@media print` block keeps `margin: 0` and a
|
||||
Letter default for the pre-JS state.
|
||||
- The default is chosen from the **region subtag** of the browser locale
|
||||
(`preferredPaperSize` — e.g. `en-GB` → A4, `en-US` → Letter). This is "by
|
||||
location" without geolocation, using the same signal as language. When no
|
||||
locale carries a region, it falls back to Letter.
|
||||
- A user override (the paper-size selector) is stored in `localStorage`
|
||||
(`voxvera_paper`) and wins over the region default.
|
||||
- **Paper size is a local print/view preference and is never written into the
|
||||
Nostr event.** A flyer authored on Letter reprints on A4 for a European
|
||||
viewer without changing the source event. The live fit-check protects
|
||||
authoring on whichever size is active; switching to a narrower size after
|
||||
authoring can ellipsis-truncate a maxed-out single-line field.
|
||||
|
||||
Field capacity therefore varies by paper size; see
|
||||
[`flyer-field-limits.md`](flyer-field-limits.md).
|
||||
|
||||
## Localization
|
||||
|
||||
Flyer defaults come from `site/locales.js`.
|
||||
|
||||
@@ -86,4 +86,64 @@ test.describe("VoxVera static client", () => {
|
||||
);
|
||||
await expect(page.locator(".content h1")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("infers language from the first supported browser preference", async ({ page }) => {
|
||||
// Browser prefers Dutch (unsupported), then German (supported), then
|
||||
// English. We should honor German rather than jumping to the English
|
||||
// fallback — i.e. walk navigator.languages, not just navigator.language.
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "languages", {
|
||||
configurable: true,
|
||||
get: () => ["nl-NL", "de-DE", "en-US"]
|
||||
});
|
||||
Object.defineProperty(navigator, "language", {
|
||||
configurable: true,
|
||||
get: () => "nl-NL"
|
||||
});
|
||||
});
|
||||
await page.goto("/");
|
||||
// German default headline (locales.js de.landing.title).
|
||||
await expect(page.locator(".content h1").first()).toContainText("STRENG GEHEIM");
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "de");
|
||||
});
|
||||
|
||||
test("defaults to A4 for an A4-region locale (no geolocation)", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "languages", { configurable: true, get: () => ["de-DE"] });
|
||||
Object.defineProperty(navigator, "language", { configurable: true, get: () => "de-DE" });
|
||||
});
|
||||
await page.goto("/");
|
||||
await expect(page.locator("html")).toHaveClass(/paper-a4/);
|
||||
const pageRule = await page.evaluate(
|
||||
() => document.getElementById("voxvera-page-size").textContent
|
||||
);
|
||||
expect(pageRule).toContain("A4");
|
||||
});
|
||||
|
||||
test("defaults to US Letter for a Letter-region locale", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "languages", { configurable: true, get: () => ["en-US"] });
|
||||
Object.defineProperty(navigator, "language", { configurable: true, get: () => "en-US" });
|
||||
});
|
||||
await page.goto("/");
|
||||
await expect(page.locator("html")).toHaveClass(/paper-letter/);
|
||||
const pageRule = await page.evaluate(
|
||||
() => document.getElementById("voxvera-page-size").textContent
|
||||
);
|
||||
expect(pageRule).toContain("Letter");
|
||||
});
|
||||
|
||||
test("manual paper-size override persists across reloads", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "languages", { configurable: true, get: () => ["en-US"] });
|
||||
});
|
||||
// Editor mode shows the topbar controls; viewer mode hides them.
|
||||
await page.goto("/#editor");
|
||||
await expect(page.locator("html")).toHaveClass(/paper-letter/);
|
||||
await page.locator("#topbar-paper").selectOption("a4");
|
||||
await expect(page.locator("html")).toHaveClass(/paper-a4/);
|
||||
await page.reload();
|
||||
// Stored override beats the en-US region default.
|
||||
await expect(page.locator("html")).toHaveClass(/paper-a4/);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@
|
||||
<button id="viewer-tab" class="tab" type="button" data-mode="viewer">Viewer</button>
|
||||
</nav>
|
||||
<select id="topbar-lang" aria-label="Language"></select>
|
||||
<select id="topbar-paper" aria-label="Paper size"></select>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -26,6 +27,7 @@
|
||||
<button type="button" id="viewer-print">Print</button>
|
||||
<button type="button" id="viewer-editor">Editor</button>
|
||||
<select id="viewer-lang" aria-label="Language"></select>
|
||||
<select id="viewer-paper" aria-label="Paper size"></select>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
|
||||
+33
-15
@@ -7,6 +7,21 @@
|
||||
--accent: #b80000;
|
||||
--ok: #1d6b3a;
|
||||
--warn: #8a4d00;
|
||||
/* Printable sheet dimensions. Switched by the paper-* class on <html>
|
||||
(applyPaperSize in nostr-client.js). The tear-off column (3.75in) and QR
|
||||
codes (1.25in) stay fixed physical sizes; the content column flexes. */
|
||||
--sheet-w: 8.5in;
|
||||
--sheet-h: 11in;
|
||||
}
|
||||
|
||||
html.paper-letter {
|
||||
--sheet-w: 8.5in;
|
||||
--sheet-h: 11in;
|
||||
}
|
||||
|
||||
html.paper-a4 {
|
||||
--sheet-w: 210mm;
|
||||
--sheet-h: 297mm;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -278,8 +293,8 @@ code {
|
||||
|
||||
.flyer-sheet {
|
||||
display: flex;
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
width: var(--sheet-w);
|
||||
height: var(--sheet-h);
|
||||
border: 1px solid #000;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
@@ -374,8 +389,8 @@ a {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
width: var(--sheet-w);
|
||||
height: var(--sheet-h);
|
||||
border: 1px solid #000;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
@@ -502,7 +517,7 @@ a {
|
||||
}
|
||||
|
||||
.content {
|
||||
width: calc(8.5in - 3.75in);
|
||||
width: calc(var(--sheet-w) - 3.75in);
|
||||
padding: 15px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
@@ -880,6 +895,9 @@ body.mode-viewer #viewer-panel.drawer-open .result-panel {
|
||||
}
|
||||
|
||||
@media print {
|
||||
/* @page size is injected by applyPaperSize() in nostr-client.js (Letter or
|
||||
A4) since @page cannot read CSS custom properties across engines. This
|
||||
rule is the margin fallback and the default size before JS runs. */
|
||||
@page {
|
||||
size: Letter portrait;
|
||||
margin: 0;
|
||||
@@ -887,8 +905,8 @@ body.mode-viewer #viewer-panel.drawer-open .result-panel {
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
width: var(--sheet-w);
|
||||
height: var(--sheet-h);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
@@ -910,16 +928,16 @@ body.mode-viewer #viewer-panel.drawer-open .result-panel {
|
||||
|
||||
main {
|
||||
display: block;
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
width: var(--sheet-w);
|
||||
height: var(--sheet-h);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-band {
|
||||
display: block;
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
width: var(--sheet-w);
|
||||
height: var(--sheet-h);
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -927,8 +945,8 @@ body.mode-viewer #viewer-panel.drawer-open .result-panel {
|
||||
|
||||
.flyer-preview {
|
||||
display: block;
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
width: var(--sheet-w);
|
||||
height: var(--sheet-h);
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -938,8 +956,8 @@ body.mode-viewer #viewer-panel.drawer-open .result-panel {
|
||||
.container {
|
||||
position: absolute;
|
||||
inset: 0 auto auto 0;
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
width: var(--sheet-w);
|
||||
height: var(--sheet-h);
|
||||
transform: none;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
|
||||
+164
-2
@@ -11,6 +11,20 @@
|
||||
const LOCALES = window.VoxVeraLocales || {};
|
||||
const FALLBACK_LANG = "en";
|
||||
const LANGUAGE_SELECT_IDS = ["topbar-lang", "field-lang", "viewer-lang"];
|
||||
const PAPER_SELECT_IDS = ["topbar-paper", "viewer-paper"];
|
||||
const PAPER_STORAGE_KEY = "voxvera_paper";
|
||||
// Print paper sizes. `css` is the CSS @page size keyword. The actual sheet
|
||||
// dimensions live in nostr-client.css (--sheet-w / --sheet-h per paper class).
|
||||
const PAPER_SIZES = {
|
||||
letter: { css: "Letter" },
|
||||
a4: { css: "A4" }
|
||||
};
|
||||
const DEFAULT_PAPER = "letter";
|
||||
// ISO 3166-1 alpha-2 regions whose default print paper is US Letter.
|
||||
// Everywhere else defaults to A4 (ISO 216), which is the global standard.
|
||||
const LETTER_REGIONS = new Set([
|
||||
"US", "CA", "MX", "CL", "CO", "CR", "GT", "DO", "PH", "VE", "PA", "NI", "SV", "HN", "BO", "EC", "PR"
|
||||
]);
|
||||
const EVENT_KIND = 30078;
|
||||
// Per-script letter-spacing policy for flyer text. `letter-spacing` is not
|
||||
// script-neutral, so the flyer tightens tracking only where it is safe:
|
||||
@@ -183,6 +197,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "تم الجلب",
|
||||
loading_content: "جارٍ تحميل المحتوى...",
|
||||
load_failed: "تعذّر تحميل هذا الملصق.",
|
||||
paper_size: "حجم الورق",
|
||||
paper_letter: "US Letter (8.5 × 11 بوصة)",
|
||||
paper_a4: "A4 (210 × 297 مم)",
|
||||
config_copied: "تم نسخ الإعداد.",
|
||||
lookup_placeholder: "naddr أو معرف حدث أو note1 أو nevent1 أو رابط nostr:",
|
||||
event_reference_required: "أدخل naddr أو معرف حدث أو note1 أو nevent1.",
|
||||
@@ -237,6 +254,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "Abgerufen",
|
||||
loading_content: "Inhalt wird geladen...",
|
||||
load_failed: "Dieses Plakat konnte nicht geladen werden.",
|
||||
paper_size: "Papierformat",
|
||||
paper_letter: "US Letter (8,5 × 11 Zoll)",
|
||||
paper_a4: "A4 (210 × 297 mm)",
|
||||
config_copied: "Konfiguration kopiert.",
|
||||
lookup_placeholder: "naddr, Event-ID, note1, nevent1 oder nostr:-URL",
|
||||
event_reference_required: "Geben Sie naddr, Event-ID, note1 oder nevent1 ein.",
|
||||
@@ -291,6 +311,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "Fetched",
|
||||
loading_content: "Loading content...",
|
||||
load_failed: "Could not load this flyer.",
|
||||
paper_size: "Paper size",
|
||||
paper_letter: "US Letter (8.5 × 11 in)",
|
||||
paper_a4: "A4 (210 × 297 mm)",
|
||||
config_copied: "Config copied.",
|
||||
lookup_placeholder: "naddr, event id, note1, nevent1, or nostr: URL",
|
||||
event_reference_required: "Enter an naddr, event id, note1, or nevent1.",
|
||||
@@ -346,6 +369,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "Recuperado",
|
||||
loading_content: "Cargando contenido...",
|
||||
load_failed: "No se pudo cargar este cartel.",
|
||||
paper_size: "Tamaño de papel",
|
||||
paper_letter: "US Letter (8,5 × 11 in)",
|
||||
paper_a4: "A4 (210 × 297 mm)",
|
||||
config_copied: "Configuración copiada.",
|
||||
lookup_placeholder: "naddr, ID de evento, note1, nevent1 o URL nostr:",
|
||||
event_reference_required: "Ingrese un naddr, ID de evento, note1 o nevent1.",
|
||||
@@ -400,6 +426,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "دریافت شد",
|
||||
loading_content: "در حال بارگذاری محتوا...",
|
||||
load_failed: "بارگذاری این پوستر ممکن نشد.",
|
||||
paper_size: "اندازه کاغذ",
|
||||
paper_letter: "US Letter (8.5 × 11 اینچ)",
|
||||
paper_a4: "A4 (210 × 297 میلیمتر)",
|
||||
config_copied: "پیکربندی کپی شد.",
|
||||
lookup_placeholder: "naddr، شناسه رویداد، note1، nevent1 یا URL nostr:",
|
||||
event_reference_required: "یک naddr، شناسه رویداد، note1 یا nevent1 وارد کنید.",
|
||||
@@ -454,6 +483,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "Charge",
|
||||
loading_content: "Chargement du contenu...",
|
||||
load_failed: "Impossible de charger cette affiche.",
|
||||
paper_size: "Format du papier",
|
||||
paper_letter: "US Letter (8,5 × 11 po)",
|
||||
paper_a4: "A4 (210 × 297 mm)",
|
||||
config_copied: "Configuration copiee.",
|
||||
lookup_placeholder: "naddr, ID d'evenement, note1, nevent1 ou URL nostr:",
|
||||
event_reference_required: "Saisissez un naddr, ID d'evenement, note1 ou nevent1.",
|
||||
@@ -508,6 +540,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "נטען",
|
||||
loading_content: "טוען תוכן...",
|
||||
load_failed: "לא ניתן לטעון כרזה זו.",
|
||||
paper_size: "גודל נייר",
|
||||
paper_letter: "US Letter (8.5 × 11 אינץ׳)",
|
||||
paper_a4: "A4 (210 × 297 מ״מ)",
|
||||
config_copied: "התצורה הועתקה.",
|
||||
lookup_placeholder: "naddr, מזהה אירוע, note1, nevent1 או כתובת nostr:",
|
||||
event_reference_required: "הזן naddr, מזהה אירוע, note1 או nevent1.",
|
||||
@@ -562,6 +597,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "लाया गया",
|
||||
loading_content: "सामग्री लोड हो रही है...",
|
||||
load_failed: "यह पोस्टर लोड नहीं हो सका।",
|
||||
paper_size: "कागज़ का आकार",
|
||||
paper_letter: "US Letter (8.5 × 11 इंच)",
|
||||
paper_a4: "A4 (210 × 297 मिमी)",
|
||||
config_copied: "कॉन्फिग कॉपी हुआ.",
|
||||
lookup_placeholder: "naddr, इवेंट ID, note1, nevent1, या nostr: URL",
|
||||
event_reference_required: "naddr, इवेंट ID, note1, या nevent1 दर्ज करें.",
|
||||
@@ -616,6 +654,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "取得しました",
|
||||
loading_content: "コンテンツを読み込んでいます...",
|
||||
load_failed: "このちらしを読み込めませんでした。",
|
||||
paper_size: "用紙サイズ",
|
||||
paper_letter: "US レター (8.5 × 11 インチ)",
|
||||
paper_a4: "A4 (210 × 297 mm)",
|
||||
config_copied: "設定をコピーしました。",
|
||||
lookup_placeholder: "naddr、イベントID、note1、nevent1、または nostr: URL",
|
||||
event_reference_required: "naddr、イベントID、note1、またはnevent1を入力してください。",
|
||||
@@ -670,6 +711,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "Buscado",
|
||||
loading_content: "Carregando conteúdo...",
|
||||
load_failed: "Não foi possível carregar este panfleto.",
|
||||
paper_size: "Tamanho do papel",
|
||||
paper_letter: "US Letter (8,5 × 11 pol)",
|
||||
paper_a4: "A4 (210 × 297 mm)",
|
||||
config_copied: "Configuracao copiada.",
|
||||
lookup_placeholder: "naddr, ID do evento, note1, nevent1 ou URL nostr:",
|
||||
event_reference_required: "Insira um naddr, ID do evento, note1 ou nevent1.",
|
||||
@@ -724,6 +768,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "Загружено",
|
||||
loading_content: "Загрузка содержимого...",
|
||||
load_failed: "Не удалось загрузить эту листовку.",
|
||||
paper_size: "Размер бумаги",
|
||||
paper_letter: "US Letter (8,5 × 11 дюйма)",
|
||||
paper_a4: "A4 (210 × 297 мм)",
|
||||
config_copied: "Конфигурация скопирована.",
|
||||
lookup_placeholder: "naddr, ID события, note1, nevent1 или URL nostr:",
|
||||
event_reference_required: "Введите naddr, ID события, note1 или nevent1.",
|
||||
@@ -778,6 +825,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "Imepakuliwa",
|
||||
loading_content: "Inapakia maudhui...",
|
||||
load_failed: "Imeshindwa kupakia bango hili.",
|
||||
paper_size: "Ukubwa wa karatasi",
|
||||
paper_letter: "US Letter (8.5 × 11 inchi)",
|
||||
paper_a4: "A4 (210 × 297 mm)",
|
||||
config_copied: "Usanidi umenakiliwa.",
|
||||
lookup_placeholder: "naddr, kitambulisho cha tukio, note1, nevent1 au URL ya nostr:",
|
||||
event_reference_required: "Weka naddr, kitambulisho cha tukio, note1 au nevent1.",
|
||||
@@ -832,6 +882,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "Getirildi",
|
||||
loading_content: "İçerik yükleniyor...",
|
||||
load_failed: "Bu el ilanı yüklenemedi.",
|
||||
paper_size: "Kağıt boyutu",
|
||||
paper_letter: "US Letter (8,5 × 11 inç)",
|
||||
paper_a4: "A4 (210 × 297 mm)",
|
||||
config_copied: "Yapılandırma kopyalandı.",
|
||||
lookup_placeholder: "naddr, olay ID, note1, nevent1 veya nostr: URL",
|
||||
event_reference_required: "Bir naddr, olay ID, note1 veya nevent1 girin.",
|
||||
@@ -886,6 +939,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
fetched: "已获取",
|
||||
loading_content: "正在加载内容...",
|
||||
load_failed: "无法加载此传单。",
|
||||
paper_size: "纸张大小",
|
||||
paper_letter: "US Letter (8.5 × 11 英寸)",
|
||||
paper_a4: "A4 (210 × 297 毫米)",
|
||||
config_copied: "配置已复制。",
|
||||
lookup_placeholder: "naddr、事件 ID、note1、nevent1 或 nostr: URL",
|
||||
event_reference_required: "请输入 naddr、事件 ID、note1 或 nevent1。",
|
||||
@@ -1142,6 +1198,14 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
const select = el(selectId);
|
||||
if (select) select.value = selected;
|
||||
});
|
||||
PAPER_SELECT_IDS.forEach((selectId) => {
|
||||
const select = el(selectId);
|
||||
if (!select) return;
|
||||
select.setAttribute("aria-label", nostrLabel("paper_size", selected));
|
||||
Array.from(select.options).forEach((opt) => {
|
||||
opt.textContent = nostrLabel(`paper_${opt.value}`, selected);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setDefaultText(lang) {
|
||||
@@ -1149,8 +1213,10 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
el("editor-relays").value = relayText;
|
||||
el("viewer-relays").value = relayText;
|
||||
populateLanguageOptions();
|
||||
const defaultLang = supportedLang(lang || getStoredUiLang() || navigator.language.split("-")[0]);
|
||||
populatePaperOptions();
|
||||
const defaultLang = supportedLang(lang || getStoredUiLang() || preferredBrowserLang());
|
||||
applyFlyerDefaults(defaultLang);
|
||||
applyPaperSize(getStoredPaper() || preferredPaperSize());
|
||||
Object.entries(EMPTY_OUTPUT_LABELS).forEach(([id, key]) => setEmptyOutput(id, key, defaultLang));
|
||||
setLocalizedText("publish-status", "idle", defaultLang);
|
||||
setLocalizedText("viewer-status", "idle", defaultLang);
|
||||
@@ -1170,6 +1236,96 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the browser's full ordered preference list (navigator.languages),
|
||||
// not just the single top choice, and return the first base language we
|
||||
// actually support. This honors a user's secondary preference (e.g. prefers
|
||||
// Dutch, then German) instead of jumping straight to the English fallback.
|
||||
function preferredBrowserLang() {
|
||||
const list = (navigator.languages && navigator.languages.length)
|
||||
? navigator.languages
|
||||
: [navigator.language || ""];
|
||||
for (const tag of list) {
|
||||
const base = String(tag || "").split("-")[0];
|
||||
if (base && LOCALES[base]) return base;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function supportedPaper(paper) {
|
||||
return PAPER_SIZES[paper] ? paper : DEFAULT_PAPER;
|
||||
}
|
||||
|
||||
function getStoredPaper() {
|
||||
try {
|
||||
const stored = window.localStorage.getItem(PAPER_STORAGE_KEY);
|
||||
return stored && PAPER_SIZES[stored] ? stored : "";
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// "By location" without geolocation: read the region subtag of the browser's
|
||||
// locale list (the "GB" in "en-GB") and pick the paper that country prints on.
|
||||
// Same signal we use for language, no permission prompt. Falls back to
|
||||
// DEFAULT_PAPER only when no locale carries a region at all.
|
||||
function preferredPaperSize() {
|
||||
const list = (navigator.languages && navigator.languages.length)
|
||||
? navigator.languages
|
||||
: [navigator.language || ""];
|
||||
for (const tag of list) {
|
||||
const parts = String(tag || "").split("-");
|
||||
const region = parts.length > 1 ? parts[parts.length - 1].toUpperCase() : "";
|
||||
if (/^[A-Z]{2}$/.test(region)) {
|
||||
return LETTER_REGIONS.has(region) ? "letter" : "a4";
|
||||
}
|
||||
}
|
||||
return DEFAULT_PAPER;
|
||||
}
|
||||
|
||||
function ensurePageSizeStyle() {
|
||||
let style = document.getElementById("voxvera-page-size");
|
||||
if (!style) {
|
||||
style = document.createElement("style");
|
||||
style.id = "voxvera-page-size";
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
// Paper size is a local print/view preference — it is never written into the
|
||||
// Nostr event, so a flyer authored on Letter reprints on A4 for a European
|
||||
// viewer without changing the source event.
|
||||
function applyPaperSize(paper) {
|
||||
const selected = supportedPaper(paper);
|
||||
const root = document.documentElement;
|
||||
root.classList.remove("paper-letter", "paper-a4");
|
||||
root.classList.add(`paper-${selected}`);
|
||||
// @page size cannot read CSS custom properties reliably across engines, so
|
||||
// drive it from an injected stylesheet that we rewrite on each change.
|
||||
ensurePageSizeStyle().textContent =
|
||||
`@media print { @page { size: ${PAPER_SIZES[selected].css} portrait; margin: 0; } }`;
|
||||
try {
|
||||
window.localStorage.setItem(PAPER_STORAGE_KEY, selected);
|
||||
} catch (_) {}
|
||||
PAPER_SELECT_IDS.forEach((id) => {
|
||||
const sel = el(id);
|
||||
if (sel) sel.value = selected;
|
||||
});
|
||||
updatePreviewScale();
|
||||
}
|
||||
|
||||
function populatePaperOptions() {
|
||||
PAPER_SELECT_IDS.forEach((id) => {
|
||||
const sel = el(id);
|
||||
if (!sel || sel.options.length) return;
|
||||
Object.keys(PAPER_SIZES).forEach((key) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = key;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyFlyerDefaults(lang) {
|
||||
const defaults = localeDefaults(lang);
|
||||
flyerSource = "default";
|
||||
@@ -1934,7 +2090,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
setDefaultText(getStoredUiLang() || navigator.language.split("-")[0]);
|
||||
setDefaultText();
|
||||
if (window.ResizeObserver) {
|
||||
new ResizeObserver(updatePreviewScale).observe(el("flyer-preview"));
|
||||
} else {
|
||||
@@ -1968,6 +2124,12 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
select.addEventListener("change", () => applyLanguageChange(select.value || FALLBACK_LANG));
|
||||
});
|
||||
|
||||
PAPER_SELECT_IDS.forEach((selectId) => {
|
||||
const select = el(selectId);
|
||||
if (!select) return;
|
||||
select.addEventListener("change", () => applyPaperSize(select.value || DEFAULT_PAPER));
|
||||
});
|
||||
|
||||
TEXT_FIELD_IDS.forEach((fieldId) => {
|
||||
const field = el(fieldId);
|
||||
if (!field) return;
|
||||
|
||||
Reference in new issue
Block a user