Compare commits

...
Author SHA1 Message Date
jcoffey 23557a72a2 Quote images through the proxy, and unproxy them on the way out (#412) (#413)
Reading a message fetches its remote images through this server, so the
sender learns nothing about the reader. Quoting the same message into a
reply fetched them directly: same pixel, same reader, but the request
carried their IP and user agent -- exactly what the proxy withholds.

A quote now proxies them the way the message view does. That alone would
be wrong, because a proxied URL belongs to this deployment: sent
unchanged it would reach the recipient as images only this server can
serve, broken for them and a beacon back here. So buildEmailObject turns
them back into the addresses they came from, beside the pass that
restores images blocked under pr411 and the one that turns editor blob
URLs into cid: references.

Deployments with the proxy off are unaffected: the quote fetches
directly, as reading does there.

Three tests from pr411 asserted the address sat in src when images were
allowed, which was the old behaviour; they now ask whether the draft
fetches it at all, proxied or not.

No new strings.
2026-09-19 16:08:52 -07:00
jcoffey d329b33912 Quoting follows the message's own image decision (#410) (#411)
Replying sanitized the quoted body with allowRemote: true, so quoting
fetched every remote image in the message whatever the reader had
decided about it. A tracking pixel in the quote then reported the
message read, and the address live, to whoever was counting -- the thing
leaving the images blocked was meant to prevent. Edit as new and opening
a draft that quotes a message did the same.

The decision now lives in one place, remoteImagesAllowed(), asked with
the same inputs the reader's answer used: the image policy, the trusted
senders, whether the sender is a contact, and whether Show images was
pressed on that message. The last of those was component state, so it
moves to the mail store, where the composer can see it.

Blocked images already keep their address in data-ihm-remote, so nothing
is lost by not fetching: it goes back on the way out, and the sent quote
is what its sender wrote. The recipient's client decides for itself, as
it would with any other client's reply.

Before pr408 this needed a rich-text default to reach; the format offer
made it reachable from plain text, which is how it was found.

No new strings.
2026-09-19 15:48:13 -07:00
jcoffey 88f9e6c50a Switching format keeps the original quote, not a flattened copy (#409) (#409)
Switching a reply between plain text and rich text converted whatever
body the draft was showing. Going from plain text to rich, that meant
the quoted message came back as the "> " text quote run through a
converter -- the sender's formatting, images and links gone, even though
the original markup was sitting on the draft untouched.

Both forms of the quote are prepared when the reply opens, so keep them
on the draft and re-attach the right one when the format changes. Only
what the author typed above the quote is converted. Where the quote
can't be found any more -- edited by hand, or a draft that quotes
nothing -- the whole body is converted as before, which is what every
non-reply draft does.

No new strings.
2026-09-19 15:17:12 -07:00
jcoffey d992442b81 Offer the message's own format when replying (#407) (#408)
A reply opened in the format the settings ask for, whatever the message
being answered was written in, and the per-draft switch was buried in
the composer's ⋮ menu. Replying in plain text to a rich text message
throws away the formatting; replying in rich text to a plain-text one
overrides what the sender chose to write in.

When the two disagree the composer now says so above the editor -- "This
message is rich text", with a Switch button and a dismiss -- and the
draft still opens in the format the settings ask for. Switching converts
that draft only and leaves the setting alone; switching from the ⋮ menu
answers the offer too. Forwards get it as well, where the formatting
being passed on is somebody else's.

What counts as rich text is hasHtmlAlternative(), which reads the body
part's own type: `htmlBody` is derived (RFC 8621 4.1.4), so a plain-text
message has one too and its presence proves nothing.

The mock said otherwise -- it returned an empty `htmlBody` for a
plain-text message, where Stalwart 0.16.21 returns the text/plain part
in both lists. Both builders now answer as the server does, so the path
this feature depends on is exercised in development rather than only
against a real mailbox.

Two new strings, translated in all nine catalogs; the buttons reuse the
menu's existing "Switch to plain text" / "Switch to rich text". The
count falling back to English stays at 16 in every language.

Fixes #407
2026-09-19 14:43:38 -07:00
jcoffey 07b39eb9b6 Call the app by its name in every sentence that names it (#406)
APP_NAME renames an instance, but only the sign-in page, the title bar and
a few headings used it. Two dozen sentences wrote "ihasmail" into
themselves, so a renamed instance still told people to keep an ihasmail
tab open and offered to open mail links "in ihasmail".

Those sentences now take the name as {app}, which also lets a translator
put it where their language wants it. brand.ts grew useAppName() for
components and currentAppName() for the few places that build strings
outside React.

Left as they are: the Files folder "ihasmail", the Sieve script
"ihasmail" and ihasmail.org. Those name things a person can go and look
at, and renaming them would rename real data.

All nine catalogues keep their translations: the name inside each one
became the placeholder. Three of the strings had no translation before
and still fall back to English.

A test walks the sources and the catalogues so a new sentence can't
hard-code the name again.
2026-09-19 14:27:34 -07:00
jcoffey bc366ac047 Reorder folders by dragging, with special folders first (#402) (#405)
The folder tree ignored sortOrder: Inbox came first, then everything
A–Z, so Sent ended up among ordinary folders. The tree now lists Inbox,
then any order the user has chosen, then the other special folders
(Drafts, Sent, Archive, Junk, Trash), then the rest A–Z. Stalwart gives
every folder sortOrder 0 until someone orders it, so an existing
sidebar changes once, to that default.

Dropping a folder on the top or bottom quarter of a row puts it above or
below that row, with a line to show where it will land. Dropping on the
middle still nests it. Special folders can now be dragged, to be
reordered but never nested; on those, the whole row reorders by the
nearer half. The folder menu gains Move up and Move down, for the
keyboard and touch. Inbox stays first.

A reorder numbers the level 10 apart and writes only the folders whose
number changes, in one Mailbox/set. The order is saved on the server,
so it follows the account to every device and to other JMAP clients.

No new strings: Move up and Move down were already translated.

Fixes #402
2026-09-19 14:06:21 -07:00
jcoffey 05df758d0a Open the composer full screen, as a setting (#401) (#404)
Settings > General > Composing has a new switch, "Open the composer full
screen". With it on, every new composer, whether a new message, reply,
forward or reopened draft, starts maximized. Restore still shrinks it to
a window. A draft put back after an undone or failed send keeps the size
it had. It's off by default, and on a phone, where the composer already
fills the screen, it changes nothing.

One new string, translated in all nine catalogs. The count falling back
to English stays at 16 in every language.

Fixes #401
2026-09-19 14:06:18 -07:00
jcoffeyandmbjboon-netizen 05d1645ab7 Update nl.ts (#403)
Signed-off-by: mbjboon-netizen <[email protected]>
Co-authored-by: mbjboon-netizen <[email protected]>
2026-09-19 13:42:51 -07:00
jcoffey 091782ae3a Drag calendar events to another day in the week grid (#400)
A timed event in the week view now moves sideways across the columns
as well as up and down, landing on the new day at the hour it was
dragged to. All-day chips above the grid drag between days the way
month chips do. Both drags count from the day the event was picked up
on, so a multi-day event grabbed on its last day moves by the distance
dragged, not by its length.
2026-09-18 21:19:58 -07:00
jcoffeyandJoe Esteves c118184975 Match Shift+letter shortcuts (Shift+I, Shift+U) (#399)
comboOf() let a shifted letter encode Shift in its case, so Shift+I
produced "I" and never matched the "shift+i" / "shift+u" bindings for
mark as read / unread. Shifted letters now yield "shift+<letter>";
symbols such as "#" and "!" still carry Shift in the character.

Fixes #398

Co-authored-by: Joe Esteves <[email protected]>
2026-09-18 08:22:13 -07:00
52 changed files with 1728 additions and 360 deletions
+28 -4
View File
@@ -239,9 +239,16 @@ for that one, and the dialog says so.
Real JMAP mailboxes, with the server's roles honored.
- Create, rename, create a subfolder, delete (with or without its mail).
- **Drag a folder onto another** to reparent it. Folders with a server role
(Inbox, Sent, Drafts, Trash, Junk, Archive) are structural and are not
offered the drag, because the server refuses to move them anyway.
- **Order:** Inbox first, then the other special folders (Drafts, Sent,
Archive, Junk, Trash), then everything else AZ, at every level.
- **Drag a folder between two others** to put it there. The line shows where
it will land. The order is saved on the server as the folders' JMAP
`sortOrder`, so it follows you to every device, and other clients that
honor `sortOrder` show it too. *Move up* and *Move down* in the folder menu
do the same from the keyboard or on touch. Inbox always stays first.
- **Drag a folder onto the middle of another** to reparent it. Folders with a
server role (Sent, Drafts, Trash, Junk, Archive) can be reordered but not
nested, because the server refuses to move them to another parent.
- **Subscribe / unsubscribe** — *Show in list* / *Hide from list*. An
unsubscribed folder still exists and still receives; it is just out of the
way. Inbox cannot be hidden.
@@ -434,6 +441,21 @@ minimizable and maximizable; full-screen on mobile.
code block, links (`Ctrl+K`), inline images, an emoji picker, and remove
formatting. Tab and Shift+Tab indent inside the body.
- **Plain text** as a per-message or default format.
- **Quoting follows the message's own image decision.** A quote renders the
message again, so the reply blocks its remote images unless that message was
allowed them — by policy, by a trusted sender, by the sender being a
contact, or by *Show images* having been pressed on it. Blocked images keep
their address and get it back when the reply is sent, so the recipient's
copy is the quote as its sender wrote it. Allowed ones are fetched through
the server's image proxy, the same as when the message was read, and the
sent copy points at their own addresses rather than at this server.
- **Answering in the format the message was written in.** Replying in plain
text to a rich text message, or the reverse, loses either the formatting or
the plain text somebody chose to write in. The composer opens in the default
format and offers the other one for that message, above the editor; the
offer is dismissible and changes no setting. Forwards too. What counts as
rich text is the body part's own type, not the presence of `htmlBody`, which
RFC 8621 derives for plain-text mail as well.
- **Recipient chips** with autocomplete from contacts, shared address books you
have added, the server directory and recent recipients; your own cards win a
tie against a colleague's copy of the same person. Free-form addresses parse
@@ -482,6 +504,8 @@ minimizable and maximizable; full-screen on mobile.
uploaded, which it was not being.
- **Attachment reminder** when the text mentions an attachment and none is there.
- **Spell check** toggle.
- **Open the composer full screen**, as a setting, for anyone whose first
move is always Maximize. Off by default; on a phone it changes nothing.
- **Drafts** save as you type and on close, with the save state shown.
- **Quoting** on reply, with the signature placed above or below it, and
reply-all as an optional default.
@@ -921,7 +945,7 @@ not reach another that already has ihasmail open until it signs in again.
| Section | Holds |
| --- | --- |
| **General** | Reading pane, mark-as-read delay, auto-advance, conversation view, snippets, avatars; compose format, quoting, signature placement, spell check; time zone, week start, language & region, date format, time format; `mailto:` handler; export / import / reset |
| **General** | Reading pane, mark-as-read delay, auto-advance, conversation view, snippets, avatars; compose format, quoting, signature placement, spell check, full-screen composer; time zone, week start, language & region, date format, time format; `mailto:` handler; export / import / reset |
| **Privacy & safety** | Remote images and the senders trusted with them, read receipts asked for and answered; the three warnings and the domains they measure against; undo-send window, attachment reminder, confirm-before-delete |
| **Appearance** | Theme, accent color, density, font size, sidebar, swipe actions, interface language |
| **Identities & signatures** | Addresses, names, Reply-To, HTML signatures, the default, and which to hide from the picker |
+7 -2
View File
@@ -122,7 +122,11 @@ export function addSignedEmail(o: { which: keyof typeof SIGNED_MESSAGES; from: [
hasAttachment: false,
preview: body.slice(0, 120),
textBody: [{ partId: "1", blobId: textBlob, size: body.length, name: null, type: "text/plain", charset: "utf-8", disposition: null, cid: null }],
htmlBody: [],
// `htmlBody` is derived (RFC 8621 4.1.4): a message with no HTML
// alternative still gets one, holding the text/plain part. Checked against
// Stalwart 0.16.21 on 2026-09-10 -- see hasHtmlAlternative() in the client,
// which reads the part's type rather than trusting this list to be empty.
htmlBody: [{ partId: "1", blobId: textBlob, size: body.length, name: null, type: "text/plain", charset: "utf-8", disposition: null, cid: null }],
attachments: [],
bodyValues: { "1": { value: body, isEncodingProblem: false, isTruncated: false } },
bodyStructure: {
@@ -192,7 +196,8 @@ export function addEmail(o: { from: [string, string]; to?: string; subject: stri
from: [{ name: o.from[0], email: o.from[1] }], to: [{ name: "Demo User", email: o.to ?? USER }], cc: null, bcc: null, replyTo: null, sender: null,
subject: o.subject, hasAttachment: Boolean(o.attach), preview: text.slice(0, 120).replace(/\n/g, " "),
textBody: [{ partId: "1", blobId: textBlob, size: text.length, name: null, type: "text/plain", charset: "utf-8", disposition: null, cid: null }],
htmlBody: o.html ? [{ partId: "2", blobId: htmlBlob, size: (o.styled ? STYLED_MARKETING_HTML : html).length, name: null, type: "text/html", charset: "utf-8", disposition: null, cid: null }] : [],
// No HTML alternative means `htmlBody` names the text part, not nothing. See addSignedEmail.
htmlBody: o.html ? [{ partId: "2", blobId: htmlBlob, size: (o.styled ? STYLED_MARKETING_HTML : html).length, name: null, type: "text/html", charset: "utf-8", disposition: null, cid: null }] : [{ partId: "1", blobId: textBlob, size: text.length, name: null, type: "text/plain", charset: "utf-8", disposition: null, cid: null }],
attachments,
bodyValues: { "1": { value: text, isEncodingProblem: false, isTruncated: false }, ...(o.html ? { "2": { value: o.styled ? STYLED_MARKETING_HTML : html, isEncodingProblem: false, isTruncated: false } } : {}) },
bodyStructure: { partId: null, blobId: null, size: 0, type: "multipart/mixed", name: null, charset: null, disposition: null, cid: null, subParts: [{ partId: "1", blobId: textBlob, size: text.length, type: "text/plain", name: null, charset: "utf-8", disposition: null, cid: null }, ...(o.html ? [{ partId: "2", blobId: htmlBlob, size: (o.styled ? STYLED_MARKETING_HTML : html).length, type: "text/html", name: null, charset: "utf-8", disposition: null, cid: null }] : []), ...attachments] },
@@ -0,0 +1,73 @@
/*
* An instance renamed with APP_NAME should be called by its name everywhere,
* not only on the sign-in page and in the title bar. So no sentence shown to
* a person may write "ihasmail" into itself: it takes the name as {app}.
*
* The exceptions are the places where "ihasmail" is not the app's name but a
* literal a person could go and look at: the Files folder, the Sieve script
* and the project's own address. Renaming those would rename real data.
*/
import { describe, expect, it } from "vitest";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const SRC = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
/** Strings that name a stored thing, not the app. */
const LITERALS = [
"Images are stored in your Files (folder “ihasmail”) and embedded when you send.",
"“{name}” will be deactivated (not deleted) and a new “ihasmail” script will take over.",
"Another script (“{name}”) is active. Saving rules here will activate the “ihasmail” script instead.",
"ihasmail.org",
"ihasmail",
];
function sources(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const path = join(dir, name);
if (statSync(path).isDirectory()) {
if (name === "locales" || name === "__tests__") continue;
sources(path, out);
} else if (/\.tsx?$/.test(name)) {
out.push(path);
}
}
return out;
}
/** Every translated string in a file, however `t` was imported. */
function translatedStrings(code: string): string[] {
return [...code.matchAll(/\b(?:t|tNode|translate)\(\s*"((?:[^"\\]|\\.)*)"/g)].map((m) =>
JSON.parse(`"${m[1]}"`),
);
}
describe("text that names the app", () => {
it("takes the name as {app} instead of writing ihasmail into the sentence", () => {
const offenders: string[] = [];
for (const file of sources(SRC)) {
for (const s of translatedStrings(readFileSync(file, "utf8"))) {
if (s.includes("ihasmail") && !LITERALS.includes(s)) {
offenders.push(`${file.slice(SRC.length)}: ${s.slice(0, 60)}`);
}
}
}
expect(offenders).toEqual([]);
});
it("keeps a placeholder in every translation of those strings", () => {
const catalogs = readdirSync(join(SRC, "locales")).filter((f) => f.endsWith(".ts") && f !== "index.ts");
const wrong: string[] = [];
for (const name of catalogs) {
const code = readFileSync(join(SRC, "locales", name), "utf8");
for (const m of code.matchAll(/^\s*"((?:[^"\\]|\\.)*)": "((?:[^"\\]|\\.)*)",$/gm)) {
const key = JSON.parse(`"${m[1]}"`);
const value = JSON.parse(`"${m[2]}"`);
// A key that takes the name must not hard-code it in the translation.
if (key.includes("{app}") && value.includes("ihasmail")) wrong.push(`${name}: ${key.slice(0, 50)}`);
}
}
expect(wrong).toEqual([]);
});
});
+21 -2
View File
@@ -11,8 +11,8 @@ import { isTextEntry, keyboard } from "@/lib/input/keyboard";
* swallows a keystroke in the first place.
*/
const pressFrom = (el: Element, key: string) => {
const e = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true });
const pressFrom = (el: EventTarget, key: string, init?: KeyboardEventInit) => {
const e = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, ...init });
el.dispatchEvent(e);
return e;
};
@@ -92,3 +92,22 @@ describe("shortcuts with a checkbox focused", () => {
expect(handler).not.toHaveBeenCalled();
});
});
describe("shifted letter shortcuts", () => {
it("matches Shift+I and Shift+U bindings", () => {
const read = vi.fn();
const unread = vi.fn();
pop = keyboard.pushScope("test", [
{ keys: "shift+i", description: "Mark as read", group: "Actions", handler: read },
{ keys: "shift+u", description: "Mark as unread", group: "Actions", handler: unread },
]);
const readEvent = pressFrom(window, "I", { shiftKey: true });
const unreadEvent = pressFrom(window, "U", { shiftKey: true });
expect(read).toHaveBeenCalledOnce();
expect(unread).toHaveBeenCalledOnce();
expect(readEvent.defaultPrevented).toBe(true);
expect(unreadEvent.defaultPrevented).toBe(true);
});
});
@@ -36,7 +36,7 @@ describe("a keydown with no key", () => {
it("leaves real keys alone", () => {
expect(comboOf(new KeyboardEvent("keydown", { key: "e" }))).toBe("e");
expect(comboOf(new KeyboardEvent("keydown", { key: "E", shiftKey: true }))).toBe("E");
expect(comboOf(new KeyboardEvent("keydown", { key: "E", shiftKey: true }))).toBe("shift+e");
expect(comboOf(new KeyboardEvent("keydown", { key: "Enter", ctrlKey: true }))).toMatch(/enter$/);
});
});
+24
View File
@@ -1,3 +1,5 @@
import { useSession } from "@/store/session";
/**
* What this instance calls itself, when nothing has said otherwise yet.
*
@@ -11,3 +13,25 @@
* three copies of a default is how two of them end up stale.
*/
export const DEFAULT_APP_NAME = "ihasmail";
/**
* What this instance calls itself, right now.
*
* Text that names the app reads it from here rather than writing "ihasmail"
* into the sentence, so an instance renamed with `APP_NAME` is called by its
* name everywhere, not only on the sign-in page and in the title bar. The
* name goes into the sentence as the `{app}` placeholder, which also lets a
* translator put it where their language wants it.
*
* Two shapes for the same fact: the hook for components, and the plain
* function for the few places that build strings outside React (the service
* worker's facts, for one). Both fall back to the default until the session
* arrives.
*/
export function useAppName(): string {
return useSession((s) => s.session?.ihasmail?.appName)?.trim() || DEFAULT_APP_NAME;
}
export function currentAppName(): string {
return useSession.getState().session?.ihasmail?.appName?.trim() || DEFAULT_APP_NAME;
}
@@ -10,6 +10,8 @@ import {
snap,
movePatch,
moveByDaysPatch,
moveAcrossPatch,
columnsMoved,
dayDelta,
resizePatch,
SNAP_MINUTES,
@@ -180,10 +182,22 @@ describe("the patch a drag sends, computed in the event's own frame", () => {
expect(resizePatch(3600, -600)).toEqual({ duration: "PT15M" });
});
it("moves by days and minutes together, as a week-grid drag does", () => {
expect(moveAcrossPatch("2026-09-04T14:00:00", 2, 90)).toEqual({ start: "2026-09-06T15:30:00" });
expect(moveAcrossPatch("2026-09-04T14:00:00", -1, 0)).toEqual({ start: "2026-09-03T14:00:00" });
expect(moveAcrossPatch("2026-09-04T14:00:00", 0, -30)).toEqual({ start: "2026-09-04T13:30:00" });
});
it("adds the days as days, so a clock change does not move the hour", () => {
// US clocks go back on 1 November 2026; 14:00 stays 14:00 across it.
expect(moveAcrossPatch("2026-10-31T14:00:00", 2, 0)).toEqual({ start: "2026-11-02T14:00:00" });
});
it("says nothing at all about a start it cannot read", () => {
expect(movePatch("not a date", 30)).toEqual({});
expect(moveByDaysPatch("", 3)).toEqual({});
expect(moveByDaysPatch("2026-09-04T14:00:00", Number.NaN)).toEqual({});
expect(moveAcrossPatch("not a date", 1, 30)).toEqual({});
});
});
@@ -228,3 +242,23 @@ describe("pixelsToMinutes", () => {
expect(SNAP_MINUTES).toBe(15);
});
});
describe("columnsMoved", () => {
it("counts whole columns, to the nearest", () => {
expect(columnsMoved(100, 100, 2, 7)).toBe(1);
expect(columnsMoved(140, 100, 2, 7)).toBe(1);
expect(columnsMoved(160, 100, 2, 7)).toBe(2);
expect(columnsMoved(-40, 100, 2, 7)).toBe(0);
expect(columnsMoved(-160, 100, 3, 7)).toBe(-2);
});
it("stops at the edges of the week instead of wrapping", () => {
expect(columnsMoved(-900, 100, 2, 7)).toBe(-2);
expect(columnsMoved(900, 100, 2, 7)).toBe(4);
});
it("never moves sideways in a one-day grid or before it is measured", () => {
expect(columnsMoved(500, 100, 0, 1)).toBe(0);
expect(columnsMoved(500, 0, 0, 7)).toBe(0);
});
});
+29
View File
@@ -142,6 +142,35 @@ export function moveByDaysPatch(storedStart: string, days: number): DragPatch {
return { start: formatStored(moved) };
}
/**
* Moved by whole days and by minutes at once -- the week grid, where a drag
* goes sideways to another day and up or down to another hour in the same
* gesture.
*
* The days go first and as days, for the reason moveByDaysPatch gives: a day
* added to a wall clock keeps its time of day across a clock change, where
* 1440 minutes would not.
*/
export function moveAcrossPatch(storedStart: string, days: number, deltaMinutes: number): DragPatch {
const byDays = days ? moveByDaysPatch(storedStart, days).start : storedStart;
if (!byDays) return {};
return snap(deltaMinutes) ? movePatch(byDays, deltaMinutes) : { start: byDays };
}
/**
* How many columns sideways the pointer has gone, kept inside the grid.
*
* Counted from the column the drag began in, so an event that crosses
* midnight moves by the same amount whichever of its two halves was picked
* up. Past the first or last column it stops at the edge rather than
* wrapping: the week on screen is the only week a drag can reach.
*/
export function columnsMoved(deltaPixels: number, columnWidth: number, fromIndex: number, columnCount: number): number {
if (!columnWidth || columnCount < 2) return 0;
const moved = Math.round(deltaPixels / columnWidth) || 0; // never -0
return Math.max(-fromIndex, Math.min(columnCount - 1 - fromIndex, moved));
}
/** Whole days between two local dates, ignoring the time of day on each. */
export function dayDelta(from: Date, to: Date): number {
const a = new Date(from.getFullYear(), from.getMonth(), from.getDate()).getTime();
+1 -1
View File
@@ -120,7 +120,7 @@ export function plural(n: number, forms: PluralForms, vars?: Vars): string {
*
* So the sentence stays whole and the elements are placeholders in it:
*
* tNode("Open {scheme} links in ihasmail.", { scheme: <code>mailto:</code> })
* tNode("Open {scheme} links in {app}.", { scheme: <code>mailto:</code> }, { app: "ihasmail" })
*
* A translator sees one sentence with a named hole and can put the hole
* wherever their language wants it.
+4 -3
View File
@@ -145,13 +145,14 @@ export function comboOf(e: KeyboardEvent): string | null {
const mod = isMac ? e.metaKey : e.ctrlKey;
if (mod) parts.push("mod");
if (e.altKey) parts.push("alt");
if (e.shiftKey && key.length > 1) parts.push("shift");
const shiftedLetter = e.shiftKey && /^[a-z]$/i.test(key);
if (e.shiftKey && (key.length > 1 || shiftedLetter)) parts.push("shift");
let k = key;
if (k === " ") k = "space";
else if (k === "Escape") k = "esc";
else if (k.length === 1) {
// Single chars: shift is encoded by the character itself (e.g. "#", "!").
k = k.length === 1 && !e.shiftKey ? k.toLowerCase() : k;
// Symbols encode Shift in the character itself (e.g. "#", "!").
k = shiftedLetter || !e.shiftKey ? k.toLowerCase() : k;
} else k = k.toLowerCase();
parts.push(k);
return parts.join("+");
+65
View File
@@ -0,0 +1,65 @@
import { unproxiedImageUrl } from "@/lib/text/html";
import type { ImagePolicy } from "@/store/settings";
/**
* Whether a message's remote images may be fetched.
*
* The reader's decision, in one place, because the composer has to make the
* same one. Quoting a message into a reply renders it again — and a quote that
* fetched what the reader had declined would report the message read, and the
* address live, to whoever was counting. The tracking pixel does not care
* which window it loaded in.
*/
export function remoteImagesAllowed(opts: {
from: string | null | undefined;
policy: ImagePolicy;
trusted: string[];
inContacts: boolean;
/** The reader pressed "Show images" on this message. */
shown: boolean;
}): boolean {
if (opts.shown || opts.policy === "always") return true;
if (opts.trusted.includes((opts.from ?? "").toLowerCase())) return true;
return opts.policy === "contacts" && opts.inContacts;
}
/**
* Point proxied images back at their own addresses, on the way out.
*
* Reading a message fetches its remote images through this server, so the
* sender learns nothing about the reader. Those URLs belong to this
* deployment, so a quote that kept them would reach the recipient as images
* only this server can serve -- broken for them, and a beacon back here for
* anyone who could load them (#412).
*/
export function unproxyImages(html: string): string {
if (!html.includes("/api/image?url=")) return html;
const doc = new DOMParser().parseFromString(html, "text/html");
for (const img of Array.from(doc.querySelectorAll("img[src]"))) {
const real = unproxiedImageUrl(img.getAttribute("src") ?? "");
if (real) img.setAttribute("src", real);
}
return doc.body.innerHTML;
}
/**
* Put back the addresses of images that were blocked when the message was
* quoted, on the way out.
*
* Blocking keeps the original URL on the element (`data-ihm-remote`), so
* nothing was lost by not fetching it. The copy that leaves here should be the
* quote as its sender wrote it: the recipient's client decides for itself
* whether to load those images, the same as it would have with any other
* client's reply.
*/
export function restoreBlockedImages(html: string): string {
if (!html.includes("data-ihm-blocked")) return html;
const doc = new DOMParser().parseFromString(html, "text/html");
for (const img of Array.from(doc.querySelectorAll("img[data-ihm-blocked]"))) {
const url = img.getAttribute("data-ihm-remote");
if (url) img.setAttribute("src", url);
img.removeAttribute("data-ihm-blocked");
img.removeAttribute("data-ihm-remote");
}
return doc.body.innerHTML;
}
@@ -0,0 +1,121 @@
import { describe, expect, it } from "vitest";
import { canPlaceFolder, compareFolders, neighbour, placeFolder, siblingsOf } from "../folderOrder";
import type { Id, Mailbox } from "@/jmap/types";
const RIGHTS = { mayRename: true, mayCreateChild: true } as Mailbox["myRights"];
const mb = (id: string, name: string, parentId: string | null, role: Mailbox["role"] = null, sortOrder = 0, over: Partial<Mailbox> = {}): Mailbox =>
({ id, name, parentId, role, sortOrder, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0, isSubscribed: true, myRights: RIGHTS, ...over });
const tree = (...list: Mailbox[]): Record<Id, Mailbox> => Object.fromEntries(list.map((m) => [m.id, m]));
/** As Stalwart hands it over before anybody orders anything: every sortOrder 0. */
const fresh = tree(
mb("zeta", "Zeta", null),
mb("trash", "Deleted Items", null, "trash"),
mb("sent", "Sent Items", null, "sent"),
mb("inbox", "Inbox", null, "inbox"),
mb("alpha", "Alpha", null),
mb("junk", "Junk Mail", null, "junk"),
mb("drafts", "Drafts", null, "drafts"),
mb("work", "Work", null),
mb("clients", "Clients", "work"),
);
const names = (all: Record<Id, Mailbox>, parentId: Id | null = null) => siblingsOf(all, parentId).map((m) => m.id);
/** Apply what `placeFolder` asks for, as the server would. */
function apply(all: Record<Id, Mailbox>, updates: Record<Id, Partial<Mailbox>> | null): Record<Id, Mailbox> {
const next = { ...all };
for (const [id, patch] of Object.entries(updates ?? {})) next[id] = { ...next[id]!, ...patch };
return next;
}
describe("compareFolders", () => {
it("lists Inbox, then the special folders in mail-client order, then the rest AZ, when nothing is ordered yet", () => {
// #402: Sent landed fourth from the bottom among the reporter's 88 folders.
expect(names(fresh)).toEqual(["inbox", "drafts", "sent", "junk", "trash", "alpha", "work", "zeta"]);
});
it("puts a saved order ahead of the special-folder default", () => {
const ordered = apply(fresh, { zeta: { sortOrder: 10 }, sent: { sortOrder: 20 }, alpha: { sortOrder: 30 }, drafts: { sortOrder: 40 }, junk: { sortOrder: 50 }, trash: { sortOrder: 60 }, work: { sortOrder: 70 } });
expect(names(ordered)).toEqual(["inbox", "zeta", "sent", "alpha", "drafts", "junk", "trash", "work"]);
});
it("keeps Inbox first whatever its sortOrder says", () => {
const a = mb("inbox", "Inbox", null, "inbox", 99);
const b = mb("alpha", "Alpha", null, null, 1);
expect(compareFolders(a, b)).toBeLessThan(0);
});
it("sorts names numerically, not by character", () => {
const all = tree(mb("f10", "Folder 10", null), mb("f9", "Folder 9", null));
expect(names(all)).toEqual(["f9", "f10"]);
});
});
describe("placeFolder", () => {
it("numbers the whole level 10 apart, with the folder where it was dropped", () => {
const next = apply(fresh, placeFolder(fresh, "zeta", "drafts", "before"));
expect(names(next)).toEqual(["inbox", "zeta", "drafts", "sent", "junk", "trash", "alpha", "work"]);
expect(siblingsOf(next, null).map((m) => m.sortOrder)).toEqual([10, 20, 30, 40, 50, 60, 70, 80]);
});
it("writes only the folders whose number changes", () => {
const once = apply(fresh, placeFolder(fresh, "zeta", "drafts", "before"));
// Swapping the last two leaves everything above them where it was.
expect(Object.keys(placeFolder(once, "work", "alpha", "before")!).sort()).toEqual(["alpha", "work"]);
});
it("asks for nothing when the folder is dropped where it already is", () => {
expect(placeFolder(fresh, "sent", "drafts", "after")).toBeNull();
expect(placeFolder(fresh, "sent", "junk", "before")).toBeNull();
});
it("moves a folder to another level, and gives it a place there", () => {
const updates = placeFolder(fresh, "alpha", "clients", "before")!;
expect(updates.alpha).toEqual({ sortOrder: 10, parentId: "work" });
expect(names(apply(fresh, updates), "work")).toEqual(["alpha", "clients"]);
});
});
describe("canPlaceFolder", () => {
it("lets a special folder be reordered among its siblings", () => {
expect(canPlaceFolder(fresh, "sent", "alpha", "after")).toBe(true);
});
it("does not let a special folder move to another level", () => {
expect(canPlaceFolder(fresh, "sent", "clients", "before")).toBe(false);
});
it("puts nothing above Inbox", () => {
expect(canPlaceFolder(fresh, "sent", "inbox", "before")).toBe(false);
expect(canPlaceFolder(fresh, "sent", "inbox", "after")).toBe(true);
});
it("does not put a folder inside its own subtree", () => {
expect(canPlaceFolder(fresh, "work", "clients", "before")).toBe(false);
});
it("needs the right to rename, which RFC 8621 folds moving into", () => {
const locked = apply(fresh, { alpha: { myRights: { ...RIGHTS, mayRename: false } } });
expect(canPlaceFolder(locked, "alpha", "zeta", "after")).toBe(false);
});
});
describe("neighbour", () => {
it("steps past the folder above or below", () => {
expect(neighbour(fresh, "alpha", "up")).toEqual({ targetId: "trash", placement: "before" });
expect(neighbour(fresh, "alpha", "down")).toEqual({ targetId: "work", placement: "after" });
});
it("has nowhere to go past either end, or above Inbox", () => {
expect(neighbour(fresh, "zeta", "down")).toBeNull();
expect(neighbour(fresh, "drafts", "up")).toBeNull();
});
it("skips folders that aren't on screen, so every step visibly moves", () => {
const hidden = apply(fresh, { trash: { isSubscribed: false } });
expect(neighbour(hidden, "alpha", "up", (m) => m.isSubscribed)).toEqual({ targetId: "junk", placement: "before" });
});
});
+101
View File
@@ -0,0 +1,101 @@
import type { Id, Mailbox } from "@/jmap/types";
import { ROLE_ORDER } from "@/store/mail/mailboxes";
import { canDropFolder, descendantIds } from "./folderMove";
/**
* The order folders are listed in, at every level of the tree (#402).
*
* Inbox always comes first. After that the folder's own `sortOrder` decides,
* which is where a folder dragged into place keeps its position, and where
* any other JMAP client that orders folders keeps its choice too. Stalwart
* gives every folder 0 until somebody orders it, so for everyone who never
* has, the tie-breaks decide: special folders first, in the usual mail-client
* order (Drafts, Sent, Archive, Junk, Trash), then the rest AZ.
*/
export function compareFolders(a: Mailbox, b: Mailbox): number {
if ((a.role === "inbox") !== (b.role === "inbox")) return a.role === "inbox" ? -1 : 1;
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
const ra = roleRank(a);
const rb = roleRank(b);
if (ra !== rb) return ra - rb;
return a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true });
}
function roleRank(m: Mailbox): number {
return m.role && m.role in ROLE_ORDER ? ROLE_ORDER[m.role]! : Number.MAX_SAFE_INTEGER;
}
/** Every folder under `parentId` (null: the top level), in list order. */
export function siblingsOf(mailboxes: Record<Id, Mailbox>, parentId: Id | null): Mailbox[] {
return Object.values(mailboxes)
.filter((m) => (m.parentId && mailboxes[m.parentId] ? m.parentId : null) === parentId)
.sort(compareFolders);
}
export type Placement = "before" | "after";
/**
* Whether `draggedId` may be put just above or below `targetId`.
*
* Special folders can be reordered but not reparented, so they may only land
* among their own siblings. Nothing goes above Inbox, which stays first.
*/
export function canPlaceFolder(mailboxes: Record<Id, Mailbox>, draggedId: Id, targetId: Id, placement: Placement): boolean {
const dragged = mailboxes[draggedId];
const target = mailboxes[targetId];
if (!dragged || !target || draggedId === targetId) return false;
if (!dragged.myRights.mayRename) return false;
if (target.role === "inbox" && placement === "before") return false;
if (descendantIds(mailboxes, draggedId).has(targetId)) return false;
const from = parentOf(mailboxes, dragged);
const to = parentOf(mailboxes, target);
return from === to || canDropFolder(mailboxes, draggedId, to);
}
/**
* The updates that put `draggedId` just above or below `targetId`, or null when
* it is already there.
*
* The new level is numbered afresh, 10 apart, so that another client can put
* a folder between two of them without renumbering. Only folders whose number
* actually changes are written.
*/
export function placeFolder(mailboxes: Record<Id, Mailbox>, draggedId: Id, targetId: Id, placement: Placement): Record<Id, Partial<Mailbox>> | null {
const dragged = mailboxes[draggedId]!;
const parentId = parentOf(mailboxes, mailboxes[targetId]!);
const reparent = parentOf(mailboxes, dragged) !== parentId;
const current = siblingsOf(mailboxes, parentId);
const order = current.filter((m) => m.id !== draggedId);
const at = order.findIndex((m) => m.id === targetId) + (placement === "after" ? 1 : 0);
order.splice(at, 0, dragged);
// Dropped where it already was. Renumbering would change nothing anyone sees.
if (!reparent && order.every((m, i) => m.id === current[i]!.id)) return null;
const updates: Record<Id, Partial<Mailbox>> = {};
order.forEach((m, i) => {
const sortOrder = (i + 1) * 10;
if (m.sortOrder !== sortOrder) updates[m.id] = { sortOrder };
});
if (reparent) updates[draggedId] = { ...updates[draggedId], parentId };
return updates;
}
/**
* The neighbour to place a folder against for "Move up" / "Move down", if it
* has one. Only folders on screen count (`shown`), so each step visibly moves
* the folder rather than passing a hidden one.
*/
export function neighbour(mailboxes: Record<Id, Mailbox>, id: Id, direction: "up" | "down", shown: (m: Mailbox) => boolean = () => true): { targetId: Id; placement: Placement } | null {
const m = mailboxes[id];
if (!m) return null;
const level = siblingsOf(mailboxes, parentOf(mailboxes, m)).filter((x) => x.id === id || shown(x));
const i = level.findIndex((x) => x.id === id);
const other = level[direction === "up" ? i - 1 : i + 1];
if (!other) return null;
const placement = direction === "up" ? "before" : "after";
return canPlaceFolder(mailboxes, id, other.id, placement) ? { targetId: other.id, placement } : null;
}
function parentOf(mailboxes: Record<Id, Mailbox>, m: Mailbox): Id | null {
return m.parentId && mailboxes[m.parentId] ? m.parentId : null;
}
+2 -1
View File
@@ -16,6 +16,7 @@
* was installed, which is the same condition background notifications already
* carry — a push subscription has to be renewed from a tab too.
*/
import { currentAppName } from "@/lib/brand";
import { withBase } from "../basePath";
import { SW_CACHE_NAME } from "./swCache";
import { t } from "../i18n";
@@ -57,7 +58,7 @@ export async function publishWorkerFacts(accountId: string | null, archiveId: st
noSubject: t("(no subject)"),
archive: t("Archive"),
markRead: t("Mark as read"),
failed: t("Could not do that — open ihasmail and try again"),
failed: t("Could not do that — open {app} and try again", { app: currentAppName() }),
},
};
try {
+18 -1
View File
@@ -1,5 +1,5 @@
import DOMPurify from "dompurify";
import { withBase } from "@/lib/basePath";
import { BASE_PATH, withBase } from "@/lib/basePath";
export interface SanitizeOptions {
/** Map of Content-ID (without angle brackets) → URL for inline images. */
@@ -158,6 +158,23 @@ export function proxiedImageUrl(url: string): string {
return withBase(`/api/image?url=${encodeURIComponent(url)}`);
}
/**
* The address a proxied image really points at, or null if this is not one.
*
* A proxied URL is this server's, so it is right for reading a message and
* wrong for sending one: a quote left this way would hand the recipient
* images that only load from inside this deployment (#412).
*/
export function unproxiedImageUrl(src: string): string | null {
const path = `${BASE_PATH}/api/image?url=`;
if (!src.startsWith(path)) return null;
try {
return decodeURIComponent(src.slice(path.length)) || null;
} catch {
return null; // Malformed escape: leave it alone rather than mangle it.
}
}
export function sanitizeEmailHtml(input: string, opts: SanitizeOptions = {}): SanitizeResult {
ensureHooks();
let bodyStyle = "";
+25 -22
View File
@@ -518,7 +518,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "Wartet auf dem Server — geht {when} raus.",
"Scheduled — click to clear the schedule": "Geplant — zum Aufheben klicken",
"Nothing scheduled": "Nichts geplant",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "Die Nachricht wartet auf dem Server und wird gesendet, ob ihasmail geöffnet ist oder nicht.",
"The message waits on the server, so it goes out whether or not {app} is open.": "Die Nachricht wartet auf dem Server und wird gesendet, ob {app} geöffnet ist oder nicht.",
"This server holds a message for up to {span}.": "Dieser Server hält eine Nachricht bis zu {span} zurück.",
"Date and time to send": "Datum und Uhrzeit für den Versand",
"Undo send window": "Zeitfenster zum Rückgängigmachen",
@@ -737,7 +737,7 @@ export const catalog: Catalog = {
"Sections": "Bereiche",
"General": "Allgemein",
"Appearance": "Darstellung",
"Make ihasmail yours.": "Machen Sie ihasmail zu Ihrem.",
"Make {app} yours.": "Machen Sie {app} zu Ihrem.",
"Reading": "Lesen",
"Reading pane": "Lesebereich",
"Right of the list": "Rechts von der Liste",
@@ -754,6 +754,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Anhang-Erinnerung",
"Warn when the message mentions an attachment but none is attached.": "Warnen, wenn die Nachricht einen Anhang erwähnt, aber keiner angehängt ist.",
"Spell check while typing": "Rechtschreibprüfung während der Eingabe",
"Open the composer full screen": "Nachrichten im Vollbild verfassen",
"Confirm before deleting": "Vor dem Löschen bestätigen",
"Show message snippets": "Nachrichtenvorschau anzeigen",
"Preview the first line of each message in the list.": "Die erste Zeile jeder Nachricht in der Liste anzeigen.",
@@ -832,7 +833,7 @@ export const catalog: Catalog = {
"Reset to defaults": "Auf Standard zurücksetzen",
"Default mail app": "Standard-E-Mail-Programm",
"Documentation": "Dokumentation",
"About ihasmail": "Über ihasmail",
"About {app}": "Über {app}",
"Server": "Server",
"Server capabilities": "Server-Funktionen",
"Accounts": "Konten",
@@ -950,8 +951,8 @@ export const catalog: Catalog = {
"Notifications": "Benachrichtigungen",
"Notifications are blocked in your browser settings.": "Benachrichtigungen sind in Ihren Browsereinstellungen blockiert.",
"Not supported in this browser.": "In diesem Browser nicht unterstützt.",
"Desktop notifications while ihasmail is open": "Desktop-Benachrichtigungen, solange ihasmail geöffnet ist",
"Notify me even when ihasmail is closed": "Auch benachrichtigen, wenn ihasmail geschlossen ist",
"Desktop notifications while {app} is open": "Desktop-Benachrichtigungen, solange {app} geöffnet ist",
"Notify me even when {app} is closed": "Auch benachrichtigen, wenn {app} geschlossen ist",
"Play a sound for new mail": "Ton bei neuer E-Mail abspielen",
"Test notification": "Testbenachrichtigung",
"Background notifications are on": "Hintergrundbenachrichtigungen sind aktiviert",
@@ -1064,7 +1065,7 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "Neue Identitäten müssen eine Adresse verwenden, von der dieses Konto senden darf (auf dem Server eingerichtete Aliase).",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "Wird beim Verfassen nicht angeboten. Die Adresse empfängt weiterhin Nachrichten, und Sie können wieder von ihr senden, indem Sie sie erneut einblenden.",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "Jede Identität ist eine Absenderadresse mit eigenem Namen, eigener Antwortadresse und eigener Signatur. Die Standardidentität ist beim Verfassen vorausgewählt; legen Sie eine Antwortadresse fest, wenn Antworten woanders hingehen sollen als an die Absenderadresse.",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "Diese Signatur überschreitet das Limit des Servers von {limit} Byte. ihasmail behält die vollständige Fassung in Ihren Dateien und speichert eine kurze Textfassung auf dem Server andere E-Mail-Programme sehen die Nur-Text-Fassung.",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "Diese Signatur \u00fcberschreitet das Limit des Servers von {limit} Byte. {app} beh\u00e4lt die vollst\u00e4ndige Fassung in Ihren Dateien und speichert eine kurze Textfassung auf dem Server \u2014 andere E-Mail-Programme sehen die Nur-Text-Fassung.",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Kategorien im Outlook-Stil, die Sie Terminen über das Rechtsklick-Menü oder den Termin-Editor zuweisen können. Der Kategoriename wird im Termin gespeichert und daher mit anderen Clients synchronisiert.",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "Nur-Text-Nachrichten folgen dem Design bereits. Ist dies aktiviert, gilt das auch für HTML-Nachrichten ohne eigene Farben, statt sie auf einer weißen Fläche darzustellen. Nachrichten mit eigener Gestaltung bleiben genau so, wie der Absender sie entworfen hat.",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "Das ist unabhängig von {setting} unter „Allgemein“, wo festgelegt wird, wie Datum, Uhrzeit und Zahlen geschrieben werden. Sie können eine englische Oberfläche mit deutschen Datumsangaben lesen — oder umgekehrt.",
@@ -1072,29 +1073,29 @@ export const catalog: Catalog = {
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "Dieser Bildschirm hat keinen Touchscreen, hier ändert sich also nichts. Ihr Telefon oder Tablet übernimmt diese Einstellungen.",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "Eine Nachricht gedrückt halten wählt sie aus, einen Ordner gedrückt halten öffnet dessen Menü. Ziehen Sie die Nachrichtenliste nach unten, um nach neuer Post zu sehen.",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "Eine Bestätigung verrät dem Anfragenden, dass diese Adresse aktiv ist und wann die Nachricht gelesen wurde, und der Absender bestimmt, wohin sie geht — deshalb gibt es keine automatische Option. Bei Massensendungen, Mailinglisten und allem, was als automatisch versendet gekennzeichnet ist, wird sie nie angeboten.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "Dieser Browser kann keine Programme für {scheme}-Links registrieren. Safari hat insbesondere keine solche Schnittstelle — Sie können ihasmail dennoch über Ihr Betriebssystem als Standard festlegen, wenn Sie es als App installieren.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "Dieser Browser kann keine Programme f\u00fcr {scheme}-Links registrieren. Safari hat insbesondere keine solche Schnittstelle \u2014 Sie k\u00f6nnen {app} dennoch \u00fcber Ihr Betriebssystem als Standard festlegen, wenn Sie es als App installieren.",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "Für die Registrierung von {scheme}-Links ist eine sichere Verbindung (HTTPS) erforderlich.",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "{scheme}-Links auf Webseiten, in Dokumenten und anderen Programmen — in ihasmail öffnen statt in einem Desktop-Mailprogramm. Ihr Browser fragt nach einer Bestätigung, und Sie können das später in seinen eigenen Einstellungen ändern (Chrome: Einstellungen Datenschutz und Sicherheit Website-Einstellungen Protokoll-Handler; Firefox: Einstellungen Allgemein Anwendungen).",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "{scheme}-Links \u2014 auf Webseiten, in Dokumenten und anderen Programmen \u2014 in {app} \u00f6ffnen statt in einem Desktop-Mailprogramm. Ihr Browser fragt nach einer Best\u00e4tigung, und Sie k\u00f6nnen das sp\u00e4ter in seinen eigenen Einstellungen \u00e4ndern (Chrome: Einstellungen \u203a Datenschutz und Sicherheit \u203a Website-Einstellungen \u203a Protokoll-Handler; Firefox: Einstellungen \u203a Allgemein \u203a Anwendungen).",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "In diesem Browser angefordert. Ob es gewirkt hat, entscheidet der Browser — prüfen Sie dessen Einstellungen, falls E-Mail-Links weiterhin anderswo geöffnet werden.",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "Für einen systemweiten Standard installieren Sie ihasmail zuerst als App (in Chrome: das Installationssymbol in der Adressleiste). Ihr Betriebssystem kann ihasmail dann überall dort direkt anbieten, wo es nach einem E-Mail-Programm fragt.",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "Für einen systemweiten Standard installieren Sie {app} zuerst als App (in Chrome: das Installationssymbol in der Adressleiste). Ihr Betriebssystem kann {app} dann überall dort direkt anbieten, wo es nach einem E-Mail-Programm fragt.",
"Needs a browser with the Push API and a mail server that publishes a push key.": "Erfordert einen Browser mit Push-API und einen Mailserver, der einen Push-Schlüssel veröffentlicht.",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "Ihr Mailserver stellt diese direkt an Ihren Browser zu, sodass sie auch ohne geöffneten ihasmail-Tab ankommen mit Absender und Betreff. Ihr Browser muss dennoch laufen: Beenden Sie ihn vollständig, warten die Benachrichtigungen und kommen an, sobald Sie ihn wieder öffnen.",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "Ihr Mailserver stellt diese direkt an Ihren Browser zu, sodass sie auch ohne ge\u00f6ffneten {app}-Tab ankommen \u2014 mit Absender und Betreff. Ihr Browser muss dennoch laufen: Beenden Sie ihn vollst\u00e4ndig, warten die Benachrichtigungen und kommen an, sobald Sie ihn wieder \u00f6ffnen.",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "Ihr Mailserver kann diesen Browser wecken, übermittelt aber weder Absender noch Betreff. Ihr Browser muss dennoch laufen.",
"This is what a new-mail notification looks like.": "So sieht eine Benachrichtigung über neue Post aus.",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "Sie sind als {user} angemeldet. Ihr Passwort wird nie im Browser gespeichert; der Server hält es pro Sitzung verschlüsselt vor, um mit Stalwart zu kommunizieren.",
"App passwords are managed by your mail administrator.": "App-Passwörter werden von Ihrer Mail-Administration verwaltet.",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "Wenn Sie Ihr Passwort ändern, werden Ihre anderen Webmail-Sitzungen abgemeldet. App-Passwörter funktionieren weiterhin.",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "Für dieses Konto ist die Zwei-Faktor-Authentifizierung aktiviert. ihasmail kann Sie noch nicht per Code anmelden; die Anmeldung auf einem anderen Gerät benötigt daher ein App-Passwort oder Sie deaktivieren die Zwei-Faktor-Authentifizierung hier.",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "F\u00fcr dieses Konto ist die Zwei-Faktor-Authentifizierung aktiviert. {app} kann Sie noch nicht per Code anmelden; die Anmeldung auf einem anderen Ger\u00e4t ben\u00f6tigt daher ein App-Passwort \u2014 oder Sie deaktivieren die Zwei-Faktor-Authentifizierung hier.",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "Ein eigenes Passwort für ein E-Mail-Programm oder Gerät, das Sie einzeln widerrufen können. App-Passwörter umgehen Zwei-Faktor-Codes und funktionieren daher auch in Programmen, die keinen abfragen können.",
"Copy it into {name} now — it isn't shown again.": "Übertragen Sie es jetzt nach {name} — es wird nicht erneut angezeigt.",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "Im Verzeichnis wurden keine weiteren Benutzer gefunden, es kann also niemand Neues hinzugefügt werden. Bestehende Freigaben sind unten aufgeführt und können weiterhin entfernt werden.",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart gibt seine Versionsnummer nicht an E-Mail-Programme weiter, daher nennt ihasmail die Edition, sofern der Server eine angibt. ihasmail benötigt 0.16 oder neuer; die Anmeldung verweigert ältere Versionen.",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart gibt seine Versionsnummer nicht an E-Mail-Programme weiter, daher nennt {app} die Edition, sofern der Server eine angibt. {app} ben\u00f6tigt 0.16 oder neuer; die Anmeldung verweigert \u00e4ltere Versionen.",
"It {damage}, so the rules in it can't be shown or edited — saving what did arrive would write it back over the rest. Reload the page to try again. Your rules are still on the server; nothing here has changed them.": "Es {damage}, daher können die enthaltenen Regeln weder angezeigt noch bearbeitet werden — das Speichern des angekommenen Teils würde den Rest überschreiben. Laden Sie die Seite neu und versuchen Sie es erneut. Ihre Regeln liegen weiterhin auf dem Server; hier wurde nichts daran geändert.",
"The visual rule editor only manages scripts it created. You can edit the script in the {tab} tab, or start fresh with rules (the existing script will be kept but deactivated).": "Der visuelle Regeleditor verwaltet nur Skripte, die er selbst erstellt hat. Sie können das Skript im Reiter {tab} bearbeiten oder neu mit Regeln beginnen (das vorhandene Skript bleibt erhalten, wird aber deaktiviert).",
"Your filter script {damage}, so only part of it arrived. Adding a rule would write that part back over the whole thing. Reload the page and try again.": "Ihr Filterskript {damage}, daher ist nur ein Teil angekommen. Eine Regel hinzuzufügen würde diesen Teil über das Ganze schreiben. Laden Sie die Seite neu und versuchen Sie es erneut.",
"Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.": "Ihr Filterskript konnte gerade nicht gelesen werden; eine Regel hinzuzufügen würde riskieren, es zu überschreiben. Laden Sie die Seite neu und versuchen Sie es erneut.",
"Your active Sieve script was written by hand, so rules can't be added automatically. Open {where} to edit the script or switch to managed rules.": "Ihr aktives Sieve-Skript wurde von Hand geschrieben, daher können Regeln nicht automatisch hinzugefügt werden. Öffnen Sie {where}, um das Skript zu bearbeiten oder zu verwalteten Regeln zu wechseln.",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Hier erscheinen nur Sprachen, in die ihasmail übersetzt wurde; die Liste wächst also mit den Übersetzungen und nicht vorab eine Sprache ohne hinterlegte Texte würde die Seite behaupten lassen, sie sei in einer Sprache, in der sie nicht ist.",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Hier erscheinen nur Sprachen, in die {app} \u00fcbersetzt wurde; die Liste w\u00e4chst also mit den \u00dcbersetzungen und nicht vorab \u2014 eine Sprache ohne hinterlegte Texte w\u00fcrde die Seite behaupten lassen, sie sei in einer Sprache, in der sie nicht ist.",
// ── Labels defined as constants, translated where they render ──────
// The catalogue checker cannot see these: they reach t() as a variable,
@@ -1138,7 +1139,7 @@ export const catalog: Catalog = {
"Drop here for the top level": "Hierher ziehen für die oberste Ebene",
// ── Remaining prose ────────────────────────────────────────────────
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "Die Version von ihasmail ist das Datum des Commits, aus dem es gebaut wurde, gefolgt davon, woher dieser Commit stammt: {example} wurde aus einem Commit vom 30. August 2026 gebaut, der über Pull Request 129 kam. Ein Commit, der nicht über einen solchen kam, trägt stattdessen seinen kurzen SHA {sha}. Die Version sagt bewusst nichts über Stalwart aus; was dieser Build vom Server benötigt, steht in der Zeile darüber.",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "Die Version von {app} ist das Datum des Commits, aus dem es gebaut wurde, gefolgt davon, woher dieser Commit stammt: {example} wurde aus einem Commit vom 30. August 2026 gebaut, der \u00fcber Pull Request 129 kam. Ein Commit, der nicht \u00fcber einen solchen kam, tr\u00e4gt stattdessen seinen kurzen SHA \u2014 {sha}. Die Version sagt bewusst nichts \u00fcber Stalwart aus; was dieser Build vom Server ben\u00f6tigt, steht in der Zeile dar\u00fcber.",
// ── Weekdays, schedule presets, rule operators ─────────────────────
// Header names (List-Id, X-Spam-Status) stay English: they are the actual
@@ -1191,10 +1192,10 @@ export const catalog: Catalog = {
// ── Composer status, calendar title ────────────────────────────────
"New message": "Neue Nachricht",
"Start a new message with what was shared?": "Neue Nachricht mit dem geteilten Inhalt beginnen?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Es wurde etwas mit ihasmail geteilt. Gesendet wird erst, wenn Sie „Senden“ wählen. Wenn Sie dies nicht gerade selbst geteilt haben, verwerfen Sie es.",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Es wurde etwas mit {app} geteilt. Gesendet wird erst, wenn Sie „Senden“ wählen. Wenn Sie dies nicht gerade selbst geteilt haben, verwerfen Sie es.",
"Start a message": "Nachricht beginnen",
"New mail": "Neue E-Mail",
"Could not do that — open ihasmail and try again": "Nicht möglich öffnen Sie ihasmail und versuchen Sie es erneut",
"Could not do that \u2014 open {app} and try again": "Nicht m\u00f6glich \u2013 \u00f6ffnen Sie {app} und versuchen Sie es erneut",
"Sending…": "Wird gesendet…",
"Saving…": "Wird gespeichert…",
"Error": "Fehler",
@@ -1364,7 +1365,7 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "Rückgängig-Zeitfenster: {seconds}s",
"You're all caught up": "Sie sind auf dem neuesten Stand",
"Your browser refused the request: {error}": "Ihr Browser hat die Anfrage abgelehnt: {error}",
"Your browser will ask whether to open mail links in ihasmail": "Ihr Browser wird fragen, ob Mail-Links in ihasmail geöffnet werden sollen",
"Your browser will ask whether to open mail links in {app}": "Ihr Browser wird fragen, ob Mail-Links in {app} geöffnet werden sollen",
"Your message mentions an attachment, but nothing is attached.": "Ihre Nachricht erwähnt einen Anhang, aber es ist nichts angehängt.",
"event": "Termin",
"Hide password": "Passwort verbergen",
@@ -1391,6 +1392,8 @@ export const catalog: Catalog = {
"Collapse all": "Alle einklappen",
"Expand all": "Alle ausklappen",
"Send now instead": "Stattdessen jetzt senden",
"This message is rich text": "Diese Nachricht ist formatierter Text",
"This message is plain text": "Diese Nachricht ist Nur-Text",
"Switch to plain text": "Zu Nur-Text wechseln",
"Switch to rich text": "Zu formatiertem Text wechseln",
"{used} of {total} used": "{used} von {total} belegt",
@@ -1456,7 +1459,7 @@ export const catalog: Catalog = {
"Also count these domains as inside": "Diese Domains ebenfalls als intern werten",
"Always": "Immer",
"Always showing images from": "Bilder immer anzeigen von",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "Ein vom Server des Absenders geladenes Bild verrät ihm, dass die Nachricht geöffnet wurde, wann und ungefähr von wo. Freigegebene Bilder werden vom Server von ihasmail abgerufen und nicht vom Browser, sodass der Absender nichts davon erfährt.",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "Ein vom Server des Absenders geladenes Bild verrät ihm, dass die Nachricht geöffnet wurde, wann und ungefähr von wo. Freigegebene Bilder werden vom Server von {app} abgerufen und nicht vom Browser, sodass der Absender nichts davon erfährt.",
"Applies to": "Gilt für",
"Archive by month": "Nach Monat archivieren",
"Archive by year": "Nach Jahr archivieren",
@@ -1677,8 +1680,8 @@ export const catalog: Catalog = {
"Fingerprint": "Fingerabdruck",
"Hide details": "Details ausblenden",
"Issued by": "Ausgestellt von",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "Sie ist mit OpenPGP signiert, und ihasmail hat keine Möglichkeit, den öffentlichen Schlüssel des Absenders zu beschaffen.",
"It uses a signature algorithm ihasmail cannot check yet.": "Sie verwendet ein Signaturverfahren, das ihasmail noch nicht prüfen kann.",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "Sie ist mit OpenPGP signiert, und {app} hat keine Möglichkeit, den öffentlichen Schlüssel des Absenders zu beschaffen.",
"It uses a signature algorithm {app} cannot check yet.": "Sie verwendet ein Signaturverfahren, das {app} noch nicht prüfen kann.",
"It was made with a certificate belonging to {name}, which does not cover this address.": "Sie wurde mit einem Zertifikat von {name} erstellt, das diese Adresse nicht abdeckt.",
"Previous fingerprint": "Vorheriger Fingerabdruck",
"Signed at": "Signiert am",
@@ -1695,14 +1698,14 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "Die Signatur gehört nicht zu diesem Absender.",
"The signed part is missing either the message or the signature.": "Im signierten Teil fehlt entweder die Nachricht oder die Signatur.",
"The signer has changed.": "Der Unterzeichner hat gewechselt.",
"This message is signed, and ihasmail could not check the signature.": "Diese Nachricht ist signiert, und ihasmail konnte die Signatur nicht prüfen.",
"This message is signed, and {app} could not check the signature.": "Diese Nachricht ist signiert, und {app} konnte die Signatur nicht prüfen.",
"This signature does not check out.": "Diese Signatur stimmt nicht.",
"Valid until": "Gültig bis",
"a different certificate": "einem anderen Zertifikat",
"an unnamed signer": "einem unbenannten Unterzeichner",
"as claimed by the signer": "laut Angabe des Unterzeichners",
"first seen {date}": "zuerst gesehen {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "ihasmail weist Sie darauf hin, wenn eine spätere Nachricht von dieser Adresse von jemand anderem signiert ist.",
"{app} will tell you if a later message from this address is signed by anybody else.": "{app} weist Sie darauf hin, wenn eine spätere Nachricht von dieser Adresse von jemand anderem signiert ist.",
"itself, or an issuer it does not name": "sich selbst, oder einem nicht genannten Aussteller",
"no address": "keine Adresse",
},
+25 -22
View File
@@ -510,7 +510,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "Esperando en el servidor: se enviará {when}.",
"Scheduled — click to clear the schedule": "Programado: haga clic para anular la programación",
"Nothing scheduled": "Nada programado",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "El mensaje espera en el servidor, así que se envía tanto si ihasmail está abierto como si no.",
"The message waits on the server, so it goes out whether or not {app} is open.": "El mensaje espera en el servidor, así que se envía tanto si {app} está abierto como si no.",
"This server holds a message for up to {span}.": "Este servidor retiene un mensaje hasta {span}.",
"Date and time to send": "Fecha y hora de envío",
"Undo send window": "Margen para deshacer el envío",
@@ -732,7 +732,7 @@ export const catalog: Catalog = {
"Sections": "Secciones",
"General": "General",
"Appearance": "Apariencia",
"Make ihasmail yours.": "Haga suyo ihasmail.",
"Make {app} yours.": "Haga suyo {app}.",
"Reading": "Lectura",
"Reading pane": "Panel de lectura",
"Right of the list": "A la derecha de la lista",
@@ -749,6 +749,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Aviso de adjunto",
"Warn when the message mentions an attachment but none is attached.": "Avisar cuando el mensaje menciona un adjunto pero no hay ninguno.",
"Spell check while typing": "Corrección ortográfica al escribir",
"Open the composer full screen": "Redactar mensajes a pantalla completa",
"Confirm before deleting": "Confirmar antes de eliminar",
"Show message snippets": "Mostrar un fragmento de los mensajes",
"Preview the first line of each message in the list.": "Mostrar la primera línea de cada mensaje en la lista.",
@@ -828,7 +829,7 @@ export const catalog: Catalog = {
"Reset to defaults": "Restablecer los valores predeterminados",
"Default mail app": "Aplicación de correo predeterminada",
"Documentation": "Documentación",
"About ihasmail": "Acerca de ihasmail",
"About {app}": "Acerca de {app}",
"About": "Acerca de",
"Server": "Servidor",
"Server capabilities": "Funciones del servidor",
@@ -956,8 +957,8 @@ export const catalog: Catalog = {
"Notifications": "Notificaciones",
"Notifications are blocked in your browser settings.": "Las notificaciones están bloqueadas en la configuración de su navegador.",
"Not supported in this browser.": "No compatible con este navegador.",
"Desktop notifications while ihasmail is open": "Notificaciones del sistema mientras ihasmail está abierto",
"Notify me even when ihasmail is closed": "Avisarme incluso cuando ihasmail esté cerrado",
"Desktop notifications while {app} is open": "Notificaciones del sistema mientras {app} está abierto",
"Notify me even when {app} is closed": "Avisarme incluso cuando {app} esté cerrado",
"Play a sound for new mail": "Reproducir un sonido al llegar correo",
"Test notification": "Probar la notificación",
"Background notifications are on": "Las notificaciones en segundo plano están activadas",
@@ -1126,7 +1127,7 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "Una identidad nueva debe usar una dirección desde la que esta cuenta tenga permiso para enviar (alias configurados en el servidor).",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "No se ofrece al redactar. La dirección sigue recibiendo correo, y puede volver a enviar desde ella mostrándola de nuevo.",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "Cada identidad es una dirección de envío con su propio nombre, dirección de respuesta y firma. La identidad predeterminada se preselecciona al redactar; defina una dirección de respuesta cuando las respuestas deban llegar a un sitio distinto del remitente.",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "Esta firma supera el límite de {limit} bytes del servidor. ihasmail conservará la versión completa en sus Archivos y guardará una versión corta de texto en el servidor: los demás clientes verán la versión en texto sin formato.",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "Esta firma supera el l\u00edmite de {limit} bytes del servidor. {app} conservar\u00e1 la versi\u00f3n completa en sus Archivos y guardar\u00e1 una versi\u00f3n corta de texto en el servidor: los dem\u00e1s clientes ver\u00e1n la versi\u00f3n en texto sin formato.",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Categorías al estilo de Outlook que puede asignar a los eventos desde el menú contextual o el editor de eventos. El nombre de la categoría se guarda en el evento, así que se sincroniza con otros clientes.",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "El correo en texto sin formato ya sigue el tema. Con esta opción, el correo HTML sin colores propios también lo hace, en lugar de mostrarse sobre un fondo blanco. Los mensajes con estilo propio se dejan exactamente como los diseñó el remitente.",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "Esto es independiente de {setting} en General, que determina cómo se escriben las fechas, horas y números. Puede leer una interfaz en inglés con fechas en español, o al revés.",
@@ -1134,40 +1135,40 @@ export const catalog: Catalog = {
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "Esta pantalla no es táctil, así que nada de esto cambia su comportamiento. Su teléfono o tableta tomará estos ajustes.",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "Mantener pulsado un mensaje lo selecciona, y mantener pulsada una carpeta abre su menú. Tire hacia abajo de la parte superior de la lista para comprobar si hay correo nuevo.",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "Una confirmación le dice a quien la pidió que esta dirección está activa y cuándo se leyó el mensaje, y el remitente elige adónde va; por eso no hay opción automática. Al correo masivo, las listas de correo y todo lo marcado como enviado automáticamente nunca se les ofrece una.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "Este navegador no puede registrar aplicaciones para los enlaces {scheme}. Safari, en particular, no dispone de esa interfaz: aun así puede establecer ihasmail como predeterminado desde su sistema operativo si lo instala como aplicación.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "Este navegador no puede registrar aplicaciones para los enlaces {scheme}. Safari, en particular, no dispone de esa interfaz: aun as\u00ed puede establecer {app} como predeterminado desde su sistema operativo si lo instala como aplicaci\u00f3n.",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "Registrarse para los enlaces {scheme} requiere una conexión segura (HTTPS).",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "Abrir los enlaces {scheme} —en páginas web, documentos y otras aplicaciones— con ihasmail en lugar de con un cliente de correo local. Su navegador le pedirá confirmación, y podrá cambiarlo más tarde en su propia configuración (Chrome: Configuración Privacidad y seguridad Configuración de sitios Controladores de protocolo; Firefox: Configuración General Aplicaciones).",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "Abrir los enlaces {scheme} \u2014en p\u00e1ginas web, documentos y otras aplicaciones\u2014 con {app} en lugar de con un cliente de correo local. Su navegador le pedir\u00e1 confirmaci\u00f3n, y podr\u00e1 cambiarlo m\u00e1s tarde en su propia configuraci\u00f3n (Chrome: Configuraci\u00f3n \u203a Privacidad y seguridad \u203a Configuraci\u00f3n de sitios \u203a Controladores de protocolo; Firefox: Configuraci\u00f3n \u203a General \u203a Aplicaciones).",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "Solicitado en este navegador. Que haya surtido efecto depende de él: revise su configuración si los enlaces de correo siguen abriéndose en otro sitio.",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "Para un valor predeterminado en todo el sistema, instale antes ihasmail como aplicación (en Chrome: el icono de instalación de la barra de direcciones). Su sistema operativo podrá entonces ofrecer ihasmail directamente allí donde pregunte qué aplicación de correo usar.",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "Para un valor predeterminado en todo el sistema, instale antes {app} como aplicación (en Chrome: el icono de instalación de la barra de direcciones). Su sistema operativo podrá entonces ofrecer {app} directamente allí donde pregunte qué aplicación de correo usar.",
"Needs a browser with the Push API and a mail server that publishes a push key.": "Requiere un navegador con la API Push y un servidor de correo que publique una clave push.",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "Su servidor de correo las entrega directamente a su navegador, así que llegan sin ninguna pestaña de ihasmail abierta, con el remitente y el asunto. Aun así, su navegador debe estar en marcha: si lo cierra por completo, las notificaciones esperan y llegan cuando vuelva a abrirlo.",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "Su servidor de correo las entrega directamente a su navegador, as\u00ed que llegan sin ninguna pesta\u00f1a de {app} abierta, con el remitente y el asunto. Aun as\u00ed, su navegador debe estar en marcha: si lo cierra por completo, las notificaciones esperan y llegan cuando vuelva a abrirlo.",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "Su servidor de correo puede despertar a este navegador, pero no incluirá el remitente ni el asunto. Aun así, su navegador debe estar en marcha.",
"This is what a new-mail notification looks like.": "Así es una notificación de correo nuevo.",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "Ha iniciado sesión como {user}. Su contraseña nunca se guarda en el navegador; el servidor la conserva cifrada por sesión para comunicarse con Stalwart.",
"App passwords are managed by your mail administrator.": "Las contraseñas de aplicación las gestiona su administrador de correo.",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "Cambiar la contraseña cierra sus demás sesiones de webmail. Las contraseñas de aplicación siguen funcionando.",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "Esta cuenta tiene activada la autenticación en dos pasos. ihasmail todavía no puede iniciar su sesión con un código, así que iniciar sesión en otro dispositivo requiere una contraseña de aplicación; o puede desactivar aquí la autenticación en dos pasos.",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "Esta cuenta tiene activada la autenticaci\u00f3n en dos pasos. {app} todav\u00eda no puede iniciar su sesi\u00f3n con un c\u00f3digo, as\u00ed que iniciar sesi\u00f3n en otro dispositivo requiere una contrase\u00f1a de aplicaci\u00f3n; o puede desactivar aqu\u00ed la autenticaci\u00f3n en dos pasos.",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "Una contraseña aparte para una aplicación de correo o un dispositivo, que puede revocar por separado. Las contraseñas de aplicación se saltan los códigos de dos pasos, así que siguen funcionando en aplicaciones que no pueden pedir uno.",
"Copy it into {name} now — it isn't shown again.": "Cópiela ahora en {name}: no se volverá a mostrar.",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "No se han encontrado más usuarios en el directorio, así que no se puede añadir a nadie nuevo. Lo que ya está compartido aparece abajo y todavía se puede quitar.",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart no comunica su número de versión a los clientes de correo, así que ihasmail indica la edición cuando el servidor la proporciona. ihasmail requiere la versión 0.16 o posterior, y el inicio de sesión rechaza cualquier versión anterior.",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart no comunica su n\u00famero de versi\u00f3n a los clientes de correo, as\u00ed que {app} indica la edici\u00f3n cuando el servidor la proporciona. {app} requiere la versi\u00f3n 0.16 o posterior, y el inicio de sesi\u00f3n rechaza cualquier versi\u00f3n anterior.",
"It {damage}, so the rules in it can't be shown or edited — saving what did arrive would write it back over the rest. Reload the page to try again. Your rules are still on the server; nothing here has changed them.": "{damage}, así que las reglas que contiene no se pueden mostrar ni editar: guardar lo que sí llegó sobrescribiría el resto. Recargue la página para intentarlo de nuevo. Sus reglas siguen en el servidor; aquí no se ha cambiado nada.",
"The visual rule editor only manages scripts it created. You can edit the script in the {tab} tab, or start fresh with rules (the existing script will be kept but deactivated).": "El editor visual de reglas solo gestiona los scripts que él mismo ha creado. Puede editar el script en la pestaña {tab}, o empezar de nuevo con reglas (el script existente se conservará pero quedará desactivado).",
"Your filter script {damage}, so only part of it arrived. Adding a rule would write that part back over the whole thing. Reload the page and try again.": "Su script de filtrado {damage}, así que solo ha llegado en parte. Añadir una regla escribiría esa parte sobre el conjunto. Recargue la página e inténtelo de nuevo.",
"Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.": "Su script de filtrado no se ha podido leer ahora mismo, así que añadir una regla podría sobrescribirlo. Recargue la página e inténtelo de nuevo.",
"Your active Sieve script was written by hand, so rules can't be added automatically. Open {where} to edit the script or switch to managed rules.": "Su script Sieve activo se escribió a mano, así que no se pueden añadir reglas automáticamente. Abra {where} para editar el script o cambiar a reglas gestionadas.",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Aquí solo aparecen los idiomas a los que se ha traducido ihasmail, así que la lista crece a medida que llegan las traducciones y no antes: un idioma ofrecido sin textos detrás haría que la página afirmara estar en un idioma que no es el suyo.",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Aqu\u00ed solo aparecen los idiomas a los que se ha traducido {app}, as\u00ed que la lista crece a medida que llegan las traducciones y no antes: un idioma ofrecido sin textos detr\u00e1s har\u00eda que la p\u00e1gina afirmara estar en un idioma que no es el suyo.",
"tell us about it": "cuéntenoslo",
"This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Esta traducción la ha generado una IA y no la ha revisado ninguna persona de habla nativa, así que está marcada como Beta hasta que alguien la dé por buena. Todo lo que suene mal merece un aviso: {report}.",
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "La versión de ihasmail es la fecha del commit a partir del cual se compiló, seguida de su procedencia: {example} se compiló a partir de un commit del 30 de agosto de 2026 que llegó mediante la pull request 129. Un commit que no llegó por esa vía lleva en su lugar su SHA corto: {sha}. La versión no dice nada sobre Stalwart a propósito; lo que esta compilación necesita del servidor está en la línea de arriba.",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "La versi\u00f3n de {app} es la fecha del commit a partir del cual se compil\u00f3, seguida de su procedencia: {example} se compil\u00f3 a partir de un commit del 30 de agosto de 2026 que lleg\u00f3 mediante la pull request 129. Un commit que no lleg\u00f3 por esa v\u00eda lleva en su lugar su SHA corto: {sha}. La versi\u00f3n no dice nada sobre Stalwart a prop\u00f3sito; lo que esta compilaci\u00f3n necesita del servidor est\u00e1 en la l\u00ednea de arriba.",
// ── Composer status, calendar title ────────────────────────────────
"New message": "Mensaje nuevo",
"Start a new message with what was shared?": "¿Empezar un mensaje nuevo con lo que se ha compartido?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Se ha compartido algo con ihasmail. No se envía nada hasta que elija Enviar. Si no acaba de compartirlo usted, descártelo.",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Se ha compartido algo con {app}. No se envía nada hasta que elija Enviar. Si no acaba de compartirlo usted, descártelo.",
"Start a message": "Empezar mensaje",
"New mail": "Correo nuevo",
"Could not do that — open ihasmail and try again": "No se pudo hacer eso: abra ihasmail e inténtelo de nuevo",
"Could not do that \u2014 open {app} and try again": "No se pudo hacer eso: abra {app} e int\u00e9ntelo de nuevo",
"Sending…": "Enviando…",
"Saving…": "Guardando…",
"Error": "Error",
@@ -1337,7 +1338,7 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "Margen para deshacer: {seconds} s",
"You're all caught up": "Está todo al día",
"Your browser refused the request: {error}": "Su navegador rechazó la solicitud: {error}",
"Your browser will ask whether to open mail links in ihasmail": "Su navegador le preguntará si quiere abrir los enlaces de correo en ihasmail",
"Your browser will ask whether to open mail links in {app}": "Su navegador le preguntará si quiere abrir los enlaces de correo en {app}",
"Your message mentions an attachment, but nothing is attached.": "Su mensaje menciona un archivo adjunto, pero no hay ninguno.",
"event": "evento",
"Hide password": "Ocultar la contraseña",
@@ -1364,6 +1365,8 @@ export const catalog: Catalog = {
"Collapse all": "Contraer todo",
"Expand all": "Expandir todo",
"Send now instead": "Enviar ahora, sin programar",
"This message is rich text": "Este mensaje es texto enriquecido",
"This message is plain text": "Este mensaje es texto sin formato",
"Switch to plain text": "Cambiar a texto sin formato",
"Switch to rich text": "Cambiar a texto enriquecido",
"{used} of {total} used": "{used} de {total} usados",
@@ -1408,7 +1411,7 @@ export const catalog: Catalog = {
"Also count these domains as inside": "Contar también estos dominios como internos",
"Always": "Siempre",
"Always showing images from": "Mostrando siempre las imágenes de",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "Una imagen cargada desde el servidor del remitente le indica que el mensaje se abrió, cuándo y desde dónde aproximadamente. Las imágenes aprobadas las descarga el propio servidor de ihasmail y no el navegador, de modo que el remitente no se entera de nada de eso.",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "Una imagen cargada desde el servidor del remitente le indica que el mensaje se abrió, cuándo y desde dónde aproximadamente. Las imágenes aprobadas las descarga el propio servidor de {app} y no el navegador, de modo que el remitente no se entera de nada de eso.",
"Applies to": "Se aplica a",
"Archive and next": "Archivar y siguiente",
"Archive by month": "Archivar por mes",
@@ -1650,8 +1653,8 @@ export const catalog: Catalog = {
"Fingerprint": "Huella digital",
"Hide details": "Ocultar detalles",
"Issued by": "Emitido por",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "Está firmado con OpenPGP, y ihasmail no tiene forma de obtener la clave pública del remitente.",
"It uses a signature algorithm ihasmail cannot check yet.": "Usa un algoritmo de firma que ihasmail todavía no puede comprobar.",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "Está firmado con OpenPGP, y {app} no tiene forma de obtener la clave pública del remitente.",
"It uses a signature algorithm {app} cannot check yet.": "Usa un algoritmo de firma que {app} todavía no puede comprobar.",
"It was made with a certificate belonging to {name}, which does not cover this address.": "Se hizo con un certificado de {name}, que no cubre esta dirección.",
"Previous fingerprint": "Huella digital anterior",
"Signed at": "Firmado el",
@@ -1668,14 +1671,14 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "La firma no corresponde a este remitente.",
"The signed part is missing either the message or the signature.": "A la parte firmada le falta el mensaje o la firma.",
"The signer has changed.": "El firmante ha cambiado.",
"This message is signed, and ihasmail could not check the signature.": "Este mensaje está firmado, y ihasmail no ha podido comprobar la firma.",
"This message is signed, and {app} could not check the signature.": "Este mensaje está firmado, y {app} no ha podido comprobar la firma.",
"This signature does not check out.": "Esta firma no cuadra.",
"Valid until": "Válido hasta",
"a different certificate": "un certificado distinto",
"an unnamed signer": "un firmante sin nombre",
"as claimed by the signer": "según declara el firmante",
"first seen {date}": "visto por primera vez el {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "ihasmail le avisará si un mensaje posterior de esta dirección lo firma otra persona.",
"{app} will tell you if a later message from this address is signed by anybody else.": "{app} le avisará si un mensaje posterior de esta dirección lo firma otra persona.",
"itself, or an issuer it does not name": "sí mismo, o un emisor que no nombra",
"no address": "ninguna dirección",
},
+25 -22
View File
@@ -515,7 +515,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "En attente sur le serveur — envoi {when}.",
"Scheduled — click to clear the schedule": "Programmé — cliquez pour annuler la programmation",
"Nothing scheduled": "Rien de programmé",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "Le message attend sur le serveur : il part que ihasmail soit ouvert ou non.",
"The message waits on the server, so it goes out whether or not {app} is open.": "Le message attend sur le serveur : il part que {app} soit ouvert ou non.",
"This server holds a message for up to {span}.": "Ce serveur conserve un message jusqu'à {span}.",
"Date and time to send": "Date et heure d'envoi",
"Undo send window": "Délai d'annulation d'envoi",
@@ -738,7 +738,7 @@ export const catalog: Catalog = {
"Sections": "Sections",
"General": "Général",
"Appearance": "Apparence",
"Make ihasmail yours.": "Faites de ihasmail le vôtre.",
"Make {app} yours.": "Faites de {app} le v\u00f4tre.",
"Reading": "Lecture",
"Reading pane": "Volet de lecture",
"Right of the list": "À droite de la liste",
@@ -755,6 +755,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Rappel de pièce jointe",
"Warn when the message mentions an attachment but none is attached.": "Avertir lorsque le message mentionne une pièce jointe alors qu'aucune n'est jointe.",
"Spell check while typing": "Vérification orthographique pendant la saisie",
"Open the composer full screen": "Rédiger les messages en plein écran",
"Confirm before deleting": "Confirmer avant de supprimer",
"Show message snippets": "Afficher un aperçu des messages",
"Preview the first line of each message in the list.": "Afficher la première ligne de chaque message dans la liste.",
@@ -834,7 +835,7 @@ export const catalog: Catalog = {
"Reset to defaults": "Rétablir les valeurs par défaut",
"Default mail app": "Application de messagerie par défaut",
"Documentation": "Documentation",
"About ihasmail": "À propos de ihasmail",
"About {app}": "À propos de {app}",
"About": "À propos",
"Server": "Serveur",
"Server capabilities": "Fonctionnalités du serveur",
@@ -962,8 +963,8 @@ export const catalog: Catalog = {
"Notifications": "Notifications",
"Notifications are blocked in your browser settings.": "Les notifications sont bloquées dans les paramètres de votre navigateur.",
"Not supported in this browser.": "Non pris en charge par ce navigateur.",
"Desktop notifications while ihasmail is open": "Notifications système lorsque ihasmail est ouvert",
"Notify me even when ihasmail is closed": "Me notifier même lorsque ihasmail est fermé",
"Desktop notifications while {app} is open": "Notifications système lorsque {app} est ouvert",
"Notify me even when {app} is closed": "Me notifier même lorsque {app} est fermé",
"Play a sound for new mail": "Émettre un son à l'arrivée d'un message",
"Test notification": "Tester la notification",
"Background notifications are on": "Les notifications en arrière-plan sont activées",
@@ -1131,7 +1132,7 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "Une nouvelle identité doit utiliser une adresse depuis laquelle ce compte est autorisé à envoyer (alias configurés sur le serveur).",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "Non proposée lors de la rédaction. L'adresse reçoit toujours du courrier, et vous pouvez de nouveau envoyer depuis elle en la réaffichant.",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "Chaque identité est une adresse d'expédition avec son propre nom, sa propre adresse de réponse et sa propre signature. L'identité par défaut est présélectionnée à la rédaction ; définissez une adresse de réponse lorsque les réponses doivent arriver ailleurs qu'à l'adresse d'expédition.",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "Cette signature dépasse la limite de {limit} octets du serveur. ihasmail conservera la version complète dans vos Fichiers et enregistrera une version texte courte sur le serveur les autres clients verront la version en texte brut.",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "Cette signature d\u00e9passe la limite de {limit} octets du serveur. {app} conservera la version compl\u00e8te dans vos Fichiers et enregistrera une version texte courte sur le serveur \u2014 les autres clients verront la version en texte brut.",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Des catégories façon Outlook, attribuables aux événements depuis le menu contextuel ou l'éditeur d'événement. Le nom de la catégorie est enregistré dans l'événement et se synchronise donc avec les autres clients.",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "Les messages en texte brut suivent déjà le thème. Avec cette option, les messages HTML sans couleurs propres le suivent aussi, au lieu de s'afficher sur un fond blanc. Les messages qui définissent leur propre style restent exactement tels que l'expéditeur les a conçus.",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "Ceci est indépendant de {setting} dans Général, qui détermine l'écriture des dates, heures et nombres. Vous pouvez lire une interface anglaise avec des dates françaises, ou l'inverse.",
@@ -1139,40 +1140,40 @@ export const catalog: Catalog = {
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "Cet écran n'est pas tactile : rien ici ne change son comportement. Votre téléphone ou votre tablette reprendra ces réglages.",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "Un appui long sur un message le sélectionne, un appui long sur un dossier ouvre son menu. Tirez le haut de la liste vers le bas pour relever le courrier.",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "Un accusé indique au demandeur que cette adresse est active et à quel moment le message a été lu, et l'expéditeur choisit où il est envoyé — il n'y a donc pas d'option automatique. Le courrier de masse, les listes de diffusion et tout ce qui est marqué comme envoyé automatiquement n'en obtiennent jamais.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "Ce navigateur ne peut pas enregistrer d'applications pour les liens {scheme}. Safari, en particulier, n'a pas d'interface pour cela vous pouvez tout de même définir ihasmail par défaut depuis votre système d'exploitation en l'installant comme application.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "Ce navigateur ne peut pas enregistrer d'applications pour les liens {scheme}. Safari, en particulier, n'a pas d'interface pour cela \u2014 vous pouvez tout de m\u00eame d\u00e9finir {app} par d\u00e9faut depuis votre syst\u00e8me d'exploitation en l'installant comme application.",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "L'enregistrement pour les liens {scheme} nécessite une connexion sécurisée (HTTPS).",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "Ouvrir les liens {scheme} dans les pages web, les documents et les autres applications — avec ihasmail plutôt qu'avec un client de messagerie local. Votre navigateur vous demandera de confirmer, et vous pourrez le modifier plus tard dans ses propres paramètres (Chrome : Paramètres Confidentialité et sécurité Paramètres des sites Gestionnaires de protocole ; Firefox : Paramètres Général Applications).",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "Ouvrir les liens {scheme} \u2014 dans les pages web, les documents et les autres applications \u2014 avec {app} plut\u00f4t qu'avec un client de messagerie local. Votre navigateur vous demandera de confirmer, et vous pourrez le modifier plus tard dans ses propres param\u00e8tres (Chrome : Param\u00e8tres \u203a Confidentialit\u00e9 et s\u00e9curit\u00e9 \u203a Param\u00e8tres des sites \u203a Gestionnaires de protocole ; Firefox : Param\u00e8tres \u203a G\u00e9n\u00e9ral \u203a Applications).",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "Demandé dans ce navigateur. C'est à lui de décider si cela a pris effet — vérifiez ses paramètres si les liens de messagerie s'ouvrent toujours ailleurs.",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "Pour un réglage valable dans tout le système, installez d'abord ihasmail comme application (dans Chrome : l'icône d'installation dans la barre d'adresse). Votre système pourra alors proposer ihasmail directement partout où il demande quelle application de messagerie utiliser.",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "Pour un réglage valable dans tout le système, installez d'abord {app} comme application (dans Chrome : l'icône d'installation dans la barre d'adresse). Votre système pourra alors proposer {app} directement partout où il demande quelle application de messagerie utiliser.",
"Needs a browser with the Push API and a mail server that publishes a push key.": "Nécessite un navigateur doté de l'API Push et un serveur de messagerie publiant une clé push.",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "Votre serveur les remet directement à votre navigateur : elles arrivent donc sans onglet ihasmail ouvert, avec l'expéditeur et l'objet. Votre navigateur doit tout de même être en cours d'exécution si vous le quittez complètement, les notifications attendent et arrivent à sa réouverture.",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "Votre serveur les remet directement \u00e0 votre navigateur : elles arrivent donc sans onglet {app} ouvert, avec l'exp\u00e9diteur et l'objet. Votre navigateur doit tout de m\u00eame \u00eatre en cours d'ex\u00e9cution \u2014 si vous le quittez compl\u00e8tement, les notifications attendent et arrivent \u00e0 sa r\u00e9ouverture.",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "Votre serveur peut réveiller ce navigateur, mais sans indiquer l'expéditeur ni l'objet. Votre navigateur doit tout de même être en cours d'exécution.",
"This is what a new-mail notification looks like.": "Voici à quoi ressemble une notification de nouveau message.",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "Vous êtes connecté en tant que {user}. Votre mot de passe n'est jamais enregistré dans le navigateur ; le serveur le conserve chiffré, par session, pour dialoguer avec Stalwart.",
"App passwords are managed by your mail administrator.": "Les mots de passe d'application sont gérés par votre administrateur de messagerie.",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "Changer votre mot de passe déconnecte vos autres sessions webmail. Les mots de passe d'application continuent de fonctionner.",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "L'authentification à deux facteurs est activée sur ce compte. ihasmail ne sait pas encore vous connecter avec un code : la connexion sur un autre appareil nécessite donc un mot de passe d'application ou vous pouvez désactiver l'authentification à deux facteurs ici.",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "L'authentification \u00e0 deux facteurs est activ\u00e9e sur ce compte. {app} ne sait pas encore vous connecter avec un code : la connexion sur un autre appareil n\u00e9cessite donc un mot de passe d'application \u2014 ou vous pouvez d\u00e9sactiver l'authentification \u00e0 deux facteurs ici.",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "Un mot de passe distinct pour une application ou un appareil, révocable indépendamment. Les mots de passe d'application contournent les codes à deux facteurs et fonctionnent donc dans les applications qui ne peuvent pas en demander.",
"Copy it into {name} now — it isn't shown again.": "Copiez-le dans {name} maintenant — il ne sera plus affiché.",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "Aucun autre utilisateur trouvé dans l'annuaire : personne de nouveau ne peut être ajouté. Les partages déjà en place sont listés ci-dessous et restent supprimables.",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart ne communique pas son numéro de version aux clients de messagerie ; ihasmail indique donc l'édition lorsque le serveur en fournit une. ihasmail requiert la version 0.16 ou ultérieure, et la connexion refuse toute version antérieure.",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart ne communique pas son num\u00e9ro de version aux clients de messagerie ; {app} indique donc l'\u00e9dition lorsque le serveur en fournit une. {app} requiert la version 0.16 ou ult\u00e9rieure, et la connexion refuse toute version ant\u00e9rieure.",
"It {damage}, so the rules in it can't be shown or edited — saving what did arrive would write it back over the rest. Reload the page to try again. Your rules are still on the server; nothing here has changed them.": "Il {damage} : les règles qu'il contient ne peuvent donc être ni affichées ni modifiées — enregistrer ce qui est arrivé écraserait le reste. Rechargez la page pour réessayer. Vos règles sont toujours sur le serveur ; rien ici ne les a modifiées.",
"The visual rule editor only manages scripts it created. You can edit the script in the {tab} tab, or start fresh with rules (the existing script will be kept but deactivated).": "L'éditeur visuel de règles ne gère que les scripts qu'il a créés. Vous pouvez modifier le script dans l'onglet {tab}, ou repartir de zéro avec des règles (le script existant sera conservé mais désactivé).",
"Your filter script {damage}, so only part of it arrived. Adding a rule would write that part back over the whole thing. Reload the page and try again.": "Votre script de filtrage {damage} : il n'est arrivé que partiellement. Ajouter une règle écraserait l'ensemble par cette partie. Rechargez la page et réessayez.",
"Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.": "Votre script de filtrage n'a pas pu être lu à l'instant ; ajouter une règle risquerait de l'écraser. Rechargez la page et réessayez.",
"Your active Sieve script was written by hand, so rules can't be added automatically. Open {where} to edit the script or switch to managed rules.": "Votre script Sieve actif a été écrit à la main : les règles ne peuvent donc pas être ajoutées automatiquement. Ouvrez {where} pour modifier le script ou passer aux règles gérées.",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Seules les langues dans lesquelles ihasmail a été traduit apparaissent ici : la liste s'allonge donc à mesure que les traductions arrivent, et non avant une langue proposée sans textes derrière elle ferait prétendre à la page qu'elle est dans une langue qui n'est pas la sienne.",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Seules les langues dans lesquelles {app} a \u00e9t\u00e9 traduit apparaissent ici : la liste s'allonge donc \u00e0 mesure que les traductions arrivent, et non avant \u2014 une langue propos\u00e9e sans textes derri\u00e8re elle ferait pr\u00e9tendre \u00e0 la page qu'elle est dans une langue qui n'est pas la sienne.",
"tell us about it": "signalez-le-nous",
"This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Cette traduction a été générée par une IA et n'a pas été relue par une personne de langue maternelle française ; elle est donc marquée Beta jusqu'à validation. Tout ce qui sonne faux mérite d'être signalé — {report}.",
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "La version de ihasmail est la date du commit à partir duquel elle a été construite, suivie de l'origine de ce commit : {example} provient d'un commit daté du 30 août 2026 arrivé via la pull request 129. Un commit qui n'est pas passé par là porte à la place son SHA court {sha}. La version ne dit délibérément rien de Stalwart ; ce dont cette build a besoin du serveur figure à la ligne ci-dessus.",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "La version de {app} est la date du commit \u00e0 partir duquel elle a \u00e9t\u00e9 construite, suivie de l'origine de ce commit : {example} provient d'un commit dat\u00e9 du 30 ao\u00fbt 2026 arriv\u00e9 via la pull request 129. Un commit qui n'est pas pass\u00e9 par l\u00e0 porte \u00e0 la place son SHA court \u2014 {sha}. La version ne dit d\u00e9lib\u00e9r\u00e9ment rien de Stalwart ; ce dont cette build a besoin du serveur figure \u00e0 la ligne ci-dessus.",
// ── Composer status, calendar title ────────────────────────────────
"New message": "Nouveau message",
"Start a new message with what was shared?": "Commencer un nouveau message avec le contenu partagé ?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Un contenu a été partagé avec ihasmail. Rien n'est envoyé tant que vous n'avez pas choisi Envoyer. Si vous ne venez pas de le partager, abandonnez-le.",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Un contenu a été partagé avec {app}. Rien n'est envoyé tant que vous n'avez pas choisi Envoyer. Si vous ne venez pas de le partager, abandonnez-le.",
"Start a message": "Commencer un message",
"New mail": "Nouveau courrier",
"Could not do that — open ihasmail and try again": "Impossible : ouvrez ihasmail et réessayez",
"Could not do that \u2014 open {app} and try again": "Impossible : ouvrez {app} et r\u00e9essayez",
"Sending…": "Envoi…",
"Saving…": "Enregistrement…",
"Error": "Erreur",
@@ -1342,7 +1343,7 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "Délai dannulation : {seconds} s",
"You're all caught up": "Vous êtes à jour",
"Your browser refused the request: {error}": "Votre navigateur a refusé la demande : {error}",
"Your browser will ask whether to open mail links in ihasmail": "Votre navigateur vous demandera sil faut ouvrir les liens de courrier dans ihasmail",
"Your browser will ask whether to open mail links in {app}": "Votre navigateur vous demandera sil faut ouvrir les liens de courrier dans {app}",
"Your message mentions an attachment, but nothing is attached.": "Votre message mentionne une pièce jointe, mais rien nest joint.",
"event": "événement",
"Hide password": "Masquer le mot de passe",
@@ -1369,6 +1370,8 @@ export const catalog: Catalog = {
"Collapse all": "Tout réduire",
"Expand all": "Tout développer",
"Send now instead": "Envoyer tout de suite",
"This message is rich text": "Ce message est en texte enrichi",
"This message is plain text": "Ce message est en texte brut",
"Switch to plain text": "Passer en texte brut",
"Switch to rich text": "Passer en texte enrichi",
"{used} of {total} used": "{used} sur {total} utilisés",
@@ -1413,7 +1416,7 @@ export const catalog: Catalog = {
"Also count these domains as inside": "Considérer aussi ces domaines comme internes",
"Always": "Toujours",
"Always showing images from": "Images toujours affichées depuis",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "Une image chargée depuis le serveur de l'expéditeur lui indique que le message a été ouvert, quand et approximativement d'où. Les images approuvées sont récupérées par le serveur d'ihasmail et non par le navigateur, de sorte que l'expéditeur n'apprend rien de tout cela.",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "Une image chargée depuis le serveur de l'expéditeur lui indique que le message a été ouvert, quand et approximativement d'où. Les images approuvées sont récupérées par le serveur d'{app} et non par le navigateur, de sorte que l'expéditeur n'apprend rien de tout cela.",
"Applies to": "S'applique à",
"Archive and next": "Archiver et suivant",
"Archive by month": "Archiver par mois",
@@ -1655,8 +1658,8 @@ export const catalog: Catalog = {
"Fingerprint": "Empreinte",
"Hide details": "Masquer les détails",
"Issued by": "Délivré par",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "Il est signé avec OpenPGP, et ihasmail n'a aucun moyen de récupérer la clé publique de l'expéditeur.",
"It uses a signature algorithm ihasmail cannot check yet.": "Il utilise un algorithme de signature qu'ihasmail ne sait pas encore vérifier.",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "Il est signé avec OpenPGP, et {app} n'a aucun moyen de récupérer la clé publique de l'expéditeur.",
"It uses a signature algorithm {app} cannot check yet.": "Il utilise un algorithme de signature qu'{app} ne sait pas encore vérifier.",
"It was made with a certificate belonging to {name}, which does not cover this address.": "Elle a été faite avec un certificat appartenant à {name}, qui ne couvre pas cette adresse.",
"Previous fingerprint": "Empreinte précédente",
"Signed at": "Signé le",
@@ -1673,14 +1676,14 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "La signature ne correspond pas à cet expéditeur.",
"The signed part is missing either the message or the signature.": "Il manque à la partie signée soit le message, soit la signature.",
"The signer has changed.": "Le signataire a changé.",
"This message is signed, and ihasmail could not check the signature.": "Ce message est signé, et ihasmail n'a pas pu vérifier la signature.",
"This message is signed, and {app} could not check the signature.": "Ce message est signé, et {app} n'a pas pu vérifier la signature.",
"This signature does not check out.": "Cette signature ne tient pas.",
"Valid until": "Valable jusqu'au",
"a different certificate": "un certificat différent",
"an unnamed signer": "un signataire sans nom",
"as claimed by the signer": "selon le signataire",
"first seen {date}": "vu pour la première fois le {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "ihasmail vous préviendra si un message ultérieur de cette adresse est signé par quelqu'un d'autre.",
"{app} will tell you if a later message from this address is signed by anybody else.": "{app} vous préviendra si un message ultérieur de cette adresse est signé par quelqu'un d'autre.",
"itself, or an issuer it does not name": "lui-même, ou un émetteur qu'il ne nomme pas",
"no address": "aucune adresse",
},
+25 -22
View File
@@ -509,7 +509,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "サーバーで待機中です。{when} に送信されます。",
"Scheduled — click to clear the schedule": "予約済み — クリックすると予約を解除します",
"Nothing scheduled": "予約されたメールはありません",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "メールはサーバーで待機するため、ihasmail を開いていなくても送信されます。",
"The message waits on the server, so it goes out whether or not {app} is open.": "メールはサーバーで待機するため、{app} を開いていなくても送信されます。",
"This server holds a message for up to {span}.": "このサーバーがメールを保持できるのは最長 {span} です。",
"Date and time to send": "送信する日時",
"Undo send window": "送信取り消しの猶予時間",
@@ -732,7 +732,7 @@ export const catalog: Catalog = {
"Sections": "セクション",
"General": "一般",
"Appearance": "外観",
"Make ihasmail yours.": "ihasmail を自分好みに整えましょう。",
"Make {app} yours.": "{app} \u3092\u81ea\u5206\u597d\u307f\u306b\u6574\u3048\u307e\u3057\u3087\u3046\u3002",
"Reading": "閲覧",
"Reading pane": "プレビューウィンドウ",
"Right of the list": "一覧の右",
@@ -749,6 +749,7 @@ export const catalog: Catalog = {
"Attachment reminder": "添付忘れの確認",
"Warn when the message mentions an attachment but none is attached.": "本文で添付に触れているのにファイルが添付されていないとき警告します。",
"Spell check while typing": "入力中にスペルチェックする",
"Open the composer full screen": "メールを全画面で作成",
"Confirm before deleting": "削除前に確認する",
"Show message snippets": "本文の抜粋を表示する",
"Preview the first line of each message in the list.": "一覧に各メールの 1 行目を表示します。",
@@ -828,7 +829,7 @@ export const catalog: Catalog = {
"Reset to defaults": "既定に戻す",
"Default mail app": "既定のメールアプリ",
"Documentation": "ドキュメント",
"About ihasmail": "ihasmail について",
"About {app}": "{app} について",
"About": "情報",
"Server": "サーバー",
"Server capabilities": "サーバーの機能",
@@ -957,8 +958,8 @@ export const catalog: Catalog = {
"Notifications": "通知",
"Notifications are blocked in your browser settings.": "ブラウザーの設定で通知がブロックされています。",
"Not supported in this browser.": "このブラウザーでは利用できません。",
"Desktop notifications while ihasmail is open": "ihasmail を開いている間のデスクトップ通知",
"Notify me even when ihasmail is closed": "ihasmail を閉じているときも通知する",
"Desktop notifications while {app} is open": "{app} を開いている間のデスクトップ通知",
"Notify me even when {app} is closed": "{app} を閉じているときも通知する",
"Play a sound for new mail": "新着メールで音を鳴らす",
"Test notification": "通知をテスト",
"Background notifications are on": "バックグラウンド通知はオンです",
@@ -1072,7 +1073,7 @@ export const catalog: Catalog = {
// ── Settings prose ─────────────────────────────────────────────────
"tell us about it": "お知らせください",
"This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "この翻訳は AI が生成したもので、母語話者による確認をまだ受けていません。そのため、話者による確認が済むまで Beta と表示しています。おかしいと感じた箇所は、ぜひ{report}。",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "ここに並ぶのは ihasmail の翻訳がある言語だけです。そのため、この一覧は翻訳が届いてから増えていきます。訳文のない言語を選べるようにすると、実際とは違う言語のページだと名乗ることになってしまいます。",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "\u3053\u3053\u306b\u4e26\u3076\u306e\u306f {app} \u306e\u7ffb\u8a33\u304c\u3042\u308b\u8a00\u8a9e\u3060\u3051\u3067\u3059\u3002\u305d\u306e\u305f\u3081\u3001\u3053\u306e\u4e00\u89a7\u306f\u7ffb\u8a33\u304c\u5c4a\u3044\u3066\u304b\u3089\u5897\u3048\u3066\u3044\u304d\u307e\u3059\u3002\u8a33\u6587\u306e\u306a\u3044\u8a00\u8a9e\u3092\u9078\u3079\u308b\u3088\u3046\u306b\u3059\u308b\u3068\u3001\u5b9f\u969b\u3068\u306f\u9055\u3046\u8a00\u8a9e\u306e\u30da\u30fc\u30b8\u3060\u3068\u540d\u4e57\u308b\u3053\u3068\u306b\u306a\u3063\u3066\u3057\u307e\u3044\u307e\u3059\u3002",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "これは「一般」の{setting}とは別の設定です。あちらは日付・時刻・数値の書き方を決めます。英語の画面にドイツ語式の日付を組み合わせることも、その逆もできます。",
"Defaults for the calendar views and new events.": "カレンダーの表示と新しい予定の既定値です。",
"Replies will go to this address instead of the From address": "返信は差出人アドレスではなく、このアドレスに届きます",
@@ -1080,31 +1081,31 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "新しい差出人には、このアカウントが送信を許可されているアドレス(サーバーで設定されたエイリアス)を使う必要があります。",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "作成時には表示されません。メールの受信は続き、再び表示すればこの差出人で送信することもできます。",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "差出人とは、それぞれ名前・返信先・署名を持つ送信用アドレスのことです。作成時には既定の差出人があらかじめ選ばれます。返信を差出人アドレス以外へ届けたい場合は、返信先を設定してください。",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "この署名はサーバーの上限 {limit} バイトを超えています。ihasmail は完全版を「ファイル」に保存し、サーバーには短いテキスト版を置きます。他のメールクライアントにはテキスト版が表示されます。",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "\u3053\u306e\u7f72\u540d\u306f\u30b5\u30fc\u30d0\u30fc\u306e\u4e0a\u9650 {limit} \u30d0\u30a4\u30c8\u3092\u8d85\u3048\u3066\u3044\u307e\u3059\u3002{app} \u306f\u5b8c\u5168\u7248\u3092\u300c\u30d5\u30a1\u30a4\u30eb\u300d\u306b\u4fdd\u5b58\u3057\u3001\u30b5\u30fc\u30d0\u30fc\u306b\u306f\u77ed\u3044\u30c6\u30ad\u30b9\u30c8\u7248\u3092\u7f6e\u304d\u307e\u3059\u3002\u4ed6\u306e\u30e1\u30fc\u30eb\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306b\u306f\u30c6\u30ad\u30b9\u30c8\u7248\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Outlook 形式の分類です。右クリックメニューや予定の編集画面から予定に割り当てられます。分類名は予定に保存されるため、他のクライアントにも同期されます。",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "プレーンテキストのメールは、もともとテーマに従います。これをオンにすると、独自の配色を持たない HTML メールもテーマに従い、白いカードの上に置かれなくなります。自分でスタイルを指定しているメールは、差出人が作ったとおりに表示されます。",
"On a touchscreen, drag a message sideways to act on it. Each direction can do one thing, or nothing. These follow your account, so a phone and a tablet agree; a mouse ignores them and keeps dragging messages into folders instead.": "タッチ画面では、メールを横にドラッグすると操作できます。各方向に 1 つの操作を割り当てるか、何も割り当てないかを選べます。この設定はアカウントに従うため、スマートフォンとタブレットで揃います。マウスはこの設定を無視し、これまでどおりメールをフォルダーへドラッグします。",
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "この画面にはタッチ機能がないため、ここでの設定は動作に影響しません。スマートフォンやタブレットに反映されます。",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "メールを長押しすると選択、フォルダーを長押しするとメニューが開きます。メール一覧の上端を下に引くと、新着メールを確認できます。",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "開封確認を返すと、要求した相手にこのアドレスが使われていることと、いつ読んだかが伝わります。しかも送り先を決めるのは差出人です。そのため自動で返す選択肢はありません。一括配信のメール、メーリングリスト、自動送信と記されたメールには、そもそも確認を返す選択肢を表示しません。",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "このブラウザーは {scheme} リンクのアプリを登録できません。とくに Safari にはその API がありません。アプリとしてインストールすれば、OS の側で ihasmail を既定にすることはできます。",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "\u3053\u306e\u30d6\u30e9\u30a6\u30b6\u30fc\u306f {scheme} \u30ea\u30f3\u30af\u306e\u30a2\u30d7\u30ea\u3092\u767b\u9332\u3067\u304d\u307e\u305b\u3093\u3002\u3068\u304f\u306b Safari \u306b\u306f\u305d\u306e API \u304c\u3042\u308a\u307e\u305b\u3093\u3002\u30a2\u30d7\u30ea\u3068\u3057\u3066\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3059\u308c\u3070\u3001OS \u306e\u5074\u3067 {app} \u3092\u65e2\u5b9a\u306b\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u3059\u3002",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "{scheme} リンクの登録には安全な接続(HTTPS)が必要です。",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "ウェブページ・文書・他のアプリにある {scheme} リンクを、デスクトップのメールクライアントではなく ihasmail で開きます。ブラウザーが確認を求め、あとからブラウザー自身の設定で変更できます(Chrome: 設定 › プライバシーとセキュリティ › サイトの設定 › プロトコル ハンドラ、Firefox: 設定 › 一般 › プログラム)。",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "\u30a6\u30a7\u30d6\u30da\u30fc\u30b8\u30fb\u6587\u66f8\u30fb\u4ed6\u306e\u30a2\u30d7\u30ea\u306b\u3042\u308b {scheme} \u30ea\u30f3\u30af\u3092\u3001\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u306e\u30e1\u30fc\u30eb\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3067\u306f\u306a\u304f {app} \u3067\u958b\u304d\u307e\u3059\u3002\u30d6\u30e9\u30a6\u30b6\u30fc\u304c\u78ba\u8a8d\u3092\u6c42\u3081\u3001\u3042\u3068\u304b\u3089\u30d6\u30e9\u30a6\u30b6\u30fc\u81ea\u8eab\u306e\u8a2d\u5b9a\u3067\u5909\u66f4\u3067\u304d\u307e\u3059\uff08Chrome: \u8a2d\u5b9a \u203a \u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3068\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3 \u203a \u30b5\u30a4\u30c8\u306e\u8a2d\u5b9a \u203a \u30d7\u30ed\u30c8\u30b3\u30eb \u30cf\u30f3\u30c9\u30e9\u3001Firefox: \u8a2d\u5b9a \u203a \u4e00\u822c \u203a \u30d7\u30ed\u30b0\u30e9\u30e0\uff09\u3002",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "このブラウザーで登録を要求しました。実際に有効になるかどうかはブラウザー次第です。メールのリンクが別のアプリで開く場合は、ブラウザーの設定をご確認ください。",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "システム全体の既定にするには、まず ihasmail をアプリとしてインストールしてください(Chrome ではアドレスバーのインストールアイコン)。以後、OS がメールアプリを尋ねる場面で ihasmail を直接選べるようになります。",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "システム全体の既定にするには、まず {app} をアプリとしてインストールしてください(Chrome ではアドレスバーのインストールアイコン)。以後、OS がメールアプリを尋ねる場面で {app} を直接選べるようになります。",
"Needs a browser with the Push API and a mail server that publishes a push key.": "Push API に対応したブラウザーと、プッシュ用の鍵を公開しているメールサーバーが必要です。",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "メールサーバーが通知をブラウザーへ直接届けるため、ihasmail のタブを開いていなくても、差出人と件名つきで届きます。ただしブラウザーは起動している必要があります。完全に終了すると、通知は次に起動したときにまとめて届きます。",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "\u30e1\u30fc\u30eb\u30b5\u30fc\u30d0\u30fc\u304c\u901a\u77e5\u3092\u30d6\u30e9\u30a6\u30b6\u30fc\u3078\u76f4\u63a5\u5c4a\u3051\u308b\u305f\u3081\u3001{app} \u306e\u30bf\u30d6\u3092\u958b\u3044\u3066\u3044\u306a\u304f\u3066\u3082\u3001\u5dee\u51fa\u4eba\u3068\u4ef6\u540d\u3064\u304d\u3067\u5c4a\u304d\u307e\u3059\u3002\u305f\u3060\u3057\u30d6\u30e9\u30a6\u30b6\u30fc\u306f\u8d77\u52d5\u3057\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u5b8c\u5168\u306b\u7d42\u4e86\u3059\u308b\u3068\u3001\u901a\u77e5\u306f\u6b21\u306b\u8d77\u52d5\u3057\u305f\u3068\u304d\u306b\u307e\u3068\u3081\u3066\u5c4a\u304d\u307e\u3059\u3002",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "メールサーバーはこのブラウザーを呼び起こせますが、差出人や件名は含めません。ブラウザーは起動している必要があります。",
"This is what a new-mail notification looks like.": "新着メールの通知はこのように表示されます。",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "{user} としてサインインしています。パスワードがブラウザーに保存されることはありません。サーバーが Stalwart との通信のために、セッションごとに暗号化して保持します。",
"App passwords are managed by your mail administrator.": "アプリパスワードはメール管理者が管理しています。",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "パスワードを変更すると、他のウェブメールのセッションはサインアウトされます。アプリパスワードはそのまま使えます。",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "このアカウントでは 2 段階認証が有効です。ihasmail はまだ確認コードでのサインインに対応していないため、他のデバイスからサインインするにはアプリパスワードが必要です。ここで 2 段階認証をオフにすることもできます。",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "\u3053\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u306f 2 \u6bb5\u968e\u8a8d\u8a3c\u304c\u6709\u52b9\u3067\u3059\u3002{app} \u306f\u307e\u3060\u78ba\u8a8d\u30b3\u30fc\u30c9\u3067\u306e\u30b5\u30a4\u30f3\u30a4\u30f3\u306b\u5bfe\u5fdc\u3057\u3066\u3044\u306a\u3044\u305f\u3081\u3001\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u30b5\u30a4\u30f3\u30a4\u30f3\u3059\u308b\u306b\u306f\u30a2\u30d7\u30ea\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u3053\u3067 2 \u6bb5\u968e\u8a8d\u8a3c\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "メールアプリやデバイスごとに用意する別のパスワードで、単独で無効化できます。アプリパスワードは 2 段階認証の確認コードを省くため、コードを入力できないアプリでも使えます。",
"Copy it into {name} now — it isn't shown again.": "いま {name} にコピーしてください。二度と表示されません。",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "ディレクトリに他のユーザーが見つからないため、新しく追加することはできません。すでに設定されている共有は下に表示され、解除はできます。",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart はメールクライアントにバージョン番号を公開しないため、ihasmail はサーバーが示すエディションだけを表示します。ihasmail には 0.16 以降が必要で、それより古いサーバーへのサインインは拒否されます。",
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "ihasmail 自身のバージョンは、ビルド元となったコミットの日付と、そのコミットの出どころを並べたものです。{example} は 2026 年 8 月 30 日付のコミットから作られ、そのコミットはプルリクエスト 129 を通って届きました。プルリクエストを経ていないコミットは、代わりに短い SHA が付きます — {sha}。バージョンには Stalwart に関する情報をあえて含めていません。このビルドがサーバーに求めるものは、上の行に示されています。",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart \u306f\u30e1\u30fc\u30eb\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306b\u30d0\u30fc\u30b8\u30e7\u30f3\u756a\u53f7\u3092\u516c\u958b\u3057\u306a\u3044\u305f\u3081\u3001{app} \u306f\u30b5\u30fc\u30d0\u30fc\u304c\u793a\u3059\u30a8\u30c7\u30a3\u30b7\u30e7\u30f3\u3060\u3051\u3092\u8868\u793a\u3057\u307e\u3059\u3002{app} \u306b\u306f 0.16 \u4ee5\u964d\u304c\u5fc5\u8981\u3067\u3001\u305d\u308c\u3088\u308a\u53e4\u3044\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u30b5\u30a4\u30f3\u30a4\u30f3\u306f\u62d2\u5426\u3055\u308c\u307e\u3059\u3002",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "{app} \u81ea\u8eab\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306f\u3001\u30d3\u30eb\u30c9\u5143\u3068\u306a\u3063\u305f\u30b3\u30df\u30c3\u30c8\u306e\u65e5\u4ed8\u3068\u3001\u305d\u306e\u30b3\u30df\u30c3\u30c8\u306e\u51fa\u3069\u3053\u308d\u3092\u4e26\u3079\u305f\u3082\u306e\u3067\u3059\u3002{example} \u306f 2026 \u5e74 8 \u6708 30 \u65e5\u4ed8\u306e\u30b3\u30df\u30c3\u30c8\u304b\u3089\u4f5c\u3089\u308c\u3001\u305d\u306e\u30b3\u30df\u30c3\u30c8\u306f\u30d7\u30eb\u30ea\u30af\u30a8\u30b9\u30c8 129 \u3092\u901a\u3063\u3066\u5c4a\u304d\u307e\u3057\u305f\u3002\u30d7\u30eb\u30ea\u30af\u30a8\u30b9\u30c8\u3092\u7d4c\u3066\u3044\u306a\u3044\u30b3\u30df\u30c3\u30c8\u306f\u3001\u4ee3\u308f\u308a\u306b\u77ed\u3044 SHA \u304c\u4ed8\u304d\u307e\u3059 \u2014 {sha}\u3002\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u306f Stalwart \u306b\u95a2\u3059\u308b\u60c5\u5831\u3092\u3042\u3048\u3066\u542b\u3081\u3066\u3044\u307e\u305b\u3093\u3002\u3053\u306e\u30d3\u30eb\u30c9\u304c\u30b5\u30fc\u30d0\u30fc\u306b\u6c42\u3081\u308b\u3082\u306e\u306f\u3001\u4e0a\u306e\u884c\u306b\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u3002",
// ── Constant labels ────────────────────────────────────────────────
"Add": "追加",
@@ -1172,10 +1173,10 @@ export const catalog: Catalog = {
// ── Composer status, calendar title ────────────────────────────────
"New message": "新規メール",
"Start a new message with what was shared?": "共有された内容で新規メールを作成しますか?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "ihasmail に何かが共有されました。「送信」を選ぶまで何も送信されません。共有した覚えがない場合は破棄してください。",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "{app} に何かが共有されました。「送信」を選ぶまで何も送信されません。共有した覚えがない場合は破棄してください。",
"Start a message": "メールを作成",
"New mail": "新着メール",
"Could not do that — open ihasmail and try again": "実行できませんでした - ihasmail を開いてやり直してください",
"Could not do that \u2014 open {app} and try again": "\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f - {app} \u3092\u958b\u3044\u3066\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044",
"Sending…": "送信中…",
"Saving…": "保存中…",
"Error": "エラー",
@@ -1345,7 +1346,7 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "取り消せる時間: {seconds} 秒",
"You're all caught up": "未読はありません",
"Your browser refused the request: {error}": "ブラウザーが要求を拒否しました: {error}",
"Your browser will ask whether to open mail links in ihasmail": "メールのリンクを ihasmail で開くかどうか、ブラウザーが確認します",
"Your browser will ask whether to open mail links in {app}": "メールのリンクを {app} で開くかどうか、ブラウザーが確認します",
"Your message mentions an attachment, but nothing is attached.": "本文で添付ファイルに触れていますが、何も添付されていません。",
"event": "予定",
"Hide password": "パスワードを隠す",
@@ -1372,6 +1373,8 @@ export const catalog: Catalog = {
"Collapse all": "すべて折りたたむ",
"Expand all": "すべて展開",
"Send now instead": "予約をやめて今すぐ送信",
"This message is rich text": "このメールはリッチテキストです",
"This message is plain text": "このメールはプレーンテキストです",
"Switch to plain text": "プレーンテキストに切り替え",
"Switch to rich text": "リッチテキストに切り替え",
"{used} of {total} used": "{total} 中 {used} を使用",
@@ -1416,7 +1419,7 @@ export const catalog: Catalog = {
"Also count these domains as inside": "次のドメインも社内として扱う",
"Always": "常に",
"Always showing images from": "常に画像を表示する差出人",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "差出人のサーバーから読み込まれた画像は、メールが開かれたこと、その時刻、おおよその場所を差出人に伝えます。許可した画像はブラウザーではなく ihasmail のサーバーが取得するため、差出人にはそのいずれも伝わりません。",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "差出人のサーバーから読み込まれた画像は、メールが開かれたこと、その時刻、おおよその場所を差出人に伝えます。許可した画像はブラウザーではなく {app} のサーバーが取得するため、差出人にはそのいずれも伝わりません。",
"Applies to": "適用先",
"Archive and next": "アーカイブして次へ",
"Archive by month": "月ごとにアーカイブ",
@@ -1658,8 +1661,8 @@ export const catalog: Catalog = {
"Fingerprint": "フィンガープリント",
"Hide details": "詳細を隠す",
"Issued by": "発行者",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "OpenPGP で署名されており、ihasmail には送信者の公開鍵を取得する手段がありません。",
"It uses a signature algorithm ihasmail cannot check yet.": "ihasmail がまだ検証できない署名アルゴリズムが使われています。",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "OpenPGP で署名されており、{app} には送信者の公開鍵を取得する手段がありません。",
"It uses a signature algorithm {app} cannot check yet.": "{app} がまだ検証できない署名アルゴリズムが使われています。",
"It was made with a certificate belonging to {name}, which does not cover this address.": "{name} の証明書で署名されており、この証明書はこのアドレスを対象にしていません。",
"Previous fingerprint": "以前のフィンガープリント",
"Signed at": "署名日時",
@@ -1676,14 +1679,14 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "この署名はこの送信者のものではありません。",
"The signed part is missing either the message or the signature.": "署名された部分に、本文か署名のどちらかが欠けています。",
"The signer has changed.": "署名者が変わりました。",
"This message is signed, and ihasmail could not check the signature.": "このメールには署名がありますが、ihasmail は署名を検証できませんでした。",
"This message is signed, and {app} could not check the signature.": "このメールには署名がありますが、{app} は署名を検証できませんでした。",
"This signature does not check out.": "この署名は正しくありません。",
"Valid until": "有効期限",
"a different certificate": "別の証明書",
"an unnamed signer": "名前のない署名者",
"as claimed by the signer": "署名者の申告による",
"first seen {date}": "初回は {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "このアドレスからの以降のメールが別の人の署名だった場合、ihasmail がお知らせします。",
"{app} will tell you if a later message from this address is signed by anybody else.": "このアドレスからの以降のメールが別の人の署名だった場合、{app} がお知らせします。",
"itself, or an issuer it does not name": "自分自身、または名前のない発行者",
"no address": "アドレスなし",
},
+69 -67
View File
@@ -22,7 +22,6 @@ import type { Catalog } from "@/lib/i18n";
* which is ordinary good Dutch UI and sidesteps it entirely.
*
* Terminology, fixed once so it cannot drift:
* --- Used capitals on all words for unity ---
* Inbox Postvak IN Archive (verb) Archiveren
* Drafts Concepten Delete Verwijderen
* Sent Verzonden Move to Verplaatsen naar
@@ -507,7 +506,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "Wacht op de server — wordt {when} verzonden.",
"Scheduled — click to clear the schedule": "Gepland — klik om de planning te wissen",
"Nothing scheduled": "Niets gepland",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "Het bericht wacht op de server en wordt verzonden, of ihasmail nu open is of niet.",
"The message waits on the server, so it goes out whether or not {app} is open.": "Het bericht wacht op de server en wordt verzonden, of {app} nu open is of niet.",
"This server holds a message for up to {span}.": "Deze server houdt een bericht tot {span} vast.",
"Date and time to send": "Datum en tijd van verzenden",
"Undo send window": "Termijn om verzenden ongedaan te maken",
@@ -731,7 +730,7 @@ export const catalog: Catalog = {
"Sections": "Onderdelen",
"General": "Algemeen",
"Appearance": "Weergave",
"Make ihasmail yours.": "Maak ihasmail van uzelf.",
"Make {app} yours.": "Maak {app} van uzelf.",
"Reading": "Lezen",
"Reading pane": "Leesvenster",
"Right of the list": "Rechts van de lijst",
@@ -748,6 +747,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Bijlageherinnering",
"Warn when the message mentions an attachment but none is attached.": "Waarschuwen wanneer het bericht een bijlage noemt maar er geen is bijgevoegd.",
"Spell check while typing": "Spellingcontrole tijdens het typen",
"Open the composer full screen": "Berichten opstellen op volledig scherm",
"Confirm before deleting": "Bevestigen voor verwijderen",
"Show message snippets": "Berichtfragmenten tonen",
"Preview the first line of each message in the list.": "De eerste regel van elk bericht in de lijst tonen.",
@@ -827,7 +827,7 @@ export const catalog: Catalog = {
"Reset to defaults": "Standaardwaarden herstellen",
"Default mail app": "Standaard e-mailprogramma",
"Documentation": "Documentatie",
"About ihasmail": "Over ihasmail",
"About {app}": "Over {app}",
"About": "Over",
"Server": "Server",
"Server capabilities": "Servermogelijkheden",
@@ -955,8 +955,8 @@ export const catalog: Catalog = {
"Notifications": "Meldingen",
"Notifications are blocked in your browser settings.": "Meldingen zijn geblokkeerd in uw browserinstellingen.",
"Not supported in this browser.": "Niet ondersteund in deze browser.",
"Desktop notifications while ihasmail is open": "Systeemmeldingen terwijl ihasmail open is",
"Notify me even when ihasmail is closed": "Ook melden wanneer ihasmail gesloten is",
"Desktop notifications while {app} is open": "Systeemmeldingen terwijl {app} open is",
"Notify me even when {app} is closed": "Ook melden wanneer {app} gesloten is",
"Play a sound for new mail": "Geluid afspelen bij nieuwe post",
"Test notification": "Testmelding",
"Background notifications are on": "Achtergrondmeldingen staan aan",
@@ -1124,7 +1124,7 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "Een nieuwe identiteit moet een adres gebruiken waarvandaan dit account mag verzenden (aliassen die op de server zijn ingesteld).",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "Wordt niet aangeboden bij het opstellen. Het adres ontvangt nog steeds post, en u kunt er weer vanaf verzenden door het opnieuw te tonen.",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "Elke identiteit is een afzenderadres met een eigen naam, antwoordadres en handtekening. De standaardidentiteit is voorgeselecteerd bij het opstellen; stel een antwoordadres in wanneer antwoorden ergens anders heen moeten dan naar het afzenderadres.",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "Deze handtekening is groter dan de limiet van {limit} bytes van de server. ihasmail bewaart de volledige versie in uw Bestanden en zet een korte tekstversie op de server andere e-mailprogramma's zien de platte-tekstversie.",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "Deze handtekening is groter dan de limiet van {limit} bytes van de server. {app} bewaart de volledige versie in uw Bestanden en zet een korte tekstversie op de server \u2014 andere e-mailprogramma's zien de platte-tekstversie.",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Categorieën in Outlook-stijl die u via het rechtsklikmenu of de afsprakeneditor aan afspraken kunt toewijzen. De categorienaam wordt in de afspraak opgeslagen en synchroniseert dus met andere clients.",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "Platte-tekstberichten volgen het thema al. Met deze optie doen HTML-berichten zonder eigen kleuren dat ook, in plaats van op een witte achtergrond te staan. Berichten met een eigen vormgeving blijven precies zoals de afzender ze heeft ontworpen.",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "Dit staat los van {setting} onder Algemeen, waar wordt bepaald hoe datums, tijden en getallen worden geschreven. U kunt een Engelse interface met Nederlandse datums lezen, of andersom.",
@@ -1132,40 +1132,40 @@ export const catalog: Catalog = {
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "Dit scherm heeft geen aanraakscherm, dus hier verandert niets. Uw telefoon of tablet neemt deze instellingen over.",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "Een bericht ingedrukt houden selecteert het, een map ingedrukt houden opent het menu. Trek de bovenkant van de berichtenlijst omlaag om nieuwe post op te halen.",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "Een bevestiging vertelt de aanvrager dat dit adres actief is en wanneer het bericht is gelezen, en de afzender bepaalt waar die heen gaat — daarom is er geen automatische optie. Bij bulkpost, mailinglijsten en alles wat als automatisch verzonden is gemarkeerd, wordt er nooit een aangeboden.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "Deze browser kan geen programma's registreren voor {scheme}-links. Safari heeft daar in het bijzonder geen voorziening voor u kunt ihasmail nog steeds als standaard instellen via uw besturingssysteem als u het als app installeert.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "Deze browser kan geen programma's registreren voor {scheme}-links. Safari heeft daar in het bijzonder geen voorziening voor \u2014 u kunt {app} nog steeds als standaard instellen via uw besturingssysteem als u het als app installeert.",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "Registreren voor {scheme}-links vereist een beveiligde (HTTPS-)verbinding.",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "{scheme}-links op webpagina's, in documenten en in andere programma's openen in ihasmail in plaats van in een lokaal e-mailprogramma. Uw browser vraagt om bevestiging, en u kunt dit later wijzigen in zijn eigen instellingen (Chrome: Instellingen Privacy en beveiliging Site-instellingen Protocol-handlers; Firefox: Instellingen Algemeen Programma's).",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "Aangevraagd in deze browser. Of het effect heeft gehad, bepaalt de browser — controleer zijn instellingen als e-maillinks nog elders openen.",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "Installeer ihasmail eerst als app voor een systeembrede standaard (in Chrome: het installatiepictogram in de adresbalk). Uw besturingssysteem kan ihasmail dan overal direct aanbieden waar het vraagt welk e-mailprogramma gebruikt moet worden.",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "{scheme}-links \u2014 op webpagina's, in documenten en in andere programma's \u2014 openen in {app} in plaats van in een lokaal e-mailprogramma. Uw browser vraagt om bevestiging, en u kunt dit later wijzigen in zijn eigen instellingen (Chrome: Instellingen \u203a Privacy en beveiliging \u203a Site-instellingen \u203a Protocol-handlers; Firefox: Instellingen \u203a Algemeen \u203a Programma's).",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "Aangevraagd in deze browser. Of het effect heeft gehad, bepaalt de browser — controleer zijn instellingen als e-mail links nog elders openen.",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "Installeer {app} eerst als app voor een systeembrede standaard (in Chrome: het installatiepictogram in de adresbalk). Uw besturingssysteem kan {app} dan overal direct aanbieden waar het vraagt welk e-mailprogramma gebruikt moet worden.",
"Needs a browser with the Push API and a mail server that publishes a push key.": "Vereist een browser met de Push-API en een mailserver die een push-sleutel publiceert.",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "Uw mailserver levert deze rechtstreeks bij uw browser af, dus ze komen aan zonder geopend ihasmail-tabblad, met afzender en onderwerp erbij. Uw browser moet wel draaien sluit u hem helemaal af, dan wachten de meldingen en komen ze binnen zodra u hem weer opent.",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "Uw mailserver levert deze rechtstreeks bij uw browser af, dus ze komen aan zonder geopend {app}-tabblad, met afzender en onderwerp erbij. Uw browser moet wel draaien \u2014 sluit u hem helemaal af, dan wachten de meldingen en komen ze binnen zodra u hem weer opent.",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "Uw mailserver kan deze browser wekken, maar vermeldt geen afzender of onderwerp. Uw browser moet wel draaien.",
"This is what a new-mail notification looks like.": "Zo ziet een melding van nieuwe post eruit.",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "U bent ingelogd als {user}. Uw wachtwoord wordt nooit in de browser opgeslagen; de server bewaart het versleuteld per sessie om met Stalwart te communiceren.",
"App passwords are managed by your mail administrator.": "App-wachtwoorden worden beheerd door uw mailbeheerder.",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "Als u uw wachtwoord wijzigt, worden uw andere webmailsessies uitgelogd. App-wachtwoorden blijven werken.",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "Voor dit account staat tweefactorauthenticatie aan. ihasmail kan u nog niet met een code inloggen, dus inloggen op een ander apparaat vereist een app-wachtwoord of u schakelt tweefactorauthenticatie hier uit.",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "Voor dit account staat tweefactorauthenticatie aan. {app} kan u nog niet met een code inloggen, dus inloggen op een ander apparaat vereist een app-wachtwoord \u2014 of u schakelt tweefactorauthenticatie hier uit.",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "Een apart wachtwoord voor een e-mailprogramma of apparaat, dat u afzonderlijk kunt intrekken. App-wachtwoorden slaan tweefactorcodes over en blijven dus werken in programma's die er geen kunnen vragen.",
"Copy it into {name} now — it isn't shown again.": "Neem het nu over in {name} — het wordt niet opnieuw getoond.",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "Geen andere gebruikers gevonden in de directory, dus er kan niemand nieuws worden toegevoegd. Bestaande gedeelde items staan hieronder en kunnen nog worden verwijderd.",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart geeft zijn versienummer niet door aan e-mailprogramma's, dus ihasmail noemt de editie als de server die opgeeft. ihasmail vereist 0.16 of nieuwer; inloggen weigert alles wat ouder is.",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart geeft zijn versienummer niet door aan e-mailprogramma's, dus {app} noemt de editie als de server die opgeeft. {app} vereist 0.16 of nieuwer; inloggen weigert alles wat ouder is.",
"It {damage}, so the rules in it can't be shown or edited — saving what did arrive would write it back over the rest. Reload the page to try again. Your rules are still on the server; nothing here has changed them.": "Het {damage}, dus de regels erin kunnen niet worden getoond of bewerkt — wat wél is aangekomen opslaan zou de rest overschrijven. Laad de pagina opnieuw om het nog eens te proberen. Uw regels staan nog op de server; hier is er niets aan veranderd.",
"The visual rule editor only manages scripts it created. You can edit the script in the {tab} tab, or start fresh with rules (the existing script will be kept but deactivated).": "De visuele regeleditor beheert alleen scripts die hij zelf heeft gemaakt. U kunt het script bewerken op het tabblad {tab}, of opnieuw beginnen met regels (het bestaande script blijft bewaard maar wordt gedeactiveerd).",
"Your filter script {damage}, so only part of it arrived. Adding a rule would write that part back over the whole thing. Reload the page and try again.": "Uw filterscript {damage}, dus slechts een deel is aangekomen. Een regel toevoegen zou dat deel over het geheel heen schrijven. Laad de pagina opnieuw en probeer het nog eens.",
"Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.": "Uw filterscript kon zojuist niet worden gelezen; een regel toevoegen zou het kunnen overschrijven. Laad de pagina opnieuw en probeer het nog eens.",
"Your active Sieve script was written by hand, so rules can't be added automatically. Open {where} to edit the script or switch to managed rules.": "Uw actieve Sieve-script is met de hand geschreven, dus regels kunnen niet automatisch worden toegevoegd. Open {where} om het script te bewerken of over te stappen op beheerde regels.",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Hier verschijnen alleen talen waarin ihasmail is vertaald; de lijst groeit dus mee met de vertalingen en niet erop vooruit een taal die wordt aangeboden zonder teksten erachter zou de pagina laten beweren dat ze in een taal is die ze niet is.",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Hier verschijnen alleen talen waarin {app} is vertaald; de lijst groeit dus mee met de vertalingen en niet erop vooruit \u2014 een taal die wordt aangeboden zonder teksten erachter zou de pagina laten beweren dat ze in een taal is die ze niet is.",
"tell us about it": "laat het ons weten",
"This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Deze vertaling is door AI gemaakt en niet gecontroleerd door iemand met Nederlands als moedertaal; ze is daarom als Beta gemarkeerd tot iemand haar goedkeurt. Alles wat verkeerd klinkt, is een melding waard — {report}.",
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "De eigen versie van ihasmail is de datum van de commit waaruit het is gebouwd, gevolgd door waar die commit vandaan kwam: {example} is gebouwd uit een commit van 30 augustus 2026 die via pull request 129 binnenkwam. Een commit die niet via zo'n verzoek kwam, draagt in plaats daarvan zijn korte SHA {sha}. De versie zegt bewust niets over Stalwart; wat deze build van de server nodig heeft, staat op de regel hierboven.",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "De eigen versie van {app} is de datum van de commit waaruit het is gebouwd, gevolgd door waar die commit vandaan kwam: {example} is gebouwd uit een commit van 30 augustus 2026 die via pull request 129 binnenkwam. Een commit die niet via zo'n verzoek kwam, draagt in plaats daarvan zijn korte SHA \u2014 {sha}. De versie zegt bewust niets over Stalwart; wat deze build van de server nodig heeft, staat op de regel hierboven.",
// ── Composer status, calendar title ────────────────────────────────
"New message": "Nieuw bericht",
"Start a new message with what was shared?": "Een nieuw bericht beginnen met wat is gedeeld?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Er is iets met ihasmail gedeeld. Er wordt niets verzonden totdat u Verzenden kiest. Hebt u dit niet zelf zojuist gedeeld, gooi het dan weg.",
"Start a new message with what was shared?": "Een nieuw bericht beginnen met wat er is gedeeld?",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Er is iets met {app} gedeeld. Er wordt niets verzonden totdat u Verzenden kiest. Hebt u dit niet zelf zojuist gedeeld, gooi het dan weg.",
"Start a message": "Bericht beginnen",
"New mail": "Nieuwe e-mail",
"Could not do that — open ihasmail and try again": "Dat lukte niet — open ihasmail en probeer het opnieuw",
"Could not do that \u2014 open {app} and try again": "Dat lukte niet \u2014 open {app} en probeer het opnieuw",
"Sending…": "Bezig met verzenden…",
"Saving…": "Bezig met opslaan…",
"Error": "Fout",
@@ -1180,8 +1180,8 @@ export const catalog: Catalog = {
"Add a contact or import a vCard file.": "Voeg een contact toe of importeer een vCard-bestand.",
"Added to your calendar": "Toegevoegd aan uw agenda",
"All events in this calendar will be deleted.": "Alle afspraken in deze agenda worden verwijderd.",
"All occurrences": "Alle herhalingen",
"An occurrence cannot be moved to another calendar on its own": "Eén herhaling kan niet los naar een andere agenda worden verplaatst",
"All occurrences": "Alle gebeurtenissen",
"An occurrence cannot be moved to another calendar on its own": "Een gebeurtenis kan niet zelfstandig naar een andere agenda worden verplaatst",
"Anything signed in with this password stops working immediately.": "Alles wat met dit wachtwoord is aangemeld, werkt meteen niet meer.",
"App password revoked": "App-wachtwoord ingetrokken",
"Applies to every date in the series.": "Geldt voor elke datum in de reeks.",
@@ -1223,7 +1223,7 @@ export const catalog: Catalog = {
"Deactivate": "Deactiveren",
"Delete failed: {error}": "Verwijderen mislukt: {error}",
"Delete forever": "Definitief verwijderen",
"Delete script “{name}”?": "Script {name} verwijderen?",
"Delete script “{name}”?": "Script {name} verwijderen?",
"Delete this event?": "Deze afspraak verwijderen?",
"Delete this identity?": "Deze afzender verwijderen?",
"Delete {name}?": "{name} verwijderen?",
@@ -1237,12 +1237,12 @@ export const catalog: Catalog = {
"Edit identity": "Afzender bewerken",
"Edit template": "Sjabloon bewerken",
"End must be after start": "Het einde moet na het begin liggen",
"Event duplicated": "Afspraak gedupliceerd",
"Event duplicated": "Afspraak dubbel aangemaakt",
"Everything inside it goes too.": "Alles wat erin zit gaat mee.",
"Filter created": "Filter aangemaakt",
"Filter created — it will run on new mail": "Filter aangemaakt — het draait op nieuwe berichten",
"Filter created — it will run on new mail": "Filter aangemaakt — het werkt op nieuwe berichten",
"Filter saved": "Filter opgeslagen",
"Filter saved — it will run on new mail": "Filter opgeslagen — het draait op nieuwe berichten",
"Filter saved — it will run on new mail": "Filter opgeslagen — het werkt op nieuwe berichten",
"Filter saved, but applying it failed: {error}": "Filter opgeslagen, maar toepassen is mislukt: {error}",
"Filters saved": "Filters opgeslagen",
"Folder changed, but its filter rules could not be updated: {error}": "Map gewijzigd, maar de filterregels konden niet worden bijgewerkt: {error}",
@@ -1261,12 +1261,12 @@ export const catalog: Catalog = {
"Images in signatures need the Files feature, which this account doesn't have.": "Afbeeldingen in handtekeningen vereisen de functie Bestanden, die dit account niet heeft.",
"Invalid address: {address}": "Ongeldig adres: {address}",
"Invalid username or password.": "Gebruikersnaam of wachtwoord is onjuist.",
"It belongs to a change that was applied to this and all later occurrences, which the server will only edit as a whole. Apply to the entire series instead?": "Het hoort bij een wijziging die op deze en alle latere herhalingen is toegepast, en die de server alleen in zijn geheel bewerkt. In plaats daarvan op de hele reeks toepassen?",
"It belongs to a change that was applied to this and all later occurrences, which the server will only edit as a whole. Apply to the entire series instead?": "Het hoort bij een wijziging die op deze en alle latere gebeurtenissen is toegepast, en die de server alleen in zijn geheel bewerkt. In plaats daarvan op de hele reeks toepassen?",
"Label name": "Labelnaam",
"Larger than {size} MB limit": "Groter dan de limiet van {size} MB",
"Message sent": "Bericht verzonden",
"Move failed: {error}": "Verplaatsen mislukt: {error}",
"Move “{name}”": "{name} verplaatsen",
"Move “{name}”": "{name} verplaatsen",
"Moved": "Verplaatst",
"Network error. Please check your connection.": "Netwerkfout. Controleer uw verbinding.",
"New all-day event on {date}": "Nieuwe hele dag durende afspraak op {date}",
@@ -1279,11 +1279,11 @@ export const catalog: Catalog = {
"Drafts": "Concepten",
"Sent": "Verzonden",
"The server does not allow this role to be changed.": "De server staat niet toe deze rol te wijzigen.",
"Folder role updated": "Maprol bijgewerkt",
"Folder role updated": "Map rol bijgewerkt",
"No contacts yet": "Nog geen contacten",
"No longer shared": "Niet langer gedeeld",
"No matches": "Geen overeenkomsten",
"No new mail in your inbox.": "Geen nieuwe berichten in uw postvak IN.",
"No new mail in your inbox.": "Geen nieuwe berichten in uw Postvak IN.",
"No results": "Geen resultaten",
"Nothing here": "Hier is niets",
"Only Deleted Items and Junk Mail can be emptied.": "Alleen de prullenbak en ongewenste e-mail kunnen worden geleegd.",
@@ -1299,7 +1299,7 @@ export const catalog: Catalog = {
"Replied": "Beantwoord",
"Report spam (!)": "Als spam melden (!)",
"Response sent": "Antwoord verzonden",
"Revoke “{name}”?": "{name} intrekken?",
"Revoke “{name}”?": "{name} intrekken?",
"Script has errors": "Het script bevat fouten",
"Script is valid": "Het script is geldig",
"Script name is required": "Een scriptnaam is verplicht",
@@ -1310,15 +1310,15 @@ export const catalog: Catalog = {
"Send failed: {error}": "Verzenden mislukt: {error}",
"Send invites": "Uitnodigingen verzenden",
"Send scheduled for {when}": "Verzenden gepland voor {when}",
"Send without a subject?": "Verzenden zonder onderwerp?",
"Share “{name}”": "{name} delen",
"Send without a subject?": "Verzenden zonder een onderwerp?",
"Share “{name}”": "{name} delen",
"Sharing updated": "Delen bijgewerkt",
"Show": "Tonen",
"Show quoted text": "Geciteerde tekst tonen",
"Show this in the compose picker": "Aanbieden bij het opstellen",
"Sign out other sessions?": "Andere sessies afmelden?",
"Sign out others": "Andere afmelden",
"Stop sharing “{name}”?": "Delen van {name} stoppen?",
"Sign out others": "Anderen afmelden",
"Stop sharing “{name}”?": "Delen van {name} stoppen?",
"Switch to {theme}": "Overschakelen naar {theme}",
"Template": "Sjabloon",
"Template name": "Sjabloonnaam",
@@ -1327,7 +1327,7 @@ export const catalog: Catalog = {
"The new passwords don't match": "De nieuwe wachtwoorden komen niet overeen",
"The server scheduled this for {when}, not the time requested.": "De server heeft dit gepland voor {when}, niet voor de gevraagde tijd.",
"This date cannot be changed on its own": "Deze datum kan niet los worden gewijzigd",
"This occurrence": "Deze herhaling",
"This occurrence": "Dit geval",
"Too many attempts. Please wait a few minutes and try again.": "Te veel pogingen. Wacht een paar minuten en probeer het opnieuw.",
"Try another search.": "Probeer een andere zoekopdracht.",
"Try different keywords or filters.": "Probeer andere zoekwoorden of filters.",
@@ -1335,13 +1335,13 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "Tijd om ongedaan te maken: {seconds} s",
"You're all caught up": "U bent helemaal bij",
"Your browser refused the request: {error}": "Uw browser heeft het verzoek geweigerd: {error}",
"Your browser will ask whether to open mail links in ihasmail": "Uw browser vraagt of e-maillinks in ihasmail moeten worden geopend",
"Your browser will ask whether to open mail links in {app}": "Uw browser vraagt of e-mail links in {app} moeten worden geopend",
"Your message mentions an attachment, but nothing is attached.": "Uw bericht noemt een bijlage, maar er is niets bijgevoegd.",
"event": "afspraak",
"Hide password": "Wachtwoord verbergen",
"Show password": "Wachtwoord tonen",
"Settings reset to defaults": "Instellingen teruggezet naar de standaardwaarden",
"Removed. Mail links will open in whatever your browser falls back to.": "Verwijderd. E-maillinks worden geopend in waar uw browser op terugvalt.",
"Removed. Mail links will open in whatever your browser falls back to.": "Verwijderd. E-mail links worden geopend in waar uw browser op terugvalt.",
"Edit rule": "Regel bewerken",
"Address copied": "Adres gekopieerd",
"Search or create label": "Label zoeken of aanmaken",
@@ -1351,7 +1351,7 @@ export const catalog: Catalog = {
"Unsubscribe message prepared — just hit Send": "Afmeldbericht klaargezet — u hoeft alleen op Verzenden te klikken",
"Maximize": "Maximaliseren",
"Full screen": "Volledig scherm",
"Resize panes": "Vensterdelen verslepen",
"Resize panes": "Grootte van vensterdelen wijzigen",
"Resize message list": "Grootte van de berichtenlijst wijzigen",
"Resize contact list": "Grootte van de contactenlijst wijzigen",
"Resize sidebar": "Grootte van de zijbalk wijzigen",
@@ -1362,6 +1362,8 @@ export const catalog: Catalog = {
"Collapse all": "Alles samenvouwen",
"Expand all": "Alles uitvouwen",
"Send now instead": "Toch nu verzenden",
"This message is rich text": "Dit bericht is opgemaakte tekst",
"This message is plain text": "Dit bericht is platte tekst",
"Switch to plain text": "Overschakelen naar platte tekst",
"Switch to rich text": "Overschakelen naar opgemaakte tekst",
"{used} of {total} used": "{used} van {total} gebruikt",
@@ -1387,7 +1389,7 @@ export const catalog: Catalog = {
// had the same gap. The keyboard bindings among them register their
// group and description in English at the call site and are translated
// at render.
" and {count} more": " en nog {count}",
" and {count} more": " en nog {count} meer",
"10 people or more": "10 personen of meer",
"20 people or more": "20 personen of meer",
"5 people or more": "5 personen of meer",
@@ -1400,13 +1402,13 @@ export const catalog: Catalog = {
"Added from a message, and removable here — previously the only way to undo one was to find another message from the same sender.": "Toegevoegd vanuit een bericht en hier te verwijderen: voorheen kon dit alleen ongedaan worden gemaakt door een ander bericht van dezelfde afzender op te zoeken.",
"Added here, or from the dialog when a link is opened. A domain also covers its subdomains.": "Hier toegevoegd, of vanuit het venster bij het openen van een link. Een domein omvat ook de subdomeinen.",
"Agenda view": "Agendaweergave",
"All three start switched off. A client that begins by interrupting is one people learn to click through, and a warning clicked through without reading costs the same attention and buys nothing.": "Alle drie staan aanvankelijk uit. Een client die begint met onderbreken is er een die mensen leren weg te klikken, en een waarschuwing die ongelezen wordt weggeklikt kost dezelfde aandacht en levert niets op.",
"All three start switched off. A client that begins by interrupting is one people learn to click through, and a warning clicked through without reading costs the same attention and buys nothing.": "Alle drie staan aanvankelijk uit. Een melding die begint met onderbreken, is er een waar mensen al snel gedachteloos doorheen klikken, en een waarschuwing waar je zonder te lezen doorheen klikt, kost net zoveel aandacht en levert niets op.",
"All {n} in {folder} are selected.": "Alle {n} in {folder} zijn geselecteerd.",
"All {n} on this page are selected.": "Alle {n} op deze pagina zijn geselecteerd.",
"Also count these domains as inside": "Deze domeinen ook als intern beschouwen",
"Always": "Altijd",
"Always showing images from": "Afbeeldingen altijd tonen van",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "Een afbeelding die van de server van de afzender wordt geladen, vertelt die afzender dat het bericht is geopend, wanneer en ongeveer waarvandaan. Goedgekeurde afbeeldingen worden opgehaald door de server van ihasmail zelf en niet door de browser, dus de afzender komt daar niets van te weten.",
"Always showing images from": "Altijd afbeeldingen tonen van",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "Een afbeelding die van de server van de afzender wordt geladen, vertelt die afzender dat het bericht is geopend, wanneer en ongeveer waarvandaan. Goedgekeurde afbeeldingen worden opgehaald door de server van {app} zelf en niet door de browser, dus de afzender komt daar niets van te weten.",
"Applies to": "Geldt voor",
"Archive and next": "Archiveren en volgende",
"Archive by month": "Archiveren per maand",
@@ -1433,17 +1435,17 @@ export const catalog: Catalog = {
"Could not import this file: {error}": "Kon dit bestand niet importeren: {error}",
"Could not load this file.": "Kon dit bestand niet laden.",
"Could not read this calendar: {reason}": "Kon deze agenda niet lezen: {reason}",
"Could not read winmail.dat. The original is still attached below.": "Kon winmail.dat niet lezen. Het origineel zit hieronder nog als bijlage.",
"Could not read winmail.dat. The original is still attached below.": "Kon winmail.dat niet lezen. Het origineel is nog steeds toegoevegd als bijlage.",
"Counts people rather than headers, so one address in To and nine in Cc is a message to ten. Catches a reply-all onto a long thread.": "Telt personen in plaats van kopregels, dus één adres in Aan en negen in Cc is een bericht aan tien. Vangt een allen-beantwoorden op een lang gesprek af.",
"Date received": "Ontvangstdatum",
"Date sent": "Verzenddatum",
"Day view": "Dagweergave",
"Palettes named after another project are that project's work, used under its own license; the shades between their published colors are derived, and every one is checked for contrast. The accent color below still applies over any of them.": "Paletten die naar een ander project zijn genoemd, zijn het werk van dat project en worden gebruikt onder de eigen licentie daarvan; de tinten tussen de gepubliceerde kleuren zijn afgeleid en elk daarvan wordt op contrast gecontroleerd. De accentkleur hieronder geldt nog steeds over elk ervan.",
"Palettes named after another project are that project's work, used under its own license; the shades between their published colors are derived, and every one is checked for contrast. The accent color below still applies over any of them.": "Paletten die naar een ander project zijn genoemd, zijn het werk van dat project en worden gebruikt onder de eigen licentie daarvan; de tinten tussen de gepubliceerde kleuren zijn afgeleid en elk daarvan wordt op contrast gecontroleerd. De accentkleur hieronder wordt nog steeds toegepast op elk palet.",
"Earlier": "Eerder",
"Every folder": "Elke map",
"Everyone addressed will receive this.": "Iedereen die is geadresseerd ontvangt dit.",
"Everyone addressed will receive this.": "Elke geadresseerde ontvangt dit.",
"File contents": "Bestandsinhoud",
"Filled in when the template is inserted, so you can edit the result before sending. One that cannot be answered yet — a recipient's name on a message you have not addressed — is left in the body as written, rather than becoming a blank.": "Wordt ingevuld wanneer de sjabloon wordt ingevoegd, zodat u het resultaat vóór verzending kunt bewerken. Een veld dat nog niet kan worden ingevuld — de naam van een ontvanger op een bericht dat u nog niet hebt geadresseerd — blijft in de tekst staan zoals het geschreven is, in plaats van een leegte te worden.",
"Filled in when the template is inserted, so you can edit the result before sending. One that cannot be answered yet — a recipient's name on a message you have not addressed — is left in the body as written, rather than becoming a blank.": "Wordt ingevuld wanneer de sjabloon wordt ingevoegd, zodat u het resultaat vóór verzending kunt bewerken. Een veld dat nog niet kan worden ingevuld — de naam van een ontvanger op een bericht dat u nog niet hebt geadresseerd — blijft in de tekst staan zoals het geschreven is, in plaats van niets te tonen.",
"Forward as attachment": "Doorsturen als bijlage",
"From the birthdays on your contacts. Nothing is stored.": "Uit de verjaardagen van uw contacten. Er wordt niets opgeslagen.",
"Go to Calendar": "Ga naar Agenda",
@@ -1486,7 +1488,7 @@ export const catalog: Catalog = {
"Open links to these domains without asking": "Links naar deze domeinen openen zonder te vragen",
"Open, and stop asking about {domain}": "Openen en niet meer vragen over {domain}",
"Opening…": "Bezig met openen…",
"Ordered by the server over the whole folder, not just the messages loaded so far. Ties always fall back to newest first, so the order never shuffles between two looks at the same folder.": "Gesorteerd door de server over de hele map, niet alleen over de tot nu toe geladen berichten. Bij gelijke waarden geldt altijd nieuwste eerst, zodat de volgorde nooit verschuift tussen twee blikken op dezelfde map.",
"Ordered by the server over the whole folder, not just the messages loaded so far. Ties always fall back to newest first, so the order never shuffles between two looks at the same folder.": "Gesorteerd door de server over de hele map, niet alleen over de tot nu toe geladen berichten. Bij gelijke waarden wordt altijd teruggevallen op nieuwste eerst, zodat de volgorde nooit verandert tussen twee keer bekijken van dezelfde map.",
"Placeholders": "Tijdelijke aanduidingen",
"Previous conversation": "Vorig gesprek",
"Previous period": "Vorige periode",
@@ -1531,10 +1533,10 @@ export const catalog: Catalog = {
"Then nothing": "Daarna niets",
"There is no preview for this kind of file.": "Voor dit soort bestand is er geen voorbeeld.",
"There is nothing in it to export": "Er zit niets in om te exporteren",
"This file is not UTF-8 text, so editing it here would corrupt it — download it instead.": "Dit bestand is geen UTF-8-tekst, dus het hier bewerken zou het beschadigen: download het in plaats daarvan.",
"This file is not UTF-8 text, so editing it here would corrupt it — download it instead.": "Dit bestand is geen UTF-8-tekst, dus het hier bewerken zou het beschadigen: download het in plaats van openen.",
"This file is too big to show here ({size}) — download it to read it.": "Dit bestand is te groot om hier te tonen ({size}): download het om het te lezen.",
"This goes to {recipients}{rest}.": "Dit gaat naar {recipients}{rest}.",
"This link does not go where it says": "Deze link gaat niet waarheen hij zegt",
"This link does not go where it says": "Deze link gaat niet naar de aangegeven bestemming",
"This message packs its attachments into a winmail.dat, which most clients cannot open.": "Dit bericht verpakt zijn bijlagen in een winmail.dat, die de meeste clients niet kunnen openen.",
"Throw away your changes?": "Uw wijzigingen weggooien?",
"Today, in your date format": "Vandaag, in uw datumnotatie",
@@ -1561,11 +1563,11 @@ export const catalog: Catalog = {
"Your filter rules have changes that have not been saved.": "Uw filterregels bevatten wijzigingen die niet zijn opgeslagen.",
"Your own identity domains are always inside and do not need listing. A domain here also covers its subdomains.": "De domeinen van uw eigen identiteiten zijn altijd intern en hoeven niet te worden vermeld. Een domein hier omvat ook zijn subdomeinen.",
"Your own:": "Uw eigen:",
"dark mode": "de donkere modus",
"dark mode": "donkere modus",
"file": "bestand",
"light mode": "de lichte modus",
"light mode": "lichte modus",
"scored {score} against a threshold of {threshold}": "scoorde {score} bij een drempel van {threshold}",
"scored {score}, with no threshold stated": "scoorde {score}, zonder vermelde drempel",
"scored {score}, with no threshold stated": "scoorde {score}, zonder een vermelde drempel",
"this view": "deze weergave",
"{count} conversations moved to {folder}": "{count} gesprekken verplaatst naar {folder}",
"{count} folders": "{count} mappen",
@@ -1587,10 +1589,10 @@ export const catalog: Catalog = {
"Remove star": "Ster verwijderen",
"Requested, to {address}. Never sent automatically.": "Gevraagd, aan {address}. Wordt nooit automatisch verzonden.",
"The sender did not request a read receipt.": "De afzender heeft geen leesbevestiging gevraagd.",
"This is bulk or list mail; read receipts for it only confirm the address is live.": "Dit is bulk- of lijstpost; een leesbevestiging bevestigt daarvoor alleen dat het adres actief is.",
"This is bulk or list mail; read receipts for it only confirm the address is live.": "Dit is bulk- of lijst e-mail; een leesbevestiging bevestigt daarvoor alleen dat het adres actief is.",
"This message has not been received, so there is nothing to report.": "Dit bericht is niet ontvangen, dus er valt niets te melden.",
"This message was sent automatically, so no read receipt is offered.": "Dit bericht is automatisch verzonden, dus er wordt geen leesbevestiging aangeboden.",
"This server will not hold a message longer than {span}.": "Deze server houdt een bericht niet langer dan {span} vast.",
"This server will not hold a message longer than {span}.": "Deze server bewaart een bericht niet langer dan {span}.",
"Upload failed": "Uploaden mislukt",
// ── Third pass ──────────────────────────────────────────────────────
// Sentences that lib/ and store/ were building in English, and the two
@@ -1614,7 +1616,7 @@ export const catalog: Catalog = {
"fourth": "vierde",
"keep it": "behouden",
"last": "laatste",
"mark it read": "als gelezen markeren",
"mark it read": "markeren als gelezen",
"move to {folder}": "verplaatsen naar {folder}",
"reject it": "weigeren",
"remove {flag}": "{flag} verwijderen",
@@ -1638,7 +1640,7 @@ export const catalog: Catalog = {
"It was not deleted": "Het is niet verwijderd",
"Empty address book": "Dit adresboek leegmaken",
"There is nothing in it to delete": "Er staat niets in om te verwijderen",
"Empty “{name}”?": "{name}” leegmaken?",
"Empty “{name}”?": "{name}” leegmaken?",
"Delete them": "Verwijderen",
"Nothing was deleted": "Er is niets verwijderd",
// ── Checking an S/MIME signature, and what may be said about it ──
@@ -1648,9 +1650,9 @@ export const catalog: Catalog = {
"Fingerprint": "Vingerafdruk",
"Hide details": "Details verbergen",
"Issued by": "Uitgegeven door",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "Het is ondertekend met OpenPGP, en ihasmail kan de openbare sleutel van de afzender niet ophalen.",
"It uses a signature algorithm ihasmail cannot check yet.": "Het gebruikt een ondertekeningsalgoritme dat ihasmail nog niet kan controleren.",
"It was made with a certificate belonging to {name}, which does not cover this address.": "Hij is gemaakt met een certificaat van {name}, dat dit adres niet dekt.",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "Het is ondertekend met OpenPGP, en {app} kan de openbare sleutel van de afzender niet ophalen.",
"It uses a signature algorithm {app} cannot check yet.": "Het gebruikt een ondertekeningsalgoritme dat {app} nog niet kan controleren.",
"It was made with a certificate belonging to {name}, which does not cover this address.": "Het is gemaakt met een certificaat dat toebehoort aan {name}, maar dit certificaat is niet geldig voor dit adres.",
"Previous fingerprint": "Vorige vingerafdruk",
"Signed at": "Ondertekend op",
"Signed by {name} — the same signer as before.": "Ondertekend door {name} — dezelfde ondertekenaar als eerder.",
@@ -1666,21 +1668,21 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "De handtekening is niet van deze afzender.",
"The signed part is missing either the message or the signature.": "In het ondertekende deel ontbreekt het bericht of de handtekening.",
"The signer has changed.": "De ondertekenaar is veranderd.",
"This message is signed, and ihasmail could not check the signature.": "Dit bericht is ondertekend, en ihasmail kon de handtekening niet controleren.",
"This message is signed, and {app} could not check the signature.": "Dit bericht is ondertekend, en {app} kon de handtekening niet controleren.",
"This signature does not check out.": "Deze handtekening klopt niet.",
"Valid until": "Geldig tot",
"a different certificate": "een ander certificaat",
"an unnamed signer": "een naamloze ondertekenaar",
"as claimed by the signer": "volgens de ondertekenaar",
"first seen {date}": "voor het eerst gezien op {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "ihasmail laat het weten als een later bericht van dit adres door iemand anders is ondertekend.",
"itself, or an issuer it does not name": "zichzelf, of een uitgever die het niet noemt",
"{app} will tell you if a later message from this address is signed by anybody else.": "{app} laat het weten als een later bericht van dit adres door iemand anders is ondertekend.",
"itself, or an issuer it does not name": "zichzelf, of een uitgever die niet wordt genoemd",
"no address": "geen adres",
},
plurals: {
// ── Administration: domains ────────────────────────────────────
"{n} accounts use this domain. Move or delete them first.": { one: "{n} account gebruikt dit domein. Verplaats of verwijder het eerst.", other: "{n} accounts gebruiken dit domein. Verplaats of verwijder ze eerst." },
"The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "De server accepteert geen e-mail meer voor dit domein en de {n} DKIM-sleutel wordt verwijderd. Dit kan niet ongedaan worden gemaakt.", other: "De server accepteert geen e-mail meer voor dit domein en de {n} DKIM-sleutels worden verwijderd. Dit kan niet ongedaan worden gemaakt." },
"{n} accounts use this domain. Move or delete them first.": { one: "{n} account gebruikt dit domein. Verplaats of verwijder deze eerst.", other: "{n} accounts gebruiken dit domein. Verplaats of verwijder ze eerst." },
"The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "De server accepteert geen e-mails meer voor dit domein en de {n} DKIM-sleutel wordt verwijderd. Dit kan niet ongedaan worden gemaakt.", other: "De server accepteert geen e-mails meer voor dit domein en de {n} DKIM-sleutels worden verwijderd. Dit kan niet ongedaan worden gemaakt." },
"{n} domains": { one: "{n} domein", other: "{n} domeinen" },
"{n} groups": { one: "{n} groep", other: "{n} groepen" },
"Its {n} members are taken out of the group first, and lose what was shared with it. The group's own mail is removed in the background, and it can't be undone.": { one: "Het {n} lid wordt eerst uit de groep gehaald en verliest wat ermee gedeeld was. De e-mail van de groep wordt op de achtergrond verwijderd, en dit kan niet ongedaan worden gemaakt.", other: "De {n} leden worden eerst uit de groep gehaald en verliezen wat ermee gedeeld was. De e-mail van de groep wordt op de achtergrond verwijderd, en dit kan niet ongedaan worden gemaakt." },
@@ -1689,7 +1691,7 @@ export const catalog: Catalog = {
"Grants {n} permissions": { one: "Kent {n} recht toe", other: "Kent {n} rechten toe" },
"{n} roles": { one: "{n} rol", other: "{n} rollen" },
"{n} tenants": { one: "{n} tenant", other: "{n} tenants" },
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} account van deze tenant staat nog op {domain}. Verplaats of verwijder het voordat u het domein eruit haalt.", other: "{n} accounts van deze tenant staan nog op {domain}. Verplaats of verwijder ze voordat u het domein eruit haalt." },
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} account van deze tenant staat nog op {domain}. Verplaats of verwijder deze voordat u het domein eruit haalt.", other: "{n} accounts van deze tenant staan nog op {domain}. Verplaats of verwijder ze voordat u het domein eruit haalt." },
"{n} DKIM keys": { one: "{n} DKIM-sleutel", other: "{n} DKIM-sleutels" },
"{n} other items": { one: "{n} ander item", other: "{n} andere items" },
// ── Administration ────────────────────────────────────────────────
@@ -1713,13 +1715,13 @@ export const catalog: Catalog = {
"Every {n} years": { one: "Elk jaar", other: "Elke {n} jaar" },
"{rule}, {n} times": { one: "{rule}, {n} keer", other: "{rule}, {n} keer" },
// ── Third pass ─────────────────────────────────────────────────────
"Move {n} messages to Trash?": { one: "{n} bericht naar de Prullenbak verplaatsen?", other: "{n} berichten naar de Prullenbak verplaatsen?" },
"Move {n} messages to Trash?": { one: "{n} bericht naar de prullenbak verplaatsen?", other: "{n} berichten naar de prullenbak verplaatsen?" },
"{n} days": { one: "{n} dag", other: "{n} dagen" },
"{n} hours": { one: "{n} uur", other: "{n} uur" },
"Updated {n} contacts, nothing new": { one: "{n} contact bijgewerkt, niets nieuws", other: "{n} contacten bijgewerkt, niets nieuws" },
"{n} updated": { one: "{n} bijgewerkt", other: "{n} bijgewerkt" },
"Updated {n} contacts you already had": { one: "Bestaand contact bijgewerkt", other: "{n} bestaande contacten bijgewerkt" },
"{n} of them look like contacts you already had": { one: "{n} daarvan lijkt op een contact dat u al had", other: "{n} daarvan lijken op contacten die u al had" },
"{n} of them look like contacts you already had": { one: "{n} daarvan lijkt op een bestaand contact", other: "{n} daarvan lijken op een bestaand contact" },
"Your administrator changed {n} settings": { one: "Uw beheerder heeft {n} instelling gewijzigd", other: "Uw beheerder heeft {n} instellingen gewijzigd" },
"Exported {n} events": { one: "{n} afspraak geëxporteerd", other: "{n} afspraken geëxporteerd" },
"Imported {n} events": { one: "{n} afspraak geïmporteerd", other: "{n} afspraken geïmporteerd" },
+25 -22
View File
@@ -513,7 +513,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "Aguardando no servidor — será enviada {when}.",
"Scheduled — click to clear the schedule": "Programada — clique para cancelar a programação",
"Nothing scheduled": "Nada programado",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "A mensagem aguarda no servidor, então ela é enviada com o ihasmail aberto ou não.",
"The message waits on the server, so it goes out whether or not {app} is open.": "A mensagem aguarda no servidor, então ela é enviada com o {app} aberto ou não.",
"This server holds a message for up to {span}.": "Este servidor retém uma mensagem por até {span}.",
"Date and time to send": "Data e hora do envio",
"Undo send window": "Prazo para desfazer o envio",
@@ -735,7 +735,7 @@ export const catalog: Catalog = {
"Sections": "Seções",
"General": "Geral",
"Appearance": "Aparência",
"Make ihasmail yours.": "Deixe o ihasmail do seu jeito.",
"Make {app} yours.": "Deixe o {app} do seu jeito.",
"Reading": "Leitura",
"Reading pane": "Painel de leitura",
"Right of the list": "À direita da lista",
@@ -752,6 +752,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Aviso de anexo",
"Warn when the message mentions an attachment but none is attached.": "Avisar quando a mensagem mencionar um anexo mas nenhum estiver anexado.",
"Spell check while typing": "Verificação ortográfica ao digitar",
"Open the composer full screen": "Escrever mensagens em tela cheia",
"Confirm before deleting": "Confirmar antes de excluir",
"Show message snippets": "Mostrar um trecho das mensagens",
"Preview the first line of each message in the list.": "Mostrar a primeira linha de cada mensagem na lista.",
@@ -831,7 +832,7 @@ export const catalog: Catalog = {
"Reset to defaults": "Restaurar os padrões",
"Default mail app": "Aplicativo de e-mail padrão",
"Documentation": "Documentação",
"About ihasmail": "Sobre o ihasmail",
"About {app}": "Sobre o {app}",
"About": "Sobre",
"Server": "Servidor",
"Server capabilities": "Recursos do servidor",
@@ -959,8 +960,8 @@ export const catalog: Catalog = {
"Notifications": "Notificações",
"Notifications are blocked in your browser settings.": "As notificações estão bloqueadas nas configurações do seu navegador.",
"Not supported in this browser.": "Sem suporte neste navegador.",
"Desktop notifications while ihasmail is open": "Notificações do sistema enquanto o ihasmail estiver aberto",
"Notify me even when ihasmail is closed": "Avisar mesmo quando o ihasmail estiver fechado",
"Desktop notifications while {app} is open": "Notificações do sistema enquanto o {app} estiver aberto",
"Notify me even when {app} is closed": "Avisar mesmo quando o {app} estiver fechado",
"Play a sound for new mail": "Tocar um som ao chegar e-mail",
"Test notification": "Testar a notificação",
"Background notifications are on": "As notificações em segundo plano estão ativadas",
@@ -1129,7 +1130,7 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "Uma nova identidade precisa usar um endereço a partir do qual esta conta tenha permissão para enviar (aliases configurados no servidor).",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "Não é oferecida ao escrever. O endereço continua recebendo mensagens, e você pode voltar a enviar por ele mostrando-o novamente.",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "Cada identidade é um endereço de envio com nome, endereço de resposta e assinatura próprios. A identidade padrão vem pré-selecionada ao escrever; defina um endereço de resposta quando as respostas devam ir para outro lugar que não o remetente.",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "Esta assinatura passa do limite de {limit} bytes do servidor. O ihasmail guardará a versão completa nos seus Arquivos e uma versão curta em texto no servidor os outros clientes verão a versão em texto simples.",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "Esta assinatura passa do limite de {limit} bytes do servidor. O {app} guardar\u00e1 a vers\u00e3o completa nos seus Arquivos e uma vers\u00e3o curta em texto no servidor \u2014 os outros clientes ver\u00e3o a vers\u00e3o em texto simples.",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Categorias no estilo do Outlook que você pode atribuir aos eventos pelo menu do botão direito ou pelo editor de eventos. O nome da categoria fica guardado no evento, então sincroniza com outros clientes.",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "As mensagens em texto simples já seguem o tema. Com esta opção, as mensagens HTML sem cores próprias também seguem, em vez de aparecerem sobre um fundo branco. As mensagens com estilo próprio ficam exatamente como o remetente as desenhou.",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "Isto é independente de {setting} em Geral, que define como datas, horas e números são escritos. Você pode ler uma interface em inglês com datas em português, ou o contrário.",
@@ -1137,40 +1138,40 @@ export const catalog: Catalog = {
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "Esta tela não é sensível ao toque, então nada aqui muda o comportamento dela. Seu celular ou tablet vai adotar estas opções.",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "Manter uma mensagem pressionada a seleciona, e manter uma pasta pressionada abre o menu dela. Puxe o topo da lista para baixo para procurar mensagens novas.",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "Uma confirmação diz a quem pediu que este endereço está ativo e quando a mensagem foi lida, e o remetente escolhe para onde ela vai — por isso não há opção automática. Para mala direta, listas de discussão e tudo o que estiver marcado como enviado automaticamente, ela nunca é oferecida.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "Este navegador não consegue registrar aplicativos para links {scheme}. O Safari, em particular, não tem essa interface mesmo assim você pode definir o ihasmail como padrão pelo seu sistema operacional se instalá-lo como aplicativo.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "Este navegador n\u00e3o consegue registrar aplicativos para links {scheme}. O Safari, em particular, n\u00e3o tem essa interface \u2014 mesmo assim voc\u00ea pode definir o {app} como padr\u00e3o pelo seu sistema operacional se instal\u00e1-lo como aplicativo.",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "Registrar-se para links {scheme} exige uma conexão segura (HTTPS).",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "Abrir links {scheme} — em páginas da web, documentos e outros aplicativos — no ihasmail em vez de um cliente de e-mail local. Seu navegador pedirá confirmação, e você pode mudar isso depois nas configurações dele (Chrome: Configurações Privacidade e segurança Configurações do site Manipuladores de protocolo; Firefox: Configurações Geral Aplicativos).",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "Abrir links {scheme} \u2014 em p\u00e1ginas da web, documentos e outros aplicativos \u2014 no {app} em vez de um cliente de e-mail local. Seu navegador pedir\u00e1 confirma\u00e7\u00e3o, e voc\u00ea pode mudar isso depois nas configura\u00e7\u00f5es dele (Chrome: Configura\u00e7\u00f5es \u203a Privacidade e seguran\u00e7a \u203a Configura\u00e7\u00f5es do site \u203a Manipuladores de protocolo; Firefox: Configura\u00e7\u00f5es \u203a Geral \u203a Aplicativos).",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "Solicitado neste navegador. Se surtiu efeito é decisão dele — verifique as configurações se os links de e-mail ainda abrirem em outro lugar.",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "Para um padrão em todo o sistema, instale o ihasmail como aplicativo primeiro (no Chrome: o ícone de instalação na barra de endereços). Seu sistema operacional poderá então oferecer o ihasmail diretamente onde quer que pergunte qual aplicativo de e-mail usar.",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "Para um padrão em todo o sistema, instale o {app} como aplicativo primeiro (no Chrome: o ícone de instalação na barra de endereços). Seu sistema operacional poderá então oferecer o {app} diretamente onde quer que pergunte qual aplicativo de e-mail usar.",
"Needs a browser with the Push API and a mail server that publishes a push key.": "Exige um navegador com a API Push e um servidor de e-mail que publique uma chave push.",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "Seu servidor de e-mail as entrega direto ao navegador, então elas chegam sem nenhuma aba do ihasmail aberta, com o remetente e o assunto. Mesmo assim o navegador precisa estar em execução — se você fechá-lo por completo, as notificações esperam e chegam quando você abri-lo de novo.",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "Seu servidor de e-mail as entrega direto ao navegador, ent\u00e3o elas chegam sem nenhuma aba do {app} aberta, com o remetente e o assunto. Mesmo assim o navegador precisa estar em execu\u00e7\u00e3o \u2014 se voc\u00ea fech\u00e1-lo por completo, as notifica\u00e7\u00f5es esperam e chegam quando voc\u00ea abri-lo de novo.",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "Seu servidor de e-mail consegue acordar este navegador, mas não informa o remetente nem o assunto. Mesmo assim o navegador precisa estar em execução.",
"This is what a new-mail notification looks like.": "É assim que uma notificação de e-mail novo aparece.",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "Você está conectado como {user}. Sua senha nunca é guardada no navegador; o servidor a mantém criptografada por sessão para falar com o Stalwart.",
"App passwords are managed by your mail administrator.": "As senhas de aplicativo são gerenciadas pelo seu administrador de e-mail.",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "Mudar sua senha encerra suas outras sessões de webmail. As senhas de aplicativo continuam funcionando.",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "Esta conta está com a autenticação em duas etapas ativada. O ihasmail ainda não consegue conectar você com um código, então entrar em outro dispositivo exige uma senha de aplicativo ou você pode desativar a autenticação em duas etapas aqui.",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "Esta conta est\u00e1 com a autentica\u00e7\u00e3o em duas etapas ativada. O {app} ainda n\u00e3o consegue conectar voc\u00ea com um c\u00f3digo, ent\u00e3o entrar em outro dispositivo exige uma senha de aplicativo \u2014 ou voc\u00ea pode desativar a autentica\u00e7\u00e3o em duas etapas aqui.",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "Uma senha separada para um aplicativo de e-mail ou dispositivo, que você pode revogar sozinha. As senhas de aplicativo dispensam os códigos de duas etapas, então continuam funcionando em aplicativos que não conseguem pedir um.",
"Copy it into {name} now — it isn't shown again.": "Copie-a para {name} agora — ela não será mostrada de novo.",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "Nenhum outro usuário encontrado no diretório, então ninguém novo pode ser adicionado. O que já está compartilhado aparece abaixo e ainda pode ser removido.",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "O Stalwart não informa seu número de versão aos clientes de e-mail, então o ihasmail indica a edição quando o servidor fornece uma. O ihasmail exige a versão 0.16 ou mais recente, e o login recusa qualquer versão anterior.",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "O Stalwart n\u00e3o informa seu n\u00famero de vers\u00e3o aos clientes de e-mail, ent\u00e3o o {app} indica a edi\u00e7\u00e3o quando o servidor fornece uma. O {app} exige a vers\u00e3o 0.16 ou mais recente, e o login recusa qualquer vers\u00e3o anterior.",
"It {damage}, so the rules in it can't be shown or edited — saving what did arrive would write it back over the rest. Reload the page to try again. Your rules are still on the server; nothing here has changed them.": "Ele {damage}, então as regras nele não podem ser mostradas nem editadas — salvar o que chegou sobrescreveria o resto. Recarregue a página para tentar de novo. Suas regras continuam no servidor; nada aqui as alterou.",
"The visual rule editor only manages scripts it created. You can edit the script in the {tab} tab, or start fresh with rules (the existing script will be kept but deactivated).": "O editor visual de regras só gerencia os scripts que ele mesmo criou. Você pode editar o script na aba {tab}, ou começar do zero com regras (o script existente será mantido, mas desativado).",
"Your filter script {damage}, so only part of it arrived. Adding a rule would write that part back over the whole thing. Reload the page and try again.": "Seu script de filtragem {damage}, então só parte dele chegou. Adicionar uma regra escreveria essa parte por cima do todo. Recarregue a página e tente de novo.",
"Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.": "Seu script de filtragem não pôde ser lido agora, então adicionar uma regra poderia sobrescrevê-lo. Recarregue a página e tente de novo.",
"Your active Sieve script was written by hand, so rules can't be added automatically. Open {where} to edit the script or switch to managed rules.": "Seu script Sieve ativo foi escrito à mão, então não dá para adicionar regras automaticamente. Abra {where} para editar o script ou mudar para regras gerenciadas.",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Aqui aparecem só os idiomas para os quais o ihasmail foi traduzido, então a lista cresce conforme as traduções chegam, e não antes um idioma oferecido sem textos por trás faria a página afirmar estar em um idioma que não é o dela.",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Aqui aparecem s\u00f3 os idiomas para os quais o {app} foi traduzido, ent\u00e3o a lista cresce conforme as tradu\u00e7\u00f5es chegam, e n\u00e3o antes \u2014 um idioma oferecido sem textos por tr\u00e1s faria a p\u00e1gina afirmar estar em um idioma que n\u00e3o \u00e9 o dela.",
"tell us about it": "conte para nós",
"This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Esta tradução foi gerada por IA e não foi revisada por uma pessoa nativa, então está marcada como Beta até que alguém a aprove. Tudo o que soar errado vale um aviso — {report}.",
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "A versão do próprio ihasmail é a data do commit a partir do qual ele foi compilado, seguida da origem desse commit: {example} foi compilado a partir de um commit de 30 de agosto de 2026 que veio pela pull request 129. Um commit que não veio por uma delas carrega no lugar o SHA curto {sha}. A versão não diz nada sobre o Stalwart de propósito; o que esta compilação precisa do servidor está na linha acima.",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "A vers\u00e3o do pr\u00f3prio {app} \u00e9 a data do commit a partir do qual ele foi compilado, seguida da origem desse commit: {example} foi compilado a partir de um commit de 30 de agosto de 2026 que veio pela pull request 129. Um commit que n\u00e3o veio por uma delas carrega no lugar o SHA curto \u2014 {sha}. A vers\u00e3o n\u00e3o diz nada sobre o Stalwart de prop\u00f3sito; o que esta compila\u00e7\u00e3o precisa do servidor est\u00e1 na linha acima.",
// ── Composer status, calendar title ────────────────────────────────
"New message": "Nova mensagem",
"Start a new message with what was shared?": "Iniciar uma nova mensagem com o que foi compartilhado?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Algo foi compartilhado com o ihasmail. Nada é enviado até você escolher Enviar. Se não foi você que acabou de compartilhar, descarte.",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "Algo foi compartilhado com o {app}. Nada é enviado até você escolher Enviar. Se não foi você que acabou de compartilhar, descarte.",
"Start a message": "Iniciar mensagem",
"New mail": "Novo e-mail",
"Could not do that — open ihasmail and try again": "Não foi possível fazer isso abra o ihasmail e tente novamente",
"Could not do that \u2014 open {app} and try again": "N\u00e3o foi poss\u00edvel fazer isso \u2014 abra o {app} e tente novamente",
"Sending…": "Enviando…",
"Saving…": "Salvando…",
"Error": "Erro",
@@ -1340,7 +1341,7 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "Tempo para desfazer: {seconds} s",
"You're all caught up": "Você está em dia",
"Your browser refused the request: {error}": "Seu navegador recusou a solicitação: {error}",
"Your browser will ask whether to open mail links in ihasmail": "Seu navegador vai perguntar se os links de e-mail devem abrir no ihasmail",
"Your browser will ask whether to open mail links in {app}": "Seu navegador vai perguntar se os links de e-mail devem abrir no {app}",
"Your message mentions an attachment, but nothing is attached.": "Sua mensagem menciona um anexo, mas nada foi anexado.",
"event": "evento",
"Hide password": "Ocultar a senha",
@@ -1367,6 +1368,8 @@ export const catalog: Catalog = {
"Collapse all": "Recolher tudo",
"Expand all": "Expandir tudo",
"Send now instead": "Enviar agora mesmo",
"This message is rich text": "Esta mensagem está em texto formatado",
"This message is plain text": "Esta mensagem está em texto simples",
"Switch to plain text": "Mudar para texto simples",
"Switch to rich text": "Mudar para texto formatado",
"{used} of {total} used": "{used} de {total} usados",
@@ -1411,7 +1414,7 @@ export const catalog: Catalog = {
"Also count these domains as inside": "Contar também estes domínios como internos",
"Always": "Sempre",
"Always showing images from": "Sempre exibindo imagens de",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "Uma imagem carregada do servidor do remetente informa a ele que a mensagem foi aberta, quando e aproximadamente de onde. As imagens aprovadas são buscadas pelo próprio servidor do ihasmail, e não pelo navegador, de modo que o remetente não fica sabendo de nada disso.",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "Uma imagem carregada do servidor do remetente informa a ele que a mensagem foi aberta, quando e aproximadamente de onde. As imagens aprovadas são buscadas pelo próprio servidor do {app}, e não pelo navegador, de modo que o remetente não fica sabendo de nada disso.",
"Applies to": "Aplica-se a",
"Archive and next": "Arquivar e próxima",
"Archive by month": "Arquivar por mês",
@@ -1653,8 +1656,8 @@ export const catalog: Catalog = {
"Fingerprint": "Impressão digital",
"Hide details": "Ocultar detalhes",
"Issued by": "Emitido por",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "Está assinada com OpenPGP, e o ihasmail não tem como obter a chave pública do remetente.",
"It uses a signature algorithm ihasmail cannot check yet.": "Usa um algoritmo de assinatura que o ihasmail ainda não consegue conferir.",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "Está assinada com OpenPGP, e o {app} não tem como obter a chave pública do remetente.",
"It uses a signature algorithm {app} cannot check yet.": "Usa um algoritmo de assinatura que o {app} ainda não consegue conferir.",
"It was made with a certificate belonging to {name}, which does not cover this address.": "Foi feita com um certificado de {name}, que não cobre este endereço.",
"Previous fingerprint": "Impressão digital anterior",
"Signed at": "Assinado em",
@@ -1671,14 +1674,14 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "A assinatura não é deste remetente.",
"The signed part is missing either the message or the signature.": "Falta à parte assinada ou a mensagem ou a assinatura.",
"The signer has changed.": "O signatário mudou.",
"This message is signed, and ihasmail could not check the signature.": "Esta mensagem está assinada, e o ihasmail não conseguiu conferir a assinatura.",
"This message is signed, and {app} could not check the signature.": "Esta mensagem está assinada, e o {app} não conseguiu conferir a assinatura.",
"This signature does not check out.": "Esta assinatura não confere.",
"Valid until": "Válido até",
"a different certificate": "um certificado diferente",
"an unnamed signer": "um signatário sem nome",
"as claimed by the signer": "conforme declarado pelo signatário",
"first seen {date}": "visto pela primeira vez em {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "O ihasmail avisará você se uma mensagem posterior deste endereço for assinada por outra pessoa.",
"{app} will tell you if a later message from this address is signed by anybody else.": "O {app} avisará você se uma mensagem posterior deste endereço for assinada por outra pessoa.",
"itself, or an issuer it does not name": "ele mesmo, ou um emissor que ele não nomeia",
"no address": "nenhum endereço",
},
+25 -22
View File
@@ -512,7 +512,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "Ожидает на сервере — будет отправлено {when}.",
"Scheduled — click to clear the schedule": "Отложено — нажмите, чтобы отменить",
"Nothing scheduled": "Ничего не отложено",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "Письмо ждёт на сервере и будет отправлено независимо от того, открыт ihasmail или нет.",
"The message waits on the server, so it goes out whether or not {app} is open.": "Письмо ждёт на сервере и будет отправлено независимо от того, открыт {app} или нет.",
"This server holds a message for up to {span}.": "Этот сервер удерживает письмо до {span}.",
"Date and time to send": "Дата и время отправки",
"Undo send window": "Время на отмену отправки",
@@ -735,7 +735,7 @@ export const catalog: Catalog = {
"Sections": "Разделы",
"General": "Общие",
"Appearance": "Внешний вид",
"Make ihasmail yours.": "Настройте ihasmail под себя.",
"Make {app} yours.": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 {app} \u043f\u043e\u0434 \u0441\u0435\u0431\u044f.",
"Reading": "Чтение",
"Reading pane": "Область чтения",
"Right of the list": "Справа от списка",
@@ -752,6 +752,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Напоминание о вложении",
"Warn when the message mentions an attachment but none is attached.": "Предупреждать, если письмо упоминает вложение, но его нет.",
"Spell check while typing": "Проверять орфографию при вводе",
"Open the composer full screen": "Писать письма во весь экран",
"Confirm before deleting": "Спрашивать перед удалением",
"Show message snippets": "Показывать начало письма",
"Preview the first line of each message in the list.": "Показывать первую строку каждого письма в списке.",
@@ -831,7 +832,7 @@ export const catalog: Catalog = {
"Reset to defaults": "Сбросить к значениям по умолчанию",
"Default mail app": "Почтовая программа по умолчанию",
"Documentation": "Документация",
"About ihasmail": "О программе ihasmail",
"About {app}": "О программе {app}",
"About": "О программе",
"Server": "Сервер",
"Server capabilities": "Возможности сервера",
@@ -959,8 +960,8 @@ export const catalog: Catalog = {
"Notifications": "Уведомления",
"Notifications are blocked in your browser settings.": "Уведомления заблокированы в настройках браузера.",
"Not supported in this browser.": "Не поддерживается в этом браузере.",
"Desktop notifications while ihasmail is open": "Системные уведомления, пока ihasmail открыт",
"Notify me even when ihasmail is closed": "Уведомлять, даже когда ihasmail закрыт",
"Desktop notifications while {app} is open": "Системные уведомления, пока {app} открыт",
"Notify me even when {app} is closed": "Уведомлять, даже когда {app} закрыт",
"Play a sound for new mail": "Звук при новом письме",
"Test notification": "Проверить уведомление",
"Background notifications are on": "Фоновые уведомления включены",
@@ -1128,7 +1129,7 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "Новый профиль должен использовать адрес, с которого этой учётной записи разрешено отправлять (псевдонимы настраиваются на сервере).",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "Не предлагается при написании письма. Адрес по-прежнему принимает почту, и с него снова можно отправлять, если показать его обратно.",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "Каждый профиль — это адрес отправителя со своим именем, обратным адресом и подписью. Основной профиль подставляется при написании письма; укажите обратный адрес, если ответы должны приходить не на адрес отправителя.",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "Эта подпись больше серверного предела в {limit} байт. ihasmail сохранит полную версию в ваших Файлах, а на сервере оставит короткий текстовый вариант — другие почтовые клиенты увидят именно его.",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "\u042d\u0442\u0430 \u043f\u043e\u0434\u043f\u0438\u0441\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u043f\u0440\u0435\u0434\u0435\u043b\u0430 \u0432 {limit} \u0431\u0430\u0439\u0442. {app} \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442 \u043f\u043e\u043b\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0432 \u0432\u0430\u0448\u0438\u0445 \u0424\u0430\u0439\u043b\u0430\u0445, \u0430 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043e\u0441\u0442\u0430\u0432\u0438\u0442 \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u2014 \u0434\u0440\u0443\u0433\u0438\u0435 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u044b \u0443\u0432\u0438\u0434\u044f\u0442 \u0438\u043c\u0435\u043d\u043d\u043e \u0435\u0433\u043e.",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Категории в стиле Outlook, которые можно присваивать событиям через контекстное меню или редактор события. Название категории хранится в самом событии и синхронизируется с другими клиентами.",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "Письма в обычном тексте уже следуют теме. С этой настройкой ей следуют и HTML-письма без собственных цветов, а не показываются на белом фоне. Письма с собственным оформлением остаются ровно такими, какими их задумал отправитель.",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "Это не то же самое, что {setting} в разделе «Общие», где задаётся, как пишутся даты, время и числа. Можно читать английский интерфейс с русскими датами — или наоборот.",
@@ -1136,40 +1137,40 @@ export const catalog: Catalog = {
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "У этого экрана нет сенсорного ввода, поэтому здесь ничего не изменится. Настройку подхватят телефон или планшет.",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "Долгое нажатие на письме выделяет его, а на папке — открывает её меню. Потяните список писем вниз, чтобы проверить почту.",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "Уведомление сообщает запросившему, что адрес действующий и когда письмо было прочитано, а отправитель сам выбирает, куда его отправить, — поэтому автоматического варианта нет. Для массовых рассылок, списков рассылки и всего помеченного как отправленное автоматически оно не предлагается вовсе.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "Этот браузер не умеет регистрировать программы для ссылок {scheme}. В частности, в Safari нет такого интерфейса — но ihasmail всё равно можно сделать программой по умолчанию средствами операционной системы, установив его как приложение.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "\u042d\u0442\u043e\u0442 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043d\u0435 \u0443\u043c\u0435\u0435\u0442 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0434\u043b\u044f \u0441\u0441\u044b\u043b\u043e\u043a {scheme}. \u0412 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438, \u0432 Safari \u043d\u0435\u0442 \u0442\u0430\u043a\u043e\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u2014 \u043d\u043e {app} \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043e\u0439 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043c\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0432 \u0435\u0433\u043e \u043a\u0430\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435.",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "Для регистрации ссылок {scheme} нужно защищённое соединение (HTTPS).",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "Открывать ссылки {scheme} — на веб-страницах, в документах и других программах — в ihasmail, а не в почтовой программе на компьютере. Браузер попросит подтверждение, и позже это можно изменить в его настройках (Chrome: Настройки › Конфиденциальность и безопасность › Настройки сайтов › Обработчики протоколов; Firefox: Настройки › Основные › Приложения).",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0438 {scheme} \u2014 \u043d\u0430 \u0432\u0435\u0431-\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0445, \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0445 \u0438 \u0434\u0440\u0443\u0433\u0438\u0445 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0430\u0445 \u2014 \u0432 {app}, \u0430 \u043d\u0435 \u0432 \u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0439 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0435 \u043d\u0430 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0435. \u0411\u0440\u0430\u0443\u0437\u0435\u0440 \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u0442 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435, \u0438 \u043f\u043e\u0437\u0436\u0435 \u044d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0432 \u0435\u0433\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 (Chrome: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u203a \u041a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u203a \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0430\u0439\u0442\u043e\u0432 \u203a \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432; Firefox: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u203a \u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u203a \u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f).",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "Запрошено в этом браузере. Сработало ли это, решает он сам — проверьте его настройки, если почтовые ссылки по-прежнему открываются в другом месте.",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "Чтобы задать программу по умолчанию для всей системы, сначала установите ihasmail как приложение (в Chrome — значок установки в адресной строке). После этого операционная система сможет предлагать ihasmail везде, где спрашивает, какой почтовой программой воспользоваться.",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "Чтобы задать программу по умолчанию для всей системы, сначала установите {app} как приложение (в Chrome — значок установки в адресной строке). После этого операционная система сможет предлагать {app} везде, где спрашивает, какой почтовой программой воспользоваться.",
"Needs a browser with the Push API and a mail server that publishes a push key.": "Нужен браузер с Push API и почтовый сервер, публикующий push-ключ.",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "Почтовый сервер доставляет их прямо в браузер, поэтому они приходят без открытой вкладки ihasmail и содержат отправителя и тему. Браузер при этом должен быть запущен: если закрыть его полностью, уведомления подождут и придут при следующем запуске.",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u0445 \u043f\u0440\u044f\u043c\u043e \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043e\u043d\u0438 \u043f\u0440\u0438\u0445\u043e\u0434\u044f\u0442 \u0431\u0435\u0437 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0439 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 {app} \u0438 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u0435\u043b\u044f \u0438 \u0442\u0435\u043c\u0443. \u0411\u0440\u0430\u0443\u0437\u0435\u0440 \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0437\u0430\u043f\u0443\u0449\u0435\u043d: \u0435\u0441\u043b\u0438 \u0437\u0430\u043a\u0440\u044b\u0442\u044c \u0435\u0433\u043e \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e, \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u043e\u0436\u0434\u0443\u0442 \u0438 \u043f\u0440\u0438\u0434\u0443\u0442 \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u0437\u0430\u043f\u0443\u0441\u043a\u0435.",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "Почтовый сервер может разбудить этот браузер, но не сообщит отправителя и тему. Браузер при этом должен быть запущен.",
"This is what a new-mail notification looks like.": "Так выглядит уведомление о новом письме.",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "Вы вошли как {user}. Пароль никогда не хранится в браузере: сервер держит его в зашифрованном виде на время сеанса, чтобы общаться со Stalwart.",
"App passwords are managed by your mail administrator.": "Паролями приложений управляет ваш почтовый администратор.",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "Смена пароля завершает остальные сеансы веб-почты. Пароли приложений продолжают работать.",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "Для этой учётной записи включена двухфакторная аутентификация. ihasmail пока не умеет входить по коду, поэтому для входа на другом устройстве нужен пароль приложения — либо двухфакторную аутентификацию можно отключить здесь.",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "\u0414\u043b\u044f \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. {app} \u043f\u043e\u043a\u0430 \u043d\u0435 \u0443\u043c\u0435\u0435\u0442 \u0432\u0445\u043e\u0434\u0438\u0442\u044c \u043f\u043e \u043a\u043e\u0434\u0443, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0430 \u0434\u0440\u0443\u0433\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u043d\u0443\u0436\u0435\u043d \u043f\u0430\u0440\u043e\u043b\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u2014 \u043b\u0438\u0431\u043e \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u043c\u043e\u0436\u043d\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0434\u0435\u0441\u044c.",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "Отдельный пароль для почтовой программы или устройства, который можно отозвать по отдельности. Пароли приложений обходят двухфакторные коды и поэтому работают там, где запросить код невозможно.",
"Copy it into {name} now — it isn't shown again.": "Скопируйте его в {name} сейчас — больше он не показывается.",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "В каталоге не найдено других пользователей, поэтому добавить некого. Уже открытый доступ перечислен ниже, и его по-прежнему можно закрыть.",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart не сообщает почтовым клиентам номер версии, поэтому ihasmail показывает редакцию, если сервер её называет. ihasmail требует версию 0.16 или новее, и вход с более старой не выполняется.",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u0442 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c \u043d\u043e\u043c\u0435\u0440 \u0432\u0435\u0440\u0441\u0438\u0438, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 {app} \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0440\u0435\u0434\u0430\u043a\u0446\u0438\u044e, \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0435\u0451 \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u0442. {app} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e 0.16 \u0438\u043b\u0438 \u043d\u043e\u0432\u0435\u0435, \u0438 \u0432\u0445\u043e\u0434 \u0441 \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0440\u043e\u0439 \u043d\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
"It {damage}, so the rules in it can't be shown or edited — saving what did arrive would write it back over the rest. Reload the page to try again. Your rules are still on the server; nothing here has changed them.": "Он {damage}, поэтому правила в нём нельзя показать или изменить: сохранение полученной части затёрло бы остальное. Перезагрузите страницу и попробуйте снова. Ваши правила остаются на сервере, здесь их ничто не меняло.",
"The visual rule editor only manages scripts it created. You can edit the script in the {tab} tab, or start fresh with rules (the existing script will be kept but deactivated).": "Визуальный редактор правил работает только со скриптами, которые создал сам. Скрипт можно изменить на вкладке {tab} или начать заново с правил (существующий скрипт сохранится, но будет отключён).",
"Your filter script {damage}, so only part of it arrived. Adding a rule would write that part back over the whole thing. Reload the page and try again.": "Ваш скрипт фильтрации {damage}, поэтому получена только его часть. Добавление правила затёрло бы этой частью весь скрипт. Перезагрузите страницу и попробуйте снова.",
"Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.": "Скрипт фильтрации сейчас не удалось прочитать, поэтому добавление правила рискует его перезаписать. Перезагрузите страницу и попробуйте снова.",
"Your active Sieve script was written by hand, so rules can't be added automatically. Open {where} to edit the script or switch to managed rules.": "Ваш активный скрипт Sieve написан вручную, поэтому правила нельзя добавить автоматически. Откройте {where}, чтобы изменить скрипт или перейти к управляемым правилам.",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Здесь показаны только языки, на которые ihasmail переведён, поэтому список растёт вместе с переводами, а не опережает их: язык без текстов заставил бы страницу утверждать, что она написана на языке, которым не является.",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "\u0417\u0434\u0435\u0441\u044c \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u044f\u0437\u044b\u043a\u0438, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 {app} \u043f\u0435\u0440\u0435\u0432\u0435\u0434\u0451\u043d, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0441\u043f\u0438\u0441\u043e\u043a \u0440\u0430\u0441\u0442\u0451\u0442 \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0430\u043c\u0438, \u0430 \u043d\u0435 \u043e\u043f\u0435\u0440\u0435\u0436\u0430\u0435\u0442 \u0438\u0445: \u044f\u0437\u044b\u043a \u0431\u0435\u0437 \u0442\u0435\u043a\u0441\u0442\u043e\u0432 \u0437\u0430\u0441\u0442\u0430\u0432\u0438\u043b \u0431\u044b \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0442\u044c, \u0447\u0442\u043e \u043e\u043d\u0430 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0430 \u043d\u0430 \u044f\u0437\u044b\u043a\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f.",
"tell us about it": "сообщите нам",
"This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Этот перевод сделан ИИ и не проверен носителем языка, поэтому помечен как Beta до тех пор, пока кто-нибудь его не подтвердит. Обо всём, что звучит неправильно, стоит сообщить — {report}.",
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "Собственная версия ihasmail — это дата коммита, из которого он собран, и указание, откуда этот коммит взялся: {example} собран из коммита от 30 августа 2026 года, пришедшего через pull request 129. Коммит, пришедший иначе, несёт вместо этого короткий SHA — {sha}. Версия намеренно ничего не сообщает о Stalwart; то, что этой сборке нужно от сервера, указано строкой выше.",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f {app} \u2014 \u044d\u0442\u043e \u0434\u0430\u0442\u0430 \u043a\u043e\u043c\u043c\u0438\u0442\u0430, \u0438\u0437 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043e\u043d \u0441\u043e\u0431\u0440\u0430\u043d, \u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0435, \u043e\u0442\u043a\u0443\u0434\u0430 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043c\u0438\u0442 \u0432\u0437\u044f\u043b\u0441\u044f: {example} \u0441\u043e\u0431\u0440\u0430\u043d \u0438\u0437 \u043a\u043e\u043c\u043c\u0438\u0442\u0430 \u043e\u0442 30 \u0430\u0432\u0433\u0443\u0441\u0442\u0430 2026 \u0433\u043e\u0434\u0430, \u043f\u0440\u0438\u0448\u0435\u0434\u0448\u0435\u0433\u043e \u0447\u0435\u0440\u0435\u0437 pull request 129. \u041a\u043e\u043c\u043c\u0438\u0442, \u043f\u0440\u0438\u0448\u0435\u0434\u0448\u0438\u0439 \u0438\u043d\u0430\u0447\u0435, \u043d\u0435\u0441\u0451\u0442 \u0432\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 SHA \u2014 {sha}. \u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u0442 \u043e Stalwart; \u0442\u043e, \u0447\u0442\u043e \u044d\u0442\u043e\u0439 \u0441\u0431\u043e\u0440\u043a\u0435 \u043d\u0443\u0436\u043d\u043e \u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0432\u044b\u0448\u0435.",
// ── Composer status, calendar title ────────────────────────────────
"New message": "Новое письмо",
"Start a new message with what was shared?": "Начать новое письмо с полученным содержимым?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "В ihasmail что-то передали через «Поделиться». Ничего не отправится, пока вы не нажмёте «Отправить». Если вы только что ничего не передавали, нажмите «Не сохранять».",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "В {app} что-то передали через «Поделиться». Ничего не отправится, пока вы не нажмёте «Отправить». Если вы только что ничего не передавали, нажмите «Не сохранять».",
"Start a message": "Начать письмо",
"New mail": "Новое письмо",
"Could not do that — open ihasmail and try again": "Не удалось — откройте ihasmail и повторите попытку",
"Could not do that \u2014 open {app} and try again": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u2014 \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 {app} \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443",
"Sending…": "Отправка…",
"Saving…": "Сохранение…",
"Error": "Ошибка",
@@ -1339,7 +1340,7 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "Время на отмену: {seconds} с",
"You're all caught up": "Всё прочитано",
"Your browser refused the request: {error}": "Браузер отклонил запрос: {error}",
"Your browser will ask whether to open mail links in ihasmail": "Браузер спросит, открывать ли почтовые ссылки в ihasmail",
"Your browser will ask whether to open mail links in {app}": "Браузер спросит, открывать ли почтовые ссылки в {app}",
"Your message mentions an attachment, but nothing is attached.": "В письме упомянуто вложение, но ничего не приложено.",
"event": "событие",
"Hide password": "Скрыть пароль",
@@ -1366,6 +1367,8 @@ export const catalog: Catalog = {
"Collapse all": "Свернуть все",
"Expand all": "Развернуть все",
"Send now instead": "Отправить сейчас",
"This message is rich text": "Это письмо в формате HTML",
"This message is plain text": "Это письмо в виде простого текста",
"Switch to plain text": "Переключиться на обычный текст",
"Switch to rich text": "Переключиться на форматированный текст",
"{used} of {total} used": "Использовано {used} из {total}",
@@ -1410,7 +1413,7 @@ export const catalog: Catalog = {
"Also count these domains as inside": "Считать внутренними также эти домены",
"Always": "Всегда",
"Always showing images from": "Всегда показывать изображения от",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "Изображение, загруженное с сервера отправителя, сообщает ему, что письмо открыли, когда и примерно откуда. Разрешённые изображения загружает сервер ihasmail, а не браузер, поэтому отправитель не узнаёт ничего из этого.",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "Изображение, загруженное с сервера отправителя, сообщает ему, что письмо открыли, когда и примерно откуда. Разрешённые изображения загружает сервер {app}, а не браузер, поэтому отправитель не узнаёт ничего из этого.",
"Applies to": "Применяется к",
"Archive and next": "Архивировать и далее",
"Archive by month": "Архивировать по месяцам",
@@ -1652,8 +1655,8 @@ export const catalog: Catalog = {
"Fingerprint": "Отпечаток",
"Hide details": "Скрыть подробности",
"Issued by": "Кем выдан",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "Письмо подписано OpenPGP, а ihasmail не может получить открытый ключ отправителя.",
"It uses a signature algorithm ihasmail cannot check yet.": "Использован алгоритм подписи, который ihasmail пока не умеет проверять.",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "Письмо подписано OpenPGP, а {app} не может получить открытый ключ отправителя.",
"It uses a signature algorithm {app} cannot check yet.": "Использован алгоритм подписи, который {app} пока не умеет проверять.",
"It was made with a certificate belonging to {name}, which does not cover this address.": "Подпись сделана сертификатом, принадлежащим {name}, который не покрывает этот адрес.",
"Previous fingerprint": "Прежний отпечаток",
"Signed at": "Подписано",
@@ -1670,14 +1673,14 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "Подпись не принадлежит этому отправителю.",
"The signed part is missing either the message or the signature.": "В подписанной части не хватает либо письма, либо подписи.",
"The signer has changed.": "Подписавший изменился.",
"This message is signed, and ihasmail could not check the signature.": "Это письмо подписано, и ihasmail не смог проверить подпись.",
"This message is signed, and {app} could not check the signature.": "Это письмо подписано, и {app} не смог проверить подпись.",
"This signature does not check out.": "Эта подпись не сходится.",
"Valid until": "Действует до",
"a different certificate": "другим сертификатом",
"an unnamed signer": "неназванным подписавшим",
"as claimed by the signer": "по словам подписавшего",
"first seen {date}": "впервые замечен {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "ihasmail сообщит, если следующее письмо с этого адреса подпишет кто-то другой.",
"{app} will tell you if a later message from this address is signed by anybody else.": "{app} сообщит, если следующее письмо с этого адреса подпишет кто-то другой.",
"itself, or an issuer it does not name": "самим собой или неназванным издателем",
"no address": "нет адреса",
},
+25 -22
View File
@@ -506,7 +506,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "Очікує на сервері — буде надіслано {when}.",
"Scheduled — click to clear the schedule": "Заплановано — натисніть, щоб скасувати",
"Nothing scheduled": "Нічого не заплановано",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "Лист чекає на сервері й буде надісланий незалежно від того, чи відкрито ihasmail.",
"The message waits on the server, so it goes out whether or not {app} is open.": "Лист чекає на сервері й буде надісланий незалежно від того, чи відкрито {app}.",
"This server holds a message for up to {span}.": "Цей сервер утримує лист до {span}.",
"Date and time to send": "Дата й час надсилання",
"Undo send window": "Час на скасування надсилання",
@@ -729,7 +729,7 @@ export const catalog: Catalog = {
"Sections": "Розділи",
"General": "Загальні",
"Appearance": "Вигляд",
"Make ihasmail yours.": "Налаштуйте ihasmail під себе.",
"Make {app} yours.": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 {app} \u043f\u0456\u0434 \u0441\u0435\u0431\u0435.",
"Reading": "Читання",
"Reading pane": "Область читання",
"Right of the list": "Праворуч від списку",
@@ -746,6 +746,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Нагадування про вкладення",
"Warn when the message mentions an attachment but none is attached.": "Попереджати, якщо лист згадує вкладення, але його немає.",
"Spell check while typing": "Перевіряти орфографію під час введення",
"Open the composer full screen": "Писати листи на весь екран",
"Confirm before deleting": "Питати перед видаленням",
"Show message snippets": "Показувати початок листа",
"Preview the first line of each message in the list.": "Показувати перший рядок кожного листа у списку.",
@@ -825,7 +826,7 @@ export const catalog: Catalog = {
"Reset to defaults": "Скинути до значень за замовчуванням",
"Default mail app": "Поштова програма за замовчуванням",
"Documentation": "Документація",
"About ihasmail": "Про ihasmail",
"About {app}": "Про {app}",
"About": "Про програму",
"Server": "Сервер",
"Server capabilities": "Можливості сервера",
@@ -953,8 +954,8 @@ export const catalog: Catalog = {
"Notifications": "Сповіщення",
"Notifications are blocked in your browser settings.": "Сповіщення заблоковано в налаштуваннях браузера.",
"Not supported in this browser.": "Не підтримується в цьому браузері.",
"Desktop notifications while ihasmail is open": "Системні сповіщення, поки ihasmail відкрито",
"Notify me even when ihasmail is closed": "Сповіщати, навіть коли ihasmail закрито",
"Desktop notifications while {app} is open": "Системні сповіщення, поки {app} відкрито",
"Notify me even when {app} is closed": "Сповіщати, навіть коли {app} закрито",
"Play a sound for new mail": "Звук при новому листі",
"Test notification": "Перевірити сповіщення",
"Background notifications are on": "Фонові сповіщення увімкнено",
@@ -1122,7 +1123,7 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "Новий профіль має використовувати адресу, з якої цьому обліковому запису дозволено надсилати (псевдоніми налаштовуються на сервері).",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "Не пропонується під час написання листа. Адреса й далі отримує пошту, і з неї знову можна надсилати, якщо показати її назад.",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "Кожен профіль — це адреса відправника зі своїм іменем, зворотною адресою та підписом. Основний профіль підставляється під час написання листа; вкажіть зворотну адресу, якщо відповіді мають надходити не на адресу відправника.",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "Цей підпис більший за серверне обмеження в {limit} байт. ihasmail збереже повну версію у ваших Файлах, а на сервері залишить короткий текстовий варіант — інші поштові клієнти побачать саме його.",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "\u0426\u0435\u0439 \u043f\u0456\u0434\u043f\u0438\u0441 \u0431\u0456\u043b\u044c\u0448\u0438\u0439 \u0437\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u0435 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f \u0432 {limit} \u0431\u0430\u0439\u0442. {app} \u0437\u0431\u0435\u0440\u0435\u0436\u0435 \u043f\u043e\u0432\u043d\u0443 \u0432\u0435\u0440\u0441\u0456\u044e \u0443 \u0432\u0430\u0448\u0438\u0445 \u0424\u0430\u0439\u043b\u0430\u0445, \u0430 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 \u0437\u0430\u043b\u0438\u0448\u0438\u0442\u044c \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u0438\u0439 \u0432\u0430\u0440\u0456\u0430\u043d\u0442 \u2014 \u0456\u043d\u0448\u0456 \u043f\u043e\u0448\u0442\u043e\u0432\u0456 \u043a\u043b\u0456\u0454\u043d\u0442\u0438 \u043f\u043e\u0431\u0430\u0447\u0430\u0442\u044c \u0441\u0430\u043c\u0435 \u0439\u043e\u0433\u043e.",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Категорії у стилі Outlook, які можна призначати подіям через контекстне меню або редактор події. Назва категорії зберігається в самій події й синхронізується з іншими клієнтами.",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "Листи у звичайному тексті вже відповідають темі. З цим налаштуванням їй відповідають і HTML-листи без власних кольорів, замість того щоб показуватися на білому тлі. Листи з власним оформленням залишаються саме такими, якими їх задумав відправник.",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "Це не те саме, що {setting} у розділі «Загальні», де визначається, як записуються дати, час і числа. Можна читати англійський інтерфейс з українськими датами — або навпаки.",
@@ -1130,40 +1131,40 @@ export const catalog: Catalog = {
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "Цей екран не сенсорний, тому тут нічого не зміниться. Налаштування підхоплять телефон або планшет.",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "Довге натискання на листі позначає його, а на теці — відкриває її меню. Потягніть список листів донизу, щоб перевірити пошту.",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "Сповіщення повідомляє тому, хто його запитав, що адреса діюча і коли лист було прочитано, а відправник сам обирає, куди його надіслати, — тому автоматичного варіанта немає. Для масових розсилок, списків розсилки та всього позначеного як надіслане автоматично воно не пропонується взагалі.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "Цей браузер не вміє реєструвати програми для посилань {scheme}. Зокрема, у Safari немає такого інтерфейсу — але ihasmail усе одно можна зробити програмою за замовчуванням засобами операційної системи, встановивши його як застосунок.",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "\u0426\u0435\u0439 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043d\u0435 \u0432\u043c\u0456\u0454 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u044c {scheme}. \u0417\u043e\u043a\u0440\u0435\u043c\u0430, \u0443 Safari \u043d\u0435\u043c\u0430\u0454 \u0442\u0430\u043a\u043e\u0433\u043e \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u2014 \u0430\u043b\u0435 {app} \u0443\u0441\u0435 \u043e\u0434\u043d\u043e \u043c\u043e\u0436\u043d\u0430 \u0437\u0440\u043e\u0431\u0438\u0442\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043e\u044e \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0437\u0430\u0441\u043e\u0431\u0430\u043c\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0432\u0448\u0438 \u0439\u043e\u0433\u043e \u044f\u043a \u0437\u0430\u0441\u0442\u043e\u0441\u0443\u043d\u043e\u043a.",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "Для реєстрації посилань {scheme} потрібне захищене з'єднання (HTTPS).",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "Відкривати посилання {scheme} — на вебсторінках, у документах та інших програмах — у ihasmail, а не в поштовій програмі на комп'ютері. Браузер попросить підтвердження, і згодом це можна змінити в його налаштуваннях (Chrome: Налаштування › Конфіденційність і безпека › Налаштування сайтів › Обробники протоколів; Firefox: Налаштування › Загальні › Програми).",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "\u0412\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f {scheme} \u2014 \u043d\u0430 \u0432\u0435\u0431\u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0430\u0445, \u0443 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0445 \u0442\u0430 \u0456\u043d\u0448\u0438\u0445 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430\u0445 \u2014 \u0443 {app}, \u0430 \u043d\u0435 \u0432 \u043f\u043e\u0448\u0442\u043e\u0432\u0456\u0439 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0456 \u043d\u0430 \u043a\u043e\u043c\u043f'\u044e\u0442\u0435\u0440\u0456. \u0411\u0440\u0430\u0443\u0437\u0435\u0440 \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0456 \u0437\u0433\u043e\u0434\u043e\u043c \u0446\u0435 \u043c\u043e\u0436\u043d\u0430 \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0432 \u0439\u043e\u0433\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 (Chrome: \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u203a \u041a\u043e\u043d\u0444\u0456\u0434\u0435\u043d\u0446\u0456\u0439\u043d\u0456\u0441\u0442\u044c \u0456 \u0431\u0435\u0437\u043f\u0435\u043a\u0430 \u203a \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0430\u0439\u0442\u0456\u0432 \u203a \u041e\u0431\u0440\u043e\u0431\u043d\u0438\u043a\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0456\u0432; Firefox: \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u203a \u0417\u0430\u0433\u0430\u043b\u044c\u043d\u0456 \u203a \u041f\u0440\u043e\u0433\u0440\u0430\u043c\u0438).",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "Запитано в цьому браузері. Чи спрацювало це, вирішує він сам — перевірте його налаштування, якщо поштові посилання й далі відкриваються деінде.",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "Щоб задати програму за замовчуванням для всієї системи, спершу встановіть ihasmail як застосунок (у Chrome — значок встановлення в адресному рядку). Після цього операційна система зможе пропонувати ihasmail усюди, де запитує, якою поштовою програмою скористатися.",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "Щоб задати програму за замовчуванням для всієї системи, спершу встановіть {app} як застосунок (у Chrome — значок встановлення в адресному рядку). Після цього операційна система зможе пропонувати {app} усюди, де запитує, якою поштовою програмою скористатися.",
"Needs a browser with the Push API and a mail server that publishes a push key.": "Потрібен браузер із Push API та поштовий сервер, який публікує push-ключ.",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "Поштовий сервер доставляє їх прямо в браузер, тому вони надходять без відкритої вкладки ihasmail і містять відправника й тему. Браузер при цьому має бути запущений: якщо закрити його повністю, сповіщення почекають і надійдуть під час наступного запуску.",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0454 \u0457\u0445 \u043f\u0440\u044f\u043c\u043e \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440, \u0442\u043e\u043c\u0443 \u0432\u043e\u043d\u0438 \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c \u0431\u0435\u0437 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e\u0457 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 {app} \u0456 \u043c\u0456\u0441\u0442\u044f\u0442\u044c \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043d\u0438\u043a\u0430 \u0439 \u0442\u0435\u043c\u0443. \u0411\u0440\u0430\u0443\u0437\u0435\u0440 \u043f\u0440\u0438 \u0446\u044c\u043e\u043c\u0443 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0438\u0439: \u044f\u043a\u0449\u043e \u0437\u0430\u043a\u0440\u0438\u0442\u0438 \u0439\u043e\u0433\u043e \u043f\u043e\u0432\u043d\u0456\u0441\u0442\u044e, \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f \u043f\u043e\u0447\u0435\u043a\u0430\u044e\u0442\u044c \u0456 \u043d\u0430\u0434\u0456\u0439\u0434\u0443\u0442\u044c \u043f\u0456\u0434 \u0447\u0430\u0441 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443.",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "Поштовий сервер може розбудити цей браузер, але не повідомить відправника й тему. Браузер при цьому має бути запущений.",
"This is what a new-mail notification looks like.": "Так виглядає сповіщення про новий лист.",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "Ви увійшли як {user}. Пароль ніколи не зберігається в браузері: сервер тримає його зашифрованим на час сеансу, щоб спілкуватися зі Stalwart.",
"App passwords are managed by your mail administrator.": "Паролями програм керує ваш поштовий адміністратор.",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "Зміна пароля завершує інші сеанси вебпошти. Паролі програм продовжують працювати.",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "Для цього облікового запису увімкнено двофакторну автентифікацію. ihasmail поки не вміє входити за кодом, тому для входу на іншому пристрої потрібен пароль програми — або двофакторну автентифікацію можна вимкнути тут.",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "\u0414\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e. {app} \u043f\u043e\u043a\u0438 \u043d\u0435 \u0432\u043c\u0456\u0454 \u0432\u0445\u043e\u0434\u0438\u0442\u0438 \u0437\u0430 \u043a\u043e\u0434\u043e\u043c, \u0442\u043e\u043c\u0443 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0443 \u043d\u0430 \u0456\u043d\u0448\u043e\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043f\u0430\u0440\u043e\u043b\u044c \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u2014 \u0430\u0431\u043e \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u043c\u043e\u0436\u043d\u0430 \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438 \u0442\u0443\u0442.",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "Окремий пароль для поштової програми чи пристрою, який можна відкликати окремо. Паролі програм обходять двофакторні коди й тому працюють там, де запитати код неможливо.",
"Copy it into {name} now — it isn't shown again.": "Скопіюйте його до {name} зараз — більше він не показується.",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "У каталозі не знайдено інших користувачів, тому додати нікого. Уже відкритий доступ перелічено нижче, і його й далі можна закрити.",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart не повідомляє поштовим клієнтам номер версії, тому ihasmail показує редакцію, якщо сервер її називає. ihasmail потребує версію 0.16 або новішу, і вхід зі старішою не виконується.",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart \u043d\u0435 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0454 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u043c \u043a\u043b\u0456\u0454\u043d\u0442\u0430\u043c \u043d\u043e\u043c\u0435\u0440 \u0432\u0435\u0440\u0441\u0456\u0457, \u0442\u043e\u043c\u0443 {app} \u043f\u043e\u043a\u0430\u0437\u0443\u0454 \u0440\u0435\u0434\u0430\u043a\u0446\u0456\u044e, \u044f\u043a\u0449\u043e \u0441\u0435\u0440\u0432\u0435\u0440 \u0457\u0457 \u043d\u0430\u0437\u0438\u0432\u0430\u0454. {app} \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0454 \u0432\u0435\u0440\u0441\u0456\u044e 0.16 \u0430\u0431\u043e \u043d\u043e\u0432\u0456\u0448\u0443, \u0456 \u0432\u0445\u0456\u0434 \u0437\u0456 \u0441\u0442\u0430\u0440\u0456\u0448\u043e\u044e \u043d\u0435 \u0432\u0438\u043a\u043e\u043d\u0443\u0454\u0442\u044c\u0441\u044f.",
"It {damage}, so the rules in it can't be shown or edited — saving what did arrive would write it back over the rest. Reload the page to try again. Your rules are still on the server; nothing here has changed them.": "Він {damage}, тому правила в ньому не можна показати чи змінити: збереження отриманої частини затерло б решту. Перезавантажте сторінку й спробуйте знову. Ваші правила залишаються на сервері, тут їх ніщо не змінювало.",
"The visual rule editor only manages scripts it created. You can edit the script in the {tab} tab, or start fresh with rules (the existing script will be kept but deactivated).": "Візуальний редактор правил працює лише зі скриптами, які створив сам. Скрипт можна змінити на вкладці {tab} або почати заново з правил (наявний скрипт збережеться, але буде вимкнено).",
"Your filter script {damage}, so only part of it arrived. Adding a rule would write that part back over the whole thing. Reload the page and try again.": "Ваш скрипт фільтрації {damage}, тому отримано лише його частину. Додавання правила затерло б цією частиною весь скрипт. Перезавантажте сторінку й спробуйте знову.",
"Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.": "Скрипт фільтрації зараз не вдалося прочитати, тому додавання правила ризикує його перезаписати. Перезавантажте сторінку й спробуйте знову.",
"Your active Sieve script was written by hand, so rules can't be added automatically. Open {where} to edit the script or switch to managed rules.": "Ваш активний скрипт Sieve написано вручну, тому правила не можна додати автоматично. Відкрийте {where}, щоб змінити скрипт або перейти до керованих правил.",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Тут показано лише мови, якими перекладено ihasmail, тому список зростає разом із перекладами, а не випереджає їх: мова без текстів змусила б сторінку стверджувати, що вона написана мовою, якою не є.",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "\u0422\u0443\u0442 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u043b\u0438\u0448\u0435 \u043c\u043e\u0432\u0438, \u044f\u043a\u0438\u043c\u0438 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434\u0435\u043d\u043e {app}, \u0442\u043e\u043c\u0443 \u0441\u043f\u0438\u0441\u043e\u043a \u0437\u0440\u043e\u0441\u0442\u0430\u0454 \u0440\u0430\u0437\u043e\u043c \u0456\u0437 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434\u0430\u043c\u0438, \u0430 \u043d\u0435 \u0432\u0438\u043f\u0435\u0440\u0435\u0434\u0436\u0430\u0454 \u0457\u0445: \u043c\u043e\u0432\u0430 \u0431\u0435\u0437 \u0442\u0435\u043a\u0441\u0442\u0456\u0432 \u0437\u043c\u0443\u0441\u0438\u043b\u0430 \u0431 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 \u0441\u0442\u0432\u0435\u0440\u0434\u0436\u0443\u0432\u0430\u0442\u0438, \u0449\u043e \u0432\u043e\u043d\u0430 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0430 \u043c\u043e\u0432\u043e\u044e, \u044f\u043a\u043e\u044e \u043d\u0435 \u0454.",
"tell us about it": "повідомте нам",
"This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Цей переклад зроблено ШІ й не перевірено носієм мови, тому його позначено як Beta, доки хтось його не підтвердить. Про все, що звучить неправильно, варто повідомити — {report}.",
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "Власна версія ihasmail — це дата коміту, з якого його зібрано, і вказівка, звідки цей коміт узявся: {example} зібрано з коміту від 30 серпня 2026 року, що надійшов через pull request 129. Коміт, який надійшов інакше, несе замість цього короткий SHA — {sha}. Версія навмисно нічого не повідомляє про Stalwart; те, що цій збірці потрібно від сервера, вказано рядком вище.",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "\u0412\u043b\u0430\u0441\u043d\u0430 \u0432\u0435\u0440\u0441\u0456\u044f {app} \u2014 \u0446\u0435 \u0434\u0430\u0442\u0430 \u043a\u043e\u043c\u0456\u0442\u0443, \u0437 \u044f\u043a\u043e\u0433\u043e \u0439\u043e\u0433\u043e \u0437\u0456\u0431\u0440\u0430\u043d\u043e, \u0456 \u0432\u043a\u0430\u0437\u0456\u0432\u043a\u0430, \u0437\u0432\u0456\u0434\u043a\u0438 \u0446\u0435\u0439 \u043a\u043e\u043c\u0456\u0442 \u0443\u0437\u044f\u0432\u0441\u044f: {example} \u0437\u0456\u0431\u0440\u0430\u043d\u043e \u0437 \u043a\u043e\u043c\u0456\u0442\u0443 \u0432\u0456\u0434 30 \u0441\u0435\u0440\u043f\u043d\u044f 2026 \u0440\u043e\u043a\u0443, \u0449\u043e \u043d\u0430\u0434\u0456\u0439\u0448\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 pull request 129. \u041a\u043e\u043c\u0456\u0442, \u044f\u043a\u0438\u0439 \u043d\u0430\u0434\u0456\u0439\u0448\u043e\u0432 \u0456\u043d\u0430\u043a\u0448\u0435, \u043d\u0435\u0441\u0435 \u0437\u0430\u043c\u0456\u0441\u0442\u044c \u0446\u044c\u043e\u0433\u043e \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 SHA \u2014 {sha}. \u0412\u0435\u0440\u0441\u0456\u044f \u043d\u0430\u0432\u043c\u0438\u0441\u043d\u043e \u043d\u0456\u0447\u043e\u0433\u043e \u043d\u0435 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0454 \u043f\u0440\u043e Stalwart; \u0442\u0435, \u0449\u043e \u0446\u0456\u0439 \u0437\u0431\u0456\u0440\u0446\u0456 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0456\u0434 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u0432\u043a\u0430\u0437\u0430\u043d\u043e \u0440\u044f\u0434\u043a\u043e\u043c \u0432\u0438\u0449\u0435.",
// ── Composer status, calendar title ────────────────────────────────
"New message": "Новий лист",
"Start a new message with what was shared?": "Почати новий лист з отриманим вмістом?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "До ihasmail щось передали через «Поділитися». Нічого не буде надіслано, доки ви не натиснете «Надіслати». Якщо ви щойно нічого не передавали, натисніть «Не зберігати».",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "До {app} щось передали через «Поділитися». Нічого не буде надіслано, доки ви не натиснете «Надіслати». Якщо ви щойно нічого не передавали, натисніть «Не зберігати».",
"Start a message": "Почати лист",
"New mail": "Новий лист",
"Could not do that — open ihasmail and try again": "Не вдалося — відкрийте ihasmail і повторіть спробу",
"Could not do that \u2014 open {app} and try again": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u2014 \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 {app} \u0456 \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0440\u043e\u0431\u0443",
"Sending…": "Надсилання…",
"Saving…": "Збереження…",
"Error": "Помилка",
@@ -1333,7 +1334,7 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "Час на скасування: {seconds} с",
"You're all caught up": "Усе прочитано",
"Your browser refused the request: {error}": "Браузер відхилив запит: {error}",
"Your browser will ask whether to open mail links in ihasmail": "Браузер запитає, чи відкривати поштові посилання в ihasmail",
"Your browser will ask whether to open mail links in {app}": "Браузер запитає, чи відкривати поштові посилання в {app}",
"Your message mentions an attachment, but nothing is attached.": "У листі згадано вкладення, але нічого не долучено.",
"event": "подія",
"Hide password": "Сховати пароль",
@@ -1360,6 +1361,8 @@ export const catalog: Catalog = {
"Collapse all": "Згорнути все",
"Expand all": "Розгорнути все",
"Send now instead": "Надіслати зараз",
"This message is rich text": "Цей лист у форматі HTML",
"This message is plain text": "Цей лист у вигляді простого тексту",
"Switch to plain text": "Перейти на звичайний текст",
"Switch to rich text": "Перейти на форматований текст",
"{used} of {total} used": "Використано {used} з {total}",
@@ -1404,7 +1407,7 @@ export const catalog: Catalog = {
"Also count these domains as inside": "Вважати внутрішніми також ці домени",
"Always": "Завжди",
"Always showing images from": "Завжди показувати зображення від",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "Зображення, завантажене із сервера відправника, повідомляє йому, що лист відкрили, коли і приблизно звідки. Дозволені зображення завантажує сервер ihasmail, а не браузер, тому відправник не дізнається нічого з цього.",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "Зображення, завантажене із сервера відправника, повідомляє йому, що лист відкрили, коли і приблизно звідки. Дозволені зображення завантажує сервер {app}, а не браузер, тому відправник не дізнається нічого з цього.",
"Applies to": "Застосовується до",
"Archive and next": "Архівувати й далі",
"Archive by month": "Архівувати за місяцями",
@@ -1646,8 +1649,8 @@ export const catalog: Catalog = {
"Fingerprint": "Відбиток",
"Hide details": "Сховати подробиці",
"Issued by": "Ким видано",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "Лист підписано OpenPGP, а ihasmail не може отримати відкритий ключ відправника.",
"It uses a signature algorithm ihasmail cannot check yet.": "Використано алгоритм підпису, який ihasmail поки не вміє перевіряти.",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "Лист підписано OpenPGP, а {app} не може отримати відкритий ключ відправника.",
"It uses a signature algorithm {app} cannot check yet.": "Використано алгоритм підпису, який {app} поки не вміє перевіряти.",
"It was made with a certificate belonging to {name}, which does not cover this address.": "Підпис зроблено сертифікатом, що належить {name} і не покриває цю адресу.",
"Previous fingerprint": "Попередній відбиток",
"Signed at": "Підписано",
@@ -1664,14 +1667,14 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "Підпис не належить цьому відправникові.",
"The signed part is missing either the message or the signature.": "У підписаній частині бракує або листа, або підпису.",
"The signer has changed.": "Підписувач змінився.",
"This message is signed, and ihasmail could not check the signature.": "Цей лист підписано, і ihasmail не зміг перевірити підпис.",
"This message is signed, and {app} could not check the signature.": "Цей лист підписано, і {app} не зміг перевірити підпис.",
"This signature does not check out.": "Цей підпис не сходиться.",
"Valid until": "Чинний до",
"a different certificate": "іншим сертифікатом",
"an unnamed signer": "неназваним підписувачем",
"as claimed by the signer": "за словами підписувача",
"first seen {date}": "уперше побачено {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "ihasmail повідомить, якщо наступний лист із цієї адреси підпише хтось інший.",
"{app} will tell you if a later message from this address is signed by anybody else.": "{app} повідомить, якщо наступний лист із цієї адреси підпише хтось інший.",
"itself, or an issuer it does not name": "самим собою або неназваним видавцем",
"no address": "немає адреси",
},
+25 -22
View File
@@ -508,7 +508,7 @@ export const catalog: Catalog = {
"Waiting on the server — goes out {when}.": "正在服务器上等待,将于 {when} 发出。",
"Scheduled — click to clear the schedule": "已定时,点击可取消定时",
"Nothing scheduled": "没有定时邮件",
"The message waits on the server, so it goes out whether or not ihasmail is open.": "邮件在服务器上等待,无论 ihasmail 是否打开都会发出。",
"The message waits on the server, so it goes out whether or not {app} is open.": "邮件在服务器上等待,无论 {app} 是否打开都会发出。",
"This server holds a message for up to {span}.": "此服务器最多可将邮件保留 {span}。",
"Date and time to send": "发送日期和时间",
"Undo send window": "撤销发送时限",
@@ -731,7 +731,7 @@ export const catalog: Catalog = {
"Sections": "分区",
"General": "常规",
"Appearance": "外观",
"Make ihasmail yours.": "把 ihasmail 调成您喜欢的样子。",
"Make {app} yours.": "\u628a {app} \u8c03\u6210\u60a8\u559c\u6b22\u7684\u6837\u5b50\u3002",
"Reading": "阅读",
"Reading pane": "阅读窗格",
"Right of the list": "列表右侧",
@@ -748,6 +748,7 @@ export const catalog: Catalog = {
"Attachment reminder": "附件提醒",
"Warn when the message mentions an attachment but none is attached.": "邮件提到附件但未添加时提醒。",
"Spell check while typing": "输入时检查拼写",
"Open the composer full screen": "全屏写邮件",
"Confirm before deleting": "删除前确认",
"Show message snippets": "显示邮件摘要",
"Preview the first line of each message in the list.": "在列表中显示每封邮件的首行。",
@@ -827,7 +828,7 @@ export const catalog: Catalog = {
"Reset to defaults": "恢复默认设置",
"Default mail app": "默认邮件应用",
"Documentation": "文档",
"About ihasmail": "关于 ihasmail",
"About {app}": "关于 {app}",
"About": "关于",
"Server": "服务器",
"Server capabilities": "服务器功能",
@@ -956,8 +957,8 @@ export const catalog: Catalog = {
"Notifications": "通知",
"Notifications are blocked in your browser settings.": "浏览器设置中已阻止通知。",
"Not supported in this browser.": "此浏览器不支持。",
"Desktop notifications while ihasmail is open": "打开 ihasmail 时显示桌面通知",
"Notify me even when ihasmail is closed": "关闭 ihasmail 后也通知我",
"Desktop notifications while {app} is open": "打开 {app} 时显示桌面通知",
"Notify me even when {app} is closed": "关闭 {app} 后也通知我",
"Play a sound for new mail": "新邮件提示音",
"Test notification": "测试通知",
"Background notifications are on": "后台通知已开启",
@@ -1071,7 +1072,7 @@ export const catalog: Catalog = {
// ── Settings prose ─────────────────────────────────────────────────
"tell us about it": "告诉我们",
"This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "本翻译由 AI 生成,尚未经母语者校对,因此在有人校对签核之前会一直标记为 Beta。任何读起来不对的地方都值得反馈——{report}。",
"Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them a language offered without strings behind it would leave the page claiming to be in a language it is not.": "这里只列出 ihasmail 已经翻译过的语言,因此列表会随着译文落地而增加,而不会提前出现——提供一种背后没有译文的语言,只会让页面声称自己使用着一种它并未使用的语言。",
"Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them \u2014 a language offered without strings behind it would leave the page claiming to be in a language it is not.": "\u8fd9\u91cc\u53ea\u5217\u51fa {app} \u5df2\u7ecf\u7ffb\u8bd1\u8fc7\u7684\u8bed\u8a00\uff0c\u56e0\u6b64\u5217\u8868\u4f1a\u968f\u7740\u8bd1\u6587\u843d\u5730\u800c\u589e\u52a0\uff0c\u800c\u4e0d\u4f1a\u63d0\u524d\u51fa\u73b0\u2014\u2014\u63d0\u4f9b\u4e00\u79cd\u80cc\u540e\u6ca1\u6709\u8bd1\u6587\u7684\u8bed\u8a00\uff0c\u53ea\u4f1a\u8ba9\u9875\u9762\u58f0\u79f0\u81ea\u5df1\u4f7f\u7528\u7740\u4e00\u79cd\u5b83\u5e76\u672a\u4f7f\u7528\u7684\u8bed\u8a00\u3002",
"This is separate from {setting} in General, which decides how dates, times and numbers are written. You can read an English interface with German dates, or the other way round.": "这与「常规」中的{setting}是两回事,后者决定日期、时间和数字的写法。您可以用英文界面配德式日期,反过来也可以。",
"Defaults for the calendar views and new events.": "日历视图和新建日程的默认设置。",
"Replies will go to this address instead of the From address": "回复将发往此地址,而不是发件人地址",
@@ -1079,31 +1080,31 @@ export const catalog: Catalog = {
"New identities must use an address this account is allowed to send from (aliases configured on the server).": "新建发件身份必须使用此账户获准发信的地址(在服务器上配置的别名)。",
"Not offered when composing. It still receives mail, and you can still send from it by showing it again.": "写邮件时不再提供此身份。它仍会接收邮件,重新显示后也仍可用于发信。",
"Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.": "每个发件身份都是一个发件地址,拥有各自的名称、回复地址和签名。写邮件时会预先选中默认身份;若希望回复发往发件人地址以外的地方,请设置回复地址。",
"This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server other mail clients will see the plain-text version.": "此签名超出了服务器 {limit} 字节的限制。ihasmail 会把完整版本保存在您的「文件」中,并在服务器上存放一段简短的文本备用版——其他邮件客户端看到的将是纯文本版本。",
"This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server \u2014 other mail clients will see the plain-text version.": "\u6b64\u7b7e\u540d\u8d85\u51fa\u4e86\u670d\u52a1\u5668 {limit} \u5b57\u8282\u7684\u9650\u5236\u3002{app} \u4f1a\u628a\u5b8c\u6574\u7248\u672c\u4fdd\u5b58\u5728\u60a8\u7684\u300c\u6587\u4ef6\u300d\u4e2d\uff0c\u5e76\u5728\u670d\u52a1\u5668\u4e0a\u5b58\u653e\u4e00\u6bb5\u7b80\u77ed\u7684\u6587\u672c\u5907\u7528\u7248\u2014\u2014\u5176\u4ed6\u90ae\u4ef6\u5ba2\u6237\u7aef\u770b\u5230\u7684\u5c06\u662f\u7eaf\u6587\u672c\u7248\u672c\u3002",
"Outlook-style categories you can assign to events from the right-click menu or the event editor. The category name is stored on the event, so it syncs to other clients.": "Outlook 风格的分类,可通过右键菜单或日程编辑器指定给日程。分类名称保存在日程上,因此会同步到其他客户端。",
"Plain-text mail already follows the theme. With this on, HTML mail that brings no colors of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them.": "纯文本邮件本就会跟随主题。开启后,未自带配色的 HTML 邮件也会跟随主题,而不再显示在白色卡片上。自带样式的邮件则完全保持发件人设计的样子。",
"On a touchscreen, drag a message sideways to act on it. Each direction can do one thing, or nothing. These follow your account, so a phone and a tablet agree; a mouse ignores them and keeps dragging messages into folders instead.": "在触摸屏上,横向拖动邮件即可对其操作。每个方向可以执行一项操作,也可以什么都不做。这些设置跟随您的账户,因此手机和平板保持一致;鼠标不受影响,仍然是把邮件拖入文件夹。",
"This screen has no touchscreen, so nothing here changes what it does. Your phone or tablet will pick these up.": "此屏幕没有触摸屏,因此这里的设置不会改变它的行为。您的手机或平板会应用这些设置。",
"Holding a message selects it, and holding a folder opens its menu. Pull the top of the message list down to check for new mail.": "长按邮件可选中它,长按文件夹可打开其菜单。下拉邮件列表顶部即可检查新邮件。",
"A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.": "回执会告诉请求方这个地址确实有人在用,以及邮件是何时被读的,而回执发往何处由发件人指定——因此这里没有自动发送的选项。群发邮件、邮件列表以及任何标记为自动提交的邮件,一律不提供发送回执的选项。",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API you can still make ihasmail the default from your operating system if you install it as an app.": "此浏览器无法为 {scheme} 链接注册应用。Safari 尤其没有相应的接口——如果您把 ihasmail 安装为应用,仍可在操作系统中将它设为默认。",
"This browser cannot register apps for {scheme} links. Safari, in particular, has no such API \u2014 you can still make {app} the default from your operating system if you install it as an app.": "\u6b64\u6d4f\u89c8\u5668\u65e0\u6cd5\u4e3a {scheme} \u94fe\u63a5\u6ce8\u518c\u5e94\u7528\u3002Safari \u5c24\u5176\u6ca1\u6709\u76f8\u5e94\u7684\u63a5\u53e3\u2014\u2014\u5982\u679c\u60a8\u628a {app} \u5b89\u88c5\u4e3a\u5e94\u7528\uff0c\u4ecd\u53ef\u5728\u64cd\u4f5c\u7cfb\u7edf\u4e2d\u5c06\u5b83\u8bbe\u4e3a\u9ed8\u8ba4\u3002",
"Registering for {scheme} links requires a secure (HTTPS) connection.": "注册 {scheme} 链接需要安全连接(HTTPS)。",
"Open {scheme} links in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).": "让网页、文档和其他应用中的 {scheme} 链接在 ihasmail 中打开,而不是桌面邮件客户端。浏览器会请您确认,之后也可以在浏览器自身的设置中更改(Chrome:设置 › 隐私和安全 › 网站设置 › 协议处理程序;Firefox:设置 › 常规 › 应用程序)。",
"Open {scheme} links \u2014 in web pages, documents and other apps \u2014 in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings \u203a Privacy and security \u203a Site settings \u203a Protocol handlers; Firefox: Settings \u203a General \u203a Applications).": "\u8ba9\u7f51\u9875\u3001\u6587\u6863\u548c\u5176\u4ed6\u5e94\u7528\u4e2d\u7684 {scheme} \u94fe\u63a5\u5728 {app} \u4e2d\u6253\u5f00\uff0c\u800c\u4e0d\u662f\u684c\u9762\u90ae\u4ef6\u5ba2\u6237\u7aef\u3002\u6d4f\u89c8\u5668\u4f1a\u8bf7\u60a8\u786e\u8ba4\uff0c\u4e4b\u540e\u4e5f\u53ef\u4ee5\u5728\u6d4f\u89c8\u5668\u81ea\u8eab\u7684\u8bbe\u7f6e\u4e2d\u66f4\u6539\uff08Chrome\uff1a\u8bbe\u7f6e \u203a \u9690\u79c1\u548c\u5b89\u5168 \u203a \u7f51\u7ad9\u8bbe\u7f6e \u203a \u534f\u8bae\u5904\u7406\u7a0b\u5e8f\uff1bFirefox\uff1a\u8bbe\u7f6e \u203a \u5e38\u89c4 \u203a \u5e94\u7528\u7a0b\u5e8f\uff09\u3002",
"Requested in this browser. Whether it took effect is up to the browser — check its settings if mail links still open elsewhere.": "已在此浏览器中提出请求。是否生效由浏览器决定——如果邮件链接仍在别处打开,请检查浏览器的设置。",
"For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.": "若要设为系统级默认,请先把 ihasmail 安装为应用(在 Chrome 中:地址栏里的安装图标)。之后操作系统在询问使用哪个邮件应用时,就会直接提供 ihasmail。",
"For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.": "若要设为系统级默认,请先把 {app} 安装为应用(在 Chrome 中:地址栏里的安装图标)。之后操作系统在询问使用哪个邮件应用时,就会直接提供 {app}。",
"Needs a browser with the Push API and a mail server that publishes a push key.": "需要支持 Push API 的浏览器,以及发布了推送密钥的邮件服务器。",
"Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running if you quit it completely, notifications wait and arrive when you open it again.": "您的邮件服务器会把通知直接送到浏览器,因此不必打开 ihasmail 标签页也能收到,并会显示发件人和主题。但浏览器仍需保持运行——如果完全退出浏览器,通知会等到您再次打开时送达。",
"Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running \u2014 if you quit it completely, notifications wait and arrive when you open it again.": "\u60a8\u7684\u90ae\u4ef6\u670d\u52a1\u5668\u4f1a\u628a\u901a\u77e5\u76f4\u63a5\u9001\u5230\u6d4f\u89c8\u5668\uff0c\u56e0\u6b64\u4e0d\u5fc5\u6253\u5f00 {app} \u6807\u7b7e\u9875\u4e5f\u80fd\u6536\u5230\uff0c\u5e76\u4f1a\u663e\u793a\u53d1\u4ef6\u4eba\u548c\u4e3b\u9898\u3002\u4f46\u6d4f\u89c8\u5668\u4ecd\u9700\u4fdd\u6301\u8fd0\u884c\u2014\u2014\u5982\u679c\u5b8c\u5168\u9000\u51fa\u6d4f\u89c8\u5668\uff0c\u901a\u77e5\u4f1a\u7b49\u5230\u60a8\u518d\u6b21\u6253\u5f00\u65f6\u9001\u8fbe\u3002",
"Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.": "您的邮件服务器可以唤醒此浏览器,但不会包含发件人或主题。浏览器仍需保持运行。",
"This is what a new-mail notification looks like.": "新邮件通知就是这个样子。",
"You're signed in as {user}. Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.": "您当前以 {user} 登录。您的密码从不保存在浏览器中;服务器会按会话加密保存,用于与 Stalwart 通信。",
"App passwords are managed by your mail administrator.": "应用专用密码由您的邮件管理员管理。",
"Changing your password signs out your other webmail sessions. Any app passwords keep working.": "更改密码会让您的其他网页邮箱会话退出登录。已有的应用专用密码仍可继续使用。",
"This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password or you can turn two-factor authentication off here.": "此账户已开启两步验证。ihasmail 目前还不能通过验证码登录,因此在其他设备上登录需要使用应用专用密码——您也可以在这里关闭两步验证。",
"This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password \u2014 or you can turn two-factor authentication off here.": "\u6b64\u8d26\u6237\u5df2\u5f00\u542f\u4e24\u6b65\u9a8c\u8bc1\u3002{app} \u76ee\u524d\u8fd8\u4e0d\u80fd\u901a\u8fc7\u9a8c\u8bc1\u7801\u767b\u5f55\uff0c\u56e0\u6b64\u5728\u5176\u4ed6\u8bbe\u5907\u4e0a\u767b\u5f55\u9700\u8981\u4f7f\u7528\u5e94\u7528\u4e13\u7528\u5bc6\u7801\u2014\u2014\u60a8\u4e5f\u53ef\u4ee5\u5728\u8fd9\u91cc\u5173\u95ed\u4e24\u6b65\u9a8c\u8bc1\u3002",
"A separate password for a mail app or device, which you can revoke on its own. App passwords skip two-factor codes, so they keep working in apps that can't ask for one.": "为某个邮件应用或设备单独设置的密码,可以单独吊销。应用专用密码会跳过两步验证码,因此在无法输入验证码的应用中仍然可用。",
"Copy it into {name} now — it isn't shown again.": "请立即把它复制到 {name}——它不会再次显示。",
"No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "目录中没有找到其他用户,因此无法添加新的共享对象。已有的共享列在下方,仍可移除。",
"Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart 不会向邮件客户端公布版本号,因此只有在服务器给出版本类型时,ihasmail 才会报告它。ihasmail 需要 0.16 或更高版本,更旧的版本一律无法登录。",
"ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "ihasmail 自身的版本号是其构建所用提交的日期,后面跟着该提交的来源:{example} 表示由 2026 年 8 月 30 日的一个提交构建而成,而该提交来自第 129 号拉取请求。未经拉取请求的提交则改用简短 SHA 表示——{sha}。版本号刻意不包含任何关于 Stalwart 的信息;此版本对服务器的要求见上一行。",
"Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart \u4e0d\u4f1a\u5411\u90ae\u4ef6\u5ba2\u6237\u7aef\u516c\u5e03\u7248\u672c\u53f7\uff0c\u56e0\u6b64\u53ea\u6709\u5728\u670d\u52a1\u5668\u7ed9\u51fa\u7248\u672c\u7c7b\u578b\u65f6\uff0c{app} \u624d\u4f1a\u62a5\u544a\u5b83\u3002{app} \u9700\u8981 0.16 \u6216\u66f4\u9ad8\u7248\u672c\uff0c\u66f4\u65e7\u7684\u7248\u672c\u4e00\u5f8b\u65e0\u6cd5\u767b\u5f55\u3002",
"{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead \u2014 {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "{app} \u81ea\u8eab\u7684\u7248\u672c\u53f7\u662f\u5176\u6784\u5efa\u6240\u7528\u63d0\u4ea4\u7684\u65e5\u671f\uff0c\u540e\u9762\u8ddf\u7740\u8be5\u63d0\u4ea4\u7684\u6765\u6e90\uff1a{example} \u8868\u793a\u7531 2026 \u5e74 8 \u6708 30 \u65e5\u7684\u4e00\u4e2a\u63d0\u4ea4\u6784\u5efa\u800c\u6210\uff0c\u800c\u8be5\u63d0\u4ea4\u6765\u81ea\u7b2c 129 \u53f7\u62c9\u53d6\u8bf7\u6c42\u3002\u672a\u7ecf\u62c9\u53d6\u8bf7\u6c42\u7684\u63d0\u4ea4\u5219\u6539\u7528\u7b80\u77ed SHA \u8868\u793a\u2014\u2014{sha}\u3002\u7248\u672c\u53f7\u523b\u610f\u4e0d\u5305\u542b\u4efb\u4f55\u5173\u4e8e Stalwart \u7684\u4fe1\u606f\uff1b\u6b64\u7248\u672c\u5bf9\u670d\u52a1\u5668\u7684\u8981\u6c42\u89c1\u4e0a\u4e00\u884c\u3002",
// ── Constant labels ────────────────────────────────────────────────
"Add": "添加",
@@ -1171,10 +1172,10 @@ export const catalog: Catalog = {
// ── Composer status, calendar title ────────────────────────────────
"New message": "新邮件",
"Start a new message with what was shared?": "用共享的内容新建邮件吗?",
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "有内容被共享到 ihasmail。在您选择“发送”之前不会发送任何内容。如果不是您刚才共享的,请放弃。",
"Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.": "有内容被共享到 {app}。在您选择“发送”之前不会发送任何内容。如果不是您刚才共享的,请放弃。",
"Start a message": "新建邮件",
"New mail": "新邮件",
"Could not do that — open ihasmail and try again": "无法执行 — 请打开 ihasmail 后重试",
"Could not do that \u2014 open {app} and try again": "\u65e0\u6cd5\u6267\u884c \u2014 \u8bf7\u6253\u5f00 {app} \u540e\u91cd\u8bd5",
"Sending…": "正在发送…",
"Saving…": "正在保存…",
"Error": "错误",
@@ -1344,7 +1345,7 @@ export const catalog: Catalog = {
"Undo window: {seconds}s": "撤销时限:{seconds} 秒",
"You're all caught up": "您已看完全部邮件",
"Your browser refused the request: {error}": "您的浏览器拒绝了该请求:{error}",
"Your browser will ask whether to open mail links in ihasmail": "浏览器会询问是否用 ihasmail 打开邮件链接",
"Your browser will ask whether to open mail links in {app}": "浏览器会询问是否用 {app} 打开邮件链接",
"Your message mentions an attachment, but nothing is attached.": "您的邮件提到了附件,但没有添加任何附件。",
"event": "日程",
"Hide password": "隐藏密码",
@@ -1371,6 +1372,8 @@ export const catalog: Catalog = {
"Collapse all": "全部折叠",
"Expand all": "全部展开",
"Send now instead": "改为立即发送",
"This message is rich text": "这封邮件是富文本",
"This message is plain text": "这封邮件是纯文本",
"Switch to plain text": "切换为纯文本",
"Switch to rich text": "切换为富文本",
"{used} of {total} used": "已使用 {used},共 {total}",
@@ -1415,7 +1418,7 @@ export const catalog: Catalog = {
"Also count these domains as inside": "也将这些域名视为内部",
"Always": "始终",
"Always showing images from": "始终显示以下发件人的图片",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.": "从发件人服务器加载的图片会告诉对方邮件已被打开、打开时间以及大致位置。已允许的图片由 ihasmail 自己的服务器抓取,而非浏览器,因此发件人无从得知这些信息。",
"An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.": "从发件人服务器加载的图片会告诉对方邮件已被打开、打开时间以及大致位置。已允许的图片由 {app} 自己的服务器抓取,而非浏览器,因此发件人无从得知这些信息。",
"Applies to": "适用于",
"Archive and next": "归档并转到下一封",
"Archive by month": "按月归档",
@@ -1657,8 +1660,8 @@ export const catalog: Catalog = {
"Fingerprint": "指纹",
"Hide details": "隐藏详情",
"Issued by": "颁发者",
"It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.": "该邮件使用 OpenPGP 签名,而 ihasmail 无法获取发件人的公钥。",
"It uses a signature algorithm ihasmail cannot check yet.": "它使用了 ihasmail 尚不能校验的签名算法。",
"It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.": "该邮件使用 OpenPGP 签名,而 {app} 无法获取发件人的公钥。",
"It uses a signature algorithm {app} cannot check yet.": "它使用了 {app} 尚不能校验的签名算法。",
"It was made with a certificate belonging to {name}, which does not cover this address.": "签名使用的是 {name} 的证书,该证书并不包含此地址。",
"Previous fingerprint": "以前的指纹",
"Signed at": "签名时间",
@@ -1675,14 +1678,14 @@ export const catalog: Catalog = {
"The signature is not for this sender.": "该签名不属于此发件人。",
"The signed part is missing either the message or the signature.": "签名部分缺少邮件正文或签名。",
"The signer has changed.": "签名者已更换。",
"This message is signed, and ihasmail could not check the signature.": "此邮件带有签名,但 ihasmail 无法校验该签名。",
"This message is signed, and {app} could not check the signature.": "此邮件带有签名,但 {app} 无法校验该签名。",
"This signature does not check out.": "此签名不成立。",
"Valid until": "有效期至",
"a different certificate": "另一份证书",
"an unnamed signer": "未具名的签名者",
"as claimed by the signer": "据签名者声称",
"first seen {date}": "首次见于 {date}",
"ihasmail will tell you if a later message from this address is signed by anybody else.": "如果此地址之后的邮件由他人签名,ihasmail 会提醒您。",
"{app} will tell you if a later message from this address is signed by anybody else.": "如果此地址之后的邮件由他人签名,{app} 会提醒您。",
"itself, or an issuer it does not name": "其自身,或一个未具名的颁发者",
"no address": "无地址",
},
@@ -18,7 +18,7 @@ function draft(over: Partial<Draft> = {}): Draft {
requestReceipt: false, priority: "normal",
showCc: false, showBcc: false, showReplyTo: false,
minimized: false, maximized: false, dirty: false, savedAt: null,
saving: false, sending: false, error: null, signatureHtml: "", replyMode: null, sendAt: null,
saving: false, sending: false, error: null, signatureHtml: "", replyMode: null, quoteHtml: "", quoteText: "", formatOffer: null, sendAt: null,
...over,
};
}
@@ -0,0 +1,41 @@
import { beforeEach, describe, expect, it } from "vitest";
import { useCompose } from "@/store/compose";
import { useMail } from "@/store/mail";
import { DEFAULT_SETTINGS, useSettings } from "@/store/settings";
/**
* "Open the composer full screen" (#401): the size a new composer starts at.
*
* Only new composers follow it. A draft put back after an undone or failed
* send keeps the size it had, since that is the window somebody was already
* looking at.
*/
beforeEach(() => {
useCompose.setState({ drafts: [], activeKey: null, pendingSends: {} });
useMail.setState({ accountId: "a1", identities: [] as never });
useSettings.setState({ settings: { ...DEFAULT_SETTINGS } });
});
const opened = () => {
const key = useCompose.getState().open();
return useCompose.getState().drafts.find((d) => d.key === key)!;
};
describe("the size a new composer opens at", () => {
it("is the usual window by default", () => {
expect(opened().maximized).toBe(false);
});
it("is full screen with the setting on", () => {
useSettings.setState((s) => ({ settings: { ...s.settings, composeMaximized: true } }));
expect(opened().maximized).toBe(true);
});
it("can still be restored to a window once open", () => {
useSettings.setState((s) => ({ settings: { ...s.settings, composeMaximized: true } }));
const d = opened();
useCompose.getState().update(d.key, { maximized: false });
expect(useCompose.getState().drafts.find((x) => x.key === d.key)!.maximized).toBe(false);
});
});
@@ -0,0 +1,98 @@
import { beforeEach, describe, expect, it } from "vitest";
import { buildEmailObject, useCompose } from "@/store/compose";
import { useMail } from "@/store/mail";
import { useContacts } from "@/store/contacts";
import { DEFAULT_SETTINGS, useSettings } from "@/store/settings";
import type { Email, EmailAddress, Identity } from "@/jmap/types";
/*
* Remote images in a quoted message (#410).
*
* Quoting renders the message a second time. The reply was fetching every
* remote image in it, whatever the reader had decided so replying to a
* message whose images had been left blocked told the tracker the mail was
* read and the address live. The composer is a window like any other.
*/
const PIXEL = "https://tracker.example/open.gif?id=42";
const MESSAGE = {
id: "m1", messageId: ["<[email protected]>"], subject: "Sale", references: [], inReplyTo: [], keywords: {},
attachments: [], receivedAt: "2026-09-04T10:00:00Z", mailboxIds: {},
from: [{ name: "Shop", email: "[email protected]" }], to: [{ name: "John", email: "[email protected]" }], cc: [],
htmlBody: [{ partId: "2", type: "text/html" }],
textBody: [{ partId: "1", type: "text/plain" }],
bodyValues: {
"1": { value: "Sale on now", isEncodingProblem: false, isTruncated: false },
"2": { value: `<p>Sale on now</p><img src="${PIXEL}" width="1" height="1">`, isEncodingProblem: false, isTruncated: false },
},
} as unknown as Email;
const IDENTITIES = [{ id: "i1", name: "John", email: "[email protected]", replyTo: null }] as unknown as Identity[];
/**
* Whether the draft will actually load the image. Allowed images go through
* the server's proxy where the deployment has one (#412), so the address is
* escaped inside an `/api/image` URL rather than sitting in `src` as it is.
*/
const fetched = (html: string) => html.includes(`/api/image?url=${encodeURIComponent(PIXEL)}`) || html.includes(`src="${PIXEL}"`);
function replyDraft() {
useMail.setState({
accountId: "a1",
identities: IDENTITIES as never,
getEmails: (async () => [MESSAGE]) as never,
defaultIdentity: (() => IDENTITIES[0]) as never,
loadIdentities: (async () => IDENTITIES) as never,
roleId: (() => null) as never,
});
return useCompose.getState().reply(MESSAGE, "reply").then((key) => useCompose.getState().drafts.find((d) => d.key === key)!);
}
beforeEach(() => {
useCompose.setState({ drafts: [], activeKey: null, pendingSends: {} });
useMail.setState({ imagesShown: {} });
useContacts.setState({ loaded: false } as never);
useSettings.setState({ settings: { ...DEFAULT_SETTINGS, imagePolicy: "ask", composeFormat: "html" } });
});
describe("quoting a message whose images were not allowed", () => {
it("does not put a fetchable address in the draft", async () => {
const d = await replyDraft();
expect(d.html).not.toContain(PIXEL.split("?")[0]! + '"');
expect(d.html).toContain("data-ihm-blocked");
// The src is what the browser would fetch; nothing else in the draft is.
expect(/<img[^>]+src="https:/.test(d.html)).toBe(false);
});
it("keeps the address, so the sent copy is the quote as it was written", async () => {
const d = await replyDraft();
expect(d.html).toContain(PIXEL);
const email = await buildEmailObject({ ...d, to: [{ name: null, email: "[email protected]" }] as EmailAddress[] }, { forSend: true });
const sent = JSON.stringify(email);
expect(sent).toContain(PIXEL);
expect(sent).not.toContain("data-ihm-blocked");
});
it("fetches them once the reader has shown images on that message", async () => {
useMail.setState({ imagesShown: { m1: true } });
const d = await replyDraft();
expect(fetched(d.html)).toBe(true);
expect(d.html).not.toContain("data-ihm-blocked");
});
it("fetches them when the policy is to show images always", async () => {
useSettings.setState((s) => ({ settings: { ...s.settings, imagePolicy: "always" } }));
expect(fetched((await replyDraft()).html)).toBe(true);
});
it("fetches them from a sender the reader trusts", async () => {
useSettings.setState((s) => ({ settings: { ...s.settings, trustedImageSenders: ["[email protected]"] } }));
expect(fetched((await replyDraft()).html)).toBe(true);
});
it("leaves them blocked for a stranger when the policy is contacts only", async () => {
useSettings.setState((s) => ({ settings: { ...s.settings, imagePolicy: "contacts" } }));
expect((await replyDraft()).html).toContain("data-ihm-blocked");
});
});
@@ -0,0 +1,99 @@
import { beforeEach, describe, expect, it } from "vitest";
import { buildEmailObject, useCompose } from "@/store/compose";
import { useMail } from "@/store/mail";
import { useContacts } from "@/store/contacts";
import { useSession } from "@/store/session";
import { DEFAULT_SETTINGS, useSettings } from "@/store/settings";
import { unproxyImages } from "@/lib/mail/remoteImages";
import type { Email, EmailAddress, Identity } from "@/jmap/types";
/*
* Remote images in a quote go through this server, and come back out pointing
* at their own addresses (#412).
*
* Reading a message proxies its images so the sender learns nothing about the
* reader. Quoting fetched them directly, which handed the same pixel the
* reader's IP and user agent. Proxying the quote is only half of it: those
* URLs belong to this deployment, so the copy that is sent has to carry the
* originals or the recipient gets images only this server can serve.
*/
const IMAGE = "https://cdn.example/banner.png?id=7";
const MESSAGE = {
id: "m1", messageId: ["<[email protected]>"], subject: "Sale", references: [], inReplyTo: [], keywords: {},
attachments: [], receivedAt: "2026-09-04T10:00:00Z", mailboxIds: {},
from: [{ name: "Shop", email: "[email protected]" }], to: [{ name: "John", email: "[email protected]" }], cc: [],
htmlBody: [{ partId: "2", type: "text/html" }],
textBody: [{ partId: "1", type: "text/plain" }],
bodyValues: {
"1": { value: "Sale on now", isEncodingProblem: false, isTruncated: false },
"2": { value: `<p>Sale</p><img src="${IMAGE}">`, isEncodingProblem: false, isTruncated: false },
},
} as unknown as Email;
const IDENTITIES = [{ id: "i1", name: "John", email: "[email protected]", replyTo: null }] as unknown as Identity[];
function draftFor(mode: "reply" | "forward") {
useMail.setState({
accountId: "a1",
identities: IDENTITIES as never,
getEmails: (async () => [MESSAGE]) as never,
defaultIdentity: (() => IDENTITIES[0]) as never,
loadIdentities: (async () => IDENTITIES) as never,
roleId: (() => null) as never,
});
return useCompose.getState().reply(MESSAGE, mode).then((key) => useCompose.getState().drafts.find((d) => d.key === key)!);
}
const proxy = (on: boolean) => useSession.setState({ session: { ihasmail: { imageProxy: on } } } as never);
beforeEach(() => {
useCompose.setState({ drafts: [], activeKey: null, pendingSends: {} });
useMail.setState({ imagesShown: {} });
useContacts.setState({ loaded: false } as never);
// Images allowed, so the question is only how they are fetched.
useSettings.setState({ settings: { ...DEFAULT_SETTINGS, imagePolicy: "always", composeFormat: "html" } });
proxy(true);
});
describe("images in a quote, while the reply is being written", () => {
it("are fetched through this server, as reading the message does", async () => {
const d = await draftFor("reply");
expect(d.html).toContain("/api/image?url=");
expect(d.html).not.toContain(`src="${IMAGE}"`);
});
it("are fetched directly where the deployment has no proxy", async () => {
proxy(false);
const d = await draftFor("reply");
expect(d.html).toContain(`src="${IMAGE}"`);
expect(d.html).not.toContain("/api/image?url=");
});
it("go through it on a forward too", async () => {
expect((await draftFor("forward")).html).toContain("/api/image?url=");
});
});
describe("the copy that is sent", () => {
it("points at the image's own address, not at this server", async () => {
const d = await draftFor("reply");
const sent = JSON.stringify(await buildEmailObject({ ...d, to: [{ name: null, email: "[email protected]" }] as EmailAddress[] }, { forSend: true }));
expect(sent).toContain(IMAGE.replace(/&/g, "&"));
expect(sent).not.toContain("/api/image?url=");
});
it("restores a signature or template image that used the proxy as well", () => {
const logo = "https://cdn.example/logo.png";
const html = `<p>Regards</p><img src="/api/image?url=${encodeURIComponent(logo)}"><img src="cid:x@1">`;
const out = unproxyImages(html);
expect(out).toContain(`src="${logo}"`);
expect(out).toContain('src="cid:x@1"');
});
it("leaves everything else alone", () => {
const html = '<img src="cid:logo@1"><img src="blob:http://localhost/abc"><a href="/api/image?url=x">link</a>';
expect(unproxyImages(html)).toBe(html);
});
});
@@ -0,0 +1,124 @@
import { beforeEach, describe, expect, it } from "vitest";
import { useCompose } from "@/store/compose";
import { useMail } from "@/store/mail";
import { DEFAULT_SETTINGS, useSettings } from "@/store/settings";
import type { Email, Identity } from "@/jmap/types";
/*
* Offering to answer a message in the format it was written in (#407).
*
* The trap is `htmlBody`: RFC 8621 derives it, so a plain-text message has one
* too, holding its text/plain part. Reading that as "there is HTML" would
* offer a switch to rich text on every plain-text message, and never offer the
* switch to plain text where it is actually wanted.
*/
const base = {
messageId: ["<[email protected]>"], subject: "Numbers", references: [], inReplyTo: [],
keywords: {}, attachments: [], receivedAt: "2026-09-04T10:00:00Z", mailboxIds: {},
from: [{ name: "Ann", email: "[email protected]" }], to: [{ name: "John", email: "[email protected]" }], cc: [],
};
/** A real multipart/alternative: two parts, one of them text/html. */
const RICH = {
...base, id: "m1",
htmlBody: [{ partId: "2", type: "text/html" }],
textBody: [{ partId: "1", type: "text/plain" }],
bodyValues: { "1": { value: "hi", isEncodingProblem: false, isTruncated: false }, "2": { value: "<p>hi</p>", isEncodingProblem: false, isTruncated: false } },
} as unknown as Email;
/** Plain text, as Stalwart returns it: both lists name the same text/plain part. */
const PLAIN = {
...base, id: "m2",
htmlBody: [{ partId: "1", type: "text/plain" }],
textBody: [{ partId: "1", type: "text/plain" }],
bodyValues: { "1": { value: "hi", isEncodingProblem: false, isTruncated: false } },
} as unknown as Email;
const IDENTITIES = [{ id: "i1", name: "John", email: "[email protected]", replyTo: null }] as unknown as Identity[];
function draftFor(email: Email, mode: "reply" | "replyAll" | "forward") {
useMail.setState({
accountId: "a1",
identities: IDENTITIES as never,
getEmails: (async () => [email]) as never,
defaultIdentity: (() => IDENTITIES[0]) as never,
loadIdentities: (async () => IDENTITIES) as never,
roleId: (() => null) as never,
});
return useCompose.getState().reply(email, mode).then((key) => useCompose.getState().drafts.find((d) => d.key === key)!);
}
const composeIn = (format: "html" | "text") => useSettings.setState({ settings: { ...DEFAULT_SETTINGS, composeFormat: format } });
beforeEach(() => {
useCompose.setState({ drafts: [], activeKey: null });
useSettings.setState({ settings: { ...DEFAULT_SETTINGS } });
});
describe("answering a message written in the other format", () => {
it("offers rich text when a plain-text reply answers a rich message", async () => {
composeIn("text");
const d = await draftFor(RICH, "reply");
expect(d.format).toBe("text");
expect(d.formatOffer).toBe("html");
});
it("offers plain text when a rich reply answers a plain-text message", async () => {
composeIn("html");
const d = await draftFor(PLAIN, "reply");
expect(d.format).toBe("html");
expect(d.formatOffer).toBe("text");
});
it("offers nothing when the formats already agree", async () => {
composeIn("html");
expect((await draftFor(RICH, "reply")).formatOffer).toBeNull();
composeIn("text");
expect((await draftFor(PLAIN, "reply")).formatOffer).toBeNull();
});
it("reads the part's own type, not the derived htmlBody list", async () => {
// PLAIN has an htmlBody; it names the text/plain part. Offering a switch
// to rich text here would fire on every plain-text message there is.
composeIn("text");
expect((await draftFor(PLAIN, "reply")).formatOffer).toBeNull();
});
it("offers on a reply all and on a forward, where the same formatting is lost", async () => {
composeIn("text");
expect((await draftFor(RICH, "replyAll")).formatOffer).toBe("html");
expect((await draftFor(RICH, "forward")).formatOffer).toBe("html");
});
it("carries both bodies either way, so switching has something to switch to", async () => {
composeIn("text");
const d = await draftFor(RICH, "reply");
expect(d.text).toContain("hi");
expect(d.html).toContain("hi");
});
it("keeps the quoted message in both formats, so a switch can restore it", async () => {
composeIn("text");
const d = await draftFor(RICH, "reply");
// The HTML quote is the original's markup, not the text one converted.
expect(d.quoteHtml).toContain("<p>hi</p>");
expect(d.quoteHtml).toContain("ihm-quote");
expect(d.quoteText).toContain("Ann");
expect(d.text.endsWith(d.quoteText)).toBe(true);
});
it("quotes nothing on a message started from scratch", () => {
composeIn("text");
const key = useCompose.getState().open();
const d = useCompose.getState().drafts.find((x) => x.key === key)!;
expect(d.quoteHtml).toBe("");
expect(d.quoteText).toBe("");
});
it("makes no offer on a message started from scratch", () => {
composeIn("text");
const key = useCompose.getState().open();
expect(useCompose.getState().drafts.find((x) => x.key === key)!.formatOffer).toBeNull();
});
});
+74 -6
View File
@@ -4,7 +4,8 @@ import type { Email, EmailAddress, EmailBodyPart, Id, Identity, SetResponse } fr
import { formatFullDate, uid } from "@/lib/format";
import { formatAddress, parseMailto, sameAddress, uniqueAddresses } from "@/lib/address";
import { escapeHtml, htmlToText, quoteText, replySubject, textToHtml } from "@/lib/text/text";
import { sanitizeEmailHtml, sanitizeEditorHtml } from "@/lib/text/html";
import { hasHtmlAlternative, sanitizeEmailHtml, sanitizeEditorHtml } from "@/lib/text/html";
import { remoteImagesAllowed, restoreBlockedImages, unproxyImages } from "@/lib/mail/remoteImages";
import { toast } from "@/ui/toast";
import { useMail, FULL_PROPS, BODY_PROPS } from "./mail";
import { useSession } from "./session";
@@ -13,6 +14,7 @@ import { formatScheduleTime, holdUntil } from "@/lib/schedule";
import { t as translate } from "@/lib/i18n";
import { BASE_PATH } from "@/lib/basePath";
import { settings } from "./settings";
import { useContacts } from "./contacts";
import { emlFilename } from "@/lib/text/emlName";
import { fillPlaceholders, type PlaceholderContext } from "@/lib/templatePlaceholders";
import { shareBody, type SharedContent } from "@/lib/shareTarget";
@@ -75,6 +77,20 @@ export interface Draft {
/** Original identity signature HTML currently embedded, to replace on identity switch. */
signatureHtml: string;
replyMode: "reply" | "replyAll" | "forward" | null;
/**
* The quoted message as it was prepared in each format, kept so that
* switching format re-attaches the original rather than a conversion of
* whatever the other format flattened it into. Empty on a draft that quotes
* nothing.
*/
quoteHtml: string;
quoteText: string;
/**
* The format the message being answered was written in, when it is not the
* one this draft opened in (#407). The composer offers the switch; answering
* it either way, or dismissing it, clears this.
*/
formatOffer: "html" | "text" | null;
mailboxIdOnSend?: Id | null;
/** When set, hand the message to the server held until this instant. */
sendAt: number | null;
@@ -137,7 +153,7 @@ function blankDraft(init: Partial<Draft> = {}): Draft {
showBcc: false,
showReplyTo: false,
minimized: false,
maximized: false,
maximized: s.composeMaximized,
dirty: false,
savedAt: null,
saving: false,
@@ -145,11 +161,39 @@ function blankDraft(init: Partial<Draft> = {}): Draft {
error: null,
signatureHtml: "",
replyMode: null,
quoteHtml: "",
quoteText: "",
formatOffer: null,
sendAt: null,
...init,
};
}
/**
* Whether this message's remote images may be fetched into a composer.
*
* The same question the reader answered, asked with the same inputs: the
* policy, the trusted senders, whether the sender is a contact, and whether
* the reader pressed "Show images" on this message.
*/
/** Whether this deployment fetches remote images through its own server. */
function imageProxyOn(): boolean {
return useSession.getState().session?.ihasmail?.imageProxy ?? true;
}
function remoteImagesForMessage(email: Email): boolean {
const s = settings();
const from = email.from?.[0]?.email;
const contacts = useContacts.getState();
return remoteImagesAllowed({
from,
policy: s.imagePolicy,
trusted: s.trustedImageSenders,
inContacts: Boolean(from && contacts.loaded && contacts.lookupByEmail(from)),
shown: Boolean(useMail.getState().imagesShown[email.id]),
});
}
export function signatureBlock(identity: Identity | undefined, format: "html" | "text"): string {
if (!identity) return "";
if (format === "text") return identity.textSignature ? `\n\n-- \n${identity.textSignature}` : "";
@@ -243,7 +287,7 @@ export const useCompose = create<ComposeState>((set, get) => ({
showCc: Boolean(full.cc?.length),
showBcc: Boolean(full.bcc?.length),
subject: full.subject ?? "",
html: html ? sanitizeEmailHtml(html, { cidMap, allowRemote: true, dropStyleBlocks: true }).html : textToHtml(text).replace(/\n/g, "<br>"),
html: html ? sanitizeEmailHtml(html, { cidMap, allowRemote: remoteImagesForMessage(full), proxyRemote: imageProxyOn(), dropStyleBlocks: true }).html : textToHtml(text).replace(/\n/g, "<br>"),
text: text || (html ? htmlToText(html) : ""),
format: html ? "html" : settings().composeFormat,
attachments,
@@ -309,7 +353,7 @@ export const useCompose = create<ComposeState>((set, get) => ({
showCc: Boolean(full.cc?.length),
showBcc: Boolean(full.bcc?.length),
subject: full.subject ?? "",
html: html ? sanitizeEmailHtml(html, { cidMap, allowRemote: true, dropStyleBlocks: true }).html : textToHtml(text).replace(/\n/g, "<br>"),
html: html ? sanitizeEmailHtml(html, { cidMap, allowRemote: remoteImagesForMessage(full), proxyRemote: imageProxyOn(), dropStyleBlocks: true }).html : textToHtml(text).replace(/\n/g, "<br>"),
text: text || (html ? htmlToText(html) : ""),
format: html ? "html" : settings().composeFormat,
attachments,
@@ -379,6 +423,13 @@ export const useCompose = create<ComposeState>((set, get) => ({
const textPart = full.textBody?.[0];
const origHtml = htmlPart?.partId ? (full.bodyValues?.[htmlPart.partId]?.value ?? "") : "";
const origText = textPart?.partId ? (full.bodyValues?.[textPart.partId]?.value ?? "") : "";
/*
* What the message being answered was really written in. `htmlBody` is
* derived, so its presence proves nothing -- hasHtmlAlternative() reads the
* part's own type. Getting this wrong would offer every plain-text message
* a switch to rich text it does not need.
*/
const origFormat = hasHtmlAlternative(htmlPart, origHtml) ? "html" : "text";
const accountId = mail.accountId!;
const attachments: ComposeAttachment[] = [];
const cidMap: Record<string, string> = {};
@@ -389,9 +440,21 @@ export const useCompose = create<ComposeState>((set, get) => ({
attachments.push({ id: uid("a"), name: a.name ?? "attachment", type: a.type, size: a.size, blobId: a.blobId, progress: 100, error: null, cid: a.cid ?? undefined, inline });
}
}
/*
* Quoting renders the message a second time, so the reader's decision
* about its remote images applies here too: a quote that fetched what
* they declined would report the message read to whoever was counting
* (#410). Blocked images keep their address and get it back on the way
* out, so the recipient's copy is the quote as its sender wrote it.
*/
const allowRemote = remoteImagesForMessage(full);
// Fetched through this server while the reply is written, as reading the
// message does, and pointed back at their own addresses on the way out
// (#412).
const proxyRemote = imageProxyOn();
// Inline images are shown via their blob URLs in the editor and converted back to cid: at send time.
const quotedHtmlBody = origHtml
? sanitizeEmailHtml(origHtml, { cidMap, allowRemote: true, proxyRemote: false, dropStyleBlocks: true }).html
? sanitizeEmailHtml(origHtml, { cidMap, allowRemote, proxyRemote, dropStyleBlocks: true }).html
: textToHtml(origText).replace(/\n/g, "<br>");
const fromStr = escapeHtml((full.from ?? []).map(formatAddress).join(", "));
const date = formatFullDate(full.receivedAt);
@@ -434,6 +497,9 @@ export const useCompose = create<ComposeState>((set, get) => ({
relatedKeyword: mode === "forward" ? "$forwarded" : "$answered",
signatureHtml: sigHtml,
replyMode: mode,
quoteHtml,
quoteText: quoteTxt,
formatOffer: origFormat === s.composeFormat ? null : origFormat,
});
set((st) => ({ drafts: [...st.drafts, d], activeKey: d.key }));
return d.key;
@@ -754,7 +820,9 @@ export async function buildEmailObject(d: Draft, opts: { forSend: boolean; mailb
if (!ident) throw new Error(translate("No sending identity available"));
const from: EmailAddress = { name: ident.name || null, email: ident.email };
let html = d.format === "html" ? d.html : "";
// Images blocked when the message was quoted keep their address; the copy
// that leaves carries it, and the recipient's client decides for itself.
let html = d.format === "html" ? unproxyImages(restoreBlockedImages(d.html)) : "";
const text = d.format === "html" ? htmlToText(d.html) : d.text;
// Inline attachments shown via blob URLs in the editor → back to cid: references.
+21 -1
View File
@@ -29,6 +29,7 @@ import { plural, t } from "@/lib/i18n";
import { withBase } from "@/lib/basePath";
import { isDeviceTrusted, loadRaw, saveJson } from "@/lib/storage";
import { MAILBOX_PROPS, LIST_PROPS, FULL_PROPS, BODY_PROPS } from "./props";
import { compareFolders } from "@/lib/mailbox/folderOrder";
import { type ListQuery, type MailState } from "./types";
import { playNewMailSound, showNotification } from "@/lib/notify/notify";
import { pushEnabledHere } from "@/lib/notify/webpush";
@@ -77,6 +78,7 @@ function offerArchiveFolder(retry: () => Promise<void>): void {
export const useMail = create<MailState>((set, get) => ({
accountId: null,
imagesShown: {},
mailboxes: {},
mailboxState: null,
mailboxesLoaded: false,
@@ -161,7 +163,7 @@ export const useMail = create<MailState>((set, get) => ({
childrenOf(parentId) {
return Object.values(get().mailboxes)
.filter((m) => (m.parentId ?? null) === parentId)
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
.sort(compareFolders);
},
async query(q, opts = {}) {
@@ -762,6 +764,20 @@ export const useMail = create<MailState>((set, get) => ({
if (before.length) await followFolders(before);
},
async arrangeMailboxes(updates) {
const accountId = get().accountId!;
const moved = Object.keys(updates).filter((id) => updates[id]!.parentId !== undefined);
const before = moved.flatMap((id) => folderRefs(get(), id));
// One request for the whole level rather than one per folder. JMAP applies
// each update on its own, so a refusal can leave the level part-numbered;
// reloading shows whatever order the server actually kept.
const res = await client.call<SetResponse>("Mailbox/set", { accountId, update: updates });
const failed = Object.values(res.notUpdated ?? {})[0];
await get().loadMailboxes();
if (failed) throw new Error(setErrorMessage(failed));
if (before.length) await followFolders(before);
},
async destroyMailbox(id, removeEmails = true) {
const accountId = get().accountId!;
const before = folderRefs(get(), id);
@@ -851,6 +867,10 @@ export const useMail = create<MailState>((set, get) => ({
}
},
showImages(id) {
set((s) => ({ imagesShown: { ...s.imagesShown, [id]: true } }));
},
select(ids, on) {
set((s) => {
const next = { ...s.selected };
+10
View File
@@ -33,6 +33,12 @@ export interface ListState extends ListQuery {
export interface MailState {
accountId: Id | null;
/**
* Messages the reader pressed "Show images" on, this session. Kept here
* rather than in the message view because replying quotes the message into
* a second window, which has to honour the same decision.
*/
imagesShown: Record<Id, boolean>;
mailboxes: Record<Id, Mailbox>;
mailboxState: string | null;
mailboxesLoaded: boolean;
@@ -99,6 +105,8 @@ export interface MailState {
/** Give something the Archive role -- adopting a folder already named for it, or making one. */
ensureArchiveFolder(): Promise<Id>;
updateMailbox(id: Id, patch: Partial<Mailbox>): Promise<void>;
/** Several folders' `sortOrder` (and at most a new parent) in one request: a reorder from the tree. */
arrangeMailboxes(updates: Record<Id, Partial<Mailbox>>): Promise<void>;
destroyMailbox(id: Id, removeEmails?: boolean): Promise<void>;
loadIdentities(): Promise<Identity[]>;
@@ -111,6 +119,8 @@ export interface MailState {
saveVacation(patch: Partial<VacationResponse>): Promise<void>;
loadQuota(): Promise<void>;
/** Remember that this message's remote images were allowed by hand. */
showImages(id: Id): void;
select(ids: Id[], on: boolean): void;
clearSelection(): void;
/** Refresh the per-label unread counts, in one request. */
+6
View File
@@ -251,6 +251,11 @@ export interface Settings {
archiveOnReply: boolean;
autoAdvance: "newer" | "older" | "list";
spellcheck: boolean;
/**
* Open every new composer full screen (#401). Desktop only: on a phone the
* composer fills the screen already and has no size to choose.
*/
composeMaximized: boolean;
sendAndArchive: boolean;
/** Width (px) of the message list when the reading pane is on the right. */
listPaneWidth: number;
@@ -376,6 +381,7 @@ export const DEFAULT_SETTINGS: Settings = {
archiveOnReply: false,
autoAdvance: "list",
spellcheck: true,
composeMaximized: false,
sendAndArchive: false,
listPaneWidth: 520,
listPaneHeight: 340,
+6
View File
@@ -1310,6 +1310,9 @@ a.menu-item:hover { color: var(--fg); }
.nav-item.active.unread .nav-label, .nav-item.active.unread .nav-count { color: inherit; }
.nav-item.drop-target { background: var(--accent-soft); outline: 2px dashed var(--accent); outline-offset: -2px; }
.nav-item.folder-row.dragging { opacity: .45; }
/* A folder dragged between two others: a line where it will land. */
.nav-item.folder-row.drop-before { box-shadow: inset 0 2px 0 var(--accent); }
.nav-item.folder-row.drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
/* A folder color tints its icon; the label keeps the sidebar's contrast. */
.folder-row .folder-icon { display: inline-flex; align-items: center; }
/* .nav-item svg sets color on the svg itself, so inheriting from the span is
@@ -1567,6 +1570,9 @@ a.menu-item:hover { color: var(--fg); }
.composer-head .title { flex: 1; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.composer-head .status { color: var(--fg-faint); font-size: .8em; margin-right: 6px; white-space: nowrap; }
.composer-body { display: flex; flex-direction: column; flex: 1; min-height: 0; }
/* An offer the draft makes about itself, above the editor: quiet, one line, and dismissible. */
.composer-notice { display: flex; align-items: center; gap: 8px; padding: 6px 10px 6px 14px; background: var(--accent-soft); color: var(--accent-soft-fg); border-bottom: 1px solid var(--border); font-size: .85em; flex: 0 0 auto; }
.composer-notice span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.composer-fields { flex: 0 0 auto; padding: 0 12px; }
.composer-field { display: flex; align-items: center; gap: 8px; min-height: 40px; border-bottom: 1px solid var(--border); padding: 4px 0; }
.composer-field > label { color: var(--fg-muted); width: 42px; flex: 0 0 auto; font-size: .92em; }
+1 -1
View File
@@ -186,7 +186,7 @@ export function AppShell({ children }: { children: ReactNode }) {
{/* The project site. It is linked from the login screen footer, which
is a page a signed-in user never sees again -- so from inside the
app there was no way back to it. */}
<MenuItem icon={<Globe size={16} />} label={t("About ihasmail")} href="https://ihasmail.org" external />
<MenuItem icon={<Globe size={16} />} label={t("About {app}", { app: appName })} href="https://ihasmail.org" external />
<MenuItem icon={<Settings size={16} />} label={t("Settings")} onClick={() => navigate("/settings")} />
{/* Only for an account whose Stalwart role manages other accounts.
Nobody else is shown an entry that would open onto refusals. */}
+3 -1
View File
@@ -1,4 +1,5 @@
import { shareSummary, type SharedContent } from "@/lib/shareTarget";
import { useAppName } from "@/lib/brand";
import { confirmDialog } from "@/ui/dialog";
import { t } from "@/lib/i18n";
@@ -23,6 +24,7 @@ export async function offerShare(share: SharedContent, open: (share: SharedConte
/** What arrived, so the reader can tell whether it is theirs. */
function ShareSummary({ share }: { share: SharedContent }) {
const appName = useAppName();
const { title, preview, files } = shareSummary(share);
return (
<div>
@@ -39,7 +41,7 @@ function ShareSummary({ share }: { share: SharedContent }) {
{files.length > 5 && <li></li>}
</ul>
)}
<p>{t("Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it.")}</p>
<p>{t("Something was shared with {app}. Nothing is sent until you choose Send. If you didn't just share this, discard it.", { app: appName })}</p>
</div>
);
}
+67 -43
View File
@@ -15,7 +15,7 @@ import type { Anchor } from "@/ui/popover";
import { CalendarContextMenu, eventColor, type CalendarContext } from "./CalendarContextMenu";
import { toast } from "@/ui/toast";
import { askEditScope, droppedMessage, runScoped } from "./scope";
import { canDragEvent, dayDelta, moveByDaysPatch, movePatch, pixelsToMinutes, resizePatch, snap, type DragPatch } from "@/lib/calendar/eventDrag";
import { canDragEvent, columnsMoved, dayDelta, moveAcrossPatch, moveByDaysPatch, pixelsToMinutes, resizePatch, snap, type DragPatch } from "@/lib/calendar/eventDrag";
import { t as translate } from "@/lib/i18n";
type View = "month" | "week" | "day" | "agenda";
@@ -237,16 +237,47 @@ function MonthView({ anchor, weekStart, onDay, onEvent, onEventContext, onSlotCo
const weeks = [...Array(6)].map((_, w) => grid.slice(w * 7, w * 7 + 7));
const dow = weeks[0]!.map((d) => formatWeekday(d));
const maxPer = 4;
const chipDrag = useChipDrag(".month-cell", onDragCommit);
return (
<div className="month-grid">
<div className="dow-row">{dow.map((d) => <div key={d}>{d}</div>)}</div>
{weeks.map((days, wi) => (
<div key={wi} className="week-row">
{days.map((d) => {
const dayEnd = addDays(d, 1);
const evs = instances.filter((i) => i.start < dayEnd && i.end > d);
const shown = evs.slice(0, maxPer);
return (
<div key={d.toISOString()} data-date={toLocalDateOnly(d)} className={`month-cell ${d.getMonth() !== anchor.getMonth() ? "other" : ""} ${isToday(d) ? "today" : ""}`} onClick={() => onCreate(d)} onDoubleClick={() => onDay(d)} onContextMenu={(e) => onSlotContext(new Date(d.getTime() + 9 * 3600_000), new Date(d.getTime() + 10 * 3600_000), false, e)}>
<span className="day-num" onClick={(e) => { e.stopPropagation(); onDay(d); }}>{d.getDate() === 1 ? formatDayMonth(d) : d.getDate()}</span>
{shown.map((i) => <EventChip key={i.key} inst={i} day={d} onClick={(el) => onEvent(i, el)} onContext={(e) => onEventContext(i, e)} onDragStart={canDragEvent(i.event, i.calendar) ? (e) => chipDrag.begin(i, d, e) : undefined} dragging={chipDrag.draggingKey === i.key} suppressClick={() => chipDrag.draggedRef.current} />)}
{evs.length > maxPer && <span className="more" onClick={(e) => { e.stopPropagation(); onDay(d); }}>{translate("+{n} more", { n: evs.length - maxPer })}</span>}
</div>
);
})}
</div>
))}
</div>
);
}
/*
* Dragging a chip to another day: the month grid, and the all-day row above
* the week grid. A cell there is a day and nothing finer, so the only question
* a drag asks is "which day". The cell under the pointer is found by asking
* the document rather than by tracking enter and leave on every cell: one
* question at the end beats bookkeeping throughout.
*
* The move is counted from the day the chip was picked up on, not from the
* event's first day, so a three-day event grabbed on its last day and dropped
* one cell to the right moves one day, not three.
*/
function useChipDrag(cellSelector: string, onDragCommit: (i: EventInstance, patch: DragPatch) => void) {
const [draggingKey, setDraggingKey] = useState<string | null>(null);
const draggedRef = useRef(false);
/*
* A month cell is a day and nothing finer, so the only question a drag here
* asks is "which day". The cell under the pointer is found by asking the
* document rather than by tracking enter and leave on forty-two cells: one
* question at the end beats bookkeeping throughout.
*/
const beginChipDrag = (inst: EventInstance, e: React.PointerEvent) => {
const begin = (inst: EventInstance, grabbedOn: Date, e: React.PointerEvent) => {
if (e.button !== 0 && e.pointerType === "mouse") return;
if (!canDragEvent(inst.event, inst.calendar)) return;
e.stopPropagation();
@@ -255,7 +286,7 @@ function MonthView({ anchor, weekStart, onDay, onEvent, onEventContext, onSlotCo
el.setPointerCapture(e.pointerId);
let landedOn: string | null = null;
const onPointerMove = (ev: PointerEvent) => {
const cell = document.elementFromPoint(ev.clientX, ev.clientY)?.closest<HTMLElement>(".month-cell");
const cell = document.elementFromPoint(ev.clientX, ev.clientY)?.closest<HTMLElement>(cellSelector);
const date = cell?.dataset.date ?? null;
if (date) landedOn = date;
if (!draggedRef.current) draggedRef.current = true;
@@ -277,8 +308,8 @@ function MonthView({ anchor, weekStart, onDay, onEvent, onEventContext, onSlotCo
const target = parts && parts.length === 3 ? new Date(parts[0]!, parts[1]! - 1, parts[2]!) : null;
// How far the hand moved it, in local days -- see moveByDaysPatch for
// why the target date itself is the wrong thing to write.
if (target && !isSameDay(target, inst.start)) {
onDragCommit(inst, moveByDaysPatch(inst.event.start, dayDelta(inst.start, target)));
if (target && !isSameDay(target, grabbedOn)) {
onDragCommit(inst, moveByDaysPatch(inst.event.start, dayDelta(grabbedOn, target)));
}
window.setTimeout(() => (draggedRef.current = false), 0);
};
@@ -287,27 +318,7 @@ function MonthView({ anchor, weekStart, onDay, onEvent, onEventContext, onSlotCo
el.addEventListener("pointercancel", finish);
};
return (
<div className="month-grid">
<div className="dow-row">{dow.map((d) => <div key={d}>{d}</div>)}</div>
{weeks.map((days, wi) => (
<div key={wi} className="week-row">
{days.map((d) => {
const dayEnd = addDays(d, 1);
const evs = instances.filter((i) => i.start < dayEnd && i.end > d);
const shown = evs.slice(0, maxPer);
return (
<div key={d.toISOString()} data-date={toLocalDateOnly(d)} className={`month-cell ${d.getMonth() !== anchor.getMonth() ? "other" : ""} ${isToday(d) ? "today" : ""}`} onClick={() => onCreate(d)} onDoubleClick={() => onDay(d)} onContextMenu={(e) => onSlotContext(new Date(d.getTime() + 9 * 3600_000), new Date(d.getTime() + 10 * 3600_000), false, e)}>
<span className="day-num" onClick={(e) => { e.stopPropagation(); onDay(d); }}>{d.getDate() === 1 ? formatDayMonth(d) : d.getDate()}</span>
{shown.map((i) => <EventChip key={i.key} inst={i} day={d} onClick={(el) => onEvent(i, el)} onContext={(e) => onEventContext(i, e)} onDragStart={canDragEvent(i.event, i.calendar) ? (e) => beginChipDrag(i, e) : undefined} dragging={draggingKey === i.key} suppressClick={() => draggedRef.current} />)}
{evs.length > maxPer && <span className="more" onClick={(e) => { e.stopPropagation(); onDay(d); }}>{translate("+{n} more", { n: evs.length - maxPer })}</span>}
</div>
);
})}
</div>
))}
</div>
);
return { begin, draggingKey, draggedRef };
}
function statusClass(i: EventInstance): string {
@@ -358,24 +369,36 @@ function TimeGrid({ days, onEvent, onEventContext, onSlotContext, onCreate, onDa
* new time, so the preview is one number and the commit is the same
* arithmetic the tests cover.
*/
const [moving, setMoving] = useState<{ key: string; deltaMin: number; mode: "move" | "resize" } | null>(null);
const [moving, setMoving] = useState<{ key: string; deltaMin: number; deltaDays: number; shiftPx: number; mode: "move" | "resize" } | null>(null);
/* A drag ends with a pointerup, and a pointerup on the same element is also
a click. Without this, letting go of a moved event opens its popover. */
const draggedRef = useRef(false);
const allDayDrag = useChipDrag(".ad-cell", onDragCommit);
const beginDrag = (inst: EventInstance, mode: "move" | "resize", e: React.PointerEvent) => {
/*
* A move goes sideways as well as up and down: across the columns to
* another day, keeping whatever hour it was dragged to. The block stays in
* its own column while it moves and is drawn shifted by whole columns, so
* the preview is two numbers and nothing is re-laid-out until it lands.
* A resize only ever changes the end, so it stays vertical.
*/
const beginDrag = (inst: EventInstance, mode: "move" | "resize", column: number, e: React.PointerEvent) => {
if (e.button !== 0 && e.pointerType === "mouse") return;
if (!canDragEvent(inst.event, inst.calendar)) return;
e.stopPropagation();
e.preventDefault();
const el = e.currentTarget as HTMLElement;
const startX = e.clientX;
const startY = e.clientY;
const colWidth = mode === "move" ? el.closest<HTMLElement>(".day-col")?.getBoundingClientRect().width ?? 0 : 0;
let delta = 0;
let deltaDays = 0;
el.setPointerCapture(e.pointerId);
const onPointerMove = (ev: PointerEvent) => {
delta = snap(pixelsToMinutes(ev.clientY - startY, HOUR_H));
if (delta !== 0) draggedRef.current = true;
setMoving({ key: inst.key, deltaMin: delta, mode });
deltaDays = columnsMoved(ev.clientX - startX, colWidth, column, days.length);
if (delta !== 0 || deltaDays !== 0) draggedRef.current = true;
setMoving({ key: inst.key, deltaMin: delta, deltaDays, shiftPx: deltaDays * colWidth, mode });
};
const finish = () => {
el.removeEventListener("pointermove", onPointerMove);
@@ -387,9 +410,9 @@ function TimeGrid({ days, onEvent, onEventContext, onSlotContext, onCreate, onDa
/* already released, which is fine */
}
setMoving(null);
if (delta !== 0) {
if (delta !== 0 || deltaDays !== 0) {
const seconds = (inst.end.getTime() - inst.start.getTime()) / 1000;
onDragCommit(inst, mode === "move" ? movePatch(inst.event.start, delta) : resizePatch(seconds, delta));
onDragCommit(inst, mode === "move" ? moveAcrossPatch(inst.event.start, deltaDays, delta) : resizePatch(seconds, delta));
}
// Cleared after the click that follows this pointerup has been swallowed.
window.setTimeout(() => (draggedRef.current = false), 0);
@@ -431,8 +454,8 @@ function TimeGrid({ days, onEvent, onEventContext, onSlotContext, onCreate, onDa
<div className="week-allday">
<div className="ad-label">{translate("all-day")}</div>
{days.map((d) => (
<div key={d.toISOString()} className="ad-cell" onClick={() => onCreate(d, addDays(d, 1), true)} onContextMenu={(e) => onSlotContext(d, addDays(d, 1), true, e)}>
{allDay(d).map((i) => <EventChip key={i.key} inst={i} day={d} onClick={(el) => onEvent(i, el)} onContext={(e) => onEventContext(i, e)} />)}
<div key={d.toISOString()} data-date={toLocalDateOnly(d)} className="ad-cell" onClick={() => onCreate(d, addDays(d, 1), true)} onContextMenu={(e) => onSlotContext(d, addDays(d, 1), true, e)}>
{allDay(d).map((i) => <EventChip key={i.key} inst={i} day={d} onClick={(el) => onEvent(i, el)} onContext={(e) => onEventContext(i, e)} onDragStart={canDragEvent(i.event, i.calendar) ? (e) => allDayDrag.begin(i, d, e) : undefined} dragging={allDayDrag.draggingKey === i.key} suppressClick={() => allDayDrag.draggedRef.current} />)}
</div>
))}
</div>
@@ -441,7 +464,7 @@ function TimeGrid({ days, onEvent, onEventContext, onSlotContext, onCreate, onDa
<div className="time-col">
{[...Array(24)].map((_, h) => h > 0 && <span key={h} className="hour-label" style={{ top: h * HOUR_H }}>{formatHourLabel(h)}</span>)}
</div>
{days.map((d) => {
{days.map((d, column) => {
const evs = layoutOverlaps(timed(d), d);
const today = isToday(d);
const nowTop = ((now.getHours() * 60 + now.getMinutes()) / 60) * HOUR_H;
@@ -490,9 +513,10 @@ function TimeGrid({ days, onEvent, onEventContext, onSlotContext, onCreate, onDa
height: Math.max(height + (moving?.key === inst.key && moving.mode === "resize" ? (moving.deltaMin / 60) * HOUR_H : 0), 18),
left: `${left}%`,
width: `calc(${width}% - 3px)`,
transform: moving?.key === inst.key && moving.shiftPx ? `translateX(${moving.shiftPx}px)` : undefined,
background: color,
}}
onPointerDown={(e) => beginDrag(inst, "move", e)}
onPointerDown={(e) => beginDrag(inst, "move", column, e)}
onClick={(e) => { e.stopPropagation(); if (draggedRef.current) return; onEvent(inst, e.currentTarget); }}
onContextMenu={(e) => onEventContext(inst, e)}
title={inst.event.title ?? ""}
@@ -501,7 +525,7 @@ function TimeGrid({ days, onEvent, onEventContext, onSlotContext, onCreate, onDa
/* Its own element rather than an edge zone on the block,
so a thumb has something to aim at and the move drag
does not have to guess which one was meant. */
<div className="ev-resize" onPointerDown={(e) => beginDrag(inst, "resize", e)} aria-hidden="true" />
<div className="ev-resize" onPointerDown={(e) => beginDrag(inst, "resize", column, e)} aria-hidden="true" />
)}
<div className="ev-title">{inst.event.title || "(untitled)"}</div>
{height > 30 && <div className="ev-time">{formatTime(inst.start)} {formatTime(inst.end)}</div>}
+33 -2
View File
@@ -147,11 +147,26 @@ export function Composer({ draft }: { draft: Draft }) {
patch({ sendAt: at.getTime() });
};
/*
* Switching format converts what has been written, but the quoted message
* is not something this draft wrote: it was prepared in both formats when
* the reply opened. Converting the plain-text quote into HTML would hand
* back a flattened copy of a message that still exists in its original
* markup, so re-attach that instead, and keep only what the author typed
* above it. Where the quote can no longer be found -- edited, or a draft
* that quotes nothing -- convert the whole body as before.
*/
const toggleFormat = () => {
// Whichever way the format is changed, the offer has been answered.
if (d.format === "html") {
patch({ format: "text", text: htmlToText(d.html) });
const at = d.quoteHtml ? d.html.indexOf('<div class="ihm-quote">') : -1;
const written = at >= 0 ? htmlToText(d.html.slice(0, at)) : htmlToText(d.html);
patch({ format: "text", text: at >= 0 ? written.replace(/\s+$/, "") + d.quoteText : written, formatOffer: null });
} else {
patch({ format: "html", html: textToHtml(d.text, { linkify: false, quoteColors: false }).replace(/\n/g, "<br>") });
const keeps = Boolean(d.quoteText) && d.text.endsWith(d.quoteText);
const written = keeps ? d.text.slice(0, d.text.length - d.quoteText.length) : d.text;
const asHtml = textToHtml(written, { linkify: false, quoteColors: false }).replace(/\n/g, "<br>");
patch({ format: "html", html: keeps ? asHtml + d.quoteHtml : asHtml, formatOffer: null });
}
};
@@ -261,6 +276,22 @@ export function Composer({ draft }: { draft: Draft }) {
)}
</div>
</div>
{/*
Replying in one format to a message written in the other loses
something either way: the formatting of a rich reply, or the plain
text somebody chose to write in. The draft opens in the format the
settings ask for, and this offers the other one for this message
only, rather than quietly overriding the setting (#407).
*/}
{d.formatOffer && (
<div className="composer-notice">
<span>{d.formatOffer === "html" ? translate("This message is rich text") : translate("This message is plain text")}</span>
<button type="button" className="btn btn-sm" onClick={toggleFormat}>
{d.formatOffer === "html" ? translate("Switch to rich text") : translate("Switch to plain text")}
</button>
<button type="button" className="icon-btn sm" aria-label={translate("Dismiss")} onClick={() => patch({ formatOffer: null })}><X size={14} /></button>
</div>
)}
{d.format === "html" ? (
<RichEditor ref={editorRef} html={d.html} onChange={onHtml} placeholder={translate("Write your message…")} spellcheck={settings.spellcheck} onFiles={(files) => addFiles(key, files)} showToolbar={showToolbar} autoFocus={initialFocus === "body"} />
) : (
+3 -1
View File
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react";
import { useAppName } from "@/lib/brand";
import { Clock } from "lucide-react";
import { Dialog } from "@/ui/dialog";
import { DateTimeField } from "@/ui/datefield";
@@ -33,6 +34,7 @@ export function ScheduleDialog({ open, maxMs, initial, onClose, onPick }: {
onClose: () => void;
onPick: (at: Date) => void;
}) {
const appName = useAppName();
const [value, setValue] = useState(() => toInputDateTime(initial ? new Date(initial) : roundToNext(new Date(Date.now() + 3_600_000), 15)));
const at = fromInputDateTime(value);
const error = scheduleError(at, new Date(), maxMs);
@@ -58,7 +60,7 @@ export function ScheduleDialog({ open, maxMs, initial, onClose, onPick }: {
<p className="hint" style={{ color: "var(--danger)" }}>{error}</p>
) : (
<p className="hint">
{`${t("The message waits on the server, so it goes out whether or not ihasmail is open.")}${maxMs > 0 ? ` ${t("This server holds a message for up to {span}.", { span: describeSpan(maxMs) })}` : ""}`}
{`${t("The message waits on the server, so it goes out whether or not {app} is open.", { app: appName })}${maxMs > 0 ? ` ${t("This server holds a message for up to {span}.", { span: describeSpan(maxMs) })}` : ""}`}
</p>
)}
</Dialog>
@@ -0,0 +1,107 @@
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { Composer } from "../Composer";
import { useCompose, type Draft } from "@/store/compose";
import { useMail } from "@/store/mail";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
/**
* The bar the composer shows when the draft's format doesn't match the message
* it is answering (#407). A store test can say the offer was made; only the
* component can say that pressing it converts the body and puts the bar away.
*/
window.matchMedia = ((q: string) => ({ matches: false, media: q, addEventListener() {}, removeEventListener() {} })) as unknown as typeof window.matchMedia;
const QUOTE_HTML = '<div class="ihm-quote"><br><div>On Friday, Ann wrote:</div><blockquote><p>Look at <b>this</b></p></blockquote></div>';
const QUOTE_TEXT = "\n\nOn Friday, Ann wrote:\n> Look at this";
const REPLY: Partial<Draft> = {
key: "d1", replyMode: "reply", subject: "Re: Numbers",
format: "text", text: QUOTE_TEXT, html: `<div><br></div>${QUOTE_HTML}`,
quoteHtml: QUOTE_HTML, quoteText: QUOTE_TEXT,
formatOffer: "html",
};
describe("the format offer in the composer", () => {
let host: HTMLDivElement;
let root: Root;
const bar = () => document.querySelector(".composer-notice");
const draft = () => useCompose.getState().drafts[0]!;
const button = (label: string) => Array.from(document.querySelectorAll<HTMLElement>(".composer-notice button")).find((b) => b.textContent === label || b.getAttribute("aria-label") === label)!;
beforeEach(() => {
useMail.setState({ accountId: "a1", identities: [] as never });
useCompose.setState({ drafts: [], activeKey: null, pendingSends: {} });
const key = useCompose.getState().open();
useCompose.getState().update(key, REPLY);
host = document.createElement("div");
document.body.appendChild(host);
root = createRoot(host);
act(() => root.render(<Composer draft={draft()} />));
});
afterEach(() => { act(() => root.unmount()); host.remove(); });
it("offers the message's own format, and says which it is", () => {
expect(bar()?.textContent).toContain("This message is rich text");
expect(button("Switch to rich text")).toBeTruthy();
});
it("switches this draft and puts the bar away", () => {
act(() => button("Switch to rich text").click());
act(() => root.render(<Composer draft={draft()} />));
expect(draft().format).toBe("html");
// The quoted reply came across, rather than the editor opening empty.
expect(draft().html).toContain("Ann wrote");
expect(bar()).toBeNull();
});
/*
* The message being quoted was prepared in both formats when the reply
* opened. Switching used to convert the plain-text body it had, handing
* back a flattened copy -- "> Look at this" -- of markup that still
* existed untouched on the draft.
*/
it("restores the original message, rather than converting the flattened quote", () => {
act(() => button("Switch to rich text").click());
act(() => root.render(<Composer draft={draft()} />));
expect(draft().html).toContain("<b>this</b>");
expect(draft().html).toContain("<blockquote>");
expect(draft().html).not.toContain("&gt; Look at this");
});
it("keeps what the author typed above the quote", () => {
useCompose.getState().update("d1", { text: `Thanks, that helps.${QUOTE_TEXT}` });
act(() => root.render(<Composer draft={draft()} />));
act(() => button("Switch to rich text").click());
act(() => root.render(<Composer draft={draft()} />));
expect(draft().html).toContain("Thanks, that helps.");
expect(draft().html).toContain("<b>this</b>");
// Once only: the typed reply must not arrive with the quote doubled.
expect(draft().html.match(/On Friday, Ann wrote:/g)).toHaveLength(1);
});
it("goes back to plain text with the prepared quote, not a re-flattened one", () => {
// A rich draft answering a plain-text message: the offer runs the other way.
act(() => {
useCompose.getState().update("d1", { format: "html", html: `<div>Thanks.</div>${QUOTE_HTML}`, formatOffer: "text" });
});
act(() => root.render(<Composer draft={draft()} />));
act(() => button("Switch to plain text").click());
act(() => root.render(<Composer draft={draft()} />));
expect(draft().format).toBe("text");
expect(draft().text).toContain("Thanks.");
// The prepared plain-text quote, not HTML run through a converter.
expect(draft().text.endsWith(QUOTE_TEXT)).toBe(true);
expect(draft().text).not.toContain("<blockquote>");
});
it("dismisses without changing the format", () => {
act(() => button("Dismiss").click());
act(() => root.render(<Composer draft={draft()} />));
expect(draft().format).toBe("text");
expect(bar()).toBeNull();
});
});
+74 -22
View File
@@ -1,6 +1,6 @@
import { lazy, Suspense, useEffect, useMemo, useState, type DragEvent, type ReactNode } from "react";
import { Link, useLocation } from "wouter";
import { AlertOctagon, Archive, ChevronDown, ChevronLeft, Clock, ChevronRight, File, Folder, FolderPlus, Inbox, Mail, MoreVertical, Palette, Send, Star, Tag, Trash2, Plus, Pencil, Eye, EyeOff, CheckCheck, Eraser, Share2, X, FolderInput } from "lucide-react";
import { AlertOctagon, Archive, ArrowDown, ArrowUp, ChevronDown, ChevronLeft, Clock, ChevronRight, File, Folder, FolderPlus, Inbox, Mail, MoreVertical, Palette, Send, Star, Tag, Trash2, Plus, Pencil, Eye, EyeOff, CheckCheck, Eraser, Share2, X, FolderInput } from "lucide-react";
import { useMail } from "@/store/mail";
import { canEmpty, confirmAndEmpty, emptyLabel } from "@/lib/mailbox/emptyFolder";
import { labelTree, visibleLabels } from "@/lib/mailbox/labelTree";
@@ -14,6 +14,7 @@ import { toast } from "@/ui/toast";
import { MailboxPicker } from "./MailboxPicker";
import { loadRaw, saveJson } from "@/lib/storage";
import { canDropFolder, canMoveFolderTo, folderColor, movable } from "@/lib/mailbox/folderMove";
import { canPlaceFolder, compareFolders, neighbour, placeFolder, type Placement } from "@/lib/mailbox/folderOrder";
import { haptic, useTouchRow } from "@/lib/input/touch";
import { plural, t } from "@/lib/i18n";
import { mailboxDisplayName } from "@/lib/mailbox/mailboxName";
@@ -78,8 +79,32 @@ export function MailboxTree() {
}
};
// Tree: AZ at every level (Inbox pinned to the top of the root), subfolders nested and
// collapsed by default. Expansion state is remembered per folder.
/** Whether the folder in flight may go just above or below this folder. */
const canPlace = (targetId: Id, placement: Placement): boolean => Boolean(draggingId) && canPlaceFolder(mailboxes, draggingId!, targetId, placement);
/** Put a folder just above or below another: a drag between rows, or Move up / Move down. */
const placeFolderAt = async (id: Id, targetId: Id, placement: Placement) => {
setDraggingId(null);
const updates = placeFolder(mailboxes, id, targetId, placement);
if (!updates) return;
try {
await useMail.getState().arrangeMailboxes(updates);
const parentId = updates[id]?.parentId;
if (parentId) {
const next = { ...expanded, [parentId]: true };
setExpanded(next);
saveJson("mbx-expanded", next);
}
} catch (err) {
toast.error(t("Could not move “{name}”: {reason}", { name: mailboxDisplayName(mailboxes[id]!), reason: (err as Error).message }));
}
};
const shown = (m: Mailbox) => showHidden || m.isSubscribed || m.role === "inbox";
// Tree: in `compareFolders` order at every level (Inbox, then any order the
// user has dragged into place, then the special folders, then AZ),
// subfolders nested and collapsed by default. Expansion state is remembered
// per folder.
const [expanded, setExpanded] = useState<Record<Id, boolean>>(() => loadRaw("mbx-expanded", {}));
const toggle = (id: Id) => {
const next = { ...expanded, [id]: !expanded[id] };
@@ -87,17 +112,13 @@ export function MailboxTree() {
saveJson("mbx-expanded", next);
};
const { rows, childrenOf, subtreeUnread } = useMemo(() => {
const all = Object.values(mailboxes).filter((m) => showHidden || m.isSubscribed || m.role === "inbox");
const all = Object.values(mailboxes).filter(shown);
const byParent = new Map<Id | null, Mailbox[]>();
for (const m of all) {
const p = m.parentId && mailboxes[m.parentId] ? m.parentId : null;
byParent.set(p, [...(byParent.get(p) ?? []), m]);
}
const cmp = (a: Mailbox, b: Mailbox) => {
if ((a.role === "inbox") !== (b.role === "inbox")) return a.role === "inbox" ? -1 : 1;
return a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true });
};
for (const list of byParent.values()) list.sort(cmp);
for (const list of byParent.values()) list.sort(compareFolders);
const out: Array<{ m: Mailbox; depth: number; hasChildren: boolean; open: boolean; hiddenUnread: number; childUnread: number }> = [];
const unreadBelow = (id: Id): number => (byParent.get(id) ?? []).reduce((n, c) => n + c.unreadEmails + unreadBelow(c.id), 0);
const walk = (parent: Id | null, depth: number) => {
@@ -207,6 +228,8 @@ export function MailboxTree() {
onFolderDragStart={() => {}}
onFolderDragEnd={() => {}}
onFolderDrop={() => {}}
canPlace={() => false}
onFolderPlace={() => {}}
/>
</>
)}
@@ -229,6 +252,8 @@ export function MailboxTree() {
onFolderDragStart={() => setDraggingId(m.id)}
onFolderDragEnd={() => { setDraggingId(null); setRootDrop(false); }}
onFolderDrop={(id) => void moveFolder(id, m.id)}
canPlace={(placement) => canPlace(m.id, placement) && !(placement === "after" && open && hasChildren)}
onFolderPlace={(id, placement) => void placeFolderAt(id, m.id, placement)}
/>
))}
{/* Labels are a flat list that belongs to the mailbox, not to whichever
@@ -260,7 +285,11 @@ export function MailboxTree() {
)}
</nav>
<Popover anchor={menu.anchor} onClose={menu.close} width={300}>
{menuTarget && <MailboxMenu mailbox={menuTarget} onClose={menu.close} onCreateChild={() => void createFolder(menuTarget.id)} onShare={() => setShareTarget(menuTarget)} onMove={() => { menu.close(); setMoveTarget(menuTarget); }} />}
{menuTarget && <MailboxMenu mailbox={menuTarget} onClose={menu.close} onCreateChild={() => void createFolder(menuTarget.id)} onShare={() => setShareTarget(menuTarget)} onMove={() => { menu.close(); setMoveTarget(menuTarget); }} onStep={(direction) => {
menu.close();
const to = neighbour(mailboxes, menuTarget.id, direction, shown);
if (to) void placeFolderAt(menuTarget.id, to.targetId, to.placement);
}} canStep={(direction) => Boolean(neighbour(mailboxes, menuTarget.id, direction, shown))} />}
</Popover>
{moveTarget && (
<MailboxPicker
@@ -280,8 +309,9 @@ export function MailboxTree() {
);
}
function FolderRow({ mailbox: m, label, depth, hasChildren, open, hiddenUnread, childUnread, onToggle, onDrillIn, currentId, onMenu, dragging, acceptsFolder, onFolderDragStart, onFolderDragEnd, onFolderDrop }: { mailbox: Mailbox; label: string; depth: number; hasChildren: boolean; open: boolean; hiddenUnread: number; childUnread: number; onToggle: () => void; onDrillIn?: () => void; currentId?: string; onMenu: (m: Mailbox, e: { currentTarget: Element }) => void; dragging: boolean; acceptsFolder: boolean; onFolderDragStart: () => void; onFolderDragEnd: () => void; onFolderDrop: (id: Id) => void }) {
const [dropping, setDropping] = useState(false);
function FolderRow({ mailbox: m, label, depth, hasChildren, open, hiddenUnread, childUnread, onToggle, onDrillIn, currentId, onMenu, dragging, acceptsFolder, onFolderDragStart, onFolderDragEnd, onFolderDrop, canPlace, onFolderPlace }: { mailbox: Mailbox; label: string; depth: number; hasChildren: boolean; open: boolean; hiddenUnread: number; childUnread: number; onToggle: () => void; onDrillIn?: () => void; currentId?: string; onMenu: (m: Mailbox, e: { currentTarget: Element }) => void; dragging: boolean; acceptsFolder: boolean; onFolderDragStart: () => void; onFolderDragEnd: () => void; onFolderDrop: (id: Id) => void; canPlace: (placement: Placement) => boolean; onFolderPlace: (id: Id, placement: Placement) => void }) {
/** Where a drop here would land: in this folder, or just above or below it. */
const [drop, setDrop] = useState<"into" | Placement | null>(null);
/** Expanding in place and drilling in are the same relationship; only one shows. */
const twisty = hasChildren && !onDrillIn;
// Scheduled counts like Drafts: everything in it is already read, so the
@@ -297,19 +327,37 @@ function FolderRow({ mailbox: m, label, depth, hasChildren, open, hiddenUnread,
// Subscribed, not read once: picking a color has to repaint the row.
const tint = useSettings((s) => folderColor(s.settings.folderColors, m.id));
/*
* A folder dropped on the top or bottom quarter of a row goes above or below
* it; anywhere else, into it. Where "into" isn't allowed -- a special folder,
* which can be reordered but never nested -- the whole row reorders, by
* whichever half the pointer is in.
*/
const folderZone = (e: DragEvent): "into" | Placement | null => {
const r = e.currentTarget.getBoundingClientRect();
const y = e.clientY - r.top;
const edge = r.height / 4;
const zone = y < edge ? "before" : y > r.height - edge ? "after" : "into";
if (zone !== "into" && canPlace(zone)) return zone;
if (acceptsFolder) return "into";
const half = y < r.height / 2 ? "before" : "after";
return canPlace(half) ? half : null;
};
const onDragOver = (e: DragEvent) => {
const folder = e.dataTransfer.types.includes(FOLDER_MIME);
if (folder ? !acceptsFolder : !e.dataTransfer.types.includes("application/x-ihasmail-emails")) return;
const zone = e.dataTransfer.types.includes(FOLDER_MIME) ? folderZone(e) : e.dataTransfer.types.includes("application/x-ihasmail-emails") ? "into" : null;
if (!zone) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
if (!dropping) setDropping(true);
if (drop !== zone) setDrop(zone);
};
const onDrop = (e: DragEvent) => {
e.preventDefault();
setDropping(false);
setDrop(null);
const folderId = e.dataTransfer.getData(FOLDER_MIME);
if (folderId) {
if (acceptsFolder) onFolderDrop(folderId);
const zone = folderZone(e);
if (zone === "into") onFolderDrop(folderId);
else if (zone) onFolderPlace(folderId, zone);
return;
}
const raw = e.dataTransfer.getData("application/x-ihasmail-emails");
@@ -348,16 +396,17 @@ function FolderRow({ mailbox: m, label, depth, hasChildren, open, hiddenUnread,
return (
<Link
href={`/mail/${m.id}`}
className={`nav-item folder-row depth-${Math.min(depth, 4)} ${currentId === m.id ? "active" : ""} ${unread ? "unread" : ""} ${dropping ? "drop-target" : ""} ${dragging ? "dragging" : ""}`}
className={`nav-item folder-row depth-${Math.min(depth, 4)} ${currentId === m.id ? "active" : ""} ${unread ? "unread" : ""} ${drop === "into" ? "drop-target" : drop ? `drop-${drop}` : ""} ${dragging ? "dragging" : ""}`}
title={label}
{...press}
// Dragging a folder is a mouse gesture; on a touchscreen the browser
// starts it from the same long press that now opens the menu.
draggable={movable(m) && !isTouch}
// starts it from the same long press that now opens the menu. Special
// folders drag too, to be reordered; only Inbox, always first, stays put.
draggable={m.role !== "inbox" && !isTouch}
onDragStart={onDragStart}
onDragEnd={onFolderDragEnd}
onDragOver={onDragOver}
onDragLeave={() => setDropping(false)}
onDragLeave={() => setDrop(null)}
onDrop={onDrop}
onContextMenu={(e) => {
e.preventDefault();
@@ -424,7 +473,7 @@ function FolderRow({ mailbox: m, label, depth, hasChildren, open, hiddenUnread,
);
}
function MailboxMenu({ mailbox: m, onClose, onCreateChild, onShare, onMove }: { mailbox: Mailbox; onClose: () => void; onCreateChild: () => void; onShare: () => void; onMove: () => void }) {
function MailboxMenu({ mailbox: m, onClose, onCreateChild, onShare, onMove, onStep, canStep }: { mailbox: Mailbox; onClose: () => void; onCreateChild: () => void; onShare: () => void; onMove: () => void; onStep: (direction: "up" | "down") => void; canStep: (direction: "up" | "down") => boolean }) {
const shared = Object.keys(m.shareWith ?? {}).length > 0;
const [, navigate] = useLocation();
const colors = useSettings((s) => s.settings.folderColors);
@@ -490,6 +539,9 @@ function MailboxMenu({ mailbox: m, onClose, onCreateChild, onShare, onMove }: {
<MenuItem icon={<FolderPlus size={16} />} label={t("New subfolder")} onClick={onCreateChild} disabled={!m.myRights.mayCreateChild} />
<MenuItem icon={<Pencil size={16} />} label={t("Rename")} onClick={() => void rename()} disabled={isSpecial || !m.myRights.mayRename} />
<MenuItem icon={<FolderInput size={16} />} label={t("Move to…")} onClick={onMove} disabled={!movable(m) || !m.myRights.mayRename} />
{/* The way to reorder without a drag: from the keyboard, and on touch. */}
<MenuItem icon={<ArrowUp size={16} />} label={t("Move up")} onClick={() => onStep("up")} disabled={!canStep("up")} />
<MenuItem icon={<ArrowDown size={16} />} label={t("Move down")} onClick={() => onStep("down")} disabled={!canStep("down")} />
<MenuItem icon={m.isSubscribed ? <EyeOff size={16} /> : <Eye size={16} />} label={m.isSubscribed ? t("Hide from list") : t("Show in list")} onClick={() => void useMail.getState().updateMailbox(m.id, { isSubscribed: !m.isSubscribed })} disabled={m.role === "inbox"} />
{/* Sharing a mail folder is withdrawn, not removed: Stalwart accepts and
stores the share, and it never reaches the other account -- its own
+8 -2
View File
@@ -17,6 +17,7 @@ import { internalDomains, isExternalSender, linkVerdict } from "@/lib/warnings";
import { spamReport, type SpamReport } from "@/lib/spamScore";
import { formatFullDate, formatListDate, formatSize } from "@/lib/format";
import { displayName, domainOf, formatAddress } from "@/lib/address";
import { remoteImagesAllowed } from "@/lib/mail/remoteImages";
import { EMAIL_BASE_CSS, TEXT_EMAIL_CSS, hasHtmlAlternative, htmlDeclaresColors, markKeptSurfaces, sanitizeEmailHtml } from "@/lib/text/html";
import { openableInTab, previewKind } from "@/lib/preview";
// Loaded when first opened: it is not needed to show mail, and it is not small.
@@ -118,7 +119,12 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
/* Stable, so the body's click handler keeps its identity between renders.
Passing an inline arrow here is what made the handler change on every
render in the first place. */
const showImages = useCallback(() => setAllowRemote(true), []);
const showImages = useCallback(() => {
setAllowRemote(true);
// Recorded for the composer: a reply quotes this message and must not
// fetch what the reader has not agreed to (#410).
useMail.getState().showImages(e.id);
}, [e.id]);
const [filterOpen, setFilterOpen] = useState(false);
const moreMenu = useMenu();
const [, navigate] = useLocation();
@@ -128,7 +134,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
const from = e.from?.[0];
const senderTrusted = settings.trustedImageSenders.includes((from?.email ?? "").toLowerCase());
const inContacts = useContacts((s) => Boolean(from && s.loaded && s.lookupByEmail(from.email)));
const remoteAllowed = allowRemote || settings.imagePolicy === "always" || senderTrusted || (settings.imagePolicy === "contacts" && inContacts);
const remoteAllowed = remoteImagesAllowed({ from: from?.email, policy: settings.imagePolicy, trusted: settings.trustedImageSenders, inContacts, shown: allowRemote });
const imageProxy = useSession((s) => s.session?.ihasmail?.imageProxy ?? true);
const scheduled = useScheduled((s) => s.pending[e.id]);
const receipt = useMemo(() => mdnDecision(e), [e]);
+7 -4
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import { currentAppName, useAppName } from "@/lib/brand";
import { BadgeCheck, ShieldAlert, ShieldQuestion, ShieldX } from "lucide-react";
import { formatFingerprint } from "@/lib/smime/x509";
import type { SignatureState } from "@/lib/smime/useSignature";
@@ -22,6 +23,7 @@ import { formatFullDate } from "@/lib/format";
* verified against itself.
*/
export function SignatureBanner({ state }: { state: SignatureState }) {
const appName = useAppName();
const [open, setOpen] = useState(false);
if (state.status !== "done") return null;
const { crypto, trust, previous, warnings } = state.report;
@@ -31,7 +33,7 @@ export function SignatureBanner({ state }: { state: SignatureState }) {
return (
<Banner tone="quiet" icon={<ShieldQuestion size={16} />}>
<span className="grow">
{t("This message is signed, and ihasmail could not check the signature.")} {explain(crypto.reason)}
{t("This message is signed, and {app} could not check the signature.", { app: appName })} {explain(crypto.reason)}
{crypto.detail && <span className="hint"> {crypto.detail}</span>}
</span>
</Banner>
@@ -77,7 +79,7 @@ export function SignatureBanner({ state }: { state: SignatureState }) {
) : (
<>
{tNode("Signed by {name}, seen here for the first time.", { name: <strong className="notranslate" translate="no">{name}</strong> })}{" "}
{t("ihasmail will tell you if a later message from this address is signed by anybody else.")}
{t("{app} will tell you if a later message from this address is signed by anybody else.", { app: appName })}
</>
)}
{warnings.includes("certificate-expired") && <> {t("The certificate has expired.")}</>}
@@ -134,11 +136,12 @@ export function SignatureBanner({ state }: { state: SignatureState }) {
/** The sayable version of why a check did not happen, or did not hold. */
function explain(reason: Reason): string {
const appName = currentAppName();
switch (reason) {
case "openpgp":
return t("It is signed with OpenPGP, and ihasmail has no way to fetch the sender's public key.");
return t("It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.", { app: appName });
case "rsa-pss":
return t("It uses a signature algorithm ihasmail cannot check yet.");
return t("It uses a signature algorithm {app} cannot check yet.", { app: appName });
case "no-certificate":
return t("The signature carries no certificate that can be read.");
case "not-signed-properly":
@@ -0,0 +1,112 @@
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MailboxTree } from "../MailboxTree";
import { useMail } from "@/store/mail";
import { useSettings } from "@/store/settings";
import type { Mailbox, MailboxRole } from "@/jmap/types";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
/**
* Reordering folders in the sidebar (#402), driven through the real tree.
*
* The placement arithmetic has its own tests in lib/mailbox; these are about
* the row deciding where a drop lands from where the pointer is, which only
* the component knows.
*/
window.matchMedia = ((q: string) => ({ matches: false, media: q, addEventListener() {}, removeEventListener() {} })) as unknown as typeof window.matchMedia;
const rights = { mayReadItems: true, mayAddItems: true, mayRemoveItems: true, maySetSeen: true, maySetKeywords: true, mayCreateChild: true, mayRename: true, mayDelete: true, maySubmit: true };
const box = (id: string, name: string, parentId: string | null, role: MailboxRole = null): Mailbox => ({
id, name, parentId, role, sortOrder: 0, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0, myRights: rights, isSubscribed: true,
});
const MAILBOXES = {
zeta: box("zeta", "Zeta", null),
trash: box("trash", "Deleted Items", null, "trash"),
sent: box("sent", "Sent", null, "sent"),
inbox: box("inbox", "Inbox", null, "inbox"),
alpha: box("alpha", "Alpha", null),
drafts: box("drafts", "Drafts", null, "drafts"),
};
/** jsdom has no DataTransfer; this is the part of one the tree touches. */
function transfer() {
const data: Record<string, string> = {};
return {
get types() { return Object.keys(data); },
setData: (k: string, v: string) => { data[k] = v; },
getData: (k: string) => data[k] ?? "",
effectAllowed: "", dropEffect: "",
};
}
function fire(el: Element, type: string, dataTransfer: ReturnType<typeof transfer>, clientY = 0) {
const e = new Event(type, { bubbles: true, cancelable: true });
Object.assign(e, { dataTransfer, clientY });
act(() => { el.dispatchEvent(e); });
}
describe("reordering folders in the tree", () => {
let host: HTMLDivElement;
let root: Root;
const arrange = vi.fn(async (_updates: Record<string, Partial<Mailbox>>) => {});
const updateMailbox = vi.fn(async (_id: string, _patch: Partial<Mailbox>) => {});
const rows = () => Array.from(document.querySelectorAll(".nav-item.folder-row")).map((r) => r.querySelector(".nav-label")?.textContent);
const rowFor = (name: string) => Array.from(document.querySelectorAll<HTMLElement>(".nav-item.folder-row")).find((r) => r.querySelector(".nav-label")?.textContent === name)!;
/** Drag `from` over `to` at a fraction of its height, drop, and say what the row showed. */
function drag(from: string, to: string, frac: number) {
const dt = transfer();
const target = rowFor(to);
// Every row is 36px tall, from 100px down the page.
target.getBoundingClientRect = () => ({ top: 100, height: 36, bottom: 136, left: 0, right: 200, width: 200, x: 0, y: 100, toJSON() {} });
fire(rowFor(from), "dragstart", dt);
fire(target, "dragover", dt, 100 + 36 * frac);
const shown = /drop-(before|after|target)/.exec(target.className)?.[1] ?? null;
fire(target, "drop", dt, 100 + 36 * frac);
return shown;
}
beforeEach(() => {
arrange.mockClear();
updateMailbox.mockClear();
window.history.replaceState({}, "", "/mail/inbox");
useMail.setState({ mailboxes: MAILBOXES, mailboxesLoaded: true, arrangeMailboxes: arrange, updateMailbox });
useSettings.setState((s) => ({ settings: { ...s.settings, showHiddenFolders: false, labelsSidebar: false } }));
host = document.createElement("div");
document.body.appendChild(host);
root = createRoot(host);
act(() => root.render(<MailboxTree />));
});
afterEach(() => { act(() => root.unmount()); host.remove(); });
it("lists special folders under Inbox before the rest, until something is dragged", () => {
expect(rows()).toEqual(["Inbox", "Drafts", "Sent", "Deleted Items", "Alpha", "Zeta"]);
});
it("puts a folder above the row when it's dropped on the row's top edge", () => {
expect(drag("Zeta", "Drafts", 0.1)).toBe("before");
expect(arrange).toHaveBeenCalledWith({
zeta: { sortOrder: 20 }, drafts: { sortOrder: 30 }, sent: { sortOrder: 40 }, trash: { sortOrder: 50 }, alpha: { sortOrder: 60 }, inbox: { sortOrder: 10 },
});
});
it("nests a folder dropped on the middle of an ordinary folder, as before", () => {
expect(drag("Zeta", "Alpha", 0.5)).toBe("target");
expect(updateMailbox).toHaveBeenCalledWith("zeta", { parentId: "alpha" });
expect(arrange).not.toHaveBeenCalled();
});
it("reorders a special folder by the nearer half, since it can't be nested", () => {
expect(drag("Deleted Items", "Drafts", 0.4)).toBe("before");
expect(Object.keys(arrange.mock.calls[0]![0])).toContain("trash");
});
it("drops nothing above Inbox", () => {
expect(drag("Sent", "Inbox", 0.1)).toBeNull();
expect(arrange).not.toHaveBeenCalled();
});
});
+6 -4
View File
@@ -1,4 +1,5 @@
import { useSession } from "@/store/session";
import { useAppName } from "@/lib/brand";
import { client } from "@/jmap/client";
import { DEFAULT_SOURCE_URL } from "@/lib/source";
import { APP_VERSION } from "@/lib/version";
@@ -6,16 +7,17 @@ import { withBase } from "@/lib/basePath";
import { t, tNode } from "@/lib/i18n";
export function AboutSettings() {
const appName = useAppName();
const session = useSession((s) => s.session);
const caps = Object.keys(session?.capabilities ?? {});
// A deployment running modified code should offer its own source, not ours.
const sourceUrl = session?.ihasmail?.sourceUrl ?? DEFAULT_SOURCE_URL;
return (
<div>
<h1>{t("About ihasmail")}</h1>
<h1>{t("About {app}", { app: appName })}</h1>
<p className="lead">{tNode("A fast, friendly, open-source webmail for {server}, built on JMAP.", { server: <a href="https://stalw.art" target="_blank" rel="noreferrer">{t("Stalwart Mail Server")}</a> })}</p>
<div className="row" style={{ gap: 16, alignItems: "center", marginBottom: 16 }}>
<img src={withBase("/img/logo.png")} alt={t("ihasmail")} width={96} />
<img src={withBase("/img/logo.png")} alt={appName} width={96} />
<div>
{/* A product name and a version string: neither is a word to translate. */}
<div style={{ fontWeight: 700, fontSize: "1.2em" }} className="notranslate" translate="no">ihasmail v{APP_VERSION}</div>
@@ -32,8 +34,8 @@ export function AboutSettings() {
<tr><td>{t("Image privacy proxy")}</td><td>{session?.ihasmail?.imageProxy ? t("enabled") : t("disabled")}</td></tr>
</tbody>
</table>
<p className="hint" style={{ marginTop: 6 }}>{t("Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.")}</p>
<p className="hint">{tNode("ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.", { example: <strong className="notranslate" translate="no">v2026.8.30+pr129</strong>, sha: <code>+g1fa6578</code> })}</p>
<p className="hint" style={{ marginTop: 6 }}>{t("Stalwart does not publish its version number to mail clients, so {app} reports the edition where the server gives one. {app} requires 0.16 or newer, and sign-in refuses anything older.", { app: appName })}</p>
<p className="hint">{tNode("{app}'s own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.", { example: <strong className="notranslate" translate="no">v2026.8.30+pr129</strong>, sha: <code>+g1fa6578</code> }, { app: appName })}</p>
<h2>{t("Server capabilities")}</h2>
<div className="row wrap gap-4">
{caps.map((c) => <span key={c} className="chip mono" style={{ fontSize: ".78em" }}>{c.replace("urn:ietf:params:jmap:", "")}</span>)}
@@ -1,4 +1,5 @@
import { useSettings } from "@/store/settings";
import { useAppName } from "@/lib/brand";
import { PALETTES, effectiveMode, type Mode, type PaletteId } from "@/lib/palette";
import { Switch, useIsTouch } from "@/ui/misc";
import { SWIPE_CHOICES, type SwipeAction } from "@/lib/input/swipe";
@@ -75,6 +76,7 @@ const ACCENTS = [
];
export function AppearanceSettings() {
const appName = useAppName();
const s = useSettings((st) => st.settings);
const update = useSettings((st) => st.update);
const prefersDark = Boolean(window.matchMedia?.("(prefers-color-scheme: dark)").matches);
@@ -84,7 +86,7 @@ export function AppearanceSettings() {
return (
<div>
<h1>{translate("Appearance")}</h1>
<p className="lead">{translate("Make ihasmail yours.")}</p>
<p className="lead">{translate("Make {app} yours.", { app: appName })}</p>
<h2>{translate("Theme")}</h2>
<div className="mode-switch" role="group" aria-label={translate("Light or dark")}>
{MODES.map((m) => (
@@ -178,7 +180,7 @@ export function AppearanceSettings() {
</p>
)}
<p className="hint">
{translate("Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them — a language offered without strings behind it would leave the page claiming to be in a language it is not.")}
{translate("Only languages {app} has been translated into appear here, so this list grows as translations land rather than ahead of them — a language offered without strings behind it would leave the page claiming to be in a language it is not.", { app: appName })}
</p>
<p className="hint">
+7 -4
View File
@@ -1,4 +1,5 @@
import { useSettings } from "@/store/settings";
import { useAppName } from "@/lib/brand";
import { Switch } from "@/ui/misc";
import { browserTimeZone, listTimeZones } from "@/lib/dates";
import { toast } from "@/ui/toast";
@@ -183,6 +184,7 @@ export function GeneralSettings() {
<Switch locked={isEnforced("includeQuote")} checked={s.includeQuote} onChange={(v) => update({ includeQuote: v })} label={t("Quote original message in replies")} />
<Switch locked={isEnforced("signatureAboveQuote")} checked={s.signatureAboveQuote} onChange={(v) => update({ signatureAboveQuote: v })} label={t("Place signature above quoted text")} />
<Switch locked={isEnforced("spellcheck")} checked={s.spellcheck} onChange={(v) => update({ spellcheck: v })} label={t("Spell check while typing")} />
<Switch locked={isEnforced("composeMaximized")} checked={s.composeMaximized} onChange={(v) => update({ composeMaximized: v })} label={t("Open the composer full screen")} />
<h2>{t("Locale")}</h2>
<div className="field-row">
@@ -254,6 +256,7 @@ export function GeneralSettings() {
* it can and points at the browser's own settings for the rest.
*/
function MailHandlerSettings() {
const appName = useAppName();
const support = mailtoHandlerSupport();
const [requested, setRequested] = useState(mailtoHandlerRequested);
@@ -261,7 +264,7 @@ function MailHandlerSettings() {
try {
registerMailtoHandler();
setRequested(true);
toast.success(t("Your browser will ask whether to open mail links in ihasmail"));
toast.success(t("Your browser will ask whether to open mail links in {app}", { app: appName }));
} catch (err) {
toast.error(t("Your browser refused the request: {error}", { error: (err as Error).message }));
}
@@ -274,7 +277,7 @@ function MailHandlerSettings() {
};
if (support === "unsupported") {
return <p className="hint">{tNode("This browser cannot register apps for {scheme} links. Safari, in particular, has no such API — you can still make ihasmail the default from your operating system if you install it as an app.", { scheme: <code>mailto:</code> })}</p>;
return <p className="hint">{tNode("This browser cannot register apps for {scheme} links. Safari, in particular, has no such API — you can still make {app} the default from your operating system if you install it as an app.", { scheme: <code>mailto:</code> }, { app: appName })}</p>;
}
if (support === "insecure") {
return <p className="hint">{tNode("Registering for {scheme} links requires a secure (HTTPS) connection.", { scheme: <code>mailto:</code> })}</p>;
@@ -283,7 +286,7 @@ function MailHandlerSettings() {
return (
<>
<p className="hint">
{tNode("Open {scheme} links — in web pages, documents and other apps — in ihasmail instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).", { scheme: <code>mailto:</code> })}
{tNode("Open {scheme} links — in web pages, documents and other apps — in {app} instead of a desktop mail client. Your browser will ask you to confirm, and you can change it later in its own settings (Chrome: Settings Privacy and security Site settings Protocol handlers; Firefox: Settings General Applications).", { scheme: <code>mailto:</code> }, { app: appName })}
</p>
<div className="row wrap">
<button className="btn btn-primary" onClick={ask}>{requested ? "Ask again" : "Make ihasmail the default mail app"}</button>
@@ -293,7 +296,7 @@ function MailHandlerSettings() {
{!isInstalledApp() && (
<p className="hint mt-8">
{t("For a system-wide default, install ihasmail as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer ihasmail directly wherever it asks which mail app to use.")}
{t("For a system-wide default, install {app} as an app first (in Chrome: the install icon in the address bar). Your operating system can then offer {app} directly wherever it asks which mail app to use.", { app: appName })}
</p>
)}
</>
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { useAppName } from "@/lib/brand";
import { Plus, Trash2, Star, Eye, EyeOff } from "lucide-react";
import { useSettings } from "@/store/settings";
import { useMail } from "@/store/mail";
@@ -75,6 +76,7 @@ export function IdentitiesSettings() {
}
function IdentityDialog({ identity, onClose }: { identity: Partial<Identity>; onClose: () => void }) {
const appName = useAppName();
const [name, setName] = useState(identity.name ?? "");
const [email, setEmail] = useState(identity.email ?? "");
const [replyTo, setReplyTo] = useState(formatAddressList(identity.replyTo));
@@ -129,7 +131,7 @@ function IdentityDialog({ identity, onClose }: { identity: Partial<Identity>; on
<span className="hint">{t("Images are stored in your Files (folder “ihasmail”) and embedded when you send.")}</span>
<span className="hint nowrap" style={tooLong ? { color: "var(--warn)", fontWeight: 600 } : undefined}>{sigLen.toLocaleString()} / {SIGNATURE_LIMIT.toLocaleString()}</span>
</div>
{tooLong && <div className="warn-box mt-8">{t("This signature is larger than the server's {limit}-byte limit. ihasmail will keep the full version in your Files and store a short text fallback on the server — other mail clients will see the plain-text version.", { limit: SIGNATURE_LIMIT })}</div>}
{tooLong && <div className="warn-box mt-8">{t("This signature is larger than the server's {limit}-byte limit. {app} will keep the full version in your Files and store a short text fallback on the server — other mail clients will see the plain-text version.", { limit: SIGNATURE_LIMIT, app: appName })}</div>}
</div>
</Dialog>
);
+3 -1
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useAppName } from "@/lib/brand";
import { Plus, Trash2 } from "lucide-react";
import { useSettings, type LabelVisibility } from "@/store/settings";
import { labelTree, descendantKeywords } from "@/lib/mailbox/labelTree";
@@ -8,6 +9,7 @@ import { promptDialog } from "@/ui/dialog";
import { t, tNode } from "@/lib/i18n";
export function LabelsSettings() {
const appName = useAppName();
const labels = useSettings((s) => s.settings.labels);
const update = useSettings((s) => s.update);
const [editing, setEditing] = useState<string | null>(null);
@@ -25,7 +27,7 @@ export function LabelsSettings() {
return (
<div>
<h1>{t("Labels")}</h1>
<p className="lead">{t("Labels are IMAP keywords stored on your messages, so every other client sees them. Names, colors and nesting are ihasmail\u2019s own and follow your account. Nesting is display only \u2014 it rewrites nothing in the mailbox.")}</p>
<p className="lead">{t("Labels are IMAP keywords stored on your messages, so every other client sees them. Names, colors and nesting are {app}\u2019s own and follow your account. Nesting is display only \u2014 it rewrites nothing in the mailbox.", { app: appName })}</p>
{labels.map((l) => (
<div key={l.keyword} className="card">
<div className="card-head">
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useAppName } from "@/lib/brand";
import { useSettings } from "@/store/settings";
import { Switch } from "@/ui/misc";
import { requestNotificationPermission, showNotification, playNewMailSound } from "@/lib/notify/notify";
@@ -10,6 +11,7 @@ import { t } from "@/lib/i18n";
import { isEnforced } from "@/lib/settingsPolicy";
export function NotificationsSettings() {
const appName = useAppName();
const s = useSettings((st) => st.settings);
const update = useSettings((st) => st.update);
const pushConnected = useSession((st) => st.pushConnected);
@@ -37,7 +39,7 @@ export function NotificationsSettings() {
}
update({ desktopNotifications: v });
}}
label={t("Desktop notifications while ihasmail is open")}
label={t("Desktop notifications while {app} is open", { app: appName })}
hint={perm === "denied" ? t("Notifications are blocked in your browser settings.") : perm === "unsupported" ? t("Not supported in this browser.") : t("Shows a system notification when new mail arrives in your Inbox while the tab is in the background.")}
disabled={perm === "denied" || perm === "unsupported"}
/>
@@ -68,18 +70,18 @@ export function NotificationsSettings() {
setBusy(false);
}
}}
label={t("Notify me even when ihasmail is closed")}
label={t("Notify me even when {app} is closed", { app: appName })}
hint={
!canBackground
? t("Needs a browser with the Push API and a mail server that publishes a push key.")
: supportsEmailPush()
? t("Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running — if you quit it completely, notifications wait and arrive when you open it again.")
? t("Your mail server delivers these straight to your browser, so they arrive with no {app} tab open, naming the sender and subject. Your browser still has to be running — if you quit it completely, notifications wait and arrive when you open it again.", { app: appName })
: t("Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running.")
}
/>
<Switch locked={isEnforced("notificationSound")} checked={s.notificationSound} onChange={(v) => update({ notificationSound: v })} label={t("Play a sound for new mail")} />
<div className="row mt-16">
<button className="btn" onClick={() => { showNotification(t("ihasmail test"), { body: t("This is what a new-mail notification looks like.") }); playNewMailSound(); }}>{t("Test notification")}</button>
<button className="btn" onClick={() => { showNotification(t("{app} test", { app: appName }), { body: t("This is what a new-mail notification looks like.") }); playNewMailSound(); }}>{t("Test notification")}</button>
</div>
<p className="hint mt-8">{t("The tab title and favicon always show your unread Inbox count.")}</p>
</div>
+3 -1
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useAppName } from "@/lib/brand";
import { useSettings, type ReadReceiptPolicy } from "@/store/settings";
import { useMail } from "@/store/mail";
import { domainOf } from "@/lib/address";
@@ -23,6 +24,7 @@ import { isEnforced } from "@/lib/settingsPolicy";
* behaves toward the reader and toward senders.
*/
export function PrivacySettings() {
const appName = useAppName();
const s = useSettings((st) => st.settings);
const update = useSettings((st) => st.update);
const trusted = s.trustedImageSenders;
@@ -43,7 +45,7 @@ export function PrivacySettings() {
<option value="always">{t("Always show")}</option>
</select>
<p className="hint">
{t("An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by ihasmail's own server rather than the browser, so the sender learns none of those.")}
{t("An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.", { app: appName })}
</p>
</div>
{trusted.length > 0 && (
+3 -1
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { useAppName } from "@/lib/brand";
import { Copy, KeyRound, ShieldCheck, Smartphone } from "lucide-react";
import { apiFetch, ApiError } from "@/jmap/client";
import { useSession } from "@/store/session";
@@ -177,6 +178,7 @@ function PasswordForm({ otpEnabled, onChanged }: { otpEnabled: boolean; onChange
* stay whoever is already enrolled needs a way back.
*/
function TwoFactorOff({ reload }: { reload: () => Promise<void> }) {
const appName = useAppName();
const [code, setCode] = useState("");
const [password, setPassword] = useState("");
const [busy, setBusy] = useState(false);
@@ -200,7 +202,7 @@ function TwoFactorOff({ reload }: { reload: () => Promise<void> }) {
<div>
<p className="hint" style={{ marginBottom: 12 }}>
{t("This account has two-factor authentication on. ihasmail can't sign you in with a code yet, so signing in on another device needs an app password — or you can turn two-factor authentication off here.")}
{t("This account has two-factor authentication on. {app} can't sign you in with a code yet, so signing in on another device needs an app password — or you can turn two-factor authentication off here.", { app: appName })}
</p>
<div className="row" style={{ alignItems: "center", gap: 10 }}>
<ShieldCheck size={18} />