feat: base UI strings on carrier's shared Fluent source

Vendors carrier's locales/en-US/main.ftl (extended to cover gsmol's
remaining strings) as the single source both clients draw wording
from, adds a dependency-free .ftl-to-JSON build step (devbox run
locale) since the file only ever uses Fluent's flat subset, and
replaces every hardcoded string in index.html/app.js with a lookup
into the generated dictionary.

Along the way: removes connect()'s requirePin check, which turned out
to be unreachable under the app's own invariants (every call site
already guarantees its target host is pinned before calling it), and
fixes a leftover call to store.setMasterSaved — removed in the earlier
settings redesign — that was silently breaking onboarding's "I saved
it" button.
This commit is contained in:
randogoth 2026-10-08 23:43:59 +03:00
parent 80aca8a29f
commit 70087fa470
7 changed files with 1080 additions and 258 deletions

View file

@ -30,7 +30,8 @@
spellcheck="false" hidden>
<div id="top-actions" class="top-actions" hidden>
<button id="theme" class="icon-only" aria-label="theme"><svg class="icon"><use href="#i-auto"/></svg></button>
<button id="settings-open" class="icon-only" aria-label="settings" title="settings"><svg class="icon"><use href="#i-settings"/></svg></button>
<button id="settings-open" class="icon-only" data-l10n-aria-label="settings-nav-label"
data-l10n-title="settings-nav-label"><svg class="icon"><use href="#i-settings"/></svg></button>
<div id="account" class="account"></div>
<div id="avatar" class="avatar" aria-hidden="true"></div>
</div>
@ -41,128 +42,124 @@
screen's concern — e.g. the server screen never asks for a username. -->
<main id="setup" class="setup" hidden>
<section id="setup-intro" class="setup-card">
<h2>welcome</h2>
<p class="lead">Encrypted mail with no account and no password. Your identity is one secret,
kept in this browser and nowhere else.</p>
<button type="button" id="create-identity" class="filled large">create a new identity</button>
<button type="button" id="show-restore" class="link">restore existing identity from seed</button>
<h2 data-l10n-id="onboard-welcome-title">welcome</h2>
<p class="lead" data-l10n-id="onboard-welcome-description">Encrypted mail with no account and no
password. Your identity is one secret, kept in this browser and nowhere else.</p>
<button type="button" id="create-identity" class="filled large" data-l10n-id="onboard-create-button">create a new identity</button>
<button type="button" id="show-restore" class="link" data-l10n-id="onboard-restore-button">restore existing identity from seed</button>
</section>
<section id="setup-seed" class="setup-card" hidden>
<h2>write this seed down</h2>
<p class="lead">It is the only secret, and it is shown once.</p>
<h2 data-l10n-id="onboard-created-title">write this seed down</h2>
<p class="lead" data-l10n-id="onboard-created-description">It is the only secret, and it is shown once.</p>
<div class="seed-card">
<div id="setup-seed-value" class="seed"></div>
</div>
<button type="button" id="seed-saved" class="filled large">I saved it</button>
<button type="button" id="seed-back" class="link">back</button>
<button type="button" id="seed-saved" class="filled large" data-l10n-id="onboard-saved-button">I saved it</button>
<button type="button" id="seed-back" class="link" data-l10n-id="action-back">back</button>
</section>
<section id="setup-restore" class="setup-card" hidden>
<h2>restore from a seed</h2>
<p class="lead">Paste the secret you saved. Your keys come back from it — it is all this client
needs.</p>
<h2 data-l10n-id="onboard-restore-title">restore from a seed</h2>
<p class="lead" data-l10n-id="onboard-restore-description">Paste the secret you saved. Your keys
come back from it — it is all this client needs.</p>
<form id="restore-form" class="stack">
<label>your secret
<input id="restore-master" placeholder="hex or base32" spellcheck="false" required>
<label><span data-l10n-id="onboard-seed-entry-title">your secret</span>
<input id="restore-master" spellcheck="false" required>
</label>
<span id="restore-error" class="error"></span>
<button class="filled large">restore</button>
<button class="filled large" data-l10n-id="onboard-restore-go-button">restore</button>
</form>
<button type="button" id="restore-back" class="link">back</button>
<button type="button" id="restore-back" class="link" data-l10n-id="action-back">back</button>
</section>
<section id="setup-server" class="setup-card" hidden>
<h2>set up the server</h2>
<h2 data-l10n-id="signup-server-title">set up the server</h2>
<form id="server-form" class="stack">
<label>host
<label><span data-l10n-id="signup-host-label">host</span>
<input id="server-host" placeholder="example.org" spellcheck="false" required>
</label>
<div id="server-key-area" class="seed-card" hidden></div>
<span id="server-error" class="error"></span>
<button id="server-action" class="filled large">fetch the server's key</button>
<button id="server-action" class="filled large" data-l10n-id="signup-fetch-key-button">fetch the server's key</button>
</form>
<button type="button" id="server-back" class="link">start over</button>
<button type="button" id="server-back" class="link" data-l10n-id="signup-start-over-button">start over</button>
</section>
<section id="setup-account" class="setup-card" hidden>
<h2>choose your address</h2>
<h2 data-l10n-id="signup-account-title">choose your address</h2>
<div id="account-mode" class="mode-toggle" role="group">
<button type="button" data-mode="new" class="active" aria-current="true">create a new address</button>
<button type="button" data-mode="existing">I already have this address</button>
<button type="button" data-mode="new" class="active" aria-current="true"
data-l10n-id="signup-mode-new-label">create a new address</button>
<button type="button" data-mode="existing" data-l10n-id="signup-mode-existing-label">I already have this address</button>
</div>
<form id="account-form" class="stack">
<label>user
<label><span data-l10n-id="signup-user-label">user</span>
<input id="account-user" placeholder="alice" spellcheck="false" required>
</label>
<label id="account-token-label"><span>invite token <span class="hint">(optional)</span></span>
<input id="account-token" placeholder="only if your server asks">
<label id="account-token-label"><span data-l10n-id="signup-invite-label">invite token (if the server requires one)</span>
<input id="account-token">
</label>
<span id="account-error" class="error"></span>
<button id="account-action" class="filled large">register</button>
<button id="account-action" class="filled large" data-l10n-id="signup-register-button">register</button>
</form>
<button type="button" id="account-back" class="link">back</button>
<button type="button" id="account-back" class="link" data-l10n-id="action-back">back</button>
</section>
</main>
<main id="app" hidden>
<nav class="rail" aria-label="folders">
<button id="write" class="compose"><svg class="icon"><use href="#i-compose"/></svg>compose</button>
<button id="write" class="compose"><svg class="icon"><use href="#i-compose"/></svg><span data-l10n-id="mail-write-button">compose</span></button>
<div id="folders">
<button data-folder="inbox" aria-label="inbox" class="active" aria-current="true">
<svg class="icon"><use href="#i-inbox"/></svg><span class="label">inbox</span>
<button data-folder="inbox" class="active" aria-current="true" data-l10n-aria-label="folder-inbox">
<svg class="icon"><use href="#i-inbox"/></svg><span class="label" data-l10n-id="folder-inbox">inbox</span>
<span id="count-inbox" class="count"></span>
</button>
<button data-folder="requests" aria-label="requests">
<svg class="icon"><use href="#i-inbox"/></svg><span class="label">requests</span>
<button data-folder="requests" data-l10n-aria-label="folder-requests">
<svg class="icon"><use href="#i-inbox"/></svg><span class="label" data-l10n-id="folder-requests">requests</span>
<span id="count-requests" class="count"></span>
</button>
<button data-folder="sent" aria-label="sent"><svg class="icon"><use href="#i-sent"/></svg><span class="label">sent</span></button>
<button data-folder="contacts" aria-label="contacts"><svg class="icon"><use href="#i-contacts"/></svg><span class="label">contacts</span></button>
<button data-folder="sent" data-l10n-aria-label="folder-sent"><svg class="icon"><use href="#i-sent"/></svg><span class="label" data-l10n-id="folder-sent">sent</span></button>
<button data-folder="contacts" data-l10n-aria-label="contacts-caption"><svg class="icon"><use href="#i-contacts"/></svg><span class="label" data-l10n-id="contacts-caption">contacts</span></button>
</div>
</nav>
<div class="listpane">
<div id="list-toolbar" class="toolbar">
<div class="toolbar">
<div id="mail-tools" class="tools">
<button id="fetch" class="primary"><svg class="icon"><use href="#i-refresh"/></svg><span class="label">fetch</span></button>
<button id="fetch" class="primary"><svg class="icon"><use href="#i-refresh"/></svg><span class="label" data-l10n-id="mail-fetch-button">fetch</span></button>
<span id="list-status" class="list-status"></span>
</div>
<form id="import-form" class="tools" hidden>
<input name="uri" placeholder="smol://alice@example.org/…" aria-label="smol:// address to import"
spellcheck="false" required>
<button class="primary">import</button>
<input name="uri" placeholder="smol://alice@example.org/…" data-l10n-placeholder="contacts-import-placeholder"
data-l10n-aria-label="contacts-import-placeholder" spellcheck="false" required>
<button class="primary" data-l10n-id="contacts-import-button">import</button>
</form>
</div>
<section id="list" class="list" aria-label="messages"></section>
</div>
<article id="view" class="view" aria-label="message"><p class="placeholder">select a message</p></article>
<article id="view" class="view" aria-label="message"><p class="placeholder" data-l10n-id="mail-empty-title">select a message</p></article>
</main>
<dialog id="compose">
<form id="compose-form" method="dialog">
<input name="to" required spellcheck="false" placeholder="to: alice@example.org or smol://…">
<input name="subject" spellcheck="false" placeholder="subject">
<textarea name="body" rows="12" placeholder="message"></textarea>
<input name="subject" spellcheck="false" data-l10n-placeholder="compose-subject-label" placeholder="subject">
<textarea name="body" rows="12" data-l10n-placeholder="compose-body-placeholder" placeholder="message"></textarea>
<div id="compose-unpinned" class="seed-card" hidden>
<h4>unpinned server</h4>
<p class="hint">
<strong id="compose-unpinned-host"></strong> is not pinned, and "require a pinned
server" refuses to trust a key fetched from it for a new recipient. Fetch its key,
compare the fingerprint against what the operator published through a trusted
channel, then pin it to send.
</p>
<h4 id="compose-unpinned-title"></h4>
<p id="compose-unpinned-body" class="hint"></p>
<div id="compose-unpinned-key-area" hidden></div>
<div class="actions">
<button type="button" id="compose-unpinned-fetch">Fetch their server's key</button>
<button type="button" id="compose-unpinned-pin" class="filled" hidden>Pin and send</button>
<button type="button" id="compose-unpinned-fetch" data-l10n-id="compose-pin-button-fetch">Fetch their server's key</button>
<button type="button" id="compose-unpinned-pin" class="filled" hidden data-l10n-id="compose-pin-button-send">Pin and send</button>
</div>
<span id="compose-unpinned-error" class="error"></span>
</div>
<footer>
<label class="anon"><input type="checkbox" name="anonymous"> anonymous — omit my address</label>
<label class="anon"><input type="checkbox" name="anonymous"> <span data-l10n-id="compose-anonymous-label">anonymous — omit my address</span></label>
<span id="compose-error" class="error"></span>
<button type="button" id="compose-cancel">cancel</button>
<button id="send" class="filled" value="send">send</button>
<button type="button" id="compose-cancel" data-l10n-id="action-cancel">cancel</button>
<button id="send" class="filled" value="send" data-l10n-id="compose-send-button">send</button>
</footer>
</form>
</dialog>
@ -172,86 +169,88 @@
all four at once was the overwhelming part. -->
<dialog id="settings">
<header class="dialog-head">
<h2>settings</h2>
<button type="button" id="settings-close" class="icon-only" aria-label="close settings"
title="close"><svg class="icon"><use href="#i-close"/></svg></button>
<h2 data-l10n-id="settings-nav-label">settings</h2>
<button type="button" id="settings-close" class="icon-only" data-l10n-aria-label="settings-close-aria-label"
data-l10n-title="settings-close-tooltip"><svg class="icon"><use href="#i-close"/></svg></button>
</header>
<div class="settings-body">
<nav id="settings-tabs" class="settings-tabs" aria-label="settings sections">
<button type="button" data-tab="identity" class="active" aria-current="true">identity</button>
<button type="button" data-tab="server">server</button>
<button type="button" data-tab="backup">backup</button>
<button type="button" data-tab="advanced">advanced</button>
<button type="button" data-tab="identity" class="active" aria-current="true"
data-l10n-id="settings-tab-identity">identity</button>
<button type="button" data-tab="server" data-l10n-id="settings-tab-server">server</button>
<button type="button" data-tab="backup" data-l10n-id="settings-tab-backup">backup</button>
<button type="button" data-tab="advanced" data-l10n-id="settings-tab-advanced">advanced</button>
</nav>
<div id="settings-panels" class="settings-panels">
<section class="panel" data-panel="identity">
<div id="identity-fields"></div>
<h3>recovery secret</h3>
<h3 data-l10n-id="settings-recovery-secret-title">recovery secret</h3>
<div id="identity-secret"></div>
</section>
<section class="panel" data-panel="server" hidden>
<h3>connection</h3>
<h3 data-l10n-id="server-connection-title">connection</h3>
<div id="server-connection"></div>
<h3>fetching</h3>
<h3 data-l10n-id="server-fetching-title">fetching</h3>
<label class="check-row">
<input type="checkbox" id="leave-on-server">
<span>
<span class="check-title">Leave mail on server</span>
<span class="hint">Keep a copy on the server after fetching, so another client can fetch
it too. When this is enabled, deleting a message removes it from this client and the
server.</span>
<span class="check-title" data-l10n-id="server-leave-mail-title">Leave mail on server</span>
<span class="hint" data-l10n-id="server-leave-mail-subtitle">Keep a copy on the server after
fetching, so another client can fetch it too. When this is enabled, deleting a message
removes it from this client and the server.</span>
</span>
</label>
</section>
<section class="panel" data-panel="backup" hidden>
<p class="hint">One file holding your mail, contacts and server info for moving to another
client, or for keeping what the server no longer has. It is sealed to this identity and
deliberately leaves the secret itself out.</p>
<p class="hint" data-l10n-id="backup-page-description">One file holding your mail, contacts and
server info for moving to another client, or for keeping what the server no longer has. It
is sealed to this identity and deliberately leaves the secret itself out.</p>
<div class="action-row">
<button type="button" id="export-data" class="filled">Export</button>
<span class="hint">Write a backup file</span>
<button type="button" id="export-data" class="filled" data-l10n-id="backup-export-title">Export</button>
<span class="hint" data-l10n-id="backup-export-subtitle">Write a backup file</span>
</div>
<div class="action-row">
<button type="button" id="import-data-trigger" class="outline">Import</button>
<span class="hint">Restore from a backup file</span>
<button type="button" id="import-data-trigger" class="outline" data-l10n-id="backup-import-title">Import</button>
<span class="hint" data-l10n-id="backup-import-subtitle">Restore from a backup file</span>
<input id="import-data" type="file" accept="application/json" hidden>
</div>
<span id="backup-error" class="error"></span>
</section>
<section class="panel" data-panel="advanced" hidden>
<h3>trust</h3>
<h3 data-l10n-id="advanced-trust-section-title">trust</h3>
<label class="check-row">
<input type="checkbox" id="require-pinned-server">
<span>
<span class="check-title">Require a pinned server</span>
<span class="hint">Refuse to look up a new recipient's key from a server you haven't
pinned. An address that already carries a key, or a recipient you already know, is
unaffected — this only applies to a new, hand-typed address.</span>
<span class="check-title" data-l10n-id="advanced-strict-pinning-title">Require a pinned server</span>
<span class="hint" data-l10n-id="advanced-strict-pinning-subtitle">Refuse to look up a new
recipient's key from a server you haven't pinned. An address that already carries a key,
or a recipient you already know, is unaffected — this only applies to a new, hand-typed
address.</span>
</span>
</label>
<h3>rotations</h3>
<p id="rotations-description" class="hint">Issues a fresh key from your secret with a signed
certificate proving it replaces this one. Contacts accept the change from that chain, and
the old key stays readable. Rotation is not revocation.</p>
<h3 data-l10n-id="advanced-rotations-title">rotations</h3>
<p id="rotations-description" class="hint" data-l10n-id="advanced-rotations-description">Issues
a fresh key from your secret with a signed certificate proving it replaces this one. Contacts
accept the change from that chain, and the old key stays readable. Rotation is not revocation.</p>
<div id="rotations-count"></div>
<button type="button" id="rotate-identity" class="outline">Rotate identity</button>
<button type="button" id="rotate-identity" class="outline" data-l10n-id="advanced-rotate-button">Rotate identity</button>
<span id="rotate-error" class="error"></span>
<h3 class="danger-head">log out</h3>
<p id="logout-description" class="hint">Removes this identity from this client entirely: the
secret, pins, contacts and every cached message. Without a backup of the secret there is no
way back in. Mail already fetched here may have no other copy anywhere, unless "leave mail on
server" was on. Whatever is still on the server comes back on the first fetch after logging
back in.</p>
<button type="button" id="logout" class="danger">Log out and delete all data</button>
<h3 class="danger-head" data-l10n-id="advanced-logout-title">log out</h3>
<p id="logout-description" class="hint" data-l10n-id="advanced-logout-description">Removes this
identity from this client entirely: the secret, pins, contacts and every cached message.
Without a backup of the secret there is no way back in. Mail already fetched here may have no
other copy anywhere, unless "leave mail on server" was on. Whatever is still on the server
comes back on the first fetch after logging back in.</p>
<button type="button" id="logout" class="danger" data-l10n-id="advanced-logout-button">Log out and delete all data</button>
<span id="logout-error" class="error"></span>
</section>
</div>
@ -264,7 +263,7 @@
<p id="confirm-text"></p>
</div>
<footer>
<button type="button" id="confirm-cancel" autofocus>Cancel</button>
<button type="button" id="confirm-cancel" autofocus data-l10n-id="action-cancel">Cancel</button>
<button type="button" id="confirm-ok"></button>
</footer>
</dialog>