Productivity features, UI translations, local email translation
- Snooze (with a Snoozed folder), keyboard shortcuts with a Settings section, mail templates (Compose > More > Templates), a Subscriptions folder with bulk unsubscribe/delete, and tracker blocking with a Privacy toggle (on by default) - Own dialogs instead of browser popups; emoji picker in compose; compose windows survive a visit to Settings - Sentence completion learned from the user's sent mail (no AI) - Interface in English and Polish (one file per language, English as the fallback), picked from the browser on first run, then kept - Email translation on the server via LibreTranslate: Translate button and "Translate to...", messages in several languages translated part by part, names/addresses/emoji/file names/phone numbers kept, an animated line while translating - Antispam "Mark as spam" loads with the message, not when the menu opens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+169
-3
@@ -1,6 +1,9 @@
|
||||
// English message catalog. Adding a locale = drop a sibling file with the same
|
||||
// keys and register it in i18n/index.tsx. Keys are flat, dotted namespaces.
|
||||
// Interpolate with {name} placeholders.
|
||||
// English message catalog — the main language and the reference: other
|
||||
// language files (pl.ts, …) use these keys, and any string a language lacks is
|
||||
// shown in English. Adding a language = a sibling file typed as `Catalog`,
|
||||
// registered in i18n/index.tsx.
|
||||
// Keys are flat, dotted namespaces. Interpolate with {name} placeholders;
|
||||
// counted strings take plural forms as "<key>_one", "<key>_few", … (index.tsx).
|
||||
|
||||
export const en = {
|
||||
"common.appName": "Ultimo Email",
|
||||
@@ -79,12 +82,74 @@ export const en = {
|
||||
"nav.sent": "Sent",
|
||||
"nav.spam": "Spam",
|
||||
"nav.trash": "Trash",
|
||||
"nav.snoozed": "Snoozed",
|
||||
"nav.subscriptions": "Subscriptions",
|
||||
"snooze.action": "Snooze",
|
||||
"snooze.unsnooze": "Unsnooze",
|
||||
"snooze.until": "Snooze until…",
|
||||
"snooze.laterToday": "Later today",
|
||||
"snooze.tomorrow": "Tomorrow",
|
||||
"snooze.weekend": "This weekend",
|
||||
"snooze.nextWeek": "Next week",
|
||||
"snooze.pick": "Pick date & time",
|
||||
"snooze.done": "Snoozed until {when}",
|
||||
"snooze.failed": "The conversation could not be snoozed.",
|
||||
"snooze.banner": "Snoozed until {when}",
|
||||
"common.undo": "Undo",
|
||||
"subs.unsubscribe": "Unsubscribe",
|
||||
"subs.unsubscribed": "Unsubscribed",
|
||||
"subs.stillSending": "Still sending",
|
||||
"subs.stillSendingHint": "Mail keeps arriving although you unsubscribed. Try again, or delete their mail and block the sender with a filter.",
|
||||
"subs.unsubscribedOn": "You unsubscribed on {date}.",
|
||||
"subs.trashAll": "Delete all",
|
||||
"subs.trashAllAction": "Move to Trash",
|
||||
"subs.email": "{count} email",
|
||||
"subs.emails": "{count} emails",
|
||||
"subs.perMonth": "{count}/month",
|
||||
"subs.unread": "{count} unread",
|
||||
"subs.noSubject": "(no subject)",
|
||||
"subs.list": "List: {id}",
|
||||
"subs.scanning": "Looking through your mail for subscriptions… {count} messages to go",
|
||||
"subs.none": "No subscriptions found",
|
||||
"subs.noneHint": "Newsletters and other mail you can unsubscribe from show up here, grouped by sender.",
|
||||
"subs.confirm": "Unsubscribe from {name}?",
|
||||
"subs.confirmOneClick": "Ultimo will ask the sender to take you off their list.",
|
||||
"subs.confirmMailto": "Ultimo will send an unsubscribe email to the sender from your account.",
|
||||
"subs.confirmWeb": "The sender's unsubscribe page will open. Finish there.",
|
||||
"subs.doneOneClick": "Unsubscribed from {name}",
|
||||
"subs.doneMailto": "Unsubscribe email to {name} is on its way",
|
||||
"subs.doneWeb": "Finish unsubscribing on the sender's page",
|
||||
"subs.failed": "Could not unsubscribe. Try again later.",
|
||||
"subs.confirmTrash": "Move all mail from {name} to Trash? Conversations you replied to stay where they are.",
|
||||
"subs.trashed": "{count} conversations moved to Trash",
|
||||
"subs.statTotal": "Emails",
|
||||
"subs.statPerMonth": "Per month",
|
||||
"subs.statRead": "Opened",
|
||||
"subs.statSince": "Since",
|
||||
"subs.recent": "Recent mail",
|
||||
"subs.senders": "{count} senders",
|
||||
"subs.select": "Select sender",
|
||||
"subs.selected": "{count} selected",
|
||||
"subs.sendersSelected": "{count} senders selected",
|
||||
"subs.bulkConfirm": "Unsubscribe from {count} senders?",
|
||||
"subs.bulkHow": "Ultimo asks each sender to take you off their list, or sends them an unsubscribe email from your account. Senders that only take it on their own web page are listed for you to open afterwards.",
|
||||
"subs.bulkNothing": "You're already unsubscribed from these senders.",
|
||||
"subs.bulkDone": "Unsubscribed from {count}",
|
||||
"subs.bulkPages": "{count} to finish on their page",
|
||||
"subs.bulkFailed": "{count} failed",
|
||||
"subs.bulkTrashConfirm": "Move all mail from {count} senders to Trash? Conversations you replied to stay where they are.",
|
||||
"subs.finishTitle": "Finish on the senders' pages",
|
||||
"subs.finishHint": "These senders only take an unsubscribe on their own web page. Open each one and confirm there.",
|
||||
"subs.openPage": "Open page",
|
||||
"subs.pageOpened": "Opened",
|
||||
"subs.finishDone": "Done",
|
||||
"nav.search": "Search mail…",
|
||||
"nav.searchInTrash": "Search in Trash",
|
||||
"nav.searchInSpam": "Search in Spam",
|
||||
"search.save": "Save this search",
|
||||
"search.saved": "Saved searches",
|
||||
"search.saveNamePrompt": "Name this saved search:",
|
||||
"search.saveNameLabel": "Name",
|
||||
"nav.storageQuota": "Storage used",
|
||||
"nav.refresh": "Refresh all folders",
|
||||
|
||||
@@ -187,10 +252,14 @@ export const en = {
|
||||
"panel.noAttachments": "No attachments",
|
||||
"panel.download": "Download",
|
||||
"mail.emptyConfirm": "Permanently delete everything in this folder? This can't be undone.",
|
||||
"mail.emptyAction": "Delete everything",
|
||||
"mail.movedToTrash": "Moved to Trash",
|
||||
"mail.loadFailed": "Couldn't load this message.",
|
||||
"mail.imagesBlocked": "Images from this sender are blocked.",
|
||||
"mail.loadImages": "Load images",
|
||||
"mail.trackerBlocked": "1 tracker blocked",
|
||||
"mail.trackersBlocked": "{count} trackers blocked",
|
||||
"mail.trackersHint": "Invisible images that tell the sender when and where you opened the mail were removed.",
|
||||
"mail.alwaysFromSender": "Always from this sender",
|
||||
"mail.to": "to",
|
||||
"mail.from": "From",
|
||||
@@ -256,6 +325,24 @@ export const en = {
|
||||
"compose.fmtHighlight": "Highlight color",
|
||||
"compose.fmtImage": "Insert image",
|
||||
"compose.insertSignature": "Insert signature",
|
||||
"compose.fmtEmoji": "Emoji",
|
||||
"compose.emojiLoading": "Loading emoji…",
|
||||
"compose.templates": "Templates",
|
||||
"compose.saveAsTemplate": "Save as template…",
|
||||
"compose.noTemplates": "No saved templates yet",
|
||||
"compose.deleteTemplate": "Delete template",
|
||||
"compose.templateName": "Template name",
|
||||
"compose.templateEmpty": "Write something first: the template is what you've written, without the signature or quoted mail.",
|
||||
"compose.templateDelete": "Delete the template \"{name}\"?",
|
||||
"compose.templateSaveFailed": "The template could not be saved.",
|
||||
"compose.saveTemplateTitle": "Save as template",
|
||||
"compose.saveTemplateHint": "The subject and what you've written are saved. The signature and any quoted mail are left out.",
|
||||
"compose.templateNamePlaceholder": "e.g. Thanks for your enquiry",
|
||||
"compose.templateReplaceNote": "A template with this name exists. Saving replaces it.",
|
||||
"compose.templateReplaceAction": "Replace",
|
||||
"compose.templateSaving": "Saving…",
|
||||
"compose.templateEmptyShort": "Write something first",
|
||||
"compose.deleteTemplateAction": "Delete",
|
||||
"compose.linkPrompt": "Link URL:",
|
||||
"compose.linkOpen": "Open the link:",
|
||||
"compose.linkEdit": "Edit",
|
||||
@@ -498,6 +585,40 @@ export const en = {
|
||||
"settings.addToCalendar": "Add to calendar",
|
||||
"settings.alsoFile": "Also mark the message as read and file it",
|
||||
"settings.integrations": "Integrations",
|
||||
"settings.shortcuts": "Keyboard shortcuts",
|
||||
"settings.privacy": "Privacy",
|
||||
"settings.completion": "Suggest sentence endings",
|
||||
"settings.completionHint": "While you write, the likely rest of the sentence appears in grey; press Tab to take it. Suggestions come from phrases you use in your own sent mail (plus common ones in English, Polish and German) and never leave this server.",
|
||||
"settings.blockTrackers": "Block email trackers",
|
||||
"settings.blockTrackersHint": "Many newsletters and sales emails hide a tiny invisible image that tells the sender when you opened the message, how often, and roughly where you were. Ultimo removes these images before showing a message. Turned off, they are treated like any other remote image: loaded only when you allow images from that sender.",
|
||||
"settings.shortcutsEnabled": "Use keyboard shortcuts",
|
||||
"settings.shortcutsHint": "Single keys act on the mail list and the open conversation. They are ignored while you type. Press ? in the mail view to see this list.",
|
||||
"shortcuts.title": "Keyboard shortcuts",
|
||||
"shortcuts.then": "then",
|
||||
"shortcuts.groupNavigation": "Navigation",
|
||||
"shortcuts.groupConversation": "Conversation",
|
||||
"shortcuts.groupCompose": "Compose",
|
||||
"shortcuts.next": "Next conversation",
|
||||
"shortcuts.prev": "Previous conversation",
|
||||
"shortcuts.back": "Back to the list",
|
||||
"shortcuts.search": "Search",
|
||||
"shortcuts.goInbox": "Go to Inbox",
|
||||
"shortcuts.goSnoozed": "Go to Snoozed",
|
||||
"shortcuts.goDrafts": "Go to Drafts",
|
||||
"shortcuts.goSent": "Go to Sent",
|
||||
"shortcuts.goSubscriptions": "Go to Subscriptions",
|
||||
"shortcuts.help": "Show keyboard shortcuts",
|
||||
"shortcuts.reply": "Reply",
|
||||
"shortcuts.replyAll": "Reply all",
|
||||
"shortcuts.forward": "Forward",
|
||||
"shortcuts.archive": "Archive",
|
||||
"shortcuts.trash": "Delete",
|
||||
"shortcuts.pin": "Pin / unpin",
|
||||
"shortcuts.snooze": "Snooze",
|
||||
"shortcuts.markRead": "Mark as read",
|
||||
"shortcuts.markUnread": "Mark as unread",
|
||||
"shortcuts.compose": "New message",
|
||||
"shortcuts.escape": "Minimize the compose windows, close a dialog",
|
||||
|
||||
"admin.title": "Ultimo Email administration",
|
||||
"admin.signInTitle": "Administrator sign-in",
|
||||
@@ -530,6 +651,7 @@ export const en = {
|
||||
"admin.inviteDownload": "Download template",
|
||||
"admin.inviteReset": "Back to the built-in template",
|
||||
"admin.inviteResetConfirm": "Remove your template and use the built-in one again?",
|
||||
"admin.inviteResetAction": "Use built-in template",
|
||||
"admin.invitePlaceholders": "Placeholders:",
|
||||
"admin.invitePlaceholdersHelp":
|
||||
"The template must contain {{invite_link}} or {{invite_button}}. The subject can use the names too. Images written into the HTML are sent inside the email.",
|
||||
@@ -595,6 +717,8 @@ export const en = {
|
||||
"settings.addAccount": "Add account",
|
||||
"settings.syncState": "State: {state}",
|
||||
"settings.confirmRemoveAccount": "Remove this account and all its mail?",
|
||||
"settings.removeAccountTitle": "Remove account",
|
||||
"settings.removeAccountAction": "Remove account",
|
||||
"settings.removingAccount": "Removing account…",
|
||||
"settings.editAccount": "Edit account",
|
||||
"settings.color": "Color",
|
||||
@@ -701,7 +825,49 @@ export const en = {
|
||||
"install.stepsFirefox": "1. Open Firefox's menu (⋮).\n2. Choose Add app to Home screen (or Install).",
|
||||
"install.hide": "Don't show this again",
|
||||
"install.gotIt": "Got it",
|
||||
"settings.language": "Language",
|
||||
"settings.languageHint": "The language of Ultimo's menus, buttons and messages. Saved with your account, so it stays the same on every device.",
|
||||
"time.justNow": "just now",
|
||||
"time.minAgo": "{count} min ago",
|
||||
"time.hoursAgo": "{count} hours ago",
|
||||
"time.hoursAgo_one": "1 hour ago",
|
||||
"time.daysAgo": "{count} days ago",
|
||||
"time.daysAgo_one": "1 day ago",
|
||||
"reminder.atTime": "At time of event",
|
||||
"reminder.minutesBefore": "{count} minutes before",
|
||||
"reminder.hoursBefore": "{count} hours before",
|
||||
"reminder.hoursBefore_one": "1 hour before",
|
||||
"reminder.daysBefore": "{count} days before",
|
||||
"reminder.daysBefore_one": "1 day before",
|
||||
"color.blue": "Blue",
|
||||
"color.green": "Green",
|
||||
"color.red": "Red",
|
||||
"color.orange": "Orange",
|
||||
"color.purple": "Purple",
|
||||
"color.teal": "Teal",
|
||||
"color.pink": "Pink",
|
||||
"color.gray": "Gray",
|
||||
"color.custom": "Custom",
|
||||
"settings.selectColor": "Select colour",
|
||||
"labels.remove": "Remove label",
|
||||
"compose.attribution": "On {date}, at {time}, {who} wrote:",
|
||||
"print.attachments": "Attachments",
|
||||
"print.popupBlocked": "Allow pop-ups for this site to print.",
|
||||
"mail.translate": "Translate",
|
||||
"mail.translateTo": "Translate to…",
|
||||
"mail.showOriginal": "Show original",
|
||||
"mail.translating": "Translating…",
|
||||
"mail.translated": "Translated",
|
||||
"mail.translatedFrom": "Translated from {language}",
|
||||
"mail.translatedFromMany": "Translated from {languages}",
|
||||
"mail.translateFailed": "Couldn't translate this message. The translation service may be busy or unavailable.",
|
||||
} as const;
|
||||
|
||||
export type MessageKey = keyof typeof en;
|
||||
export type Messages = Record<MessageKey, string>;
|
||||
/** Plural forms a language may add beside a key (Intl.PluralRules categories). */
|
||||
export type PluralKey = `${MessageKey}_${"zero" | "one" | "two" | "few" | "many" | "other"}`;
|
||||
/** A language file. English (this file) has every key; another language may
|
||||
* lack some — those show in English (i18n/index.tsx) — and adds the plural
|
||||
* forms it needs. */
|
||||
export type Catalog = Partial<Messages> & Partial<Record<PluralKey, string>>;
|
||||
|
||||
Reference in New Issue
Block a user