German, generated by AI and marked Beta until somebody signs it off

The first language, and the first one where the honest thing to say is not
flattering: no native speaker has read it. That is stated in the app rather
than in a commit nobody reads, because it is the fact a reader needs to judge
what they are looking at. Somebody told a translation is unchecked forgives an
odd sentence and reports it; somebody told it was reviewed reasonably concludes
the product is sloppy. The setting carries a link straight to a report, which
is the whole review process here.

`beta` is a property of the language, not of the catalogue's completeness. A
file can be word-for-word finished and still read like a machine wrote it, and
that is what the flag marks. Removing it is a person's decision.

Register is "Sie", consistently, and written down in the file so the next
language and the next contributor inherit the decision rather than re-take it.
Thunderbird and Outlook use it; ihasmail is as often a company's mail as
somebody's own, where "du" from software the workplace deployed reads as
presumptuous. Where a string can dodge the question it does, which is ordinary
good German UI. The glossary at the top of the file fixes the vocabulary once
-- Posteingang, Papierkorb, Entwürfe, archivieren -- because inconsistency
reads as amateur far more than an imperfect word choice does. "Label" and
"Spam" stay English, since translating them would name things no German mail
client calls that.

766 of 781 strings. The fifteen left are product names, bare URLs and example
addresses, which should stay English and now do.

Two things this turned up that the earlier work had hidden:

Labels defined as module-level constants -- the entire settings navigation,
the theme cards, the swipe choices, the date formats, the sharing permissions
-- are evaluated once, before any catalogue loads, so they could only ever be
English. Nothing failed; the German build simply had an English sidebar. They
are translated where they render now, which keeps the constant as data and
makes its English text the key.

And the codemod's narrowed rule, which let it take 73 more strings last time,
was too broad after all: text stranded after an inline <a> or <strong> came
through as sentence fragments -- ", and what a new account starts on." Eight
of them, rebuilt with tNode so the sentence stays whole and the element is a
named hole a translator can move.

scripts/i18n-catalog-check.mjs is new and earned itself immediately: it found
three keys invented that the code never asks for, which is the silent failure
in a catalogue -- a translation that looks right, is never looked up, and
renders English for ever. It also had to be taught about t(variable), because
it cried wolf 33 times over the constants above, and a check that cries wolf
gets switched off.

Verified in the browser rather than only in tests, which is where the settings
sidebar being English was visible and nowhere else.
This commit is contained in:
2026-08-31 11:06:54 -07:00
parent d0cbfc7870
commit 87383440bb
19 changed files with 1058 additions and 33 deletions
+10 -2
View File
@@ -20,12 +20,20 @@ describe("resolveUiLanguage", () => {
it("refuses a language whose strings are not shipped", () => {
// The account travels between machines and can outlive a catalogue. A
// page that says lang="de" while rendering English is worse than one that
// page that says lang="fr" while rendering English is worse than one that
// admits to English: it stops the reader translating it themselves.
expect(resolveUiLanguage("de")).toBe("en");
expect(resolveUiLanguage("fr")).toBe("en");
expect(resolveUiLanguage("xx-XX")).toBe("en");
});
it("carries the Beta flag until a person has signed the language off", () => {
// Not a completeness measure. A catalogue can be word-for-word finished
// and still read like a machine wrote it, which is what this marks.
const de = UI_LANGUAGES.find((l) => l.tag === "de");
expect(de?.beta).toBe(true);
expect(UI_LANGUAGES.find((l) => l.tag === "en")?.beta).toBeUndefined();
});
it("honours one that is", () => {
for (const l of UI_LANGUAGES) expect(resolveUiLanguage(l.tag)).toBe(l.tag);
});
+14
View File
@@ -23,12 +23,26 @@ export interface UiLanguage {
tag: string;
/** The language's name in that language, which is how a picker should read. */
name: string;
/**
* Machine-translated and not yet checked by somebody who speaks it.
*
* Stays true until a native speaker has actually read the catalogue and said
* so. It is not a measure of how complete the file is -- a catalogue can be
* word-for-word finished and still read like a machine wrote it, which is
* the thing this flag is about. Removing it is a deliberate act by a person,
* not something a coverage number earns.
*/
beta?: boolean;
}
export const UI_LANGUAGES: readonly UiLanguage[] = [
{ tag: "en", name: "English" },
{ tag: "de", name: "Deutsch", beta: true },
];
/** Where to report a bad translation. Beta languages depend on it. */
export const TRANSLATION_ISSUE_URL = "https://github.com/Coffey-Labs/ihasmail/issues/new?title=Translation%3A%20";
export const DEFAULT_UI_LANGUAGE = "en";
/**
+860
View File
@@ -0,0 +1,860 @@
import type { Catalog } from "@/lib/i18n";
/**
* German — generated by AI, and not reviewed by a native speaker.
*
* That is stated in the app as well as here, because it is the fact a reader
* needs in order to judge what they are looking at. Somebody told the
* translation is unchecked forgives an odd sentence and reports it; somebody
* told it was reviewed reasonably concludes the product is sloppy. The report
* link in Settings is the entire review process, and it only works if the app
* is honest about needing one.
*
* It was written against the standard terminology every German mail client
* already uses rather than invented, and the glossary below is the part that
* makes it read as one voice. But "written carefully" and "correct" are
* different claims and only the first is being made. The language stays marked
* Beta until a person who speaks German has read it and said otherwise --
* which is a decision somebody makes, not one a completeness percentage
* earns.
*
* A string missing from here renders its English source, so this file can be
* incomplete or wrong in places without the app breaking. Deleting an entry is
* a valid fix.
*
* ── Decisions this file is consistent about ──────────────────────────────
*
* Register: **Sie**, throughout. Thunderbird and Outlook use it, and ihasmail
* is as often a company's mail as somebody's own — "du" from a mail client
* that a workplace deployed reads as presumptuous in a way "Sie" never does.
* Where a string can avoid the question entirely it does ("Nachricht löschen?"
* rather than "Möchten Sie die Nachricht löschen?"), which is ordinary good
* German UI and sidesteps the choice; the choice is still pinned for the
* strings that cannot.
*
* Terminology, fixed once so it cannot drift:
*
* Inbox Posteingang Archive (verb) archivieren
* Drafts Entwürfe Delete löschen
* Sent Gesendet Move to verschieben nach
* Deleted Items Papierkorb Reply antworten
* Junk / Spam Spam Reply all allen antworten
* Folder Ordner Forward weiterleiten
* Label Label Star markieren
* Conversation Konversation Read / unread gelesen / ungelesen
* Message Nachricht Settings Einstellungen
* Attachment Anhang Signature Signatur
* Contact Kontakt Identity Identität
*
* "Label" is left in English because IMAP keywords are what they are and every
* German mail client leaves it: translating it to "Etikett" would name
* something the reader cannot find anywhere else. "Spam" likewise — "Junk" is
* the English word Germans do not use, "Spam" is the one they do.
*
* Product names are never translated: ihasmail, Stalwart, JMAP, Sieve, vCard.
*/
export const catalog: Catalog = {
strings: {
// ── Actions ────────────────────────────────────────────────────────
"Archive": "Archivieren",
"Archive (e)": "Archivieren (e)",
"Delete": "Löschen",
"Delete (#)": "Löschen (#)",
"Reply": "Antworten",
"Reply (r)": "Antworten (r)",
"Reply all": "Allen antworten",
"Forward": "Weiterleiten",
"Move to…": "Verschieben nach…",
"Move to (v)": "Verschieben nach (v)",
"Move to folder": "In Ordner verschieben",
"Move here": "Hierher verschieben",
"Mark as read": "Als gelesen markieren",
"Mark as read (Shift+I)": "Als gelesen markieren (Umschalt+I)",
"Mark as unread": "Als ungelesen markieren",
"Mark as unread (Shift+U)": "Als ungelesen markieren (Umschalt+U)",
"Mark all as read": "Alle als gelesen markieren",
"Mark all as read, incl. subfolders": "Alle als gelesen markieren, inkl. Unterordner",
"Star": "Markieren",
"Labels": "Labels",
"Labels (l)": "Labels (l)",
"Label as": "Label vergeben",
"Label…": "Label…",
"Compose": "Verfassen",
"Compose message": "Nachricht verfassen",
"Send": "Senden",
"Send at": "Senden am",
"Cancel send": "Senden abbrechen",
"Schedule send": "Senden planen",
"Save": "Speichern",
"Save & activate": "Speichern & aktivieren",
"Save & close (Esc)": "Speichern & schließen (Esc)",
"Save as template": "Als Vorlage speichern",
"Save draft now": "Entwurf jetzt speichern",
"Cancel": "Abbrechen",
"Close": "Schließen",
"Done": "Fertig",
"Continue": "Weiter",
"Edit": "Bearbeiten",
"Edit…": "Bearbeiten…",
"Rename": "Umbenennen",
"Remove": "Entfernen",
"Restore": "Wiederherstellen",
"Retry": "Erneut versuchen",
"Reload": "Neu laden",
"Refresh": "Aktualisieren",
"Copy": "Kopieren",
"Copy email address": "E-Mail-Adresse kopieren",
"Download": "Herunterladen",
"Download all": "Alle herunterladen",
"Download (.eml)": "Herunterladen (.eml)",
"Download latest as .eml": "Neueste als .eml herunterladen",
"Upload": "Hochladen",
"Upload files…": "Dateien hochladen…",
"Print": "Drucken",
"Print conversation": "Konversation drucken",
"Undo (Ctrl+Z)": "Rückgängig (Strg+Z)",
"Redo": "Wiederherstellen",
"Dismiss": "Schließen",
"Discard changes": "Änderungen verwerfen",
"Discard draft": "Entwurf verwerfen",
"Duplicate": "Duplizieren",
"Validate": "Prüfen",
"Revoke": "Widerrufen",
"Turn off": "Deaktivieren",
"Clear": "Leeren",
"Clear selection": "Auswahl aufheben",
"Clear custom colour": "Eigene Farbe entfernen",
"Select": "Auswählen",
"Select all": "Alle auswählen",
"Unsubscribe": "Abbestellen",
"Share…": "Freigeben…",
"Stop sharing": "Freigabe aufheben",
"Open": "Öffnen",
"Open in new tab": "In neuem Tab öffnen",
"Open in calendar": "Im Kalender öffnen",
"Back": "Zurück",
"Back (u)": "Zurück (u)",
"Back to list": "Zurück zur Liste",
"Back to my files": "Zurück zu meinen Dateien",
"Go back to the list": "Zurück zur Liste",
"Next": "Weiter",
"Previous": "Zurück",
"More": "Mehr",
"More actions": "Weitere Aktionen",
"More options": "Weitere Optionen",
"Move up": "Nach oben",
"Move down": "Nach unten",
"Drag to reorder": "Zum Umsortieren ziehen",
"Right-click for options": "Rechtsklick für Optionen",
// ── Mail ───────────────────────────────────────────────────────────
"Mail": "E-Mail",
"Message": "Nachricht",
"Messages": "Nachrichten",
"Message body": "Nachrichtentext",
"Message headers": "Kopfzeilen",
"Message size": "Nachrichtengröße",
"Message-ID": "Message-ID",
"Original message": "Ursprüngliche Nachricht",
"Delete this message": "Diese Nachricht löschen",
"New message to this address": "Neue Nachricht an diese Adresse",
"Conversation view": "Konversationsansicht",
"Draft": "Entwurf",
"Unread": "Ungelesen",
"Unread only": "Nur ungelesene",
"All mail": "Alle Nachrichten",
"Sender": "Absender",
"Sender domain": "Absenderdomäne",
"Recipients": "Empfänger",
"From": "Von",
"To": "An",
"Cc": "Cc",
"Bcc": "Bcc",
"Subject": "Betreff",
"Subject (optional)": "Betreff (optional)",
"Body": "Text",
"Body text": "Fließtext",
"Attach files": "Dateien anhängen",
"Attach from Files": "Aus Dateien anhängen",
"Remove attachment": "Anhang entfernen",
"Has attachment": "Hat Anhang",
"Has the words": "Enthält die Wörter",
"Header name": "Name der Kopfzeile",
"Show headers": "Kopfzeilen anzeigen",
"Show original": "Original anzeigen",
"Show details": "Details anzeigen",
"Show images": "Bilder anzeigen",
"Remote images": "Externe Bilder",
"Remote images are blocked to protect your privacy.": "Externe Bilder werden zum Schutz Ihrer Privatsphäre blockiert.",
"Always from {email}": "Immer von {email}",
"This looks like a mailing list.": "Das sieht nach einer Mailingliste aus.",
"This folder is empty": "Dieser Ordner ist leer",
"This folder is empty.": "Dieser Ordner ist leer.",
"Delete all spam now": "Gesamten Spam jetzt löschen",
"Deleting spam is permanent — it does not go to Deleted Items first.": "Das Löschen von Spam ist endgültig — er wandert nicht zuerst in den Papierkorb.",
"Keep in Inbox": "Im Posteingang behalten",
"Newer (k)": "Neuere (k)",
"Older (j)": "Ältere (j)",
"Open the next (older) conversation": "Nächste (ältere) Konversation öffnen",
"Open the previous (newer) conversation": "Vorherige (neuere) Konversation öffnen",
"Loading conversation…": "Konversation wird geladen…",
"Important": "Wichtig",
"Unverified": "Nicht verifiziert",
"Priority": "Priorität",
"High": "Hoch",
"Normal": "Normal",
"Low": "Niedrig",
"to {recipients}": "an {recipients}",
"From: {sender}": "Von: {sender}",
"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.",
"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",
"Read receipt requested": "Lesebestätigung angefordert",
"The sender asked for a read receipt.": "Der Absender hat eine Lesebestätigung angefordert.",
"Request read receipt": "Lesebestätigung anfordern",
"Always request read receipts": "Immer Lesebestätigungen anfordern",
"Receipt": "Bestätigung",
"Never send one": "Nie senden",
"Not this time": "Diesmal nicht",
"It would go to {address}, which is not where the message came from.": "Sie ginge an {address} — nicht dorthin, woher die Nachricht kam.",
"Use “Show original” for the complete raw message.": "Nutzen Sie „Original anzeigen“ für die vollständige Rohnachricht.",
// ── Folders, calendar, contacts, files ─────────────────────────────
"Folder": "Ordner",
"Folders": "Ordner",
"Folder options": "Ordneroptionen",
"New folder": "Neuer Ordner",
"New subfolder": "Neuer Unterordner",
"Delete folder": "Ordner löschen",
"No matching folders": "Keine passenden Ordner",
"No subfolders here.": "Hier gibt es keine Unterordner.",
"Type a folder name…": "Ordnernamen eingeben…",
" New folder…": " Neuer Ordner…",
"Create, rename and hide folders.": "Ordner erstellen, umbenennen und ausblenden.",
"Show unsubscribed (hidden) folders": "Nicht abonnierte (ausgeblendete) Ordner anzeigen",
"Storage: {used} of {total} used.": "Speicher: {used} von {total} belegt.",
"{used} of {total}": "{used} von {total}",
"Calendar": "Kalender",
"My calendars": "Meine Kalender",
"New calendar": "Neuer Kalender",
"Calendar options": "Kalenderoptionen",
"Calendar & contacts": "Kalender & Kontakte",
"Calendar is not available": "Kalender ist nicht verfügbar",
"Event": "Termin",
"New event": "Neuer Termin",
"(new event)": "(neuer Termin)",
"New all-day event": "Neuer ganztägiger Termin",
"Add title": "Titel hinzufügen",
"Add location": "Ort hinzufügen",
"Add to calendar": "Zum Kalender hinzufügen",
"Add to my calendar": "Zu meinem Kalender hinzufügen",
"Remove from calendar": "Aus dem Kalender entfernen",
"Remove from my calendar": "Aus meinem Kalender entfernen",
"All day": "Ganztägig",
"all-day": "ganztägig",
"Starts": "Beginnt",
"Starts (optional)": "Beginnt (optional)",
"Ends": "Endet",
"Ends (optional)": "Endet (optional)",
"Day": "Tag",
"Week": "Woche",
"Month": "Monat",
"Agenda": "Agenda",
"Today": "Heute",
"Go to day": "Zum Tag",
"Go to week": "Zur Woche",
"Previous month": "Voriger Monat",
"Next month": "Nächster Monat",
"Does not repeat": "Wiederholt sich nicht",
"Daily": "Täglich",
"Every weekday": "Jeden Wochentag",
"Yearly": "Jährlich",
"Custom…": "Benutzerdefiniert…",
"Weekly on {weekday}": "Wöchentlich am {weekday}",
"Monthly on day {day}": "Monatlich am {day}.",
"Repeat every": "Wiederholen alle",
"Repeat until": "Wiederholen bis",
"after N times": "nach N Malen",
"on date": "am Datum",
"never": "nie",
"day(s)": "Tag(e)",
"week(s)": "Woche(n)",
"month(s)": "Monat(e)",
"year(s)": "Jahr(e)",
"Reminders": "Erinnerungen",
"Add reminder": "Erinnerung hinzufügen",
"Remove reminder": "Erinnerung entfernen",
"Default reminder": "Standarderinnerung",
"At time of event": "Zum Zeitpunkt des Termins",
"5 minutes before": "5 Minuten vorher",
"10 minutes before": "10 Minuten vorher",
"15 minutes before": "15 Minuten vorher",
"30 minutes before": "30 Minuten vorher",
"1 hour before": "1 Stunde vorher",
"1 day before": "1 Tag vorher",
"15 minutes": "15 Minuten",
"30 minutes": "30 Minuten",
"45 minutes": "45 Minuten",
"1 hour": "1 Stunde",
"1.5 hours": "1,5 Stunden",
"2 hours": "2 Stunden",
"Default event length": "Standarddauer für Termine",
"Default view": "Standardansicht",
"Guests": "Teilnehmer",
"Add guests by name or email": "Teilnehmer nach Name oder E-Mail hinzufügen",
"Send invitation emails to guests": "Einladungen per E-Mail an Teilnehmer senden",
"Going?": "Nehmen Sie teil?",
"Yes": "Ja",
"No": "Nein",
"Maybe": "Vielleicht",
"Confirmed": "Zugesagt",
"Tentative": "Vorläufig",
"Cancelled": "Abgesagt",
"organizer": "Organisator",
"Organizer: {name}": "Organisator: {name}",
"Free": "Frei",
"Busy": "Gebucht",
"Free/busy": "Frei/Gebucht",
"Show as": "Anzeigen als",
"Availability on {date}": "Verfügbarkeit am {date}",
"Count all events as busy": "Alle Termine als gebucht zählen",
"Only events I'm attending": "Nur Termine, an denen ich teilnehme",
"Don't include in availability": "Nicht in die Verfügbarkeit einbeziehen",
"Meeting link": "Besprechungslink",
"No events in the next 60 days.": "Keine Termine in den nächsten 60 Tagen.",
"Working hours": "Arbeitszeiten",
"Working hours start": "Arbeitszeit beginnt",
"Working hours end": "Arbeitszeit endet",
"Colour categories": "Farbkategorien",
"Category": "Kategorie",
"No category": "Keine Kategorie",
"New category": "Neue Kategorie",
"Delete category": "Kategorie löschen",
"Manage categories…": "Kategorien verwalten…",
"Use category color": "Kategoriefarbe verwenden",
"Use calendar color": "Kalenderfarbe verwenden",
"Use the default colour": "Standardfarbe verwenden",
"+{n} more": "+{n} weitere",
"Contacts": "Kontakte",
"Contacts are not available": "Kontakte sind nicht verfügbar",
"New contact": "Neuer Kontakt",
"Edit contact": "Kontakt bearbeiten",
"Select a contact": "Kontakt auswählen",
"All contacts": "Alle Kontakte",
"Search contacts": "Kontakte durchsuchen",
"Search contacts to add…": "Kontakte zum Hinzufügen suchen…",
"Loading contacts…": "Kontakte werden geladen…",
"Add to contacts": "Zu Kontakten hinzufügen",
"Add to my contacts": "Zu meinen Kontakten hinzufügen",
"Remove from my contacts": "Aus meinen Kontakten entfernen",
"Address book": "Adressbuch",
"Address books": "Adressbücher",
"All address books": "Alle Adressbücher",
"My address books": "Meine Adressbücher",
"New address book": "Neues Adressbuch",
"No address books yet.": "Noch keine Adressbücher.",
"Choose from address books": "Aus Adressbüchern wählen",
"Import vCard": "vCard importieren",
"Export all": "Alle exportieren",
"Export book": "Adressbuch exportieren",
"Email group": "Gruppe anschreiben",
"Email everyone": "Alle anschreiben",
"Members": "Mitglieder",
"Members ({count})": "Mitglieder ({count})",
"Group": "Gruppe",
"· group": "· Gruppe",
"Person": "Person",
"First name": "Vorname",
"Last name": "Nachname",
"More name fields": "Weitere Namensfelder",
"Nickname": "Spitzname",
"Prefix": "Anrede",
"Suffix": "Namenszusatz",
"Dr.": "Dr.",
"Jr.": "Jr.",
"Display name": "Anzeigename",
"Job title": "Position",
"Organization": "Organisation",
"Company": "Firma",
"Birthday": "Geburtstag",
"Notes": "Notizen",
"Website": "Website",
"Phone": "Telefon",
"Add phone": "Telefon hinzufügen",
"Add email": "E-Mail hinzufügen",
"Add address": "Adresse hinzufügen",
"Address": "Adresse",
"Street": "Straße",
"City": "Stadt",
"State / Region": "Bundesland / Region",
"Postal code": "Postleitzahl",
"Country": "Land",
"Change photo": "Foto ändern",
"Remove photo": "Foto entfernen",
"Updated {date}": "Aktualisiert am {date}",
"Search names and addresses": "Namen und Adressen durchsuchen",
"Add a person or group…": "Person oder Gruppe hinzufügen…",
"Choose recipients": "Empfänger auswählen",
"Available to add": "Zum Hinzufügen verfügbar",
"vCard": "vCard",
"vCard attachment": "vCard-Anhang",
"Files": "Dateien",
"My files": "Meine Dateien",
"File storage is not available": "Dateispeicher ist nicht verfügbar",
"Drag files here or use Upload.": "Dateien hierher ziehen oder „Hochladen“ nutzen.",
"Shared": "Freigegeben",
"Shared with me": "Für mich freigegeben",
"Nothing is shared with you.": "Für Sie ist nichts freigegeben.",
"Not shared with anyone yet.": "Noch für niemanden freigegeben.",
"Check for new shares": "Nach neuen Freigaben suchen",
"Shared files are copied to your account when attached.": "Freigegebene Dateien werden beim Anhängen in Ihr Konto kopiert.",
"Viewer": "Leseberechtigt",
"Editor": "Bearbeitungsberechtigt",
"Size": "Größe",
// ── Settings ───────────────────────────────────────────────────────
"Settings": "Einstellungen",
"All settings": "Alle Einstellungen",
"Sections": "Bereiche",
"General": "Allgemein",
"Appearance": "Darstellung",
"Make ihasmail yours.": "Machen Sie ihasmail zu Ihrem.",
"Reading": "Lesen",
"Reading pane": "Lesebereich",
"Reading, sending and list behaviour. Settings are stored in this browser.": "Verhalten beim Lesen, Senden und in der Liste. Die Einstellungen werden in diesem Browser gespeichert.",
"Right of the list": "Rechts von der Liste",
"Below the list": "Unter der Liste",
"Hidden (open full width)": "Ausgeblendet (in voller Breite öffnen)",
"Off (open messages full width)": "Aus (Nachrichten in voller Breite öffnen)",
"Off": "Aus",
"Composing": "Verfassen",
"Default format": "Standardformat",
"Rich text (HTML)": "Formatierter Text (HTML)",
"Plain text": "Nur Text",
"Quote original message in replies": "Ursprüngliche Nachricht in Antworten zitieren",
"Place signature above quoted text": "Signatur über dem zitierten Text platzieren",
"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",
"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.",
"Show sender avatars": "Absenderbilder anzeigen",
"Group messages from the same thread together.": "Nachrichten desselben Threads zusammenfassen.",
"After archiving or deleting": "Nach Archivieren oder Löschen",
"Ask before showing (recommended)": "Vor dem Anzeigen fragen (empfohlen)",
"Always (all messages)": "Immer (alle Nachrichten)",
"Show automatically from my contacts": "Bei meinen Kontakten automatisch anzeigen",
"Immediately when opened": "Sofort beim Öffnen",
"After 2 seconds": "Nach 2 Sekunden",
"After 5 seconds": "Nach 5 Sekunden",
"Never automatically": "Nie automatisch",
"When someone requests a read receipt": "Wenn jemand eine Lesebestätigung anfordert",
"Ask me on each message": "Bei jeder Nachricht fragen",
"5 seconds": "5 Sekunden",
"8 seconds": "8 Sekunden",
"15 seconds": "15 Sekunden",
"30 seconds": "30 Sekunden",
"Locale": "Regionales",
"Language": "Sprache",
"Interface language": "Sprache der Oberfläche",
"Language & region": "Sprache & Region",
"Date format": "Datumsformat",
"Time format": "Zeitformat",
"Time zone": "Zeitzone",
"Week starts on": "Woche beginnt am",
"Monday": "Montag",
"Saturday": "Samstag",
"Sunday": "Sonntag",
"12-hour clock (6:23 PM)": "12-Stunden-Format (6:23 PM)",
"24-hour clock (18:23)": "24-Stunden-Format (18:23)",
"Browser default ({zone})": "Browser-Standard ({zone})",
"Default ({zone})": "Standard ({zone})",
"Automatic ({example})": "Automatisch ({example})",
"Automatic ({locale})": "Automatisch ({locale})",
"Preview: {example}": "Vorschau: {example}",
"Dates, times and month names follow this choice.": "Datum, Uhrzeit und Monatsnamen richten sich nach dieser Auswahl.",
"Your mail server reports {name} ({tag}).": "Ihr Mailserver meldet {name} ({tag}).",
"Your mail server does not report a locale, so the browser's is used.": "Ihr Mailserver meldet keine Spracheinstellung, daher wird die des Browsers verwendet.",
"Dates": "Datum",
"Date": "Datum",
"Time": "Uhrzeit",
"When": "Wann",
"Then": "Dann",
"then": "dann",
"Theme": "Design",
"Accent color": "Akzentfarbe",
"Color": "Farbe",
"Colour": "Farbe",
"Text color": "Textfarbe",
"Density & text": "Dichte & Text",
"Display density": "Anzeigedichte",
"Comfortable": "Komfortabel",
"Cozy (default)": "Ausgewogen (Standard)",
"Compact": "Kompakt",
"Text size": "Schriftgröße",
"Font size": "Schriftgröße",
"Small": "Klein",
"Medium": "Mittel",
"Large": "Groß",
"Huge": "Sehr groß",
"Sidebar": "Seitenleiste",
"Show labels in the sidebar": "Labels in der Seitenleiste anzeigen",
"Collapse sidebar to icons": "Seitenleiste auf Symbole verkleinern",
"Apply the theme to messages too": "Design auch auf Nachrichten anwenden",
"Swiping": "Wischgesten",
"Swipe left": "Nach links wischen",
"Swipe right": "Nach rechts wischen",
"Backup": "Sicherung",
"Export settings": "Einstellungen exportieren",
"Import settings": "Einstellungen importieren",
"Settings imported": "Einstellungen importiert",
"Invalid settings file": "Ungültige Einstellungsdatei",
"Reset to defaults": "Auf Standard zurücksetzen",
"Default mail app": "Standard-E-Mail-Programm",
"Documentation": "Dokumentation",
"About ihasmail": "Über ihasmail",
"Server": "Server",
"Server capabilities": "Server-Funktionen",
"Accounts": "Konten",
"Account": "Konto",
"Max upload": "Maximaler Upload",
"{size} MB": "{size} MB",
"KB": "KB",
"Image privacy proxy": "Bild-Datenschutz-Proxy",
"enabled": "aktiviert",
"disabled": "deaktiviert",
"Enabled": "Aktiviert",
"active": "aktiv",
"hidden": "ausgeblendet",
"connected": "verbunden",
"reconnecting…": "Verbindung wird wiederhergestellt…",
"AGPL-3.0 source": "AGPL-3.0-Quellcode",
// ── Identities, templates, filters ─────────────────────────────────
"Identities & signatures": "Identitäten & Signaturen",
"Add identity": "Identität hinzufügen",
"Delete identity": "Identität löschen",
"Make default": "Als Standard festlegen",
"Default": "Standard",
"Show when composing": "Beim Verfassen anzeigen",
"Hide when composing": "Beim Verfassen ausblenden",
"Signature": "Signatur",
"Your signature…": "Ihre Signatur…",
"Reply-To": "Antwort an",
"Reply-To (optional)": "Antwort an (optional)",
"Reply-To: {addresses}": "Antwort an: {addresses}",
"Replies go to…": "Antworten gehen an…",
"Set a Reply-To address": "Antwortadresse festlegen",
"{email} is now your default identity": "{email} ist jetzt Ihre Standardidentität",
"Templates": "Vorlagen",
"New template": "Neue Vorlage",
"Delete template": "Vorlage löschen",
"Insert template": "Vorlage einfügen",
"Template text…": "Vorlagentext…",
"Subject: {subject}": "Betreff: {subject}",
"Filters & rules": "Filter & Regeln",
"Filters unavailable": "Filter nicht verfügbar",
"Rules": "Regeln",
"Rule name": "Regelname",
"New rule": "Neue Regel",
"Delete rule": "Regel löschen",
"No filters yet": "Noch keine Filter",
"Add condition": "Bedingung hinzufügen",
"Remove condition": "Bedingung entfernen",
"Add action": "Aktion hinzufügen",
"Remove action": "Aktion entfernen",
"all of the following match": "alle folgenden zutreffen",
"any of the following match": "eine der folgenden zutrifft",
"contains": "enthält",
"does not contain": "enthält nicht",
"is larger than": "ist größer als",
"is smaller than": "ist kleiner als",
"Stop processing more rules": "Keine weiteren Regeln verarbeiten",
"keep copy": "Kopie behalten",
"Forward to": "Weiterleiten an",
"Reject with message": "Mit Nachricht abweisen",
"Scripts": "Skripte",
"Scripts (advanced)": "Skripte (fortgeschritten)",
"Script name": "Skriptname",
"New script": "Neues Skript",
"Delete script": "Skript löschen",
"Sieve source": "Sieve-Quelltext",
"Preview generated Sieve script": "Erzeugtes Sieve-Skript ansehen",
"Start with rules": "Mit Regeln beginnen",
"Switch to rules?": "Zu Regeln wechseln?",
"Create filter": "Filter erstellen",
"Filter messages like this": "Nachrichten wie diese filtern",
"Filter messages like this…": "Nachrichten wie diese filtern…",
"Also apply to existing messages in": "Auch auf vorhandene Nachrichten anwenden in",
"keyword (e.g. $important, work)": "Schlüsselwort (z. B. $important, work)",
"Out of office": "Abwesenheit",
"Auto-reply enabled": "Automatische Antwort aktiviert",
// ── Security, sessions, notifications ──────────────────────────────
"Security & sessions": "Sicherheit & Sitzungen",
"Password": "Passwort",
"Your password": "Ihr Passwort",
"Current password": "Aktuelles Passwort",
"New password": "Neues Passwort",
"Confirm new password": "Neues Passwort bestätigen",
"Current code": "Aktueller Code",
"Code from your authenticator": "Code aus Ihrer Authenticator-App",
"Two-factor authentication": "Zwei-Faktor-Authentifizierung",
"Turn off two-factor authentication": "Zwei-Faktor-Authentifizierung deaktivieren",
"Your password alone will be enough to sign in again.": "Ihr Passwort allein genügt dann wieder zum Anmelden.",
"App passwords": "App-Passwörter",
"New app password for": "Neues App-Passwort für",
"Your new app password": "Ihr neues App-Passwort",
"Secret": "Geheimnis",
"Thunderbird on my laptop": "Thunderbird auf meinem Laptop",
"Active webmail sessions": "Aktive Webmail-Sitzungen",
"Sign out": "Abmelden",
"Sign out here": "Hier abmelden",
"Sign out all other sessions": "Alle anderen Sitzungen abmelden",
"Signed in as": "Angemeldet als",
"This is my own device": "Das ist mein eigenes Gerät",
"this device": "dieses Gerät",
"Device": "Gerät",
"IP": "IP",
"Last active": "Zuletzt aktiv",
"Created": "Erstellt",
"Expires": "Läuft ab",
"Status": "Status",
"Online": "Online",
"Reason": "Grund",
"Type": "Typ",
"Email or username": "E-Mail oder Benutzername",
"Use your usual address as the username.": "Verwenden Sie Ihre gewohnte Adresse als Benutzernamen.",
"Fast, friendly webmail. Your mailbox, your way.": "Schnelle, freundliche Webmail. Ihr Postfach, wie Sie es wollen.",
"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",
"Play a sound for new mail": "Ton bei neuer E-Mail abspielen",
"Test notification": "Testbenachrichtigung",
"Background notifications are on": "Hintergrundbenachrichtigungen sind aktiviert",
"The tab title and favicon always show your unread Inbox count.": "Tab-Titel und Favicon zeigen immer die Anzahl ungelesener Nachrichten im Posteingang.",
"Live updates are delivered via JMAP push ({state}).": "Live-Aktualisierungen kommen über JMAP-Push ({state}).",
"Shows a system notification when new mail arrives in your Inbox while the tab is in the background.": "Zeigt eine Systembenachrichtigung, wenn neue Nachrichten im Posteingang eintreffen, während der Tab im Hintergrund ist.",
// ── Editor, search, shortcuts, misc ────────────────────────────────
"Formatting": "Formatierung",
"Formatting options": "Formatierungsoptionen",
"Remove formatting": "Formatierung entfernen",
"Bold (Ctrl+B)": "Fett (Strg+B)",
"Italic (Ctrl+I)": "Kursiv (Strg+I)",
"Underline (Ctrl+U)": "Unterstrichen (Strg+U)",
"Strikethrough": "Durchgestrichen",
"Highlight": "Hervorheben",
"Bulleted list": "Aufzählung",
"Numbered list": "Nummerierte Liste",
"Increase indent": "Einzug vergrößern",
"Decrease indent": "Einzug verkleinern",
"Align left": "Linksbündig",
"Align right": "Rechtsbündig",
"Center": "Zentriert",
"Quote": "Zitat",
"Code block": "Codeblock",
"Normal text": "Normaler Text",
"Insert link (Ctrl+K)": "Link einfügen (Strg+K)",
"Insert image": "Bild einfügen",
"Link": "Link",
"List": "Liste",
"Emoji": "Emoji",
"Write your message…": "Schreiben Sie Ihre Nachricht…",
"Search": "Suchen",
"Search mail": "E-Mail durchsuchen",
"Advanced search": "Erweiterte Suche",
"Keyboard shortcuts": "Tastenkürzel",
"Keyboard shortcuts (?)": "Tastenkürzel (?)",
"Shortcuts": "Kürzel",
"Go to": "Gehe zu",
"Menu": "Menü",
"Options": "Optionen",
"Send options": "Sendeoptionen",
"Name": "Name",
"Email": "E-Mail",
"Email address": "E-Mail-Adresse",
"Description": "Beschreibung",
"Location": "Ort",
"Visibility": "Sichtbarkeit",
"Private": "Privat",
"Work": "Arbeit",
"Loading…": "Wird geladen…",
"None": "Keine",
"optional": "optional",
"Always show": "Immer anzeigen",
"to": "an",
"Received": "Empfangen",
"In-Reply-To": "In-Reply-To",
"References": "References",
"Add label / keyword": "Label / Schlüsselwort hinzufügen",
"Manage labels": "Labels verwalten",
"Create “{name}”": "„{name}“ erstellen",
"Type a name to create your first label.": "Geben Sie einen Namen ein, um Ihr erstes Label zu erstellen.",
"Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "Labels sind IMAP-Schlüsselwörter, die in Ihren Nachrichten gespeichert werden und daher mit anderen Clients synchronisiert werden. Namen und Farben bleiben in diesem Browser.",
"PDF": "PDF",
"Large attachments may be rejected by some servers": "Große Anhänge werden von manchen Servern abgelehnt",
"Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "Bilder werden in Ihren Dateien (Ordner „ihasmail“) gespeichert und beim Senden eingebettet.",
"Thanks for your message. I'm away until … and will reply when I'm back.": "Vielen Dank für Ihre Nachricht. Ich bin bis … abwesend und melde mich nach meiner Rückkehr.",
"Automatically reply to people who email you while you're away. Each sender gets at most one reply.": "Automatisch auf E-Mails antworten, während Sie abwesend sind. Jeder Absender erhält höchstens eine Antwort.",
"Sort incoming mail automatically. Rules run on the server (Sieve), so they work for every client you use.": "Eingehende Nachrichten automatisch sortieren. Die Regeln laufen auf dem Server (Sieve) und gelten daher für jeden Client, den Sie nutzen.",
"Canned responses you can insert into any message from the composer's template button.": "Vorgefertigte Antworten, die Sie über die Vorlagen-Schaltfläche im Editor in jede Nachricht einfügen können.",
"Create a rule to move newsletters to a folder, flag important senders, or forward mail.": "Erstellen Sie eine Regel, um Newsletter in einen Ordner zu verschieben, wichtige Absender zu markieren oder Nachrichten weiterzuleiten.",
"Advanced: manage raw Sieve scripts. Only one script can be active at a time.": "Fortgeschritten: Sieve-Skripte direkt verwalten. Es kann immer nur ein Skript aktiv sein.",
"Only part of your filter script arrived.": "Ihr Filterskript ist nur teilweise angekommen.",
"Your active script “{name}” was written by hand.": "Ihr aktives Skript „{name}“ wurde von Hand geschrieben.",
"Another script (“{name}”) is active. Saving rules here will activate the “ihasmail” script instead.": "Ein anderes Skript („{name}“) ist aktiv. Wenn Sie hier Regeln speichern, wird stattdessen das Skript „ihasmail“ aktiviert.",
"“{name}” will be deactivated (not deleted) and a new “ihasmail” script will take over.": "„{name}“ wird deaktiviert (nicht gelöscht) und ein neues Skript „ihasmail“ übernimmt.",
"Sieve filtering is not available for this account.": "Sieve-Filterung ist für dieses Konto nicht verfügbar.",
"Sieve filtering is not enabled for this account.": "Sieve-Filterung ist für dieses Konto nicht aktiviert.",
"Vacation responses are not available for this account.": "Abwesenheitsnotizen sind für dieses Konto nicht verfügbar.",
"This account does not have the JMAP calendars capability.": "Dieses Konto verfügt nicht über die JMAP-Kalenderfunktion.",
"This account does not have the JMAP contacts capability.": "Dieses Konto verfügt nicht über die JMAP-Kontaktfunktion.",
"This account does not have the JMAP file storage capability.": "Dieses Konto verfügt nicht über die JMAP-Dateispeicherfunktion.",
"tell us about it": "sagen Sie uns Bescheid",
"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}.": "Diese Übersetzung wurde von einer KI erstellt und nicht von einer Person mit Muttersprache Deutsch geprüft; sie ist daher als Beta gekennzeichnet, bis jemand sie freigibt. Alles, was falsch klingt, ist eine Meldung wert — {report}.",
// ── Longer prose ───────────────────────────────────────────────────
// Product names, example addresses and bare URLs are deliberately absent:
// they fall back to English because that is what they should say.
"After": "Nach",
"Before": "Vor",
"Modified": "Geändert",
"Middle name": "Zweiter Vorname",
"Minimize": "Minimieren",
"New label": "Neues Label",
"Delete label": "Label löschen",
"Choose a date": "Datum wählen",
"Choose a date and time": "Datum und Uhrzeit wählen",
"Pick date and time…": "Datum und Uhrzeit wählen…",
"Search mail (from:, to:, subject:, has:attachment, is:unread, in:, before:, after:)": "E-Mail durchsuchen (from:, to:, subject:, has:attachment, is:unread, in:, before:, after:)",
"Settings → Filters & rules": "Einstellungen → Filter & Regeln",
"Open the Mail view to see all shortcuts.": "Öffnen Sie die E-Mail-Ansicht, um alle Tastenkürzel zu sehen.",
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Tastenkürzel im Gmail-Stil sind immer aktiv. Drücken Sie überall {key}, um diese Liste zu sehen.",
"Select a conversation to read it here · Press {key} for shortcuts": "Wählen Sie eine Konversation, um sie hier zu lesen · {key} für Tastenkürzel",
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Tipp: Drücken Sie {key} auf einer Konversation, um Labels zu vergeben. Suchen Sie mit {operator}.",
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Eine schnelle, freundliche Open-Source-Webmail für {server}, auf JMAP aufgebaut.",
"Defaults for the calendar views and new events.": "Vorgaben für die Kalenderansichten und neue Termine.",
"Replies will go to this address instead of the From address": "Antworten gehen an diese Adresse statt an die Absenderadresse",
"Replies to mail sent from this identity go here instead of the From address.": "Antworten auf Nachrichten von dieser Identität gehen hierhin statt an die Absenderadresse.",
"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.",
"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 colours 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.",
"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.": "Auf einem Touchscreen ziehen Sie eine Nachricht zur Seite, um sie zu bearbeiten. Jede Richtung kann eine Aktion ausführen — oder keine. Die Einstellung folgt Ihrem Konto, sodass Telefon und Tablet übereinstimmen; mit der Maus wird sie ignoriert und Nachrichten werden weiterhin in Ordner gezogen.",
"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.",
"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).",
"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.",
"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 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.",
"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.",
"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.",
// ── Labels defined as constants, translated where they render ──────
// The catalogue checker cannot see these: they reach t() as a variable,
// so there is no string literal for it to find. Listed here deliberately.
"About": "Über",
"Add": "Hinzufügen",
"Add files": "Dateien hinzufügen",
"Automatic": "Automatisch",
"Create subfolders": "Unterordner erstellen",
"Dark": "Dunkel",
"Light": "Hell",
"Match system": "System folgen",
"Day.Month.Year": "Tag.Monat.Jahr",
"Day/Month/Year": "Tag/Monat/Jahr",
"Month/Day/Year": "Monat/Tag/Jahr",
"Year-Month-Day (ISO 8601)": "Jahr-Monat-Tag (ISO 8601)",
"Edit all": "Alles bearbeiten",
"Edit contents": "Inhalte bearbeiten",
"Edit own": "Eigene bearbeiten",
"Flag": "Markieren",
"Mark read": "Als gelesen markieren",
"Private props": "Private Eigenschaften",
"Read": "Lesen",
"Read events": "Termine lesen",
"RSVP": "Zusagen",
"See free/busy": "Frei/Gebucht sehen",
"Share": "Freigeben",
"Write": "Schreiben",
"Live updates connected": "Live-Aktualisierungen verbunden",
"Live updates reconnecting…": "Live-Aktualisierungen werden wieder verbunden…",
"Live updates off — checking periodically instead": "Live-Aktualisierungen aus — es wird stattdessen regelmäßig geprüft",
"Mark as read / unread": "Als gelesen / ungelesen markieren",
"Star / unstar": "Markieren / Markierung entfernen",
"Report spam / not spam": "Als Spam / Nicht-Spam melden",
"Report spam": "Als Spam melden",
"Not spam": "Kein Spam",
"Nothing": "Nichts",
"No conversation selected": "Keine Konversation ausgewählt",
"Drop here for the top level": "Hierher ziehen für die oberste Ebene",
// ── Remaining prose ────────────────────────────────────────────────
"{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} ist die Farbpalette von {site} und das, womit ein neues Konto startet. Es ist ein dunkles Design und zählt daher überall dort als dunkel, wo das eine Rolle spielt; die Akzentfarbe unten wirkt weiterhin darauf.",
"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.",
// ── Weekdays, schedule presets, rule operators ─────────────────────
// Header names (List-Id, X-Spam-Status) stay English: they are the actual
// field names in the message, not words.
"Tuesday": "Dienstag",
"Wednesday": "Mittwoch",
"Thursday": "Donnerstag",
"Friday": "Freitag",
"Later today": "Später heute",
"Tomorrow morning": "Morgen früh",
"Tomorrow afternoon": "Morgen Nachmittag",
"Monday morning": "Montagmorgen",
"Open draft": "Entwurf öffnen",
"Undo": "Rückgängig",
"Deleted Items": "Papierkorb",
"Other header…": "Andere Kopfzeile…",
"is": "ist",
"is not": "ist nicht",
"matches (wildcards * ?)": "entspricht (Platzhalter * ?)",
"does not match": "entspricht nicht",
"matches regex": "entspricht regulärem Ausdruck",
"does not match regex": "entspricht regulärem Ausdruck nicht",
"exists": "ist vorhanden",
"does not exist": "ist nicht vorhanden",
},
plurals: {
"{n} messages": { one: "{n} Nachricht", other: "{n} Nachrichten" },
"{n} selected": { one: "{n} ausgewählt", other: "{n} ausgewählt" },
"{n} conversations": { one: "{n} Konversation", other: "{n} Konversationen" },
},
};
@@ -34,8 +34,15 @@ describe("applyLang", () => {
expect(document.documentElement.lang).toBe("en");
});
it("falls back to English rather than claiming a language it cannot render", () => {
it("serves a language whose catalogue is shipped", () => {
applyLang({ ...DEFAULT_SETTINGS, uiLanguage: "de" });
expect(document.documentElement.lang).toBe("de");
});
it("falls back to English rather than claiming a language it cannot render", () => {
// A tag no catalogue exists for -- an account carrying a preference from a
// build that shipped more languages than this one.
applyLang({ ...DEFAULT_SETTINGS, uiLanguage: "fr" });
expect(document.documentElement.lang).toBe("en");
});
+2 -2
View File
@@ -81,7 +81,7 @@ export function AppShell({ children }: { children: ReactNode }) {
</Link>
<SearchBar />
<div className="topbar-actions">
<span className="push-status hide-mobile" role="img" aria-label={PUSH_LABEL[pushState]} title={PUSH_LABEL[pushState]}>
<span className="push-status hide-mobile" role="img" aria-label={t(PUSH_LABEL[pushState])} title={t(PUSH_LABEL[pushState])}>
<span className={`push-dot ${pushState}`} />
</span>
<button className="icon-btn hide-mobile" aria-label={t("Keyboard shortcuts")} title={t("Keyboard shortcuts (?)")} onClick={() => setHelpOpen(true)}>
@@ -132,7 +132,7 @@ export function AppShell({ children }: { children: ReactNode }) {
}}
>
{section === "files" ? <Upload size={22} /> : section === "calendar" || section === "contacts" ? <Plus size={22} /> : <PenSquare size={22} />}
<span>{section === "calendar" ? "New event" : section === "contacts" ? "New contact" : section === "files" ? "Upload" : "Compose"}</span>
<span>{section === "calendar" ? t("New event") : section === "contacts" ? t("New contact") : section === "files" ? t("Upload") : t("Compose")}</span>
</button>
<div className="sidebar-scroll">
{(section === "mail" || section === "search") && <MailboxTree />}
+2 -2
View File
@@ -8,7 +8,7 @@ import { RuleDialog } from "../settings/RuleDialog";
import { toast } from "@/ui/toast";
import { Spinner } from "@/ui/misc";
import { Dialog } from "@/ui/dialog";
import { t } from "@/lib/i18n";
import { t, tNode } from "@/lib/i18n";
/** "Filter messages like this…" — creates a Sieve rule seeded from a message, optionally applying it to the current folder. */
export function FilterFromMessageDialog({ email, mailboxId, onClose }: { email: Email; mailboxId: Id | null; onClose: () => void }) {
@@ -47,7 +47,7 @@ export function FilterFromMessageDialog({ email, mailboxId, onClose }: { email:
{damage ? (
<p>{t("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 })}</p>
) : loaded ? (
<p>{t("Your active Sieve script was written by hand, so rules can't be added automatically. Open")} <b>{t("Settings → Filters & rules")}</b> {t("to edit the script or switch to managed rules.")}</p>
<p>{tNode("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.", { where: <b>{t("Settings → Filters & rules")}</b> })}</p>
) : (
<p>{t("Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.")}</p>
)}
+3 -3
View File
@@ -16,7 +16,7 @@ import { confirmDialog } from "@/ui/dialog";
import { toast } from "@/ui/toast";
import { isUnknownMailbox } from "@/lib/mailboxRoute";
import { scheduledMailboxIdFrom, useScheduled } from "@/store/scheduled";
import { t as translate } from "@/lib/i18n";
import { plural, t as translate, tNode } from "@/lib/i18n";
export function MailView({ mailboxId, threadId, search }: { mailboxId?: string; threadId?: string; search?: boolean }) {
const [, navigate] = useLocation();
@@ -353,8 +353,8 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
) : (
<div className="no-thread">
<img src="/img/logo.png" alt="" />
<div>{list?.total ? `${list.total} conversation${list.total === 1 ? "" : "s"}` : "No conversation selected"}</div>
<div className="hint">{translate("Select a conversation to read it here · Press")} <kbd className="kbd">?</kbd> {translate("for shortcuts")}</div>
<div>{list?.total ? plural(list.total, { one: "{n} conversation", other: "{n} conversations" }) : translate("No conversation selected")}</div>
<div className="hint">{tNode("Select a conversation to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })}</div>
</div>
)}
</div>
+1 -1
View File
@@ -143,7 +143,7 @@ export function MailboxTree() {
if (id) void moveFolder(id, null);
}}
>
<span>{draggingId && canDropOn(null) ? "Drop here for the top level" : "Folders"}</span>
<span>{draggingId && canDropOn(null) ? t("Drop here for the top level") : t("Folders")}</span>
<button className="icon-btn" title={t("New folder")} aria-label={t("New folder")} onClick={() => void createFolder(null)}>
<Plus size={16} />
</button>
+2 -2
View File
@@ -24,7 +24,7 @@ import { useScheduled } from "@/store/scheduled";
import { formatScheduleTime } from "@/lib/schedule";
import { mdnDecision, refusalText } from "@/lib/mdn";
import { sendReadReceipt } from "@/store/mdn";
import { t as translate } from "@/lib/i18n";
import { t as translate, tNode } from "@/lib/i18n";
interface Props {
email: Email;
@@ -226,7 +226,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
{translate("The sender asked for a read receipt.")}
{receipt.redirected && (
<> {translate("It would go to")} <strong>{receipt.to!.email}</strong>{translate(", which is not where the message came from.")}</>
<>{tNode("It would go to {address}, which is not where the message came from.", { address: <strong className="notranslate" translate="no">{receipt.to!.email}</strong> })}</>
)}
</span>
<button
+25 -9
View File
@@ -1,8 +1,8 @@
import { useSettings } from "@/store/settings";
import { Switch, useIsTouch } from "@/ui/misc";
import { SWIPE_CHOICES, type SwipeAction } from "@/lib/swipe";
import { UI_LANGUAGES } from "@/lib/languages";
import { t as translate } from "@/lib/i18n";
import { TRANSLATION_ISSUE_URL, UI_LANGUAGES } from "@/lib/languages";
import { t as translate, tNode } from "@/lib/i18n";
/**
* The theme cards, each previewing the background it actually paints. Kept as
@@ -31,6 +31,8 @@ export function AppearanceSettings() {
const s = useSettings((st) => st.settings);
const update = useSettings((st) => st.update);
const isTouch = useIsTouch();
const chosen = UI_LANGUAGES.find((l) => l.tag === s.uiLanguage);
const betaChosen = Boolean(chosen?.beta);
return (
<div>
<h1>{translate("Appearance")}</h1>
@@ -40,12 +42,12 @@ export function AppearanceSettings() {
{THEMES.map((t) => (
<button key={t.id} className={`theme-card ${s.theme === t.id ? "active" : ""}`} onClick={() => update({ theme: t.id })}>
<div className="preview" style={{ background: t.preview }} />
{t.label}
{translate(t.label)}
</button>
))}
</div>
<p className="hint" style={{ marginTop: 10 }}>
<strong>{translate("ihasmail")}</strong> {translate("is the palette from")} <a href="https://ihasmail.org" target="_blank" rel="noopener noreferrer">{translate("ihasmail.org")}</a>{translate(", and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.")}
{tNode("{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.", { name: <strong className="notranslate" translate="no">ihasmail</strong>, site: <a href="https://ihasmail.org" target="_blank" rel="noopener noreferrer">ihasmail.org</a> })}
</p>
<Switch
checked={s.themeMessageBody}
@@ -84,7 +86,7 @@ export function AppearanceSettings() {
<label htmlFor="ui-language">{translate("Interface language")}</label>
<select id="ui-language" className="select" value={s.uiLanguage} onChange={(e) => update({ uiLanguage: e.target.value })}>
{UI_LANGUAGES.map((l) => (
<option key={l.tag} value={l.tag}>{l.name}</option>
<option key={l.tag} value={l.tag}>{l.beta ? `${l.name} (Beta)` : l.name}</option>
))}
</select>
</div>
@@ -93,13 +95,27 @@ export function AppearanceSettings() {
looks broken; a picker with one entry and a sentence explaining that
more are coming is a roadmap.
*/}
{/*
Said plainly rather than buried. A machine translation presented as a
finished one is the version of this that does harm: a reader told it was
unchecked forgives an odd sentence and reports it, while a reader told
it was reviewed reasonably concludes the product is sloppy. The report
link is the entire review process, so it belongs one click from the
thing being complained about.
*/}
{betaChosen && (
<p className="hint">
{tNode("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}.", {
report: <a href={`${TRANSLATION_ISSUE_URL}${encodeURIComponent(chosen?.name ?? "")}`} target="_blank" rel="noopener noreferrer">{translate("tell us about it")}</a>,
})}
</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.")}
</p>
<p className="hint">
{translate("This is separate from")} <strong>{translate("Language & region")}</strong> {translate("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.")}
{tNode("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: <strong>{translate("Language & region")}</strong> })}
</p>
<h2>{translate("Swiping")}</h2>
@@ -112,7 +128,7 @@ export function AppearanceSettings() {
<label htmlFor="swipe-right">{translate("Swipe right")}</label>
<select id="swipe-right" className="select" value={s.swipeRight} onChange={(e) => update({ swipeRight: e.target.value as SwipeAction })}>
{SWIPE_CHOICES.map((c) => (
<option key={c.value} value={c.value}>{c.label}</option>
<option key={c.value} value={c.value}>{translate(c.label)}</option>
))}
</select>
</div>
@@ -120,7 +136,7 @@ export function AppearanceSettings() {
<label htmlFor="swipe-left">{translate("Swipe left")}</label>
<select id="swipe-left" className="select" value={s.swipeLeft} onChange={(e) => update({ swipeLeft: e.target.value as SwipeAction })}>
{SWIPE_CHOICES.map((c) => (
<option key={c.value} value={c.value}>{c.label}</option>
<option key={c.value} value={c.value}>{translate(c.label)}</option>
))}
</select>
</div>
+1 -1
View File
@@ -159,7 +159,7 @@ export function GeneralSettings() {
<select className="select" value={s.dateFormat} onChange={(e) => update({ dateFormat: e.target.value as DateFormat })}>
{DATE_FORMATS.map((f) => (
<option key={f.value} value={f.value}>
{f.label} ({withPrefs({ locale: s.locale, dateFormat: f.value }, () => formatDate(SAMPLE))})
{t(f.label)} ({withPrefs({ locale: s.locale, dateFormat: f.value }, () => formatDate(SAMPLE))})
</option>
))}
</select>
+2 -2
View File
@@ -3,7 +3,7 @@ import { Plus, Trash2 } from "lucide-react";
import { useSettings } from "@/store/settings";
import { CALENDAR_COLORS, ColorSwatches } from "@/ui/misc";
import { promptDialog } from "@/ui/dialog";
import { t } from "@/lib/i18n";
import { t, tNode } from "@/lib/i18n";
export function LabelsSettings() {
const labels = useSettings((s) => s.settings.labels);
@@ -39,7 +39,7 @@ export function LabelsSettings() {
</div>
))}
<button className="btn" onClick={() => void add()}><Plus size={16} /> {t("New label")}</button>
<p className="hint mt-8">{t("Tip: press")} <kbd className="kbd">l</kbd> {t("on a conversation to apply labels. Search with")} <code>label:name</code>.</p>
<p className="hint mt-8">{tNode("Tip: press {key} on a conversation to apply labels. Search with {operator}.", { key: <kbd className="kbd">l</kbd>, operator: <code>label:name</code> })}</p>
</div>
);
}
+3 -3
View File
@@ -5,7 +5,7 @@ import { useSession } from "@/store/session";
import { formatFullDate } from "@/lib/format";
import { toast } from "@/ui/toast";
import { confirmDialog, Dialog } from "@/ui/dialog";
import { t } from "@/lib/i18n";
import { t, tNode } from "@/lib/i18n";
interface SessionRow {
id: string;
@@ -59,7 +59,7 @@ export function SecuritySettings() {
return (
<div>
<h1>{t("Security & sessions")}</h1>
<p className="lead">{t("You're signed in as")} <b>{session?.username}</b>{t(". Your password is never stored in the browser; the server keeps it encrypted per-session for talking to Stalwart.")}</p>
<p className="lead">{tNode("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: <b className="notranslate" translate="no">{session?.username}</b> })}</p>
<h2>{t("Password")}</h2>
{unsupported ? (
@@ -303,7 +303,7 @@ function AppPasswords({ state, reload }: { state: SecurityState | null; reload:
footer={<button className="btn btn-primary" onClick={() => setIssued(null)}>{t("Done")}</button>}>
{issued && (
<div>
<p>{t("Copy it into")} <b>{issued.description}</b> {t("now — it isn't shown again.")}</p>
<p>{tNode("Copy it into {name} now — it isn't shown again.", { name: <b>{issued.description}</b> })}</p>
<CopyableSecret value={issued.secret} />
<p className="hint mt-8"><Smartphone size={13} style={{ verticalAlign: "-2px" }} /> {t("Use your usual address as the username.")}</p>
</div>
+1 -1
View File
@@ -44,7 +44,7 @@ export function SettingsView({ section }: { section?: string }) {
{SECTIONS.map((s) => (
<Link key={s.id} href={`/settings/${s.id}`} className={`nav-item ${section === s.id ? "active" : ""}`}>
{s.icon}
<span className="nav-label">{s.label}</span>
<span className="nav-label">{t(s.label)}</span>
</Link>
))}
<div className="nav-section" style={{ paddingLeft: 8 }}><span>{t("Shortcuts")}</span></div>
+1 -1
View File
@@ -142,7 +142,7 @@ export function ShareDialog({ kind, id, name, shareWith, onClose }: { kind: Kind
{RIGHTS[kind].map((rt) => (
<label key={rt.key} className="check" style={{ padding: "2px 6px" }}>
<input type="checkbox" checked={Boolean(r[rt.key])} onChange={(e) => setRights({ ...rights, [pid]: { ...r, [rt.key]: e.target.checked } })} />
<span className="small">{rt.label}</span>
<span className="small">{t(rt.label)}</span>
</label>
))}
</div>
+2 -2
View File
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { keyboard } from "@/lib/keyboard";
import { Kbd } from "@/ui/misc";
import { t } from "@/lib/i18n";
import { t, tNode } from "@/lib/i18n";
export function ShortcutsSettings() {
const list = useMemo(() => keyboard.list(), []);
@@ -17,7 +17,7 @@ export function ShortcutsSettings() {
return (
<div>
<h1>{t("Keyboard shortcuts")}</h1>
<p className="lead">{t("Gmail-style shortcuts are always on. Press")} <kbd className="kbd">?</kbd> {t("anywhere to see this list.")}</p>
<p className="lead">{tNode("Gmail-style shortcuts are always on. Press {key} anywhere to see this list.", { key: <kbd className="kbd">?</kbd> })}</p>
<div className="shortcut-grid">
{groups.map(([group, items]) => (
<div key={group}>