Compare commits

...
206 Commits
Author SHA1 Message Date
Linus Rath 53c0bc0fc2 chore: update version number to 0.1.63 2026-09-24 22:18:42 +02:00
Linus Rath a8e74c8b6d Merge branch 'main' of https://github.com/bulwarkmail/native 2026-09-24 22:18:06 +02:00
Linus Rath 949bb10ac5 docs: record the device run, the r8 and label fixes and the push crash it found 2026-09-24 22:16:46 +02:00
Linus Rath e5659ed5e8 ci: attach the r8 mapping to android releases
Release APKs are obfuscated since R8 was turned on, and nothing kept
the mapping that decodes their stack traces. The workflow now copies
`android/app/build/outputs/mapping/release/mapping.txt` to
`bulwark-mobile-<version>-<commit>-mapping.txt`, uploads it as a
workflow artifact and attaches it to the release next to the APK. The
in-app updater only picks `.apk` assets, so it ignores the new file.

If R8 is on (`android.enableMinifyInReleaseBuilds=true`) and no mapping
was written, the step fails instead of publishing an APK whose crashes
cannot be read. docs/android-release.md explains how to decode a trace
with `retrace`.
2026-09-24 22:14:35 +02:00
Linus Rath a764a07c65 fix: show a white splash in light mode
The splash background was #09090b in both modes, so in light mode a
cold start went from a near-black splash to the white first frame.
It is now #ffffff by day and #09090b at night (`values-night`), as
`splash.backgroundColor` and `splash.dark.backgroundColor` in
app.config.js already ask for, so a later `expo prebuild` writes the
same colours. Checked on the API 35 emulator: light mode stays white
from splash to login screen, dark mode is unchanged.
2026-09-24 22:14:35 +02:00
Linus Rath 5ddc2df64b fix: stop holding a wake lock for 30 seconds after every push
Every background push kept the app awake for the full 30 s timeout of
the BulwarkPushTask headless task, however quickly the task was done.
AppRegistry reports a finished task through the native
HeadlessJsTaskSupport module, and React Native 0.81 registers that
module only for the old bridge (CoreModulesPackage); the bridgeless
runtime's CoreReactPackage leaves it out. JS could never say it was
done, so the service, or the job, and its wake lock waited for the
timeout. BulwarkHeadlessJsTaskSupportModule provides the module under
the same name when the app runs bridgeless.

The push path also leaked a wake lock now and then. startHeadlessTask
called HeadlessJsTaskService.acquireWakeLockNow on the FCM worker
thread while the service took the same static lock on the main thread.
The method is not thread-safe, so both could create a lock, and the one
that was overwritten stayed held. The FCM thread now takes its own
wake lock, timed at 10 s, and the service drops it as soon as it holds
React Native's.

Measured on the API 35 emulator with the R8 release build:

- push handled by the job: 30,000 ms before, 75-155 ms now
- push handled by the service: the service ran 31 s before, it is
  gone within a second now
- `com.facebook.react.i` wake lock still held 5 s after a push: 2 of
  4 pushes before, 0 of 8 now
2026-09-24 22:14:35 +02:00
Linus Rath fef23e4c33 fix: stop a background push from crashing the app on android 12 and later
A push that arrived while the app was in the background started
BulwarkPushTaskService with `startService`. Android 8+ allows that only
while the app is temporarily allowlisted, which a high-priority FCM
message grants. The relay does send FCM data messages at HIGH priority,
but FCM delivers some at normal priority anyway (it downgrades apps
whose high-priority messages rarely end in a notification), and a
UnifiedPush distributor may not raise the app at all. The call then
threw `BackgroundServiceStartNotAllowedException` and the app crashed,
from the FCM thread or, for UnifiedPush, on the main thread.

The service start is kept as the first choice, so a high-priority push
works as before. When Android refuses it, the same headless task now
runs from a JobScheduler job (BulwarkPushJobService): expedited on
Android 12+, which runs at once while the app has expedited-job quota
and as a regular job after that, and a regular job on Android 8-11.
JobScheduler is a platform API, so nothing is added to the build;
WorkManager is not in the dependency tree. A generic "new mail"
notification built natively was the other option, but the relay's
payload cannot tell new mail from other changes and the native side
knows neither the notification settings nor what was already shown.

Checked on the API 35 emulator with the R8 release build, the process
killed and the push delivered past the allowlist:

- FCM data message: before, FATAL EXCEPTION on
  Firebase-Messaging-Intent-Handle; now the job is scheduled 18 ms
  after the refusal and runs the task
- UnifiedPush message (connector 3.3.5, sent the way a distributor
  does): before, FATAL EXCEPTION on main; now the same job path
- allowlisted FCM message: still starts BulwarkPushTaskService
2026-09-24 22:14:35 +02:00
Linus Rath 4a3bce154c feat: translate the new app strings and screen-reader labels into all 26 languages 2026-09-24 22:06:28 +02:00
Linus Rath 13a3e95a0a chore: drop unused app strings and retranslate the ones that became plurals 2026-09-24 21:45:29 +02:00
Linus Rath fd55e16492 fix: translate the last export errors, dates and participant counts
A sweep of src for what the earlier passes missed:
- The message export errors ("Sharing is not available on this
  device", "Download failed: 404") were English; they come from the
  catalog now.
- Event cards and details glued the participant count to a lowercased
  "Participants"; they use the webmail's participant plural.
- Contact activity dates ignored the app language.
- Imported filter rules from an unknown tool showed the parser's
  English "External" instead of the translated label.
- The contact field remove button and the vacation editor's
  bold/italic/underline/list buttons get screen reader labels.
2026-09-24 21:44:47 +02:00
Linus Rath 5e557266d1 fix: count the files selection and delete prompt with plurals
The files screen filled `files.selected_count` and
`files.delete_items_title` with its own `{name}` substitution, a
leftover from before t() took arguments, so they could not be
plurals. Both are ICU plurals now and every message on the screen gets
its values through t(). The upload progress line is one message
instead of "Uploading..." followed by the numbers.
2026-09-24 21:44:47 +02:00
Linus Rath b178ee8d07 fix: translate the tab bar and label the unified inbox and drawer buttons
The bottom tabs showed their route names ("Mail", "Calendar", …) in
every language; they now use the webmail's navigation names, and an
unavailable tab says so with one translated message instead of an
English "(unavailable)" suffix. The first loading screen is
translated as well.

The Unified Inbox selection toolbar, its back and clear-search buttons,
and the drawer's close, expand/collapse and remove-account buttons get
screen reader labels. Its unknown-sender fallback, selection count and
"account(s) could not be loaded" banner are proper translations and
plurals now.

The key harvest and the translation coverage test did not look at
App.tsx, which sits next to src/; they do now.
2026-09-24 21:44:47 +02:00
Linus Rath 1cbf763a36 fix: send calendar reminders in the app language
Reminder notifications are worded when they are scheduled, outside
React, and were always English ("Starts in 15 min", "(No title)",
"Task due"). They now come from the catalog through the locale store,
with ICU plurals for the minutes, hours and days. Each scheduled
reminder records its language, so switching languages rewords the
pending ones, and the Android channel ("Calendar reminders", shown in
the system settings) is named and renamed in the app language.

The reminder labels picked the English form whenever a catalog entry
was an ICU plural, which is all of them; they now pass the count to
t(), and "minutes after" is one plural message instead of a number
glued to a fragment. The import refusal error, the exported alarm
text and the "sharing is not available" error are translated too.
2026-09-24 21:44:46 +02:00
Linus Rath 84fef142b9 fix: translate the calendar sheets and label their buttons
- The custom recurrence editor and its summary were English throughout
  (labels, units, weekday and month names, "on the third Thursday");
  they now use the webmail's recurrence fragments and the app's date
  locale. The interval and occurrence counts are ICU plurals, also in
  the event details, which filled "Every {count} days" with
  `.replace`.
- The iCal subscription sheet is translated with the webmail's
  subscription strings, and its "last updated" time is relative in the
  app language. The feed errors it shows come from the catalog too.
- The calendar share sheet asked for `calendar.share.role_*` keys no
  catalog has; it now uses the webmail's sharing presets and title.
- Close, remove, refresh, unsubscribe, delete and new-task buttons get
  screen reader labels, the event editor's switches are named, color
  swatches read out their color, and task checkboxes and calendar rows
  report whether they are checked.

The calendar i18n test now covers the sheets.
2026-09-24 21:44:46 +02:00
Linus Rath b3a82f1241 fix: translate the week view and label the calendar toolbar
The week view's "all-day" label and its "Untitled" fallback were
hard-coded; they now use the catalog's "All day" and "(No title)" like
the scrolling time grid. The calendar's menu, tasks, view switch, new
event and previous/next buttons get screen reader labels (the view
buttons also say which one is selected), and a calendar i18n test
covers the calendar views.
2026-09-24 21:44:46 +02:00
Linus Rath 13cba64854 fix: translate the viewer's leftovers and label its icon buttons
The message details panel built its received line, attachment summary
and spam score by gluing strings together; each is now one ICU message
(the attachment count is a proper plural). The SPF/DKIM/DMARC results
use the webmail's translated result names, and an invitation's date is
formatted in the app language instead of always in English.

The quick reply box wrote its own "date, name <email>:" quote header;
it now uses the composer's translated "On {date}, {from} wrote:" line
so both kinds of reply look the same.

The back button, the subject and header star buttons, the sender
avatar and the More/Tag sheet close buttons get screen reader labels,
and the screens i18n test covers the viewer files.
2026-09-24 21:44:46 +02:00
Linus Rath e3501f939d fix: offer undo for replies sent from the viewer
A quick reply held by the undo-send delay could not be undone: the
undo bar lived only on the mail list, and the quick reply never
recorded the held send for it.

- The undo bar is now hosted on every stack screen through the
  navigator's `screenLayout`, next to the toasts, and only the focused
  screen's renders, so it shows wherever the send started and never
  twice. The list's own instance is gone; archive, delete, move and
  spam undo use the same bar as before. The composer gets none, since
  its Undo reopens the composer.
- The quick reply records a held send through the same store action as
  the composer, including the account it went out of, so Undo and
  Send now reach a shared account's submission and Undo restores the
  reply into that account's Drafts.
- The quick reply dismisses the keyboard after sending, which would
  otherwise cover the bar.
2026-09-24 21:16:59 +02:00
Linus Rath 5e3cbf2419 fix: shrink the android release with r8
Release builds had R8 and resource shrinking off, so every class of
every dependency went into the APK unminified: the published 0.1.62 is
42.8 MB for arm64, 25.85 MB of it dex (PF9 in the audit).

Turn both on through the properties build.gradle already reads
(`android.enableMinifyInReleaseBuilds` and
`android.enableShrinkResourcesInReleaseBuilds`). The consumer rules of
React Native, Expo, Firebase, ML Kit and the other libraries cover what
they load by reflection. The app's own package is kept whole, which
costs a few KB and keeps its frames readable in an obfuscated stack
trace.

Measured on a local x86_64 release build:

- APK: 44.7 MB -> 38.0 MB (download size 23.8 MB -> 17.2 MB)
- dex: 30.4 MB in 4 files -> 11.3 MB in 2 files
- classes: 27,631 -> 16,724

With `-dontobfuscate` the APK would be 38.8 MB (13.5 MB of dex), so
obfuscation stays on. Library frames in a release stack trace are now
obfuscated; retrace them with that build's
`android/app/build/outputs/mapping/release/mapping.txt`.

Checked on the emulator: cold start in light and dark mode, German and
Russian strings, SENDTO and VIEW mailto and SEND share intents, every
sign-in screen, the QR scanner with ML Kit, and an FCM data message
starting the headless push task.
2026-09-24 21:13:42 +02:00
Linus Rath 9c031c8761 docs: update the fix-pass notes for the labels and strings that landed since 2026-09-24 21:02:09 +02:00
Linus Rath c81dc5179f docs: tick the parity items this pass completed
Tick the parity items the fix pass finished (server-side recurrence,
filters and vacation for shared accounts, in-app file preview, the
trusted-senders toggle, UnifiedPush, sender taps in the viewer), and
note on the already ticked ones what this pass changed, such as the tag
view and search reaching shared accounts or contact photos as avatars.
Items that only moved forward say what is left.

Fix hints and "verified" lines that named removed functions
(`setEmailKeywords`, `getEmailDetail`, `fetchEmailsChunked`,
`buildRecurrenceOverridePatch`) or the old whole-map keyword writes now
point at what replaced them.

PARITY_CHECKLIST.md counts done and open items per area: 379 of 415 are
done, 36 open.
2026-09-24 21:02:09 +02:00
Linus Rath d72aab2519 docs: correct the stale parity checklist entries
Apply the audit's "Stale parity checklist" section to docs/parity:
- ticked items the audit found broken keep their tick and name the
  commit that fixed them; the date-locale item is unticked again, since
  there is still no `dateLocale` setting
- the items that were done but unticked are ticked with where they
  landed
- natural-language event input, print contact and the Sieve editor
  gutter move to the N/A lists
- the text about linking, push types, RTL, `useTypography`, the Sieve
  "Keep" and `FileNode/get ids:null` now describes main
2026-09-24 21:02:09 +02:00
Linus Rath d4ac1e1df4 docs: record what still needs a device check and a decision after the fix pass
The audit gains a "Fix pass, 2026-09-24" section after the summary. It
says how the pass was done and tested, and lists what the fix agents
could not settle from the code alone:
- the device and server checks that matter before a release
- the product calls, starting with the release signing key and secrets
  that have to exist before the next version bump
- the follow-ups the agents left, one line each
2026-09-24 21:02:08 +02:00
Linus Rath 06ce86e31a docs: tick the audit items fixed in this pass
Every audit item a commit on main fixed is ticked with the commit that
fixed it, including the "fix these ten first" list and the stale parity
entries that the fixes resolved. Items fixed only in part keep their box
and say what is left; U8, PF9, B2 and the product calls stay open.
2026-09-24 21:02:08 +02:00
Linus Rath 6fe907b4bc fix: translate the Themes and Updates settings tab names 2026-09-24 20:49:56 +02:00
Linus Rath c1e5cd6c8b fix: label the remaining settings icon buttons for screen readers
The filter rule reorder, delete and remove-condition/action buttons,
the filter, Sieve and folder editor close buttons and the attachment
keyword remove buttons had no label. The Sieve editor's line count now
uses an ICU plural, and the settings i18n test covers every pane.
2026-09-24 20:49:56 +02:00
Linus Rath 455608f9f0 fix: translate push setup errors and the open-link prompt
The Notifications pane shows a push setup failure as "<phase>: <message>"
with a translated phase but an English message, and the open-link
confirmation and the new-mail notification title were English too.
They now go through the locale store's t() (translate() in the
background task), with the technical detail passed as an argument.
2026-09-24 20:49:55 +02:00
Linus Rath 5801d6c97a fix: show a translated notice when the sign-in session expired
The sign-in screen showed the auth store's raw English error ("Session
expired", "Invalid username or password") as the notice title. It now
runs it through describeLoginError like every other sign-in failure,
which also learns to explain an expired session with the webmail's
login.session_expired copy. The back and close buttons of the sign-in
steps get screen reader labels, and the email placeholders use the
webmail's translated example address.
2026-09-24 20:49:55 +02:00
Linus Rath 771558d8c7 fix: translate the composer's leftovers and label its header buttons
Group recipient chips, the group size in suggestions, the upload
progress and the upload/send failure fallbacks were English or glued
together from a translated word and a number; they now use ICU
messages. The close, attach, schedule and sender-options buttons, the
recipient and attachment remove buttons and the From override switch
get screen reader labels (audit U8).
2026-09-24 20:49:55 +02:00
Linus Rath 9c0c35c7ba fix: translate the mail list and label its icon buttons
The filter sheet, the .eml import result, the selection count, the
folder totals and the offline sync banner were English literals or
built by gluing translated words to numbers. They now use t() with ICU
arguments and plurals; filter chips go through a "{field}: {value}"
message so translators control the order. The invitation trust
warnings used keys that no catalog had, so they are now literal t()
calls and harvested.

For screen readers (audit U8) the hamburger, select-all checkbox,
filter, sort and compose buttons, the selection toolbar, search and
chip clear buttons, and the close buttons of the move, tag, template,
identity, QR and action sheets get translated labels and roles.
2026-09-24 20:49:55 +02:00
Linus Rath ccdc4dee56 fix: add the template settings strings to the catalog
TemplateSettings calls the translate function `tr` because `t` is a
template variable there, and both the key harvester and the coverage
test only looked for `t('...')`. Ten of its keys were never added to
locales/rn/en.json, so they could not be translated and the test did
not notice. Both now also match `tr('...')`, and the missing keys are
added.
2026-09-24 20:49:55 +02:00
Linus Rath aab29da148 fix: translate the file sharing roles and label the files toolbar
The share sheet asked for files.share_role_* keys that no catalog has,
so Viewer, Editor and Manager stayed English everywhere; it now uses
the webmail's sharing.preset names and its "Share \"{name}\"" title.
The per-file menu, back and clear-selection buttons get screen reader
labels, and the "sharing is not available" error is translated.
2026-09-24 20:49:55 +02:00
Linus Rath d59c571d93 fix: translate the contacts screens
The contact list, detail and edit screens, the group screen, the
import, picker and tag sheets and the contacts drawer were written in
English, including the activity panel ("Unknown", "Upcoming events"),
alert titles and every form label and option. They now go through
t(), reusing the webmail's contacts.* keys where the meaning matches,
with ICU plurals for member, contact and import counts instead of
`contact${n === 1 ? '' : 's'}`. JSContact context, phone feature,
personal-info and gender values on the detail screen are shown with
their translated labels instead of the raw keys.

Icon-only buttons on these screens (back, edit, more, bulk actions,
remove, close, clear search) get screen reader labels, and chips,
checkboxes and collapsible sections report their state.
2026-09-24 20:49:55 +02:00
Linus Rath cb5c152cb3 fix: name the settings switches for screen readers
A settings switch had a role and a checked state but no label, so
TalkBack only announced "switch, off". A switch inside a SettingItem now
takes that row's label, and the switches outside one (filter rules,
expanded view, template and sidebar app editors, filter rule options)
get their visible label passed explicitly.
2026-09-24 20:49:55 +02:00
Linus Rath 6e6fd95e5e fix: translate the contacts and content settings
The Contacts and Content & Senders panes, the swipe behaviour row in
Layout, the device time zone option in Calendar and the identity editor
placeholders were written as English literals, so they stayed English
in every language. They now go through t(), reusing the webmail's
settings.contacts, contacts.address_books and
settings.email_behavior.trusted_senders keys where the wording matches.
Counts use ICU plurals instead of `contact${n === 1 ? '' : 's'}`, and the
address book icon buttons get screen reader labels.

The settings i18n test now parses each pane with the TypeScript compiler
and also flags JSX text, alerts and accessibility labels, and covers
the newly cleaned panes.
2026-09-24 20:49:55 +02:00
Linus Rath 8af163a6a3 fix: show today and the now-line in the calendar time zone
The calendar placed events by the calendar time-zone setting, but
"today", the now-line and the "Today" button still went by the device
clock. With the setting away from the device zone, the grid could
mark yesterday as today, draw the now-line hours off and jump to the
wrong day.

- the views, the agenda's "Today"/"Tomorrow" headers and today anchor,
  the selected day, the scroll window's start and the "Today" button
  use `displayNow`, `isDisplayToday`, `isDisplayTomorrow` and
  `displayNowMinutes`
- a long press at an hour already hands the editor a calendar-zone
  time, which it converts when it saves
- changing the setting re-buckets the days and reloads the loaded
  range, since the server reads floating times and the range bounds
  in that zone
2026-09-24 20:40:51 +02:00
Linus Rath b7f711050e fix: translate the calendar import sheet 2026-09-24 20:34:35 +02:00
Linus Rath 9282ee6d72 fix: open the event a calendar link points at 2026-09-24 20:34:35 +02:00
Linus Rath a8e6f6e8c4 fix: keep event and task times when the calendar time zone is not the device's
With the calendar time-zone setting away from the device zone, the
event and task editors showed device-local times but saved them
labelled with the calendar zone, so opening an event and saving it
unchanged moved it by the difference between the two zones.

The calendar now works in display dates like the webmail
(`toDisplayDate` / `fromDisplayDate`): Dates whose local fields read
as the wall clock in the calendar zone. They are the identity while
the calendar follows the device.

- `getEventStartDate` / `getEventEndDate` return display dates, so the
  views, the detail sheet and the editor show calendar-zone times
- the editors convert back through `eventTimeFieldsToSave` and
  `buildTaskEditorChanges`: a start, end or due the user left alone is
  saved exactly as stored, a changed one is converted into the zone the
  event or task lives in (new, floating and formerly all-day ones take
  the calendar zone), and durations count whole days as calendar days
  in that zone
- `getTaskDueDate` stays the real instant for reminders; the tasks
  sheet uses `getTaskDueDisplayDate`, `displayNow` and
  `isDisplayToday`
- wall clocks are parsed from their digits, so one inside the device's
  own DST gap still converts right
- duplicating an event keeps its wall clock in its own zone instead of
  reading the device time
2026-09-24 20:34:35 +02:00
Linus Rath b7a5953255 fix: ask for both neighbouring conversations in one request
When the pages beside the open message were let go, each asked for its
conversation on its own, so every open sent two `Thread/get` requests
for its neighbours.

Conversations asked for in the same tick for the same account now go
out together: `getThreadsHeaders` takes several thread ids in one
back-referenced Thread/get → Email/get, and the cache batches its
callers into it. A conversation the server does not know, or a failed
request, still falls back to the copy held from before.

`getThreadEmails`, which downloaded every member's full body, had no
callers left; it is removed and its tests moved to `getFullEmails` and
`getThreadsHeaders`.
2026-09-24 20:29:41 +02:00
Linus Rath 0cee422373 fix: stop reloading an invitation when the message is marked read
The invitation banner loaded the .ics whenever it was handed a new
message object, so marking an invitation read or starring it
downloaded and parsed the calendar part again (and fetched its raw
text when the method had to be read from it).

It now loads once per message, account and calendar part
(`calendarInvitationKey`), and reads the current message only when the
load runs.
2026-09-24 20:29:41 +02:00
Linus Rath 644443193c fix: drop the mail the viewer held in memory on sign-out
The viewer's message cache, the rendered documents of their bodies and
their measured heights stayed in memory after signing out or removing
an account. Their keys name the server and login, so no other account
saw them, but a signed-out user's mail should not stay around.

Signing out (one account or all) and removing an account now clear all
three. A load still in flight when the cache is cleared no longer
stores what it brings back.
2026-09-24 20:29:41 +02:00
Linus Rath d2066dad07 fix: act on the exact row picked in "All folders" and tag lists #1082
An "All folders" search or a tag view holds rows of the own and the
shared accounts, and Stalwart hands out ids per account, so an own and a
team message can share an id. The list still named its rows by bare id:
selecting one of such a pair selected both, a batch action hit both, and
a swipe or single-row action took whichever came first, in the other
account.

The list now names every row by `rowKeyOf` (its id, scoped by its
account in a list spanning accounts; the bare id in a folder, so nothing
changes there) end to end:
- FlatList keys, row presses, swipes, selection and select-all
- thread expansion of a selection (`expandThreadSelection`)
- every store action the list calls: read, unread, star, pin, the
  per-account keyword batch, move, archive, delete and spam, and the
  rows they drop or keep in view
- the viewer's pages, which now come only from the rows of the opened
  message's account (`listRowsOfAccount`), and its mark-read, which
  goes through the store when the list holds that account's row

A key whose row is no longer loaded is ignored rather than guessed onto
the open folder's account. The unified inbox already keys its rows by
account and needed no change.
2026-09-24 20:28:43 +02:00
Linus Rath dd0969d48b chore: drop translations of app strings the calendar agenda no longer uses 2026-09-24 20:27:58 +02:00
Linus Rath 96bffa51e8 refactor: check only the days a timed event spans for the all-day strip
`buildTimedFullDayWeekSegments` tested every event against every day it
was given. That was fine for the paged week, but the scrolling week and
day grids hand in months of days. It now tests each event only on the
days it spans, with the same `isTimedEventFullDayOnDate` check, so the
strip and the timed grid still agree on every day (DST days included).
The time grid's own copy of this is gone.
2026-09-24 20:25:41 +02:00
Linus Rath 5434656e03 feat: bring a tapped reminder's day into view in the calendar 2026-09-24 20:25:41 +02:00
Linus Rath de3d697aa9 feat: scroll the calendar week and day views sideways #759
The week view showed one week and only the arrows moved it, and there
was no day view at all (a "day" default view opened the agenda). With
free scrolling on (the default) both are now one strip of day columns
that scrolls sideways, as in the webmail (15a5497a):

- the week view shows seven columns and snaps to single days; the new
  day view shows one column and moves a day per swipe; the title
  follows the first day in view and the arrows step from it
- reaching either end adds days and loads only their events; days added
  at the start keep the columns on screen in place
- the day headers and the all-day strip follow the sideways scroll on
  the native thread; all-day bars are cut per week (per day in the day
  view) so each part carries its title
- columns are windowed and lay out their events once per change

The day view is offered as a default view again. With free scrolling
off, or in right-to-left layouts, the week view pages as before and the
day view shows a single day.
2026-09-24 20:25:40 +02:00
Linus Rath caf61ece79 feat: scroll the calendar month view continuously #759
The month view showed one month at a time and only the arrows moved it.
With the new "Free scrolling" setting (on by default, the webmail's
`calendarFreeScroll`) it is one continuous list of weeks, as in the
webmail (15a5497a, ae403b06):

- the month under the upper part of the grid is drawn at full strength,
  names the title and is where the arrows step from; the first day of
  each month carries the month's name
- reaching the top or the bottom adds weeks there and loads only their
  events; weeks added above keep the rows on screen in place
- rows have a fixed height and snap to whole weeks, so the list stays
  windowed and navigation lands exactly on the month

Turning the setting off brings back the paged month and a fixed 30-day
agenda. The week view still pages.
2026-09-24 20:25:40 +02:00
Linus Rath 71522b9b92 feat: scroll the calendar agenda endlessly instead of a fixed 30 days
The agenda listed 30 days from the current date and the arrows paged
through it 30 days at a time. It now keeps a window of days that grows
as the user scrolls, like the webmail's agenda (60e05c00):

- reaching the end loads further ahead, doubling that side up to a year
- "Show earlier events" loads the past without moving what is on screen
- "Today" and the arrows scroll to their day while it lies inside the
  window and start a fresh window there otherwise; the title follows the
  day in view
- the "Today" row only anchors the list when today was actually fetched

Growing the window fetches only the new days: `extendRange` in the
calendar store loads the missing part and merges it into the loaded
window (up to about three years) instead of refetching all of it.

Navigation now goes through one focus for every view, so the week
view's arrows move the grid (it kept showing the selected day's week)
and the month arrows carry the selected day along.
2026-09-24 20:25:40 +02:00
Linus Rath 67935d2edc feat: translate the app-only strings into all 26 languages
Every language now carries its own `locales/rn/<lang>.json` with the
roughly 600 strings the webmail catalog does not have, so settings,
sign-in, files, calendar and push screens no longer fall back to
English.

Terms and the form of address follow each language's vendored webmail
catalog, and plural messages use the language's own CLDR categories.
Wording that had no precedent in the catalog would benefit from a
native speaker's review.
2026-09-24 20:25:40 +02:00
Linus Rath d7c890869b fix: reuse the startup identity read for the first message open 2026-09-24 20:20:59 +02:00
Linus Rath 4f3940f1d3 fix: catch up on a folder's changes in one request
Refreshing an open folder that was already loaded took up to four
round trips in a row: `Email/queryChanges`, `Email/changes`, `Email/get`
of the added and changed rows, and a `Thread/get` for their
conversation sizes.

They now go in one request, the `Email/get` of the added messages and
the `Thread/get` of their threads chained by result reference to
`/added/*/id` and `/list/*/threadId`. The list's own query and Email
states still drive it. Each part can fail on its own: a query the
server cannot diff falls back to the full re-query as before, an Email
state it cannot diff is forgotten as before, and added messages the
chained `Email/get` refused (more than one call takes) are fetched
apart. Only further `Email/changes` pages and rows that changed in
place without being re-added cost more requests.
2026-09-24 20:20:59 +02:00
Linus Rath 9c2f819eac fix: load the folder list once, and right away, when signing in
Signing in sent two full `Mailbox/get`s before the inbox loaded, and
the first only went out once the main screen had mounted and painted.
There is no deliberate delay behind the gap: the mail screen's mount
effect started the folder load, and the push setup read the same
folders again with a `Mailbox/get` of its own (plus the shared
accounts') just to find the Junk folders for its filter. The latter
also happened on every start with push notifications on.

- Sign-in (password and OAuth/pairing) starts the folder load as soon
  as the session is up, like a restored session already did.
- The mail screen's mount now joins a load on its way
  (`ensureMailboxes`) instead of queueing another behind it.
- The push filter takes the folders from the mail store (joining its
  load) and only fetches them itself when the store has none for the
  account.
2026-09-24 20:20:58 +02:00
Linus Rath 9fa931c589 fix: sync at once on resume and stop pinging the server while live updates run
Coming back to the app first waited for a `Core/echo` (sometimes two:
the keep-alive timer that was due fired alongside) and for the event
stream to be set up before the catch-up sync started. A failed echo
only led to `retrySession`, which does nothing while the session is
live. On top of that, a `Core/echo` went out every 30 s although the
event stream's own pings already show the server is reachable.

- On resume the stream reconnects and the folders and open list are
  synced right away; one echo runs alongside for the connection dot.
- The 30 s echo is only sent while the event stream is not open
  (polling fallback, reconnecting, client certificate). An open stream
  whose pings arrive (the live-updates handle's new `healthy`) marks
  the account connected instead.
- Echoes never overlap, and a periodic one right after the resume one
  is skipped.
2026-09-24 20:20:58 +02:00
Linus Rath 1196ad56c2 fix: probe the server's keyword sort order once instead of on every start
The list sorts pinned (and unread, starred or tagged, with an order
preset) messages first with `hasKeyword` comparators, and Stalwart reads
their direction the other way round (#718). To find out, the app sent a
probe request before the first list load of every start, because the
verdict was only kept in memory.

A conclusive verdict (RFC order, inverted, or keyword sorts refused) is
now stored per server and account under a versioned key and reused on
later starts. It is probed again once it is a week old, since a server
upgrade may change it. An inconclusive probe is still only kept in
memory and retried after a few minutes.
2026-09-24 20:20:58 +02:00
Linus Rath 3bf2c30c99 fix: stop asking for the Stalwart principal on every start and message open
Every sign-in and app start read the account's full name with
`x:Account/get`, and the first message opened (invitation or not)
looked up the account aliases with it too. Stalwart refuses that method
to everyone but admins, so for most users both were wasted requests
that always came back `forbidden`, and the full name was never
refreshed.

- The full name now comes from `x:AccountSettings/get` (its
  `description`), which every user can read, like the webmail (#900).
- The alias lookup only runs for a message that carries a calendar
  invitation (and the calendar).
- A refusal of `x:Account/get` is remembered per account for the
  session, so it is asked at most once.
2026-09-24 20:20:58 +02:00
Linus Rath 018accd865 fix: stop re-reading the mail list when the server echoes our own changes
Every change the app made (read, star, move, send, a draft save) came
back as a push, and the push re-read the open folder and the whole
folder list: `Mailbox/changes`, `Mailbox/get`, the shared accounts'
`Mailbox/get`, `Email/queryChanges`, `Email/changes`, `Email/get` and
`Thread/get`, often twice. One send cost about 10 requests.

- `jmapClient.request` now logs the Email states each of our own
  `Email/set` (also the implicit one of `onSuccessUpdateEmail`),
  `Email/import` and `Email/copy` responses moved between, with what
  they created, patched and destroyed.
- When a push reports an Email state that only our own writes led to
  from the state the open folder was read at, the store applies those
  writes to the rows (the optimistic updates mostly did already), moves
  the list's state on and skips the refresh. It still re-reads when
  anything else changed, when new mail was delivered, when a write adds
  a message to the folder or changes a keyword the list is sorted on,
  and when the folder's count shows a message it does not hold left.
  A push that overtakes its write's response waits for it briefly.
- A folder push re-reads only the part of the folder list that
  changed: the own folders or the shared accounts', and nothing for a
  Mailbox state it already holds.
2026-09-24 20:20:58 +02:00
Linus Rath 1c7c2a3be8 fix: stop reloading every account when returning to the unified inbox
The unified inbox reloaded page 1 of every account each time it came
back into focus, so opening a message and going back cost one
`Email/query` per account even when nothing had changed.

It now reloads on focus only when something may have changed since the
last load: the inputs or the active account changed, the live session
reported an `Email` state change, the opened message was unread (the
thread screen marks it read), the app was in the background (the event
stream is closed there), or accounts without a live session are shown
and the last load is over a minute old. An `Email` change that lands
within a few seconds of a focus that skipped the reload (the echo of an
archive or delete from the thread screen) reloads then.
2026-09-24 20:20:58 +02:00
Linus Rath 49ed48791d fix: name the webmail's Link Mobile App page in the QR sign-in hints 2026-09-24 20:16:36 +02:00
Linus Rath ef143a4dbb fix: take a conversation's size from the list row's own account
The placeholder cards held for a loading conversation looked its size up
by bare thread id whenever the open folder was the message's account.
Rows of an "All folders" search or a tag view now come from several
accounts and the list keys their conversation sizes by account, so the
viewer looks the size up the same way, and only for pages that are the
list's own rows.
2026-09-24 20:12:17 +02:00
Linus Rath 2a055ddfe2 fix: keep a star set while the mark-as-read delay runs 2026-09-24 20:12:17 +02:00
Linus Rath cd911ada79 fix: show toasts on every screen, including the Unified Inbox and contacts
Only the tab screens and the viewer had a toast host, so a viewer
action that failed after going back to the Unified Inbox or a contact
(both stack screens of their own) raised a toast nobody saw.

Every stack screen now gets a host through the navigator's
`screenLayout`, and only the focused screen's host renders, so the
screen kept live under the top one never shows the same toast a second
time. The tab screens' and the viewer's own hosts are gone.
2026-09-24 20:12:17 +02:00
Linus Rath e0b72883ac fix: show the tapped message when the viewer is already open
Opening the viewer while it was on top, e.g. by tapping a notification,
did not push a new screen: React Navigation handed the new target to
the same route as new params, and the viewer, which fixes its message,
account and page list when it opens, ignored them. The tap did nothing.

The route now mounts the viewer afresh for every new target it is
given, keyed by the message, its account and the open, so the new
message is shown with its own page snapshot. Params that stay the same
keep the viewer as it is.
2026-09-24 20:12:17 +02:00
Linus Rath 54b169c6c7 fix: open a message at its full height and keep the quick reply still
Every body started as a 120 px strip that grew once the page had
measured itself, so an open showed a skeleton, then a clipped strip of
the body, then the full body, and the quick reply box below it jumped
down as the WebView grew. A conversation arriving after the message
swapped the single-message layout for cards, which reloaded the body
and pushed it down under a "Loading conversation" row.

- `lib/body-heights` remembers the last measured height of each body
  (per width); a body shown before starts at that height.
- A page showing one message stretches its body over the space below
  the header until it has measured itself, and the quick reply sits at
  the bottom of the page, so it does not move whether the body turns
  out shorter or longer. Conversation cards start from an estimate
  (most of a screen for HTML, the wrapped line count for plain text).
- A single message is rendered through the conversation's card list,
  so its body stays mounted when the conversation arrives; while it
  loads, the size the folder list knows for it holds the older cards'
  places, and the loading indicator sits in the subject row.
- An open card whose body has not arrived shows its header over
  placeholder lines, with its reply buttons disabled until it has.
2026-09-24 20:12:17 +02:00
Linus Rath 6ebadf738f fix: sanitise a message body once instead of on every mount
The body view reset its inline-image map to a new empty object right
after mounting, which rebuilt the sandboxed document, so every body was
sanitised twice per mount. A body that mounted again (the page beside
the open one becoming the open one, a card closed and reopened, the
message opened a second time) was sanitised from scratch as well.

The document is now built by `lib/email-body-document`, which keeps the
last ten per message, body and rendering settings and hands back the
same one when they match, and the mount no longer replaces the empty
map.
2026-09-24 20:12:17 +02:00
Linus Rath 01fc6fbba5 fix: let the pages beside an opened message wait until it has loaded
The pager renders the pages before and after the open one, and each
fetched its message and conversation and mounted its own WebView at the
same time as the message the user had tapped, which competed with it
for the network, the JS thread and the WebView process.

The neighbouring pages now show their header and placeholder lines
until the open message's body has reported its height, the user starts
to swipe, or 1.5 s have passed. Their details are then loaded together
in one request, and their conversations and WebViews follow.
2026-09-24 20:12:17 +02:00
Linus Rath 7cdebc465a fix: stop asking for the identities on every message open
The viewer and the read-receipt banner each sent `Identity/get`
whenever the identity list was empty. Identities are not persisted, so
the first open of every session asked twice, and an account the server
lists no identities for asked on every open.

The settings store now remembers which signed-in account (server,
login, JMAP account) its identities were read for, and the viewer and
the banner only ask when they are not held for the current account.
Concurrent reads share one request. Switching accounts drops the old
account's identities instead of letting the quick reply send through
one the new account does not have.
2026-09-24 20:12:17 +02:00
Linus Rath ad1f053f72 fix: offer archiving in the viewer for a folder named Archive without the role 2026-09-24 20:12:17 +02:00
Linus Rath 1348e7d20a fix: start loading a message on the tap, before the viewer opens
The detail and conversation requests only went out once the viewer had
mounted and its effects ran, after the navigation had rendered.

A tap on a row in the folder list, the Unified Inbox or a contact's
activity, and a notification tap, now start the message's detail and
its conversation (when threading is on) through the shared cache, and
the viewer joins the requests already in flight. It runs on the tap and
not on press-in, because on a phone a touch that starts a scroll
presses rows too.

The Unified Inbox and contact activity also leave their rows in the
cache, so pages handed over by id get their header painted from the row
(and the right thread id) instead of a skeleton.
2026-09-24 20:12:17 +02:00
Linus Rath ac8e3fb2e9 fix: paint the message header from the list row while the body loads
The viewer showed a full skeleton until the message's detail had come
back, although the list row it was opened from already holds the
subject, sender, date and avatar. The header appeared about 0.4 s after
the tap.

A page whose list row is known now renders the real subject and header
from that row straight away, with placeholder lines where the body
goes; the body (and the star, reply and quick reply that need the full
message) follows when the detail arrives. Pages handed over by id only
(notification, deep link) keep the skeleton.
2026-09-24 20:12:17 +02:00
Linus Rath 33e303e248 fix: open a message from the cache and fetch only the open cards' bodies
Every open went to the network for the full message, even for a message
the viewer had just shown, and the conversation fetch then downloaded
every member's full body again, the opened one included. The pager's
three panes each started their own detail and thread fetch, so one open
from the Unified Inbox fetched the body twice and the thread twice.

Messages and conversations now live in a shared cache
(`lib/email-detail-cache`), keyed by server, login and JMAP account:

- a held copy renders at once; bodies never change, so it is only
  checked, with a keywords/folders-only `Email/get`, when the account's
  Email state (from push or our own reads) moved on or the list row the
  message was opened from disagrees with it;
- a copy in the offline cache is shown before anything goes out and is
  checked the same way;
- a conversation is listed from its members' headers (`Thread/get` and
  list properties in one request) and bodies are downloaded only for
  the open cards, in one batched `Email/get`;
- concurrent loads of the same message or thread share one request.

The store's network-first `getEmailDetail` is gone; its offline-copy
refresh moved into the cache.
2026-09-24 20:12:16 +02:00
Linus Rath 96c5a3f046 feat: let attendees answer a single occurrence of a recurring event
An answer to an invitation always went to the whole series, even when
given on one occurrence. The webmail asks which occurrences it covers
(#1086); the app now does too.

- answering on an occurrence opens the scope dialog with "This event
  only" and "All events" (an attendee cannot split the organizer's
  series, so "This and following" is not offered)
- "This event only" writes the answer to that occurrence: through its
  synthetic id, or as an override on the base event for one expanded
  on the device, with the whole participant map, the organizer and
  the sequence (`buildOccurrenceRsvpPatch`), so Stalwart replies with
  the occurrence's RECURRENCE-ID
- "All events" and answers on single events work as before
2026-09-24 20:10:08 +02:00
Linus Rath f397ee3e1b feat: change or delete one occurrence through its own id
"This event only" wrote a `recurrenceOverrides/<id>` pointer on the
master for every occurrence. That pointer fails while the series has
no overrides yet, replaced whatever the occurrence already overrode,
and left the new override without the occurrence's title, place or
reminders. Deleting one occurrence went the same way.

The store now keeps a change to an occurrence on that occurrence, the
way the webmail's `resolveMutationTarget` does:

- a server-expanded occurrence is updated or destroyed through its
  synthetic id; a server that refuses synthetic ids gets the same
  change as an override on the base event, and is remembered
- an occurrence expanded on the device becomes an override on its base
  event that keeps the existing override, pins start and duration,
  and is sent as the whole map when the series has none yet
- a new override carries the occurrence's details and sequence
  (`withNewOverrideDetails`), so it neither loses them nor ranks below
  the series
- "All events" and "This and following" still go to the master

The calendar's scope dialog hands "This event only" to `updateEvent`
and `deleteEvent` with the occurrence's id, and
`buildRecurrenceOverridePatch` is gone.
2026-09-24 20:10:08 +02:00
Linus Rath 84548898c1 feat: let the server expand recurring events when it can
Recurring series were always fetched whole and expanded on the device,
the webmail's fallback for Stalwart before 0.16.20. Newer servers
expand them with `expandRecurrences` and accept the synthetic
occurrence ids in `CalendarEvent/set`; the webmail uses that since
`lib/recurrence-instances.ts`.

- `supportsSyntheticCalendarIds` probes once per server and account
  with an update of an id that cannot exist (`notFound` means
  supported); the probe rides along with the first `Calendar/get`
- when supported, a range is loaded with `queryExpandedEvents`, the
  occurrences are fetched with `baseEventId` and hydrated with their
  base event's rules, overrides, all-day flag and duration; duplicate
  listings of one occurrence are collapsed like the webmail does
- a range the server refuses to expand ("expanded recurrences
  exceeds") and older servers keep the device-side expansion, which
  now passes server occurrences through untouched
- changes, deletes and RSVPs resolve a synthetic id to its base event
  (`resolveSeriesTarget`), including a master fetched for an
  occurrence of a shared calendar, and reload the range afterwards
  since synthetic ids reshuffle
- reminder keys use the base event and recurrence id, so a reshuffle
  does not reschedule them
- client-only fields are stripped from `CalendarEvent/set` payloads
2026-09-24 20:10:08 +02:00
Linus Rath 1a651a9c25 fix: read floating task dues in the calendar time zone like events
A task due without a time zone was read as device wall time, while
Stalwart computes a floating event's start in the calendar time-zone
setting the app sends with every query. With that setting away from
the device zone, a floating task and a floating event at the same
wall time showed hours apart, and task reminders fired at the device
reading.

`getTaskDueDate` now resolves a floating timed due in the effective
calendar zone; dues with their own zone and date-only or all-day dues
are unchanged. The device zone is cached for 30 seconds, since task
sorting resolves a due per comparison and building an
`Intl.DateTimeFormat` is slow on Hermes.
2026-09-24 20:10:08 +02:00
Linus Rath 83db70ac86 fix: open the event when a calendar reminder is tapped
Tapping a reminder notification only brought the app to the front:
nothing listened for expo-notifications responses, so the event never
opened.

- reminders now carry the event's server id, account, occurrence,
  start and the signed-in account alongside the store id
- `startCalendarReminderTapHandling` picks up the reminder that
  launched the app and any tapped while it runs; it only acts on
  reminders it scheduled, and mail push taps keep coming through the
  native tap store
- the app switches to the reminder's account if needed, parks the
  target in `pending-calendar-open` and opens the Calendar tab, which
  shows the event in the detail sheet or the task in the tasks sheet
- the event is looked up by its server identity, first in the loaded
  window, then in the day around its start, then as the stored event,
  so an occurrence outside the visible month still opens; a deleted
  one says "This event is no longer available."
2026-09-24 20:10:08 +02:00
Linus Rath a4ac6cad5c fix: list the events an .ics import could not bring in
A partial import dropped the refused events silently: the sheet said
"Imported 3 events" for a file of five, and the server's reason never
reached the user. The webmail reports how many events failed and why.

- `batchCreateEvents` returns the refused events by index with the
  server's reason instead of just the created count
- `importEvents` resolves with `{ imported, refused }`, also counting a
  refused link of a UID that lives in another calendar and every event
  of a chunk whose request failed; when nothing got in it rejects with
  an `ImportRefusedError` carrying the list, so the invitation banner
  still never reads a refusal as "Already in your calendar"
- the import sheet shows the imported count and lists each refused
  event with its date and reason
2026-09-24 20:10:07 +02:00
Linus Rath a7e4e84f5d fix: say when saving a task fails and keep what was typed
Saving in the tasks sheet ran `onCreate`/`onUpdate` in a `try/finally`
without a `catch`, so a refused create or update became an unhandled
rejection: the spinner stopped, nothing was shown, and the user could
not tell whether the task was saved.

The editor state and the save logic move to `src/lib/task-editor.ts`.
`submitTaskEditor` returns the server's reason instead of throwing; the
sheet shows it in an alert and leaves the editor untouched so the input
can be fixed or saved again. It is cleared only after a successful save.
2026-09-24 20:10:07 +02:00
Linus Rath e0a79ca5c6 test: keep the locale tests independent of which overlay keys are translated 2026-09-24 20:07:00 +02:00
Linus Rath edab181e60 fix: load list attachment chips from each search hit's own account #1082
The attachment chips on list rows fetched every row's parts from the
account of the open folder. In an "All folders" search or a tag view the
rows come from several accounts, so a hit from the team inbox asked the
user's own account, which answers for another message with the same id,
or not at all.

A row that carries an account stamp now loads its chips, and opens an
attachment from them, in its own account; the chips a row remembers are
keyed by that account too.
2026-09-24 20:05:38 +02:00
Linus Rath d097ae9ec3 fix: stop re-counting every tag each time the sidebar opens
Every drawer open sent the whole set of tag count queries again (14
Email/query calls for 7 tags, twice that with a shared account), although
nothing had changed since the last open.

The counts now live in a small store, keyed by the login, the accounts
and the tags they were fetched for. Opening the drawer only fetches when
there are no counts for that key yet or when an Email change was
reported for one of the accounts since (push or the poll fallback); a
change that arrives while the drawer is open refreshes them there and
then. Overlapping opens share one request, and a change that lands while
the counts are in flight leaves them due for another fetch.
2026-09-24 20:05:38 +02:00
Linus Rath 8d5bced7f3 fix: list and count tagged mail from every account in the tag view #1038
A tag sits on messages in the user's own account and in the shared
(group) accounts alike, but the tag view only asked the account of the
open folder, and the sidebar badge only counted the own account: with 2
own and 2 team messages tagged Red, the Red view showed 2, and which 2
depended on the folder it was opened from.

The tag view now fans out like the "All folders" search (#1082), which
it is: every folder of the own account and of each shared account with
folders in the sidebar, in one request, with rows stamped with their
account so opening, untagging and every other action reach the right
one. A search inside the tag view narrows it in every account.

The tag badges sum the counts of the same accounts; the query pairs for
all of them go out packed into as few requests as `maxCallsInRequest`
allows, and an account that can't be reached counts nothing instead of
failing the badges.
2026-09-24 20:05:38 +02:00
Linus Rath ee50418bac fix: search the shared accounts' folders too with "All folders" #1082
"All folders" sent its Email/query only to the account of the open
folder, so mail in a shared (group) account the same login reaches never
showed up: "zephyr" found the 3 own messages and missed the 5 in the team
inbox, although that inbox sits right there in the sidebar.

The search now asks the own account and every shared account with
folders in the sidebar, like the webmail's `searchAcrossAccounts`:

- one request carries Email/query, Email/get and Thread/get for each
  account (`queryEmailPagesAcrossAccounts`), and the pages are merged
  under the list order
- each row is stamped with its JMAP account (`Email.jmapAccountId`), so
  opening a hit opens it in that account and read, star, tag, archive,
  delete, spam and move act there, filing into that account's own
  Archive, Trash or Junk; one undo restores rows in every account
- "load more" continues each account from its own rows on screen rather
  than one shared offset, which would skip messages of the account with
  more hits
- an account that fails leaves the others' hits on screen with a notice
- a push for any account re-runs the search
- list keys, thread grouping and conversation sizes are scoped by
  account, as JMAP ids are only unique per account
- deleting a hit judges Trash and Junk by the hit's own folders, not the
  open folder, both for the move and for the permanent-delete confirm

A search scoped to one folder still asks only that folder's account.
2026-09-24 20:05:38 +02:00
Linus Rath ddff2f7b66 fix: queue star, tag and unread from the viewer while offline
The viewer's star, tag and mark-unread buttons called the API
directly, so offline they failed and the list row behind the viewer
kept its old state. They now go through the email store's keyword
action like the list's selection actions: offline the change waits in
the outbox, and the list row follows. The store action takes the
viewed message and its account, so it only touches the list when the
list holds that account and never a same-id row of another one.
2026-09-24 19:45:07 +02:00
Linus Rath d8db4a5597 fix: show the right image when two attachments share a name
The reader's attachment preview wrote every file to the same
`bulwark-exports/<name>`. React Native caches images by path, so a
second attachment with the same name (such as Outlook's `image001.png`)
could show the first one's picture. The preview now uses a temp folder
of its own per preview, as the Files tab does, also for parts unpacked
on the device, and deletes it when the preview closes unless the file
was shared to another app. A download that finishes after its preview
was closed no longer reopens it.
2026-09-24 19:45:07 +02:00
Linus Rath 1fb60a5c61 fix: tell the user when an automatic read receipt fails to send 2026-09-24 19:45:07 +02:00
Linus Rath 115e162b86 fix: delete large file selections in batches the server accepts
Deleting files sent every selected id in one FileNode/set. A selection
larger than the server's `maxObjectsInSet` was refused as a whole, so
nothing was deleted. The ids now go out in batches of that size; a node
already removed with its folder by an earlier batch counts as deleted,
and a refused item no longer stops the remaining batches.
2026-09-24 19:45:07 +02:00
Linus Rath a3193ebaf7 fix: create new groups in the address book being viewed 2026-09-24 19:45:07 +02:00
Linus Rath 40ace883b7 fix: keep a contact's recent-email previews on one line 2026-09-24 19:45:07 +02:00
Linus Rath 9135deed80 fix: translate the no-subject fallback in mail lists and notifications
The mail list, the unified inbox, a contact's recent emails and new-mail
notifications showed a hard-coded English "(no subject)". They now use
the webmail's `email_viewer.no_subject` (and `contacts.activity.no_subject`
for the contact view). The notification task reads the in-app language
override itself, as it cannot use the locale store.
2026-09-24 19:45:06 +02:00
Linus Rath 3c7b96e11a fix: remove shared-file copies left behind by an upload cut short
Files shared into the app are copied into the cache as `upload-*`
before they are uploaded. When the app was killed mid-upload the copy
was never deleted. The launch-time sweep of stale export files now also
removes upload copies made by an earlier run of the app; copies of the
current run may still be uploading and are left alone.
2026-09-24 19:45:06 +02:00
Linus Rath 2e78c9870c fix: remove the unsent copy when the server refuses a send
A send creates the message in Drafts and submits it in the same request.
When the server refused the submission, the created copy stayed in
Drafts without anyone tracking it, so sending again created a second
copy and left the first behind as a stray draft. The copy is now
destroyed again when the submission is refused; the composer still
holds the message and the previous draft version is left alone.
2026-09-24 19:45:06 +02:00
Linus Rath 46e285b3c9 feat: search settings from the Settings screen
The webmail's Settings screen has a search field; the app had none, so
finding a setting meant opening panes one by one. Settings now has a
search field above the pane list that narrows the list to the panes
that match and lists up to six matching settings under each.

The index follows the webmail's `lib/settings-search.ts`: per pane, the
translation keys it renders plus a few English keywords, walked for
label/description pairs. The paths list what the native panes show
rather than the webmail's, the walker prefers the `*_mobile` variants
the panes render, and every label goes through t() for the current
language. A test checks that each result is a key its pane renders,
so results do not point at webmail-only settings.

Tapping a result opens the pane, scrolls to the matching row and tints
it briefly. Back clears the search before leaving Settings.
2026-09-24 19:40:08 +02:00
Linus Rath 4c9efbc44d fix: stop an open settings page from catching back on other tabs 2026-09-24 19:40:08 +02:00
Linus Rath 7964e646c4 feat: add the Roundcube Elastic and Aurora Glass themes
The Themes pane offered four of the webmail's built-in themes. The
webmail also ships Roundcube Elastic and Aurora Glass, so their colour
tokens are now mapped onto the native palette the same way as the
other four, for both light and dark.

Aurora Glass dark uses translucent surfaces that the webmail frosts
with a backdrop blur. RN has no blur, so those fill tokens are
composited onto the theme background instead of letting the content
underneath show through. The skins (fonts, radii, Elastic's dark task
rail, Aurora's gradient mesh) and the "Flat fields" theme are not
ported.
2026-09-24 19:40:08 +02:00
Linus Rath 008bff29f8 feat: load app-only strings in every language, not just English
The RN overlay (`locales/rn/en.json`, the strings the webmail catalog
does not carry) only existed in English, so those 600 or so labels
showed in English whatever language the app was set to.

- load `locales/rn/<lang>.json` for every language next to its
  vendored catalog; an untranslated key still falls back to English
- a test keeps each translation to English keys and the same ICU
  arguments
- `node scripts/rn-missing-translations.mjs [lang]` lists what a
  language still lacks
2026-09-24 19:39:48 +02:00
Linus Rath af6290fd4c fix: only show the unified mailbox when it can hold more than one account's mail #843
The drawer showed "All inboxes" and the Unified section as soon as a
second account was signed in, but with cross-account off the unified
views only cover the active account, so they repeated its own folders.
They now follow the webmail's rule: shown with cross-account on and
several accounts, or when group inboxes are merged in.
2026-09-24 19:36:41 +02:00
Linus Rath 4e6426171a feat: add a setting to clear the search when switching folders 2026-09-24 19:36:41 +02:00
Linus Rath b1fb2e9b22 feat: show attachment chips on mail list rows
A message with attachments only showed a paperclip, so reaching a file
meant opening the message and scrolling to the bottom. Rows now list the
attached files as chips under the preview, two at a time with the rest
as a count, like the webmail (0e1c34cd). A tap opens the file the way
the reader's chips do: the attachment click setting saves it or
previews it in-app, and other types go to the platform viewer. Inline
and Content-ID parts (signature images, spacers) are left out.

The list request still only asks for `hasAttachment`: the webmail found
that asking for `attachments` there makes Stalwart parse every message's
raw blob (#1089). Rows with a paperclip ask for their parts once
rendered; rows that render together are coalesced into one Email/get
per account and the answers are cached, so scrolling back costs nothing.
2026-09-24 19:36:41 +02:00
Linus Rath 25c088383f feat: show the inbox unread count on the app icon
Ports the webmail's app-icon badge (`useAppBadge`): the icon shows the
unread count of the active account's own inbox, and a new "Unread count
on app icon" toggle (on by default, exported as the webmail's
`faviconUnreadBadge`) turns it off. Signing out of the last account
clears it.

This is iOS only. On Android expo-notifications badges through
ShortcutBadger, which few launchers honour, and implements a count of 0
as `NotificationManager.cancelAll()`, so clearing the badge would also
dismiss other accounts' mail notifications and calendar reminders.
Android launchers already badge the icon from the notifications on
display. For the same reason the Android-only background push task
does not update the badge.
2026-09-24 19:36:41 +02:00
Linus Rath 9f75e70218 fix: only offer archiving when the archive folder can take the mail #578
The list resolved the Archive folder with a loose name match ("Archives",
"Archived" or any name containing "archive"), while the store's archive
actions only accept the `archive` role or a folder named exactly
"Archive". With a role-less "Archives" folder the swipe and bulk archive
were offered but silently did nothing.

`findArchiveMailbox` now uses the webmail's rules (role first, then the
exact name, case-insensitive) and both store archive actions resolve
through it, so the list offers archiving only when it will run and
otherwise shows the existing "no Archive folder" message.
2026-09-24 19:36:41 +02:00
Linus Rath 30aea63a66 fix: subscribe folders created on the phone so IMAP clients show them #951 2026-09-24 19:36:40 +02:00
Linus Rath 85f23dfdf4 test: check that session restore waits for the persisted stores together
The audit listed the four hydration waits in `restoreSession()` as
serial. They are already awaited together with `Promise.all`, and
restore only reads the stores after all four have settled, so the order
they finish in does not matter. The remaining queueing is inside
AsyncStorage, whose native side runs reads one at a time.

This test pins the concurrent wait: with all four stores stuck, restore
gives up after one `HYDRATION_TIMEOUT_MS`, not four.
2026-09-24 19:35:47 +02:00
Linus Rath 4a5d837cd7 fix: stop re-rendering hidden tabs and the mail list on every mailbox fetch
The tab navigator subscribed to the whole `mailboxes` array to show one
badge. Every mailbox fetch sets a new array, so each refresh re-rendered
the navigator and, through its inline render callback, the mail list,
also while a message was open on top of it. It now selects the inbox's
unread count, a number that only changes when the badge does.

Both navigators also set `freezeOnBlur`, so tabs out of view stop
re-rendering until they are shown again, and so do stack screens more
than one level below the top. On Fabric, native-stack deliberately
keeps the screen directly below the top live so the back transition
can show it, so the list under an open message still re-renders
through its own store subscriptions.
2026-09-24 19:35:47 +02:00
Linus Rath f2dd3a8b4b fix: float the notification prompt instead of pushing the list down
The push onboarding card was laid out above the tab navigator. It
arrives 1.5 s into a launch, so it pushed the whole mail list down, and
it drew under the status bar because it ignored the top inset.

It now floats above the tab bar like the webmail's card and the toasts,
respecting the bottom inset, and fades and slides in (instantly when
animations are off), so nothing else moves when it appears or goes.
2026-09-24 19:35:47 +02:00
Linus Rath b9db3764ff fix: stop the cold start flashing white and the mail header jumping in
On a cold start the splash was followed by about a second of blank
white, then mail rows whose header sat under the status bar until it
popped in and pushed the list down.

- `AppTheme` had no `android:windowBackground`, so while JS loaded the
  window showed the platform default (near-white, or grey at night).
  It is now the app's own background, #ffffff by day and #09090b at
  night (`values-night`), matching `src/theme/tokens.ts`.
- `NavigationContainer` had no theme, so React Navigation painted its
  light default behind and between screens even in dark mode. It now
  uses the palette's background.
- The mail list took its top inset from the native `SafeAreaView`,
  which on Fabric only learns the insets a few frames after mounting.
  It now pads with `useSafeAreaInsets()`, which the navigator's
  safe-area provider fills from the initial window metrics on the first
  render; the value is the same, it just arrives in time.
2026-09-24 19:35:47 +02:00
Linus Rath 82ca359df7 fix: stop loading the whole app for a background push
`index.ts` imported `App` at the top, so when an FCM data message
started the headless push task in a cold process, the bundle evaluated
every screen, store, catalog and icon before the task could show its
notification.

The headless task is still registered at the top of `index.ts`, but the
root component now requires `App` when the UI first renders. A headless
start therefore only evaluates the task and its own imports; a normal
launch evaluates the same modules as before, one render later.

Measured with `npx expo export --platform android` (modules reached
through top-level requires from the entry, i.e. what a headless start
evaluates): 1,516 modules, 7,238 KB -> 202 modules, 745 KB.
2026-09-24 19:35:47 +02:00
Linus Rath 5bcfe1fe4d fix: load only the active language's calendar date locale
`src/lib/calendar-locale.ts` imported its 27 date-fns locales from the
`date-fns/locale` barrel, which re-exports all of date-fns' ~100
locales. Metro bundled every one of them and evaluated them at startup
through the calendar screens.

Each locale is now required from its own `date-fns/locale/<code>`
module the first time `getDateFnsLocale()` asks for it; en-US stays a
static import as the fallback. A test checks that every UI language
still maps to the right date-fns locale.

Measured with `npx expo export --platform android`:
- modules in the bundle: 2,469 -> 2,114 (date-fns locale modules:
  531 -> 170, 1,213 KB -> 358 KB unminified)
- Hermes bundle: 8.19 MB -> 7.61 MB
- modules evaluated at startup: 2,038 -> 1,516 (date-fns locale
  modules: 531 -> 10, i.e. only en-US)
2026-09-24 19:35:47 +02:00
Linus Rath 063432bf28 fix: bundle and load only the icons the app uses
93 files import icons from the `lucide-react-native` barrel. Metro has
no tree shaking, so the barrel pulled all 1,694 icons into the bundle
and evaluated every one of them at startup.

A small Babel plugin (`scripts/babel-plugin-lucide-imports.js`, wired
up in the new `babel.config.js` on top of `babel-preset-expo`) rewrites
each named import to the per-icon module the barrel re-exports, e.g.
`import { Mail } from 'lucide-react-native'` becomes a default import of
`lucide-react-native/dist/esm/icons/mail.js`. The name table is read
from the package's own barrel, so aliases such as `MailIcon` or renamed
icons keep resolving; an unknown name fails the build. The package's
`exports` map only lists the barrel, so the new `metro.config.js`
resolves these deep paths as plain files instead of warning about each.
A test checks that every icon imported in the app maps to an existing
module and that no default or namespace import brings the barrel back.

Measured with `npx expo export --platform android`:
- modules in the bundle: 3,995 -> 2,469 (lucide: 1,701 -> 175)
- Hermes bundle: 9.34 MB -> 8.19 MB
- modules evaluated at startup: 3,564 -> 2,038

Metro does not key its transform cache on the Babel config, so a warm
dev cache keeps serving the old barrel imports until
`npx expo start --clear`.
2026-09-24 19:35:47 +02:00
Linus Rath 604eb39340 fix: load only the active language's catalog at startup
All 27 locale catalogs were imported statically by `src/i18n/index.ts`,
so every cold start evaluated about 4.3 MB of JSON before the first
frame, although only English (the fallback) and the active language are
ever read.

Each catalog is now required from a per-locale loader the first time
`translate()` or `getDictionary()` asks for it. `t()` stays synchronous;
after a language switch the next render loads the new catalog. English
stays a static import because every lookup falls back to it, and the RN
overlay is merged when a catalog is first built, as before.

Measured with `npx expo export --platform android` (unminified bundle,
counting modules reached through top-level requires from the entry):
- catalogs evaluated at startup: 28 modules, 4,287 KB -> 2 modules,
  193 KB (English and the RN overlay; a non-English user also builds
  their own catalog on the first render)
- modules evaluated at startup: 3,590 -> 3,564; their code: 15,219 KB
  -> 11,122 KB
- the Hermes bundle keeps its size (9.34 MB); the catalogs are still in
  it, they are just not evaluated until used
2026-09-24 19:35:47 +02:00
Linus Rath 6c31c8d0c6 fix: stop filters saved on the phone from moving spam out of Junk
Stalwart only files spam into Junk when no rule decided where the
message goes, so the webmail now adds
`not spamtest :percent :value "ge" :comparator "i;ascii-numeric" "50"`
to every move and copy rule when the server supports `spamtestplus`
and `relational`, with a per-rule "Also move messages marked as spam"
opt-in stored as `includeSpam` (6241ed61). Saving filters on the phone
dropped the guard and the opt-in, so every folder rule started pulling
spam out of Junk again, and opted-in rules lost their setting.

Port the guard and keep `includeSpam` in the rules metadata. The rule
editor gets the opt-in switch while the rule moves or copies mail.

The round-trip fixtures now cover the guard and a script with every
newer feature plus a hand-written rule. Both clients add two blank
lines before a script's external rules on each save, so that fixture is
compared with what the webmail itself writes when it saves it again.
2026-09-24 19:29:36 +02:00
Linus Rath 3f88868aad fix: keep folder ids, forward copies and flag order in filters saved on the phone
The webmail now stores the target folder's id on move and copy actions
and writes `fileinto :mailboxid "<id>" "<path>"` when the server lists
`mailboxid`, so a rule keeps working after the folder is renamed. It
also offers "Keep a copy" on forwards (`redirect :copy`) and sets flags
before the move, because `fileinto` stores the flags set at that moment
(dadfb08f). The phone did none of this: saving filters on the phone
turned every `:mailboxid` target back into a bare path, forwards with a
copy into plain redirects, and a rule that stars and moves mail stopped
starring it if the move was picked first.

Port the generator and parser changes and pass the server's Sieve
extensions to the generator. In the rule editor:
- folders are picked by id, and saving stores the id next to the path
  and refreshes the path from it, so older rules pick up the id;
- a folder that isn't listed stays selected instead of being replaced;
- forwards get the "Keep a copy" switch;
- changing an action's type drops the id and the copy flag it no
  longer uses.
2026-09-24 19:29:36 +02:00
Linus Rath d53bfec68a fix: keep the auto-reply running when filters are saved on the phone
Stalwart runs one active Sieve script and turns the auto-reply on by
activating its own `vacation` script. The webmail therefore re-activates
the filters script with `include :personal :optional "vacation"` and
records `includeVacation` in the rules metadata (198a3c0d). The phone
dropped both, so saving any filter on the phone silently switched the
auto-reply off, and turning the auto-reply on from the phone switched
every filter off.

Port the webmail's handling:
- the generator writes the include (and `require "include"`) and the
  parser reads the flag back, dropping the `include` require once the
  auto-reply is off;
- loading filters while the vacation script is active keeps it through
  the include, as does every save;
- switching the auto-reply on or off in the vacation settings adds or
  removes the include, and the settings read the include as "on",
  because VacationResponse reports it off in that state;
- the "vacation active" banner also shows for an include.

Scripts the webmail wrote with the include now save back unchanged;
`fixtures/webmail/` holds scripts from its current generator.
2026-09-24 19:29:36 +02:00
Linus Rath 4fdf39aa08 fix: go up one folder on Android back in the Files tab
Back inside a Files subfolder switched to the Mail tab. It now closes an
open preview, then leaves selection mode, then goes up one folder, and
only switches tabs at the root. `filesBackStep` holds that order.
2026-09-24 19:28:37 +02:00
Linus Rath 409cdb139e feat: preview images, text and PDFs in the Files tab
Tapping a file in the Files tab downloaded it and went straight to the
Android "Open with" chooser or the iOS share sheet. It now opens the
reader's attachment preview for the types that preview can show, and
hands only the rest to another app, as before.

- `filePreviewKind` (`src/lib/file-preview.ts`) builds on the reader's
  `previewKindFor` and adds source code and config files as text; HTML,
  SVG, images React Native cannot decode and PDFs on Android (no in-app
  renderer) still go to another app
- files over a cap per type (text 512 KB, .eml 10 MB, images 25 MB,
  PDFs 50 MB) open in another app instead; when a node has no size,
  the downloaded file is checked
- the preview downloads the file once, with the same authenticated
  download as Download, into a temp folder of its own; Open with, Share
  and Download reuse that copy, so they work without another fetch
- the folder is deleted when the preview closes, or left to the
  stale-export sweep, which now also clears preview folders, once
  another app got the file
- the per-file menu offers Preview and "Open with…" instead of the
  combined "Preview / Share"
- the shared preview gains a header Open button, an optional Download
  button, labels on its icon buttons and a fallback when an image does
  not decode; the .eml parsing moves into `emlPreviewFromBytes`, which
  the reader uses too
2026-09-24 19:28:37 +02:00
Linus Rath 1d755fc066 fix: list event participants once with contact names and organizer status
Ported from webmail 8cbc5bc5 and 6f99c017. Stalwart can emit the
organizer both as the ORGANIZER-derived participant and as an ATTENDEE,
so the detail sheet listed them twice and counted a phantom
participant. It also drops the organizer's display name, leaving a bare
address, and the organizer without a status got the pending dot.

- fold participants with the same address into one entry, keeping a
  real RSVP over needs-action, and count addresses in
  `getParticipantCount`
- show the organizer as accepted when they have no status, with the
  status label like everyone else
- fill missing names from contact cards (exact address, then the same
  local part on a related domain), then the account and identity
  names, via `useContactNameResolver`; the editor uses it for the
  organizer name of a new invitation
2026-09-24 19:12:03 +02:00
Linus Rath a6ef0abc2a fix: report calendar deletes and imports the server refused
`deleteEvents` never looked at the `CalendarEvent/set` answer, so a
refused delete (read-only calendar, method error) removed the event
from the screen and said nothing until the next refresh brought it
back. Clearing a calendar ignored `notDestroyed`/`notUpdated`, and an
import the server refused entirely returned 0, which the invitation
banner showed as "Already in your calendar".

- check the destroy result in `deleteEvents` and `clearCalendarEvents`
  with `assertSetResult`, and the unlink result as well
- throw from `batchCreateEvents` when nothing was created and the
  server refused events; a partial import still returns its count
- show an alert when deleting an event or a task fails, and refresh
  after a partly failed import or clear
2026-09-24 19:12:02 +02:00
Linus Rath c0582ab950 fix: send the RSVP for invitations outside the loaded calendar window
Accepting an invitation looked the event up in the calendar store,
which only holds the window the calendar last showed. For an event
further out the lookup came back empty, no response was sent, and the
banner still said "Added to calendar".

- find the event on the server with a `uid` filter, like webmail's
  `queryCalendarEvents({ uid })`, both when the banner opens (so it
  shows as already in the calendar) and after importing it
- let `rsvpEvent` take the event when it isn't in the store, so the
  organizer repair still applies
- show "Could not send your response" whenever no response went out
2026-09-24 19:12:02 +02:00
Linus Rath 15717f137a fix: keep what was typed in the event editor when calendars refresh
The editor reset its form whenever `calendars` or `currentUserEmails`
got a new identity, which happens when a calendar push refetches the
calendars or the alias lookup finishes, so the title and times the user
had typed were replaced mid-edit. Reset only when the editor opens or
moves to another event, and fill in the calendar separately when the
calendars arrive after a new-event form opened.
2026-09-24 19:12:02 +02:00
Linus Rath 4b903ed179 fix: keep calendar reminders when the calendar view moves
Reminders were rebuilt from the events of the window the calendar
showed, and everything else was cancelled, so looking at another month
dropped every pending reminder. The sync only started once the Calendar
tab opened, and without a notification handler a reminder that fired
while the app was open was never shown.

- load the events for reminders from their own window (now to a week
  past the horizon), independent of the view, via `loadEventsInRange`
  factored out of `fetchEvents`
- leave scheduled reminders alone when that window can't be loaded
  (offline, no session yet), cancel only ours, and cancel them all on
  sign-out
- start the sync at launch and rerun it after sign-in or an account
  switch, on `Calendar`/`CalendarEvent` state changes, on in-app edits
  and when the app returns to the foreground; only the Calendar tab may
  ask for notification permission
- install a foreground handler that shows our reminders; mail push is
  posted natively and doesn't go through expo-notifications
2026-09-24 19:12:02 +02:00
Linus Rath 6a975b822c fix: show a task's due time in local time like events
A task's `due` is wall time in the task's `timeZone`, but the tasks
sheet, the calendar chips and task reminders read it as device time,
so a task due at 17:00 Europe/Berlin showed and fired at 17:00
wherever the phone was. Events already go through `utcStart`.

- resolve the due in its zone with a new `getTaskDueDate` and use it
  for the list, sorting, overdue, the chips and reminder times
- save an edited timed due with the effective time zone, as the event
  editor does, so the converted local time isn't read back in the
  task's old zone
2026-09-24 19:12:02 +02:00
Linus Rath 1b64c14280 fix: treat CalDAV tasks marked COMPLETED, NEEDS_ACTION or canceled as such
Tasks created over CalDAV can come back with their iCalendar STATUS
spelling or as `canceled` (stalwartlabs/calcard#20). The app compared
against the JSCalendar values only, so a completed CalDAV task counted
as pending, still raised its reminder, and a tap on it completed it
instead of reopening it.

- normalize the known spellings when events are read, like webmail's
  `normalizeCalendarTask` (5f1d2795), and keep unknown values as they
  are
- reopen a cancelled task on tap, since the tasks sheet shows it ticked
2026-09-24 19:12:02 +02:00
Linus Rath dcdda5ed9f fix: let tasks be completed again and say when the server refuses #958
Ticking a task sent `progressUpdated`, which JSCalendar 2.0 dropped and
Stalwart rejects with `invalidProperties`, so the whole update failed.
The checkbox fired the toggle with `void`, so the task silently stayed
open.

- drop `progressUpdated` from the toggle patch, like webmail 6b30e91c
- flip the checkbox optimistically and put it back when the update
  fails
- show a "Failed to update task" alert with the server's reason
2026-09-24 19:12:02 +02:00
Linus Rath 548b647f15 feat: add background colour and the webmail's 16 text colours to the composer
The composer offered 8 text colours and no background colour. It now
has the webmail's 2 x 8 palette (7e0a86a6, 3957b358) for text colour,
and a background colour button with the same palette and a "Remove
background color" action, applied with the editor's `hiliteColor`.

The webmail's font-size picker is not ported: the editor has no mark
model to scope a size to, so it needs a selection-rewriting command in
the page script that can only be checked on a device.
2026-09-24 19:10:46 +02:00
Linus Rath fc00781b47 fix: stop each keystroke in the composer re-rendering the whole screen twice
The editor page posted its full `innerHTML` on every input event and its
formatting state on every selection change, which fires on every
keystroke too. Each message re-rendered the whole composer, where a
`JSON.stringify` snapshot of the body ran on every render (PF8).

- typing now posts at most one `change` per 400 ms: the first keystroke
  at once, so the composer knows it is dirty, then the latest content
  when the window ends; toolbar commands and blur post straight away
- the formatting state is posted only when it changes
- the empty-placeholder check no longer serializes the body per
  keystroke, and the dirty-tracking snapshot is memoized
- autosave reads the live editor DOM, like send, save-and-close and the
  format switch already do, so the throttle can't make it save a
  slightly older body

The body stays in state rather than a ref: the Send button and the
close guard render from it.
2026-09-24 19:10:45 +02:00
Linus Rath 3faac98627 fix: sign quick replies with the sending identity's signature
Quick replies went out with no signature at all. They are now signed
the way the composer signs a plain-text reply: between the reply and
the quote or at the end, following the signature position setting, with
the "-- " separator when it is on, and with the primary identity's
signature when the sending alias has none of its own.

The quick reply also resolves its sending identity like the composer
(webmail 4fc7b8b5): recipients in To, Cc, Bcc order, and a reply to our
own message from the identity that sent it. It still never takes the
catch-all From rewrite, having no From row to show it.
2026-09-24 19:10:45 +02:00
Linus Rath 472c4d8f26 feat: add per-message HTML/plain-text toggle #1022
The composer's format came only from the "plain text only" setting, and
a reopened draft was forced into it: with plain text on, an HTML draft
was flattened on its first autosave, and with it off a text draft opened
in the rich-text editor.

Ported from the webmail (f6ad8a59, `compose-format.ts`):

- a reopened draft opens in the format it was written in; an empty
  draft and new messages still follow the setting
- a toolbar button switches just this message between rich and plain
  text, converting the body in place; the signature keeps its place
  (end or above the quote) in the target format, so it is never
  doubled or lost
- images inserted into the rich-text body stay on as ordinary
  attachments after a switch to plain text instead of being dropped
2026-09-24 19:10:45 +02:00
Linus Rath 0ffdcacea9 fix: start a new message in a shared folder from that mailbox's identity
A new message composed while a shared or group folder was open always
defaulted to the user's own identity: `findComposeIdentityId` was
ported from the webmail but never called.

The composer now reads the folder that was open when it was started and,
for a shared folder whose account name is an address the user has an
identity for, preselects that identity (webmail 129f545a, a971d5a1).
Own folders resolve to nothing and keep the preferred identity. Unlike
the webmail this isn't gated on "Reply from received address": the
webmail gates it only because it falls back to the login address, which
would override a chosen default identity (#507), and this path has no
such fallback.
2026-09-24 19:10:45 +02:00
Linus Rath 2d907ffab8 feat: choose exact-address or same-domain matching for reply from received address #1000
Ported from the webmail (565d5c10). With "Reply from received address"
on, a new "Received address matching" choice limits the catch-all From
rewrite: "Exact address only" keeps replies to configured identities,
"Any address on my domains" (the default, and the old behaviour) also
sends from other addresses on an identity's domain. Deployments whose
extra addresses are distribution lists rather than aliases want exact.

The setting is `replyIdentityMatch`, the webmail's key, so it round-trips
through settings export and import. The toggle's description now says
that identities always match, since that no longer depends on it.
2026-09-24 19:10:45 +02:00
Linus Rath a0c2bd7f4a fix: reply and forward from the address the message was delivered to
Replies went out from the default identity unless "Reply from received
address" was on, because that setting gated both the match against the
user's own identities and the catch-all From rewrite. Forwards took the
rewrite too, and a reply to one's own sent message resolved the identity
from the other party's address. Ported from the webmail (4fc7b8b5):

- the own-identity match is unconditional and applies to forwards;
  only the catch-all From rewrite stays behind the setting, and never
  on a forward
- a reply to our own message keeps the identity that sent it (#703)
- `findReplyIdentityId` scans recipients in To, Cc, Bcc order instead
  of the identity list's, and prefers an untagged identity over a
  differently tagged sibling
2026-09-24 19:10:44 +02:00
Linus Rath 235f4ea77f fix: stop replies and forwards loading the remote images the viewer blocked
The composer's editor page had no Content-Security-Policy, so quoting a
message loaded every remote image and tracking pixel in it, even when
the viewer had shown that message with remote content blocked.

The editor page now carries a CSP that mirrors the viewer's: nothing
loads but our inline script, styles and images, and remote images and
fonts are blocked whenever the quoted original is one the viewer would
block (untrusted sender and a policy other than "allow"). A reopened
draft that carries a quote counts as untrusted. Inline and pasted
images (`data:`, `blob:`) keep working, and the sent HTML is unchanged.
2026-09-24 19:10:44 +02:00
Linus Rath 92b42e4a4f fix: keep the composer open when its draft can't be saved
"Save draft" returned early without saving when the identities hadn't
loaded (offline when the composer opened) or the account had no Drafts
folder, and the close dialog then closed the composer as if the save had
worked, dropping the text.

The close dialog now offers only Cancel and Discard in that case and
says why the draft can't be saved; the save path refuses to close too.
The close guard moves from a bare `beforeRemove` listener to
`usePreventRemove`, which also stops the iOS modal swipe-down that the
listener could not prevent.
2026-09-24 19:10:44 +02:00
Linus Rath 191f4f32cd fix: stop the date and time pickers offering times past the server's hold limit 2026-09-24 19:10:43 +02:00
Linus Rath 7e598c5bc9 fix: stop a failed reschedule from dropping or double-sending the message
Rescheduling cancelled the original submission and created its
replacement in one `EmailSubmission/set`, whose create and update the
server applies independently. A refused replacement (a hold over the
server's limit, a quota) still cancelled the original, so the message
silently never went out, and a refused cancel (the original had just
gone out) still let the replacement go, so it was sent twice.

Ported the webmail's order:
- the replacement is created first; if it is refused, the original
  stays scheduled
- the original is cancelled only once the replacement exists
- if the original can't be cancelled, the replacement is withdrawn
  again, unless a cancel whose answer got lost did go through
- "Send now" is a 1-second hold, so the replacement can still be
  withdrawn
2026-09-24 19:10:43 +02:00
Linus Rath ebd8dacdfd fix: stop re-rendering every mail row when the list re-renders
The mail list passed `onEndReached` and `onRefresh` as inline arrows,
so FlatList (a pure component) re-rendered on every render of the
screen, and without `strictMode` it then handed its cells a new render
function, so every mounted row re-rendered too. Each row also built a
fresh swipe `context` object and `onAction` closure, so its
`SwipeableRow` could never skip a render. Loading the next page while
scrolling flips `loading` and appends rows, re-rendering up to a couple
of hundred rows mid-fling.

- `onEndReached` and `onRefresh` are stable callbacks and FlatList runs
  with `strictMode`, which the default React Native typings lack, so
  `src/types/react-native-flatlist.d.ts` declares it
- each row is an `EmailListItem`, memoized on primitives and stable
  callbacks, which builds its swipe context and callback once per row
- rows share one swipe callback that reads the current handler through
  a ref, so rebuilding the handler no longer re-renders every row
2026-09-24 19:10:43 +02:00
Linus Rath aa250759e5 fix: keep swipe bands unmounted until a mail row is swiped
Every row in the mail list rendered both swipe action bands behind its
content, with an icon, a label and four fresh `Animated` interpolations
on each render, although a band is only visible while the row is
dragged or held open. `SwipeableRow` was also not memoized.

- the bands now mount when a drag is granted and unmount once the row
  springs back to rest (or has flown off), where they are covered and
  fully transparent, so the swipe looks the same
- the band interpolations are built once per row
- `SwipeableRow` is memoized; `context` compares by its fields since
  callers build it per row
2026-09-24 19:10:43 +02:00
Linus Rath cb9a741244 fix: reuse the list's date formatters instead of building them per row
Every mail row formatted its date through `toLocaleTimeString` and
`toLocaleDateString`, which build a new Intl formatter on each call; a
row from the last week built two, on every render. On Hermes that goes
through ICU over JNI and is the costly part of formatting a date.

`formatListDate` now keeps one `Intl.DateTimeFormat` per locale and
format in a module-level map. The output is unchanged, since those
methods format exactly as `Intl.DateTimeFormat` with the same options;
a test checks the two against each other across locales, formats and
ages. The cache starts over when the device's UTC offset changes, so a
new time zone still shows as it did when every call picked it up.
2026-09-24 19:10:43 +02:00
Linus Rath 695aa23563 fix: show the Bulwark shield instead of the stock envelope in the status bar 2026-09-24 19:09:36 +02:00
Linus Rath 8618abf1d7 fix: open a group mailbox notification's message in the group account #839
The notification only carried the local account it was posted for, so
tapping one for a message in a group or shared mailbox opened the
thread against the user's own JMAP account, where the message doesn't
exist.

The background task now passes the JMAP account the message was
fetched from as `jmapAccountId`. The native module stores it as an
intent extra, validated like the other ids, and hands it back with the
tap. `App.tsx` routes the open with it, the way the unified inbox does
for group messages. Notifications posted before this change have no
such extra and open against the account's own mail as before.
2026-09-24 19:09:36 +02:00
Linus Rath a3d3af797c fix: keep push off after it is turned off or this device is revoked
The launch-time effect in `App.tsx` re-ran the full setup for the
active account whenever a relay URL was stored, and that URL is shared
by every account on the device. Turning push off for one account, or
revoking this device, lasted only until the next launch or token
rotation. A subscription revoked from another device was re-created
without asking.

- Turning push off in settings and revoking this device now go through
  `disablePushForAccount`, which tears the registration down and marks
  the account as opted out. Enabling push again clears the mark.
- The effect calls `resyncPushNotifications`, which skips opted-out
  accounts. If the server no longer has the recorded subscription
  although it still had a day or more to live, it treats the
  subscription as revoked and turns push off for the account instead
  of registering again. A subscription that lapsed is re-created as
  before.
- Setup records the expiry the server reports: from the create
  response (`createPushSubscription` now returns it) and from every
  listing.
2026-09-24 19:09:35 +02:00
Linus Rath ddfe85a5b2 fix: show an error when the server refuses a push registration change
The PushSubscription calls read the first method response without
checking it, so an `error` response listed no devices, and a refused
verify or destroy (revoking another device) was reported as done. Route
every call through `requireMethodResult` and `assertSetResult`; a
destroy the server answers with `notFound` still counts as done.
2026-09-24 19:09:35 +02:00
Linus Rath ccaa24ecd3 fix: stop re-patching the push filter on every launch
`PushSubscription/get` ran with the default properties, which leave out
`emailPush`, so the stored filter never compared equal to the wanted one
and every launch sent it again. The calls also never named the
emailpush capability in `using`.

Ask for `emailPush` explicitly and send
`urn:ietf:params:jmap:emailpush` with every call that reads or writes
it, as the webmail's `lib/jmap/client.ts` does. Servers without the
capability keep the default properties and never see the property, so
the filter is only patched when it differs from what the server holds.
2026-09-24 19:09:35 +02:00
Linus Rath d8046a8aad fix: keep push on for users with mailboxes shared to them by ACL
The emailPush map named every account with mailboxes. Stalwart refuses
the whole map as `forbidden` when it names an account the user neither
owns nor belongs to as a group member, so a user with a mailbox shared
by ACL had every refresh rejected. The refresh path then destroyed the
working subscription and the recreate was refused the same way, leaving
push off for good.

- On a `forbidden` refusal, retry with a narrower map: first the
  accounts that look owned (Stalwart sets `mayCreateCalendar` and its
  siblings only on the user's own and group accounts), then the primary
  account alone. Stalwart only fans a subscription out to owned accounts,
  so nothing is lost.
- Remember the refused accounts per account and leave them out of the
  map on later runs; a forced re-register re-learns them.
- Keep the old subscription until its replacement is verified, so a
  refused or failed replacement never leaves the device without push.
- `createPushSubscription` and `updatePushSubscription` now throw a
  `JMAPMethodError` carrying the SetError type.

The JMAP part is shared by FCM and UnifiedPush, so both get the fix.
2026-09-24 19:09:35 +02:00
Linus Rath 9e95273c14 feat: tell the user when a viewer action fails and when mail is sent
The viewer fired its store actions with `void` and went back to the
list, so a refused delete, archive, move, spam report, star, tag or
read change failed silently, and nothing confirmed a sent message.

- each viewer action now reports a failure as an error toast, and a
  refused star, tag or read change is taken back on screen
- the viewer mounts its own toast host, since the list's sits under it
- an immediate send from the composer or the quick reply box shows
  "Email sent"; a send held for the undo delay keeps its undo bar
  (webmail `ec67ab99`, `b03a0c1d`)
2026-09-24 19:06:55 +02:00
Linus Rath 33d26744b6 fix: archive from the viewer when the list does not hold the message
Viewer Archive looked the message up in the loaded list and silently
did nothing when it was not there: for messages opened from the Unified
Inbox, a notification or a deep link, and for any message past the
loaded page. It also resolved the Archive folder from the folder open
in the list, so it was only offered for mail of that folder's account.

`archiveEmail` now takes the viewed message like delete, move and spam
do: it archives the viewer's copy into the Archive folder of the
message's own account (year/month foldering included) and leaves the
open list alone when that is another account.
2026-09-24 19:06:55 +02:00
Linus Rath 5e42c6737e fix: report spam from the viewer the way the list does #695
The viewer's Spam and Not spam buttons were a plain move into Junk or
Inbox: they sent no `$junk`/`$notjunk` flip, so the server's classifier
never learned from them, and they ignored the "trash-and-read" delete
action. They now call the store's `markSpam`/`unmarkSpam`, the path the
list and swipe actions use, which take the viewed message and file it
in its own account's Junk or Inbox, also when the list does not hold
it. Not spam is only offered when that account has an Inbox.
2026-09-24 19:06:55 +02:00
Linus Rath 9c31de2e80 fix: keep the viewer on the opened message when new mail arrives
The viewer's pager followed the store's live list. New mail or a
refresh shifted the rows under the fixed scroll position, so the
screen switched by itself to another message while Star, Delete and
Archive kept acting on the one that had been opened.

The pages are now taken once when the viewer opens, in `viewerPages`,
so the page on screen and the message the toolbar acts on stay the
same. Delete, archive, move and spam leave the viewer as before, so no
page is left showing a message that is gone.
2026-09-24 19:06:54 +02:00
Linus Rath 5c7301b3f1 fix: stop viewer actions from hitting another account's message with the same id
Stalwart ids are only unique per account. When its opener named no
account, the viewer used the account of the folder open in the list,
and it resolved Trash, Junk and move targets from that folder as well.
Deleting a team message from the Unified Inbox therefore sent
`Email/set` to the user's own account and trashed an unrelated message
with the same id, and a message opened from a notification, deep link
or contact while a shared folder was open went to the shared account.

- the list names the open folder's account when it opens the viewer,
  and the viewer takes the account from its opener only, fixed at open
- Trash, Junk, Inbox and move targets come from the message's account,
  and delete and move take the message out of its own folder
- the store's delete and move take the viewed message: they only touch
  the list's row when the list shows that account, and refuse folders
  of any other account
- another account's message is marked read on the server directly
  instead of through the open folder's list
- quick reply, unsubscribe and read receipts file into the message's
  account's folders
- undo no longer slots another account's message into the open list
- Archive is only offered for mail of the open folder's account, since
  it still resolves the archive folder from there
2026-09-24 19:06:54 +02:00
Linus Rath d987930034 fix: load each list page and search in one request
Opening a folder, searching or loading more took three sequential
round trips: `Email/query`, then `Email/get` of its ids, then a
`Thread/get` the list screen fired after rendering to size the
conversation badges.

- `queryEmailPage` sends all three in one request, chaining
  `Email/get` to the query's `/ids` and `Thread/get` to the fetched
  messages' `/list/*/threadId` (left out while threading is off). The
  page is capped at `maxObjectsInGet` so the chained get can't be
  refused, and a failed `Thread/get` keeps the page.
- The full refresh, search and load-more use it; load-more no longer
  needs its own chunked `Email/get`.
- The thread sizes now live in the email store (`threadCounts`,
  cleared on folder or account change) and the list screen reads them
  instead of fetching them itself. Rows restored from a snapshot or
  added by an incremental refresh get theirs with one `Thread/get`
  after the refresh.
- The chained `Email/get` also reports the Email state for an empty
  folder, so that list gets its own `Email/changes` baseline.
2026-09-24 19:04:37 +02:00
Linus Rath 35e0792558 fix: stop "load more" skipping messages in the Unread and Starred views
Rows read in the Unread view (or unstarred in Starred, untagged in a
tag view) stay on screen, but they have left the server's result.
`loadMoreEmails` still counted them in `position`, so the next page
started that many messages too late, and the `emails.length >= total`
guard could stop paging while server rows were still unloaded.

Page from the number of loaded rows the query still returns, like the
webmail, and take the total from the load-more query so a list that
shrank under the user doesn't keep asking for a page that isn't there.
2026-09-24 19:04:36 +02:00
Linus Rath 0a8686a008 fix: keep messages in the list after a background refresh
`inMailbox` is a mutable filter, so Stalwart's `Email/queryChanges`
reports every updated message as removed and re-added. The incremental
refresh dropped those rows and never re-fetched them because they
"already existed"; they only came back after a pull-to-refresh. It
worked only while `Email/changes` happened to list the same ids, and
the account-wide Email state that call needed was deleted whenever a
folder query returned no ids, so visiting an empty folder was enough
to break the next refresh.

- Fetch every id `Email/queryChanges` adds, including rows already in
  the list, and put them back at their new index with fresh keywords.
- Keep the Email state per folder list (keyed by store mailbox id) and
  run `Email/changes` from the list's own state, like the webmail's
  `emailListSync`, so refreshing another folder can't move it past
  changes this list hasn't seen. A list without a state is re-queried.
- An empty query result keeps the list's previous state instead of
  discarding it.
- An added index past the loaded window is left to load-more instead
  of being appended after a gap.
2026-09-24 19:04:36 +02:00
Linus Rath 668e01023c fix: stop the contacts tab downloading every card on each open
Opening Contacts re-ran the full `ContactCard/query` + `ContactCard/get`
for every account (58 KB on the audit server) although the cards are
loaded at startup and every live `ContactCard` state change already
refetches them.

The tab now only fetches when no complete fetch happened this session,
after a reset or account switch, after a filtered load, or when the
last full fetch is more than 5 minutes old, which covers changes missed
while the app was in the background or live updates were down. Pull
to refresh and the Refresh buttons still force a reload (PF6).
2026-09-24 19:03:35 +02:00
Linus Rath c888e95c5a fix: create new contacts in the address book being viewed 2026-09-24 19:03:35 +02:00
Linus Rath e6be8922b7 fix: stop re-sorting the contact list on every search keystroke
Typing in the Contacts search re-ran the full sort on each keystroke,
with `localeCompare` computing both display names and setting up a
collator for every comparison. The list is now sorted once per
contacts, category or sort-order change and search only filters the
sorted list; the sort itself computes each sort name once and reuses
a single `Intl.Collator` (PF10).
2026-09-24 19:03:34 +02:00
Linus Rath 182774884e feat: add a setting to sort contacts by last name #963
Port of the webmail's `sortContactsByLastName` (fb24ef5d). With the new
"Sort by last name" switch in Settings > Contacts, the contact list is
ordered and letter-grouped by "Surname, Given Middle", so family members
sit together. Cards without a structured surname fall back to the last
word of `name.full`; organizations, nicknames and bare addresses keep
their display name. The setting defaults to off, is persisted, and is
exported and imported under the webmail's key.
2026-09-24 19:03:34 +02:00
Linus Rath a734d0ade1 fix: always write the contact's full name so vCards carry FN #430
`name.full` feeds the vCard FN, which is mandatory; strict CardDAV
clients such as Apple Contacts drop cards without it. The app left it
out or let it go stale on several write paths:

- the contact form (and "Add to contacts", which goes through it) only
  sent `full` when the optional display-name field was filled in
- editing a card put its stored `full` into that field, so renaming
  the person wrote the old name back as FN
- vCard import dropped FN once it had split it into components

Port the webmail's fix (a210b22e): the form derives `full` from the
name components in display order unless a display name of its own is
set, and the edit form only shows `full` when it differs from what
would be derived, so a derived one is recomputed on every save. Import
keeps FN verbatim whether it comes before or after N, and a card with
N but no FN gets a derived `full` too.
2026-09-24 19:03:34 +02:00
Linus Rath 95c24a4214 fix: stop notifications fetching sender favicons when favicons are off
The "Sender Favicons" setting only covered the in-app avatars. Push
notifications always fetched the sender's favicon from
icons.duckduckgo.com, so with the setting off every notification still
sent the sender's domain and the device's IP to a third party.

The webmail has the same setting, on by default, and fetches through
its own `/api/favicon` proxy, so DuckDuckGo sees the server rather than
the user. Native has no proxy, but turning the default off would
silently drop the logos existing users see and diverge from the
webmail, which is a product call. So the default stays on and the one
existing switch now covers every favicon fetch: the notification task
reads it from the persisted settings and shows initials when it is off,
or when the settings cannot be read.
2026-09-24 19:03:34 +02:00
Linus Rath eca3871545 feat: show the sender's contact photo as their avatar #54
Sender avatars ignored address-book photos, so a contact whose photo
shows in Contacts still got initials or a favicon in the mail list,
the viewer and the thread cards. Like the webmail's `Avatar`, the
contact photo now wins over the favicon and the initials; `disableImages`
(Junk) still shows initials only.

The lookup goes through a lowercased email -> photo index that is built
once per contacts array, and each avatar selects just its own photo
string, so a list of avatars neither rescans the address book per row
nor re-renders when an unrelated contact changes. Stalwart returns
photos as inline data URIs, which the index normalizes the same way
Contacts does, so no authenticated download is involved.
2026-09-24 19:03:34 +02:00
Linus Rath d89e33110a fix: load remote content only for senders in the trusted senders book
With "Sync trusted senders to address book" on, the viewer trusted every
address in every address book, so remote images and tracking pixels
loaded for any contact, including mail with a spoofed From that uses a
contact's address. The toggle also switched itself on the first time
Content & Senders was opened on a contacts-capable account, so merely
visiting settings widened trust to the whole address book.

Now only the local allow-list and, while sync is on, the dedicated
"Trusted Senders" book count, as in the webmail viewer. The setting
stays off until the user turns it on, and the viewer only fetches the
book while sync is on.
2026-09-24 19:03:33 +02:00
Linus Rath ac58b36a77 test: give the translation key scans a 30 s timeout so they pass under load 2026-09-24 19:01:24 +02:00
Linus Rath e6549d31da fix: use the right plural forms on Android instead of the English ones
Hermes has no `Intl.PluralRules`, so `src/i18n/format.ts` fell back to
the English rule (1 is "one", everything else "other") for every
language. Russian showed "5 письма" instead of "5 писем", Polish,
Czech, Arabic and the other languages with more than two forms were
wrong in the same way, and French treated 0 as plural.

Add the CLDR 48 cardinal rules for the app's 27 locales and use them
whenever `Intl.PluralRules` is missing or rejects the locale. Engines
that have it keep using it; a test checks the built-in rules against
it for every locale.
2026-09-24 19:01:24 +02:00
Linus Rath 2511df1059 fix: translate the plain text font, message spacing and read receipt settings 2026-09-24 19:01:24 +02:00
Linus Rath a86cd1d0eb feat: manage filters and vacation of shared and group accounts
The phone only ever read and wrote Sieve scripts and the vacation
responder of the user's own account, and the "Shared with me" list in
Account settings said to use the webmail for the rest. Port the
webmail's scoped settings (4c6c1aab) for these two panes:

- `api/sieve.ts` and `api/vacation.ts` take an optional account id and
  gate on the account's own `accountCapabilities` for
  `urn:ietf:params:jmap:sieve` and `urn:ietf:params:jmap:vacationresponse`,
  treating non-personal accounts as capable because Stalwart doesn't
  always advertise capabilities on them. Script blobs are uploaded and
  downloaded against the same account.
- Tapping a shared account in Account settings scopes Settings to it:
  only the Filters and Vacation Responder panes it supports are listed,
  behind a "Managing: <name>" banner that leads back to the own account.
  The scope is dropped when the signed-in account changes.
- The filter and vacation stores remember which account they hold,
  reset when it changes and drop replies for an account the user has
  already left, so one account's rules are never saved into another.
- The rule editor offers the managed account's folders as targets, and
  the "vacation active" banner only shows for the own account.
2026-09-24 19:01:24 +02:00
Linus Rath f63d739fb3 fix: file "Keep" filter rules into the inbox so allow-lists beat the spam filter #1027
A bare `keep;` delivers to the default place, which on Stalwart is Junk
for a message its spam filter has already classified, so an allow-list
rule made with "Keep in inbox" changed nothing. The webmail now writes
`fileinto "INBOX";` for it (62465e1e), but saving filters on the phone
wrote `keep;` again and undid that.

The action stays `keep` in the metadata, so scripts written by either
client read back as "Keep in inbox" and save unchanged.
2026-09-24 19:01:23 +02:00
Linus Rath 72f48db4e5 fix: list and cancel scheduled sends held in shared accounts
A send from a shared or group account's identity (quick reply with the
undo-send delay, or a message scheduled in the webmail) is held in that
account. The Scheduled screen only looked in the primary account, so it
never listed such a message, and it could not be cancelled, rescheduled
or edited before it went out (webmail #874).

The Scheduled list now covers every account that advertises submission
and can hold mail. Each entry carries its account, and cancel,
reschedule, "Send now" and edit act there. Edit restores the message
into that account's Drafts and opens it as a shared draft. A shared
account that fails to answer is skipped, so the user's own scheduled
mail still shows.
2026-09-24 19:00:09 +02:00
Linus Rath 4e7ae2fa30 fix: only offer scheduled-send times the server accepts
Stalwart advertises `maxDelayedSend` as a fixed 30 days, but its MTA
rejects any hold beyond its `futureRelease` limit, 7 days by default.
Now that scheduled send works on Stalwart, a time 8 to 30 days ahead
passed the checks and then failed with the raw "501 5.5.4 Requested
hold time exceeds maximum of 604800 seconds" reply.

Ported from the webmail:
- on Stalwart the advertised 30 days count as 7
- a submission or reschedule refused for its hold time throws
  `ScheduleTooLateError`, and the limit it names replaces the advertised
  one for that server
- the composer and the Scheduled screen show "Too far ahead" for it
  instead of the SMTP reply

The composer's and the Scheduled screen's presets now only list times
within the limit, and the undo-send hold, from the composer and from
quick reply, is capped at it.
2026-09-24 19:00:08 +02:00
Linus Rath 48825e919a fix: stop rewriting the mail, contacts and calendar caches on every state change
zustand's JSON storage serialises the whole partialized slice and
writes it to AsyncStorage on every set(), including ones that only
flip `loading`. Opening one unread message rewrote the mail cache
about five times.

`createPersistStorage` now coalesces writes per store:
- a slice whose fields are the same references as the last one is
  dropped without serialising; the email and contacts slices are
  built through `memoizeSlice`, so a set() that only touches UI flags
  hands over the same slice
- other changes wait up to 1 s and only the newest is serialised, and
  JSON identical to the stored row, or to the row just hydrated from,
  isn't written again
- pending writes are flushed when the app leaves the foreground and
  when an account is signed out
- the account registry is still written straight away, since the
  headless push task reads it from AsyncStorage
- "Refresh cached data" removes the rows through the stores, so their
  storage forgets the old JSON and writes the re-fetched data back
2026-09-24 18:56:38 +02:00
Linus Rath 24849baca5 fix: let the offline mail cache grow past 6 MB on android
AsyncStorage caps its Android SQLite database at 6 MB unless
`AsyncStorage_db_size_in_MB` is set. The offline cache keeps message
bodies in that database, defaults to 50 MB and can be set to 500 MB,
so it filled the database long before its own limit, after which every
AsyncStorage write failed, including the account registry, the outbox
and the mail list cache. Raise the ceiling to 1 GB. It is a maximum,
not a preallocation, and needs a native rebuild to take effect.
2026-09-24 18:56:37 +02:00
Linus Rath e37621be5e fix: cap the cached mail list so android can still read it back
The whole email cache is one AsyncStorage row, and Android can't read a
row over ~2 MB. The row held every window the user had paged through,
in every folder of every account, stored the open folder twice (as
`emails` and in `mailboxSnapshots`), kept a stale copy of the active
account in `accountSnapshots`, and rows seeded from the offline body
cache carried whole message bodies. Once it outgrew the limit the next
cold start could not load it.

- the active view is persisted the way an account switch tucks it away
  (`snapshotFromActive`), so the open folder is stored once, and
  `merge` rebuilds `emails` from it with `viewFromSnapshot`
- `boundEmailCache` keeps at most 100 rows per folder (the largest page
  size, so the next refresh can still patch the window) and 500
  overall, the open folder first; a window is cut to a prefix, which
  keeps its queryState valid, and a folder past the budget is dropped
- body parts are stripped from persisted rows and the active
  account's stale entry is left out of `accountSnapshots`

500 typical rows serialise to about 0.3 MB.
2026-09-24 18:56:37 +02:00
Linus Rath 29169c3426 fix: stop session restore hanging when a persisted cache can't be read
zustand only marks a persisted store hydrated when its storage read
succeeds, and `restoreSession` waited on the account, email, calendar
and contacts stores with no timeout. A read that failed, such as an
email-cache row too large for Android's ~2 MB CursorWindow or a row
that no longer parses, left the app on the splash screen for good.

- the four stores now use `createPersistStorage`
  (`src/stores/persist-storage.ts`), whose read logs the failure and
  resolves as nothing stored, so the store hydrates with its defaults
  and its next write replaces the bad row
- `waitForHydration` gives up after `HYDRATION_TIMEOUT_MS` (5 s) and
  carries on, covering a hydration that throws in migrate/merge or a
  storage call that never settles
2026-09-24 18:56:37 +02:00
Linus Rath 5237848312 docs: add the android release signing and key rotation guide
`docs/android-release.md` walks through generating the release key
with keytool, creating the debug-to-release signing lineage with
`apksigner rotate` and which capabilities the debug key keeps, the
five repository secrets, and how to check the first rotated release
on the emulators. It spells out what rotation protects and what it
does not: devices that have not updated yet, Android 7.0-8.1, which
still verify the debug-signed v2 signature, and when the lineage can
be dropped. It also covers local release builds and the `versionCode`
formula. The README links to it instead of the bare workflow file.
2026-09-24 18:52:15 +02:00
Linus Rath 746572c784 fix: raise the android versionCode with every release
`versionCode` was hard-coded to 1, so every release looked like the
same version to Android. It is now derived from `VERSION` as
`major * 10000000 + minor * 10000 + patch` (0.1.62 gives 10062), which
is above the 1 existing installs have and keeps growing, so Android
never mistakes an update for a downgrade.

The formula only increases while patch stays below 10000 and minor
below 1000. `build.gradle` refuses a `VERSION` outside that range or
outside Android's 2100000000 ceiling, and `scripts/bump-version.mjs`,
which only ever bumps patch, refuses to create such a version before
anything is committed or tagged.
2026-09-24 18:52:15 +02:00
Linus Rath 6f0abb4384 fix: stop signing release apks with the public debug key
Every published APK up to 0.1.62 was signed with the React Native
template's debug keystore, whose key is public. Anyone could sign an
"update" that Android installs over the app, with access to its
SecureStore credentials and mail cache. The release build type fell
back to the debug signing config whenever no release key was passed,
and the workflow carried on silently when `ANDROID_KEYSTORE_BASE64`
was empty.

- `android/app/build.gradle` reads the release key from the
  `BULWARK_RELEASE_*` gradle properties or environment variables and
  has no fallback. Without them a release build fails in the new
  `checkReleaseSigning` task. It only runs for release variants, so
  debug builds and `expo run:android` are unchanged. A local test
  build can opt in with `-PallowDebugSignedRelease=true`, which is
  ignored whenever `CI` is set.
- The release workflow fails in its first step when a signing secret
  is missing, checks the keystore, alias and password before the
  build, and refuses a debug key uploaded as the release key.
- An optional `ANDROID_SIGNING_LINEAGE_BASE64` secret re-signs the APK
  with APK Signature Scheme v3 key rotation from the debug key to the
  release key, from API 28, so existing installs on Android 9+ can
  update and stop trusting the debug key afterwards.
- Before anything is uploaded, `apksigner verify` has to show the
  release key as the Android 9+ signer, never the debug certificate.
- `*.keystore` and `*.lineage` files are gitignored.

Audit item S2 in `docs/audit-2026-09.md`.
2026-09-24 18:52:15 +02:00
Linus Rath 1d1d6a5bfc fix: hide body-embedded cid parts typed octet-stream from the attachment chips
Some senders ship the images their HTML body embeds by `cid:` as
`application/octet-stream` with no disposition and no name. The body
rendered them, but the chip filter only hid parts declared inline with
an image type, so they also showed up as nameless "Attachment" chips.

Port the webmail's rule: scan the HTML the body actually renders for
`cid:` references and hide a referenced part when it is an image or
generically typed. An explicit attachment disposition keeps its chip,
as does a referenced part with a real non-image type. A plain-text
render references nothing, so nothing extra is hidden there.
2026-09-24 18:50:55 +02:00
Linus Rath d45c7996d2 fix: keep list previews and subjects on one line
Previews keep the body's paragraph breaks, so a line-clamped row showed
a dangling "…" line and rows came out at uneven heights, and a subject
carrying CR/LF rendered over several lines in the list and the viewer.
Fold every whitespace run to a single space before display with a new
`singleLine` helper, in the mailbox and unified inbox rows, the viewer
subject and the collapsed thread card preview.
2026-09-24 18:50:55 +02:00
Linus Rath 476780553c fix: show large message bodies in full instead of a cut-off stub #884
Full-message fetches cap body values at 512 KB and never read
`isTruncated`, so a bigger body rendered whatever the server cut off
with no notice; for a 586 KB HTML report Stalwart returned a 59
character stub and the viewer showed a heading, one line and "…".

Port the webmail's refetch: when the first HTML or text body part
comes back truncated, `getFullEmail`, `getFullEmails` and
`getThreadEmails` fetch the HTML and text body values of those messages
again in one batched `Email/get` with an 8 MB cap, keeping the values
of other parts. Truncated text attachments don't trigger it. If the
body is still truncated, or the refetch fails, the viewer now says the
message is too large to show in full.
2026-09-24 18:50:55 +02:00
Linus Rath 910c503f4a fix: render right-to-left mail right-to-left
The HTML and plain-text body documents had no direction, so an Arabic
or Hebrew message was laid out left-to-right: left-aligned, with its
sentence-final punctuation on the wrong side. Port the webmail's
`dir="auto"` on the body of both documents so the first strong
character picks the direction. An email that declares its own `dir`
on its `<html>` or `<body>` tag keeps it; the parser merges those
attributes into ours only while ours has none.

The shrink-to-fit reporter widens its wrapper to the content's natural
width and scales it from the top left. In a right-to-left body that
over-wide block overflowed to the left instead, so a 600px newsletter
scaled into the space left of the screen. A negative right margin now
keeps the widened wrapper flush left in either direction, so the
existing origin, pan clamp and pinch anchoring still hold.
2026-09-24 18:50:55 +02:00
Linus Rath 7531118727 fix: let sender images and tables keep their own max-width #790
The body stylesheet forced `max-width: 100% !important` onto every image
and table, which also beat a sender's inline max-width. An image sized
with `max-width:200px; max-height:134px; width:100%` grew to the pane
width while its max-height still clamped the height, so it rendered
squashed, and a table with `max-width:300px` spanned the pane.

Only force the cap on images and tables that declare no max-width of
their own, as the webmail does. Content wider than the pane is still
scaled down by the injected reporter, and images keep
`height: auto !important`.
2026-09-24 18:50:55 +02:00
Linus Rath 55f1ec431f chore: sync the locale catalogs from the webmail
The vendored catalogs were last synced on 2026-08-29 and had fallen
about a hundred keys behind, including the German update and the
translations of `email_viewer.forward_as_attachment`.

- re-vendor all 27 `locales/<lang>/common.json` from webmail e33ab899
- drop the overlay's `email_composer.send_failed`, which the webmail
  catalog now carries with translations
- keep 15 keys the webmail renamed or dropped but the app still uses,
  moved into `locales/rn/en.json` by `npm run i18n:harvest`
2026-09-24 18:46:59 +02:00
Linus Rath cb2f03bc09 fix: mark read and star a list selection in one request
Bulk mark read/unread and star/unstar from the list selection ran the
single-message action once per message, so a large selection sent one
`Email/set` per message against Stalwart's limit of four concurrent
requests. They now go through `setKeywordForEmails`, which sends one
`keywords/$seen` or `keywords/$flagged` patch for the whole selection
(chunked by `maxObjectsInSet`). It also keeps rows that leave an open
Unread or Starred view until the view is re-opened, as the single
actions do.
2026-09-24 18:44:29 +02:00
Linus Rath 50418971f4 fix: stop mark-read, star, pin and tag from erasing other keywords
Every keyword change sent the message's whole `keywords` map, which the
server takes as a replacement. Opening a message that wasn't in the
loaded list sent `{"$seen": true}` and wiped its star and tags; starring
a listed message sent the list's stale map and reverted keywords another
client had changed since the list loaded.

Keyword changes now go out as `keywords/<name>` patch pointers (`true`
to set, `null` to clear) everywhere:
- mark read/unread, star, pin and tagging in the email store
- spam / not spam, trash-and-read, and the spam undo, which now puts
  back only the keywords the action touched
- star, tag and unread from the message viewer
- the offline outbox, whose keyword ops carry a patch and merge per
  keyword when coalesced

Ops queued by earlier builds still hold a whole map. They replay as a
patch that only sets the keywords in it (clearing the other half of
`$junk`/`$notjunk`), so an unread or unstar queued before the update is
lost but nothing gets erased. The list and the offline cache apply the
same patch locally instead of overwriting their keyword maps.
2026-09-24 18:44:28 +02:00
Linus Rath 859770b814 fix: stop signing out on launch when a redirect drops the auth header
When a proxy redirects the session request (an http to https upgrade, a
trailing slash, another port), iOS and OkHttp follow it without the
Authorization header. The server then answers 401 or, like Stalwart, an
empty session, which the client reported as invalid credentials, so a
restored account was evicted and its stored credentials deleted. The
retry that was meant to catch this keyed on `response.redirected`,
which React Native's fetch never sets.

Session discovery now also detects a redirect by comparing the final
`response.url` with the requested URL, and on a 401 or an empty session
refetches the final URL with the header. Credentials only follow within
the same host or its https upgrade, the rule `secureFetch` applies; a
redirect elsewhere fails as a discovery error, which `loadAccount`
turns into a `NetworkError`, so the stored account is kept.
2026-09-24 18:43:56 +02:00
Linus Rath c51a848dfb fix: stop reporting a send as failed when only filing it into Sent failed
`sendEmail` threw on any method error in the response. Stalwart appends
one for the implicit `onSuccessUpdateEmail` Email/set that moves the
sent message from Drafts to Sent, so a message that had already gone
out showed "Send failed" and a retry sent it a second time. A method
error that follows a created submission is now returned as a filing
warning, like the record-level filing failures already were; errors
before the submission exists still fail the send.
2026-09-24 18:43:56 +02:00
Linus Rath e67947aa93 fix: keep cc and bcc recipients when rescheduling a send
Rescheduling a held message rebuilt the replacement submission's
envelope from the Email's To only, so Cc and Bcc recipients silently
stopped getting the message, and a catch-all From was used as the
envelope sender the server refuses. The replacement now reuses the held
submission's envelope (`rcptTo` and `mailFrom`), falling back to the
message's To, Cc and Bcc when the server returns none.
2026-09-24 18:43:56 +02:00
Linus Rath 802b19916c fix: hide the schedule button when the server can't schedule sends 2026-09-24 18:43:56 +02:00
Linus Rath e28e8b4d9c fix: turn scheduled send and the undo-send delay back on for Stalwart #57
RFC 8621 puts the submission limits (`maxDelayedSend`,
`submissionExtensions`) and the mail limits
(`maxSizeAttachmentsPerEmail`) under each account's
`accountCapabilities`. Stalwart advertises an empty object for them at
session level, which is the only place the client looked, so scheduled
send was refused, the undo-send delay silently fell back to an instant
send, the Scheduled view listed nothing and the per-message attachment
cap was never enforced.

The client now reads these from the account that sends (the account's
own submission capability, else the session's primary submission
account, like the webmail), falling back to the session-level object
for servers that only advertise it there. Quick reply, which can send
from a shared account, now checks that account before holding the
message for the undo-send delay.
2026-09-24 18:43:56 +02:00
Linus Rath ee652d39df fix: open mailto links whose address contains a plus
The mailto parser decoded `+` as a space, as in HTML form encoding, so
`mailto:alice+news@partner.example` became "alice news@…", failed
validation and opened nothing; a `+` in a subject or body was turned
into a space as well. RFC 6068 only uses percent-encoding, so a `+` now
stays a plus, like the webmail's `parseMailtoUrl`.

App compose links (`bulwarkmobile://compose?…`) went through the same
parser after `URLSearchParams.toString()`, which writes spaces as `+`.
They are now re-encoded with percent-encoding, and the recipient is read
from the `to=` parameter only, which also stops it from being added
twice.
2026-09-24 18:43:43 +02:00
Linus Rath 06e89422bd fix: open the composer for mailto links sent with SENDTO
Most apps start an email with `ACTION_SENDTO` and a `mailto:` URI. The
manifest offered the app for it, but React Native's Linking only
forwards VIEW intents, so the app opened on the inbox instead of the
composer.

MainActivity now rewrites a `SENDTO mailto:` intent into the equivalent
`VIEW mailto:` link before React Native sees it, on a cold start and
when the running app receives it. The EXTRA_EMAIL, EXTRA_CC, EXTRA_BCC,
EXTRA_SUBJECT and EXTRA_TEXT extras are folded into the link's query.
Bcc recipients of a mailto link now reach the composer too; they were
dropped for VIEW links as well.
2026-09-24 18:43:43 +02:00
Linus Rath cd4ce9d08b fix: upload files shared into the app with their real name and type
Sharing a photo or document from another app into Bulwark failed every
time: the upload task was handed the share's `content://` URI, but it
can only stream from a file path, so it failed with "Directory for
'/external/images/media/…' doesn't exist". The attachment was also named
after the URI's last segment (a MediaStore row id such as "1000000027"),
and SEND_MULTIPLE shares only carried a wildcard type like `image/*`.

- `uploadBlob` copies `content://` URIs into the cache, uploads the copy
  and deletes it afterwards, also when the upload fails
- `ShareIntentStore` asks the providing app for the display name, size
  and MIME type when the intent arrives, falling back to the URI segment
  plus an extension for the MIME type
- the composer gets the real name, type and size, so the size limit
  check now applies to shared files too
2026-09-24 18:43:43 +02:00
Linus Rath 8d7fbfc99c fix: clear the Files selection on Android back instead of switching to Mail 2026-09-24 18:41:23 +02:00
Linus Rath caea3d0c60 fix: offer sharing files and calendars with other users on Stalwart
`supportsSharing()` required `urn:ietf:params:jmap:principals:owner`,
which Stalwart advertises nowhere; its session only has `principals` and
`principals:availability`. The file menu therefore never showed
"Sharing & access", and `getPrincipals()` returned nothing, so the
calendar share sheet had no one to offer either.

- Gate sharing on `urn:ietf:params:jmap:principals`, like the webmail's
  `supportsPrincipals()`.
- Keep `principals:owner` out of FileNode requests unless the server
  advertises it, since an unknown capability in `using` fails the whole
  request (why the gate had been tied to it).
- Page the principal lookup by `maxObjectsInGet`, as the webmail does:
  one unbounded `Principal/query` fed every id to `Principal/get`, which
  the server refuses once the directory has more than 500 entries.
2026-09-24 18:41:23 +02:00
Linus Rath 00ad2f49fb fix: show an error when the server refuses a file rename, move or delete
`updateFileNode` and `deleteFileNodes` only looked for `notUpdated` and
`notDestroyed` in the first method response. A method-level `error`
reply (e.g. `forbidden`) has neither, so a refused rename, move or delete
reported success and the Files tab silently refreshed to the old state.

Every `FileNode/set` in `src/api/files.ts` now goes through
`requireMethodResult`, so the error reaches the existing alert in the
Files screen and the share sheet with the server's description.
2026-09-24 18:41:23 +02:00
Linus Rath 4eb8c47670 fix: list every file when the account holds more than 500 nodes #1069
The Files tab read the whole drive with `FileNode/get ids:null`, which
Stalwart silently caps at `maxObjectsInGet` (500 by default). Anything
past that, including folders created after the first 500 files and
everything inside them, never showed up, both for the own drive and for
shared accounts.

A full first page now counts as truncated, as in the webmail: the
remaining ids come from paging `FileNode/query`, are fetched in
`maxObjectsInGet`-sized `FileNode/get` batches packed into as few
requests as `maxCallsInRequest` allows, and parents that are still
unknown afterwards are fetched by id, which recovers folders on Stalwart
before 0.16.6 where the query returns files only. If the paging fails,
the first page is kept as before.
2026-09-24 18:41:23 +02:00
Linus Rath bd42206686 fix: accept a pasted JMAP session URL as the server address #971 2026-09-24 18:40:43 +02:00
Linus Rath ab845783ab fix: stop adding an OAuth account from signing in the current user again
Adding a second account through the server's own OAuth opened the
authorization endpoint in the shared system browser with no `prompt`,
so the identity provider found the session of the account that was
already signed in and quietly handed back the same user.

When adding an account, the PKCE flow now sends
`prompt=select_account` like the webmail does (5771a213), or
`prompt=login` when the server's metadata lists no account picker, and
on iOS it runs the auth session ephemerally so the browser's cookies
don't carry over. The webmail hand-off gets the ephemeral session too.
Android's Custom Tabs have no private mode, so there the prompt is the
only guard.
2026-09-24 18:40:43 +02:00
Linus Rath 5f3f584ca8 fix: auto-send read receipts only for the message on screen and clean up failed ones
With "Always send" the banner's auto-send ran in every page the viewer
pager keeps mounted, so opening one message also answered the receipt
requests of its unseen neighbours. The receipt is imported into Sent
before it is submitted, so a refused submission left a "Read: ..." copy
there, and the failure cleared the per-instance guard, so the next
render or remount imported another one.

- the pager marks the page on screen `active`, and the banner only
  auto-sends there
- an automatic receipt is tried once per message per session, keyed by
  account and id, so a failure or a remounted page does not send again
- `sendReadReceipt` destroys the imported copy when the submission
  fails, which also covers the manual "Send receipt" button
2026-09-24 18:36:15 +02:00
Linus Rath cb6ddd6fcf fix: strip CR/LF from read-receipt header values (GHSA-w38p-hpqv-g89c)
The read receipt is assembled as raw RFC 5322 text, and every header
value in it comes from the message being answered. Stalwart returns
subjects already RFC 2047-decoded, so a sender could hide CRLF in an
encoded-word subject (or in the Message-ID or
Disposition-Notification-To) and have it interpolated verbatim. That
let them add headers such as Reply-To, or a whole body, to a message
the user's own account files in Sent and submits. `encodeHeaderWord`
passed any pure-ASCII value through untouched, CR/LF included.

Port the webmail fix: free-text values go through
`stripHeaderControls` (controls collapse to a space, then RFC 2047 if
non-ASCII), address-like values and message-ids through `headerAtom`
(controls and whitespace removed), and the From display name is quoted
when it uses specials.
2026-09-24 18:36:15 +02:00
Linus Rath ee04e95d4f feat: add UnifiedPush as an alternative to FCM for push notifications 2026-09-24 18:17:43 +02:00
Linus Rath 5b8c4353d8 Merge pull request #53 from lucletoffe/fix/calendar-description-links
fix(calendar): make URLs in the event description tappable
2026-09-09 22:20:23 +02:00
lucletoffeandClaude Opus 5 c2584c2bd5 fix(calendar): make URLs in the event description tappable
A meeting invitation puts the join URL in the description - Teams, Meet,
Zoom and the plain iTIP fallbacks all do - and the event detail sheet
rendered it as bare <Text>. The one actionable thing in the event could
not be tapped: you had to select the URL by hand and paste it into a
browser. The mail body already linkifies (plainTextToSafeHtml, rendered
in the WebView); only the calendar was left out.

`splitTextLinks` (lib/linkify-text) splits the text into runs and marks
the http(s) URLs, using the same rule as plainTextToSafeHtml so a message
linkifies identically in the body and in the calendar. DetailRow gains an
opt-in `linkify` prop - set on the description row only, not on rows the
app composed itself - and hands each URL to `openExternalUrl` with
`confirm: true`, the same guard the "Open link" button on the video row
already uses: scheme allow-list plus the host confirmation, since this
text arrives in someone else's data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QPPAw4SK4M9Z7euVPM2cL
2026-09-04 09:49:44 +02:00
Linus Rath 6c82130267 chore: update version number to 0.1.62 2026-08-30 00:37:37 +02:00
Linus Rath f228257ec9 fix: stop the viewer crashing on any message with attachments by keeping store selectors referentially stable 2026-08-29 22:41:09 +02:00
Linus Rath a02e7f8a49 test: add an opt-in live Stalwart integration suite 2026-08-29 22:28:42 +02:00
Linus Rath ea1f53f550 fix: keep the created message id when the server answers a send with an implicit filing update 2026-08-29 22:28:41 +02:00
Linus Rath 4e035f3011 chore: update version number to 0.1.61 2026-08-29 20:25:25 +02:00
Linus Rath e04bea451a fix: check the GitHub CLI login before bumping so a failed release does not leave a bare tag 2026-08-29 20:20:54 +02:00
Linus Rath 5cb28f5d5d chore: update version number to 0.1.60 2026-08-29 20:20:15 +02:00
431 changed files with 66890 additions and 4404 deletions
+181 -12
View File
@@ -10,6 +10,13 @@ on:
required: false
default: ""
# Signing is described in docs/android-release.md.
env:
# Certificate SHA-256 of android/app/debug.keystore, the stock React Native
# template key (CN=Android Debug, SHA-1 5E:8F:16:06:...:F6:25). It is public,
# so Android 9+ must never see it as the signer of a published build.
DEBUG_CERT_SHA256: fac61745dc0903786fb9ede62a962b399f7348f0bb6f899b8332667591033b9c
jobs:
build:
runs-on: ubuntu-latest
@@ -17,6 +24,26 @@ jobs:
permissions:
contents: write
steps:
# Fail in seconds instead of after a 20-minute build. There is no
# unsigned or debug-signed fallback: anything signed with the debug key
# can be "updated" by anyone.
- name: Require the release signing secrets
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
missing=""
[ -n "$KEYSTORE_BASE64" ] || missing="$missing ANDROID_KEYSTORE_BASE64"
[ -n "$KEYSTORE_PASSWORD" ] || missing="$missing ANDROID_KEYSTORE_PASSWORD"
[ -n "$KEY_ALIAS" ] || missing="$missing ANDROID_KEY_ALIAS"
[ -n "$KEY_PASSWORD" ] || missing="$missing ANDROID_KEY_PASSWORD"
if [ -n "$missing" ]; then
echo "::error title=Release signing is not configured::Missing repository secrets:$missing. Release APKs are never signed with the public debug key; see docs/android-release.md."
exit 1
fi
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -66,23 +93,38 @@ jobs:
- name: Install JS dependencies
run: npm ci
- name: Decode release keystore
id: keystore
# Checks the password and alias before the long build, and refuses a
# debug key that was uploaded as the release key by mistake.
- name: Decode and check the release key
id: key
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
if [ -n "$KEYSTORE_BASE64" ]; then
echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.keystore"
echo "path=$RUNNER_TEMP/release.keystore" >> "$GITHUB_OUTPUT"
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
set -euo pipefail
KEYSTORE="$RUNNER_TEMP/release.keystore"
CERT="$RUNNER_TEMP/release-cert.der"
echo "$KEYSTORE_BASE64" | base64 -d > "$KEYSTORE"
if ! keytool -exportcert -keystore "$KEYSTORE" -alias "$KEY_ALIAS" \
-storepass:env KEYSTORE_PASSWORD -file "$CERT"; then
echo "::error title=Release key unreadable::Could not read alias ANDROID_KEY_ALIAS from ANDROID_KEYSTORE_BASE64 with ANDROID_KEYSTORE_PASSWORD."
exit 1
fi
CERT_SHA256=$(sha256sum "$CERT" | cut -d' ' -f1)
OWNER=$(keytool -printcert -file "$CERT")
if [ "$CERT_SHA256" = "$DEBUG_CERT_SHA256" ] || [[ "$OWNER" == *"CN=Android Debug"* ]]; then
echo "::error title=Release key is a debug key::ANDROID_KEYSTORE_BASE64 holds an Android debug key. Generate a release key as described in docs/android-release.md."
exit 1
fi
echo "Release certificate SHA-256: $CERT_SHA256"
echo "store_file=$KEYSTORE" >> "$GITHUB_OUTPUT"
echo "cert_sha256=$CERT_SHA256" >> "$GITHUB_OUTPUT"
- name: Build release APK
working-directory: android
env:
ORG_GRADLE_PROJECT_BULWARK_RELEASE_STORE_FILE: ${{ steps.keystore.outputs.path }}
ORG_GRADLE_PROJECT_BULWARK_RELEASE_STORE_FILE: ${{ steps.key.outputs.store_file }}
ORG_GRADLE_PROJECT_BULWARK_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ORG_GRADLE_PROJECT_BULWARK_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ORG_GRADLE_PROJECT_BULWARK_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
@@ -90,15 +132,135 @@ jobs:
chmod +x ./gradlew
./gradlew assembleRelease --no-daemon
- name: Rename APK with version
- name: Put apksigner on the PATH
run: |
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/*/ | sort -V | tail -n 1)
echo "Using $BUILD_TOOLS"
echo "$BUILD_TOOLS" >> "$GITHUB_PATH"
# Optional v3 key rotation away from the debug key that signed every
# release up to 0.1.62. Gradle signed the APK with the release key only,
# which existing installs reject as a different app. With a lineage the
# APK is re-signed so that Android 9+ sees the release key plus proof
# that the debug key handed over to it, while Android 7-8.1 (v2 only)
# still see the debug key. docs/android-release.md has the details.
- name: Apply the signing key rotation
id: rotation
env:
LINEAGE_BASE64: ${{ secrets.ANDROID_SIGNING_LINEAGE_BASE64 }}
KEYSTORE: ${{ steps.key.outputs.store_file }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
RELEASE_CERT_SHA256: ${{ steps.key.outputs.cert_sha256 }}
run: |
set -euo pipefail
if [ -z "$LINEAGE_BASE64" ]; then
echo "No ANDROID_SIGNING_LINEAGE_BASE64: keeping the release-key-only signature."
echo "rotated=false" >> "$GITHUB_OUTPUT"
exit 0
fi
LINEAGE="$RUNNER_TEMP/signing.lineage"
echo "$LINEAGE_BASE64" | base64 -d > "$LINEAGE"
apksigner lineage --in "$LINEAGE" --print-certs | tee "$RUNNER_TEMP/lineage.txt"
DIGESTS=$(sed -n 's/^Signer #[0-9]* in lineage certificate SHA-256 digest: //p' "$RUNNER_TEMP/lineage.txt")
if [ "$(sed -n '1p' <<< "$DIGESTS")" != "$DEBUG_CERT_SHA256" ] \
|| [ "$(sed -n '$p' <<< "$DIGESTS")" != "$RELEASE_CERT_SHA256" ]; then
echo "::error title=Wrong signing lineage::ANDROID_SIGNING_LINEAGE_BASE64 must lead from the debug key to the key in ANDROID_KEYSTORE_BASE64."
exit 1
fi
# Rollback on an old signer lets anything signed with it take the app
# back after the rotation, which would undo the whole point.
PAST_ROLLBACK=$(grep 'Has rollback capability' "$RUNNER_TEMP/lineage.txt" | sed -e 's/.*: *//' -e '$d')
if [[ "$PAST_ROLLBACK" == *true* ]]; then
echo "::error title=Unsafe signing lineage::An old signer in the lineage has the rollback capability. Recreate it with --set-rollback false."
exit 1
fi
# Oldest signer first: apksigner uses it for v2, and the rotated key
# for v3 from API 28. Without --rotation-min-sdk-version it would
# only rotate on Android 13+ (v3.1) and leave 9-12L on the debug key.
APK=android/app/build/outputs/apk/release/app-release.apk
apksigner sign \
--v4-signing-enabled false \
--ks android/app/debug.keystore --ks-key-alias androiddebugkey \
--ks-pass pass:android --key-pass pass:android \
--next-signer \
--ks "$KEYSTORE" --ks-key-alias "$KEY_ALIAS" \
--ks-pass env:KEYSTORE_PASSWORD --key-pass env:KEY_PASSWORD \
--lineage "$LINEAGE" \
--rotation-min-sdk-version 28 \
--out "$RUNNER_TEMP/app-release-rotated.apk" \
"$APK"
mv "$RUNNER_TEMP/app-release-rotated.apk" "$APK"
echo "rotated=true" >> "$GITHUB_OUTPUT"
# The last gate before anything is uploaded.
- name: Verify the APK signature
env:
RELEASE_CERT_SHA256: ${{ steps.key.outputs.cert_sha256 }}
ROTATED: ${{ steps.rotation.outputs.rotated }}
run: |
set -euo pipefail
APK=android/app/build/outputs/apk/release/app-release.apk
apksigner verify --verbose --print-certs "$APK"
signers() {
apksigner verify --print-certs "$@" "$APK" \
| sed -n 's/^Signer #[0-9]* certificate SHA-256 digest: //p'
}
MODERN=$(signers --min-sdk-version 28)
LEGACY=$(signers --max-sdk-version 27)
echo "Signer on Android 9+: $MODERN"
echo "Signer on Android 7-8.1: $LEGACY"
if [[ "$MODERN" == *"$DEBUG_CERT_SHA256"* ]]; then
echo "::error title=APK is debug-signed::Android 9+ would see the public debug key as the signer."
exit 1
fi
if [ "$MODERN" != "$RELEASE_CERT_SHA256" ]; then
echo "::error title=Wrong APK signer::Android 9+ would see '$MODERN', expected only the release key $RELEASE_CERT_SHA256."
exit 1
fi
if [ "$ROTATED" = "true" ]; then
echo "::warning title=Android 7-8.1 still trust the debug key::The v2 signature is made with the debug key so that installs on Android 7-8.1 can update. Those devices remain exposed; see docs/android-release.md."
elif [ "$LEGACY" != "$RELEASE_CERT_SHA256" ]; then
echo "::error title=Wrong APK signer::Android 7-8.1 would see '$LEGACY', expected only the release key $RELEASE_CERT_SHA256."
exit 1
fi
aapt2 dump badging "$APK" | grep '^package:' || true
# The R8 mapping goes out with the APK: without the one from this exact
# build, the obfuscated frames in a release stack trace cannot be
# decoded (docs/android-release.md, "Stack traces").
- name: Rename APK and R8 mapping with version
id: artifact
run: |
set -euo pipefail
VERSION=$(cat VERSION | tr -d '[:space:]')
SRC=android/app/build/outputs/apk/release/app-release.apk
DEST="bulwark-mobile-${VERSION}-${GITHUB_SHA::7}.apk"
cp "$SRC" "$DEST"
MAPPING_SRC=android/app/build/outputs/mapping/release/mapping.txt
MAPPING=""
if [ -f "$MAPPING_SRC" ]; then
MAPPING="bulwark-mobile-${VERSION}-${GITHUB_SHA::7}-mapping.txt"
cp "$MAPPING_SRC" "$MAPPING"
elif grep -Eq '^android\.enableMinifyInReleaseBuilds=true\s*$' android/gradle.properties; then
echo "::error title=R8 mapping missing::R8 is on but the build wrote no $MAPPING_SRC, so stack traces from this APK could not be decoded."
exit 1
fi
echo "path=$DEST" >> "$GITHUB_OUTPUT"
echo "mapping=$MAPPING" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
{
echo "files<<FILES_EOF"
echo "$DEST"
if [ -n "$MAPPING" ]; then echo "$MAPPING"; fi
echo "FILES_EOF"
} >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
@@ -106,6 +268,13 @@ jobs:
path: ${{ steps.artifact.outputs.path }}
if-no-files-found: error
- uses: actions/upload-artifact@v4
if: steps.artifact.outputs.mapping != ''
with:
name: ${{ steps.artifact.outputs.mapping }}
path: ${{ steps.artifact.outputs.mapping }}
if-no-files-found: error
- name: Resolve target release tag
id: target_tag
env:
@@ -125,9 +294,9 @@ jobs:
echo "No release tag resolved - APK is in workflow artifacts only."
fi
- name: Attach APK to release
- name: Attach APK and R8 mapping to release
if: steps.target_tag.outputs.tag != ''
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.target_tag.outputs.tag }}
files: ${{ steps.artifact.outputs.path }}
files: ${{ steps.artifact.outputs.files }}
+3
View File
@@ -17,6 +17,9 @@ expo-env.d.ts
*.p12
*.key
*.mobileprovision
*.keystore
!/android/app/debug.keystore
*.lineage
# Metro / Expo dev-server logs
.metro-health-check*
+166 -55
View File
@@ -2,7 +2,9 @@ import React from 'react';
import { ActivityIndicator, AppState, Linking, StyleSheet, Text, View } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { useColorScheme } from 'react-native';
import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native';
import {
DarkTheme, DefaultTheme, NavigationContainer, createNavigationContainerRef, useIsFocused,
} from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator, type NativeStackScreenProps } from '@react-navigation/native-stack';
import { Mail, Calendar, BookUser, HardDrive, Settings } from 'lucide-react-native';
@@ -11,6 +13,15 @@ import { startLiveUpdates, type LiveUpdatesHandle } from './src/api/push-stream'
import { jmapClient } from './src/api/jmap-client';
import type { StateChange } from './src/api/types';
import { dispatchStateChange } from './src/lib/state-change-bus';
import {
startCalendarNotificationSync,
startCalendarReminderTapHandling,
} from './src/lib/calendar-notifications';
import { startLivenessMonitor } from './src/lib/connection-liveness';
import {
setPendingCalendarOpen,
type CalendarReminderTarget,
} from './src/navigation/pending-calendar-open';
import { sweepStaleExportFiles } from './src/lib/email-export';
import { useFilterStore } from './src/stores/filter-store';
import { useVacationStore } from './src/stores/vacation-store';
@@ -20,10 +31,12 @@ import {
addTokenRefreshListener,
getInitialNotificationTap,
getStoredRelayBaseUrl,
setupPushNotifications,
notificationTapJmapAccountId,
resyncPushNotifications,
teardownPushNotificationsForAccount,
type NotificationTapPayload,
} from './src/lib/push-notifications';
import { addUnifiedPushEndpointListener } from './src/lib/unified-push';
import type { MainTabsParamList, RootStackParamList } from './src/navigation/types';
import ComposeScreen from './src/screens/ComposeScreen';
import EmailThreadScreen from './src/screens/EmailThreadScreen';
@@ -43,7 +56,9 @@ import { useAccountStore } from './src/stores/account-store';
import { useAuthStore } from './src/stores/auth-store';
import { useCalendarStore } from './src/stores/calendar-store';
import { useContactsStore } from './src/stores/contacts-store';
import { useEmailStore } from './src/stores/email-store';
import { useEmailStore, viewerParamsForRow } from './src/stores/email-store';
import { mailboxAccountId } from './src/lib/mailbox-tree';
import { prefetchMessage } from './src/lib/email-detail-cache';
import { useHasCalendar, useHasContacts, useHasFiles } from './src/lib/capabilities';
import { useSettingsStore } from './src/stores/settings-store';
import { useLocaleStore } from './src/stores/locale-store';
@@ -52,6 +67,8 @@ import { useUpdatesStore } from './src/stores/updates-store';
import { UpdateBanner } from './src/components/UpdateBanner';
import { PushOnboardingPrompt } from './src/components/PushOnboardingPrompt';
import { ToastHost } from './src/components/ToastHost';
import { UndoSnackbar } from './src/components/UndoSnackbar';
import { AppIconBadge } from './src/components/AppIconBadge';
import { getEmails } from './src/api/email';
import { handleDeepLink, parseDeepLink, shareToDeepLink, type DeepLink } from './src/navigation/linking';
import { addShareListener, getInitialShare, shareAttachments } from './src/lib/share-intent';
@@ -81,13 +98,36 @@ async function navigateToNotificationTap(payload: NotificationTapPayload): Promi
if (useAuthStore.getState().activeAccountId !== payload.accountId) return;
}
prefetchMessage({ id: payload.emailId, threadId: payload.threadId });
navigationRef.navigate('EmailThread', {
emailId: payload.emailId,
threadId: payload.threadId,
subject: payload.subject,
// A group mailbox's message lives under another JMAP account (#839).
jmapAccountId: notificationTapJmapAccountId(payload),
});
}
// A tapped calendar reminder opens its event (or task) in the Calendar tab,
// under the account it was scheduled for. The target is parked for
// CalendarScreen, which also picks it up when it mounts after a cold start.
async function openCalendarReminder(target: CalendarReminderTarget): Promise<void> {
const auth = useAuthStore.getState();
if (target.appAccountId && target.appAccountId !== auth.activeAccountId) {
if (!useAccountStore.getState().getAccountById(target.appAccountId)) return;
await auth.switchAccount(target.appAccountId);
if (useAuthStore.getState().activeAccountId !== target.appAccountId) return;
}
setPendingCalendarOpen(target);
// On a cold start the navigator mounts right after the auth gate flips.
for (let attempt = 0; attempt < 50 && !navigationRef.isReady(); attempt++) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (navigationRef.isReady()) {
navigationRef.navigate('MainTabs', { screen: 'Calendar' } as never);
}
}
// Deep links (bulwarkmobile://, webmail https permalinks, mailto:) and
// Android share-sheet payloads all end up here once the navigator is ready.
async function openDeepLink(link: DeepLink): Promise<void> {
@@ -111,6 +151,33 @@ async function openDeepLink(link: DeepLink): Promise<void> {
});
}
// Toasts and the undo bar show on whichever stack screen is up, so a failure
// that lands after leaving the screen it came from (a viewer action finishing
// on the Unified Inbox or a contact) is still seen, and a reply held by the
// undo-send delay can be undone from the viewer it was sent from. Every
// screen carries the hosts, and only the focused one renders them, so
// neither ever shows twice (the screen below the top one is kept live).
// The composer gets no undo bar: its Undo would hand the open composer
// another draft. A held send's bar shows on the screen below once it closes.
function FocusedHosts({ undo }: { undo: boolean }) {
if (!useIsFocused()) return null;
return (
<>
{undo && <UndoSnackbar />}
<ToastHost />
</>
);
}
function withHosts({ route, children }: { route: { name: string }; children: React.ReactElement }) {
return (
<>
{children}
<FocusedHosts undo={route.name !== 'Compose'} />
</>
);
}
function LoadingScreen({ message }: { message: string }) {
const c = useColors();
return (
@@ -123,23 +190,38 @@ function LoadingScreen({ message }: { message: string }) {
function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParamList, 'MainTabs'>) {
const c = useColors();
const mailboxes = useEmailStore((state) => state.mailboxes);
// Select the count, not the list: every mailbox fetch sets a new array, and
// re-rendering here re-renders the mail list with it.
const inboxUnreadCount = useEmailStore(
(state) => state.mailboxes.find((mailbox) => mailbox.role === 'inbox')?.unreadEmails ?? 0,
);
const logout = useAuthStore((state) => state.logout);
const inboxUnreadCount = mailboxes.find((mailbox) => mailbox.role === 'inbox')?.unreadEmails ?? 0;
const hasCalendar = useHasCalendar();
const hasContacts = useHasContacts();
const hasFiles = useHasFiles();
const disabledTabStyle = { opacity: 0.4 } as const;
const t = useLocaleStore((state) => state.t);
// The webmail's navigation names; a tab the server lacks says so.
const tabLabels = {
mail: t('sidebar.mail', 'Mail'),
calendar: t('sidebar.calendar', 'Calendar'),
contacts: t('sidebar.contacts', 'Contacts'),
files: t('sidebar.files', 'Files'),
settings: t('sidebar.settings', 'Settings'),
};
const unavailable = (name: string) => t('sidebar.tab_unavailable', '{name} (unavailable)', { name });
return (
<View style={{ flex: 1, backgroundColor: c.background }}>
<UpdateBanner />
<OfflineCacheBanner />
<PushOnboardingPrompt />
<ToastHost />
<AppIconBadge />
<Tab.Navigator
screenOptions={{
headerShown: false,
// Tabs out of view stop re-rendering until they are shown again.
freezeOnBlur: true,
tabBarActiveTintColor: c.text,
tabBarInactiveTintColor: c.textSecondary,
tabBarStyle: {
@@ -159,6 +241,7 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
<Tab.Screen
name="Mail"
options={{
title: tabLabels.mail,
tabBarIcon: ({ color, size }) => <Mail size={size} color={color} />,
tabBarBadge: inboxUnreadCount > 0 ? (inboxUnreadCount > 99 ? '99+' : inboxUnreadCount) : undefined,
tabBarBadgeStyle: {
@@ -179,10 +262,22 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
<EmailListScreen
onComposePress={() => navigation.navigate('Compose')}
onEmailPress={(email) => {
// The list holds the open folder's mail: name its account so the
// viewer never has to guess it from whatever folder is open.
const { mailboxes: all, currentMailboxId } = useEmailStore.getState();
const target = {
jmapAccountId: mailboxAccountId(all, currentMailboxId),
// A row of an "All folders" list or a tag view names its own
// account instead, and pages over that account's rows.
...viewerParamsForRow(email),
};
// Start on the body now: mounting the viewer takes a while.
prefetchMessage(email, target.jmapAccountId);
navigation.navigate('EmailThread', {
emailId: email.id,
threadId: email.threadId,
subject: email.subject,
...target,
});
}}
/>
@@ -192,9 +287,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
name="Calendar"
component={CalendarScreen}
options={{
title: tabLabels.calendar,
tabBarIcon: ({ color, size }) => <Calendar size={size} color={color} />,
tabBarItemStyle: hasCalendar ? undefined : disabledTabStyle,
tabBarAccessibilityLabel: hasCalendar ? 'Calendar' : 'Calendar (unavailable)',
tabBarAccessibilityLabel: hasCalendar ? tabLabels.calendar : unavailable(tabLabels.calendar),
}}
listeners={{
tabPress: (e) => {
@@ -206,9 +302,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
name="Contacts"
component={ContactsScreen}
options={{
title: tabLabels.contacts,
tabBarIcon: ({ color, size }) => <BookUser size={size} color={color} />,
tabBarItemStyle: hasContacts ? undefined : disabledTabStyle,
tabBarAccessibilityLabel: hasContacts ? 'Contacts' : 'Contacts (unavailable)',
tabBarAccessibilityLabel: hasContacts ? tabLabels.contacts : unavailable(tabLabels.contacts),
}}
listeners={{
tabPress: (e) => {
@@ -220,9 +317,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
name="Files"
component={FilesScreen}
options={{
title: tabLabels.files,
tabBarIcon: ({ color, size }) => <HardDrive size={size} color={color} />,
tabBarItemStyle: hasFiles ? undefined : disabledTabStyle,
tabBarAccessibilityLabel: hasFiles ? 'Files' : 'Files (unavailable)',
tabBarAccessibilityLabel: hasFiles ? tabLabels.files : unavailable(tabLabels.files),
}}
listeners={{
tabPress: (e) => {
@@ -233,6 +331,7 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
<Tab.Screen
name="Settings"
options={{
title: tabLabels.settings,
tabBarIcon: ({ color, size }) => <Settings size={size} color={color} />,
}}
>
@@ -248,6 +347,7 @@ export default function App() {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const client = useAuthStore((state) => state.client);
const restoreSession = useAuthStore((state) => state.restoreSession);
const t = useLocaleStore((state) => state.t);
// Resolve the user's theme preference to a concrete light/dark style for the
// system status bar. The rest of the app's colors are still hard-coded dark
@@ -257,6 +357,13 @@ export default function App() {
const resolvedScheme: 'light' | 'dark' =
themePref === 'system' ? (systemScheme === 'light' ? 'light' : 'dark') : themePref;
const statusBarStyle: 'light' | 'dark' = resolvedScheme === 'light' ? 'dark' : 'light';
// React Navigation's default theme is light: without this its containers
// paint white behind and between screens, even in dark mode.
const background = useColors().background;
const navigationTheme = React.useMemo(() => {
const base = resolvedScheme === 'light' ? DefaultTheme : DarkTheme;
return { ...base, colors: { ...base.colors, background, card: background } };
}, [resolvedScheme, background]);
// Persisted active account is the signal that the user was already signed
// in on the previous launch. When present we render the main UI with the
// cached mail list instead of the "Restoring session" spinner; the real
@@ -272,6 +379,9 @@ export default function App() {
React.useEffect(() => {
void useSettingsStore.getState().hydrate();
void useLocaleStore.getState().hydrate();
// Calendar reminders are kept scheduled from launch, not only once the
// Calendar tab has been opened.
startCalendarNotificationSync();
return useNetworkStore.getState().init();
}, []);
@@ -376,6 +486,15 @@ export default function App() {
};
}, [isAuthenticated]);
// Calendar reminders are local expo notifications; their taps come through
// expo-notifications, not the native tap store used for mail push above.
React.useEffect(() => {
if (!isAuthenticated) return;
return startCalendarReminderTapHandling((target) => {
void openCalendarReminder(target);
});
}, [isAuthenticated]);
// Deep links and share-sheet payloads. The cold-start URL / share is read
// once auth is restored so the target screen has credentials.
React.useEffect(() => {
@@ -426,7 +545,8 @@ export default function App() {
// and whenever the FCM token rotates. Honours the user's notification
// preference - flipping it off tears down THIS account's subscription so
// notifications stop arriving for it. Other logged-in accounts keep their
// setups intact.
// setups intact. An account the user turned push off for, or whose device
// registration was revoked, stays off (see resyncPushNotifications).
const emailNotificationsEnabled = useSettingsStore(
(s) => s.emailNotificationsEnabled,
);
@@ -447,7 +567,7 @@ export default function App() {
const relayBaseUrl = await getStoredRelayBaseUrl();
if (!relayBaseUrl) return;
try {
await setupPushNotifications({
await resyncPushNotifications({
relayBaseUrl,
accountLabel: client.username ?? undefined,
});
@@ -464,10 +584,16 @@ export default function App() {
const unsubscribe = addTokenRefreshListener(() => {
void doSetup();
});
// UnifiedPush equivalent of an FCM token rotation: the distributor handed
// out a new endpoint, so re-register it with the relay.
const unsubscribeUp = addUnifiedPushEndpointListener(() => {
void doSetup();
});
return () => {
cancelled = true;
unsubscribe();
unsubscribeUp();
};
}, [client, isAuthenticated, emailNotificationsEnabled, activeAccountId]);
@@ -528,6 +654,23 @@ export default function App() {
void useOutboxStore.getState().flush();
};
// Foreground liveness for the per-account connection dot: the open
// stream's pings, or a `Core/echo` every 30 s while there is none (see
// startLivenessMonitor). NetInfo cannot tell an unreachable server.
const liveness = startLivenessMonitor({
ping: () => jmapClient.ping(),
streamHealthy: () => handle?.healthy ?? false,
isActive: () => appActive,
isOnline: () => useNetworkStore.getState().online,
onConnected: (connected) => {
if (!mounted || !activeAccountId) return;
const account = useAccountStore.getState().getAccountById(activeAccountId);
if (account && account.isConnected !== connected) {
useAccountStore.getState().updateAccount(activeAccountId, { isConnected: connected });
}
},
});
void start();
const subscription = AppState.addEventListener('change', (state) => {
@@ -539,19 +682,12 @@ export default function App() {
handle = null;
return;
}
// Coming back: verify the session is still alive before trusting the
// stream, then reconnect and catch up on what was missed.
void (async () => {
const alive = await jmapClient.ping().catch(() => false);
if (!mounted) return;
if (!alive && useNetworkStore.getState().online) {
const ok = await useAuthStore.getState().retrySession().catch(() => false);
if (!ok || !mounted) return;
}
if (!handle) await start();
else handle.reconnect();
refreshAfterResume();
})();
// Coming back: reconnect and catch up on what was missed at once;
// one echo alongside checks the server for the connection dot.
if (!handle) void start();
else handle.reconnect();
refreshAfterResume();
void liveness.check();
});
// Reconnect when the network comes back while foregrounded.
@@ -566,39 +702,12 @@ export default function App() {
mounted = false;
subscription.remove();
unsubscribeNetwork();
liveness.stop();
handle?.close();
handle = null;
};
}, [client, isAuthenticated, haveLiveSession, activeAccountId]);
// Foreground keep-alive: a `Core/echo` every 30 s tells us when the server
// is unreachable even though the device is online (NetInfo cannot), and
// drives the per-account connection dot.
React.useEffect(() => {
if (!isAuthenticated || !haveLiveSession || !activeAccountId) return;
let cancelled = false;
let failures = 0;
const timer = setInterval(() => {
if (AppState.currentState !== 'active') return;
if (!useNetworkStore.getState().online) return;
void jmapClient.ping().then((ok) => {
if (cancelled) return;
failures = ok ? 0 : failures + 1;
const account = useAccountStore.getState().getAccountById(activeAccountId);
if (!account) return;
// One missed echo can be a blip; two in a row is a lost connection.
const connected = failures < 2;
if (account.isConnected !== connected) {
useAccountStore.getState().updateAccount(activeAccountId, { isConnected: connected });
}
}).catch(() => undefined);
}, 30_000);
return () => {
cancelled = true;
clearInterval(timer);
};
}, [isAuthenticated, haveLiveSession, activeAccountId]);
// Skip the "Restoring session" flash for returning users: if we already
// have a persisted active account, render the main UI immediately with
// whatever the email-store hydrated from cache. restoreSession still runs
@@ -607,7 +716,7 @@ export default function App() {
return (
<>
<StatusBar style={statusBarStyle} />
<LoadingScreen message="Loading..." />
<LoadingScreen message={t('common.loading', 'Loading...')} />
</>
);
}
@@ -622,9 +731,11 @@ export default function App() {
}
return (
<NavigationContainer ref={navigationRef}>
<NavigationContainer ref={navigationRef} theme={navigationTheme}>
<StatusBar style={statusBarStyle} />
<Stack.Navigator screenOptions={{ headerShown: false }}>
{/* On Fabric, native-stack keeps the screen right below the top live and
freezes the ones further down. */}
<Stack.Navigator screenOptions={{ headerShown: false, freezeOnBlur: true }} screenLayout={withHosts}>
<Stack.Screen name="MainTabs" component={MainTabsNavigator} />
<Stack.Screen name="EmailThread" component={EmailThreadScreen} />
<Stack.Screen name="EmailSource" component={EmailSourceScreen} />
+25 -14
View File
@@ -75,23 +75,34 @@ commit hash. The remaining `- [ ]` items are either deferred with a note
for TOTP accounts, relay APNs/UnifiedPush transports, a server-side settings
store for native #1).
## Status after the 2026-09-24 fix pass
The native audit of 2026-09-22 ([docs/audit-2026-09.md](docs/audit-2026-09.md))
spot-checked this list and found 14 ticks that were broken or overstated and 11
open items that were already done. The 2026-09-24 fix pass fixed 13 of the 14
(their ticks now also name the fixing commit; the date-locale item is open
again), dropped three obsolete items and ticked what it completed: 380 of 415
items are done, 35 open. What still needs a device check or a decision is in
the audit's "Fix pass, 2026-09-24" section.
## Areas
| # | Area | File | Items | P1 | P2 | P3 |
|---|---|---|---|---|---|---|
| 01 | Authentication, login, session, multi-account | [docs/parity/01-auth-accounts.md](docs/parity/01-auth-accounts.md) | 30 | 4 | 8 | 18 |
| 02 | Mail list, folders, unified views, search, tags | [docs/parity/02-mail-list-folders.md](docs/parity/02-mail-list-folders.md) | 54 | 1 | 20 | 33 |
| 03 | Email viewer, thread view, rendering, attachments | [docs/parity/03-email-viewer.md](docs/parity/03-email-viewer.md) | 46 | 6 | 13 | 26 |
| 04 | Composer, drafts, sending, identities, templates, scheduled send | [docs/parity/04-composer-send.md](docs/parity/04-composer-send.md) | 51 | 5 | 14 | 32 |
| 05 | Calendar and tasks | [docs/parity/05-calendar.md](docs/parity/05-calendar.md) | 51 | 5 | 20 | 26 |
| 06 | Contacts and address books | [docs/parity/06-contacts.md](docs/parity/06-contacts.md) | 51 | 1 | 18 | 31 |
| 07 | Filters (Sieve), vacation responder, Files | [docs/parity/07-filters-vacation-files.md](docs/parity/07-filters-vacation-files.md) | 33 | 3 | 8 | 22 |
| 08 | Settings, sync, push, i18n, themes, updates, misc UI | [docs/parity/08-settings-push-i18n-ui.md](docs/parity/08-settings-push-i18n-ui.md) | 46 | 0 | 14 | 32 |
| 09 | JMAP client core, live sync, offline, security, S/MIME | [docs/parity/09-jmap-core-sync-security.md](docs/parity/09-jmap-core-sync-security.md) | 56 | 7 | 23 | 26 |
| | **Total** | | **418** | **32** | **138** | **246** |
| # | Area | File | Items | Done | Open | P1 | P2 | P3 |
|---|---|---|---|---|---|---|---|---|
| 01 | Authentication, login, session, multi-account | [docs/parity/01-auth-accounts.md](docs/parity/01-auth-accounts.md) | 30 | 29 | 1 | 4 | 8 | 18 |
| 02 | Mail list, folders, unified views, search, tags | [docs/parity/02-mail-list-folders.md](docs/parity/02-mail-list-folders.md) | 54 | 50 | 4 | 1 | 20 | 33 |
| 03 | Email viewer, thread view, rendering, attachments | [docs/parity/03-email-viewer.md](docs/parity/03-email-viewer.md) | 46 | 45 | 1 | 6 | 13 | 26 |
| 04 | Composer, drafts, sending, identities, templates, scheduled send | [docs/parity/04-composer-send.md](docs/parity/04-composer-send.md) | 51 | 46 | 5 | 5 | 14 | 32 |
| 05 | Calendar and tasks | [docs/parity/05-calendar.md](docs/parity/05-calendar.md) | 50 | 43 | 7 | 5 | 20 | 25 |
| 06 | Contacts and address books | [docs/parity/06-contacts.md](docs/parity/06-contacts.md) | 50 | 46 | 4 | 1 | 18 | 31 |
| 07 | Filters (Sieve), vacation responder, Files | [docs/parity/07-filters-vacation-files.md](docs/parity/07-filters-vacation-files.md) | 32 | 29 | 3 | 3 | 8 | 21 |
| 08 | Settings, sync, push, i18n, themes, updates, misc UI | [docs/parity/08-settings-push-i18n-ui.md](docs/parity/08-settings-push-i18n-ui.md) | 46 | 38 | 8 | 0 | 14 | 32 |
| 09 | JMAP client core, live sync, offline, security, S/MIME | [docs/parity/09-jmap-core-sync-security.md](docs/parity/09-jmap-core-sync-security.md) | 56 | 54 | 2 | 7 | 23 | 26 |
| | **Total** | | **415** | **380** | **35** | **32** | **138** | **244** |
Counts are of `- [ ]` items per file (a handful of "verified" notes carry a
priority tag too, so P-sums can differ by one or two from the item count).
Counts are of the `- [ ]` and `- [x]` items per file as of 2026-09-24. Done and
Open split them by tick; the P columns count the priority tags on those items
(one item carries none).
## All P1 findings (fix these first)
+2 -2
View File
@@ -21,7 +21,7 @@ React Native (Expo SDK 54) client for [Bulwark Webmail](https://github.com/bulwa
- Email list, threads, compose
- Calendar (basic)
- Contacts (basic)
- Push notifications via FCM relay
- Push notifications via the Bulwark relay - FCM by default, or [UnifiedPush](https://unifiedpush.org) (e.g. ntfy) for devices without Google Play services
- In-app sideload updates from GitHub Releases
## What's missing or rough
@@ -40,7 +40,7 @@ npx expo start
Then press `a` for Android, `i` for iOS, or scan the QR with Expo Go.
For a release APK build see [.github/workflows/release-android.yml](.github/workflows/release-android.yml).
For release APK builds and signing see [docs/android-release.md](docs/android-release.md).
For iOS builds and TestFlight distribution see [docs/ios-release.md](docs/ios-release.md).
## License
+1 -1
View File
@@ -1 +1 @@
0.1.59
0.1.63
+117 -8
View File
@@ -7,6 +7,62 @@ def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
def appVersionName = file("${projectRoot}/VERSION").text.trim()
/**
* versionCode has to grow with every release, or Android refuses the update as
* a downgrade. It is derived from VERSION as
*
* major * 10000000 + minor * 10000 + patch (0.1.62 -> 10062)
*
* which keeps increasing as long as patch stays below 10000 and minor below
* 1000 (scripts/bump-version.mjs, which only ever bumps patch, enforces both),
* and stays under Android's 2100000000 ceiling up to major 209.
*/
def versionParts = (appVersionName =~ /^(\d+)\.(\d+)\.(\d+)$/)
if (!versionParts.matches()) {
throw new GradleException("VERSION must be MAJOR.MINOR.PATCH, got '${appVersionName}'")
}
def (versionMajor, versionMinor, versionPatch) = (1..3).collect { versionParts.group(it) as long }
if (versionMinor > 999 || versionPatch > 9999) {
throw new GradleException("VERSION ${appVersionName}: minor must stay below 1000 and patch below 10000, " +
"or versionCode stops increasing. Move to the next minor or major version instead.")
}
def appVersionCode = versionMajor * 10000000L + versionMinor * 10000L + versionPatch
if (appVersionCode < 1 || appVersionCode > 2100000000L) {
throw new GradleException("VERSION ${appVersionName} gives versionCode ${appVersionCode}, " +
"outside the 1..2100000000 range Android accepts.")
}
/**
* Release signing. The key never lives in the repo: CI passes it as
* ORG_GRADLE_PROJECT_BULWARK_RELEASE_* environment variables, and a local
* release build can set the same names with -P, in ~/.gradle/gradle.properties
* or as plain environment variables. See docs/android-release.md.
*
* There is deliberately no automatic fallback to the debug key. It is the
* public React Native template key, so an APK signed with it can be replaced
* by an "update" anyone can sign.
*/
def releaseSigningNames = [
'BULWARK_RELEASE_STORE_FILE',
'BULWARK_RELEASE_STORE_PASSWORD',
'BULWARK_RELEASE_KEY_ALIAS',
'BULWARK_RELEASE_KEY_PASSWORD',
]
def releaseSigning = releaseSigningNames.collectEntries { name ->
[(name): (findProperty(name) ?: System.getenv(name))?.toString()]
}
def missingReleaseSigning = releaseSigningNames.findAll { !releaseSigning[it]?.trim() }
def releaseSigningConfigured = missingReleaseSigning.isEmpty()
def releaseStoreFile = releaseSigningConfigured ? file(releaseSigning.BULWARK_RELEASE_STORE_FILE.trim()) : null
// Escape hatch for a local release build that never leaves the machine
// (profiling, checking R8). It is refused whenever CI is set, so a stray flag
// cannot turn a published build back into a debug-signed one.
def ciEnv = System.getenv('CI')?.trim()?.toLowerCase()
def onCi = ciEnv && !(ciEnv in ['false', '0'])
def allowDebugSignedRelease = (findProperty('allowDebugSignedRelease') ?: 'false').toString().trim().toBoolean()
def debugSignedRelease = !releaseSigningConfigured && allowDebugSignedRelease && !onCi
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
@@ -95,7 +151,7 @@ android {
applicationId 'com.anonymous.bulwarkmobile'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionCode appVersionCode.toInteger()
versionName appVersionName
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
@@ -107,13 +163,12 @@ android {
keyAlias 'androiddebugkey'
keyPassword 'android'
}
if (project.hasProperty('BULWARK_RELEASE_STORE_FILE')
&& project.property('BULWARK_RELEASE_STORE_FILE')?.trim()) {
if (releaseSigningConfigured) {
release {
storeFile file(project.property('BULWARK_RELEASE_STORE_FILE'))
storePassword project.property('BULWARK_RELEASE_STORE_PASSWORD')
keyAlias project.property('BULWARK_RELEASE_KEY_ALIAS')
keyPassword project.property('BULWARK_RELEASE_KEY_PASSWORD')
storeFile releaseStoreFile
storePassword releaseSigning.BULWARK_RELEASE_STORE_PASSWORD
keyAlias releaseSigning.BULWARK_RELEASE_KEY_ALIAS
keyPassword releaseSigning.BULWARK_RELEASE_KEY_PASSWORD
}
}
}
@@ -122,7 +177,13 @@ android {
signingConfig signingConfigs.debug
}
release {
signingConfig signingConfigs.findByName('release') ?: signingConfigs.debug
// With neither a key nor the opt-in there is no signing config at
// all; checkReleaseSigning below stops the build before packaging.
if (releaseSigningConfigured) {
signingConfig signingConfigs.release
} else if (debugSignedRelease) {
signingConfig signingConfigs.debug
}
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
shrinkResources enableShrinkResources.toBoolean()
minifyEnabled enableMinifyInReleaseBuilds
@@ -142,6 +203,48 @@ android {
}
}
// Stop a release build that has no real key. This runs only when a release
// variant is actually built, so `expo run:android`, assembleDebug and IDE sync
// keep working with no key configured.
def releaseSigningHelp = [
"Release signing is not configured (missing: ${missingReleaseSigning.join(', ')}).",
"",
"Release builds are never signed with the debug key: it is the public React Native",
"template key, so an APK signed with it can be replaced by anyone. Provide the release",
"key as gradle properties (-P or ~/.gradle/gradle.properties) or environment variables:",
" BULWARK_RELEASE_STORE_FILE path to the keystore",
" BULWARK_RELEASE_STORE_PASSWORD",
" BULWARK_RELEASE_KEY_ALIAS",
" BULWARK_RELEASE_KEY_PASSWORD",
"",
onCi && allowDebugSignedRelease
? "allowDebugSignedRelease is ignored because CI is set: published builds must use the release key."
: "For a local test build that is never installed anywhere else, pass -PallowDebugSignedRelease=true.",
"See docs/android-release.md.",
].join('\n')
def checkReleaseSigning = tasks.register('checkReleaseSigning') {
group = 'verification'
description = 'Fails a release build that has no release signing key.'
doLast {
if (releaseSigningConfigured) {
if (!releaseStoreFile.isFile()) {
throw new GradleException("BULWARK_RELEASE_STORE_FILE points to ${releaseStoreFile}, which does not exist.")
}
} else if (debugSignedRelease) {
logger.warn("WARNING: this release build is signed with the PUBLIC debug key " +
"(allowDebugSignedRelease). Do not distribute it.")
} else {
throw new GradleException(releaseSigningHelp)
}
}
}
tasks.configureEach { task ->
if (task.name == 'preReleaseBuild') {
task.dependsOn(checkReleaseSigning)
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
@@ -171,6 +274,12 @@ dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
implementation("com.google.firebase:firebase-messaging")
// UnifiedPush - Google-free push via a user-chosen distributor app such
// as ntfy. Coexists with FCM; the user picks the transport in settings.
// NOTE: 3.3.2-3.3.4 are built with Kotlin 2.3 and break this project's
// Kotlin 2.1 toolchain; 3.3.5 went back to 2.2 which it can read.
implementation("org.unifiedpush.android:connector:3.3.5")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
+9
View File
@@ -12,3 +12,12 @@
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:
# The app's own native code (com.anonymous.bulwarkmobile). What R8 must not
# drop or rename there is already covered elsewhere: React Native's rules keep
# every NativeModule, whose @ReactMethod methods JS calls by name, and AAPT
# keeps the application, activity and services listed in AndroidManifest.xml
# (FCM, UnifiedPush and the headless JS task). Keeping the package whole on
# top of that costs a few KB and leaves its class and method names readable
# in a crash report from an obfuscated release build.
-keep class com.anonymous.bulwarkmobile.** { *; }
+6
View File
@@ -59,6 +59,12 @@
</intent-filter>
</service>
<service android:name=".BulwarkPushTaskService" android:exported="false"/>
<service android:name=".BulwarkPushJobService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"/>
<service android:name=".BulwarkUnifiedPushService" android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
</intent-filter>
</service>
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="bulwark_mail"/>
</application>
</manifest>
@@ -66,6 +66,7 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
val threadId = options.getString("threadId")
val subject = options.takeIf { it.hasKey("subject") }?.getString("subject")
val accountId = options.takeIf { it.hasKey("accountId") }?.getString("accountId")
val jmapAccountId = options.takeIf { it.hasKey("jmapAccountId") }?.getString("jmapAccountId")
val groupKey = options.takeIf { it.hasKey("groupKey") }?.getString("groupKey")
?: accountId?.let { "bulwark-mail:$it" }
val groupTitle = options.takeIf { it.hasKey("groupTitle") }?.getString("groupTitle")
@@ -78,7 +79,7 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
?: makeLetterAvatar(initials, bgColorHex)
postNotification(
notificationId, title, body, largeIcon, bgColorHex,
emailId, threadId, subject, accountId, groupKey,
emailId, threadId, subject, accountId, jmapAccountId, groupKey,
)
if (groupKey != null) postGroupSummary(groupKey, groupTitle, bgColorHex, accountId)
promise.resolve(null)
@@ -107,6 +108,7 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
threadId: String?,
subject: String?,
accountId: String?,
jmapAccountId: String?,
groupKey: String?,
) {
val ctx = reactApplicationContext
@@ -116,6 +118,7 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
if (threadId != null) putExtra(NotificationTapStore.EXTRA_THREAD_ID, threadId)
if (subject != null) putExtra(NotificationTapStore.EXTRA_SUBJECT, subject)
if (accountId != null) putExtra(NotificationTapStore.EXTRA_ACCOUNT_ID, accountId)
if (jmapAccountId != null) putExtra(NotificationTapStore.EXTRA_JMAP_ACCOUNT_ID, jmapAccountId)
}
val pending = PendingIntent.getActivity(
ctx,
@@ -125,7 +128,7 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
)
val builder = NotificationCompat.Builder(ctx, BulwarkMessagingService.CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_email)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(largeIcon)
.setContentTitle(title)
.setContentText(body)
@@ -178,7 +181,7 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val summary = NotificationCompat.Builder(ctx, BulwarkMessagingService.CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_email)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(groupTitle)
.setContentText(if (count == 1) "1 new message" else "$count new messages")
.setStyle(inbox)
@@ -3,14 +3,20 @@ package com.anonymous.bulwarkmobile
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
import com.facebook.react.uimanager.ViewManager
class BulwarkFcmPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
listOf(
BulwarkFcmModule(reactContext),
BulwarkClientCertModule(reactContext),
)
buildList {
add(BulwarkFcmModule(reactContext))
add(BulwarkUnifiedPushModule(reactContext))
add(BulwarkClientCertModule(reactContext))
// The bridge registers React Native's own module under this name.
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
add(BulwarkHeadlessJsTaskSupportModule(reactContext))
}
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
emptyList()
@@ -0,0 +1,35 @@
package com.anonymous.bulwarkmobile
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.jstasks.HeadlessJsTaskContext
/**
* React Native's HeadlessJsTaskSupport module, which AppRegistry calls when a
* headless JS task's promise settles. RN 0.81 registers it only for the old
* bridge (CoreModulesPackage); the bridgeless runtime's CoreReactPackage
* leaves it out, so in this app JS could never report a finished task and
* every BulwarkPushTask ran until its 30 s timeout, keeping the service or job
* and its wake lock alive that long. Same logic as the RN module.
*/
class BulwarkHeadlessJsTaskSupportModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "HeadlessJsTaskSupport"
@ReactMethod
fun notifyTaskFinished(taskId: Double) {
val tasks = HeadlessJsTaskContext.getInstance(reactApplicationContext)
val id = taskId.toInt()
if (tasks.isTaskRunning(id)) tasks.finishTask(id)
}
@ReactMethod
fun notifyTaskRetry(taskId: Double, promise: Promise) {
val tasks = HeadlessJsTaskContext.getInstance(reactApplicationContext)
val id = taskId.toInt()
promise.resolve(tasks.isTaskRunning(id) && tasks.retryTask(id))
}
}
@@ -8,8 +8,9 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.util.Log
import androidx.core.app.NotificationCompat
import com.facebook.react.HeadlessJsTaskService
import com.facebook.react.bridge.Arguments
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
@@ -31,7 +32,7 @@ class BulwarkMessagingService : FirebaseMessagingService() {
// receives JMAP push directly, so the fcm:message event below is
// sufficient for it to refresh state.
if (!isAppInForeground()) {
startHeadlessTask(data)
startHeadlessTask(applicationContext, data)
}
val params = Arguments.createMap().apply {
@@ -42,25 +43,60 @@ class BulwarkMessagingService : FirebaseMessagingService() {
BulwarkFcmModule.emit("fcm:message", params)
}
private fun isAppInForeground(): Boolean {
val info = ActivityManager.RunningAppProcessInfo()
ActivityManager.getMyMemoryState(info)
return info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND ||
info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
}
private fun startHeadlessTask(data: Map<String, String>) {
val intent = Intent(applicationContext, BulwarkPushTaskService::class.java)
val bundle = Bundle().apply {
for ((k, v) in data) putString(k, v)
}
intent.putExtras(bundle)
applicationContext.startService(intent)
HeadlessJsTaskService.acquireWakeLockNow(applicationContext)
}
companion object {
const val CHANNEL_ID = "bulwark_mail"
private const val TAG = "BulwarkPush"
private const val START_WAKE_LOCK_MS = 10_000L
// Shared with BulwarkUnifiedPushService - a push arriving over either
// transport is dispatched the same way.
fun isAppInForeground(): Boolean {
val info = ActivityManager.RunningAppProcessInfo()
ActivityManager.getMyMemoryState(info)
return info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND ||
info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
}
fun startHeadlessTask(context: Context, data: Map<String, String>) {
val intent = Intent(context, BulwarkPushTaskService::class.java)
val bundle = Bundle().apply {
for ((k, v) in data) putString(k, v)
}
intent.putExtras(bundle)
// Keep the device awake until the service holds React Native's
// wake lock; BulwarkPushTaskService releases this one then. Not
// HeadlessJsTaskService.acquireWakeLockNow: it is not
// thread-safe, and FCM calls this on a worker thread while the
// service takes the same static lock on the main thread, so both
// could create a lock and the overwritten one stayed held.
startWakeLock = context.getSystemService(PowerManager::class.java)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "bulwark:push-start")
.apply {
setReferenceCounted(false)
acquire(START_WAKE_LOCK_MS)
}
try {
context.startService(intent)
} catch (e: IllegalStateException) {
// Android 8+ refuses to start a service from the background
// unless the app is temporarily allowlisted. A high-priority
// FCM message does that; a downgraded one or a UnifiedPush
// message may not (BackgroundServiceStartNotAllowedException
// on 12+), and uncaught this crashed the app on every such
// push. Run the same task from a job instead.
releaseStartWakeLock()
Log.w(TAG, "Push service start refused in the background, using a job: ${e.message}")
BulwarkPushJobService.schedule(context, data)
}
}
@Volatile private var startWakeLock: PowerManager.WakeLock? = null
/** Drops the wake lock taken in [startHeadlessTask]; it also times out on its own. */
fun releaseStartWakeLock() {
startWakeLock?.release()
startWakeLock = null
}
fun ensureChannel(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
@@ -0,0 +1,180 @@
package com.anonymous.bulwarkmobile
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.os.SystemClock
import android.util.Log
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceEventListener
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
import com.facebook.react.jstasks.HeadlessJsTaskConfig
import com.facebook.react.jstasks.HeadlessJsTaskContext
import com.facebook.react.jstasks.HeadlessJsTaskEventListener
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
/**
* Runs the BulwarkPushTask headless JS task from a JobScheduler job.
*
* A push normally starts BulwarkPushTaskService, but Android 8+ only lets a
* background app start a service while it is temporarily allowlisted, which a
* high-priority FCM message does. FCM delivers some messages at normal
* priority anyway (it downgrades apps whose high-priority pushes rarely end in
* a notification), and a UnifiedPush distributor may not raise the app at all.
* The service start then throws, so the push is handed to this job instead:
* expedited on Android 12+, where it runs right away while the app has
* expedited-job quota and as a regular job after that, and a regular job on
* Android 8-11. It needs no new dependency and ends in the same notification
* as the service path.
*/
class BulwarkPushJobService : JobService(), HeadlessJsTaskEventListener {
private class RunningTask(val params: JobParameters, val startedAt: Long)
private val running = ConcurrentHashMap<Int, RunningTask>()
private var taskContext: HeadlessJsTaskContext? = null
override fun onStartJob(params: JobParameters): Boolean {
val config = BulwarkPushTaskService.taskConfig(Bundle(params.extras))
withReadyReactContext { startTask(it, config, params) }
return true
}
// The JS task has its own 30 s timeout and finishes on its own; nothing to
// reschedule if the system stops the job first.
override fun onStopJob(params: JobParameters): Boolean {
running.values.removeIf { it.params.jobId == params.jobId }
return false
}
override fun onDestroy() {
taskContext?.removeTaskEventListener(this)
super.onDestroy()
}
override fun onHeadlessJsTaskStart(taskId: Int) = Unit
override fun onHeadlessJsTaskFinish(taskId: Int) {
val task = running.remove(taskId) ?: return
val ms = SystemClock.elapsedRealtime() - task.startedAt
Log.i(TAG, "Push task $taskId from job ${task.params.jobId} finished after $ms ms")
jobFinished(task.params, false)
}
private fun startTask(context: ReactContext, config: HeadlessJsTaskConfig, params: JobParameters) {
val tasks = HeadlessJsTaskContext.getInstance(context)
if (taskContext !== tasks) {
taskContext?.removeTaskEventListener(this)
tasks.addTaskEventListener(this)
taskContext = tasks
}
UiThreadUtil.runOnUiThread {
try {
val taskId = tasks.startTask(config)
running[taskId] = RunningTask(params, SystemClock.elapsedRealtime())
} catch (e: IllegalStateException) {
// The app came to the foreground since the push arrived and
// syncs on its own there.
Log.i(TAG, "Push job ${params.jobId} skipped: ${e.message}")
jobFinished(params, false)
}
}
}
/**
* Calls [onReady] once with a React context whose JS instance is up,
* starting React Native if the process was woken for this job.
*
* A context exists before its instance is ready, and a task started on
* it then never reaches JS (HeadlessJsTaskContext only logs "CatalystInstance
* not available" and waits for the timeout), which happened when several
* pushes arrived while the app was starting. So the listener goes in
* first and the ready check after it; whichever sees the ready instance
* first runs the task.
*/
private fun withReadyReactContext(onReady: (ReactContext) -> Unit) {
val done = AtomicBoolean(false)
val runOnce = { context: ReactContext -> if (done.compareAndSet(false, true)) onReady(context) }
val app = application as ReactApplication
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
val host = checkNotNull(app.reactHost) { "ReactHost is not initialized in New Architecture" }
val listener = object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
host.removeReactInstanceEventListener(this)
runOnce(context)
}
}
host.addReactInstanceEventListener(listener)
val current = host.currentReactContext
if (current != null && current.hasActiveReactInstance()) {
host.removeReactInstanceEventListener(listener)
runOnce(current)
} else {
host.start()
}
} else {
@Suppress("DEPRECATION")
val manager = app.reactNativeHost.reactInstanceManager
val listener = object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
manager.removeReactInstanceEventListener(this)
runOnce(context)
}
}
manager.addReactInstanceEventListener(listener)
val current = manager.currentReactContext
if (current != null && current.hasActiveReactInstance()) {
manager.removeReactInstanceEventListener(listener)
runOnce(current)
} else if (!manager.hasStartedCreatingInitialContext()) {
manager.createReactContextInBackground()
}
}
}
companion object {
private const val TAG = "BulwarkPush"
// A block of job ids for push jobs, one per push that is waiting or
// running. A pending job with the same id would be replaced (and a
// running one stopped), so each push takes a free id.
private const val JOB_ID_BASE = 0x42554c00
private const val JOB_ID_COUNT = 32
/** Schedules the push task as a job. Returns false if no job could be scheduled. */
fun schedule(context: Context, data: Map<String, String>): Boolean = try {
val scheduler = context.getSystemService(JobScheduler::class.java)
val busy = scheduler.allPendingJobs.mapTo(HashSet()) { it.id }
val jobId = (JOB_ID_BASE until JOB_ID_BASE + JOB_ID_COUNT).firstOrNull { it !in busy }
if (jobId == null) {
// Every slot holds a push that has not finished yet; any of
// them fetches the same new mail.
Log.w(TAG, "Push not scheduled: $JOB_ID_COUNT push jobs already pending")
false
} else {
val extras = PersistableBundle().apply {
for ((key, value) in data) putString(key, value)
}
val job = JobInfo.Builder(jobId, ComponentName(context, BulwarkPushJobService::class.java))
.setExtras(extras)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) setExpedited(true) }
.build()
val scheduled = scheduler.schedule(job) == JobScheduler.RESULT_SUCCESS
Log.i(TAG, "Push job $jobId scheduled: $scheduled")
scheduled
}
} catch (e: Exception) {
Log.w(TAG, "Push job could not be scheduled", e)
false
}
}
}
@@ -1,18 +1,32 @@
package com.anonymous.bulwarkmobile
import android.content.Intent
import android.os.Bundle
import com.facebook.react.bridge.Arguments
import com.facebook.react.jstasks.HeadlessJsTaskConfig
import com.facebook.react.HeadlessJsTaskService
class BulwarkPushTaskService : HeadlessJsTaskService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super.onStartCommand(intent, flags, startId).also {
// startTask has taken React Native's wake lock by now.
BulwarkMessagingService.releaseStartWakeLock()
}
override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
val extras = intent?.extras ?: return null
return HeadlessJsTaskConfig(
"BulwarkPushTask",
Arguments.fromBundle(extras),
30000L,
false,
)
return taskConfig(extras)
}
companion object {
// Shared with BulwarkPushJobService, which runs the same task when
// this service may not be started.
fun taskConfig(extras: Bundle): HeadlessJsTaskConfig =
HeadlessJsTaskConfig(
"BulwarkPushTask",
Arguments.fromBundle(extras),
30000L,
false,
)
}
}
@@ -0,0 +1,110 @@
package com.anonymous.bulwarkmobile
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import org.unifiedpush.android.connector.UnifiedPush
// JS-facing surface for UnifiedPush distributor management. Events from the
// distributor (endpoint changes, incoming messages) arrive through
// BulwarkUnifiedPushService, which emits them on the shared device-event
// channel.
class BulwarkUnifiedPushModule(reactContext: ReactApplicationContext)
: ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "BulwarkUnifiedPush"
@ReactMethod
fun getDistributors(promise: Promise) {
try {
val array = Arguments.createArray()
for (d in UnifiedPush.getDistributors(reactApplicationContext)) array.pushString(d)
promise.resolve(array)
} catch (err: Exception) {
promise.reject("up_distributors_failed", err)
}
}
@ReactMethod
fun getSavedDistributor(promise: Promise) {
try {
promise.resolve(UnifiedPush.getSavedDistributor(reactApplicationContext))
} catch (err: Exception) {
promise.reject("up_distributor_failed", err)
}
}
@ReactMethod
fun getAckDistributor(promise: Promise) {
try {
promise.resolve(UnifiedPush.getAckDistributor(reactApplicationContext))
} catch (err: Exception) {
promise.reject("up_distributor_failed", err)
}
}
@ReactMethod
fun saveDistributor(distributor: String, promise: Promise) {
try {
UnifiedPush.saveDistributor(reactApplicationContext, distributor)
promise.resolve(null)
} catch (err: Exception) {
promise.reject("up_save_distributor_failed", err)
}
}
// Asks the saved distributor for an endpoint. The endpoint itself arrives
// asynchronously via BulwarkUnifiedPushService.onNewEndpoint (event
// `up:newEndpoint`); failures via `up:registrationFailed`. `vapid` is the
// relay's VAPID public key - optional, some distributors use it to
// restrict who may push.
@ReactMethod
fun register(vapid: String?, promise: Promise) {
try {
UnifiedPush.register(
reactApplicationContext,
messageForDistributor = null,
vapid = vapid?.takeIf { it.isNotBlank() },
)
promise.resolve(null)
} catch (err: Exception) {
promise.reject("up_register_failed", err)
}
}
@ReactMethod
fun unregister(promise: Promise) {
try {
UnifiedPush.unregister(reactApplicationContext)
BulwarkUnifiedPushService.clearEndpoint(reactApplicationContext)
promise.resolve(null)
} catch (err: Exception) {
promise.reject("up_unregister_failed", err)
}
}
@ReactMethod
fun getEndpoint(promise: Promise) {
val stored = BulwarkUnifiedPushService.readEndpoint(reactApplicationContext)
if (stored == null) {
promise.resolve(null)
return
}
val (url, p256dh, auth) = stored
val map = Arguments.createMap().apply {
putString("url", url)
putString("p256dh", p256dh)
putString("auth", auth)
}
promise.resolve(map)
}
// NativeEventEmitter required no-ops.
@ReactMethod
fun addListener(eventName: String) {}
@ReactMethod
fun removeListeners(count: Int) {}
}
@@ -0,0 +1,100 @@
package com.anonymous.bulwarkmobile
import android.content.Context
import com.facebook.react.bridge.Arguments
import org.json.JSONArray
import org.json.JSONObject
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.PushService
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage
// Receives UnifiedPush events from the user's distributor app (ntfy, ...).
// The relay sends the same content-blind ForwardPayload it sends over FCM -
// as encrypted Web Push when the connector provided RFC 8291 keys (the
// connector decrypts before this service sees it) - so an incoming message is
// dispatched exactly like an FCM data message: headless task when the app is
// backgrounded, `fcm:message` device event for the live JS instance.
class BulwarkUnifiedPushService : PushService() {
override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
saveEndpoint(this, endpoint)
val params = Arguments.createMap().apply {
putString("url", endpoint.url)
putString("p256dh", endpoint.pubKeySet?.pubKey)
putString("auth", endpoint.pubKeySet?.auth)
}
BulwarkFcmModule.emit("up:newEndpoint", params)
}
override fun onMessage(message: PushMessage, instance: String) {
BulwarkMessagingService.ensureChannel(this)
val data = parsePayload(message.content) ?: return
if (!BulwarkMessagingService.isAppInForeground()) {
BulwarkMessagingService.startHeadlessTask(applicationContext, data)
}
// Same event name and shape as the FCM path so the foreground JS
// handler (App.tsx addMessageListener) works transport-agnostically.
val params = Arguments.createMap().apply {
val dataMap = Arguments.createMap()
for ((k, v) in data) dataMap.putString(k, v)
putMap("data", dataMap)
}
BulwarkFcmModule.emit("fcm:message", params)
}
override fun onUnregistered(instance: String) {
clearEndpoint(this)
BulwarkFcmModule.emit("up:unregistered", Arguments.createMap())
}
override fun onRegistrationFailed(reason: FailedReason, instance: String) {
val params = Arguments.createMap().apply { putString("reason", reason.name) }
BulwarkFcmModule.emit("up:registrationFailed", params)
}
companion object {
private const val PREFS = "bulwark_unifiedpush"
private const val KEY_URL = "endpointUrl"
private const val KEY_P256DH = "endpointP256dh"
private const val KEY_AUTH = "endpointAuth"
// The endpoint arrives asynchronously (possibly while no JS instance
// is alive), so persist it for BulwarkUnifiedPushModule.getEndpoint.
fun saveEndpoint(context: Context, endpoint: PushEndpoint) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
.putString(KEY_URL, endpoint.url)
.putString(KEY_P256DH, endpoint.pubKeySet?.pubKey)
.putString(KEY_AUTH, endpoint.pubKeySet?.auth)
.apply()
}
fun readEndpoint(context: Context): Triple<String, String?, String?>? {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val url = prefs.getString(KEY_URL, null) ?: return null
return Triple(url, prefs.getString(KEY_P256DH, null), prefs.getString(KEY_AUTH, null))
}
fun clearEndpoint(context: Context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
}
// The relay's ForwardPayload JSON, flattened to the string map the
// FCM data payload uses (repos/relay/src/fcm.ts) so the headless task
// and foreground handler can stay transport-blind.
fun parsePayload(content: ByteArray): Map<String, String>? = try {
val json = JSONObject(String(content, Charsets.UTF_8))
buildMap {
json.optString("kind").takeIf { it.isNotEmpty() }?.let { put("kind", it) }
json.optString("accountLabel").takeIf { it.isNotEmpty() }?.let { put("accountLabel", it) }
json.optString("accountId").takeIf { it.isNotEmpty() }?.let { put("accountId", it) }
put("emailIds", (json.optJSONArray("emailIds") ?: JSONArray()).toString())
put("changed", (json.optJSONObject("changed") ?: JSONObject()).toString())
}
} catch (_: Exception) {
null
}
}
}
@@ -17,15 +17,17 @@ class MainActivity : ReactActivity() {
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
setTheme(R.style.AppTheme);
ShareIntentStore.rewriteSendToAsView(intent)
super.onCreate(null)
NotificationTapStore.captureFromIntent(intent)
ShareIntentStore.captureFromIntent(intent)
ShareIntentStore.captureFromIntent(intent, contentResolver)
}
override fun onNewIntent(intent: Intent) {
ShareIntentStore.rewriteSendToAsView(intent)
super.onNewIntent(intent)
setIntent(intent)
ShareIntentStore.captureFromIntent(intent)?.let { share ->
ShareIntentStore.captureFromIntent(intent, contentResolver)?.let { share ->
BulwarkFcmModule.emit("app:share", share.toMap())
}
val payload = NotificationTapStore.captureFromIntent(intent) ?: return
@@ -40,13 +40,17 @@ object NotificationTapStore {
// the navigation payload.
val subject = extras.getString(EXTRA_SUBJECT)?.take(512)
val accountId = extras.getString(EXTRA_ACCOUNT_ID)?.takeIf { ACCOUNT_ID_PATTERN.matches(it) }
val payload = TapPayload(emailId, threadId, subject, accountId)
// The JMAP account a group/shared mailbox's message lives in - a JMAP
// id like the email id. Optional: older notifications don't carry it.
val jmapAccountId = extras.getString(EXTRA_JMAP_ACCOUNT_ID)?.takeIf { ID_PATTERN.matches(it) }
val payload = TapPayload(emailId, threadId, subject, accountId, jmapAccountId)
pending = payload
// Clear so a subsequent activity lifecycle event doesn't replay this.
extras.remove(EXTRA_EMAIL_ID)
extras.remove(EXTRA_THREAD_ID)
extras.remove(EXTRA_SUBJECT)
extras.remove(EXTRA_ACCOUNT_ID)
extras.remove(EXTRA_JMAP_ACCOUNT_ID)
return payload
}
@@ -55,12 +59,14 @@ object NotificationTapStore {
val threadId: String,
val subject: String?,
val accountId: String?,
val jmapAccountId: String?,
) {
fun toMap(): WritableMap = Arguments.createMap().apply {
putString("emailId", emailId)
putString("threadId", threadId)
if (subject != null) putString("subject", subject)
if (accountId != null) putString("accountId", accountId)
if (jmapAccountId != null) putString("jmapAccountId", jmapAccountId)
}
}
@@ -68,4 +74,5 @@ object NotificationTapStore {
const val EXTRA_THREAD_ID = "bulwark.notification.threadId"
const val EXTRA_SUBJECT = "bulwark.notification.subject"
const val EXTRA_ACCOUNT_ID = "bulwark.notification.accountId"
const val EXTRA_JMAP_ACCOUNT_ID = "bulwark.notification.jmapAccountId"
}
@@ -1,15 +1,19 @@
package com.anonymous.bulwarkmobile
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
// Process-wide holder for the most recent ACTION_SEND / ACTION_SEND_MULTIPLE
// intent (the system share sheet). MainActivity fills this on onCreate /
// onNewIntent; JS drains it via BulwarkFcm.getInitialShare or reacts to the
// live "app:share" event and opens the composer.
// live "app:share" event and opens the composer. `SENDTO mailto:` intents are
// rewritten into `VIEW mailto:` links instead (see rewriteSendToAsView).
object ShareIntentStore {
@Volatile
private var pending: SharePayload? = null
@@ -20,7 +24,7 @@ object ShareIntentStore {
return value
}
fun captureFromIntent(intent: Intent?): SharePayload? {
fun captureFromIntent(intent: Intent?, resolver: ContentResolver): SharePayload? {
val action = intent?.action ?: return null
if (action != Intent.ACTION_SEND && action != Intent.ACTION_SEND_MULTIPLE) return null
@@ -35,11 +39,14 @@ object ShareIntentStore {
val type = intent.type
if (text.isNullOrBlank() && uris.isEmpty()) return null
val files = uris.mapIndexed { i, uri -> describe(resolver, uri, type, i) }
val payload = SharePayload(
text = text,
subject = subject,
uris = uris.map { it.toString() },
mimeTypes = uris.map { type ?: "" },
mimeTypes = files.map { it.mimeType },
names = files.map { it.name },
sizes = files.map { it.size },
)
pending = payload
// Clear so a later lifecycle event doesn't replay this share.
@@ -50,6 +57,80 @@ object ShareIntentStore {
return payload
}
// `SENDTO mailto:` is how most apps start an email, but React Native's
// Linking only forwards VIEW intents, so the app opened on the inbox.
// Turn it into the equivalent `VIEW mailto:` link, folding the recipient,
// subject and text extras into the URI, so it reaches JS like any other
// mailto link: Linking.getInitialURL on a cold start, the "url" event
// while running. Must run before the intent is handed to React Native.
fun rewriteSendToAsView(intent: Intent?) {
val action = intent?.action ?: return
if (action != Intent.ACTION_SENDTO) return
val data = intent.data ?: return
if (!"mailto".equals(data.scheme, ignoreCase = true)) return
val params = mutableListOf<String>()
addressExtra(intent, Intent.EXTRA_EMAIL).forEach { params.add("to=${Uri.encode(it)}") }
addressExtra(intent, Intent.EXTRA_CC).forEach { params.add("cc=${Uri.encode(it)}") }
addressExtra(intent, Intent.EXTRA_BCC).forEach { params.add("bcc=${Uri.encode(it)}") }
intent.getCharSequenceExtra(Intent.EXTRA_SUBJECT)?.let {
params.add("subject=${Uri.encode(it.toString().take(512))}")
}
intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.let {
params.add("body=${Uri.encode(it.toString().take(MAX_TEXT))}")
}
val link = data.toString()
val separator = if (link.contains('?')) "&" else "?"
intent.action = Intent.ACTION_VIEW
intent.data = if (params.isEmpty()) data else Uri.parse(link + separator + params.joinToString("&"))
}
// EXTRA_EMAIL/CC/BCC are documented as String[], but some apps put a
// single String.
private fun addressExtra(intent: Intent, key: String): List<String> =
intent.getStringArrayExtra(key)?.filterNotNull() ?: listOfNotNull(intent.getStringExtra(key))
private class SharedFile(val name: String, val mimeType: String, val size: Double)
// Ask the provider for the file's real name, type and size: a MediaStore
// URI ends in a bare row id ("1000000027") and SEND_MULTIPLE only carries
// a wildcard type such as "image/*". Size is -1 when unknown.
private fun describe(resolver: ContentResolver, uri: Uri, intentType: String?, index: Int): SharedFile {
var displayName: String? = null
var size = -1.0
try {
resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameColumn >= 0 && !cursor.isNull(nameColumn)) displayName = cursor.getString(nameColumn)
val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE)
if (sizeColumn >= 0 && !cursor.isNull(sizeColumn)) size = cursor.getLong(sizeColumn).toDouble()
}
}
} catch (_: Exception) {
// Provider without OpenableColumns: fall back to the URI below.
}
val resolvedType = try {
resolver.getType(uri)
} catch (_: Exception) {
null
}
val mimeType = resolvedType ?: intentType?.takeUnless { it.contains('*') } ?: ""
val name = displayName?.takeIf { it.isNotBlank() } ?: fallbackName(uri, mimeType, index)
return SharedFile(name, mimeType, size)
}
// Last path segment plus an extension for the MIME type, so the
// recipient's mail client can still open the attachment.
private fun fallbackName(uri: Uri, mimeType: String, index: Int): String {
val base = uri.lastPathSegment?.substringAfterLast('/')?.takeIf { it.isNotBlank() }
?: "shared-${index + 1}"
if (base.contains('.')) return base
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
return if (extension.isNullOrEmpty()) base else "$base.$extension"
}
@Suppress("DEPRECATION")
private fun streamExtra(intent: Intent): Uri? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -71,12 +152,16 @@ object ShareIntentStore {
val subject: String?,
val uris: List<String>,
val mimeTypes: List<String>,
val names: List<String>,
val sizes: List<Double>,
) {
fun toMap(): WritableMap = Arguments.createMap().apply {
if (text != null) putString("text", text)
if (subject != null) putString("subject", subject)
putArray("uris", Arguments.fromList(uris))
putArray("mimeTypes", Arguments.fromList(mimeTypes))
putArray("names", Arguments.fromList(names))
putArray("sizes", Arguments.fromList(sizes))
}
}
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Status-bar icon for mail notifications: the Bulwark shield from
assets/logos/Bulwark Logo White.svg, white on transparent. Android draws
only the alpha channel of a small icon; the group scale keeps the shield
inside the 22dp live area of the 24dp canvas. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<group
android:pivotX="500"
android:pivotY="500"
android:scaleX="0.9"
android:scaleY="0.9">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M 373.467 557.247 L 55.226 271.52 L 55.226 385.564 C 55.226 398.153 62.453 415.6 71.355 424.501 L 107.525 460.671 C 116.426 469.573 123.653 487.02 123.653 499.609 L 123.653 682.08 C 123.655 718.462 133.336 750.017 152.047 778.668 L 373.467 557.247 Z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M 626.533 557.247 L 944.774 271.52 L 944.774 396.969 C 944.774 403.263 941.16 411.987 936.709 416.437 L 884.411 468.736 C 879.96 473.186 876.347 481.91 876.347 488.204 L 876.347 682.08 C 876.345 718.462 866.664 750.017 847.953 778.668 L 626.533 557.247 Z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M 500 638.743 L 500 638.743 C 466.206 638.738 448.855 625.316 424.448 603.033 L 197.809 829.672 C 261.382 884.635 364.213 931.859 500 990 Q 500 990 500 990 C 635.787 931.859 738.618 884.635 802.191 829.672 L 575.552 603.033 C 551.145 625.316 533.794 638.738 500 638.743 Z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M 483.028 563.647 L 63.713 187.183 C 59.029 182.978 55.226 174.454 55.226 168.159 L 55.226 122.542 C 55.226 116.247 60.206 109.988 66.339 108.573 L 215.181 74.225 C 221.314 72.809 226.293 76.77 226.293 83.065 L 226.293 176.92 L 352.034 147.895 C 358.167 146.479 363.147 140.219 363.147 133.925 L 363.147 51.483 C 363.147 45.189 368.126 38.93 374.259 37.514 L 488.888 11.061 C 495.021 9.646 504.979 9.646 511.112 11.061 L 625.741 37.514 C 631.874 38.93 636.853 45.189 636.853 51.483 L 636.853 133.925 C 636.853 140.219 641.833 146.479 647.966 147.895 L 773.707 176.92 L 773.707 83.065 C 773.707 76.77 778.686 72.809 784.819 74.225 L 933.661 108.573 C 939.794 109.988 944.774 116.247 944.774 122.542 L 944.774 168.159 C 944.774 174.454 940.971 182.978 936.287 187.183 L 516.972 563.647 C 507.605 572.056 492.395 572.056 483.028 563.647 Z" />
</group>
</vector>
@@ -1 +1,6 @@
<resources/>
<resources>
<!-- As `splash.dark.backgroundColor` in app.config.js. -->
<color name="splashscreen_background">#09090b</color>
<!-- Matches the dark palette background (src/theme/tokens.ts). -->
<color name="window_background">#09090b</color>
</resources>
+4 -1
View File
@@ -1,6 +1,9 @@
<resources>
<color name="splashscreen_background">#09090b</color>
<!-- Splash background, as `splash.backgroundColor` in app.config.js; the dark one is in values-night. -->
<color name="splashscreen_background">#ffffff</color>
<color name="iconBackground">#09090b</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#09090b</color>
<!-- Behind the React root until the first frame; matches the light palette background (src/theme/tokens.ts). -->
<color name="window_background">#ffffff</color>
</resources>
@@ -4,6 +4,7 @@
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#09090b</item>
<item name="android:windowBackground">@color/window_background</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
+11
View File
@@ -25,6 +25,11 @@ android.useAndroidX=true
# Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true
# Shrink release builds with R8 (code) and the resource shrinker. Keep rules
# for classes reached by reflection go in app/proguard-rules.pro.
android.enableMinifyInReleaseBuilds=true
android.enableShrinkResourcesInReleaseBuilds=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
@@ -63,3 +68,9 @@ expo.useLegacyPackaging=false
# Specifies whether the app is configured to use edge-to-edge via the app config or plugin
# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge.
expo.edgeToEdgeEnabled=true
# AsyncStorage caps its SQLite database at 6 MB unless this is set. The
# offline mail cache keeps message bodies there and can be set to 500 MB,
# and once the database is full every write fails, the account registry
# and the outbox included. This is a ceiling, not an allocation.
AsyncStorage_db_size_in_MB=1024
+15
View File
@@ -0,0 +1,15 @@
const path = require('node:path');
// babel-preset-expo ships inside `expo` rather than as a direct dependency, so
// resolve it from there; this is the preset Expo uses when no config exists.
const presetExpo = require.resolve('babel-preset-expo', {
paths: [path.dirname(require.resolve('expo/package.json'))],
});
module.exports = function babelConfig(api) {
api.cache(true);
return {
presets: [presetExpo],
plugins: [require.resolve('./scripts/babel-plugin-lucide-imports')],
};
};
+294
View File
@@ -0,0 +1,294 @@
# Android release signing
The release APK is built by
[.github/workflows/release-android.yml](../.github/workflows/release-android.yml)
on an Ubuntu runner whenever a GitHub release is published, and attached to that
release. Installed apps pick it up through the in-app updater.
Every APK up to and including 0.1.62 was signed with `android/app/debug.keystore`,
the debug key that ships with the React Native template. Its password is
`android` and copies of it are everywhere, so anyone could sign an APK that
Android accepts as an update to Bulwark Mobile. Such an "update" runs as the
app, with its accounts, SecureStore credentials and mail cache. This is
[S2 in the 2026-09 audit](audit-2026-09.md#s2).
Release builds now need a real key:
- `android/app/build.gradle` reads the key from `BULWARK_RELEASE_*` properties.
Without them, a release build fails in `checkReleaseSigning` before anything
is packaged. Debug builds and `npx expo run:android` are not affected.
- The workflow fails in its first step when a signing secret is missing, checks
the key before the build, and refuses to upload an APK that Android 9+ would
not see as signed by the release key.
- `versionCode` now comes from `VERSION` (see [versionCode](#versioncode)).
## One-time setup
The commands are for Git Bash on Windows, run from the repository root. They
need `keytool`, which ships with every JDK, and `apksigner` from the Android SDK
build-tools. Its `.jar` runs on Java 8 or newer:
```bash
apksigner() { java -jar "$LOCALAPPDATA/Android/Sdk/build-tools/36.0.0/lib/apksigner.jar" "$@"; }
```
On macOS or Linux, call `$ANDROID_HOME/build-tools/36.0.0/apksigner` instead.
### 1. Generate the release key
```bash
read -rsp 'Keystore password: ' BULWARK_KS_PASS; echo; export BULWARK_KS_PASS
keytool -genkeypair -v -storetype PKCS12 -keystore bulwark-release.p12 \
-alias bulwark -keyalg RSA -keysize 4096 -validity 10000 \
-dname "CN=Bulwark Mail, O=Bulwark Mail" \
-storepass:env BULWARK_KS_PASS -keypass:env BULWARK_KS_PASS
```
`-validity 10000` is about 27 years. A PKCS12 keystore uses one password for
the store and the key, so `ANDROID_KEYSTORE_PASSWORD` and `ANDROID_KEY_PASSWORD`
below get the same value. `*.p12` is gitignored, but move the file out of the
checkout once the secrets are set.
**Back up `bulwark-release.p12` and its password in two places**, for example
a password manager and an offline copy. If the key is lost, no later APK can
update an installed app: every user has to uninstall, which loses their accounts
and local data, and install again. If the key leaks, you are back where S2
started.
The certificate's SHA-256 is what the workflow log prints as the release key:
```bash
keytool -exportcert -keystore bulwark-release.p12 -alias bulwark \
-storepass:env BULWARK_KS_PASS | sha256sum
```
### 2. Create the rotation lineage
Skip this and the first release signed with the new key cannot be installed
over any existing install. Android reports "App not installed as package
conflicts with an existing package", and every user has to uninstall and
reinstall.
APK Signature Scheme v3, read by Android 9 and newer, lets an APK carry a
*signing lineage*: a record signed by the old key that names the new key as its
successor. The old key here is the public debug key, so you can create that
record yourself:
```bash
apksigner rotate --out bulwark.lineage \
--old-signer --ks android/app/debug.keystore --ks-key-alias androiddebugkey \
--ks-pass pass:android --key-pass pass:android \
--set-installed-data true --set-rollback false \
--set-permission false --set-auth false --set-shared-uid false \
--new-signer --ks bulwark-release.p12 --ks-key-alias bulwark \
--ks-pass env:BULWARK_KS_PASS --key-pass env:BULWARK_KS_PASS
apksigner lineage --in bulwark.lineage --print-certs
```
The `--set-*` flags on the old signer decide what the debug key can still do on
a device that has moved to the new key:
- `--set-installed-data true` lets the new key take over an install made with
the debug key. Without it the rotation does nothing.
- `--set-rollback false` keeps an APK signed with the debug key from being
accepted again afterwards. With `true` the rotation would be pointless, and the
workflow refuses such a lineage.
- `--set-permission false` stops granting the app's signature permissions to
apps signed with the debug key, which includes every React Native debug build.
The merged manifest defines one,
`com.anonymous.bulwarkmobile.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION`. It
guards the receivers that are registered as not exported on Android 12L and
older.
- `--set-auth false` and `--set-shared-uid false`: the app uses neither.
The lineage holds only certificates and a signature made with the debug key, so
it is not secret. It is stored as a secret only so the workflow can find it.
### 3. Repository secrets
Settings → Secrets and variables → Actions:
| Secret | Value |
| --- | --- |
| `ANDROID_KEYSTORE_BASE64` | `bulwark-release.p12`, base64-encoded |
| `ANDROID_KEYSTORE_PASSWORD` | The keystore password |
| `ANDROID_KEY_ALIAS` | `bulwark` |
| `ANDROID_KEY_PASSWORD` | The same password again |
| `ANDROID_SIGNING_LINEAGE_BASE64` | `bulwark.lineage`, base64-encoded. Optional, but see step 2 |
With the GitHub CLI, from the checkout and in the same shell as step 1:
```bash
base64 -w0 bulwark-release.p12 | gh secret set ANDROID_KEYSTORE_BASE64
printf %s "$BULWARK_KS_PASS" | gh secret set ANDROID_KEYSTORE_PASSWORD
printf %s "$BULWARK_KS_PASS" | gh secret set ANDROID_KEY_PASSWORD
gh secret set ANDROID_KEY_ALIAS --body bulwark
base64 -w0 bulwark.lineage | gh secret set ANDROID_SIGNING_LINEAGE_BASE64
```
Or copy a file as base64 for the web form:
```powershell
[Convert]::ToBase64String([IO.File]::ReadAllBytes('bulwark-release.p12')) | Set-Clipboard
```
## The first release with the new key
1. Set all five secrets.
2. Release as usual with `npm run bump`.
3. In the workflow run, "Verify the APK signature" prints the release key's
SHA-256 as the Android 9+ signer and `fac61745…3b9c`, the debug key, as the
Android 7-8.1 signer, followed by a warning about the latter. The warning is
expected for as long as the lineage is used.
4. Check it on the emulators before you announce anything:
- Install 0.1.62 (`gh release download 0.1.62 -p '*.apk'`) on
`Pixel_6_Pro_API_32` and `Pixel_7_API_34` and sign in. Then `adb install -r`
the new APK. It has to install over 0.1.62 and keep the account.
- Set `VERSION` above the new release locally (don't commit it), build with
`-PallowDebugSignedRelease=true` (see [Local release builds](#local-release-builds))
and `adb install -r` that APK over the updated app. It has to fail with
`INSTALL_FAILED_UPDATE_INCOMPATIBLE`.
5. Add a line `severity: security` to the release notes (web UI or
`gh release edit`). The in-app update banner then cannot be dismissed, which
matters because a device on 0.1.62 accepts a debug-signed "update" until it
has installed this release.
## What rotation does and does not protect
| Device | After it installs a rotated release |
| --- | --- |
| Android 9 and newer (API 28+) | Updates in place and keeps accounts and data. From then on it accepts only updates signed with the release key. An APK signed with the debug key is refused, and so is one that carries a lineage from the debug key to some other key. |
| Android 7.0-8.1 (API 24-27) | Updates in place, but these versions do not read v3 signatures. They check the v2 signature, which the workflow still makes with the debug key so that they can update at all. They keep accepting debug-signed "updates". |
What it does not cover:
- **Devices that have not updated yet.** Until a device installs a rotated
release it trusts the debug key as before. Whoever gets a debug-signed APK onto
it first can attach a lineage to their own key and keep the app for good. The
window closes for each device when it updates, which is why the release should
carry `severity: security`.
- **Android 7.0-8.1**, as long as the lineage is used. New installs on those
versions also start out trusting the debug key. The only fix is
[ending the migration](#ending-the-migration), after which they need a
reinstall.
- **Anything that already happened.** Rotation does not repair a device that
already runs a malicious "update".
- **The release key itself.** Rotation moves the trust to
`bulwark-release.p12`. It is only as safe as that file and its password.
The workflow passes `--rotation-min-sdk-version 28` to `apksigner sign`. By
default apksigner puts the rotated key in a v3.1 block that only Android 13+
reads and signs the v3.0 block with the old key, which would leave Android
9-12L on the debug key.
If you restricted the Firebase API key in `google-services.json` to Android apps
by certificate fingerprint in the Google Cloud console, add the release key's
SHA-1 there before the first release and keep the debug key's entry until the
migration has ended. Otherwise push registration can fail. An unrestricted key
needs nothing.
## Ending the migration
Keep `ANDROID_SIGNING_LINEAGE_BASE64` set while installs of 0.1.62 or older may
still be around: without it they cannot update at all.
Once you delete it, every APK is signed with the release key alone:
- Devices that already moved to the release key keep updating normally.
- Anything still on 0.1.62 or older, and every Android 7.0-8.1 install, refuses
the update. Those users have to uninstall, reinstall and sign in again, and
they lose local data. Afterwards their devices trust only the release key.
## Local release builds
CI signs releases, so you normally don't need the release key on your machine.
For a local release build, for profiling or checking R8, opt in to the debug key:
```bash
cd android
./gradlew assembleRelease -PallowDebugSignedRelease=true
```
```powershell
$env:ORG_GRADLE_PROJECT_allowDebugSignedRelease = 'true'
npx expo run:android --variant release
```
The build prints a warning. The APK is signed with the public key, so never
give it to anyone. On a device that has moved to the release key it will not
install over the official app; uninstall that first. `allowDebugSignedRelease`
is ignored when the `CI` environment variable is set, so the workflow cannot
use it.
To sign locally with the real key, set these as `-P` flags, in
`~/.gradle/gradle.properties` or as environment variables. Never put them in
`android/gradle.properties`, which is committed.
```properties
BULWARK_RELEASE_STORE_FILE=C:/Users/you/keys/bulwark-release.p12
BULWARK_RELEASE_STORE_PASSWORD=...
BULWARK_RELEASE_KEY_ALIAS=bulwark
BULWARK_RELEASE_KEY_PASSWORD=...
```
That APK has no lineage, so it will not install over a debug-signed install
either.
With neither, a release build stops and lists what is missing. Debug builds and
`npx expo run:android` never need any of this.
## Stack traces
Release builds are shrunk and obfuscated by R8, so library frames in a crash
report look like `com.facebook.react.uimanager.U.a`. The app's own classes
(`com.anonymous.bulwarkmobile`) keep their names. The workflow attaches
`bulwark-mobile-<version>-<commit>-mapping.txt` to the release next to the APK
and keeps it as a workflow artifact. Decode a trace with the mapping of that
exact build:
```bash
retrace bulwark-mobile-0.1.63-abc1234-mapping.txt stacktrace.txt
```
`retrace` is in the Android SDK command-line tools (`cmdline-tools/latest/bin`).
A local build writes its mapping to
`android/app/build/outputs/mapping/release/mapping.txt`.
## versionCode
`versionCode` was `1` for every release up to 0.1.62. It is now derived from
`VERSION` in `android/app/build.gradle`:
```
major * 10000000 + minor * 10000 + patch 0.1.63 -> 10063
```
Android refuses an update whose `versionCode` is lower than the installed one,
so the number may only grow. The formula keeps growing while patch stays below
10000 and minor below 1000, and `npm run bump` refuses to go past either. It
stays under Android's limit of 2100000000 up to major 209. Existing installs
have `versionCode` 1, so the first release with this change updates them
normally.
Never switch to a formula that gives smaller numbers: installed apps would
refuse every update until the numbers caught up.
## Rotating again
If the release key ever has to change, extend the lineage instead of starting
a new one. Generate `bulwark-release-2.p12` as in step 1, with its password in
`NEW_KS_PASS`:
```bash
apksigner rotate --in bulwark.lineage --out bulwark-2.lineage \
--old-signer --ks bulwark-release.p12 --ks-key-alias bulwark \
--ks-pass env:BULWARK_KS_PASS --key-pass env:BULWARK_KS_PASS --set-rollback false \
--new-signer --ks bulwark-release-2.p12 --ks-key-alias bulwark \
--ks-pass env:NEW_KS_PASS --key-pass env:NEW_KS_PASS
```
Then update the secrets. The workflow accepts any lineage that starts at the
debug key and ends at the key in `ANDROID_KEYSTORE_BASE64`, and refuses one in
which an earlier key has the rollback capability. As with the first rotation,
only Android 9+ devices that install the new release follow it. If the old key
leaked, a device that someone reached first stays theirs.
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -54,7 +54,7 @@ RN covers the happy paths (password login, webmail-mediated OAuth handoff, QR pa
- What RN does: `rewriteSessionUrls` only rewrites absolute URLs; `extractOrigin` returns null for `/jmap/` so the relative value is passed through unchanged and `secureFetch('/jmap/')` fails (`src/api/jmap-client.ts:320-340`). Same in the unified inbox `rewriteApiUrl` (`src/api/unified-inbox.ts:55-60`).
- Fix hint: when the URL has no `^https?://`, return `serverOrigin + (url.startsWith('/') ? url : '/' + url)`; keep the plain string splitting (the comment about the RN URL polyfill corrupting templates still applies).
- [x] **Redirected session fetch may lose the Authorization header** — `P3` — `partial` — fixed in 0b1c240
- [x] **Redirected session fetch may lose the Authorization header** — `P3` — `partial` — fixed in 0b1c240; the check keyed on `response.redirected`, which React Native's fetch never sets, so it only works since 859770b (audit B15)
- What WEB does: `fetchSessionResponse` detects a redirected 200 with no accounts/username (Safari and some auth proxies drop `Authorization` on redirect) and refetches `response.url` with the header (`lib/jmap/client.ts:911-925`).
- What RN does: `fetchSession` trusts the redirected response (`src/api/jmap-client.ts:363-383`); Stalwart 0.16.19 answers `/.well-known/jmap` with a 307 to `/jmap/session` (verified against stw-test19), and iOS `NSURLSession` can strip the header on redirect, which ends in `resolveAccountId` throwing "No account found in JMAP session".
- Fix hint: mirror the WEB check (`response.redirected` and empty `accounts`/`username` → refetch `response.url` with the header).
@@ -106,12 +106,12 @@ RN covers the happy paths (password login, webmail-mediated OAuth handoff, QR pa
- What RN does: `connectWithOAuth` uses `session.username || accessToken.slice(0, 8)` (`src/api/jmap-client.ts:135-136`), so the same mailbox logged in via password and via OAuth can produce two registry entries, and the drawer synthesizes `username@host`.
- Fix hint: read `Identity/get` after connect and use the primary identity email for `email` (and ideally for the registry id).
- [ ] **Cannot remove a non-active account** — `P2` — `missing` (changelog 1.7.8 "Remove a specific account from the switcher")
- [x] **Cannot remove a non-active account** — `P2` — `missing` (changelog 1.7.8 "Remove a specific account from the switcher") — done in 6ae21d5, 490355c
- What WEB does: per-row remove button in the switcher (`components/layout/account-switcher.tsx:138-143, 322-333`) backed by `removeAccount` which tears down the client, evicts caches and clears cookies (`stores/auth-store.ts:1382-1401`).
- What RN does: only "Sign out of <active>" and "Sign out all" (`src/components/SidebarDrawer.tsx:400-437`).
- Fix hint: add a per-row remove (long-press or trailing X) that runs `teardownPushNotificationsForAccount(id)`, `jmapClient.clearAccountCredentials(id)`, `useEmailStore.removeAccount(id)`, `accountStore.removeAccount(id)`.
- [ ] **Switcher shows stale connection state and no error indicator** — `P3` — `partial`
- [x] **Switcher shows stale connection state and no error indicator** — `P3` — `partial` — done in 6ae21d5 (the drawer shows `hasError` with its message)
- What WEB does: shows `hasError` (alert icon) with `errorMessage` and a live `isConnected` dot per account (`account-switcher.tsx:296-303`).
- What RN does: renders only the `isConnected` dot (`SidebarDrawer.tsx:355-361`); `hasError`/`errorMessage` (set in `restoreSession`) are never displayed, and since only the active account is ever connected, other rows show whatever value they had last.
- Fix hint: render `hasError`; show the dot only for the active account (or treat inactive as "cached").
@@ -126,17 +126,17 @@ RN covers the happy paths (password login, webmail-mediated OAuth handoff, QR pa
- What RN does: `authMode = props.authMode ?? 'basic'` and `quotaUsed = props.quotaUsed ?? 0` (`AccountSettings.tsx:46-47`) but `SettingsScreen` renders `<Component />` with no props (`SettingsScreen.tsx:242`), so OAuth accounts read "Basic" and the storage row never appears (RN has no `Quota/get` at all — flag for the mail/settings audit).
- Fix hint: derive from `jmapClient.usesBearerAuth` (or persist `authMode` on `AccountEntry`); fetch quota via `Quota/get` when `urn:ietf:params:jmap:quota` is advertised.
- [ ] **Hard account cap of 5** — `P3` — `partial`
- [x] **Hard account cap of 5** — `P3` — `partial` — done in 6ae21d5 (cap raised to 10, `src/lib/account-utils.ts:4`)
- What WEB does: 5 on HTTP/1.1, lifted to 50 when HTTP/2 is observed, because each web account pins an SSE socket (`lib/account-utils.ts:75-107`).
- What RN does: `MAX_ACCOUNTS = 5` (`src/lib/account-utils.ts:1`) although only the active account holds a live connection.
- Fix hint: raise the constant (or drop the cap) once the unified inbox cost per account is acceptable.
- [ ] **Shared/group account settings scope is missing** — `P3` — `missing` (changelog 1.7.5 "Manage shared/group account settings from the Accounts page") — deferred: shared accounts are now listed read-only in AccountSettings (490355c); scoped filter/vacation stores need an accountId parameter first
- [ ] **Shared/group account settings scope is missing** — `P3` — `missing` (changelog 1.7.5 "Manage shared/group account settings from the Accounts page") — partly: shared accounts are listed in AccountSettings (490355c), and tapping one scopes Filters and Vacation to it since a86cd1d; calendar and contacts settings are not scoped yet
- What WEB does: lists `client.getSharedAccounts()` (non-primary) on the Accounts page and enters a scoped settings mode (filters, vacation, calendars, contacts) via `managed-account-store` (`components/settings/account-settings.tsx:43-48, 112-118, 225-262`; `stores/managed-account-store.ts`).
- What RN does: `getSharedMailAccounts()` exists for mail/unified inbox (`src/api/jmap-client.ts:480-495`) but there is no shared-account listing or scoped settings.
- Fix hint: list non-personal `session.accounts` in `AccountSettings` and pass an `accountId` into the filter/vacation stores.
- [ ] **Unified-inbox shared-account filter disagrees with `getSharedMailAccounts`** — `P3` — `rn-only-bug`
- [x] **Unified-inbox shared-account filter disagrees with `getSharedMailAccounts`** — `P3` — `rn-only-bug` — done in 6ae21d5
- What WEB does: a non-personal account counts as mail-capable even when its `accountCapabilities` omit mail (`lib/jmap/client.ts:4421-4445`); RN's own `getSharedMailAccounts` mirrors that (`jmap-client.ts:486-493`).
- What RN does: the unified inbox requires `accountCapabilities == null || MAIL in accountCapabilities` (`src/api/unified-inbox.ts:208-210`), so a shared account that advertises other capabilities but not mail is silently skipped there while it appears in the sidebar.
- Fix hint: reuse the `!advertisesMail && info.isPersonal → skip` rule from `getSharedMailAccounts`.
+13 -13
View File
@@ -74,7 +74,7 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
### Unified mailbox / cross-account views
- [x] **Unified scope: account-bounded by default, cross-account opt-in; single-account + group inboxes case** — `P2` — `partial` — fixed in 6ae21d5
- [x] **Unified scope: account-bounded by default, cross-account opt-in; single-account + group inboxes case** — `P2` — `partial` — fixed in 6ae21d5; the drawer only shows the Unified section when it can hold more than one account's mail since af6290f (#843)
- What WEB does: unified views stay inside the active account (own + shared/group folders) unless `unifiedCrossAccount` is on *and* the admin gate allows it (`components/mail/mail-app.tsx:465-500`, `stores/settings-store.ts:405-431`); the section is shown with one account as soon as `includeGroupInUnified && hasGroupInboxes` (`components/layout/sidebar.tsx:905-907`).
- What RN does: "All inboxes" is shown only when `accounts.length > 1` (`src/components/SidebarDrawer.tsx:438-446`) and always spans every account (`src/api/unified-inbox.ts:236-257`). A single account with group inboxes gets no unified view even with "Include Group Inboxes" on (`ReadingSettings.tsx:257`). Admin policy gates do not exist in RN (see N/A).
- Fix hint: show the row when `accounts.length > 1 || (includeGroupInUnified && mailboxes.some(m => m.isShared))`; add a `unifiedCrossAccount` toggle (default off, like WEB) and pass only the active account id when it is off. Note WEB's default for `includeGroupInUnified` is `true` (`settings-store.ts:639`) while RN defaults to `false` (`src/stores/settings-store.ts:235`).
@@ -102,14 +102,14 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
- [x] **No actions on unified rows (swipe, star, read, delete, archive, spam, selection)** — `P2` — `missing` — fixed in 6ae21d5
- What WEB does: every list action resolves the email's own client + owner account via `resolveEmailActionContext` (`stores/email-store.ts:581-640`), incl. batch actions grouped by `sourceAccountId` (`:2634-2660`, `:2740-2760`), archive into the owner's archive (`components/mail/mail-app.tsx:2141-2160`); changelog 1.7.5/1.7.7 "route counter/keyword updates to the email's own account in aggregate views".
- What RN does: rows are plain `Pressable`s (`src/screens/UnifiedInboxScreen.tsx:95-138`); nothing but open.
- Fix hint: wrap rows in `SwipeableRow`; because the JMAP client is single-account, actions on a *different registry account* need the detached `jmapPost` path (`unified-inbox.ts:116-131`) with `Email/set` against `email.jmapAccountId`; for the active account and its group accounts route through `setEmailKeywords(..., email.jmapAccountId)` / `moveEmail(...)` overrides.
- Fix hint: wrap rows in `SwipeableRow`; because the JMAP client is single-account, actions on a *different registry account* need the detached `jmapPost` path (`unified-inbox.ts:116-131`) with `Email/set` against `email.jmapAccountId`; for the active account and its group accounts route through `setEmailKeywords(..., email.jmapAccountId)` / `moveEmail(...)` overrides. (`setEmailKeywords` is gone since 5041897; keyword changes now go through `patchKeywordsForEmails` in `src/api/email.ts` or the store's `setKeywordForEmails`.)
- [x] **Unified list stays stale after acting on a message; just-read mail handling** — `P3` — `partial` — fixed in 6ae21d5
- What WEB does: aggregate views refresh through the fan-out on push (`refreshCurrentMailbox`, #791) and keep just-read/unstarred rows in the Unread/Starred views until re-opened (`retainedInViewIds`, `stores/email-store.ts:1007-1050`).
- What RN does: `UnifiedInboxScreen` loads once per mount/`includeGroup` change (`:57-59`); after opening (mark read) or deleting in the thread screen and going back, the unread dot/row is stale until pull-to-refresh. No push hookup.
- Fix hint: `useFocusEffect` → reload, or patch the local list from the thread screen result; when Unread/Starred views are added, keep the WEB retain semantics.
- [x] **Opening a unified-inbox message that is not in the active folder page shows another message's body** — `P1` — `rn-only-bug` — fixed in c8be383
- [x] **Opening a unified-inbox message that is not in the active folder page shows another message's body** — `P1` — `rn-only-bug` — fixed in c8be383; the viewer's actions still hit the open folder's account and followed the live list until 5c7301b and 9c31de2 (audit B3, B5)
- What WEB does: opens the clicked row object itself and fetches by source client/account (#847, `components/mail/mail-app.tsx:3111-3190`).
- What RN does: `UnifiedInboxScreen.onOpen` switches account then navigates to `EmailThread` (`src/screens/UnifiedInboxScreen.tsx:61-86`). `EmailThreadScreen` pages over the *active folder's* `emails` (`src/screens/EmailThreadScreen.tsx:131-133`, FlatList `data={emails}` `:538`, `initialScrollIndex={Math.max(0, findIndex)}`). A group-inbox message (or any message not in the first page of the user's own inbox snapshot) is not in `emails`, so index 0 is shown: the pane renders `emails[0]` while the toolbar/`activeEmailId` (and delete/archive/spam) refer to the message that was tapped; with an empty snapshot the body area is blank. Bare-id `findIndex` also collides across accounts (Stalwart reuses id ranges, #847).
- Fix hint: when `route.params.emailId` is not found in `emails` (or `jmapAccountId` is set), page over a one-element list `[{ id, threadId }]` instead of `emails`; compare ids together with the owning account.
@@ -130,7 +130,7 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
- What RN does: `EmailRow` shows no tags; `tagPill/tagDot/tagText` styles exist but are unused (`src/screens/EmailListScreen.tsx:1481-1495`). Tags are only visible inside the thread screen tag menu.
- Fix hint: compute tag ids from `item.keywords` (`$label:*`, `$color:*`), look up `useKeywordsStore().keywords` for label/colour (unknown ids → grey with raw id), render pills on the subject row; add a `tintListRowsByTag` setting if desired.
- [x] **Tag view (tap a tag → cross-folder `hasKeyword` list, #175) and tag counts in the drawer** — `P2` — `missing` — fixed in 6ae21d5
- [x] **Tag view (tap a tag → cross-folder `hasKeyword` list, #175) and tag counts in the drawer** — `P2` — `missing` — fixed in 6ae21d5; the view and the badges cover shared accounts since 8d5bced (#1038), and the counts are no longer re-fetched on every drawer open since d097ae9
- What WEB does: "Tags" sidebar section with unread/total per tag (`fetchTagCounts`, `stores/email-store.ts:1192`), visibility rules (`components/layout/sidebar.tsx:1002-1013`), `selectKeyword` → `getEmails(undefined, …, '$label:<id>')` across all folders (`stores/email-store.ts:1343-1420`, changelog 1.4.13 #175), counts kept in step with read/unread (1.7.8).
- What RN does: no tag section in `SidebarDrawer`; `EmailFilters` has no keyword field (`src/stores/email-store.ts:140-149`).
- Fix hint: add `keyword?: string` to `EmailFilters` and a "Tags" section in the drawer; when set, `buildJmapFilter` should omit `inMailbox` (search across folders) and add `{ hasKeyword: '$label:<id>' }`; counts via one `Email/query` (calculateTotal) per tag, optional.
@@ -160,12 +160,12 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
- What RN does: tapping a draft opens the read-only `EmailThreadScreen`; `Compose` params have no draft/edit mode (`src/navigation/types.ts:7-24`), locale keys `email_viewer.draft_banner`/`edit_draft` exist but are unused.
- Fix hint: in `handleRowPress`, if `item.keywords.$draft` (or folder role is drafts) navigate to `Compose` with a `draft: { id, to, cc, bcc, subject, body, blobId }` param; on send, destroy the old draft (composer area).
- [ ] **Permanent delete without confirmation (Trash, `deleteAction: permanent`, junk auto-permanent)** — `P2` — `bugfix-parity` — list side fixed in 3194b5e (single, batch, swipe via `src/lib/delete-confirm.ts`); deferred: thread screen `onDelete` belongs to the viewer area — use `confirmPermanentDelete()`/`isPermanentDelete()` from `src/lib/delete-confirm.ts`
- [x] **Permanent delete without confirmation (Trash, `deleteAction: permanent`, junk auto-permanent)** — `P2` — `bugfix-parity` — list side fixed in 3194b5e (single, batch, swipe via `src/lib/delete-confirm.ts`); viewer side done in d2ed27f (`src/screens/EmailThreadScreen.tsx`)
- What WEB does: confirm dialog before any permanent destroy, single and batch (`components/mail/mail-app.tsx:2086-2100`, `components/email/email-list.tsx:229-247`).
- What RN does: `deleteEmail`/`deleteEmailsBatch` destroy immediately when in Trash, when `deleteAction === 'permanent'`, or junk + `permanentlyDeleteJunk` (`src/stores/email-store.ts:1286-1298`, `1449-1458`); the swipe fires without any prompt (`EmailListScreen.tsx:369-378`), and no undo is offered (correctly, `:1322-1324`).
- Fix hint: compute `destroy` in the screen before calling the store (same rule) and show `Alert.alert` with Cancel/Delete; same for the thread screen `onDelete`.
- [ ] **"Never" mark-as-read (`markAsReadDelay === -1`) marks read instantly** — `P2` — `rn-only-bug`
- [x] **"Never" mark-as-read (`markAsReadDelay === -1`) marks read instantly** — `P2` — `rn-only-bug` — fixed in d2ed27f
- What WEB does: `-1` → never, `0` → instant, else timer (`components/mail/mail-app.tsx:1511-1528`).
- What RN does: `if (markAsReadDelay > 0) { timer } else { markRead() }` (`src/screens/EmailThreadScreen.tsx:236-245`) so the "Never" option in `ReadingSettings.tsx:171` behaves like "Instant".
- Fix hint: add `if (markAsReadDelay === -1) return;` before the branch.
@@ -184,7 +184,7 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
- What RN does: `handleSwipeAction 'spam'` only checks `currentMailboxId !== junkMailboxId` (`src/screens/EmailListScreen.tsx:379-388`), so a swipe in Sent moves your own mail to Junk; in Junk the swipe silently does nothing.
- Fix hint: skip when `currentMailbox.role` is sent/drafts; in junk call an `undoSpam` (see next item) and change the band label to "Not spam".
- [x] **Spam / not-spam do not flip `$junk`/`$notjunk` (#850) nor honour "trash-and-read"** — `P2` — `bugfix-parity` — fixed in 3194b5e
- [x] **Spam / not-spam do not flip `$junk`/`$notjunk` (#850) nor honour "trash-and-read"** — `P2` — `bugfix-parity` — fixed in 3194b5e; the viewer's Spam and Not spam stayed a plain move until 5e42c67 (#695)
- What WEB does: `markAsSpam` patches `mailboxIds` + `keywords/$junk: true` + `keywords/$notjunk: null` (+ `$seen` when `deleteAction === 'trash-and-read'`); `undoSpam` restores the original mailbox with `$junk: null`, `$notjunk: true` (`lib/jmap/client.ts:2422-2475`, store `stores/email-store.ts:2947-3100`, undo toast in `components/mail/mail-app.tsx:2228-2270`).
- What RN does: swipe and thread-screen spam are plain `moveToMailbox` calls (`src/screens/EmailListScreen.tsx:381`, `src/screens/EmailThreadScreen.tsx:344-356`); the keywords stay untouched, so other clients/Stalwart's classifier never learn; undo label reads "Email moved to Junk".
- Fix hint: add `markAsSpam(ids, junkRawId, accountId, alsoMarkRead)` / `undoSpam(ids, targetRawId, accountId)` to `src/api/email.ts` writing the keyword pointers; store actions with `pendingUndo.kind = 'spam'` (type already exists, `src/stores/email-store.ts:155`) whose `undoLast` also restores the keywords; queue-safe via an outbox `keywords` op.
@@ -214,12 +214,12 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
- What RN does: 300 ms debounce into `setSearchQuery` → full `Email/query` + `Email/get` per pause (`src/screens/EmailListScreen.tsx:512-516`); with the `*` wildcard a single letter matches the whole mailbox.
- Fix hint: search on `onSubmitEditing` (keep the clear button live), or debounce ≥ 600 ms with a minimum of 2 characters.
- [x] **Date format: no regional date-locale setting; relative strings hard-coded** — `P3` — `partial` — fixed in 0205aa0
- [ ] **Date format: no regional date-locale setting; relative strings hard-coded** — `P3` — `partial` — partly: 0205aa0 localized the relative strings; there is still no `dateLocale` setting (see area 08, "Language list not localized")
- What WEB does: user-selectable regional format (`dateLocale`, changelog 1.7.7) and a preset picker (#331); strings localized.
- What RN does: `formatListDate` follows the app locale only (`src/lib/date-format.ts:17-31`), "Just now"/"m ago" are English (`:38-41`).
- Fix hint: localize the relative strings via `t()`; add a `dateLocale` select if parity is wanted.
- [x] **Show avatars in Junk (off by default, 1.5.1); avatar priority** — `P3` — `partial` — fixed in bea3fc4
- [x] **Show avatars in Junk (off by default, 1.5.1); avatar priority** — `P3` — `partial` — fixed in bea3fc4; contact photos win over favicons and initials since eca3871 (#54)
- What WEB does: `showAvatarsInJunk` hides favicon/photo images in Junk (`components/email/thread-list-item.tsx:137`); avatar priority contact photo > plugin (Gravatar) > favicon > initials (`components/ui/avatar.tsx:236`).
- What RN does: favicons always load, including in Junk (`src/components/SenderAvatar.tsx:37`); no contact-photo lookup.
- Fix hint: pass `disableImages={currentMailbox.role === 'junk' && !showAvatarsInJunk}` into `SenderAvatar`; contact photos optional.
@@ -241,7 +241,7 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
### Search
- [x] **Search folder scope: always the current folder; no all-folders default (#788) and no folder picker** — `P2` — `bugfix-parity` — fixed in 3194b5e
- [x] **Search folder scope: always the current folder; no all-folders default (#788) and no folder picker** — `P2` — `bugfix-parity` — fixed in 3194b5e; "All folders" only searched the open folder's account until ee50418 (#1082)
- What WEB does: `searchMailboxId` defaults to `""` = all folders, is a separate, persisted choice in the filter panel (`stores/email-store.ts:95-101`, `2344-2378`, changelog 1.9.0 #788); cross-mailbox queries.
- What RN does: `buildJmapFilter` always starts with `{ inMailbox: current }` (`src/stores/email-store.ts:255`); the filter modal has no folder field.
- Fix hint: add `folder?: string | 'all'` to `EmailFilters` (default `'all'` while a text query or filter is active, i.e. omit `inMailbox`); expose a folder select in the modal; the base-view snapshot logic already keys off `isBaseView`.
@@ -291,11 +291,11 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
- Missing Archive/Trash/Junk → error alert (`EmailListScreen.tsx:363-387`), never silently destroys when Trash is missing (#195 equivalent).
- Delete semantics: trash / trash-and-read (#323) / permanent / junk auto-permanent, single and batch (`email-store.ts:1268-1335`, `1434-1527`); undo snackbar for archive/delete/move (single + batch) — RN is ahead of WEB here.
- Archive single / by year / by month with folder creation in one request, batch archive, "Reorganize existing archive" (`src/api/email.ts:579-692`, `ReadingSettings.tsx:105-159`).
- Swipe actions: configurable per direction, instant and reveal modes, pure gesture module with tests, stale-props fix (`SwipeableRow.tsx`, `swipe-gesture.ts`); more actions than WEB (pin, move). RTL: RN ships no RTL locales, nothing to do yet.
- Swipe actions: configurable per direction, instant and reveal modes, pure gesture module with tests, stale-props fix (`SwipeableRow.tsx`, `swipe-gesture.ts`); more actions than WEB (pin, move). RTL: ar, he and fa ship since 990cd84, but swipe directions are not mirrored yet (area 08, RTL finding).
- Multi-select: long-press, select-all-visible/indeterminate, batch star/read/tag/move/archive/delete (`EmailListScreen.tsx:320-500`).
- Pagination via `onEndReached`, pull-to-refresh, incremental `Email/queryChanges` + `Email/changes` refresh with per-account state tokens, base-view restore after search (#10), sort change invalidates snapshots (#5), offline seed and fallback.
- Search: full-text with wildcard suffix (`src/lib/search-utils.ts` = WEB `toWildcardQuery`), from/to/subject/date/attachment/unread/starred tri-state filters, chips row, search inside a shared folder routed to its owner.
- Keyword writes send the whole `keywords` map (no JSON-pointer escaping issue, no `false` values) — not affected by 5c484af1 / be97c8bf; tag ids normalized exactly like WEB `normalizeKeywordLevel` (`KeywordSettings.tsx:118-123`); batch tag toggle via `TagSheet` with "all have it" semantics.
- Keyword writes send RFC 6901-escaped `keywords/<name>` patch pointers since 5041897 (`src/api/patch-pointer.ts`; the whole-map writes erased other keywords, audit B4), with `null` instead of `false` — not affected by 5c484af1 / be97c8bf; tag ids normalized exactly like WEB `normalizeKeywordLevel` (`KeywordSettings.tsx:118-123`); batch tag toggle via `TagSheet` with "all have it" semantics.
- Unread dot (#27), date formats smart/relative/full with 12/24h, preview toggle, density-aware rows, sender favicon avatars with failure cache, `.eml`/`.zip` import into the open folder, Scheduled quick view, "Include group inboxes" setting and shared badge/account dot in the unified list.
- Mark-as-read delay (0/3s/5s) in the thread screen; both clients still display `receivedAt` in the list (WEB `lib/email-date.ts` is not wired into the list yet).
@@ -305,6 +305,6 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou
- Admin policy gates for unified/cross views (`usePolicyStore` feature flags) — RN has no admin policy channel; only the user-side toggles apply.
- Return-to-list-after-action setting — the RN thread screen always returns after delete/archive/spam.
- "Refresh" toolbar button and F5/Ctrl+R interception — pull-to-refresh covers it.
- Favicon unread badge, sidebar collapse/resize, Pro multi-pane shell, folder subscription (neither client exposes `isSubscribed`).
- Favicon unread badge (its native counterpart, the app-icon badge, landed in 25c0883, iOS only), sidebar collapse/resize, Pro multi-pane shell, a folder subscription UI (neither client shows `isSubscribed`; both subscribe the folders they create, native since 30aea63, #951).
- Plugin search hooks / external search results, `onEmailsFetched` transforms.
- Source-folder chip on rows is N/A *until* All-mail/cross views exist in RN.
+7 -7
View File
@@ -12,7 +12,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin
- What RN does: the list collapses same-thread rows to the newest message with a count badge (`src/screens/EmailListScreen.tsx:224-238`, default `disableThreading: false` at `src/stores/settings-store.ts:266`), the row opens `EmailThreadScreen` with a single `emailId` (`App.tsx:145-151`), and the screen only ever shows that one message (`src/screens/EmailThreadScreen.tsx:113-120`, `685-840`). `getThread` exists in `src/api/email.ts:448` but is only used by a test. Older messages of a thread cannot be opened at all unless the user turns on "Disable Thread Grouping".
- Fix hint: on open, call `getThread(threadId, ownerAccountId)` + `getFullEmails(ids)` and render a vertical list of collapsible message cards inside the pane (newest + unread expanded), reusing `EmailBodyView` per card; keep the horizontal pager for thread-to-thread navigation.
- [x] **Pager shows the wrong message (or nothing) when the opened email is not in the store list — fixed in c8be383** — `P1` — `rn-only-bug`
- [x] **Pager shows the wrong message (or nothing) when the opened email is not in the store list — fixed in c8be383** — `P1` — `rn-only-bug` — the body was right after c8be383, but delete, archive, move and spam hit the open folder's account, and new mail moved the page, until 5c7301b and 9c31de2 (audit B3, B5)
- What WEB does: the viewer renders `selectedEmail` directly; prev/next are derived from the current list (`components/email/email-viewer.tsx:5220-5279`).
- What RN does: the pager is a `FlatList` over `useEmailStore(s => s.emails)` and the initial index is `Math.max(0, emails.findIndex(id))` (`src/screens/EmailThreadScreen.tsx:128-133`, `535-572`). `UnifiedInboxScreen` keeps its own local list and never writes the store (`src/screens/UnifiedInboxScreen.tsx:35-50`, `61-84`), `ContactActivity` navigates with ids from a contact search (`src/components/contacts/ContactActivity.tsx:170-177`). In both cases the opened id is absent from `emails`, so the visible pane is `emails[0]` (a different mailbox's first message, or blank when the list is empty) while the toolbar/mark-read/delete act on `activeEmailId`.
- Fix hint: when `route.params.emailId` is not in `emails`, render a single-item pager (`data=[{id: emailId}]`) and disable prev/next; or have the unified inbox / contact activity pass their own id list through route params.
@@ -104,7 +104,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin
- What RN does: none.
- Fix hint: port `parseUnsubscribeUrls`/`isValidUnsubscribeUrl` from `lib/validation.ts`; for mailto call `sendEmail` with the parsed fields; open http via `expo-web-browser`.
- [x] **Read receipts (MDN, RFC 8098): request banner, send, "always/ask/never", `$mdnsent` — fixed in d2ed27f** — `P2` — `missing`
- [x] **Read receipts (MDN, RFC 8098): request banner, send, "always/ask/never", `$mdnsent` — fixed in d2ed27f** — `P2` — `missing` — the receipt headers were not sanitized (GHSA-w38p) and "Always send" answered the unseen neighbour pages until cb6ddd6 and 5f3f584 (audit S1, B7)
- What WEB does: detects `Disposition-Notification-To`, offers Send/Ignore (or auto-sends in "always" mode) only in received folders, builds the multipart/report with `lib/mdn.ts`, uploads → `Email/import` into Sent → `EmailSubmission/set` with explicit envelope, then sets `$mdnsent` (`components/email/email-viewer.tsx:2686-2784`, `lib/jmap/client.ts:4080-4130`, `components/email/read-receipt-banner.tsx`).
- What RN does: none; no `readReceiptResponse` setting.
- Fix hint: port `lib/mdn.ts` verbatim (pure string builder), reuse `uploadBytes` (`src/api/blob.ts:67`) + `importEmailBlob` (`src/api/email.ts:423`) + an `EmailSubmission/set` helper; add the setting.
@@ -124,7 +124,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin
- [x] **Tags, `$important` badge and answered/forwarded state in the header — fixed in d2ed27f** — `P3` — `missing`
- What WEB does: tag badges (removable) under the subject, "Important" pill (`components/email/email-viewer.tsx:3789-3806`); `$answered`/`$forwarded` set after send (`components/mail/mail-app.tsx:1663-1667`).
- What RN does: Tag sheet exists but no badges are rendered in the pane; `$answered`/`$forwarded` are never set after a reply/forward (no hit for `$answered` in `src/`).
- Fix hint: render `keywordDefs` matches from `email.keywords` under the subject; after a successful send in `ComposeScreen` call `setEmailKeywords(originalId, {...keywords, $answered|$forwarded: true}, accountId)`.
- Fix hint: render `keywordDefs` matches from `email.keywords` under the subject; after a successful send in `ComposeScreen` call `setEmailKeywords(originalId, {...keywords, $answered|$forwarded: true}, accountId)`. (`setEmailKeywords` is gone since 5041897: set `keywords/$answered` or `keywords/$forwarded` through `patchKeywordsForEmails` or the store's `setKeywordForEmails`.)
### Calendar invitation banner
@@ -138,7 +138,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin
- What RN does: `extractMethodFromRawIcs` exists (`src/lib/calendar-invitation.ts:103-106`) but is never called; only `inferInvitationMethod` heuristics are used (`CalendarInvitationBanner.tsx:62`), so a cancellation whose event status is not `cancelled`, or a REPLY from a single attendee, is shown as a plain invitation with RSVP buttons.
- Fix hint: fetch the blob text (`getDownloadUrl` + `secureFetch`, as `fetchRawEmail` does) in parallel with parse and prefer `extractMethodFromRawIcs`.
- [ ] **Banner lacks WEB's trust assessment, actor summary, existing-event / "already in calendar" state, calendar picker, "View in calendar", collapse, sequence badge, counter-proposal review** — `P3` — `partial` — deferred: trust assessment, iTIP method and calendar picker landed in 8206893 (calendar agent); existing-event state, "View in calendar", collapse, sequence badge and counter-proposal review remain open in the calendar area
- [ ] **Banner lacks WEB's trust assessment, actor summary, existing-event / "already in calendar" state, calendar picker, "View in calendar", collapse, sequence badge, counter-proposal review** — `P3` — `partial` — deferred: trust assessment, iTIP method and calendar picker landed in 8206893 (calendar agent); the existing-event state followed in c0582ab, which looks the event up on the server by `uid` (also so Accept sends the RSVP, audit B22); "View in calendar", collapse, sequence badge and counter-proposal review remain open in the calendar area
- What WEB does: `getInvitationTrustAssessment` (sender vs organizer + auth results) warning, actor line ("X accepted"), `queryCalendarEvents({uid})` to show "already in calendar" and current RSVP, picker when >1 calendar, "View in calendar" navigation, collapsible card, `sequence` "updated" pill, apply counter proposal for organizers (`components/email/calendar-invitation-banner.tsx:392-474`, `502-553`, `657-672`, `721-752`, `802-1140`).
- What RN does: title/date/location/video/organizer rows, Yes/Maybe/No, Add to first writable calendar (`src/components/email/CalendarInvitationBanner.tsx:83-207`); no dedupe check before offering "Add", no picker, no trust warning, no navigation to the event.
- Fix hint: port `getInvitationTrustAssessment` (needs the headers finding for auth), look up `useCalendarStore.events` by `uid` to switch to "already in calendar"/current response, add a calendar picker sheet when `calendars.length > 1`, and a "View in calendar" button that navigates to `CalendarScreen` with the start date.
@@ -165,7 +165,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin
- What RN does: shows `winmail.dat` as an opaque attachment; the real files inside are unreachable.
- Fix hint: `lib/tnef.ts` is pure `Uint8Array` code; copy it and feed `jmapClient.fetchBlobArrayBuffer`.
- [x] **Attachment chip list: no MIME-based fallback names, MDN/DSN report parts and non-inline cid images mishandled — fixed in d2ed27f** — `P3` — `partial`
- [x] **Attachment chip list: no MIME-based fallback names, MDN/DSN report parts and non-inline cid images mishandled — fixed in d2ed27f** — `P3` — `partial` — body-embedded `cid:` parts typed octet-stream are hidden from the chips since 1d1d6a5
- What WEB does: unnamed parts get `Document.pdf`/`Email.eml`/`Attachment.<sub>` (`components/email/email-viewer.tsx:191-225`); `message/disposition-notification` and `message/delivery-status` parts are hidden (`1604-1606`); inline hiding requires `disposition === 'inline'` (`1601-1603`).
- What RN does: unnamed → `'attachment'` (`src/screens/EmailThreadScreen.tsx:745`); report parts are listed; any `cid` image is hidden regardless of disposition (`719-721`), so an image attached with a Content-ID but `disposition: attachment` (common from some clients) disappears from the list even when the body does not reference it.
- Fix hint: port `getAttachmentDisplayName`, add the two report-type filters, and check `disposition === 'inline'` (or that the body actually references the cid via `extractCidRefs`).
@@ -247,7 +247,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin
- External content policy ask/block/allow, per-message "Load external content", "Trust sender" persisting locally and to the address book, trusted list from the address book and contacts (`src/components/EmailBodyView.tsx:480-535`, `643-672`).
- cid inline images fetched with auth and injected as data URIs, transparent placeholder otherwise (`src/components/EmailBodyView.tsx:546-584`, `src/lib/email-html.ts:213-218`).
- Shrink-to-fit of fixed-width emails + in-page pinch zoom/pan with pager/scroll locking (`src/components/EmailBodyView.tsx:159-460`) — RN-only, better than WEB's horizontal scroll (#409).
- Mark read on open with delay setting (apart from the `-1` bug), optimistic star/unread/tag toggles, delete/archive/spam/not-spam/move scoped to the message's account, `no trash folder` alert (`src/screens/EmailThreadScreen.tsx:221-364`).
- Mark read on open with delay setting (apart from the `-1` bug), optimistic star/unread/tag toggles, delete/archive/spam/not-spam/move scoped to the message's account, `no trash folder` alert (`src/screens/EmailThreadScreen.tsx:221-364`). The account scoping was wrong for messages opened from the Unified Inbox, a notification or a deep link until 5c7301b (audit B3); star, tag and unread are queued offline since ddff2f7.
- Prev/next navigation with prefetched neighbour panes and swipe (`src/screens/EmailThreadScreen.tsx:122-181`, `535-572`) — RN-only pager.
- Skeleton while loading (`src/screens/EmailThreadScreen.tsx:845-886`).
- Attachments: chips with size, show-all toggle, `hideInlineImageAttachments` setting, `attachmentPosition` setting, `mailAttachmentAction` preview/download, templated filenames + transforms identical to WEB (`src/lib/download-filename.ts` diff vs `lib/download-filename.ts` is whitespace/quotes only, minus `DEFAULT_BUNDLE_TEMPLATE`/`BUNDLE_TOKENS`), Downloads settings screen with live preview (`src/components/settings/DownloadsSettings.tsx`).
@@ -259,7 +259,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin
## N/A on mobile
- Print (WEB `handlePrint`, `components/email/email-viewer.tsx:2621-2670`) — could be done with `expo-print` later, but not a parity requirement.
- Copy permalink / deep link to a message (#733) — RN has no linking configuration; no `linking` prop in `App.tsx`.
- Copy permalink to a message (#733) — RN opens incoming message links since 5802cae (`src/navigation/linking.ts`) but has no "copy link" action.
- Fullscreen reading toggle, Pro split panes, toolbar overflow measurement, plugin slots/hooks (`email-banner`, `email-detail-sidebar`, `onRenderEmailBody`, `onBeforeExternalLink`, `ui.rerenderEmail`), keyboard shortcuts, drag attachments out to the file system (#267), drag emails out as `.eml`, import `.eml` from the viewer menu (RN imports from the list screen instead).
- Iframe-specific fixes: "Render the email body on DOM parse instead of iframe load" (#635), iframe flash/flicker fixes (1.5.4, 1.6.7), `blob:` in CSP `object-src`/`frame-src` for PDF previews (#253), pdf.js bytes vs `blob:` fetch (#871), RSC 307 loop (#919), iOS Safari viewport zoom on input focus (#838).
- Demo-mode welcome pane, tour, contact sidebar panel layout (desktop only; the actions themselves are listed above).
+6 -6
View File
@@ -9,7 +9,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the
### Drafts
- [x] **No draft autosave / save-on-close — closing a compose loses everything** — fixed in 5b72c4d — `P1` — `missing`
- [x] **No draft autosave / save-on-close — closing a compose loses everything** — fixed in 5b72c4d — `P1` — `missing` — offline or without a Drafts folder the close dialog still dropped the text, and the iOS swipe skipped the dialog, until 92b42e4 (audit B16)
- What WEB does: debounced 2 s autosave to the server Drafts folder with `$draft`+`$seen` keywords (`components/email/email-composer.tsx:1648-1815` saveDraftOnce, `:1842-1883` timer; `lib/jmap/client.ts:2998-3060` createDraft), serialized against send (#303, `:1821-1837`), `beforeunload` best-effort save (`:1082-1091`), "Save / Discard / Cancel" close dialog (`:2347-2426`, `handleSaveDraftAndClose`, `handleDiscardAndClose`), toast when the save is refused so text is not dropped (`:2384-2401`, #702).
- What RN does: the only close path is `onClose` in `src/screens/ComposeScreen.tsx:518-541`, an Alert with Cancel / **Discard** — no "Save draft" option and no server draft is ever created. The hardware back button / iOS swipe-back is not intercepted at all (no `beforeRemove` listener anywhere in `ComposeScreen.tsx` or `src/navigation/*`), so the dirty guard is bypassed entirely by the OS back gesture.
- Fix hint: add a `createDraft(...)`-style call in `src/api/email.ts` (Email/set create into the `drafts`-role mailbox with `$draft`/`$seen`, destroy previous draft id only after the new create succeeds), call it from a debounced effect + from the close Alert ("Save draft" button), and register `navigation.addListener('beforeRemove', …)` to route back gestures through the same guard.
@@ -54,7 +54,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the
- [x] **Reply / forward does not mark the original `$answered` / `$forwarded`** — fixed in 7f956d6 — `P2` — `missing`
- What WEB does: after a successful reply/forward the original gets `$answered` or `$forwarded` (`components/mail/mail-app.tsx:1663-1673`), routed to the owning account.
- What RN does: nothing — `grep -F '$answered'` over `src/` returns no hits; `performSend` (`ComposeScreen.tsx:941-974`) has no access to the original email id (`replyTo` params in `src/navigation/types.ts:10-19` carry no `emailId`).
- Fix hint: add `originalEmailId`/`jmapAccountId` to the `replyTo` route param and call the existing keyword primitive (`setEmailKeywords` via `applyOrQueue` in `src/stores/outbox-store.ts`) after the send resolves.
- Fix hint: add `originalEmailId`/`jmapAccountId` to the `replyTo` route param and call the existing keyword primitive (`setEmailKeywords` via `applyOrQueue` in `src/stores/outbox-store.ts`) after the send resolves. (`setEmailKeywords` is gone since 5041897; the keyword primitive is now `patchKeywordsForEmails`, and the outbox queues `keywords/<name>` patches.)
- [x] **Identity `replyTo` / `bcc` are never applied to outgoing mail** — fixed in 7f956d6 — `P2` — `missing`
- What WEB does: `sendEmail` copies the identity's `replyTo` onto the message (`lib/jmap/client.ts:3167-3172`, `:3184`); identity form lets users edit Reply-To and Bcc (`components/identity/identity-form.tsx:39-41`, `:66-70`, `:132-133`).
@@ -221,7 +221,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the
### Scheduled send
- [x] **Scheduled screen: cancel only — no reschedule, no "send now", no edit** — fixed in c659b54 — `P2` — `partial`
- [x] **Scheduled screen: cancel only — no reschedule, no "send now", no edit** — fixed in c659b54 — `P2` — `partial` — reschedule dropped Cc and Bcc until e67947a, and a refused half of it could drop or double-send the message until 7e598c5 (audit B19)
- What WEB does: scheduled view with send-now button (changelog 1.6.x), reschedule (`stores/email-store.ts:4353` rescheduleScheduledEmail, `lib/jmap/client.ts:7985`), cancel-for-edit that reopens the composer (`email-store.ts:4326`), listing routed to the owning account (#874).
- What RN does: `src/screens/ScheduledScreen.tsx:51-78` offers only "Cancel send"; `src/api/email.ts:902-988` has `listScheduledEmails`/`cancelScheduledSend` only.
- Fix hint: add `rescheduleScheduledSend(submissionId, emailId, identityId, holdFor)` = `EmailSubmission/set { update: {id: {undoStatus:'canceled'}}, create: {replacement: {emailId, identityId, envelope(HOLDFOR)}} }` and an edit path once drafts exist (cancel → open composer prefilled from the Email).
@@ -253,7 +253,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the
### mailto / share intents
- [ ] **No `mailto:` handling / share-to-app intent** — `P2` — `missing` — composer side (prefill*, prefillAttachments upload) done in 5b72c4d; scheme/intent filters + Linking → Compose belong to area 08
- [x] **No `mailto:` handling / share-to-app intent** — `P2` — `missing` — composer side (prefill*, prefillAttachments upload) done in 5b72c4d; scheme, intent filters and links to Compose done in 5802cae (area 08); shared files, `SENDTO` and `+` addresses work since cd4ce9d, 06e8942, ee652d3 (audit B9–B11)
- What WEB does: PWA `protocol_handlers` for mailto (`app/manifest.ts:118`), `parseMailtoUrl` (`lib/validation.ts:133-167`), in-app composer open through the unsaved-draft guard (`components/mail/mail-app.tsx:1110`, changelog 1.9.0).
- What RN does: `app.config.js:29` registers only the custom `bulwarkmobile` scheme; no `intentFilters` (Android `mailto`/`SEND`) or iOS `LSApplicationQueriesSchemes`/`CFBundleURLTypes` for `mailto`; no `Linking.getInitialURL`/`addEventListener('url')` handling that routes to `Compose` (grep over `App.tsx`/`src/navigation` shows none). Tapping a mailto link in another app cannot open Bulwark; sharing a file/text to the app is impossible.
- Fix hint: add `scheme: ['bulwarkmobile', 'mailto']` + Android `intentFilters` for `mailto` and `android.intent.action.SEND`, parse with a port of `parseMailtoUrl`, and navigate to `Compose` with `prefillTo`/subject/body (add those params); for SEND intents seed `attachments` from the shared `content://` URI (upload path already supports it, `src/api/blob.ts:17-22`).
@@ -281,7 +281,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the
- Fix hint: route through `t()` with keys in `locales/<lang>/common.json`.
## Verified at parity (brief list, so the fixer knows what NOT to redo)
- Scheduled send via `EmailSubmission` envelope `HOLDFOR` with explicit `rcptTo` (bare addresses, names stripped) and `maxDelayedSend`/FUTURERELEASE capability checks: `src/api/email.ts:828-844`, `src/api/jmap-client.ts:513-531`, `ComposeScreen.tsx:796-821` — matches `lib/jmap/client.ts:592-612`, `:3245-3258`.
- Scheduled send via `EmailSubmission` envelope `HOLDFOR` with explicit `rcptTo` (bare addresses, names stripped) and `maxDelayedSend`/FUTURERELEASE capability checks: `src/api/email.ts:828-844`, `src/api/jmap-client.ts:513-531`, `ComposeScreen.tsx:796-821` — matches `lib/jmap/client.ts:592-612`, `:3245-3258`. The capability check read only the session-level object, which Stalwart leaves empty, so scheduling and the undo delay were off on Stalwart until e28e8b4 (#57, audit B8); holds are capped at Stalwart's 7-day limit since 4e7ae2f.
- Undo-send delay setting (0/5/10/20/30 s) applied only when the server supports delayed send: `ComposeScreen.tsx:825-828`, `ComposingSettings.tsx:42-48`.
- Send-time readback of the live editor DOM with timeout + loud abort (RN #9/#11): `RichTextEditor.tsx:143-152`, `ComposeScreen.tsx:890-902`; escape-free page script with a parse test.
- `Name <addr>` recipients split into JMAP `name`/`email` (#672): `ComposeScreen.tsx:69-84`, `:944-947`.
@@ -310,4 +310,4 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the
- Signature "double-click to unlock" atom (Tiptap-specific); the RN contenteditable is always editable.
- Quoted-HTML shadow-island typing detection (#654) — Tiptap/shadow-DOM specific.
- Send through another logged-in account's client for DKIM (#461) — RN is single-active-account; identities always belong to the submitting account.
- Route scheduled sends / identity on reschedule to a shared account (#874, a15af722) — RN does not compose on behalf of shared accounts.
- Route scheduled sends / identity on reschedule to a shared account (#874, a15af722) — RN does not compose on behalf of shared accounts, but the Scheduled screen lists, cancels, reschedules and edits sends held in shared accounts since 72f48db.
+12 -16
View File
@@ -10,7 +10,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi
- [x] **Recurrence scope dialog is a stub: "This event" / "This and following" behave as "All events"** — `P1` — `missing` — fixed in 161baf1
- What WEB does: `handleScopeSelect` implements all three scopes: "this" writes a `recurrenceOverrides/<recurrenceId>` patch (or the synthetic id on Stalwart >= 0.16.20), "this_and_future" truncates the master with `until` and creates a new series, "all" patches the master; delete does the mirror (exclude / truncate / destroy) and refetches the range (ref `components/calendar/calendar-app.tsx:842-998`, `lib/recurrence-overrides.ts:21-32`, changelog 1.9.0 "Edit a single recurring occurrence via a one-shot override patch").
- What RN does: `handleScopeSelect` ignores the chosen scope with a TODO ("treat all scopes as 'all'") and either opens the edit modal on the occurrence or deletes `action.event.id`, which the store resolves to the master via `originalId` (ref `src/screens/CalendarScreen.tsx:347-363`, `src/stores/calendar-store.ts:306-329`). Choosing "This event" on delete destroys the entire series.
- Fix hint: port WEB's `handleScopeSelect`/`truncateRecurrenceAtEvent`/`findMasterEvent` into CalendarScreen; add `buildRecurrenceOverridePatch` (WEB `lib/recurrence-overrides.ts`) and `RECURRENCE_OVERRIDE_IMMUTABLE_KEYS`; for "this" write `{ [`recurrenceOverrides/${recurrenceId}`]: override }` on `originalId`; for delete-this write `{ excluded: true }`; refresh after.
- Fix hint: port WEB's `handleScopeSelect`/`truncateRecurrenceAtEvent`/`findMasterEvent` into CalendarScreen; add `buildRecurrenceOverridePatch` (WEB `lib/recurrence-overrides.ts`; removed again in f397ee3, where "This event only" goes to `updateEvent`/`deleteEvent` with the occurrence's id) and `RECURRENCE_OVERRIDE_IMMUTABLE_KEYS`; for "this" write `{ [`recurrenceOverrides/${recurrenceId}`]: override }` on `originalId`; for delete-this write `{ excluded: true }`; refresh after.
- [x] **Editing an expanded occurrence rewrites the master's `start` to the occurrence date (series shifts / earlier occurrences vanish)** — `P1` — `bug` — fixed in 161baf1
- What WEB does: for scope "all" the updates go to the master but `start` from an occurrence is never applied to the master unless the user changed it; scope "this" goes into an override (ref `components/calendar/calendar-app.tsx:930-943`).
@@ -27,7 +27,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi
- What RN does: `buildRuleFromEditorValue` sets `rscale: 'gregorian', skip: 'omit'` on every custom rule (ref `src/lib/recurrence.ts:220-221`). RN affected: any custom recurrence created on mobile breaks Android CalDAV sync.
- Fix hint: delete the two properties in `buildRuleFromEditorValue`; the simple presets in `EventModal.tsx:143-146` are already clean.
- [ ] **Synthetic occurrence ids / server-side expansion (Stalwart >= 0.16.20) not used** — `P3` — `missing` — deferred: optional; client-side expansion is at parity and the scope dialog now writes overrides through the master, so the synthetic-id path buys nothing functionally yet
- [x] **Synthetic occurrence ids / server-side expansion (Stalwart >= 0.16.20) not used** — `P3` — `missing` — done in 8454889 (server expansion when the probe finds synthetic ids, device-side expansion otherwise), f397ee3 (one occurrence changed or deleted through its own id) and 96c5a3f (RSVP to one occurrence)
- What WEB does: probes `CalendarEvent/set` with `h333333` once, then queries with `expandRecurrences: true`, hydrates occurrences from base events and writes occurrence patches through the synthetic id with a fallback to base-event overrides (ref `lib/jmap/client.ts:5648-5680, 5747-5751, 5828-5868`, `lib/recurrence-instances.ts:35, 73, 208-232`, `stores/calendar-store.ts:82-184`).
- What RN does: always fetches raw events and expands on the client (`src/stores/calendar-store.ts:228`, `src/lib/recurrence-expansion.ts`). Functionally fine on all server versions; RN's expansion file is a line-for-line port of WEB's (diffed: only comments, formatting and the `isServerRecurrenceInstance` pass-through differ).
- Fix hint: optional. If adopted, port `recurrence-instances.ts` wholesale and the `isServerRecurrenceInstance` guard in `expandRecurringEvents`.
@@ -88,11 +88,6 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi
- What RN does: `EventDetailSheet` has an `onDuplicate` prop that `CalendarScreen` never passes (`src/components/calendar/EventDetailSheet.tsx:59, 336-342`); no export/copy/note.
- Fix hint: wire `onDuplicate` (clone like WEB `handleDuplicateFromDetail`), add "Share .ics" via `expo-sharing` + a port of `eventToICS`, add "Copy link" with `Clipboard`.
- [ ] **Quick natural-language event input** — `P3` — `missing` — deferred: low priority, needs a port of the webmail's quick-event parser and a text field above the month grid
- What WEB does: `QuickEventInput` in the toolbar (ref `components/calendar/quick-event-input.tsx`, `calendar-toolbar.tsx`).
- What RN does: none.
- Fix hint: low priority; could reuse WEB's parser in a text field above the month grid.
### Invitations / scheduling
- [x] **Invitations are never sent: participants built with retired `sendTo`, no organizer, no `organizerCalendarAddress`** — `P1` — `bugfix-parity` — fixed in 5dc4070
@@ -174,7 +169,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi
### Views / navigation / locale
- [ ] **Entire calendar UI is hard-coded English although RN has i18n** — `P2` — `missing` — partial in 0f80e98: screen, month/week/agenda views, event card/sheet/modal, drawer, scope dialog, settings, banner and calendar/share sheets use `t()` + date-fns locales; TasksSheet (09c24fa) and `formatReminder` (2abea97) localized too; deferred: ICalImportSheet, ICalSubscriptionSheet and RecurrenceEditor still hard-code English
- [ ] **Entire calendar UI is hard-coded English although RN has i18n** — `P2` — `missing` — partial in 0f80e98: screen, month/week/agenda views, event card/sheet/modal, drawer, scope dialog, settings, banner and calendar/share sheets use `t()` + date-fns locales; TasksSheet (09c24fa), `formatReminder` (2abea97) and ICalImportSheet (b7f7110) localized too; deferred: ICalSubscriptionSheet and RecurrenceEditor still hard-code English
- What WEB does: everything through `next-intl` (`t('calendar.*')`), month/day names and popover dates localized (changelog 1.6.x "Localize event start date in detail popover and event modal").
- What RN does: `src/i18n/index.ts` + `useLocaleStore` are used by 18 other files, but none of `src/components/calendar/*`, `src/screens/CalendarScreen.tsx` or `CalendarSettings.tsx` import it; all labels ("Today", "No events", "Does not repeat", "Going?", RSVP labels, drawer titles, etc.) and `date-fns` `format()` calls without a `locale` are English (`CalendarScreen.tsx:408, 448, 504, 640-641`, `EventModal.tsx:60-67, 326, ...`, `MonthView.tsx:25-26`, `AgendaView.tsx:33-37`).
- Fix hint: add a `calendar.*` namespace to `locales/*.json` (WEB's keys can be copied) and pass a date-fns locale from the locale store into `format()`.
@@ -199,7 +194,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi
- What RN does: `CalendarSettings` shows "Hover preview" with delay options and nothing consumes `calendarHoverPreview` (`src/components/settings/CalendarSettings.tsx:87-102`; no other reference in `src/`).
- Fix hint: remove the setting row (keep the store key for settings-sync compatibility or drop it).
- [x] **No day view (falls back to agenda); tapping a month day does not open the day** — `P3` — `partial` — fixed in 0f80e98 ("Day" hidden from the mobile default-view options; a synced "day" shows as Agenda)
- [x] **No day view (falls back to agenda); tapping a month day does not open the day** — `P3` — `partial` — fixed in 0f80e98 ("Day" hidden from the mobile default-view options; a synced "day" shows as Agenda); a real day view is back since de3d697, with the week and month views scrolling freely (de3d697, caf61ec, #759) and an endless agenda (71522b9)
- What WEB does: day view with time grid; on mobile, tapping a date in month view switches to day view and offers "back to month" (ref `components/calendar/calendar-app.tsx:461-470`, `calendar-day-view.tsx`).
- What RN does: `calendarDefaultView === 'day'` maps to agenda (`src/screens/CalendarScreen.tsx:131-137`); month view shows a list below the grid instead. Acceptable UX, but the settings option "Day" is misleading.
- Fix hint: either reuse `WeekView` with a single-day `weekDays` array as a day grid, or hide "Day" from the RN default-view options.
@@ -219,9 +214,9 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi
- What RN does: `formatRange` uses `eventTimeRange` -> `getEventEndDate` (exclusive), so a single-day all-day event on Mar 1 prints "Mar 1 – Mar 2" and a two-day one "Mar 1 – Mar 3" (`src/components/calendar/EventDetailSheet.tsx:63-74`, `src/lib/calendar-utils.ts:84-90`).
- Fix hint: for `allDay` use `getEventDisplayEndDate(event)` before comparing/formatting.
- [ ] **Deep links to calendar/event** — `P3` — `missing` — deferred: no `linking` config exists yet (mail area plumbing); `Calendar` route still takes no params
- [ ] **Deep links to calendar/event** — `P3` — `missing` — partly: incoming links are parsed since 5802cae (`src/navigation/linking.ts`), and an event link opens the event since 9282ee6; a date link only opens the Calendar tab
- What WEB does: `parseCalendarPath`/`buildCalendarPath` handle `/calendar/<view>/<date>?event=` (ref `lib/deep-links.ts:272-300`, changelog 1.8.3 "Deep links for mail, calendar, contacts, files").
- What RN does: `Calendar: undefined` route params (`src/navigation/types.ts:33`); no linking config for calendar. Only matters once RN adds a `linking` config; the mail area may already cover the plumbing.
- What RN does: `Calendar: undefined` route params (`src/navigation/types.ts:33`); `handleDeepLink` hands an event to the Calendar tab through `pending-calendar-open`, the same path a tapped reminder takes, but ignores the date.
- Fix hint: accept `{ date?, eventId? }` params on the Calendar route and open the detail sheet.
### Data loading / sync
@@ -251,28 +246,28 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi
- What RN does: `createEvent` merges the `/set` echo over the payload (`src/api/calendar.ts:272-274`) and appends without expansion (`src/stores/calendar-store.ts:286-304`).
- Fix hint: after create, `CalendarEvent/get` the id (RN `getEvents([id])`) and, if `recurrenceRules` is set, run `expandRecurringEvents` for `loadedRange` or call `refresh()`.
- [ ] **Push: FCM subscription excludes `CalendarEvent`/`Calendar` types** — `P3` — `partial` — deferred: local reminders (8e1d03c) are scheduled from foreground fetches only; adding the calendar types to the FCM subscription lives in `src/lib/push-notifications.ts` (push area) and would need a background refresh + reschedule hook
- [ ] **Push: FCM subscription excludes `CalendarEvent`/`Calendar` types** — `P3` — `partial` — deferred: local reminders (8e1d03c) are resynced at launch, on `Calendar`/`CalendarEvent` state changes and on return to the foreground (4b903ed), but nothing refreshes them in the background; adding the calendar types to the push subscription lives in `src/lib/push-notifications.ts` (push area) and would need a background refresh + reschedule hook
- What WEB does: push subscription covers calendar types so the sidebar refreshes on external changes (FEATURES.md "JMAP push keeps everything in sync").
- What RN does: foreground EventSource/polling dispatches to `useCalendarStore.handleStateChange` (`App.tsx:390-397`, `src/stores/calendar-store.ts:261-284`) — at parity while the app is open; the background FCM subscription lists only `['Email','EmailDelivery','Mailbox']` (`src/lib/push-notifications.ts:104`), so calendar changes never wake the app. Acceptable unless local reminders are added.
- What RN does: foreground EventSource/polling dispatches to `useCalendarStore.handleStateChange` (`App.tsx:390-397`, `src/stores/calendar-store.ts:261-284`) — at parity while the app is open; the background push subscription carries only `['EmailDelivery']` since 00faceb (`src/lib/push-notifications.ts`), so calendar changes never wake the app.
- Fix hint: leave as is unless background calendar refresh becomes needed.
### Alerts / notifications
- [x] **Reminders never fire on mobile (no alert scheduler, no local notifications)** — `P2` — `missing` — fixed in 8e1d03c (expo-notifications local notifications scheduled from the store for the next 7 days, calendar default alerts honoured, cancelled events / completed tasks / acknowledged alerts skipped)
- [x] **Reminders never fire on mobile (no alert scheduler, no local notifications)** — `P2` — `missing` — fixed in 8e1d03c (expo-notifications local notifications scheduled from the store for the next 7 days, calendar default alerts honoured, cancelled events / completed tasks / acknowledged alerts skipped); moving the calendar view cancelled them until 4b903ed (audit B17), and a tapped reminder opens its event since 83db70a
- What WEB does: `useCalendarAlerts` polls every 60 s, proactively fetches the next 24 h, computes fire times from `alerts` (offset relative to start/end, absolute triggers, calendar default alerts via `useDefaultAlerts`), dedupes through a persisted acknowledged store, plays a sound and toasts; task due alerts too; cancelled events muted (#572) (ref `lib/calendar-alerts.ts:39-201`, `hooks/use-calendar-alerts.ts:21-158`, `stores/calendar-notification-store.ts`).
- What RN does: `src/lib/calendar-alerts.ts` only converts offsets <-> reminder presets (1-103); the `calendarNotificationsEnabled`/`calendarNotificationSound` settings are shown in `NotificationSettings.tsx:52-53, 235-240` but nothing reads them; `package.json` has no `expo-notifications`. Users who set "15 minutes before" get nothing.
- Fix hint: add `expo-notifications`, port `computeFireTime`/`getPendingAlerts`, and schedule local notifications for the next N days on every `fetchEvents` (cancel + reschedule by event id); honour `useDefaultAlerts` via `Calendar.defaultAlertsWithTime/WithoutTime` (add those to `Calendar/get` properties).
### Tasks
- [x] **Task sheet supports only title + due date; no edit, description, priority, due time, progress states, alerts, filters, "show on calendar"** — `P2` — `partial` — fixed in 09c24fa (edit, description, priority, due date + time, calendar move, all/pending/completed/overdue filters, overdue-first sort, `needs-action` <-> `completed` with `progressUpdated`, due tasks overlaid on the grid; task alerts still come only from the server data)
- [x] **Task sheet supports only title + due date; no edit, description, priority, due time, progress states, alerts, filters, "show on calendar"** — `P2` — `partial` — fixed in 09c24fa (edit, description, priority, due date + time, calendar move, all/pending/completed/overdue filters, overdue-first sort, `needs-action` <-> `completed` with `progressUpdated`, due tasks overlaid on the grid; task alerts still come only from the server data); completing failed on Stalwart, which rejects `progressUpdated`, until dcdda5e (audit B12, #958)
- What WEB does: task modal with description, due date + optional time, priority (none/high/medium/low -> 1-9), progress, calendar, alert; list filters all/pending/completed/overdue, overdue-first sort, `showTasksOnCalendar` overlays due tasks on the grid; completion toggles `needs-action` <-> `completed` with `progressUpdated` (ref `components/calendar/task-modal.tsx:55-140`, `task-list-view.tsx:80-110`, `stores/task-store.ts:366, 442-452`).
- What RN does: `TasksSheet` creates `{ title, progress, due }` only, no tap-to-edit, delete + toggle only (`src/components/calendar/TasksSheet.tsx:117-135, 210-235`); un-completing sets `in-process` (`src/stores/calendar-store.ts:418-426`); `showTasksOnCalendar` (`settings-store.ts:174`) is unused; tasks are never drawn in month/week/agenda.
- Fix hint: add a task edit sheet mirroring `task-modal.tsx`, filter chips, and render due tasks as all-day chips when `showTasksOnCalendar`.
### Settings
- [ ] **No custom time-zone setting (#755); no birthday-calendar colour; no "Day" view on mobile (see above)** — `P3` — `missing` — time-zone setting (`calendarTimeZone`, used for saved events and JMAP queries) done in 58a6f45 + 0f80e98; deferred: birthday-calendar colour setting
- [ ] **No custom time-zone setting (#755); no birthday-calendar colour; no "Day" view on mobile (see above)** — `P3` — `missing` — time-zone setting (`calendarTimeZone`, used for saved events and JMAP queries) done in 58a6f45 + 0f80e98; the editors and views work in calendar-zone wall time since a8e6f6e and 8af163a, and floating task dues since 1a651a9; deferred: birthday-calendar colour setting
- What WEB does: `timeZone: 'auto' | IANA` overriding browser detection, used for display (`displayNow`, `toDisplayDate`) and for the `timeZone` on saved events (ref `lib/timezone.ts:29-75`, `stores/settings-store.ts:317, 553`, changelog 1.9.2 #755); `birthdayCalendarColor` (`settings-store.ts:377, 609`).
- What RN does: device zone only (`src/api/calendar.ts:15-21`); birthday colour fixed (`src/lib/birthday-calendar.ts:7`).
- Fix hint: add `timeZone` to the settings store (synced with WEB's key so it round-trips through settings sync), pass it as the JMAP `timeZone` arg and into saved events; convert display via `Intl` like WEB's `getWallClock`.
@@ -304,3 +299,4 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi
- `webcal:` / `mailto:` protocol-handler registration and import-or-subscribe prompt for detected webcal links.
- Agenda plugin sidecar, plugin event-action slot (Jitsi), Pro tab deep-link URL writing.
- Refresh-gesture interception (F5/Ctrl-R) — RN has pull-to-refresh.
- Quick natural-language event input: the webmail has no parser for it either (dropped from the findings by the 2026-09 audit).
+9 -11
View File
@@ -190,29 +190,29 @@ RN covers the visible surface reasonably well (list with alphabetical index, det
### Composer autocomplete
- [ ] **Autocomplete lacks groups, recent recipients (Sent), on-demand server search and directory principals** — `P2` — `missing`
- store-side done in 26a34d5 (`getAutocomplete` with group entries + recent recipients, `loadRecentRecipients`/`searchRecipients` fed by `src/api/recent-recipients.ts`, `getGroupRecipients`, #672 normalisation); wiring `ComposeScreen`/`ParticipantInput` belongs to the composer agent; directory principals not ported.
- store-side done in 26a34d5 (`getAutocomplete` with group entries + recent recipients, `loadRecentRecipients`/`searchRecipients` fed by `src/api/recent-recipients.ts`, `getGroupRecipients`, #672 normalisation); the composer uses `getAutocomplete` and the recent Sent recipients since 5b72c4d. Still open: the on-demand server search (`searchRecipients` has no caller), directory principals, and the calendar's `ParticipantInput`, which still offers contacts only.
- What WEB does: `getAutocomplete` merges contacts, group entries (as one chip with `memberCount`), RFC 9670 directory principals and recent Sent recipients, deduped and sanitised (#672) (`stores/contact-store.ts:622-707, 23-49`; `loadRecentRecipients` `:1118-1142` fed from `mail-app.tsx:423`; `searchRecipients` `:1144-1154` used by `components/email/email-composer.tsx:1175-1212`; group chip insertion `:1218-1233`; changelogs 1.6.x "Recipient autocomplete from Sent, with on-demand server search", 1.7.x group chips).
- What RN does: suggestions come only from `individuals` matched with `matchesContactSearch` (`src/screens/ComposeScreen.tsx:367-392`); groups are explicitly filtered out and nothing else is consulted. `ParticipantInput` (calendar) likewise flattens contacts only (`src/components/calendar/ParticipantInput.tsx:34-51`).
- Fix hint: add `recentRecipients` (scan Sent `to/cc`, 300 msgs) to the contacts store, include groups as a pickable entry that expands to member emails on pick, and reuse `sanitizeDisplayName/splitMailbox`-style cleanup for names that contain a mailbox (#672).
### Viewer integration
- [ ] **"Add sender to contacts" from the message view missing** — `P2` — `missing`
- [x] **"Add sender to contacts" from the message view missing** — `P2` — `missing` — done in d2ed27f (the viewer's address sheet, `src/components/email/AddressActionSheet.tsx`, offers "Add to contacts" and "Open contact")
- contact-side done: `ContactForm { prefill }` seeds email + given/surname (9800ad0), `findContactByEmail` in the store (26a34d5); the sender sheet in `EmailThreadScreen` belongs to the viewer agent.
- What WEB does: contact sidebar/popover on sender click with "Add to contacts" that creates a card with given/surname split from the display name (`components/email/email-viewer.tsx:5350-5366, 558-564`); also the mobile recipient popover (#306) and `/contacts/new?addEmail=` deep link (`components/contacts/contacts-app.tsx:182-198`).
- What RN does: no contact lookup or "add" action in `EmailThreadScreen` (grep contact/ContactForm: only `SenderAvatar` at `:794`). Only `ContactForm`'s `route.params` support `contactId/addressBookId/asGroup` (`src/navigation/types.ts:24`) — no email/name prefill.
- Fix hint: add `prefill?: { email; name }` to the `ContactForm` route, and a sender-tap sheet in `EmailThreadScreen` with "Add to contacts" / "Open contact" (lookup by email in `useContactsStore`).
- [ ] **Sender-name tap → contact popup missing** — `P3` — `missing`
- [x] **Sender-name tap → contact popup missing** — `P3` — `missing` — done in d2ed27f (tapping a sender or recipient opens `AddressActionSheet`, which shows the matching contact and "Open contact")
- deferred: viewer agent (same sheet as above; `findContactByEmail` + `ContactDetail { contactId }` are ready).
- What WEB does: clicking the sender name opens the contact popup with details/actions (changelog 1.4.x "Show contact popup when clicking the sender name", `email-viewer.tsx:392-455`).
- What RN does: none (see previous item).
- Fix hint: same sheet as above; show the matching card's phone/org and an "Open" that navigates to `ContactDetail`.
- [ ] **Contact deep links (`/contacts/<id>[/edit]`, `/contacts/new`)** — `P3` — `missing`
- deferred: navigation/deep-link agent; contact routes already exist.
- partly: `/contacts/<id>` opens the contact since 5802cae; `/contacts/new` and `/contacts/<id>/edit` only open the Contacts tab.
- What WEB does: `parseContactsPath`/`buildContactsPath` (`lib/deep-links.ts:323-329`), applied once on mount (`components/contacts/contacts-app.tsx:182-224`).
- What RN does: no `linking` config anywhere (grep `prefixes|LinkingOptions|getInitialURL` in `App.tsx`/`src`: none). Cross-reference the navigation/deep-link agent; contact-side requirement is just `ContactDetail { contactId }` / `ContactForm { contactId }` which already exist (`src/navigation/types.ts:23-24`).
- What RN does: `src/navigation/linking.ts` (5802cae) maps `/contacts/<id>` to `ContactDetail { contactId }` and every other contacts path to the tab; it never opens `ContactForm`.
### vCard import / export (`lib/vcard.ts` vs `src/lib/vcard.ts`)
@@ -257,7 +257,7 @@ RN covers the visible surface reasonably well (list with alphabetical index, det
- Fix hint: in the modal, list `trustedSenders ∪ trustedSenderEmails`, mark book entries, and call `removeFromTrustedSendersBook` (+ local remove) on delete; show the union count.
- [x] **`trustedSendersAddressBook` setting is dead (`false`, no setter, no toggle) — book is consulted unconditionally** — `P3` — `partial` — fixed in 78114b6
- Setting is now `boolean | null`, auto-`true` once the session advertises contacts, with a "Sync trusted senders to address book" toggle. Gating the passive load / write in `EmailBodyView` belongs to the viewer agent.
- Setting is `boolean | null` with a "Sync trusted senders to address book" toggle. Until d89e331 it switched itself on for a contacts-capable account, and the viewer then trusted every contact in every book; it now stays off until the user turns it on, and only the Trusted Senders book counts (the webmail still auto-enables it).
- What WEB does: `null` until the account proves contacts support, then auto-`true` (`stores/auth-store.ts:380-385`, `stores/settings-store.ts:361,593`); toggle in settings (`content-senders-settings.tsx:77-82`); the book is only loaded/used when enabled (`mail-app.tsx:199-203`, `email-viewer.tsx:1705`).
- What RN does: default `false`, no setter (`src/stores/settings-store.ts:106,239`); `EmailBodyView` loads the book regardless and ORs `trustedSenderEmails` into the trust check (`:480-485, 513-516`); the `addressBookEmails` branch (treats *every* contact as trusted) is gated on the dead flag (`:486-498`) so it never runs.
- Fix hint: either drop the flag and the dead `addressBookEmails` branch, or expose a "Sync trusted senders to address book" toggle that gates the passive load and the `addToTrustedSendersBook` write.
@@ -278,7 +278,7 @@ RN covers the visible surface reasonably well (list with alphabetical index, det
- What RN does: lower-cases the whole string and stores `emails` only (`src/stores/contacts-store.ts:308-321`); a `Name <email>` string (e.g. typed in settings) becomes an invalid address.
- Fix hint: reuse the WEB regex.
- [ ] **Auto-trust reply recipients missing** — `P3` — `missing`
- [x] **Auto-trust reply recipients missing** — `P3` — `missing` — done in 5b72c4d (area 04)
- deferred: composer agent; `addToTrustedSendersBook` now accepts `Name <email>` and never races the passive load (26a34d5).
- What WEB does: on reply/replyAll every To/Cc recipient is added to the trusted book (`components/email/email-composer.tsx:2284-2292`; changelog 1.4.x "Auto-add recipients to trusted senders when replying").
- What RN does: `ComposeScreen` has no trusted-sender code (grep `trusted`: none).
@@ -291,16 +291,14 @@ RN covers the visible surface reasonably well (list with alphabetical index, det
- What RN does: filters `useCalendarStore().events` (`src/components/contacts/ContactActivity.tsx:90-91, 127-142`), i.e. only the range the calendar tab happened to load; on a fresh start it is empty.
- Fix hint: call the RN calendar API for `[now, now+365d]` with a participant filter, falling back to the cache.
- [ ] **Print contact** — N/A on mobile (share vCard exists). Listed under N/A.
## Verified at parity (brief list, so the fixer knows what NOT to redo)
- Address book list/create/rename/delete with `myRights` checks (`src/components/settings/ContactsSettings.tsx`, `AddressBookPickerSheet`); inline "New address book…" in the move sheet (WEB #415).
- Move contacts between books (single from detail, bulk from list) via `addressBookIds` patch (`contacts-store.ts:237-241`) — RN equivalent of WEB drag-and-drop.
- Group by first letter with sticky headers + settings toggle (`ContactsScreen.tsx:158-162`, `ContactsSettings.tsx:132-137`); sort by display name.
- Group by first letter with sticky headers + settings toggle (`ContactsScreen.tsx:158-162`, `ContactsSettings.tsx:132-137`); sort by display name, or by last name since 1827748 (#963); the sort no longer re-runs on every search keystroke since e6be892.
- Search over name/emails/phones/orgs (RN also matches keywords).
- Detail screen renders every WEB field block: emails/phones with contexts+features+labels, addresses (components, flat, timezone), org/units/titles/roles, anniversaries with age, gender/pronouns, languages, personalInfo, online services, calendar URIs, crypto keys, categories, relatedTo, notes, address book names, created/updated; quick actions call/SMS/mail/share; copy via long-press.
- Contact form: prefix/given/middle/surname/suffix, nickname, display name (`name.full`), multiple emails/phones with context + phone feature pills, addresses as RFC 9553 components, multiple orgs (org/department/title/role), anniversaries with native date picker, online services, personalInfo w/ level, gender+pronouns, calendar URIs, categories (chips + suggestions from existing keywords), multiple notes, photo pick/remove, address book pick, dirty-state discard confirm, email regex validation, `media: {}` when photo removed (equivalent of WEB `media: null`).
- `name.full` display fallback (#179), `getContactPrimaryEmail` with `pref`.
- `name.full` display fallback (#179), `getContactPrimaryEmail` with `pref`. `name.full` (the vCard FN) is written on every form, "add sender" and import save since a734d0a (#430).
- Group detail: member list resolved via id/uid/`urn:uuid:` (`contacts-store.ts:401-415`), add members via `ContactPickerSheet`, remove member, delete group, email all.
- Tags (keywords) = WEB categories: drawer section with counts, filter by tag, bulk assign via `TagAssignSheet`, per-contact edit in form.
- vCard import sheet: file picker, 5 MB cap, parse, duplicate detection by email (`detectDuplicates` identical), select all/none, target book, result count; export all (settings) and single (detail) with share sheet; multi-vCard files; QUOTED-PRINTABLE + charset decode (RN has a Hermes-safe `decodeBytes`), RFC 6868 params, KIND/MEMBER, PHOTO/LOGO/SOUND, RFC 6474/6715/8605/9554 properties.
+10 -14
View File
@@ -32,7 +32,7 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a
- What RN does: the `filtersExpandedView` toggle exists (`src/components/settings/FilterSettings.tsx:350-355`) but only lifts the `numberOfLines` clamp on the same one-line summary (`:268`, `:292`); `if`/`then`/`match_all_conditions` locale keys are unused.
- Fix hint: add a `VisualRuleSummary` component rendering rows of chips (all conditions, all actions) when `expandedView` is true.
- [ ] **Per-account (shared/group) filters not supported** — `P3` — `missing` (depends on whether RN gets managed shared-account settings at all) — deferred: RN has no managed/shared-account settings yet
- [x] **Per-account (shared/group) filters not supported** — `P3` — `missing` (depends on whether RN gets managed shared-account settings at all) — done in a86cd1d (tap a shared account in Account settings to manage its filters; the rule editor offers that account's folders)
- What WEB does: `filter-store` tracks `availableAccounts`/`selectedAccountId` and every Sieve call takes an `accountId` (`stores/filter-store.ts:62-134`); `FilterSettings` scopes to `managedAccountId` and fetches that account's mailboxes for the move target list (`components/settings/filter-settings.tsx:188-232`, `248-255`); client `getSieveAccounts()` (`lib/jmap/client.ts:4465-4469`).
- What RN does: `src/api/sieve.ts:20-23` always uses the primary Sieve account; store has no account selection (`src/stores/filter-store.ts:32`). No `managedAccount` concept exists in RN (`grep -ri managedAccount src` → none).
- Fix hint: only if the Accounts area adds shared-account management: thread an optional `accountId` through `api/sieve.ts` and the store's `fetchFilters/saveFilters`.
@@ -42,10 +42,6 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a
- What RN does: `src/lib/sieve/__tests__/{parser,generator,external-rules}.test.ts` start with a UTF-8 BOM and contain "→" (double-encoded arrow) at e.g. `external-rules.test.ts:257`, `:343`, `generator.test.ts:369`, `parser.test.ts:104`. Cosmetic, but it shows the files were copied through a wrong encoding; re-copy from WEB when porting the 1.7.3 changes and add tests for list values / `:mime`.
- Fix hint: rewrite the three files as UTF-8 without BOM from the WEB copies.
- [ ] **Raw Sieve editor lacks line-number gutter / Tab-inserts-spaces** — `P3` — `partial` (mostly N/A on mobile)
- What WEB does: gutter with line numbers, Tab inserts two spaces (`components/filters/sieve-editor-modal.tsx:61-72`, `101-128`).
- What RN does: shows "N lines" below the editor (`src/components/filters/SieveEditorSheet.tsx:107`). Acceptable on mobile; listed so nobody re-audits it.
### Vacation responder
- [x] **fromDate/toDate sent without timezone designator (not a JMAP UTCDate)** — fixed in b538a0e — `P2` — `rn-only-bug` — not verified live against stw-test; RFC 8621 UTCDate semantics implemented
@@ -73,7 +69,7 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a
- What RN does: `VacationSettings.tsx` never calls `useLocaleStore`; "Vacation Responder", "Date Range", "Saved", warnings etc. are literals (`:57-59`, `:72-74`, `:88`, `:101-158`). RN `locales/en/common.json` already contains `settings.vacation.{title,description,status,date_range,message,preview,save,saving,warnings}`.
- Fix hint: wire `t('settings.vacation.…')` like `FilterSettings.tsx` does.
- [ ] **Vacation fetch/save on a shared account** — `P3` — `missing` (same dependency as the filters item) — deferred: same dependency (no managed-account concept in RN)
- [x] **Vacation fetch/save on a shared account** — `P3` — `missing` (same dependency as the filters item) — done in a86cd1d
- What WEB does: `fetchVacationResponse(client, managedAccountId)` (`components/settings/vacation-settings.tsx:52-56`, `121-128`).
- What RN does: always `jmapClient.accountId` (`src/api/vacation.ts:27`, `:43`).
@@ -114,7 +110,7 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a
- What RN does: `getDocumentAsync({ multiple: false })` (`src/screens/FilesScreen.tsx:357-360`); uploading the same name twice creates two nodes with the same name.
- Fix hint: `multiple: true` + sequential upload loop; reuse WEB's `getUniqueName` against `visibleFiles`.
- [ ] **No in-app preview (image/text/markdown/PDF/audio/video/eml)** — `P2` — `missing` — deferred: the OS viewer/share sheet stays the mobile answer for now; no inline viewer ported
- [x] **No in-app preview (image/text/markdown/PDF/audio/video/eml)** — `P2` — `missing` — done in 409cdb1 (images, text and source files, `.eml`, and PDFs on iOS open in the reader's preview; PDFs on Android, audio, video, HTML and Markdown still go to another app)
- What WEB does: `ImagePreviewModal` and `FilePreviewModal` (text, markdown, PDF incl. mobile pdf.js viewer, audio, video, EML) chosen via `getFilePreviewKind` (`lib/file-preview.ts:29-68`; `components/files/file-preview-modal.tsx`, `image-preview-modal.tsx`, `pdf-mobile-viewer.tsx`; `files-app.tsx:487-495`, `690-708`).
- What RN does: tapping a file calls `shareAttachment` which downloads to cache and hands off to the OS share/viewer sheet (`src/screens/FilesScreen.tsx:248-258`; `src/lib/email-export.ts` "preview" variant). Works but there is no inline viewer and no "recent files" tracking.
- Fix hint: reuse whatever attachment viewer the mail area has (image modal / WebView for text+PDF); N/A if the team decides the OS viewer is the mobile answer - then close this item.
@@ -147,10 +143,10 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a
- What WEB does: on first listing, reparents nodes named with `/`, `∕`, `⁄`, `/` separators and replaces blob-backed "dir marker" files with real folders, with a progress overlay (`files-app.tsx:245-263`, `710-734`).
- What RN does: reads only the real hierarchy (`src/api/files.ts:6-14` comment acknowledges WEB migrates). A user who never opens WEB sees legacy files as flat names at the root. Acceptable if WEB is always used at least once; otherwise port `migrateLegacyFlatNodes`.
- [ ] **Deep link `/files/<folder>` and `?preview=` not handled** — `P3` — `missing` (changelog 1.8.1 "Deep links for mail, calendar, contacts, files") — deferred: belongs to the navigation area (RN has no linking config yet)
- [ ] **Deep link `/files/<folder>` and `?preview=` not handled** — `P3` — `missing` (changelog 1.8.1 "Deep links for mail, calendar, contacts, files") — partly: `/files` links open the Files tab since 5802cae; folder paths and `?preview=` are ignored
- What WEB does: `parseFilesPath`/`buildFilesPath` walk the drive to the folder and open the preview (`components/files/files-app.tsx:190-278`; `lib/deep-links.ts:381`).
- What RN does: no React Navigation `linking` config anywhere (`grep -rn "linking" src App.tsx` → none); `Files: undefined` in `src/navigation/types.ts:35`.
- Fix hint: belongs to the navigation area; when linking is added, map `/files/*` to `Files` with a `path` param and walk `allNodes` by names.
- What RN does: `src/navigation/linking.ts` (5802cae) turns any `/files/...` link into `{ kind: 'files' }` and opens the tab; `Files: undefined` in `src/navigation/types.ts:35`.
- Fix hint: parse the rest of the path in `linking.ts`, pass it to `Files` as a `path` param and walk `allNodes` by names.
- [x] **Hard-coded English throughout Files screen and ShareSheet** — fixed in c0e1d6b — `P3` — `rn-only-bug`
- What WEB does: everything via `files.*` locale keys (`locales/en/common.json` `files` has ~120 keys incl. `share`, `shared`, `shared_with_me`, `shared_by`).
@@ -161,7 +157,7 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a
- What WEB does: persistent yellow "stability_warning" above the browser (`files-app.tsx:617-620`; changelog 1.4.10 "Update file feature disabled messages and add stability warnings").
- What RN does: nothing. Optional; product call.
- [ ] **Storage quota not surfaced in Files** — `P3` — `missing` (partly N/A) — N/A: quota belongs to the Account/Settings area, Files shows none (like WEB's mobile layout)
- [x] **Storage quota not surfaced in Files** — `P3` — `missing` (partly N/A) — done in 490355c: the quota shows in Account settings; Files shows none (like WEB's mobile layout)
- What WEB does: passes `quota` from the email store into the navigation rail beside Files (`files-app.tsx:55`, `:569`); changelog 1.7.5 "Storage quota not shown with Stalwart (#577)".
- What RN does: `FilesScreen` shows no quota. Whether RN shows quota anywhere is outside this area; if it exists in Settings/Account, this is N/A.
@@ -169,17 +165,17 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a
- `repos/react-native/README.md:30` lists filters, S/MIME, plugins, themes and file storage as stubs; filters, vacation and files are real implementations (`src/api/files.ts`, `src/screens/FilesScreen.tsx`, `src/components/files/ShareSheet.tsx`, `src/api/__tests__/files.test.ts`). Update the README when the items above land.
## Verified at parity (brief list, so the fixer knows what NOT to redo)
- Sieve parser/generator/types are a faithful port up to 1.7.2: `diff -w --strip-trailing-cr lib/sieve/parser.ts repos/react-native/src/lib/sieve/parser.ts` shows only the multi-value/attachment hunks (and `debug.warn` → `console.warn`). Included and identical: `@metadata` JSON round-trip, external-rule parsing with origin labels (Roundcube/Nextcloud/etc., changelog 1.4.14 #201), Nextcloud marker regions preserved verbatim, `# Rule:` dedupe of Bulwark blocks that failed to re-parse (1.7.0 "literal braces" fix, `parser.ts` `findBodyOpenBrace` + `filteredExternal`), vacation-only script detection, `externalRequires` merge, `INBOX` canonical path (1.7.1 #313: `FilterRuleModal.tsx:57`), all 10 action types, `computeRequires` (fileinto/copy/imap4flags/reject/body/vacation), `stop` folding into `stopProcessing`.
- Sieve parser/generator/types are a faithful port up to 1.7.2: `diff -w --strip-trailing-cr lib/sieve/parser.ts repos/react-native/src/lib/sieve/parser.ts` shows only the multi-value/attachment hunks (and `debug.warn` → `console.warn`). Included and identical: `@metadata` JSON round-trip, external-rule parsing with origin labels (Roundcube/Nextcloud/etc., changelog 1.4.14 #201), Nextcloud marker regions preserved verbatim, `# Rule:` dedupe of Bulwark blocks that failed to re-parse (1.7.0 "literal braces" fix, `parser.ts` `findBodyOpenBrace` + `filteredExternal`), vacation-only script detection, `externalRequires` merge, `INBOX` canonical path (1.7.1 #313: `FilterRuleModal.tsx:57`), all 10 action types, `computeRequires` (fileinto/copy/imap4flags/reject/body/vacation), `stop` folding into `stopProcessing`. Since then the webmail writes "Keep" as `fileinto "INBOX"` (62465e1e, #1027) and added the vacation `include`, `:mailboxid` targets, `redirect :copy` and the spam guard; ported in f63d739, d53bfec, 3f88868 and 6c31c8d.
- Filter store semantics identical: skip server-managed `vacation` script (1.4.10), keep activation via `onSuccessActivateScript` on create/update (1.4.10), external/opaque rules read-only, bulwark rules kept contiguous before external ones, `addRule/updateRule/deleteRule/reorderRules/toggleRule`, rollback on failed save, opaque banner with "Open raw editor" / two-step "Reset to visual builder", `SieveScript/validate` via uploaded blob with error-method handling, `createSieveScript('filters')`.
- Filter rule editor: name/match type/conditions (from/to/cc/subject/header+name/size/body)/comparators/actions (move & copy folder picker from own non-shared mailboxes, forward, reject text, add_label from keywords store, mark read/star/discard/keep/stop)/stop-processing; empty-name/condition/action validation; comparator reconciliation on field change; default first mailbox for move/copy. Reorder via up/down chevrons instead of drag (fine on mobile). Delete uses a native confirm. Mailboxes are loaded at login (`src/stores/auth-store.ts:84`), so WEB fix #485 (load mailboxes when Filters opened directly) is not needed.
- Raw Sieve editor: warning banner, Validate with server errors, two-step confirm save, re-seeds on open, atomic `setOpaqueScript`.
- Vacation: `VacationResponse/get|set` on singleton with `core+mail+vacationresponse` using, default object when the list is empty, capability gating (`isVacationSupported`), enable toggle + status pill, subject, plain-text body, preview, end-before-start warning, empty-body warning, error surfacing.
- Files API: folder detection `blobId == null` (`src/api/files.ts:12-14`), `FileNode/get ids:null` (not `/query`, which drops folders), `shareWith/myRights` requested explicitly, `principals:owner` in `using`, real-hierarchy folder create without blob/type/size, cascade delete with `onDestroyRemoveChildren`, MIME-type >30 chars → `application/octet-stream` (1.4.12), cross-account "shared with me" aggregation with `accountId:nodeId` namespacing and owner-routed download URLs, shared subtrees browsable and writes hidden inside them, `Share2`/`Users` badges, ShareSheet with the same read/readWrite/manager presets as WEB `FILE_PRESETS`, custom-rights detection, principal search excluding self, revoke, refresh after change; unit tests in `src/api/__tests__/files.test.ts`.
- Files API: folder detection `blobId == null` (`src/api/files.ts:12-14`), `FileNode/get ids:null`, which Stalwart caps at `maxObjectsInGet` (500), continued since 4eb8c47 by paging `FileNode/query` (which lists folders too since Stalwart 0.16.6) and batched gets (#1069), `shareWith/myRights` requested explicitly, `principals:owner` in `using` only when advertised (sharing is gated on `principals` since caea3d0), real-hierarchy folder create without blob/type/size, cascade delete with `onDestroyRemoveChildren`, MIME-type >30 chars → `application/octet-stream` (1.4.12), cross-account "shared with me" aggregation with `accountId:nodeId` namespacing and owner-routed download URLs, shared subtrees browsable and writes hidden inside them, `Share2`/`Users` badges, ShareSheet with the same read/readWrite/manager presets as WEB `FILE_PRESETS`, custom-rights detection, principal search excluding self, revoke, refresh after change; unit tests in `src/api/__tests__/files.test.ts`.
- Files UI: list/grid toggle persisted to settings, folder breadcrumb, pull-to-refresh, hidden-file filter, colored/plain icons, long-press multi-select (own nodes only), rename/new-folder prompts rejecting `/`, delete confirmation naming folder cascade, path stack pruned when a folder disappears, Files tab disabled when the capability is missing.
## N/A on mobile
- Drag-and-drop upload / whole-folder upload (`uploadFolder`, `getDroppedFilesAndFolders`), drag-out of files, marquee selection, keyboard shortcuts (Ctrl+A/C/X/V, F2, Delete, Backspace), right-click context menus, resizable folder-tree sidebar, breadcrumb right-click dropdown, `?preview=` window title updates, `window.open` blob preview safety list (`isMimeTypeSafeForInlinePreview`).
- WebDAV proxy client (`lib/webdav/client.ts`, `stores/webdav-store.ts`) - WEB itself only uses it for calendar collections now; the Files app is JMAP-only, so RN correctly has no WebDAV code.
- Pro-shell cross-account Files picker / `accountFolders` / `__account_root__` breadcrumb sentinel, `filesEnabled` admin policy gate (RN has no policy store), plugin hooks `onSieveScriptGenerate`/`onFiltersSave`.
- Drag-to-reorder filter rules (RN uses chevrons), line-number gutter in the Sieve editor.
- Drag-to-reorder filter rules (RN uses chevrons), line-number gutter and Tab-inserts-spaces in the Sieve editor (RN shows "N lines" below it).
- "Attach from Files" in the composer and "Save attachment to Files" in the viewer: neither client implements these (WEB composer only takes local files, `components/email/email-composer.tsx:1391-1555`; no `createFileNode` call outside the Files store), so nothing to port.
+12 -12
View File
@@ -12,7 +12,7 @@ Legend: RN? = key exists in RN store; Honored = read by RN app code outside `sto
| WEB key (default) | RN key (default) | RN? | Honored | Notes |
|---|---|---|---|---|
| fontSize (medium) | fontSize (medium) | yes | partial | Only `useTypography()` callers scale: EmailListScreen, SidebarDrawer, CalendarSidebarDrawer (`RN: src/theme/dynamic.ts:47`); 89 files import static `typography` from tokens |
| fontSize (medium) | fontSize (medium) | yes | partial | Only `useTypography()` callers scale: EmailListScreen and its attachment chips (`RN: src/theme/dynamic.ts:47`); the other files import static `typography` from tokens |
| density (regular) | density (regular) | yes | yes | EmailListScreen via `useDensity()` |
| animationsEnabled (true) | animationsEnabled (true) | yes | partial | Only via `useAnimDuration` in 3 files; 18 files call `Animated.timing` with fixed durations (UndoSnackbar, SwipeableRow, every sheet) |
| messageListOrder / messageListOrderScope (#718) | mailSortAscending (false) | partial | yes | Single boolean; native issue #5. Owner: mail-list agent |
@@ -99,7 +99,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
- What RN does: 25, true, true, true, false, false, right=read/left=archive, 4 English keywords (`RN: src/stores/settings-store.ts:232-325`).
- Fix hint: align defaults where the behaviour is the same on both clients (attachment keywords, includeGroupInUnified, autoSelectReplyIdentity, showBirthdayCalendar) before any sync exists, otherwise the first sync flips them for existing users.
- [ ] **`trustedSendersAddressBook` can never be turned on in RN** — `P3` — `partial`
- [x] **`trustedSendersAddressBook` can never be turned on in RN** — `P3` — `partial` — done in 78114b6 (toggle in Content & Senders); since d89e331 it stays off until the user turns it on, where the webmail turns it on by itself
- What WEB does: `null` resolves to `true` on first connect when the server has contacts; the Content & Senders tab has a toggle (`components/settings/content-senders-settings.tsx`, `updateSetting('trustedSendersAddressBook'`).
- What RN does: default `false` (`RN: src/stores/settings-store.ts:239`), read by `EmailBodyView.tsx:470,489` but `ContentSendersSettings.tsx` has no control for it.
- Fix hint: add the toggle; treat undefined as auto-on when `useHasContacts()`.
@@ -120,7 +120,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
- Fix hint: add `resetToDefaults` to the store (`set(DEFAULT_PERSISTED)` + persist), an export via `expo-sharing` and import via `expo-document-picker` using WEB's export shape so files round-trip between clients; add a "refresh cached data" that clears `email-snapshot`/contacts/calendar caches without logging out.
- [x] **Debug mode / categories are inert local state; no debug/logger abstraction** — fixed in 8deff66 — `P3` — `missing` — src/lib/debug.ts; dead Debug tab removed in 3d0d440
- What WEB does: `debugMode`/`debugCategories` persisted and honoured by `lib/debug.ts:8-13`; `lib/error-reporting.ts` funnels error-boundary reports; `lib/logger.ts` server side. Debug tab gated by admin (`settings-app.tsx:766`).
- What WEB does: `debugMode`/`debugCategories` persisted and honoured by `lib/debug.ts:8-13`; `lib/logger.ts` server side (`lib/error-reporting.ts`, cited here before, has since been deleted from the webmail). Debug tab gated by admin (`settings-app.tsx:766`).
- What RN does: `AboutDataSettings.tsx:61-66,226-244` keeps toggles in `useState` with categories (`sync`, `render`) that exist nowhere; the "Debug" tab is `implemented: false` (`SettingsScreen.tsx:111`); code uses raw `console.warn`.
- Fix hint: persist `debugMode`/`debugCategories`, add `src/lib/debug.ts` mirroring WEB's category API, route the `[push]`, `[settings-store]`, `[updates-store]` warnings through it, and drop the dead Debug tab or implement a log viewer.
@@ -136,7 +136,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
### Push notifications
- [x] **RN subscribes to `Email` and `Mailbox` state changes, not just `EmailDelivery`** — fixed in 00faceb — `P2` — `bugfix-parity`
- [x] **RN subscribes to `Email` and `Mailbox` state changes, not just `EmailDelivery`** — fixed in 00faceb — `P2` — `bugfix-parity` — the `emailPush` map it added was refused for ACL-shared mailboxes, which could leave push off for good, and was re-patched on every launch, until d8046a8 and ccaa24e (audit B18)
- What WEB does: `PUSH_TYPES = ['EmailDelivery']` because `Email` fires on every mutation and produced spurious notifications (`lib/web-push.ts:39-44`; changelog 1.5.x "Scope new-mail notifications to genuine inbox deliveries").
- What RN does: `PUSH_TYPES = ['Email', 'EmailDelivery', 'Mailbox']` (`RN: src/lib/push-notifications.ts:104`). Every read/flag/move/draft on any client wakes the device through FCM and runs the headless task.
- Fix hint: change to `['EmailDelivery']` and bump the subscription (the refresh path only patches `expires`, `:411-429`; add a `types` mismatch check like WEB `:369-383` so existing subscriptions get corrected).
@@ -151,7 +151,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
- What RN does: `identifyAccountFromFcmData` scans payload values for a locally stored `deviceClientId` (`RN: src/lib/push-background-task.ts:67-87`), but the relay sends `kind, accountLabel, accountId, emailIds, changed` (`repos/relay/src/fcm.ts` message.data) - never the subscription id. Result: every push processes every logged-in account serially inside a 30 s headless budget (`BulwarkPushTaskService.kt` timeout), loading each account's JMAP session.
- Fix hint: the relay's `accountId` is the JMAP primary account id; persist each account's JMAP account id in `AccountEntry` (`RN: src/stores/account-store.ts:7-19` has none) or in the push registry (`push:accountIds:v1` → map local id → jmap id) at setup time, and match on that. `accountLabel` (= username) is a weaker second key.
- [x] **No `emailPush` delivery filter (spam still wakes the device)** — fixed in 00faceb — `P2` — `bugfix-parity`
- [x] **No `emailPush` delivery filter (spam still wakes the device)** — fixed in 00faceb — `P2` — `bugfix-parity` — the map was refused for ACL-shared mailboxes and never read back until d8046a8 and ccaa24e (audit B18)
- What WEB does: when the session advertises `urn:ietf:params:jmap:emailpush` (Stalwart ≥ 0.16.16) the subscription carries a per-account filter `notKeyword:$junk AND inMailboxOtherThan:[junk ids]` (`lib/web-push.ts:53-108,491-497`, commit 63fa2d2d) and re-syncs it in the background on every page load (`:668-688`, `components/push-notification-prompt.tsx:119-126`, 1.9.2).
- What RN does: `createPushSubscription` has no `emailPush` parameter (`RN: src/api/push.ts:23-57`); the Inbox-only query hides junk from the notification but the wake-up and JMAP round-trip still happen for every spam delivery.
- Fix hint: port `buildEmailPushConfig` + `serverSupportsEmailPush` (session capabilities are in `jmapClient.currentSession.capabilities`), add `emailPush` to `createPushSubscription`/`updatePushSubscription`, and extend `refreshSubscriptionExpires` to patch `types`/`emailPush` when they differ.
@@ -176,7 +176,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
- What RN does: `getNative()` returns `null` off Android and setup throws "Push notifications require Android" (`RN: src/lib/push-notifications.ts:125-128,303-304`); the relay has only FCM and Web Push transports (`repos/relay/README.md` endpoints).
- Fix hint: needs an APNs transport in the relay (`POST /api/push/register/apns` storing a device token, HTTP/2 to `api.push.apple.com` with a `.p8` key) plus an iOS native module (or `expo-notifications`) for the token and a Notification Service Extension to fetch sender/subject over JMAP. Content-blind payload stays the same.
- [ ] **No non-FCM transport (UnifiedPush, native #44/#48)** — `P3` — `missing` — deferred: relay must accept key-less web registrations + a UnifiedPush connector module
- [x] **No non-FCM transport (UnifiedPush, native #44/#48)** — `P3` — `missing` — done in ee04e95 (Android: UnifiedPush connector through a distributor app such as ntfy; the relay falls back to unencrypted delivery for distributors without Web Push keys); not in a release yet
- What WEB does: N/A (browser push).
- What RN does: FCM only (`BulwarkMessagingService.kt`, `push-notifications.ts:120-128`).
- Fix hint: UnifiedPush distributors expose an RFC 8030 endpoint, which is exactly what the relay's `/api/push/register/web` already accepts (`repos/relay/src/server.ts` handleRegisterWeb); VAPID/`keys` are optional in UnifiedPush so the relay must accept a record without `p256dh`/`auth` and send unencrypted (or only the id). Client side: `unifiedpush-react-native`/`org.unifiedpush.android.connector` receiving the message and calling the same headless task.
@@ -220,7 +220,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
- [ ] **No RTL support (ar/he/fa)** — `P2` — `missing` — done in 990cd84: forceRTL/allowRTL on override + restart hint, isLayoutRTL() helper in src/i18n; deferred: SwipeableRow side swap (mail-list agent's file - call isLayoutRTL() there)
- What WEB does: `getLocaleDirection` sets `dir=rtl`, logical CSS, JS popovers flip, RTL-aware swipe (`i18n/direction.ts`; changelog 1.7.0-1.7.3 RTL entries).
- What RN does: none of the three RTL locales are shipped; no `I18nManager` usage anywhere; `android:supportsRtl="true"` is set (`RN: AndroidManifest.xml:16`) so the OS would mirror layouts when the device locale is RTL, but the app's own locale override cannot force it, and `SwipeableRow` maps left/right physically.
- What RN does: ar, he and fa ship since 990cd84, which also calls `I18nManager.forceRTL`/`allowRTL` when the language override changes (after a restart); mail bodies get `dir="auto"` since 910c503 (audit U4). `SwipeableRow` still maps left/right physically.
- Fix hint: when adding ar/he/fa, call `I18nManager.forceRTL(dir==='rtl')` + `allowRTL` on override change (requires reload), audit `paddingLeft/Right` → `paddingStart/End`, and swap swipe actions when `I18nManager.isRTL`.
- [ ] **Language list not localized and no "auto" date locale** — `P3` — `partial` — deferred: language labels are native names (webmail parity); timeZone landed as calendarTimeZone (calendar agent); dateLocale needs a locale override threaded through src/lib/date-format.ts call sites in other areas
@@ -230,9 +230,9 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
### Themes and appearance
- [ ] **Font size setting is honoured by 3 of ~90 files** — `P3` — `partial` — deferred: Text.defaultProps is gone on the new architecture; needs the static typography import replaced by useTypography() per screen
- [ ] **Font size setting is honoured by the mail list only** — `P3` — `partial` — deferred: Text.defaultProps is gone on the new architecture; needs the static typography import replaced by useTypography() per screen
- What WEB does: `--font-size-base` on `:root` scales everything (`stores/settings-store.ts:1264-1274`).
- What RN does: `useTypography()` (`RN: src/theme/dynamic.ts:47-68`) is used by EmailListScreen, SidebarDrawer, CalendarSidebarDrawer; the other screens spread the static `typography` from `tokens.ts`, so Small/Large changes almost nothing (the thread reader, compose, settings, calendar, contacts all stay fixed).
- What RN does: `useTypography()` (`RN: src/theme/dynamic.ts:47-68`) is used by EmailListScreen and its attachment chips only; the other screens spread the static `typography` from `tokens.ts`, so Small/Large changes almost nothing (the thread reader, compose, settings, calendar, contacts all stay fixed).
- Fix hint: either make `tokens.typography` a hook-backed getter or, cheaper, set `Text.defaultProps`/`maxFontSizeMultiplier` and scale via `allowFontScaling` + a root `PixelRatio` factor.
- [x] **`animationsEnabled` ignored by most animations; no reduce-motion respect** — fixed in 3ae9bbe — `P3` — `partial` — useShouldAnimate ORs in AccessibilityInfo.isReduceMotionEnabled; the 18 fixed-duration Animated.timing calls in other areas' files still need useAnimDuration
@@ -250,7 +250,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
- What RN does: `RN: app.config.js:33` sets `userInterfaceStyle: 'dark'`, which prebuild writes as `UIUserInterfaceStyle = Dark` in Info.plist, so `useColorScheme()` (`src/theme/colors.ts:16`, `App.tsx:220`) always returns `dark` and "System" behaves like "Dark". On Android without `expo-system-ui` (not in `package.json`) the key is ignored with a prebuild warning, so Android is probably fine. Splash background is also dark-only (`:38`).
- Fix hint: set `userInterfaceStyle: 'automatic'` and give the splash a light variant (`splash.dark`).
- [x] **Themes tab is a stub with fake themes; `activeThemeId` never applied** — fixed in fe45a44 — `P3` — `missing` (record as N/A for now) — Qui/Nord/Catppuccin/Solarized token sets generated from lib/builtin-themes.ts (src/theme/builtin-themes.ts); useColors() applies activeThemeId; upload button removed
- [x] **Themes tab is a stub with fake themes; `activeThemeId` never applied** — fixed in fe45a44 — `P3` — `missing` (record as N/A for now) — Qui/Nord/Catppuccin/Solarized token sets generated from lib/builtin-themes.ts (src/theme/builtin-themes.ts); useColors() applies activeThemeId; upload button removed; Roundcube Elastic and Aurora Glass added in 7964e64, "Flat fields" not ported
- What WEB does: 6 built-in themes (`lib/builtin-themes.ts:879-936`), zip upload, marketplace, admin forced/default theme, compiled CSS tokens, PWA theme-color meta (`stores/theme-store.ts`).
- What RN does: `ThemesSettings.tsx:18-22` lists `Default/Qui/Sepia` (Qui exists in WEB; Sepia does not), stores `activeThemeId` that nothing reads, "Upload .zip" has no handler (`:81-83`); the tab is marked Experimental.
- Fix hint: minimal viable subset = map WEB's built-in theme token sets (`builtin-themes.ts` light/dark `colors`) onto `ThemePalette` and let `useColors()` pick `activeThemeId`; remove the upload button. Otherwise hide the tab.
@@ -294,7 +294,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
### Misc UI
- [x] **Deep-link scheme registered but nothing handles it** — fixed in 5802cae — `P2` — `missing` — bulwarkmobile://, webmail https permalinks, mailto: (scheme + SENDTO filter) and ACTION_SEND/SEND_MULTIPLE share targets → Compose; iOS universal links still deferred
- [x] **Deep-link scheme registered but nothing handles it** — fixed in 5802cae — `P2` — `missing` — bulwarkmobile://, webmail https permalinks, mailto: (scheme + SENDTO filter) and ACTION_SEND/SEND_MULTIPLE share targets → Compose; iOS universal links still deferred; shared files never uploaded, `SENDTO` opened the inbox and `+` addresses opened nothing until cd4ce9d, 06e8942 and ee652d3 (audit B9–B11)
- What WEB does: permalinks for mail/calendar/contacts/files/settings with build/parse helpers (`lib/deep-links.ts:196-235,272-311,322-370,379-414`), `mailto:` opens the built-in composer, protocol handler registration, `webcal:`.
- What RN does: `scheme: 'bulwarkmobile'` (`RN: app.config.js:29`) and a `VIEW`/`BROWSABLE` intent filter (`AndroidManifest.xml:25-30`) exist, but `NavigationContainer` has no `linking` prop (`App.tsx:443`) and there is no `Linking.getInitialURL`/`addEventListener('url')` anywhere, so tapping a `bulwarkmobile://` URL merely launches the app. No `https` App Links for the webmail permalink format, no `mailto:` intent filter (tapping a mailto link in another app never offers Bulwark), no `SEND`/`SEND_MULTIPLE` share target.
- Fix hint: add a `linking` config mapping `bulwarkmobile://mail/message/:emailId`, `/calendar/event/:id`, `/contacts/:id`, `/settings/:tab` to the existing routes (`src/navigation/types.ts`), reuse WEB's path grammar so a webmail permalink can be rewritten; add `<data android:scheme="mailto"/>` and `ACTION_SEND` (`text/*`, `image/*`) filters routed to `Compose` with `prefillTo`/attachments. iOS: `CFBundleURLTypes` via `scheme` is already emitted; add `associatedDomains` for universal links later.
@@ -314,7 +314,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c
- What RN does: `OfflineBanner.tsx:22-29` hard-codes "You are offline" with an English plural; network-store logic itself is sound (`network-store.ts:16-22` treats `isInternetReachable === null` as online) and App.tsx retries the session on reconnect (`:245-252`).
- Fix hint: `t('common.offline', ...)` and reuse plural helper once interpolation exists.
- [ ] **Accessibility labels almost absent** — `P3` — `partial` — done in my files (RadioGroup/Select roles, Dialog header, Settings back button, ToastHost, UpdateBanner, AccountSettings, security key rows); other areas' screens untouched
- [x] **Accessibility labels almost absent** — `P3` — `partial` — done in my files (RadioGroup/Select roles, Dialog header, Settings back button, ToastHost, UpdateBanner, AccountSettings, security key rows); the mail list, composer, Files, contacts, sign-in and settings switches followed in 9c0c35c, 771558d, aab29da, d59c571, 5801d6c, cb5c152 and c1e5cd6, and the viewer, calendar, tab bar, unified inbox and drawer in 13cba64, b3a82f1, 84fef14 and b178ee8 (audit U8)
- What WEB does: aria-labels on icon buttons, `role="checkbox"`, screen-reader improvements (changelog 1.8.0 Navigation).
- What RN does: `accessibilityLabel`/`Role` appear in 8 of 94 `.tsx` files (FilesScreen 4, EmailListScreen 3, login 4, EmailThreadScreen 2, ToggleSwitch 1). Icon-only `Pressable`s in settings (`SettingsScreen.tsx:217-222` back button, `SidebarAppsSettings.tsx:91-96`, `SmimeSettings.tsx:91-106`, `UpdateBanner.tsx:40`), the tab bar badge and the `RadioGroup`/`Select` in `settings-section.tsx` have none.
- Fix hint: give every icon-only Pressable an `accessibilityLabel` + `accessibilityRole="button"`, `ToggleSwitch` `accessibilityRole="switch"` + `accessibilityState`, `RadioGroup` options `radio`.
+10 -10
View File
@@ -52,14 +52,14 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess
- What RN does: `rewriteSessionUrls` only rewrites when `extractOrigin(url)` matches `^https?://` (`src/api/jmap-client.ts:320-340`); a relative `apiUrl` is returned unchanged and `fetch('/jmap/')` then fails with "Network request failed". Same gap in `src/api/unified-inbox.ts:55-60`. (RN correctly avoids `new URL()` so templates are not corrupted.)
- Fix hint: in `rewrite()`, when `extractOrigin` is null and the string starts with `/`, return `serverOrigin + url`.
- [x] **Capability check uses the broad `principals` URN, not `principals:owner` (44360024)** — `P2` — `bugfix-parity` — fixed in a0ce925
- [x] **Capability check uses the broad `principals` URN, not `principals:owner` (44360024)** — `P2` — `bugfix-parity` — fixed in a0ce925; since Stalwart advertises `principals:owner` nowhere, that also hid sharing, so since caea3d0 the sharing UI is gated on `principals` and `principals:owner` only goes into `using` when advertised
- What WEB does: declares `urn:ietf:params:jmap:principals:owner` in `using` only when that exact capability is advertised in session or account capabilities (`lib/jmap/client.ts:4689-4698`, `6472`; changelog "Check the specific capability a request declares (principals:owner), not a broader one").
- What RN does: `supportsSharing()` checks `CAPABILITIES.PRINCIPALS` (`src/api/files.ts:23-25`) and then pushes `PRINCIPALS_OWNER` into `using` (`files.ts:40-43`). A server advertising `principals` but not `principals:owner` rejects every FileNode request with `unknownCapability`.
- Fix hint: check `hasCapability(CAPABILITIES.PRINCIPALS_OWNER) || accountCapabilities[...]` before adding it.
- [x] **PatchObject pointer keys not JSON-Pointer escaped (5c484af1)** — `P3` — `bugfix-parity` — fixed in ccbe67c
- What WEB does: `keywordPointer()`/`pointerToken()` escape `~` and `/` (`lib/jmap/patch-pointer.ts:39-55`); mailbox moves use a whole-map replacement (`lib/jmap/client.ts:66-74`).
- What RN does: `moveEmail`/`moveEmails` build `mailboxIds/${id}` pointers unescaped (`src/api/email.ts:456-457`, `473-474`). Stalwart ids never contain `/`, so exposure is limited to non-Stalwart servers; keywords are always sent as a whole map (`email.ts:436-445`), so tag names with `/` are safe.
- What RN does: `moveEmail`/`moveEmails` build `mailboxIds/${id}` pointers unescaped (`src/api/email.ts:456-457`, `473-474`). Stalwart ids never contain `/`, so exposure is limited to non-Stalwart servers; keywords are always sent as a whole map (`email.ts:436-445`), so tag names with `/` are safe. (Since 5041897 keywords are sent as `keywords/<name>` pointers too, escaped by `keywordPointer` in `src/api/patch-pointer.ts`.)
- Fix hint: add `pointerToken()` or switch `moveEmail(s)` to the full `mailboxIds` replacement RN already uses in `restoreEmailMailboxes`.
- [x] **Send: message created directly in Sent instead of Drafts + `onSuccessUpdateEmail` (#188)** — `P2` — `bugfix-parity` — fixed in ccbe67c
@@ -96,7 +96,7 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess
- What RN does: `fetchMailboxes` does one `Mailbox/get`; an empty list on a brand-new Stalwart account stays empty until pull-to-refresh (`src/stores/email-store.ts:584-588`).
- Fix hint: if the list is empty and there is no `inbox` role, retry after ~1 s up to 3 times.
- [x] **Session redirect with dropped Authorization not handled** — `P3` — `partial` — fixed in 0b1c240
- [x] **Session redirect with dropped Authorization not handled** — `P3` — `partial` — fixed in 0b1c240; the check keyed on `response.redirected`, which React Native's fetch never sets, so it only works since 859770b (audit B15)
- What WEB does: detects `response.redirected` + empty accounts/username and refetches the final URL with the header (`lib/jmap/client.ts:911-925`).
- What RN does: `fetchSession` accepts whatever comes back; `resolveAccountId` then throws "No account found in JMAP session" (`src/api/jmap-client.ts:363-401`). iOS strips Authorization on cross-origin redirects.
- Fix hint: same detection; retry against `response.url`.
@@ -128,7 +128,7 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess
- What RN does: `Email`/`EmailDelivery`/`Mailbox` (`src/stores/email-store.ts:981-1020`), `AddressBook`/`ContactCard`/`Contact` (`src/stores/contacts-store.ts:152-168`), `Calendar`/`CalendarEvent` incl. shared accounts (`src/stores/calendar-store.ts:261-283`). `ScheduledScreen`, `filter-store`, `vacation-store`, Files and Identity never refresh on push (`grep handleStateChange src/stores/filter-store.ts src/stores/vacation-store.ts` empty).
- Fix hint: add `EmailSubmission` -> scheduled list refresh (when the Scheduled screen is mounted), `SieveScript` -> `filter-store.fetch`, `VacationResponse` -> vacation store, `FileNode` -> Files screen refresh; dispatch from the `onStateChange` fan-out in `App.tsx:391-396`.
- [ ] **Contacts state changes ignore shared/group accounts** — `P3` — `partial`
- [x] **Contacts state changes ignore shared/group accounts** — `P3` — `partial` — done in 26a34d5 (`handleStateChange` walks `getContactCapableAccountIds()`)
- What WEB does: per-account clients; `handleStateChange` reacts to the client's own account (`stores/email-store.ts:3255-3257`); calendar/contacts stores use the account of the shared collection.
- What RN does: `contacts-store.handleStateChange` only reads `change.changed[jmapClient.accountId]` (`src/stores/contacts-store.ts:154-156`) although address books from shared accounts are shown; `calendar-store` already folds shared accounts in (`calendar-store.ts:263-272`).
- Fix hint: mirror the calendar store's `known` set using `addressBook.accountId`.
@@ -143,7 +143,7 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess
- What RN does: `react-native-sse` accumulates `xhr.responseText` for the life of the connection (`EventSource.js:108`, `_lastIndexProcessed`), and RN requests `{ping}=30`, `{closeafter}=no` (`src/api/push.ts:126-129`), so memory grows for every ping/event until reconnect. No ping-timeout detection either.
- Fix hint: either request `closeafter=state` (server closes after each event; library auto-reconnects) or recycle the EventSource every ~30 min / when no `ping` event arrived in 90 s (add a `ping` listener).
- [x] **Push subscription `types` include `Email` and `Mailbox`; no `emailPush` spam filter** — `P2` — `partial` — fixed in 00faceb
- [x] **Push subscription `types` include `Email` and `Mailbox`; no `emailPush` spam filter** — `P2` — `partial` — fixed in 00faceb; the `emailPush` map was refused for ACL-shared mailboxes and never read back until d8046a8 and ccaa24e (audit B18)
- What WEB does: subscribes to `EmailDelivery` only, plus an `emailPush` filter excluding `$junk`/Junk when the server advertises `urn:ietf:params:jmap:emailpush` (`lib/web-push.ts:39-52`, `lib/jmap/client.ts:8085-8142`; changelog 1.9.x "Stop sending notifications for spam").
- What RN does: `PUSH_TYPES = ['Email', 'EmailDelivery', 'Mailbox']` (`src/lib/push-notifications.ts:104`); `createPushSubscription` has no `emailPush` support (`src/api/push.ts:23-57`). Every read/flag/move on any device wakes the relay -> FCM -> headless task, which then re-queries the inbox (`src/lib/push-background-task.ts:452-455`); a spam delivery still triggers a push (only the inbox `notKeyword $seen` query hides it, at the cost of a wake-up).
- Fix hint: subscribe to `EmailDelivery` only; add `hasEmailPushCapability()` + `emailPush` filter as in WEB; the relay must forward it (see memory note: relay lacks EmailPush).
@@ -181,7 +181,7 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess
- What RN does: `android:allowBackup="true"` with `@xml/secure_store_backup_rules` from the expo-secure-store plugin, which only excludes SecureStore's own prefs (`android/app/src/main/AndroidManifest.xml:16`); the AsyncStorage SQLite DB (full cached bodies `webmail:offline-cache:entry:v2:*`, outbox, settings, account registry) is backed up to the user's Google account.
- Fix hint: add an app-owned `fullBackupContent`/`dataExtractionRules` excluding `databases/RKStorage` (AsyncStorage) or set `allowBackup=false` via an Expo config plugin.
- [ ] **Send has no offline path and no draft fallback** — `P2` — `missing`
- [ ] **Send has no offline path and no draft fallback** — `P2` — `missing` — partly: drafts autosave to the server since 5b72c4d, and since 92b42e4 the close dialog no longer drops the text when the draft can't be saved; an offline send is still not queued, and a draft can't be saved offline
- What WEB does: no offline queue either, but send failures leave the composer open with the draft autosaved (composer agent's area); send itself has a deadline (#702).
- What RN does: `ComposeScreen` shows `Alert('Send failed')` and keeps the modal (`src/screens/ComposeScreen.tsx:975-979`); there is no draft API in `src/api/email.ts` (`grep -n '[Dd]raft' src/api/email.ts` only finds the send create id), so the body is lost if the user leaves, and an offline send is not queued although the outbox exists.
- Fix hint: either add an `outbox` `send` op kind (blob uploads must already be done) or at minimum persist the composer state locally on failure.
@@ -298,11 +298,11 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess
- `Email/queryChanges` + `Email/changes` incremental list refresh with snapshot guard (issue #10) and full re-query fallback (`src/stores/email-store.ts:770-980`).
- `null` for patch removal, `false` stripped from `mailboxIds` maps (`src/api/email.ts:456`, `551-556`).
- FilterOperator/FilterCondition kept separate in query filters (`src/api/email.ts:225-237`).
- `getSharedMailboxes` chunks to `maxCallsInRequest` (`src/api/email.ts:84-105`); `getEvents`/`getContacts`/`fetchEmailsChunked`/offline sync chunk to `maxObjectsInGet`.
- Send: `/error`, `notCreated` checked; HOLDFOR envelope for scheduled send; FUTURERELEASE capability check (`src/api/email.ts:836-873`, `src/api/jmap-client.ts:505-531`).
- Push subscription create/verify/update/destroy with `using: [core]` and 90-day expiry refresh (`src/api/push.ts:9-114`, `src/lib/push-notifications.ts`).
- `getSharedMailboxes` chunks to `maxCallsInRequest` (`src/api/email.ts:84-105`); `getEvents`/`getContacts`/offline sync chunk to `maxObjectsInGet`, and list pages are capped at it since `queryEmailPage` replaced `fetchEmailsChunked` (d987930).
- Send: `/error`, `notCreated` checked; HOLDFOR envelope for scheduled send; FUTURERELEASE capability check (`src/api/email.ts:836-873`, `src/api/jmap-client.ts:505-531`). Two gaps found later: the capability was read from the session instead of the account until e28e8b4 (#57, audit B8), and a method error after a created submission (Stalwart's failed filing into Sent) was reported as a failed send until c51a848 (audit B23).
- Push subscription create/verify/update/destroy with `using: [core]` and 90-day expiry refresh (`src/api/push.ts:9-114`, `src/lib/push-notifications.ts`); calls that read or write `emailPush` also name `urn:ietf:params:jmap:emailpush` since ccaa24e, and every call checks the response since ddfe85a.
- Outbox: idempotent full-state ops, per-account buckets, destroy-wins coalescing, oldest-first replay, account-switch guards (`src/stores/outbox-store.ts`).
- Offline cache: per-account buckets, index + entry keys, eviction by `receivedAt`, optimistic `patch`, abort on account switch (`src/stores/offline-cache-store.ts`); `getEmailDetail` cache fallback.
- Offline cache: per-account buckets, index + entry keys, eviction by `receivedAt`, optimistic `patch`, abort on account switch (`src/stores/offline-cache-store.ts`); the offline copy is shown before the network since 33e303e (`src/lib/email-detail-cache`, which replaced `getEmailDetail`).
- Credentials in `expo-secure-store` (never AsyncStorage); SecureStore key sanitising; per-account keys; legacy migration.
- Client-cert module uses the platform default `X509TrustManager` (no trust-all) (`BulwarkClientCertModule.kt:216-222`).
- WebView hardening: `originWhitelist=['about:blank']`, no file/universal access, `mixedContentMode="never"`, no multiple windows, no cookies/DOM storage, `incognito`, external links limited to `https?|mailto|tel|sms` (`src/components/EmailBodyView.tsx:675-724`).
+11 -3
View File
@@ -1,7 +1,7 @@
import { createElement } from 'react';
import { AppRegistry } from 'react-native';
import { registerRootComponent } from 'expo';
import App from './App';
import { pushBackgroundTask } from './src/lib/push-background-task';
// Runs in a fresh headless JS runtime when BulwarkPushTaskService is started
@@ -9,7 +9,15 @@ import { pushBackgroundTask } from './src/lib/push-background-task';
// before the native service tries to invoke the task.
AppRegistry.registerHeadlessTask('BulwarkPushTask', () => pushBackgroundTask);
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// A headless push task evaluates this file too, so App is required when the
// UI first renders instead of imported above: importing it here would load
// every screen, store and icon before the task could run.
function Root() {
const App = (require('./App') as typeof import('./App')).default;
return createElement(App);
}
// registerRootComponent calls AppRegistry.registerComponent('main', () => Root);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
registerRootComponent(Root);
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "و{count} آخرين",
"draft": "مسودة",
"starred": "مميزة بنجمة",
"pinned": "مثبتة",
"replied": "تم الرد عليها",
"forwarded": "تمت إعادة توجيهها",
"has_attachment": "تحتوي على مرفق",
"conversations_count": "{count} من {total} محادثة",
"conversations_count_plus": "{count}+ محادثة",
"conversations_count_simple": "{count} محادثة",
@@ -292,6 +296,7 @@
"forward": "إعادة توجيه",
"delete": "حذف",
"archive": "أرشفة",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "تمييز بنجمة",
"unstar": "إزالة النجمة",
"mark_unread": "تمييز كغير مقروءة",
@@ -303,12 +308,10 @@
"move": "نقل",
"print": "طباعة",
"view_source": "عرض المصدر",
"fullscreen": "ملء الشاشة",
"exit_fullscreen": "الخروج من ملء الشاشة",
"export_email": "تصدير كملف ‎.eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "إعادة التوجيه كمرفق",
"import_email": "استيراد ملف ‎.eml أو ‎.zip",
"keyboard_shortcuts": "اختصارات لوحة المفاتيح (؟)",
"email_source": "مصدر الرسالة",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "إيصال القراءة مطلوب (انقر للتعطيل)",
"read_receipt_off": "طلب إيصال قراءة",
"dsn_on": "تم طلب إشعار التسليم (انقر للتعطيل)",
"dsn_off": "طلب إشعار بالتسليم",
"format_plain_text": "التبديل إلى نص عادي",
"format_rich_text": "التبديل إلى نص منسق (HTML)",
"require_tls_on": "التسليم المشفّر مطلوب (انقر للتعطيل)",
"require_tls_off": "اشتراط التسليم المشفّر (TLS)",
"new_message": "رسالة جديدة",
"signature_edit_hint": "انقر نقرًا مزدوجًا لتحرير التوقيع",
"reply": "رد",
@@ -634,11 +643,12 @@
"from_override": {
"toggle_off": "تجاوز",
"toggle_on": "إلغاء التجاوز",
"toggle_tooltip": "عدّل اسم وعنوان \"من\" بحرية. سيُرسل البريد عبر هويتك، لكن ترويسة \"من\" الظاهرة فقط هي التي تتغير.",
"toggle_tooltip": "عدّل اسم وعنوان \"من\" بحرية. سيُرسل البريد عبر هويتك.",
"name_label": "اسم المرسل",
"name_placeholder": "الاسم",
"email_label": "عنوان بريد المرسل",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "إذا لم يقبل خادمك هذا العنوان كمرسل للمغلف، فسيُستخدم {identity} بدلاً منه، ويمكن للمستلمين رؤيته في ترويسة Return-Path."
},
"use_template": "قالب",
"save_as_template": "حفظ كقالب",
@@ -651,6 +661,8 @@
"upload_progress": "جارٍ الرفع {uploaded} / {total}",
"upload_cancel": "إلغاء الرفع",
"upload_failed": "فشل رفع {filename}",
"attachment_too_large": "\"{name}\" أكبر مما يسمح به الخادم ({max} لكل ملف)",
"attachments_total_too_large": "ستتجاوز المرفقات حد الخادم البالغ {max} لكل رسالة",
"drop_files": "أفلت الملفات لإرفاقها",
"show_less": "إظهار أقل",
"send_failed": "فشل إرسال الرسالة",
@@ -726,7 +738,11 @@
"delete_column": "حذف العمود",
"toggle_header_row": "تبديل صف الرأس",
"delete_table": "حذف الجدول",
"pick_size": "اختيار الحجم"
"pick_size": "اختيار الحجم",
"font_size": "حجم الخط",
"font_size_default": "افتراضي",
"background_color": "لون الخلفية",
"remove_background_color": "إزالة لون الخلفية"
},
"send_filing_warning": "تم الإرسال - لكن التنظيف بعد الإرسال فشل، وقد تبقى مسودة قديمة."
},
@@ -765,6 +781,10 @@
"email_sent": "تم إرسال الرسالة بنجاح",
"email_deleted": "تم حذف الرسالة",
"email_archived": "تمت أرشفة الرسالة",
"emails_deleted": "{count, plural, one {تم حذف رسالة واحدة} other {تم حذف # رسائل}}",
"emails_archived": "{count, plural, one {تمت أرشفة رسالة واحدة} other {تمت أرشفة # رسائل}}",
"error_archiving": "فشل أرشفة الرسالة",
"error_updating": "فشل تحديث الرسالة",
"email_starred": "تم تمييز الرسالة بنجمة",
"email_unstarred": "تمت إزالة النجمة عن الرسالة",
"email_marked_read": "تم تمييز الرسالة كمقروءة",
@@ -1144,6 +1164,7 @@
"status_active": "نشط على هذا الجهاز",
"status_inactive": "غير مفعّل على هذا الجهاز",
"status_unsupported": "هذا المتصفح لا يدعم Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "جارٍ العمل…",
"enable": "تفعيل",
"reenable": "إعادة التسجيل",
@@ -1181,7 +1202,9 @@
"enabled": "إشعارات البريد",
"enabled_desc": "إظهار الإشعارات عند وصول رسائل جديدة",
"sound": "صوت الإشعار",
"sound_desc": "تشغيل تنبيه صوتي عند وصول رسائل جديدة"
"sound_desc": "تشغيل تنبيه صوتي عند وصول رسائل جديدة",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "إشعارات التقويم",
@@ -1241,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "مسح البحث عند تبديل المجلدات",
"description": "النقر على مجلد (البريد الوارد، المرسل…) يمسح البحث النشط ويعرض ذلك المجلد. عند الإيقاف، يبقى البحث ويُعاد تشغيله في المجلد الذي تنتقل إليه."
},
"request_read_receipt": {
"label": "طلب إيصالات القراءة افتراضيًا",
"description": "تفعيل طلب إيصال القراءة مسبقًا عند إنشاء رسالة جديدة."
@@ -1324,6 +1351,12 @@
"label": "الرد من العنوان المستلَم إليه",
"description": "عند الرد، الإرسال من العنوان الذي أُرسلت إليه الرسالة أصلًا. يطابق الهويات أولًا؛ وبالنسبة لعمليات التسليم الشاملة للنطاق، يعيد كتابة ترويسة \"من\" إلى العنوان البديل مع الإرسال عبر هويتك الأساسية."
},
"reply_identity_match": {
"label": "مطابقة عنوان الاستلام",
"description": "أي عناوين الاستلام تُعدّ عناوينك. «العنوان المطابق فقط» يختار إحدى هوياتك المضبوطة فقط؛ أما «نفس النطاق» فيعامل أيضًا أي عنوان آخر على نطاقات هوياتك كاسم مستعار شامل ويستبدل به ترويسة المرسل. اختر العنوان المطابق إذا كانت تلك العناوين قوائم توزيع.",
"exact": "العنوان المطابق فقط",
"domain": "أي عنوان على نطاقاتي"
},
"signature_position": {
"label": "موضع التوقيع",
"description": "أين يُدرج توقيعك في الردود وإعادة التوجيه. أعلى النص المقتبس يُقرأ بشكل طبيعي كخاتمة للرد؛ وأسفله يُبقي الرسالة الأصلية متصلة.",
@@ -1753,7 +1786,9 @@
"warnings": {
"end_before_start": "يجب أن يكون تاريخ الانتهاء بعد تاريخ البدء",
"start_in_past": "تاريخ البدء في الماضي",
"empty_body": "نص الرسالة فارغ - سيتلقى المستلمون ردًا فارغًا"
"empty_body": "نص الرسالة فارغ - سيتلقى المستلمون ردًا فارغًا",
"subject_too_long": "الموضوع طويل جدًا بالنسبة للخادم (بحد أقصى {max} بايت).",
"body_too_long": "الرسالة طويلة جدًا بالنسبة للخادم (بحد أقصى {max} بايت بما في ذلك التنسيق)."
}
},
"folders": {
@@ -1770,6 +1805,8 @@
"role_trash": "المهملات",
"role_junk": "البريد المزعج",
"role_archive": "الأرشيف",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "بلا",
"create_folder": "إنشاء مجلد",
"create_subfolder": "إنشاء مجلد فرعي",
@@ -1879,6 +1916,8 @@
"title": "جهات الاتصال",
"description": "استيراد وتصدير جهات اتصالك",
"group_by_letter_label": "التجميع حسب الحرف الأول",
"sort_by_last_name_label": "الترتيب حسب اسم العائلة",
"sort_by_last_name_description": "ترتيب قائمة جهات الاتصال حسب اسم العائلة بحيث يظهر أفراد العائلة معًا",
"group_by_letter_description": "إظهار عناوين أقسام أبجدية في قائمة جهات الاتصال",
"import_label": "استيراد جهات الاتصال",
"import_description": "استيراد جهات الاتصال من ملف vCard (‎.vcf)",
@@ -1970,6 +2009,7 @@
"actions": "الإجراءات",
"add_action": "إضافة إجراء",
"stop_processing": "إيقاف معالجة القواعد التالية",
"include_spam": "نقل الرسائل المصنفة كرسائل مزعجة أيضًا",
"attachment_type_placeholder": "مثال: pdf, doc, jpg",
"value_placeholder_multi": "القيمة (عدة قيم مفصولة بفواصل)",
"condition_fields": {
@@ -2011,6 +2051,8 @@
"copy_to_folder": "اختر المجلد",
"forward_to": "إعادة التوجيه إلى عنوان بريد إلكتروني",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "الاحتفاظ بنسخة",
"forward_limit": "حد إعادة التوجيه لكل رسالة على هذا الخادم: {count}. يتم تخطي عمليات إعادة التوجيه الإضافية.",
"reject_message": "رسالة الرفض",
"reject_placeholder": "تم رفض رسالتك",
"label_name": "اسم الوسم",
@@ -2207,6 +2249,7 @@
"new_subfolder": "مجلد فرعي جديد...",
"new_folder": "مجلد جديد...",
"rename": "إعادة تسمية...",
"share": "مشاركة...",
"import_email": "استيراد ملف ‎.eml أو ‎.zip...",
"empty_folder": "إفراغ المجلد",
"empty_folder_generic": "إفراغ المجلد",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "فشلت إعادة تسمية المجلد",
"toast_error_delete": "فشل حذف المجلد",
"toast_error_delete_has_children": "المجلد يحتوي على مجلدات فرعية. أزلها أولًا.",
"toast_error_delete_has_email": "المجلد ليس فارغًا. أفرغه أولًا."
"toast_error_delete_has_email": "المجلد ليس فارغًا. أفرغه أولًا.",
"delete_with_contents_title": "المجلد ليس فارغًا",
"delete_with_contents_message": "لا يزال المجلد \"{name}\" يحتوي على {count, plural, one {رسالة واحدة} other {# رسائل}}. هل تريد حذف المجلد وكل رسائله؟ لا يمكن التراجع عن هذا.",
"delete_with_contents_confirm": "حذف المجلد والرسائل"
},
"shortcuts": {
"title": "اختصارات لوحة المفاتيح",
@@ -2270,7 +2316,8 @@
"search": "التركيز على البحث",
"help": "إظهار الاختصارات",
"refresh": "تحديث الرسائل",
"select_all": "تحديد الكل"
"select_all": "تحديد الكل",
"global_search": "البحث في كل شيء"
},
"threads": {
"expand_collapse": "توسيع/طي المحادثة"
@@ -2722,7 +2769,10 @@
"any_month": "أي شهر",
"has_email": "لديه بريد إلكتروني",
"has_phone": "لديه هاتف",
"has_photo": "لديه صورة"
"has_photo": "لديه صورة",
"sort_by": "الترتيب حسب",
"sort_first_name": "الاسم الأول",
"sort_last_name": "اسم العائلة"
}
},
"calendar": {
@@ -2758,6 +2808,10 @@
"duplicate": "تكرار",
"today_header": "اليوم",
"tomorrow_header": "غدًا",
"agenda_show_earlier": "عرض الأحداث السابقة",
"agenda_loading": "جارٍ التحميل…",
"agenda_range_start": "الأحداث من {date}",
"agenda_range_end": "الأحداث حتى {date}",
"export_ics": "التصدير كملف ‎.ics",
"copy_title": "نسخ العنوان",
"copy_link": "نسخ رابط الاجتماع",
@@ -2793,6 +2847,14 @@
"color": "اللون"
},
"participants": {
"availability": {
"title": "التوفر",
"free": "متاح",
"busy": "مشغول",
"tentative": "مبدئي",
"unknown": "ليس مستخدمًا على هذا الخادم",
"checking": "جارٍ التحقق…"
},
"title": "المشاركون",
"add": "إضافة مشارك",
"organizer": "المنظّم",
@@ -2867,7 +2929,10 @@
"all_events": "جميع الأحداث",
"cancel": "إلغاء",
"save": "حفظ",
"delete": "حذف"
"delete": "حذف",
"rsvp_title": "الرد على حدث متكرر",
"rsvp_description": "هذا حدث متكرر. على أي الأحداث ينطبق ردك؟",
"respond": "رد"
},
"alerts": {
"title": "التذكيرات",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "عرض أوقات الأحداث في عرض التقويم الشهري",
"show_week_numbers": "إظهار أرقام الأسابيع",
"show_week_numbers_desc": "عرض أرقام الأسابيع في التقويم المصغّر",
"calendar_free_scroll": "تمرير حر",
"calendar_free_scroll_desc": "التمرير باستمرار عبر الأشهر والأسابيع والأيام بدلاً من فترة واحدة في كل مرة",
"enable_tasks": "تفعيل المهام",
"enable_tasks_desc": "إظهار عرض للمهام في التقويم لإدارة قوائم المهام",
"show_tasks_on_calendar": "إظهار المهام في التقويم",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "تأخير ثانية واحدة",
"hover_preview_delay_2s": "تأخير ثانيتين",
"hover_preview_off": "معطّل",
"organizer_identity": "تنظيم الدعوات باسم",
"organizer_identity_desc": "عنوان التقويم المستخدم كمنظّم عند دعوة الآخرين إلى الأحداث.",
"organizer_identity_failed": "فشل تغيير عنوان المنظّم",
"show_birthday_calendar": "تقويم أعياد ميلاد جهات الاتصال",
"show_birthday_calendar_desc": "إظهار تقويم افتراضي بأعياد ميلاد جهات اتصالك"
},
@@ -2973,6 +3043,9 @@
"alert_now": "يبدأ الآن",
"alert_in_minutes": "خلال {count} دقيقة",
"invitation_sent": "تم إرسال الدعوات",
"invitations_denied_title": "تعذر إرسال الدعوات",
"invitations_denied": "رفض الخادم إرسال الدعوات: {reason}. هل تريد حفظ الحدث دون إرسالها؟",
"save_without_invitations": "حفظ دون دعوات",
"rsvp_updated": "تم تحديث الرد",
"rsvp_error": "فشل تحديث الرد",
"event_duplicated": "تم تكرار الحدث",
@@ -3002,7 +3075,7 @@
"supported_formats": "يدعم ملفات iCalendar (‎.ics)",
"url_description": "أدخل رابط تغذية iCalendar (‎.ics) خارجية لاستيراد الأحداث.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "يدعم روابط CalDAV وiCalendar (‎.ics)",
"url_hint": "يدعم روابط iCalendar (‎.ics)",
"fetch": "جلب",
"invalid_url": "يرجى إدخال رابط صالح",
"url_fetch_failed": "فشل جلب التقويم من الرابط",
@@ -3149,6 +3222,25 @@
"overdue": "متأخرة"
}
},
"calendar_event_notifications": {
"someone": "شخص ما",
"invited": "دعاك {name} إلى \"{title}\"",
"updated": "حدّث {name} \"{title}\"",
"cancelled": "ألغى {name} \"{title}\"",
"untitled": "حدث بدون عنوان"
},
"share_notifications": {
"someone": "شخص ما",
"shared": "شارك {name} معك {kind} \"{object}\"",
"changed": "غيّر {name} صلاحيتك على {kind} \"{object}\"",
"revoked": "أزال {name} صلاحيتك على {kind} \"{object}\"",
"object": {
"folder": "المجلد",
"calendar": "التقويم",
"address_book": "دفتر العناوين",
"files": "مجلد الملفات"
}
},
"sharing": {
"title": "مشاركة \"{name}\"",
"description": "امنح الوصول لمستخدمين أو مجموعات أخرى على هذا الخادم. تُطبَّق التغييرات فورًا.",
@@ -3187,6 +3279,8 @@
"has_attachment": "المرفقات",
"date_after": "بعد",
"date_before": "قبل",
"size_min": "أكبر من (كيلوبايت)",
"size_max": "أصغر من (كيلوبايت)",
"starred": "مميز بنجمة",
"unread": "غير مقروءة",
"read": "مقروءة",
@@ -3228,6 +3322,10 @@
"hint": "انقر على أي رسالة على اليسار للبدء، أو خذ الجولة أدناه."
},
"files": {
"office_edit": "تحرير المستند",
"office_loading": "جارٍ فتح محرر المستندات…",
"office_error": "تعذر فتح محرر المستندات.",
"office_close": "إغلاق المحرر",
"open_in_new_tab": "فتح في علامة تبويب جديدة",
"title": "الملفات",
"search_placeholder": "بحث في الملفات...",
@@ -3240,6 +3338,8 @@
"rename": "إعادة تسمية",
"rename_title": "إعادة تسمية",
"new_name": "الاسم الجديد",
"name_forbidden_chars": "لا يسمح الخادم بهذه الأحرف في الأسماء: {chars}",
"name_reserved": "هذا الاسم محجوز على الخادم.",
"delete": "حذف",
"delete_confirm_title": "حذف المورد",
"delete_confirm_message": "هل أنت متأكد أنك تريد حذف \"{name}\"؟ لا يمكن التراجع عن هذا.",
@@ -3501,5 +3601,45 @@
"copy_event": "نسخ رابط الحدث",
"copied": "تم نسخ الرابط",
"copy_failed": "تعذّر نسخ الرابط"
},
"plugins": {
"oauth_callback": {
"completing": "جارٍ إكمال التفويض…",
"completing_message": "يرجى الانتظار بينما يتم إنشاء الاتصال.",
"received_title": "تم استلام التفويض",
"received_message": "يمكنك العودة إلى علامة التبويب التي بدأت فيها الاتصال.",
"failed_title": "فشل التفويض",
"denied": "تم رفض التفويض.",
"failed_detail": "فشل التفويض: {error}",
"missing_params": "رمز التفويض أو الحالة مفقود.",
"delivery_failed": "تعذّر إرسال نتيجة التفويض إلى تبويب التطبيق. يرجى إغلاق هذه النافذة والمحاولة مرة أخرى."
}
},
"global_search": {
"title": "البحث في كل شيء",
"placeholder": "ابحث في البريد وجهات الاتصال والتقويم والملفات...",
"scope_label": "البحث في",
"account_label": "الحساب",
"scope_all": "الكل",
"scope_mail": "البريد",
"scope_contacts": "جهات الاتصال",
"scope_calendar": "التقويم",
"scope_files": "الملفات",
"all_accounts": "جميع الحسابات",
"show_all": "عرض كل النتائج",
"no_results": "لا توجد نتائج لـ “{query}”",
"server_hint": "الكلمات الكاملة تبحث في الخادم؛ أجزاء الكلمات تطابق العناصر المحمّلة بالفعل",
"searching": "جارٍ البحث...",
"account_error": "تعذر البحث في {account}",
"recent_searches": "عمليات البحث الأخيرة",
"recurring": "متكرر",
"no_subject": "(no subject)",
"empty_hint": "ابحث في البريد وجهات الاتصال والأحداث والملفات في جميع الحسابات. جرّب from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "مسطح حسب التاريخ",
"grouped": "مجمّع",
"load_more": "تحميل المزيد",
"preview_empty": "اختر نتيجة لمعاينتها",
"open_full": "فتح",
"view_label": "العرض"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "i {count} més",
"draft": "Esborrany",
"starred": "Destacat",
"pinned": "Fixat",
"replied": "Respost",
"forwarded": "Reenviat",
"has_attachment": "Té adjunt",
"conversations_count": "{count} de {total} converses",
"conversations_count_plus": "{count}+ converses",
"conversations_count_simple": "{count} converses",
@@ -292,6 +296,7 @@
"forward": "Reenvia",
"delete": "Suprimeix",
"archive": "Arxiva",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Marca amb estrella",
"unstar": "Treu l'estrella",
"mark_unread": "Marca com a no llegit",
@@ -303,12 +308,10 @@
"move": "Mou",
"print": "Imprimeix",
"view_source": "Mostra el codi font",
"fullscreen": "Pantalla completa",
"exit_fullscreen": "Surt de la pantalla completa",
"export_email": "Exporta com a .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Reenvia com a fitxer adjunt",
"import_email": "Importa .eml o .zip",
"keyboard_shortcuts": "Dreceres de teclat (?)",
"email_source": "Codi font del correu",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Confirmació de lectura sol·licitada (feu clic per desactivar-la)",
"read_receipt_off": "Sol·licita una confirmació de lectura",
"dsn_on": "Notificació de lliurament sol·licitada (fes clic per desactivar)",
"dsn_off": "Sol·licita una notificació de lliurament",
"format_plain_text": "Canvia a text sense format",
"format_rich_text": "Canvia a text enriquit (HTML)",
"require_tls_on": "Lliurament xifrat obligatori (fes clic per desactivar)",
"require_tls_off": "Exigeix lliurament xifrat (TLS)",
"new_message": "Missatge nou",
"signature_edit_hint": "Feu doble clic per editar la signatura",
"reply": "Respon",
@@ -634,11 +643,12 @@
"from_override": {
"toggle_off": "Sobreescriu",
"toggle_on": "Cancel·la la sobreescriptura",
"toggle_tooltip": "Editeu lliurement el nom i l'adreça del remitent. El correu es continua enviant a través de la vostra identitat: només canvia la capçalera «De» visible.",
"toggle_tooltip": "Editeu lliurement el nom i l'adreça del remitent. El correu es continua enviant a través de la vostra identitat.",
"name_label": "Nom del remitent",
"name_placeholder": "Nom",
"email_label": "Adreça electrònica del remitent",
"email_placeholder": "alias@exemple.com"
"email_placeholder": "alias@exemple.com",
"envelope_notice": "Si el vostre servidor no accepta aquesta adreça com a remitent del sobre, s'hi fa servir {identity}, i els destinataris la poden veure a la capçalera Return-Path."
},
"use_template": "Plantilla",
"save_as_template": "Desa com a plantilla",
@@ -651,6 +661,8 @@
"upload_progress": "Pujant {uploaded} / {total}",
"upload_cancel": "Cancel·la la pujada",
"upload_failed": "No s'ha pogut pujar {filename}",
"attachment_too_large": "«{name}» supera la mida que permet el servidor ({max} per fitxer)",
"attachments_total_too_large": "Els adjunts superarien el límit del servidor de {max} per missatge",
"drop_files": "Deixeu anar els fitxers per adjuntar-los",
"show_less": "Mostra'n menys",
"send_failed": "No s'ha pogut enviar el correu",
@@ -726,7 +738,11 @@
"delete_column": "Elimina la columna",
"toggle_header_row": "Commuta la fila de capçalera",
"delete_table": "Elimina la taula",
"pick_size": "Tria la mida"
"pick_size": "Tria la mida",
"font_size": "Mida de la lletra",
"font_size_default": "Per defecte",
"background_color": "Color de fons",
"remove_background_color": "Elimina el color de fons"
},
"send_filing_warning": "Enviat, però la neteja posterior a l'enviament ha fallat; és possible que quedi un esborrany obsolet."
},
@@ -765,6 +781,10 @@
"email_sent": "Correu enviat correctament",
"email_deleted": "Correu suprimit",
"email_archived": "Correu arxivat",
"emails_deleted": "{count, plural, one {# correu suprimit} other {# correus suprimits}}",
"emails_archived": "{count, plural, one {# correu arxivat} other {# correus arxivats}}",
"error_archiving": "No s'ha pogut arxivar el correu",
"error_updating": "No s'ha pogut actualitzar el correu",
"email_starred": "Correu destacat",
"email_unstarred": "S'ha tret l'estrella del correu",
"email_marked_read": "Correu marcat com a llegit",
@@ -1144,6 +1164,7 @@
"status_active": "Actiu en aquest dispositiu",
"status_inactive": "No activat en aquest dispositiu",
"status_unsupported": "Aquest navegador no admet Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "Treballant…",
"enable": "Activa",
"reenable": "Torna a registrar",
@@ -1181,7 +1202,9 @@
"enabled": "Notificacions de correu",
"enabled_desc": "Mostra notificacions quan arribin correus nous",
"sound": "So de notificació",
"sound_desc": "Reprodueix un avís sonor quan arribin correus nous"
"sound_desc": "Reprodueix un avís sonor quan arribin correus nous",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Notificacions de calendari",
@@ -1241,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Esborra la cerca en canviar de carpeta",
"description": "En clicar una carpeta (Safata d'entrada, Enviats…) s'esborra la cerca activa i es mostra aquesta carpeta. Si està desactivat, la cerca es manté i es torna a executar a la carpeta triada."
},
"request_read_receipt": {
"label": "Sol·licita confirmacions de lectura per defecte",
"description": "Activa per defecte la sol·licitud de confirmació de lectura en redactar un missatge nou."
@@ -1324,6 +1351,12 @@
"label": "Respon des de l'adreça de recepció",
"description": "En respondre, envia des de l'adreça a la qual es va enviar originalment el missatge. Primer intenta coincidir amb les identitats; per als lliuraments de captura general de domini, reescriu la capçalera «De» amb l'àlies mentre envia a través de la identitat principal."
},
"reply_identity_match": {
"label": "Coincidència de l'adreça de recepció",
"description": "Quines adreces de recepció compten com a teves. «Només l'adreça exacta» tria únicament una de les teves identitats configurades; «mateix domini» també tracta qualsevol altra adreça dels teus dominis d'identitat com un àlies catch-all i hi reescriu la capçalera De. Tria l'adreça exacta si aquestes adreces són llistes de distribució.",
"exact": "Només l'adreça exacta",
"domain": "Qualsevol adreça dels meus dominis"
},
"signature_position": {
"label": "Posició de la signatura",
"description": "On inserir la signatura a les respostes i reenviaments. Abans del text citat es llegeix de manera natural com a tancament de la resposta; després manté el missatge original contigu.",
@@ -1753,7 +1786,9 @@
"warnings": {
"end_before_start": "La data de finalització ha de ser posterior a la data d'inici",
"start_in_past": "La data d'inici és al passat",
"empty_body": "El cos del missatge està buit - els destinataris rebran una resposta en blanc"
"empty_body": "El cos del missatge està buit - els destinataris rebran una resposta en blanc",
"subject_too_long": "L'assumpte és massa llarg per al servidor (com a màxim {max} bytes).",
"body_too_long": "El missatge és massa llarg per al servidor (com a màxim {max} bytes, amb el format inclòs)."
}
},
"folders": {
@@ -1770,6 +1805,8 @@
"role_trash": "Paperera",
"role_junk": "Correu brossa",
"role_archive": "Arxiu",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Cap",
"create_folder": "Crea una carpeta",
"create_subfolder": "Crea una subcarpeta",
@@ -1879,6 +1916,8 @@
"title": "Contactes",
"description": "Importeu i exporteu els contactes",
"group_by_letter_label": "Agrupa per lletra inicial",
"sort_by_last_name_label": "Ordena per cognom",
"sort_by_last_name_description": "Ordena la llista de contactes per cognom perquè els membres d'una família apareguin junts",
"group_by_letter_description": "Mostra capçaleres de secció alfabètiques a la llista de contactes",
"import_label": "Importa contactes",
"import_description": "Importeu contactes des d'un fitxer vCard (.vcf)",
@@ -1970,6 +2009,7 @@
"actions": "Accions",
"add_action": "Afegeix una acció",
"stop_processing": "Atura el processament de les regles següents",
"include_spam": "Mou també els missatges marcats com a correu brossa",
"attachment_type_placeholder": "p. ex. pdf, doc, jpg",
"value_placeholder_multi": "Valor (múltiples separats per comes)",
"condition_fields": {
@@ -2011,6 +2051,8 @@
"copy_to_folder": "Selecciona una carpeta",
"forward_to": "Reenvia a l'adreça electrònica",
"forward_placeholder": "correu@exemple.com",
"forward_keep_copy": "Conserva'n una còpia",
"forward_limit": "Límit de reenviaments per missatge en aquest servidor: {count}. Els reenviaments addicionals s'ometen.",
"reject_message": "Missatge de rebuig",
"reject_placeholder": "El vostre correu ha estat rebutjat",
"label_name": "Nom de l'etiqueta",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Subcarpeta nova...",
"new_folder": "Carpeta nova...",
"rename": "Canvia el nom...",
"share": "Comparteix...",
"import_email": "Importa .eml o .zip...",
"empty_folder": "Buida la carpeta",
"empty_folder_generic": "Buida la carpeta",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "No s'ha pogut canviar el nom de la carpeta",
"toast_error_delete": "No s'ha pogut suprimir la carpeta",
"toast_error_delete_has_children": "La carpeta té subcarpetes. Elimineu-les primer.",
"toast_error_delete_has_email": "La carpeta no és buida. Buideu-la primer."
"toast_error_delete_has_email": "La carpeta no és buida. Buideu-la primer.",
"delete_with_contents_title": "La carpeta no està buida",
"delete_with_contents_message": "La carpeta \"{name}\" encara conté {count, plural, one {1 missatge} other {# missatges}}. Vols suprimir la carpeta i tots els seus missatges? No es pot desfer.",
"delete_with_contents_confirm": "Suprimeix la carpeta i els missatges"
},
"shortcuts": {
"title": "Dreceres de teclat",
@@ -2270,7 +2316,8 @@
"search": "Enfoca la cerca",
"help": "Mostra les dreceres",
"refresh": "Actualitza els correus",
"select_all": "Selecciona-ho tot"
"select_all": "Selecciona-ho tot",
"global_search": "Cerca-ho tot"
},
"threads": {
"expand_collapse": "Desplega/replega el fil"
@@ -2722,7 +2769,10 @@
"any_month": "Qualsevol mes",
"has_email": "Té correu electrònic",
"has_phone": "Té telèfon",
"has_photo": "Té foto"
"has_photo": "Té foto",
"sort_by": "Ordena per",
"sort_first_name": "Nom",
"sort_last_name": "Cognom"
}
},
"calendar": {
@@ -2758,6 +2808,10 @@
"duplicate": "Duplica",
"today_header": "Avui",
"tomorrow_header": "Demà",
"agenda_show_earlier": "Mostra esdeveniments anteriors",
"agenda_loading": "Carregant…",
"agenda_range_start": "Esdeveniments des del {date}",
"agenda_range_end": "Esdeveniments fins al {date}",
"export_ics": "Exporta com a .ics",
"copy_title": "Copia el títol",
"copy_link": "Copia l'enllaç de la reunió",
@@ -2793,6 +2847,14 @@
"color": "Color"
},
"participants": {
"availability": {
"title": "Disponibilitat",
"free": "Disponible",
"busy": "Ocupat",
"tentative": "Provisional",
"unknown": "No és un usuari d'aquest servidor",
"checking": "Comprovant…"
},
"title": "Participants",
"add": "Afegeix un participant",
"organizer": "Organitzador",
@@ -2867,7 +2929,10 @@
"all_events": "Tots els esdeveniments",
"cancel": "Cancel·la",
"save": "Desa",
"delete": "Suprimeix"
"delete": "Suprimeix",
"rsvp_title": "Respon a l'esdeveniment periòdic",
"rsvp_description": "Aquest és un esdeveniment periòdic. A quins esdeveniments s'aplica la vostra resposta?",
"respond": "Respon"
},
"alerts": {
"title": "Recordatoris",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Mostra l'hora dels esdeveniments a la vista mensual del calendari. En pantalles petites, això mostra entrades completes d'esdeveniment en lloc de punts.",
"show_week_numbers": "Mostra els números de setmana",
"show_week_numbers_desc": "Mostra els números de setmana al minicalendari",
"calendar_free_scroll": "Desplaçament lliure",
"calendar_free_scroll_desc": "Desplaça't de manera contínua per mesos, setmanes i dies en lloc d'un període cada vegada",
"enable_tasks": "Activa les tasques",
"enable_tasks_desc": "Mostra una vista de tasques al calendari per gestionar pendents",
"show_tasks_on_calendar": "Mostra les tasques al calendari",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Retard d'1 segon",
"hover_preview_delay_2s": "Retard de 2 segons",
"hover_preview_off": "Desactivada",
"organizer_identity": "Organitza les invitacions com a",
"organizer_identity_desc": "L'adreça de calendari usada com a organitzador quan convides gent als esdeveniments.",
"organizer_identity_failed": "No s'ha pogut canviar l'adreça de l'organitzador",
"show_birthday_calendar": "Calendari d'aniversaris dels contactes",
"show_birthday_calendar_desc": "Mostra un calendari virtual amb els aniversaris dels vostres contactes"
},
@@ -2973,6 +3043,9 @@
"alert_now": "Comença ara",
"alert_in_minutes": "D'aquí a {count} min",
"invitation_sent": "Invitacions enviades",
"invitations_denied_title": "No es poden enviar les invitacions",
"invitations_denied": "El servidor ha refusat enviar les invitacions: {reason}. Voleu desar l'esdeveniment sense enviar-les?",
"save_without_invitations": "Desa sense invitacions",
"rsvp_updated": "Resposta actualitzada",
"rsvp_error": "No s'ha pogut actualitzar la resposta",
"event_duplicated": "Esdeveniment duplicat",
@@ -3002,7 +3075,7 @@
"supported_formats": "Admet fitxers iCalendar (.ics)",
"url_description": "Introduïu l'URL d'un canal iCalendar (.ics) extern per importar-ne els esdeveniments.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Admet URL de CalDAV i iCalendar (.ics)",
"url_hint": "Admet URL d’iCalendar (.ics)",
"fetch": "Obtén",
"invalid_url": "Introduïu un URL vàlid",
"url_fetch_failed": "No s'ha pogut obtenir el calendari des de l'URL",
@@ -3149,6 +3222,25 @@
"overdue": "Vençuda"
}
},
"calendar_event_notifications": {
"someone": "Algú",
"invited": "{name} t'ha convidat a \"{title}\"",
"updated": "{name} ha actualitzat \"{title}\"",
"cancelled": "{name} ha cancel·lat \"{title}\"",
"untitled": "Esdeveniment sense títol"
},
"share_notifications": {
"someone": "Algú",
"shared": "{name} ha compartit amb tu {kind} \"{object}\"",
"changed": "{name} ha canviat el teu accés a {kind} \"{object}\"",
"revoked": "{name} ha retirat el teu accés a {kind} \"{object}\"",
"object": {
"folder": "la carpeta",
"calendar": "el calendari",
"address_book": "la llibreta d'adreces",
"files": "la carpeta de fitxers"
}
},
"sharing": {
"title": "Comparteix «{name}»",
"description": "Concediu accés a altres usuaris o grups d'aquest servidor. Els canvis s'apliquen immediatament.",
@@ -3187,6 +3279,8 @@
"has_attachment": "Fitxers adjunts",
"date_after": "Després de",
"date_before": "Abans de",
"size_min": "Més gran que (KB)",
"size_max": "Més petit que (KB)",
"starred": "Destacat",
"unread": "No llegit",
"read": "Llegit",
@@ -3228,6 +3322,10 @@
"hint": "Feu clic a qualsevol correu de l'esquerra per començar, o feu la visita guiada de sota."
},
"files": {
"office_edit": "Edita el document",
"office_loading": "S'està obrint l'editor de documents…",
"office_error": "No s'ha pogut obrir l'editor de documents.",
"office_close": "Tanca l'editor",
"open_in_new_tab": "Obre en una pestanya nova",
"title": "Fitxers",
"search_placeholder": "Cerca fitxers...",
@@ -3240,6 +3338,8 @@
"rename": "Canvia el nom",
"rename_title": "Canvia el nom",
"new_name": "Nom nou",
"name_forbidden_chars": "El servidor no permet aquests caràcters als noms: {chars}",
"name_reserved": "Aquest nom està reservat al servidor.",
"delete": "Suprimeix",
"delete_confirm_title": "Suprimeix el recurs",
"delete_confirm_message": "Segur que voleu suprimir «{name}»? Això no es pot desfer.",
@@ -3501,5 +3601,45 @@
"copy_event": "Copia l'enllaç de l'esdeveniment",
"copied": "Enllaç copiat",
"copy_failed": "No s'ha pogut copiar l'enllaç"
},
"plugins": {
"oauth_callback": {
"completing": "Completant l'autorització…",
"completing_message": "Espereu mentre s'estableix la connexió.",
"received_title": "S'ha rebut l'autorització",
"received_message": "Podeu tornar a la pestanya on heu iniciat la connexió.",
"failed_title": "Ha fallat l'autorització",
"denied": "Sha denegat lautorització.",
"failed_detail": "Ha fallat lautorització: {error}",
"missing_params": "Falta el codi dautorització o lestat.",
"delivery_failed": "No sha pogut lliurar el resultat de lautorització a la pestanya de laplicació. Tanqueu aquesta finestra i torneu-ho a provar."
}
},
"global_search": {
"title": "Cerca-ho tot",
"placeholder": "Cerca al correu, contactes, calendari, fitxers...",
"scope_label": "Cerca a",
"account_label": "Compte",
"scope_all": "Tot",
"scope_mail": "Correu",
"scope_contacts": "Contactes",
"scope_calendar": "Calendari",
"scope_files": "Fitxers",
"all_accounts": "Tots els comptes",
"show_all": "Mostra tots els resultats",
"no_results": "Cap resultat per a “{query}”",
"server_hint": "Les paraules completes cerquen al servidor; les parcials coincideixen amb els elements ja carregats",
"searching": "Cercant...",
"account_error": "No s'ha pogut cercar a {account}",
"recent_searches": "Cerques recents",
"recurring": "Periòdic",
"no_subject": "(no subject)",
"empty_hint": "Cerca correu, contactes, esdeveniments i fitxers a tots els comptes. Prova from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Pla per data",
"grouped": "Agrupat",
"load_more": "Carrega'n més",
"preview_empty": "Seleccioneu un resultat per previsualitzar-lo",
"open_full": "Obre",
"view_label": "Vista"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "a {count} dalších",
"draft": "Koncept",
"starred": "S hvězdičkou",
"pinned": "Připnuto",
"replied": "Zodpovězeno",
"forwarded": "Přeposláno",
"has_attachment": "Obsahuje přílohu",
"conversations_count": "{count} z {total} konverzací",
"conversations_count_plus": "Více než {count} konverzací",
"conversations_count_simple": "{count} konverzací",
@@ -292,6 +296,7 @@
"forward": "Přeposlat",
"delete": "Odstranit",
"archive": "Archivovat",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Označit hvězdičkou",
"unstar": "Odebrat hvězdičku",
"mark_unread": "Označit jako nepřečtené",
@@ -303,12 +308,10 @@
"move": "Přesunout",
"print": "Tisk",
"view_source": "Zobrazit zdrojový kód",
"fullscreen": "Celá obrazovka",
"exit_fullscreen": "Ukončit režim celé obrazovky",
"export_email": "Exportovat jako .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Přeposlat jako přílohu",
"import_email": "Importovat .eml nebo .zip",
"keyboard_shortcuts": "Klávesové zkratky (?)",
"email_source": "Zdrojový kód zprávy",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Vyžádáno potvrzení o přečtení (kliknutím vypnete)",
"read_receipt_off": "Vyžádat potvrzení o přečtení",
"dsn_on": "Oznámení o doručení vyžádáno (kliknutím vypnete)",
"dsn_off": "Vyžádat oznámení o doručení",
"format_plain_text": "Přepnout na prostý text",
"format_rich_text": "Přepnout na formátovaný text (HTML)",
"require_tls_on": "Šifrované doručení vyžadováno (kliknutím vypnete)",
"require_tls_off": "Vyžadovat šifrované doručení (TLS)",
"new_message": "Nová zpráva",
"signature_edit_hint": "Dvojklikem upravíte podpis",
"reply": "Odpovědět",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Přepsat",
"toggle_on": "Zrušit přepsání",
"toggle_tooltip": "Volně upravujte jméno a adresu odesílatele. Pošta se stále odesílá přes vaši identitu - mění se pouze viditelné záhlaví Od.",
"toggle_tooltip": "Volně upravujte jméno a adresu odesílatele. Pošta se stále odesílá přes vaši identitu.",
"name_label": "Jméno odesílatele",
"name_placeholder": "Jméno",
"email_label": "E-mailová adresa odesílatele",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Pokud váš server tuto adresu jako odesílatele obálky nepřijme, použije se místo ní {identity} a příjemci ji uvidí v záhlaví Return-Path."
},
"use_template": "Šablona",
"save_as_template": "Uložit jako šablonu",
@@ -648,6 +658,8 @@
"upload_progress": "Nahrávání {uploaded} / {total}",
"upload_cancel": "Zrušit nahrávání",
"upload_failed": "Soubor {filename} se nepodařilo nahrát",
"attachment_too_large": "„{name}“ je větší, než server povoluje ({max} na soubor)",
"attachments_total_too_large": "Přílohy by překročily limit serveru {max} na zprávu",
"drop_files": "Přetáhněte soubory sem a připojte je",
"show_less": "Zobrazit méně",
"send_failed": "Odeslání zprávy selhalo",
@@ -726,7 +738,11 @@
"delete_column": "Odstranit sloupec",
"toggle_header_row": "Přepnout řádek záhlaví",
"delete_table": "Odstranit tabulku",
"pick_size": "Vybrat velikost"
"pick_size": "Vybrat velikost",
"font_size": "Velikost písma",
"font_size_default": "Výchozí",
"background_color": "Barva pozadí",
"remove_background_color": "Odebrat barvu pozadí"
},
"send_filing_warning": "Odesláno - ale následný úklid selhal, může zůstat zastaralý koncept."
},
@@ -765,6 +781,10 @@
"email_sent": "Zpráva byla úspěšně odeslána",
"email_deleted": "Zpráva byla odstraněna",
"email_archived": "Zpráva byla archivována",
"emails_deleted": "{count, plural, one {# e-mail smazán} few {# e-maily smazány} other {# e-mailů smazáno}}",
"emails_archived": "{count, plural, one {# e-mail archivován} few {# e-maily archivovány} other {# e-mailů archivováno}}",
"error_archiving": "E-mail se nepodařilo archivovat",
"error_updating": "E-mail se nepodařilo aktualizovat",
"email_starred": "Zpráva byla označena hvězdičkou",
"email_unstarred": "Hvězdička byla odebrána",
"email_marked_read": "Zpráva byla označena jako přečtená",
@@ -1143,7 +1163,9 @@
"enabled": "E-mailová oznámení",
"enabled_desc": "Zobrazovat oznámení při příchodu nových e-mailů",
"sound": "Zvuk oznámení",
"sound_desc": "Přehrát zvukové upozornění při příchodu nových e-mailů"
"sound_desc": "Přehrát zvukové upozornění při příchodu nových e-mailů",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Oznámení kalendáře",
@@ -1172,6 +1194,7 @@
"status_busy": "Pracuji…",
"status_inactive": "Není povoleno na tomto zařízení",
"status_unsupported": "Tento prohlížeč nepodporuje Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Oznámení na pozadí",
"devices_title": "Registrovaná zařízení",
"devices_desc": "Všechna zařízení registrovaná pro příjem oznamení na pozadí u tohoto účtu. Odvolání okamžitě zastaví jejich příjem.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Vymazat hledání při přepnutí složky",
"description": "Kliknutí na složku (Doručená pošta, Odeslaná…) vymaže aktivní hledání a zobrazí složku. Když je vypnuto, hledání zůstává a spustí se znovu ve zvolené složce."
},
"request_read_receipt": {
"label": "Standardně vyžadovat potvrzení o přečtení",
"description": "Při psaní nové zprávy předem zapnout žádost o potvrzení o přečtení."
@@ -1321,6 +1348,12 @@
"label": "Automaticky vybírat adresu pro odpověď",
"description": "Při odpovídání automaticky přepnout adresu odesílatele na identitu, která původně obdržela zprávu"
},
"reply_identity_match": {
"label": "Porovnávání přijímací adresy",
"description": "Které přijímací adresy se počítají jako vaše. „Pouze přesná adresa“ vybere jen jednu z vašich nastavených identit; „stejná doména“ navíc považuje jakoukoli jinou adresu na doménách vašich identit za catch-all alias a přepíše na ni hlavičku Od. Pokud jsou tyto adresy distribuční seznamy, zvolte přesnou adresu.",
"exact": "Pouze přesná adresa",
"domain": "Jakákoli adresa na mých doménách"
},
"signature_position": {
"label": "Pozice podpisu",
"description": "Kam vložit podpis v odpovědích a přeposláních. Nad citovaným textem působí přirozeně jako zakončení odpovědi; pod ním zachovává původní zprávu vcelku.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "Datum ukončení musí být po datu zahájení",
"start_in_past": "Datum zahájení je v minulosti",
"empty_body": "Tělo zprávy je prázdné - příjemci obdrží prázdnou odpověď"
"empty_body": "Tělo zprávy je prázdné - příjemci obdrží prázdnou odpověď",
"subject_too_long": "Předmět je pro server příliš dlouhý (nejvýše {max} bajtů).",
"body_too_long": "Zpráva je pro server příliš dlouhá (nejvýše {max} bajtů včetně formátování)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Koš",
"role_junk": "Spam / Nevyžádaná pošta",
"role_archive": "Archiv",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Žádná",
"create_folder": "Vytvořit složku",
"create_subfolder": "Vytvořit podsložku",
@@ -1872,6 +1909,8 @@
"title": "Kontakty",
"description": "Import a export kontaktů",
"group_by_letter_label": "Seskupit podle prvního písmena",
"sort_by_last_name_label": "Řadit podle příjmení",
"sort_by_last_name_description": "Seřadit seznam kontaktů podle příjmení, aby členové rodiny byli pohromadě",
"group_by_letter_description": "Zobrazit záhlaví sekcí podle abecedy v seznamu kontaktů",
"import_label": "Importovat kontakty",
"import_description": "Importovat kontakty ze souboru vCard (.vcf)",
@@ -1963,6 +2002,7 @@
"actions": "Akce",
"add_action": "Přidat akci",
"stop_processing": "Zastavit zpracování dalších pravidel",
"include_spam": "Přesouvat i zprávy označené jako spam",
"attachment_type_placeholder": "např. pdf, doc, jpg",
"value_placeholder_multi": "Hodnota (více oddělených čárkami)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Vyberte složku",
"forward_to": "Přeposlat na e-mailovou adresu",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Ponechat kopii",
"forward_limit": "Limit přeposlání na zprávu na tomto serveru: {count}. Další přeposlání se přeskočí.",
"reject_message": "Zpráva o odmítnutí",
"reject_placeholder": "Váš e-mail byl odmítnut",
"label_name": "Název štítku",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Nová podsložka...",
"new_folder": "Nová složka...",
"rename": "Přejmenovat...",
"share": "Sdílet...",
"import_email": "Importovat .eml nebo .zip...",
"empty_folder": "Vyprázdnit složku",
"empty_folder_generic": "Vyprázdnit složku",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "Nepodařilo se přejmenovat složku",
"toast_error_delete": "Nepodařilo se smazat složku",
"toast_error_delete_has_children": "Složka obsahuje podsložky. Nejprve je odstraňte.",
"toast_error_delete_has_email": "Složka není prázdná. Nejprve ji vyprázdněte."
"toast_error_delete_has_email": "Složka není prázdná. Nejprve ji vyprázdněte.",
"delete_with_contents_title": "Složka není prázdná",
"delete_with_contents_message": "Složka „{name}“ stále obsahuje {count, plural, one {1 zprávu} few {# zprávy} other {# zpráv}}. Smazat složku i všechny její zprávy? Tuto akci nelze vrátit.",
"delete_with_contents_confirm": "Smazat složku i zprávy"
},
"shortcuts": {
"title": "Klávesové zkratky",
@@ -2270,7 +2316,8 @@
"search": "Přejít do hledání",
"help": "Zobrazit zkratky",
"refresh": "Obnovit zprávy",
"select_all": "Vybrat vše"
"select_all": "Vybrat vše",
"global_search": "Hledat ve všem"
},
"threads": {
"expand_collapse": "Rozbalit/sbalit vlákno"
@@ -2721,7 +2768,10 @@
"any_month": "Kterýkoli měsíc",
"has_email": "Má e-mail",
"has_phone": "Má telefon",
"has_photo": "Má fotku"
"has_photo": "Má fotku",
"sort_by": "Řadit podle",
"sort_first_name": "Jméno",
"sort_last_name": "Příjmení"
},
"open_categories": "Otevřít kategorie"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Duplikovat",
"today_header": "Dnes",
"tomorrow_header": "Zítra",
"agenda_show_earlier": "Zobrazit dřívější události",
"agenda_loading": "Načítání…",
"agenda_range_start": "Události od {date}",
"agenda_range_end": "Události do {date}",
"export_ics": "Exportovat jako .ics",
"copy_title": "Kopírovat název",
"copy_link": "Kopírovat odkaz na schůzku",
@@ -2793,6 +2847,14 @@
"color": "Barva"
},
"participants": {
"availability": {
"title": "Dostupnost",
"free": "Volný",
"busy": "Zaneprázdněn",
"tentative": "Předběžně",
"unknown": "Není uživatelem tohoto serveru",
"checking": "Ověřuje se…"
},
"title": "Účastníci",
"add": "Přidat účastníka",
"organizer": "Organizátor",
@@ -2867,7 +2929,10 @@
"all_events": "Všechny události",
"cancel": "Zrušit",
"save": "Uložit",
"delete": "Odstranit"
"delete": "Odstranit",
"rsvp_title": "Odpovědět na opakující se událost",
"rsvp_description": "Toto je opakující se událost. Na které události se vaše odpověď vztahuje?",
"respond": "Odpovědět"
},
"alerts": {
"title": "Připomenutí",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Zobrazovat časy událostí v měsíčním zobrazení kalendáře",
"show_week_numbers": "Zobrazit čísla týdnů",
"show_week_numbers_desc": "Zobrazovat čísla týdnů v minikalendáři",
"calendar_free_scroll": "Volné posouvání",
"calendar_free_scroll_desc": "Plynule procházet měsíce, týdny a dny místo jednoho období najednou",
"enable_tasks": "Povolit úkoly",
"enable_tasks_desc": "Zobrazovat zobrazení úkolů v kalendáři pro správu úkolů",
"show_tasks_on_calendar": "Zobrazit úkoly v kalendáři",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Zpoždění 1 sekunda",
"hover_preview_delay_2s": "Zpoždění 2 sekundy",
"hover_preview_off": "Vypnuto",
"organizer_identity": "Organizovat pozvánky jako",
"organizer_identity_desc": "Adresa kalendáře použitá jako organizátor, když zvete lidi na události.",
"organizer_identity_failed": "Nepodařilo se změnit adresu organizátora",
"show_birthday_calendar": "Kalendář narozenin",
"show_birthday_calendar_desc": "Zobrazovat virtuální kalendář s narozeninami z vašich kontaktů"
},
@@ -2947,6 +3017,9 @@
"alert_now": "Začíná nyní",
"alert_in_minutes": "Za {count} min",
"invitation_sent": "Pozvánky byly odeslány",
"invitations_denied_title": "Pozvánky nelze odeslat",
"invitations_denied": "Server odmítl odeslat pozvánky: {reason}. Uložit událost bez jejich odeslání?",
"save_without_invitations": "Uložit bez pozvánek",
"rsvp_updated": "Odpověď byla aktualizována",
"rsvp_error": "Aktualizace odpovědi selhala",
"event_duplicated": "Událost byla duplikována",
@@ -2975,7 +3048,7 @@
"supported_formats": "Podporuje soubory iCalendar (.ics)",
"url_description": "Zadejte adresu URL externího zdroje iCalendar (.ics) pro import událostí.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Podporuje adresy URL CalDAV a iCalendar (.ics)",
"url_hint": "Podporuje adresy URL iCalendar (.ics)",
"fetch": "Načíst",
"invalid_url": "Zadejte platnou adresu URL",
"url_fetch_failed": "Načtení kalendáře z adresy URL selhalo",
@@ -3164,6 +3237,8 @@
"has_attachment": "Obsahuje přílohu",
"date_after": "Po",
"date_before": "Před",
"size_min": "Větší než (KB)",
"size_max": "Menší než (KB)",
"starred": "S hvězdičkou",
"unread": "Nepřečtené",
"read": "Přečtené",
@@ -3205,6 +3280,10 @@
"hint": "Klikněte na libovolnou zprávu vlevo pro začátek, nebo využijte průvodce níže."
},
"files": {
"office_edit": "Upravit dokument",
"office_loading": "Otevírání editoru dokumentů…",
"office_error": "Editor dokumentů se nepodařilo otevřít.",
"office_close": "Zavřít editor",
"open_in_new_tab": "Otevřít na nové kartě",
"title": "Soubory",
"search_placeholder": "Hledat soubory...",
@@ -3217,6 +3296,8 @@
"rename": "Přejmenovat",
"rename_title": "Přejmenovat",
"new_name": "Nový název",
"name_forbidden_chars": "Server nepovoluje v názvech tyto znaky: {chars}",
"name_reserved": "Tento název je na serveru vyhrazen.",
"delete": "Odstranit",
"delete_confirm_title": "Odstranit položku",
"delete_confirm_message": "Opravdu chcete odstranit \"{name}\"? Tuto akci nelze vrátit zpět.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "Vyhledávání není ve sjednoceném zobrazení k dispozici"
},
"calendar_event_notifications": {
"someone": "Někdo",
"invited": "{name} vás pozval(a) na „{title}“",
"updated": "{name} aktualizoval(a) „{title}“",
"cancelled": "{name} zrušil(a) „{title}“",
"untitled": "Událost bez názvu"
},
"share_notifications": {
"someone": "Někdo",
"shared": "{name} s vámi sdílí {kind} „{object}“",
"changed": "{name} změnil(a) váš přístup k {kind} „{object}“",
"revoked": "{name} odebral(a) váš přístup k {kind} „{object}“",
"object": {
"folder": "složku",
"calendar": "kalendář",
"address_book": "adresář",
"files": "složku souborů"
}
},
"sharing": {
"title": "Sdílet „{name}\"",
"description": "Udělte přístup dalším uživatelům nebo skupinám na tomto serveru. Změny se projeví okamžitě.",
@@ -3501,5 +3601,45 @@
"copy_event": "Kopírovat odkaz na událost",
"copied": "Odkaz zkopírován",
"copy_failed": "Odkaz se nepodařilo zkopírovat"
},
"plugins": {
"oauth_callback": {
"completing": "Dokončování autorizace…",
"completing_message": "Počkejte prosím, než se naváže spojení.",
"received_title": "Autorizace přijata",
"received_message": "Můžete se vrátit na kartu, kde jste zahájili připojení.",
"failed_title": "Autorizace se nezdařila",
"denied": "Autorizace byla zamítnuta.",
"failed_detail": "Autorizace se nezdařila: {error}",
"missing_params": "Chybí autorizační kód nebo stav.",
"delivery_failed": "Výsledek autorizace se nepodařilo předat kartě aplikace. Zavřete toto okno a zkuste to znovu."
}
},
"global_search": {
"title": "Hledat ve všem",
"placeholder": "Hledat v poště, kontaktech, kalendáři, souborech...",
"scope_label": "Hledat v",
"account_label": "Účet",
"scope_all": "Vše",
"scope_mail": "Pošta",
"scope_contacts": "Kontakty",
"scope_calendar": "Kalendář",
"scope_files": "Soubory",
"all_accounts": "Všechny účty",
"show_all": "Zobrazit všechny výsledky",
"no_results": "Žádné výsledky pro „{query}“",
"server_hint": "Celá slova prohledávají server; části slov odpovídají již načteným položkám",
"searching": "Hledání...",
"account_error": "Nepodařilo se prohledat {account}",
"recent_searches": "Nedávná hledání",
"recurring": "Opakovaná",
"no_subject": "(no subject)",
"empty_hint": "Hledejte poštu, kontakty, události a soubory ve všech účtech. Zkuste from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Ploše podle data",
"grouped": "Seskupené",
"load_more": "Načíst další",
"preview_empty": "Vyberte výsledek pro náhled",
"open_full": "Otevřít",
"view_label": "Zobrazení"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "og {count} andre",
"draft": "Kladde",
"starred": "Stjernemarkerede",
"pinned": "Fastgjort",
"replied": "Besvaret",
"forwarded": "Videresendt",
"has_attachment": "Har vedhæftning",
"conversations_count": "{count} af {total} samtaler",
"conversations_count_plus": "{count}+ samtaler",
"conversations_count_simple": "{count} samtaler",
@@ -292,6 +296,7 @@
"forward": "Videresend",
"delete": "Slet",
"archive": "Arkivér",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Stjernemarkér",
"unstar": "Fjern stjernemarkering",
"mark_unread": "Markér som ulæst",
@@ -303,12 +308,10 @@
"move": "Flyt",
"print": "Udskriv",
"view_source": "Vis kilde",
"fullscreen": "Fuld skærm",
"exit_fullscreen": "Afslut fuld skærm",
"export_email": "Eksportér som .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Videresend som vedhæftet fil",
"import_email": "Importér .eml eller .zip",
"keyboard_shortcuts": "Tastaturgenveje (?)",
"email_source": "E-mail-kilde",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Læsekvittering anmodet (klik for at deaktivere)",
"read_receipt_off": "Anmod om læsekvittering",
"dsn_on": "Leveringsbesked anmodet (klik for at slå fra)",
"dsn_off": "Anmod om leveringsbesked",
"format_plain_text": "Skift til ren tekst",
"format_rich_text": "Skift til formateret tekst (HTML)",
"require_tls_on": "Krypteret levering påkrævet (klik for at slå fra)",
"require_tls_off": "Kræv krypteret levering (TLS)",
"new_message": "Ny besked",
"signature_edit_hint": "Dobbeltklik for at redigere signaturen",
"reply": "Svar",
@@ -633,11 +642,12 @@
"from_override": {
"toggle_off": "Tilsidesæt",
"toggle_on": "Annuller tilsidesættelse",
"toggle_tooltip": "Redigér Fra-navn og -adresse frit. Mail sendes stadig gennem din identitet - kun den synlige Fra-header ændres.",
"toggle_tooltip": "Redigér Fra-navn og -adresse frit. Mail sendes stadig gennem din identitet.",
"name_label": "Fra-navn",
"name_placeholder": "Navn",
"email_label": "Fra-e-mailadresse",
"email_placeholder": "alias@eksempel.dk"
"email_placeholder": "alias@eksempel.dk",
"envelope_notice": "Hvis din server ikke accepterer denne adresse som konvolutafsender, bruges {identity} i stedet, og modtagerne kan se den i Return-Path-headeren."
},
"use_template": "Skabelon",
"save_as_template": "Gem som skabelon",
@@ -650,6 +660,8 @@
"upload_progress": "Uploader {uploaded} / {total}",
"upload_cancel": "Annuller upload",
"upload_failed": "Kunne ikke uploade {filename}",
"attachment_too_large": "\"{name}\" er større, end serveren tillader ({max} pr. fil)",
"attachments_total_too_large": "Vedhæftningerne ville overskride serverens grænse på {max} pr. besked",
"drop_files": "Træk filer hertil for at vedhæfte",
"show_less": "Vis mindre",
"send_failed": "Kunne ikke sende e-mail",
@@ -726,7 +738,11 @@
"delete_column": "Slet kolonne",
"toggle_header_row": "Slå overskriftsrække til/fra",
"delete_table": "Slet tabel",
"pick_size": "Vælg størrelse"
"pick_size": "Vælg størrelse",
"font_size": "Skriftstørrelse",
"font_size_default": "Standard",
"background_color": "Baggrundsfarve",
"remove_background_color": "Fjern baggrundsfarve"
},
"send_filing_warning": "Sendt - men oprydningen bagefter mislykkedes, en forældet kladde kan blive stående."
},
@@ -765,6 +781,10 @@
"email_sent": "E-mail sendt succesfuldt",
"email_deleted": "E-mail slettet",
"email_archived": "E-mail arkiveret",
"emails_deleted": "{count, plural, one {# e-mail slettet} other {# e-mails slettet}}",
"emails_archived": "{count, plural, one {# e-mail arkiveret} other {# e-mails arkiveret}}",
"error_archiving": "Kunne ikke arkivere e-mail",
"error_updating": "Kunne ikke opdatere e-mail",
"email_starred": "E-mail stjernemarkeret",
"email_unstarred": "Stjernemarkering fjernet",
"email_marked_read": "E-mail markeret som læst",
@@ -1144,6 +1164,7 @@
"status_active": "Aktiv på denne enhed",
"status_inactive": "Ikke aktiveret på denne enhed",
"status_unsupported": "Denne browser understøtter ikke Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "Arbejder…",
"enable": "Aktivér",
"reenable": "Genregistrér",
@@ -1181,7 +1202,9 @@
"enabled": "E-mail-notifikationer",
"enabled_desc": "Vis notifikationer, når nye e-mails ankommer",
"sound": "Notifikationslyd",
"sound_desc": "Afspil en lydadvarsel, når nye e-mails ankommer"
"sound_desc": "Afspil en lydadvarsel, når nye e-mails ankommer",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Kalendernotifikationer",
@@ -1241,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Ryd søgningen ved skift af mappe",
"description": "Klik på en mappe (Indbakke, Sendt …) rydder den aktive søgning og viser mappen. Når det er slået fra, bevares søgningen og køres igen i den valgte mappe."
},
"request_read_receipt": {
"label": "Anmod om læsekvittering som standard",
"description": "Aktivér anmodningen om læsekvittering på forhånd, når du skriver en ny besked."
@@ -1324,6 +1351,12 @@
"label": "Svar fra modtaget adresse",
"description": "Når du svarer, send fra den adresse som beskeden oprindeligt blev sendt til. Matcher først identiteter; for domæne catch-all-leveringer omskriver Fra-headeren til aliaset, mens der sendes gennem din primære identitet."
},
"reply_identity_match": {
"label": "Matchning af modtageradresse",
"description": "Hvilke modtageradresser der tæller som dine. „Kun præcis adresse“ vælger udelukkende en af dine opsatte identiteter; „samme domæne“ behandler desuden enhver anden adresse på dine identitetsdomæner som et catch-all-alias og omskriver Fra-headeren til den. Vælg præcis adresse, hvis disse adresser er distributionslister.",
"exact": "Kun præcis adresse",
"domain": "Enhver adresse på mine domæner"
},
"signature_position": {
"label": "Signaturplacering",
"description": "Hvor din signatur indsættes i svar og videresendelser. Før citeret tekst læses naturligt som en afslutning på svaret; under bevarer den oprindelige besked sammenhængende.",
@@ -1749,7 +1782,9 @@
"warnings": {
"end_before_start": "Slutdato skal være efter startdato",
"start_in_past": "Startdato er tidligere end i dag",
"empty_body": "Beskedteksten er tom - modtagere vil modtage et tomt svar"
"empty_body": "Beskedteksten er tom - modtagere vil modtage et tomt svar",
"subject_too_long": "Emnet er for langt til serveren (højst {max} byte).",
"body_too_long": "Beskeden er for lang til serveren (højst {max} byte inkl. formatering)."
}
},
"folders": {
@@ -1766,6 +1801,8 @@
"role_trash": "Papirkurv",
"role_junk": "Spam",
"role_archive": "Arkiv",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Ingen",
"create_folder": "Opret mappe",
"create_subfolder": "Opret undermappe",
@@ -1875,6 +1912,8 @@
"title": "Kontakter",
"description": "Importér og eksportér dine kontakter",
"group_by_letter_label": "Gruppér efter første bogstav",
"sort_by_last_name_label": "Sortér efter efternavn",
"sort_by_last_name_description": "Sortér kontaktlisten efter efternavn, så familiemedlemmer står samlet",
"group_by_letter_description": "Vis alfabetiske sektionsoverskrifter i kontaktlisten",
"import_label": "Importér kontakter",
"import_description": "Importér kontakter fra en vCard-fil (.vcf)",
@@ -1966,6 +2005,7 @@
"actions": "Handlinger",
"add_action": "Tilføj handling",
"stop_processing": "Stop behandling af efterfølgende regler",
"include_spam": "Flyt også beskeder, der er markeret som spam",
"attachment_type_placeholder": "f.eks. pdf, doc, jpg",
"value_placeholder_multi": "Værdi (flere adskilt med komma)",
"condition_fields": {
@@ -2007,6 +2047,8 @@
"copy_to_folder": "Vælg mappe",
"forward_to": "Videresend til e-mailadresse",
"forward_placeholder": "email@eksempel.dk",
"forward_keep_copy": "Behold en kopi",
"forward_limit": "Grænse for videresendelser pr. besked på denne server: {count}. Yderligere videresendelser springes over.",
"reject_message": "Afvisningsbesked",
"reject_placeholder": "Din e-mail er blevet afvist",
"label_name": "Tag-navn",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Ny undermappe...",
"new_folder": "Ny mappe...",
"rename": "Omdøb...",
"share": "Del...",
"import_email": "Importér .eml eller .zip...",
"empty_folder": "Tøm mappe",
"empty_folder_generic": "Tøm mappe",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "Kunne ikke omdøbe mappe",
"toast_error_delete": "Kunne ikke slette mappe",
"toast_error_delete_has_children": "Mappen har undermapper. Fjern dem først.",
"toast_error_delete_has_email": "Mappen er ikke tom. Tøm den først."
"toast_error_delete_has_email": "Mappen er ikke tom. Tøm den først.",
"delete_with_contents_title": "Mappen er ikke tom",
"delete_with_contents_message": "Mappen \"{name}\" indeholder stadig {count, plural, one {1 besked} other {# beskeder}}. Slet mappen og alle dens beskeder? Dette kan ikke fortrydes.",
"delete_with_contents_confirm": "Slet mappe og beskeder"
},
"shortcuts": {
"title": "Tastaturgenveje",
@@ -2270,7 +2316,8 @@
"search": "Fokusér søgning",
"help": "Vis genveje",
"refresh": "Opdatér e-mails",
"select_all": "Vælg alle"
"select_all": "Vælg alle",
"global_search": "Søg i alt"
},
"threads": {
"expand_collapse": "Udvid/skjul tråd"
@@ -2721,7 +2768,10 @@
"any_month": "Enhver måned",
"has_email": "Har e-mail",
"has_phone": "Har telefon",
"has_photo": "Har billede"
"has_photo": "Har billede",
"sort_by": "Sortér efter",
"sort_first_name": "Fornavn",
"sort_last_name": "Efternavn"
},
"open_categories": "Åbn kategorier"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Duplikér",
"today_header": "I dag",
"tomorrow_header": "I morgen",
"agenda_show_earlier": "Vis tidligere begivenheder",
"agenda_loading": "Indlæser…",
"agenda_range_start": "Begivenheder fra {date}",
"agenda_range_end": "Begivenheder til {date}",
"export_ics": "Eksportér som .ics",
"copy_title": "Kopier titel",
"copy_link": "Kopier mødelink",
@@ -2793,6 +2847,14 @@
"color": "Farve"
},
"participants": {
"availability": {
"title": "Tilgængelighed",
"free": "Ledig",
"busy": "Optaget",
"tentative": "Foreløbig",
"unknown": "Ikke en bruger på denne server",
"checking": "Kontrollerer…"
},
"title": "Deltagere",
"add": "Tilføj deltager",
"organizer": "Arrangør",
@@ -2867,7 +2929,10 @@
"all_events": "Alle begivenheder",
"cancel": "Annuller",
"save": "Gem",
"delete": "Slet"
"delete": "Slet",
"rsvp_title": "Svar på tilbagevendende begivenhed",
"rsvp_description": "Dette er en tilbagevendende begivenhed. Hvilke begivenheder gælder dit svar for?",
"respond": "Svar"
},
"alerts": {
"title": "Påmindelse",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Vis begivenhedstider i månedskalendervisningen",
"show_week_numbers": "Vis ugenumre",
"show_week_numbers_desc": "Vis ugenumre i minikalenderen",
"calendar_free_scroll": "Fri rulning",
"calendar_free_scroll_desc": "Rul fortløbende gennem måneder, uger og dage i stedet for én periode ad gangen",
"enable_tasks": "Aktivér opgaver",
"enable_tasks_desc": "Vis en opgavevisning i kalenderen til styring af gøremål",
"show_tasks_on_calendar": "Vis opgaver på kalender",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1 sekunds forsinkelse",
"hover_preview_delay_2s": "2 sekunders forsinkelse",
"hover_preview_off": "Deaktiveret",
"organizer_identity": "Organisér invitationer som",
"organizer_identity_desc": "Kalenderadressen, der bruges som arrangør, når du inviterer folk til begivenheder.",
"organizer_identity_failed": "Kunne ikke ændre arrangøradressen",
"show_birthday_calendar": "Kontaktfødselsdagskalender",
"show_birthday_calendar_desc": "Vis en virtuel kalender med fødselsdage fra dine kontakter"
},
@@ -2973,6 +3043,9 @@
"alert_now": "Starter nu",
"alert_in_minutes": "Om {count} min",
"invitation_sent": "Invitationer sendt",
"invitations_denied_title": "Invitationerne kan ikke sendes",
"invitations_denied": "Serveren afviste at sende invitationerne: {reason}. Gem begivenheden uden at sende dem?",
"save_without_invitations": "Gem uden invitationer",
"rsvp_updated": "Svar opdateret",
"rsvp_error": "Kunne ikke opdatere svar",
"event_duplicated": "Begivenhed duplikeret",
@@ -3001,7 +3074,7 @@
"supported_formats": "Understøtter iCalendar (.ics) filer",
"url_description": "Indtast URL'en til et eksternt iCalendar (.ics)-feed for at importere begivenheder.",
"url_placeholder": "https://eksempel.dk/kalender.ics",
"url_hint": "Understøtter CalDAV og iCalendar (.ics) URL'er",
"url_hint": "Understøtter iCalendar (.ics) URL'er",
"fetch": "Hent",
"invalid_url": "Indtast en gyldig URL",
"url_fetch_failed": "Kunne ikke hente kalender fra URL",
@@ -3149,6 +3222,25 @@
},
"nav_open_menu": "Åbn menu"
},
"calendar_event_notifications": {
"someone": "Nogen",
"invited": "{name} har inviteret dig til \"{title}\"",
"updated": "{name} har opdateret \"{title}\"",
"cancelled": "{name} har aflyst \"{title}\"",
"untitled": "Unavngivet begivenhed"
},
"share_notifications": {
"someone": "Nogen",
"shared": "{name} har delt {kind} \"{object}\" med dig",
"changed": "{name} har ændret din adgang til {kind} \"{object}\"",
"revoked": "{name} har fjernet din adgang til {kind} \"{object}\"",
"object": {
"folder": "mappen",
"calendar": "kalenderen",
"address_book": "adressebogen",
"files": "filmappen"
}
},
"sharing": {
"title": "Del \"{name}\"",
"description": "Giv adgang til andre brugere eller grupper på denne server. Ændringer træder i kraft øjeblikkeligt.",
@@ -3187,6 +3279,8 @@
"has_attachment": "Vedhæftninger",
"date_after": "Efter",
"date_before": "Før",
"size_min": "Større end (KB)",
"size_max": "Mindre end (KB)",
"starred": "Stjernemarkerede",
"unread": "Ulæst",
"read": "Læst",
@@ -3228,6 +3322,10 @@
"hint": "Klik på en e-mail til venstre for at komme i gang, eller tag rundvisningen nedenfor."
},
"files": {
"office_edit": "Rediger dokument",
"office_loading": "Åbner dokumenteditor…",
"office_error": "Dokumenteditoren kunne ikke åbnes.",
"office_close": "Luk editor",
"open_in_new_tab": "Åbn i ny fane",
"title": "Filer",
"search_placeholder": "Søg efter filer...",
@@ -3240,6 +3338,8 @@
"rename": "Omdøb",
"rename_title": "Omdøb",
"new_name": "Nyt navn",
"name_forbidden_chars": "Serveren tillader ikke disse tegn i navne: {chars}",
"name_reserved": "Dette navn er reserveret på serveren.",
"delete": "Slet",
"delete_confirm_title": "Slet ressource",
"delete_confirm_message": "Er du sikker på, at du vil slette \"{name}\"? Dette kan ikke fortrydes.",
@@ -3501,5 +3601,45 @@
"copy_event": "Kopier link til begivenhed",
"copied": "Link kopieret",
"copy_failed": "Kunne ikke kopiere linket"
},
"plugins": {
"oauth_callback": {
"completing": "Fuldfører godkendelse…",
"completing_message": "Vent venligst, mens forbindelsen oprettes.",
"received_title": "Godkendelse modtaget",
"received_message": "Du kan vende tilbage til den fane, hvor du startede forbindelsen.",
"failed_title": "Godkendelse mislykkedes",
"denied": "Godkendelse blev nægtet.",
"failed_detail": "Godkendelse mislykkedes: {error}",
"missing_params": "Autorisationskode eller tilstand mangler.",
"delivery_failed": "Resultatet af godkendelsen kunne ikke leveres til app-fanen. Luk dette vindue og prøv igen."
}
},
"global_search": {
"title": "Søg i alt",
"placeholder": "Søg i mail, kontakter, kalender, filer...",
"scope_label": "Søg i",
"account_label": "Konto",
"scope_all": "Alt",
"scope_mail": "Mail",
"scope_contacts": "Kontakter",
"scope_calendar": "Kalender",
"scope_files": "Filer",
"all_accounts": "Alle konti",
"show_all": "Vis alle resultater",
"no_results": "Ingen resultater for “{query}”",
"server_hint": "Hele ord søger på serveren; dele af ord matcher allerede indlæste elementer",
"searching": "Søger...",
"account_error": "Kunne ikke søge i {account}",
"recent_searches": "Seneste søgninger",
"recurring": "Tilbagevendende",
"no_subject": "(no subject)",
"empty_hint": "Søg i mail, kontakter, begivenheder og filer på alle konti. Prøv from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Fladt efter dato",
"grouped": "Grupperet",
"load_more": "Indlæs flere",
"preview_empty": "Vælg et resultat for at se det",
"open_full": "Åbn",
"view_label": "Visning"
}
}
+212 -72
View File
@@ -142,8 +142,8 @@
"unified_all_unread": "Alle ungelesenen",
"unified_all_starred": "Alle markierten",
"unified_all_mail": "Alle Mails",
"remove_account": "Remove account",
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
"remove_account": "Konto entfernen",
"remove_account_confirm": "{account} von diesem Gerät entfernen? Sie können das Konto später wieder hinzufügen."
},
"protocol_handlers": {
"title": "Standard-Apps",
@@ -152,7 +152,7 @@
"mailto_label": "E-Mail-Links",
"mailto_description": "Öffnet mailto:-Links in Bulwark mit vorausgefülltem Editor.",
"protocol_open_mode_label": "Beim Öffnen von Protokoll-Links",
"protocol_open_mode_description": "Wähle, ob Bulwark mailto:- und webcal:-Links immer in einem neuen Tab öffnet oder eine offene Sitzung wiederverwendet. Für die aktive Sitzung benötigt Bulwark Benachrichtigungen, damit du das Fenster per Klick in den Vordergrund holen kannst, falls der Browser den Fokus blockiert.",
"protocol_open_mode_description": "Legen Sie fest, ob Bulwark mailto:- und webcal:-Links immer in einem neuen Tab öffnet oder eine offene Sitzung wiederverwendet. Für die aktive Sitzung benötigt Bulwark Benachrichtigungen, damit Sie das Fenster per Klick in den Vordergrund holen können, falls der Browser den Fokus blockiert.",
"protocol_open_mode_active_session": "Wenn möglich in aktiver Sitzung öffnen",
"protocol_open_mode_new_tab": "Immer neuen Tab öffnen",
"focus_notification_title": "Bulwark öffnen",
@@ -168,8 +168,8 @@
"opening_webcal": "Kalender wird geöffnet...",
"browser_note": "Ihr Browser oder Betriebssystem kann eine Bestätigung verlangen. Eventuell muss Bulwark installiert sein, bevor es als Standard-App ausgewählt werden kann.",
"select_account_title": "Account auswählen",
"select_mailto_account": "Wähle aus, mit welchem Account dieser E-Mail-Link geöffnet werden soll.",
"select_webcal_account": "Wähle aus, mit welchem Account dieser Kalender-Link geöffnet werden soll.",
"select_mailto_account": "Wählen Sie aus, mit welchem Account dieser E-Mail-Link geöffnet werden soll.",
"select_webcal_account": "Wählen Sie aus, mit welchem Account dieser Kalender-Link geöffnet werden soll.",
"select_account_note": "Diese Auswahl gilt nur für diesen Protokoll-Link.",
"detail_to": "An",
"detail_subject": "Betreff",
@@ -225,6 +225,10 @@
"and_others": "und {count} weitere",
"draft": "Entwurf",
"starred": "Mit Stern",
"pinned": "Angeheftet",
"replied": "Beantwortet",
"forwarded": "Weitergeleitet",
"has_attachment": "Mit Anhang",
"conversations_count": "{count} von {total} Unterhaltungen",
"conversations_count_plus": "{count}+ Unterhaltungen",
"conversations_count_simple": "{count} Unterhaltungen",
@@ -292,6 +296,7 @@
"forward": "Weiterleiten",
"delete": "Löschen",
"archive": "Archivieren",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Stern hinzufügen",
"unstar": "Stern entfernen",
"mark_unread": "Als ungelesen markieren",
@@ -303,12 +308,10 @@
"move": "Verschieben",
"print": "Drucken",
"view_source": "Quelltext anzeigen",
"fullscreen": "Vollbild",
"exit_fullscreen": "Vollbild beenden",
"export_email": "Als .eml exportieren",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Als Anhang weiterleiten",
"import_email": ".eml oder .zip importieren",
"keyboard_shortcuts": "Tastaturkürzel (?)",
"email_source": "E-Mail-Quelltext",
@@ -509,8 +512,8 @@
"review_request": "Anfrage prüfen",
"view_in_calendar": "Im Kalender anzeigen",
"select_calendar": "Kalender auswählen",
"already_in_calendar": "Bereits in deinem Kalender",
"request_info": "Du wurdest zu diesem Termin eingeladen. Antworte, um dem Organisator deine Verfügbarkeit mitzuteilen.",
"already_in_calendar": "Bereits in Ihrem Kalender",
"request_info": "Sie wurden zu diesem Termin eingeladen. Antworten Sie, um dem Organisator Ihre Verfügbarkeit mitzuteilen.",
"cancel_info": "Der Organisator hat diesen Termin abgesagt.",
"event_updated": "Aktualisierung #{sequence}",
"event_status_tentative": "Vorläufig",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Lesebestätigung angefordert (klicken zum Deaktivieren)",
"read_receipt_off": "Lesebestätigung anfordern",
"dsn_on": "Zustellbenachrichtigung angefordert (zum Deaktivieren klicken)",
"dsn_off": "Zustellbenachrichtigung anfordern",
"format_plain_text": "Zu Nur-Text wechseln",
"format_rich_text": "Zu formatiertem Text (HTML) wechseln",
"require_tls_on": "Verschlüsselte Zustellung erforderlich (zum Deaktivieren klicken)",
"require_tls_off": "Verschlüsselte Zustellung verlangen (TLS)",
"new_message": "Neue Nachricht",
"signature_edit_hint": "Doppelklick, um die Signatur zu bearbeiten",
"reply": "Antworten",
@@ -597,8 +606,8 @@
"send": "Senden",
"cancel": "Abbrechen",
"attach": "Anhängen",
"attach_photos": "Photos & Videos",
"attach_files": "Files",
"attach_photos": "Fotos & Videos",
"attach_files": "Dateien",
"discard": "Verwerfen",
"discard_draft_title": "Entwurf verwerfen?",
"discard_draft_confirm": "Sie haben ungespeicherte Änderungen. Möchten Sie diesen Entwurf verwerfen?",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Überschreiben",
"toggle_on": "Überschreibung aufheben",
"toggle_tooltip": "Bearbeiten Sie Absendername und -adresse frei. Die E-Mail wird weiterhin über Ihre Identität gesendet - nur die sichtbare Absenderkopfzeile ändert sich.",
"toggle_tooltip": "Bearbeiten Sie Absendername und -adresse frei. Die E-Mail wird weiterhin über Ihre Identität gesendet.",
"name_label": "Absendername",
"name_placeholder": "Name",
"email_label": "Absender-E-Mail-Adresse",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Akzeptiert Ihr Server diese Adresse nicht als Umschlag-Absender, wird dort {identity} verwendet, und Empfänger können sie in der Kopfzeile Return-Path sehen."
},
"use_template": "Vorlage",
"save_as_template": "Als Vorlage speichern",
@@ -648,6 +658,8 @@
"upload_progress": "Hochladen {uploaded} / {total}",
"upload_cancel": "Hochladen abbrechen",
"upload_failed": "Hochladen von {filename} fehlgeschlagen",
"attachment_too_large": "\"{name}\" ist größer, als der Server erlaubt ({max} pro Datei)",
"attachments_total_too_large": "Die Anhänge würden das Limit des Servers von {max} pro Nachricht überschreiten",
"send_failed": "E-Mail konnte nicht gesendet werden",
"smime_sign_on": "S/MIME-Signatur aktiviert",
"smime_sign_off": "S/MIME-Signatur deaktiviert",
@@ -726,7 +738,11 @@
"delete_column": "Spalte löschen",
"toggle_header_row": "Kopfzeile umschalten",
"delete_table": "Tabelle löschen",
"pick_size": "Größe wählen"
"pick_size": "Größe wählen",
"font_size": "Schriftgröße",
"font_size_default": "Standard",
"background_color": "Hintergrundfarbe",
"remove_background_color": "Hintergrundfarbe entfernen"
},
"send_filing_warning": "Gesendet - aber das Aufräumen danach schlug fehl, evtl. bleibt ein alter Entwurf sichtbar."
},
@@ -753,7 +769,7 @@
"unknown": "Unbekannt",
"app_title": "Webmail",
"reconnecting": "Verbindung verloren. Verbindung wird wiederhergestellt…",
"rate_limited_title": "Die Serverauthentifizierung ist vorubergehend begrenzt.",
"rate_limited_title": "Die Serverauthentifizierung ist vorübergehend begrenzt.",
"rate_limited_detail": "Bulwark hat Hintergrundanfragen pausiert, um eine Sperre zu vermeiden. Neuer Versuch in {seconds}s.",
"rate_limited_action_title": "Anfrage pausiert, um eine Sperre zu vermeiden.",
"rate_limited_action_detail": "Bulwark wartet, bis die Serverabklingzeit endet, bevor weitere authentifizierte Anfragen gesendet werden. Versuchen Sie es in {seconds}s erneut.",
@@ -765,6 +781,10 @@
"email_sent": "E-Mail erfolgreich gesendet",
"email_deleted": "E-Mail gelöscht",
"email_archived": "E-Mail archiviert",
"emails_deleted": "{count, plural, one {# E-Mail gelöscht} other {# E-Mails gelöscht}}",
"emails_archived": "{count, plural, one {# E-Mail archiviert} other {# E-Mails archiviert}}",
"error_archiving": "E-Mail konnte nicht archiviert werden",
"error_updating": "E-Mail konnte nicht aktualisiert werden",
"email_starred": "Stern hinzugefügt",
"email_unstarred": "Stern entfernt",
"email_marked_read": "E-Mail als gelesen markiert",
@@ -1037,26 +1057,26 @@
}
},
"all_mail": {
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"folders_label": "Ordner in „Alle Mails“",
"folders_description": "Wählen Sie aus, welche Ordner in der Ansicht „Alle Mails“ zusammengeführt werden.",
"account_hint": "Gilt für {account}.",
"no_folders": "No folders available."
"no_folders": "Keine Ordner verfügbar."
},
"colorful_sidebar_icons": {
"label": "Farbige Seitenleistensymbole",
"description": "Ordner- und Tag-Symbole nach Typ einfärben (blauer Posteingang, roter Spam, grüner Gesendet usw.). Für eine monochrome Seitenleiste deaktivieren."
},
"tint_list_rows": {
"label": "Tint List Rows by Tag Color",
"description": "Shade each message row with its first tag color. Disable to keep rows plain; tag dots and chips still show the color."
"label": "Nachrichtenzeilen mit Tag-Farbe einfärben",
"description": "Die einzelnen Nachrichtenzeilen mit der Farbe ihres ersten Tags einfärben. Deaktivieren Sie diese Option, um die Zeilen neutral zu halten; Tag-Punkte und -Markierungen zeigen die Farbe weiterhin an."
},
"show_folder_total_count": {
"label": "Gesamtzahl der Nachrichten anzeigen",
"description": "Zeigt neben Ordnern und Tags die Gesamtzahl der Nachrichten zusätzlich zur Anzahl ungelesener Nachrichten an. Deaktivieren, um nur ungelesene Nachrichten anzuzeigen."
},
"favicon_unread_badge": {
"label": "Unread Count on Tab Icon",
"description": "Show the inbox unread count as a badge on the browser tab icon, so new mail is visible when the tab is not focused. Disable to keep the icon unbadged."
"label": "Anzahl ungelesener Nachrichten am Tab-Symbol",
"description": "Die Anzahl ungelesener Nachrichten im Posteingang als Abzeichen am Browser-Tab-Symbol anzeigen, damit neue E-Mails auch bei nicht fokussiertem Tab sichtbar sind. Deaktivieren Sie diese Option, um das Symbol ohne Abzeichen anzuzeigen."
},
"pro_interface": {
"label": "Pro-Oberfläche (experimentell)",
@@ -1066,7 +1086,7 @@
},
"cross_unread": {
"label": "Alle Konten: Ungelesen",
"description": "Zeigt in der Sektion „All accounts“ einen Eintrag mit ungelesenen Mails über alle Konten, über alle Ordner außer Junk, Gesendet, Archiv, Papierkorb und Entwürfe."
"description": "Zeigt in der Sektion „All accounts“ einen Eintrag mit ungelesenen Mails über alle Konten und Ordner außer Junk, Gesendet, Archiv, Papierkorb und Entwürfe."
},
"cross_starred": {
"label": "Alle Konten: Markiert",
@@ -1143,7 +1163,9 @@
"enabled": "E-Mail-Benachrichtigungen",
"enabled_desc": "Benachrichtigungen anzeigen, wenn neue E-Mails eintreffen",
"sound": "Benachrichtigungston",
"sound_desc": "Einen Ton abspielen, wenn neue E-Mails eintreffen"
"sound_desc": "Einen Ton abspielen, wenn neue E-Mails eintreffen",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Kalender-Benachrichtigungen",
@@ -1172,6 +1194,7 @@
"status_busy": "Wird verarbeitet…",
"status_inactive": "Auf diesem Gerät nicht aktiviert",
"status_unsupported": "Dieser Browser unterstützt Web Push nicht",
"status_lite": "Web Push ist in Bulwark Lite nicht verfügbar; es benötigt den Bulwark-Server.",
"title": "Hintergrundbenachrichtigungen",
"devices_title": "Registrierte Geräte",
"devices_desc": "Alle Geräte, die Hintergrundbenachrichtigungen für dieses Konto erhalten. Wird eines widerrufen, empfängt es sofort keine mehr.",
@@ -1211,12 +1234,12 @@
"preview_older": "Älter:"
},
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"label": "Region für Datumsformat",
"description": "Legt fest, in welcher Reihenfolge numerische Datumsangaben angezeigt werden (Tag, Monat, Jahr).",
"auto": "Automatisch (passend zur Sprache)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
"dmy": "Tag/Monat/Jahr",
"mdy": "Monat/Tag/Jahr"
},
"time_format": {
"label": "Zeitformat",
@@ -1231,13 +1254,17 @@
"monday": "Montag"
},
"time_zone": {
"label": "Time zone",
"description": "Show email and calendar times in this time zone instead of the one reported by your browser",
"auto": "Automatic ({zone})",
"preview_now": "Now:"
"label": "Zeitzone",
"description": "E-Mail- und Kalenderzeiten in dieser Zeitzone statt in der von Ihrem Browser gemeldeten Zeitzone anzeigen",
"auto": "Automatisch ({zone})",
"preview_now": "Aktuelle Zeit:"
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Suche beim Ordnerwechsel löschen",
"description": "Ein Klick auf einen Ordner (Posteingang, Gesendet …) löscht die aktive Suche und zeigt den Ordner. Ist die Option aus, bleibt die Suche bestehen und wird im neuen Ordner erneut ausgeführt."
},
"request_read_receipt": {
"label": "Lesebestätigung standardmäßig anfordern",
"description": "Beim Verfassen einer neuen Nachricht die Anforderung einer Lesebestätigung vorab aktivieren."
@@ -1321,6 +1348,12 @@
"label": "Antwortadresse automatisch wählen",
"description": "Beim Antworten die Absenderadresse automatisch auf die Identität umstellen, die die ursprüngliche Nachricht erhalten hat"
},
"reply_identity_match": {
"label": "Abgleich der Empfangsadresse",
"description": "Welche Empfangsadressen als eigene gelten. „Nur exakte Adresse“ wählt ausschließlich eine Ihrer eingerichteten Identitäten. „Gleiche Domain“ behandelt zusätzlich jede andere Adresse auf einer Ihrer Identitäts-Domains als Catch-all-Alias und ersetzt den Absender-Header damit. Wählen Sie die exakte Adresse, wenn solche Adressen Verteilerlisten und keine Aliasse sind.",
"exact": "Nur exakte Adresse",
"domain": "Jede Adresse auf meinen Domains"
},
"signature_position": {
"label": "Signaturposition",
"description": "Wo Ihre Signatur in Antworten und Weiterleitungen eingefügt wird. Über dem zitierten Text liest sie sich natürlich als Abschluss der Antwort; darunter bleibt die ursprüngliche Nachricht zusammenhängend.",
@@ -1387,7 +1420,7 @@
"close": "Schließen",
"invalid_email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"already_added": "Dieser Absender ist bereits vertrauenswürdig",
"save_error": "Speichern fehlgeschlagen - prüfe das Kontakte-Debug-Log für Details",
"save_error": "Speichern fehlgeschlagen - prüfen Sie das Kontakte-Debug-Log für Details",
"use_address_book_label": "Mit Adressbuch synchronisieren",
"use_address_book_description": "Vertrauenswürdige Absender in einem eigenen \"Vertrauenswürdige Absender\"-Adressbuch speichern, damit sie auf allen Geräten synchronisiert werden"
},
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "Das Enddatum muss nach dem Startdatum liegen",
"start_in_past": "Das Startdatum liegt in der Vergangenheit",
"empty_body": "Der Nachrichtentext ist leer - Empfänger erhalten eine leere Antwort"
"empty_body": "Der Nachrichtentext ist leer - Empfänger erhalten eine leere Antwort",
"subject_too_long": "Der Betreff ist für den Server zu lang (höchstens {max} Byte).",
"body_too_long": "Die Nachricht ist für den Server zu lang (höchstens {max} Byte, inklusive Formatierung)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Papierkorb",
"role_junk": "Spam / Junk",
"role_archive": "Archiv",
"role_scheduled": "Geplant",
"role_snoozed": "Zurückgestellt",
"role_none": "Keine",
"create_folder": "Ordner erstellen",
"create_subfolder": "Unterordner erstellen",
@@ -1835,9 +1872,9 @@
"button": "Tastaturkürzel anzeigen"
},
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions — fixes a stale or wrong view without signing out.",
"button": "Refresh"
"label": "Zwischengespeicherte Daten aktualisieren",
"description": "Kontakte, Kalender und Ordner vom Server neu laden. Konten und Sitzungen bleiben erhalten; veraltete oder fehlerhafte Ansichten werden korrigiert, ohne dass Sie sich abmelden müssen.",
"button": "Aktualisieren"
},
"reset_settings": {
"label": "Einstellungen zurücksetzen",
@@ -1882,7 +1919,9 @@
"categories_description": "Kontaktkategorien umbenennen",
"no_categories": "Keine Kategorien gefunden",
"group_by_letter_description": "Alphabetische Abschnittsüberschriften in der Kontaktliste anzeigen",
"group_by_letter_label": "Nach Anfangsbuchstaben gruppieren"
"group_by_letter_label": "Nach Anfangsbuchstaben gruppieren",
"sort_by_last_name_label": "Nach Nachname sortieren",
"sort_by_last_name_description": "Kontaktliste nach Nachnamen ordnen, damit Familienmitglieder zusammenstehen"
},
"downloads": {
"title": "Downloads",
@@ -1963,6 +2002,7 @@
"actions": "Aktionen",
"add_action": "Aktion hinzufügen",
"stop_processing": "Verarbeitung nachfolgender Regeln stoppen",
"include_spam": "Auch als Spam markierte Nachrichten verschieben",
"attachment_type_placeholder": "z.B. pdf, doc, jpg",
"value_placeholder_multi": "Wert (mehrere mit Komma trennen)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Ordner auswählen",
"forward_to": "An E-Mail-Adresse weiterleiten",
"forward_placeholder": "email@beispiel.de",
"forward_keep_copy": "Kopie behalten",
"forward_limit": "Weiterleitungen pro Nachricht auf diesem Server: höchstens {count}. Weitere werden übersprungen.",
"reject_message": "Ablehnungsnachricht",
"reject_placeholder": "Ihre E-Mail wurde abgelehnt",
"label_name": "Label-Name",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Neuer Unterordner...",
"new_folder": "Neuer Ordner...",
"rename": "Umbenennen...",
"share": "Freigeben...",
"import_email": ".eml oder .zip importieren...",
"empty_folder": "Ordner leeren",
"empty_folder_generic": "Ordner leeren",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "Ordner konnte nicht umbenannt werden",
"toast_error_delete": "Ordner konnte nicht gelöscht werden",
"toast_error_delete_has_children": "Ordner enthält Unterordner. Entferne diese zuerst.",
"toast_error_delete_has_email": "Ordner ist nicht leer. Leere ihn zuerst."
"toast_error_delete_has_email": "Ordner ist nicht leer. Leere ihn zuerst.",
"delete_with_contents_title": "Ordner ist nicht leer",
"delete_with_contents_message": "Der Ordner „{name}“ enthält noch {count, plural, one {1 Nachricht} other {# Nachrichten}}. Ordner samt allen Nachrichten löschen? Das kann nicht rückgängig gemacht werden.",
"delete_with_contents_confirm": "Ordner und Nachrichten löschen"
},
"shortcuts": {
"title": "Tastaturkürzel",
@@ -2270,7 +2316,8 @@
"search": "Suche fokussieren",
"help": "Tastaturkürzel anzeigen",
"refresh": "E-Mails aktualisieren",
"select_all": "Alle auswählen"
"select_all": "Alle auswählen",
"global_search": "Alles durchsuchen"
},
"threads": {
"expand_collapse": "Unterhaltung erweitern/einklappen"
@@ -2496,32 +2543,32 @@
"cert_already_imported": "Zertifikat bereits importiert",
"cert_imported": "Zertifikat importiert",
"cert_import_failed": "Import des Zertifikats fehlgeschlagen",
"section_contact": "Contact details",
"section_work": "Work",
"section_personal": "Personal",
"email_default_label": "Email",
"phone_default_label": "Phone",
"address_default_label": "Address",
"section_contact": "Kontaktdaten",
"section_work": "Berufliches",
"section_personal": "Persönliches",
"email_default_label": "E-Mail",
"phone_default_label": "Telefon",
"address_default_label": "Adresse",
"online_service_default_label": "Online",
"organization_label": "Organization",
"title_label": "Title",
"role_label": "Role",
"language_label": "Language",
"related_default_label": "Related",
"organization_label": "Organisation",
"title_label": "Titel",
"role_label": "Rolle",
"language_label": "Sprache",
"related_default_label": "Verknüpft",
"more_actions": "Weitere Aktionen",
"age_years": "{count, plural, one {1 Jahr alt} other {# Jahre alt}}",
"years_since": "{count, plural, one {1 Jahr} other {# Jahre}}"
},
"activity": {
"recent_emails": "Recent Emails",
"upcoming_events": "Upcoming Events",
"no_emails": "No recent emails",
"no_events": "No upcoming events",
"no_subject": "(No subject)",
"no_title": "(No title)",
"load_failed": "Failed to load",
"unknown_sender": "Unknown sender",
"all_day": "All day"
"recent_emails": "Letzte E-Mails",
"upcoming_events": "Bevorstehende Termine",
"no_emails": "Keine aktuellen E-Mails",
"no_events": "Keine bevorstehenden Termine",
"no_subject": "(Kein Betreff)",
"no_title": "(Kein Titel)",
"load_failed": "Laden fehlgeschlagen",
"unknown_sender": "Unbekannter Absender",
"all_day": "Ganztägig"
},
"form": {
"create_title": "Neuer Kontakt",
@@ -2642,11 +2689,11 @@
"search_members": "Kontakte zum Hinzufügen suchen...",
"no_members": "Keine Mitglieder in dieser Gruppe",
"member_count": "{count, plural, =0 {Keine Mitglieder} one {1 Mitglied} other {# Mitglieder}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email": "E-Mail an Gruppe senden",
"send_email_to": "An",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
"no_member_emails": "Diese Gruppe hat keine Mitglieder mit einer E-Mail-Adresse."
},
"import": {
"title": "Kontakte importieren",
@@ -2721,7 +2768,10 @@
"any_month": "Beliebiger Monat",
"has_email": "Mit E-Mail",
"has_phone": "Mit Telefon",
"has_photo": "Mit Foto"
"has_photo": "Mit Foto",
"sort_by": "Sortieren nach",
"sort_first_name": "Vorname",
"sort_last_name": "Nachname"
},
"open_categories": "Kategorien öffnen"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Duplizieren",
"today_header": "Heute",
"tomorrow_header": "Morgen",
"agenda_show_earlier": "Frühere Termine anzeigen",
"agenda_loading": "Wird geladen…",
"agenda_range_start": "Termine ab {date}",
"agenda_range_end": "Termine bis {date}",
"export_ics": "Als .ics exportieren",
"copy_title": "Titel kopieren",
"copy_link": "Meeting-Link kopieren",
@@ -2793,6 +2847,14 @@
"color": "Farbe"
},
"participants": {
"availability": {
"title": "Verfügbarkeit",
"free": "Verfügbar",
"busy": "Beschäftigt",
"tentative": "Vorläufig",
"unknown": "Kein Benutzer auf diesem Server",
"checking": "Wird geprüft…"
},
"title": "Teilnehmer",
"add": "Teilnehmer hinzufügen",
"organizer": "Organisator",
@@ -2867,7 +2929,10 @@
"all_events": "Alle Ereignisse",
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Löschen"
"delete": "Löschen",
"rsvp_title": "Auf wiederkehrendes Ereignis antworten",
"rsvp_description": "Dies ist ein wiederkehrendes Ereignis. Für welche Ereignisse gilt Ihre Antwort?",
"respond": "Antworten"
},
"alerts": {
"title": "Erinnerung",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Ereigniszeiten in der Monatskalenderansicht anzeigen",
"show_week_numbers": "Kalenderwochen anzeigen",
"show_week_numbers_desc": "Kalenderwochen im Minikalender anzeigen",
"calendar_free_scroll": "Freies Scrollen",
"calendar_free_scroll_desc": "Fortlaufend durch Monate, Wochen und Tage scrollen statt nur einen Zeitraum auf einmal anzuzeigen",
"enable_tasks": "Aufgaben aktivieren",
"enable_tasks_desc": "Eine Aufgabenansicht im Kalender zum Verwalten von To-dos anzeigen",
"show_tasks_on_calendar": "Aufgaben im Kalender anzeigen",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1 Sekunde Verzögerung",
"hover_preview_delay_2s": "2 Sekunden Verzögerung",
"hover_preview_off": "Deaktiviert",
"organizer_identity": "Einladungen organisieren als",
"organizer_identity_desc": "Die Kalenderadresse, die als Organisator verwendet wird, wenn du Personen zu Terminen einlädst.",
"organizer_identity_failed": "Organisatoradresse konnte nicht geändert werden",
"show_birthday_calendar": "Geburtstags-Kalender",
"show_birthday_calendar_desc": "Zeigt einen virtuellen Kalender mit Geburtstagen aus deinen Kontakten"
},
@@ -2947,6 +3017,9 @@
"alert_now": "Beginnt jetzt",
"alert_in_minutes": "In {count} Min.",
"invitation_sent": "Einladungen gesendet",
"invitations_denied_title": "Einladungen können nicht gesendet werden",
"invitations_denied": "Der Server hat das Senden der Einladungen abgelehnt: {reason}. Termin ohne Einladungen speichern?",
"save_without_invitations": "Ohne Einladungen speichern",
"rsvp_updated": "Antwort aktualisiert",
"rsvp_error": "Antwort konnte nicht aktualisiert werden",
"event_duplicated": "Termin dupliziert",
@@ -2975,7 +3048,7 @@
"supported_formats": "iCalendar (.ics) Dateien werden unterstützt",
"url_description": "Geben Sie die URL eines externen iCalendar (.ics) Feeds ein, um Termine zu importieren.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Unterstützt CalDAV und iCalendar (.ics) URLs",
"url_hint": "Unterstützt iCalendar (.ics) URLs",
"fetch": "Abrufen",
"invalid_url": "Bitte geben Sie eine gültige URL ein",
"url_fetch_failed": "Kalender konnte nicht von der URL abgerufen werden",
@@ -3164,6 +3237,8 @@
"has_attachment": "Anhänge",
"date_after": "Nach dem",
"date_before": "Vor dem",
"size_min": "Größer als (KB)",
"size_max": "Kleiner als (KB)",
"starred": "Markiert",
"unread": "Ungelesen",
"read": "Gelesen",
@@ -3205,6 +3280,10 @@
"hint": "Klicken Sie links auf eine E-Mail, um loszulegen, oder starten Sie die Tour."
},
"files": {
"office_edit": "Dokument bearbeiten",
"office_loading": "Dokumenteditor wird geöffnet…",
"office_error": "Der Dokumenteditor konnte nicht geöffnet werden.",
"office_close": "Editor schließen",
"open_in_new_tab": "In neuem Tab öffnen",
"title": "Dateien",
"search_placeholder": "Dateien suchen...",
@@ -3217,6 +3296,8 @@
"rename": "Umbenennen",
"rename_title": "Umbenennen",
"new_name": "Neuer Name",
"name_forbidden_chars": "Diese Zeichen sind in Namen auf dem Server nicht erlaubt: {chars}",
"name_reserved": "Dieser Name ist auf dem Server reserviert.",
"delete": "Löschen",
"delete_confirm_title": "Ressource löschen",
"delete_confirm_message": "Möchten Sie \"{name}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
@@ -3268,10 +3349,10 @@
"duplicate": "Duplizieren",
"duplicate_success": "Erfolgreich dupliziert",
"duplicate_error": "Duplizieren fehlgeschlagen",
"share": "Share",
"shared": "Shared",
"shared_with_me": "Shared with me",
"shared_by": "Shared by {name}",
"share": "Teilen",
"shared": "Geteilt",
"shared_with_me": "Mit mir geteilt",
"shared_by": "Von {name} geteilt",
"create_file_success": "Datei erstellt",
"create_file_error": "Datei konnte nicht erstellt werden",
"favorites": "Favoriten",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "Die Suche ist in der vereinheitlichten Ansicht nicht verfügbar"
},
"calendar_event_notifications": {
"someone": "Jemand",
"invited": "{name} hat dich zu „{title}“ eingeladen",
"updated": "{name} hat „{title}“ aktualisiert",
"cancelled": "{name} hat „{title}“ abgesagt",
"untitled": "Unbenannter Termin"
},
"share_notifications": {
"someone": "Jemand",
"shared": "{name} hat {kind} „{object}“ mit dir geteilt",
"changed": "{name} hat deinen Zugriff auf {kind} „{object}“ geändert",
"revoked": "{name} hat deinen Zugriff auf {kind} „{object}“ entfernt",
"object": {
"folder": "den Ordner",
"calendar": "den Kalender",
"address_book": "das Adressbuch",
"files": "den Dateiordner"
}
},
"sharing": {
"title": "„{name}\" freigeben",
"description": "Anderen Benutzern oder Gruppen auf diesem Server Zugriff gewähren. Änderungen werden sofort wirksam.",
@@ -3501,5 +3601,45 @@
"copy_event": "Link zum Termin kopieren",
"copied": "Link kopiert",
"copy_failed": "Link konnte nicht kopiert werden"
},
"plugins": {
"oauth_callback": {
"completing": "Autorisierung wird abgeschlossen…",
"completing_message": "Bitte warten, während die Verbindung hergestellt wird.",
"received_title": "Autorisierung empfangen",
"received_message": "Sie können zu dem Tab zurückkehren, in dem Sie die Verbindung gestartet haben.",
"failed_title": "Autorisierung fehlgeschlagen",
"denied": "Autorisierung wurde verweigert.",
"failed_detail": "Autorisierung fehlgeschlagen: {error}",
"missing_params": "Autorisierungscode oder State fehlt.",
"delivery_failed": "Das Autorisierungsergebnis konnte nicht an den App-Tab übermittelt werden. Bitte schließen Sie dieses Fenster und versuchen Sie es erneut."
}
},
"global_search": {
"title": "Alles durchsuchen",
"placeholder": "Mail, Kontakte, Kalender, Dateien durchsuchen...",
"scope_label": "Suchen in",
"account_label": "Konto",
"scope_all": "Alle",
"scope_mail": "E-Mail",
"scope_contacts": "Kontakte",
"scope_calendar": "Kalender",
"scope_files": "Dateien",
"all_accounts": "Alle Konten",
"show_all": "Alle Ergebnisse anzeigen",
"no_results": "Keine Ergebnisse für „{query}“",
"server_hint": "Ganze Wörter durchsuchen den Server; Wortteile treffen bereits geladene Einträge",
"searching": "Suche läuft...",
"account_error": "{account} konnte nicht durchsucht werden",
"recent_searches": "Letzte Suchen",
"recurring": "Wiederkehrend",
"no_subject": "(kein Betreff)",
"empty_hint": "Mail, Kontakte, Termine und Dateien aller Konten durchsuchen. Zum Beispiel from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Flach nach Datum",
"grouped": "Gruppiert",
"load_more": "Mehr laden",
"preview_empty": "Ergebnis auswählen, um es anzuzeigen",
"open_full": "Öffnen",
"view_label": "Ansicht"
}
}
+163 -116
View File
@@ -225,6 +225,10 @@
"and_others": "and {count} others",
"draft": "Draft",
"starred": "Starred",
"pinned": "Pinned",
"replied": "Replied",
"forwarded": "Forwarded",
"has_attachment": "Has attachment",
"conversations_count": "{count} of {total} conversations",
"conversations_count_plus": "{count}+ conversations",
"conversations_count_simple": "{count} conversations",
@@ -292,6 +296,7 @@
"forward": "Forward",
"delete": "Delete",
"archive": "Archive",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Star",
"unstar": "Unstar",
"mark_unread": "Mark as unread",
@@ -578,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Read receipt requested (click to disable)",
"read_receipt_off": "Request a read receipt",
"dsn_on": "Delivery notification requested (click to disable)",
"dsn_off": "Request a delivery notification",
"format_plain_text": "Switch to plain text",
"format_rich_text": "Switch to rich text (HTML)",
"require_tls_on": "Encrypted delivery required (click to disable)",
"require_tls_off": "Require encrypted delivery (TLS)",
"new_message": "New Message",
"signature_edit_hint": "Double-click to edit the signature",
"reply": "Reply",
@@ -632,11 +643,12 @@
"from_override": {
"toggle_off": "Override",
"toggle_on": "Cancel override",
"toggle_tooltip": "Edit the From name and address freely. Mail is still sent through your identity - only the visible From header changes.",
"toggle_tooltip": "Edit the From name and address freely. Mail is still sent through your identity.",
"name_label": "From name",
"name_placeholder": "Name",
"email_label": "From email address",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "If your server doesn't accept this address as the envelope sender, {identity} is used there instead, and recipients can see it in the Return-Path header."
},
"use_template": "Template",
"save_as_template": "Save as Template",
@@ -649,6 +661,8 @@
"upload_progress": "Uploading {uploaded} / {total}",
"upload_cancel": "Cancel upload",
"upload_failed": "Failed to upload {filename}",
"attachment_too_large": "\"{name}\" is larger than the server allows ({max} per file)",
"attachments_total_too_large": "The attachments would exceed the server's limit of {max} per message",
"drop_files": "Drop files to attach",
"show_less": "Show less",
"send_failed": "Failed to send email",
@@ -724,7 +738,11 @@
"delete_column": "Delete column",
"toggle_header_row": "Toggle header row",
"delete_table": "Delete table",
"pick_size": "Pick size"
"pick_size": "Pick size",
"font_size": "Font size",
"font_size_default": "Default",
"background_color": "Background color",
"remove_background_color": "Remove background color"
},
"send_filing_warning": "Sent - but the post-send cleanup failed, a stale draft may remain."
},
@@ -763,6 +781,10 @@
"email_sent": "Email sent successfully",
"email_deleted": "Email deleted",
"email_archived": "Email archived",
"emails_deleted": "{count, plural, one {# email deleted} other {# emails deleted}}",
"emails_archived": "{count, plural, one {# email archived} other {# emails archived}}",
"error_archiving": "Failed to archive email",
"error_updating": "Failed to update email",
"email_starred": "Email starred",
"email_unstarred": "Email unstarred",
"email_marked_read": "Email marked as read",
@@ -1027,7 +1049,7 @@
},
"unified_mailbox": {
"label": "Unified Mailbox",
"description": "Show combined folders (Inbox, Sent, etc.) for the active account and its shared folders.",
"description": "Show combined folders (Inbox, Sent, etc.) for the active account and its shared folders. Needs more than one connected account (or a group inbox included below) before any folders appear here - with just one account, this only adds the section header. The All mail / Unread / Starred entries below are separate, admin-gated options that won't show up until your admin turns them on.",
"cross_account": {
"label": "Across all accounts",
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
@@ -1056,8 +1078,8 @@
"description": "Show the total message count next to folders and tags, alongside the unread count. Disable to show only unread counts."
},
"favicon_unread_badge": {
"label": "Unread Count on Tab Icon",
"description": "Show the inbox unread count as a badge on the browser tab icon, so new mail is visible when the tab is not focused. Disable to keep the icon unbadged."
"label": "Unread Count on Tab and App Icon",
"description": "Show the inbox unread count as a badge on the browser tab icon, and on the installed app icon where supported, so new mail is visible when the tab is not focused. Disable to keep both icons unbadged."
},
"pro_interface": {
"label": "Pro Interface (Experimental)",
@@ -1142,6 +1164,7 @@
"status_active": "Active on this device",
"status_inactive": "Not enabled on this device",
"status_unsupported": "This browser does not support Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "Working…",
"enable": "Enable",
"reenable": "Re-register",
@@ -1179,7 +1202,9 @@
"enabled": "Email notifications",
"enabled_desc": "Show notifications when new emails arrive",
"sound": "Notification sound",
"sound_desc": "Play an audio alert when new emails arrive"
"sound_desc": "Play an audio alert when new emails arrive",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Calendar Notifications",
@@ -1239,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Clear search when switching folders",
"description": "Clicking a folder (Inbox, Sent, …) clears the active search and browses that folder. When off, the search stays applied and re-runs in the folder you switch to."
},
"request_read_receipt": {
"label": "Request read receipts by default",
"description": "Pre-enable the read-receipt request when composing a new message."
@@ -1320,7 +1349,13 @@
},
"auto_select_reply_identity": {
"label": "Reply From Received Address",
"description": "When replying, send from the address the message was originally sent to. Matches identities first; for domain catch-all deliveries, rewrites the From header to the alias while sending through your primary identity."
"description": "When replying, send from the address the message was originally sent to. Your configured identities always match; whether other addresses on your domains count too (catch-all) is set by “Received Address Matching” below."
},
"reply_identity_match": {
"label": "Received Address Matching",
"description": "Which received addresses count as yours. Exact address only picks one of your configured identities. Same domain also treats any other address on one of your identity domains as a catch-all alias and rewrites the From header to it. Choose exact address if those addresses are distribution lists rather than aliases.",
"exact": "Exact address only",
"domain": "Any address on my domains"
},
"signature_position": {
"label": "Signature Position",
@@ -1751,7 +1786,9 @@
"warnings": {
"end_before_start": "End date must be after start date",
"start_in_past": "Start date is in the past",
"empty_body": "Message body is empty - recipients will receive a blank reply"
"empty_body": "Message body is empty - recipients will receive a blank reply",
"subject_too_long": "The subject is too long for the server (at most {max} bytes).",
"body_too_long": "The message is too long for the server (at most {max} bytes, formatting included)."
}
},
"folders": {
@@ -1768,6 +1805,8 @@
"role_trash": "Trash",
"role_junk": "Spam / Junk",
"role_archive": "Archive",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "None",
"create_folder": "Create Folder",
"create_subfolder": "Create Subfolder",
@@ -1878,6 +1917,8 @@
"description": "Import and export your contacts",
"group_by_letter_label": "Group by first letter",
"group_by_letter_description": "Show alphabetical section headers in the contact list",
"sort_by_last_name_label": "Sort by last name",
"sort_by_last_name_description": "Order the contact list by surname so family members appear together",
"import_label": "Import Contacts",
"import_description": "Import contacts from a vCard (.vcf) file",
"export_label": "Export Contacts",
@@ -1968,6 +2009,7 @@
"actions": "Actions",
"add_action": "Add Action",
"stop_processing": "Stop processing subsequent rules",
"include_spam": "Also move messages marked as spam",
"attachment_type_placeholder": "e.g. pdf, doc, jpg",
"value_placeholder_multi": "Value (multiple separated by commas)",
"condition_fields": {
@@ -2009,6 +2051,8 @@
"copy_to_folder": "Select folder",
"forward_to": "Forward to email address",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Keep a copy",
"forward_limit": "Forward limit per message on this server: {count}. Extra forwards are skipped.",
"reject_message": "Rejection message",
"reject_placeholder": "Your email has been rejected",
"label_name": "Tag name",
@@ -2205,6 +2249,7 @@
"new_subfolder": "New subfolder...",
"new_folder": "New folder...",
"rename": "Rename...",
"share": "Share...",
"import_email": "Import .eml or .zip...",
"empty_folder": "Empty folder",
"empty_folder_generic": "Empty folder",
@@ -2234,7 +2279,10 @@
"toast_error_rename": "Failed to rename folder",
"toast_error_delete": "Failed to delete folder",
"toast_error_delete_has_children": "Folder has subfolders. Remove them first.",
"toast_error_delete_has_email": "Folder is not empty. Empty it first."
"toast_error_delete_has_email": "Folder is not empty. Empty it first.",
"delete_with_contents_title": "Folder is not empty",
"delete_with_contents_message": "The folder \"{name}\" still contains {count, plural, one {1 message} other {# messages}}. Delete the folder and all of its messages? This cannot be undone.",
"delete_with_contents_confirm": "Delete folder and messages"
},
"shortcuts": {
"title": "Keyboard Shortcuts",
@@ -2268,7 +2316,8 @@
"search": "Focus search",
"help": "Show shortcuts",
"refresh": "Refresh emails",
"select_all": "Select all"
"select_all": "Select all",
"global_search": "Search everything"
},
"threads": {
"expand_collapse": "Expand/collapse thread"
@@ -2720,7 +2769,10 @@
"any_month": "Any month",
"has_email": "Has email",
"has_phone": "Has phone",
"has_photo": "Has photo"
"has_photo": "Has photo",
"sort_by": "Sort by",
"sort_first_name": "First name",
"sort_last_name": "Last name"
}
},
"calendar": {
@@ -2756,16 +2808,17 @@
"duplicate": "Duplicate",
"today_header": "Today",
"tomorrow_header": "Tomorrow",
"agenda_show_earlier": "Show earlier events",
"agenda_loading": "Loading…",
"agenda_range_start": "Showing events from {date}",
"agenda_range_end": "Showing events until {date}",
"export_ics": "Export as .ics",
"copy_title": "Copy title",
"copy_link": "Copy meeting link",
"new_event": "New event",
"new_all_day_event": "New all-day event",
"new_task": "New task",
"go_to_today": "Go to today",
"tap_to_create": "Tap + to create one",
"export_ics_short": ".ics",
"copy_link_short": "Link"
"go_to_today": "Go to today"
},
"detail": {
"add_note": "Add a note...",
@@ -2791,12 +2844,17 @@
"save": "Save",
"cancel": "Cancel",
"delete_confirm": "Are you sure you want to delete this event?",
"color": "Color",
"time": "Time",
"location_placeholder": "Place or address",
"description_placeholder": "Notes, agenda…"
"color": "Color"
},
"participants": {
"availability": {
"title": "Availability",
"free": "Available",
"busy": "Busy",
"tentative": "Tentative",
"unknown": "Not a user on this server",
"checking": "Checking…"
},
"title": "Participants",
"add": "Add participant",
"organizer": "Organizer",
@@ -2871,7 +2929,10 @@
"all_events": "All events",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete"
"delete": "Delete",
"rsvp_title": "Respond to recurring event",
"rsvp_description": "This is a recurring event. Which events does your response apply to?",
"respond": "Respond"
},
"alerts": {
"title": "Reminders",
@@ -2888,8 +2949,7 @@
"add": "Add reminder",
"remove": "Remove reminder",
"amount": "Reminder amount",
"unit": "Reminder unit",
"unit_minutes_after": "minutes after"
"unit": "Reminder unit"
},
"settings": {
"title": "Calendar settings",
@@ -2910,6 +2970,8 @@
"show_time_in_month_view_desc": "Display event times in the month calendar view. On small screens this shows full event entries instead of dots.",
"show_week_numbers": "Show week numbers",
"show_week_numbers_desc": "Display week numbers in the mini-calendar",
"calendar_free_scroll": "Free scrolling",
"calendar_free_scroll_desc": "Scroll continuously through months, weeks and days instead of one period at a time",
"enable_tasks": "Enable tasks",
"enable_tasks_desc": "Show a tasks view in the calendar for managing to-dos",
"show_tasks_on_calendar": "Show tasks on calendar",
@@ -2921,12 +2983,11 @@
"hover_preview_delay_1s": "1-second delay",
"hover_preview_delay_2s": "2-second delay",
"hover_preview_off": "Disabled",
"organizer_identity": "Organize invitations as",
"organizer_identity_desc": "The calendar address used as organizer when you invite people to events.",
"organizer_identity_failed": "Failed to change the organizer address",
"show_birthday_calendar": "Contact birthday calendar",
"show_birthday_calendar_desc": "Show a virtual calendar with birthdays from your contacts",
"time_zone": "Time zone",
"time_zone_desc": "Zone used for new events and for interpreting the calendar. \"Device\" follows the phone.",
"time_zone_auto": "Device time zone",
"show_week_numbers_mobile_desc": "Display week numbers in the month view."
"show_birthday_calendar_desc": "Show a virtual calendar with birthdays from your contacts"
},
"days": {
"monday": "Monday",
@@ -2982,6 +3043,9 @@
"alert_now": "Starting now",
"alert_in_minutes": "In {count} min",
"invitation_sent": "Invitations sent",
"invitations_denied_title": "Invitations can't be sent",
"invitations_denied": "The server refused to send the invitations: {reason}. Save the event without sending them?",
"save_without_invitations": "Save without invitations",
"rsvp_updated": "Response updated",
"rsvp_error": "Failed to update response",
"event_duplicated": "Event duplicated",
@@ -3011,7 +3075,7 @@
"supported_formats": "Supports iCalendar (.ics) files",
"url_description": "Enter the URL of an external iCalendar (.ics) feed to import events.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Supports CalDAV and iCalendar (.ics) URLs",
"url_hint": "Supports iCalendar (.ics) URLs",
"fetch": "Fetch",
"invalid_url": "Please enter a valid URL",
"url_fetch_failed": "Failed to fetch calendar from URL",
@@ -3076,15 +3140,7 @@
"kind_events": "Events",
"kind_tasks": "Tasks",
"kind_both": "Events and tasks",
"kind_hint": "Decides which apps see this calendar when it is synced over CalDAV. It cannot be changed later.",
"new_calendar": "New calendar",
"edit_calendar": "Edit calendar",
"description_placeholder": "Optional",
"clear_title": "Remove all events?",
"clear_description": "Every event in this calendar will be deleted. Events that also belong to another calendar are only unlinked.",
"clear_confirm": "Remove all",
"delete_title": "Delete calendar?",
"delete_description": "The calendar and all of its events will be deleted. This cannot be undone."
"kind_hint": "Decides which apps see this calendar when it is synced over CalDAV. It cannot be changed later."
},
"subscription": {
"title": "iCal Subscription",
@@ -3163,59 +3219,26 @@
"quick_add_placeholder": "Add a task...",
"due_today": "Today",
"due_tomorrow": "Tomorrow",
"overdue": "Overdue",
"add_placeholder": "Add a task…",
"add_time": "Add time"
},
"agenda": {
"no_upcoming": "No upcoming events",
"days_clear": "The next {count} days are clear."
},
"drawer": {
"no_calendars": "No calendars yet.",
"my_calendars": "My calendars",
"shared_with_me": "Shared with me",
"subscribed": "Subscribed"
},
"share": {
"title": "Share",
"shared_with": "Shared with",
"add_people": "Add people",
"search_placeholder": "Search by name or email",
"no_principals": "Sharing is not available on this server.",
"no_matches": "No matches",
"role_free_busy": "Free/busy",
"role_viewer": "Viewer",
"role_editor": "Editor",
"role_manager": "Manager",
"role_custom": "Custom",
"error": "Failed to update sharing"
},
"invitation": {
"reading": "Reading invitation…",
"title": "Calendar invitation",
"cancelled": "This event was cancelled",
"reply": "A participant replied to this invitation",
"organizer": "Organizer",
"join_video": "Join video call",
"already_in_calendar": "Already in your calendar",
"added": "Added to calendar",
"add_error": "Could not add the event",
"response_sent": "Response sent",
"response_error": "Could not send your response",
"add_to_calendar": "Add to calendar",
"no_writable_calendar": "No writable calendar available",
"accept": "Yes",
"tentative": "Maybe",
"decline": "No",
"trust_verified": "Sender verified",
"trust_sender_mismatch_unverified": "The sender does not match the organizer and the message is not authenticated.",
"trust_authentication_failed": "This message failed sender authentication (SPF/DKIM/DMARC).",
"trust_sender_mismatch": "The sender differs from the event organizer.",
"trust_authentication_missing": "The sender could not be verified."
},
"event_modal": {
"participant_placeholder": "Add participant by email"
"overdue": "Overdue"
}
},
"calendar_event_notifications": {
"someone": "Someone",
"invited": "{name} invited you to \"{title}\"",
"updated": "{name} updated \"{title}\"",
"cancelled": "{name} cancelled \"{title}\"",
"untitled": "Untitled event"
},
"share_notifications": {
"someone": "Someone",
"shared": "{name} shared the {kind} \"{object}\" with you",
"changed": "{name} changed your access to the {kind} \"{object}\"",
"revoked": "{name} removed your access to the {kind} \"{object}\"",
"object": {
"folder": "folder",
"calendar": "calendar",
"address_book": "address book",
"files": "file folder"
}
},
"sharing": {
@@ -3256,6 +3279,8 @@
"has_attachment": "Attachments",
"date_after": "After",
"date_before": "Before",
"size_min": "Larger than (KB)",
"size_max": "Smaller than (KB)",
"starred": "Starred",
"unread": "Unread",
"read": "Read",
@@ -3297,6 +3322,10 @@
"hint": "Click any email on the left to get started, or take the tour below."
},
"files": {
"office_edit": "Edit document",
"office_loading": "Opening document editor…",
"office_error": "The document editor could not be opened.",
"office_close": "Close editor",
"open_in_new_tab": "Open in new tab",
"title": "Files",
"search_placeholder": "Search files...",
@@ -3309,6 +3338,8 @@
"rename": "Rename",
"rename_title": "Rename",
"new_name": "New name",
"name_forbidden_chars": "The server does not allow these characters in names: {chars}",
"name_reserved": "This name is reserved on the server.",
"delete": "Delete",
"delete_confirm_title": "Delete resource",
"delete_confirm_message": "Are you sure you want to delete \"{name}\"? This cannot be undone.",
@@ -3409,31 +3440,7 @@
"disabled_description": "Large file uploads via WebDAV can cause Stalwart/RocksDB instability, including out-of-memory crashes and unrecoverable disk usage. Deleted files may not be immediately purged from blob storage. This feature is not recommended for production environments.",
"stability_warning": "Large file uploads can cause server instability. Deleted files may not be immediately purged from storage. Use with caution.",
"migration_title": "Updating your files…",
"migration_description": "Organising folders and files into their proper structure. This happens only once.",
"move_to": "Move to…",
"sharing_access": "Sharing & access",
"shared_with": "Shared with",
"share_add_people": "Add people",
"share_search_placeholder": "Search by name or email",
"share_no_users": "No other users found",
"share_no_matches": "No matches",
"share_folder_hint": "Sharing a folder shares everything inside it.",
"share_error": "Failed to update sharing",
"share_remove": "Remove access",
"share_empty": "Not shared with anyone yet",
"share_role_viewer": "Viewer",
"share_role_editor": "Editor",
"share_role_manager": "Manager",
"share_role_custom": "Custom",
"done": "Done",
"invalid_name": "Invalid name",
"invalid_name_slash": "Names cannot contain \"/\".",
"upload_unavailable": "The file picker is not installed in this build. Rebuild the app to enable uploads.",
"selected_count": "{count} selected",
"folder_empty": "This folder is empty",
"delete_item_title": "Delete this item?",
"delete_items_title": "Delete {count} items?",
"delete_cascade_hint": "Folders are deleted along with everything inside them. This can't be undone."
"migration_description": "Organising folders and files into their proper structure. This happens only once."
},
"smime": {
"your_certificates": "Your Certificates",
@@ -3594,5 +3601,45 @@
"copy_event": "Copy link to event",
"copied": "Link copied",
"copy_failed": "Couldn't copy the link"
},
"plugins": {
"oauth_callback": {
"completing": "Completing authorization…",
"completing_message": "Please wait while the connection is established.",
"received_title": "Authorization received",
"received_message": "You can return to the tab where you started the connection.",
"failed_title": "Authorization failed",
"denied": "Authorization was denied.",
"failed_detail": "Authorization failed: {error}",
"missing_params": "Missing authorization code or state.",
"delivery_failed": "The authorization result could not be delivered to the app tab. Please close this window and try again."
}
},
"global_search": {
"title": "Search everything",
"placeholder": "Search mail, contacts, calendar, files...",
"scope_label": "Search in",
"account_label": "Account",
"scope_all": "All",
"scope_mail": "Mail",
"scope_contacts": "Contacts",
"scope_calendar": "Calendar",
"scope_files": "Files",
"all_accounts": "All accounts",
"show_all": "Show all results",
"no_results": "No results for “{query}”",
"server_hint": "Complete words search the server; partial words match already-loaded items",
"searching": "Searching...",
"account_error": "Couldn't search {account}",
"recent_searches": "Recent searches",
"recurring": "Recurring",
"no_subject": "(no subject)",
"empty_hint": "Search mail, contacts, events and files across all accounts. Try from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Flat by date",
"grouped": "Grouped",
"load_more": "Load more",
"preview_empty": "Select a result to preview it",
"open_full": "Open",
"view_label": "View"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "y {count} más",
"draft": "Borrador",
"starred": "Destacado",
"pinned": "Fijado",
"replied": "Respondido",
"forwarded": "Reenviado",
"has_attachment": "Tiene adjunto",
"conversations_count": "{count} de {total} conversaciones",
"conversations_count_plus": "{count}+ conversaciones",
"conversations_count_simple": "{count} conversaciones",
@@ -292,6 +296,7 @@
"forward": "Reenviar",
"delete": "Eliminar",
"archive": "Archivar",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Destacar",
"unstar": "Quitar destacado",
"mark_unread": "Marcar como no leído",
@@ -303,12 +308,10 @@
"move": "Mover",
"print": "Imprimir",
"view_source": "Ver código fuente",
"fullscreen": "Pantalla completa",
"exit_fullscreen": "Salir de pantalla completa",
"export_email": "Exportar como .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Reenviar como archivo adjunto",
"import_email": "Importar .eml o .zip",
"keyboard_shortcuts": "Atajos de teclado (?)",
"email_source": "Código Fuente del Correo",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Confirmación de lectura solicitada (haz clic para desactivar)",
"read_receipt_off": "Solicitar confirmación de lectura",
"dsn_on": "Notificación de entrega solicitada (clic para desactivar)",
"dsn_off": "Solicitar notificación de entrega",
"format_plain_text": "Cambiar a texto sin formato",
"format_rich_text": "Cambiar a texto enriquecido (HTML)",
"require_tls_on": "Entrega cifrada obligatoria (clic para desactivar)",
"require_tls_off": "Exigir entrega cifrada (TLS)",
"new_message": "Nuevo Mensaje",
"signature_edit_hint": "Haz doble clic para editar la firma",
"reply": "Responder",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Anular",
"toggle_on": "Cancelar anulación",
"toggle_tooltip": "Edita libremente el nombre y la dirección del remitente. El correo aún se envía a través de tu identidad - solo cambia el encabezado De visible.",
"toggle_tooltip": "Edita libremente el nombre y la dirección del remitente. El correo aún se envía a través de tu identidad.",
"name_label": "Nombre del remitente",
"name_placeholder": "Nombre",
"email_label": "Dirección de correo del remitente",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Si tu servidor no acepta esta dirección como remitente del sobre, se usa {identity} en su lugar, y los destinatarios pueden verla en el encabezado Return-Path."
},
"use_template": "Plantilla",
"save_as_template": "Guardar como plantilla",
@@ -648,6 +658,8 @@
"upload_progress": "Subiendo {uploaded} / {total}",
"upload_cancel": "Cancelar subida",
"upload_failed": "Error al subir {filename}",
"attachment_too_large": "\"{name}\" supera el tamaño que permite el servidor ({max} por archivo)",
"attachments_total_too_large": "Los adjuntos superarían el límite del servidor de {max} por mensaje",
"send_failed": "Error al enviar el correo",
"smime_sign_on": "Firma S/MIME activada",
"smime_sign_off": "Firma S/MIME desactivada",
@@ -726,7 +738,11 @@
"delete_column": "Eliminar columna",
"toggle_header_row": "Alternar fila de encabezado",
"delete_table": "Eliminar tabla",
"pick_size": "Elegir tamaño"
"pick_size": "Elegir tamaño",
"font_size": "Tamaño de fuente",
"font_size_default": "Predeterminado",
"background_color": "Color de fondo",
"remove_background_color": "Quitar color de fondo"
},
"send_filing_warning": "Enviado - pero la limpieza posterior falló, puede quedar un borrador obsoleto."
},
@@ -765,6 +781,10 @@
"email_sent": "Correo enviado exitosamente",
"email_deleted": "Correo eliminado",
"email_archived": "Correo archivado",
"emails_deleted": "{count, plural, one {# correo eliminado} other {# correos eliminados}}",
"emails_archived": "{count, plural, one {# correo archivado} other {# correos archivados}}",
"error_archiving": "No se pudo archivar el correo",
"error_updating": "No se pudo actualizar el correo",
"email_starred": "Correo destacado",
"email_unstarred": "Correo sin destacar",
"email_marked_read": "Correo marcado como leído",
@@ -1143,7 +1163,9 @@
"enabled": "Notificaciones de correo",
"enabled_desc": "Mostrar notificaciones cuando lleguen nuevos correos",
"sound": "Sonido de notificación",
"sound_desc": "Reproducir un sonido cuando lleguen nuevos correos"
"sound_desc": "Reproducir un sonido cuando lleguen nuevos correos",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Notificaciones de calendario",
@@ -1172,6 +1194,7 @@
"status_busy": "Trabajando…",
"status_inactive": "No habilitado en este dispositivo",
"status_unsupported": "Este navegador no admite Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Notificaciones en segundo plano",
"devices_title": "Dispositivos registrados",
"devices_desc": "Todos los dispositivos registrados para recibir notificaciones en segundo plano de esta cuenta. Al revocar uno, deja de recibirlas de inmediato.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Borrar la búsqueda al cambiar de carpeta",
"description": "Al hacer clic en una carpeta (Bandeja de entrada, Enviados…) se borra la búsqueda activa y se muestra esa carpeta. Si está desactivado, la búsqueda se mantiene y se vuelve a ejecutar en la carpeta a la que cambies."
},
"request_read_receipt": {
"label": "Solicitar confirmaciones de lectura de forma predeterminada",
"description": "Activar previamente la solicitud de confirmación de lectura al redactar un mensaje nuevo."
@@ -1316,6 +1343,12 @@
"label": "Seleccionar dirección de respuesta automáticamente",
"description": "Al responder, cambia automáticamente la dirección del remitente a la identidad que recibió el mensaje original"
},
"reply_identity_match": {
"label": "Coincidencia de la dirección de recepción",
"description": "Qué direcciones de recepción cuentan como tuyas. «Solo la dirección exacta» elige únicamente una de tus identidades configuradas; «mismo dominio» también trata cualquier otra dirección de tus dominios de identidad como un alias catch-all y reescribe la cabecera De con ella. Elige la dirección exacta si esas direcciones son listas de distribución.",
"exact": "Solo la dirección exacta",
"domain": "Cualquier dirección de mis dominios"
},
"signature_position": {
"label": "Posición de la firma",
"description": "Dónde insertar tu firma en respuestas y reenvíos. Encima del texto citado se lee de forma natural como cierre de la respuesta; debajo mantiene el mensaje original contiguo.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "La fecha de fin debe ser posterior a la fecha de inicio",
"start_in_past": "La fecha de inicio está en el pasado",
"empty_body": "El cuerpo del mensaje está vacío - los destinatarios recibirán una respuesta en blanco"
"empty_body": "El cuerpo del mensaje está vacío - los destinatarios recibirán una respuesta en blanco",
"subject_too_long": "El asunto es demasiado largo para el servidor (máximo {max} bytes).",
"body_too_long": "El mensaje es demasiado largo para el servidor (máximo {max} bytes, formato incluido)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Papelera",
"role_junk": "Spam / No deseado",
"role_archive": "Archivo",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Ninguno",
"create_folder": "Crear carpeta",
"create_subfolder": "Crear subcarpeta",
@@ -1882,7 +1919,9 @@
"categories_description": "Renombrar categorías de contactos",
"no_categories": "No se encontraron categorías",
"group_by_letter_description": "Mostrar encabezados alfabéticos en la lista de contactos",
"group_by_letter_label": "Agrupar por primera letra"
"group_by_letter_label": "Agrupar por primera letra",
"sort_by_last_name_label": "Ordenar por apellido",
"sort_by_last_name_description": "Ordenar la lista de contactos por apellido para que los miembros de una familia aparezcan juntos"
},
"downloads": {
"title": "Descargas",
@@ -1963,6 +2002,7 @@
"actions": "Acciones",
"add_action": "Agregar acción",
"stop_processing": "Detener el procesamiento de reglas posteriores",
"include_spam": "Mover también los mensajes marcados como spam",
"attachment_type_placeholder": "p. ej. pdf, doc, jpg",
"value_placeholder_multi": "Valor (varios separados por comas)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Seleccionar carpeta",
"forward_to": "Reenviar a dirección de correo",
"forward_placeholder": "correo@ejemplo.com",
"forward_keep_copy": "Conservar una copia",
"forward_limit": "Límite de reenvíos por mensaje en este servidor: {count}. Los reenvíos adicionales se omiten.",
"reject_message": "Mensaje de rechazo",
"reject_placeholder": "Su correo ha sido rechazado",
"label_name": "Nombre de la etiqueta",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Nueva subcarpeta...",
"new_folder": "Nueva carpeta...",
"rename": "Renombrar...",
"share": "Compartir...",
"import_email": "Importar .eml o .zip...",
"empty_folder": "Vaciar carpeta",
"empty_folder_generic": "Vaciar carpeta",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Error al eliminar la carpeta",
"toast_error_delete_has_children": "La carpeta tiene subcarpetas. Elimínelas primero.",
"toast_error_delete_has_email": "La carpeta no está vacía. Vacíela primero.",
"delete_with_contents_title": "La carpeta no está vacía",
"delete_with_contents_message": "La carpeta \"{name}\" aún contiene {count, plural, one {1 mensaje} other {# mensajes}}. ¿Eliminar la carpeta y todos sus mensajes? Esta acción no se puede deshacer.",
"delete_with_contents_confirm": "Eliminar carpeta y mensajes",
"placeholder_folder_name": "Nombre de carpeta",
"create": "Crear",
"rename_confirm": "Renombrar"
@@ -2270,7 +2316,8 @@
"search": "Enfocar búsqueda",
"help": "Mostrar atajos",
"refresh": "Actualizar correos",
"select_all": "Seleccionar todo"
"select_all": "Seleccionar todo",
"global_search": "Buscar en todo"
},
"threads": {
"expand_collapse": "Expandir/contraer conversación"
@@ -2721,7 +2768,10 @@
"any_month": "Cualquier mes",
"has_email": "Con correo",
"has_phone": "Con teléfono",
"has_photo": "Con foto"
"has_photo": "Con foto",
"sort_by": "Ordenar por",
"sort_first_name": "Nombre",
"sort_last_name": "Apellido"
},
"open_categories": "Abrir categorías"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Duplicar",
"today_header": "Hoy",
"tomorrow_header": "Mañana",
"agenda_show_earlier": "Mostrar eventos anteriores",
"agenda_loading": "Cargando…",
"agenda_range_start": "Eventos desde el {date}",
"agenda_range_end": "Eventos hasta el {date}",
"export_ics": "Exportar como .ics",
"copy_title": "Copiar título",
"copy_link": "Copiar enlace de reunión",
@@ -2793,6 +2847,14 @@
"color": "Color"
},
"participants": {
"availability": {
"title": "Disponibilidad",
"free": "Disponible",
"busy": "Ocupado",
"tentative": "Provisional",
"unknown": "No es un usuario de este servidor",
"checking": "Comprobando…"
},
"title": "Participantes",
"add": "Agregar participante",
"organizer": "Organizador",
@@ -2867,7 +2929,10 @@
"all_events": "Todos los eventos",
"cancel": "Cancelar",
"save": "Guardar",
"delete": "Eliminar"
"delete": "Eliminar",
"rsvp_title": "Responder a evento recurrente",
"rsvp_description": "Este es un evento recurrente. ¿A qué eventos se aplica su respuesta?",
"respond": "Responder"
},
"alerts": {
"title": "Recordatorio",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Mostrar las horas de los eventos en la vista mensual del calendario",
"show_week_numbers": "Mostrar números de semana",
"show_week_numbers_desc": "Mostrar los números de semana en el minicalendario",
"calendar_free_scroll": "Desplazamiento libre",
"calendar_free_scroll_desc": "Desplazarse de forma continua por meses, semanas y días en lugar de un periodo cada vez",
"enable_tasks": "Activar tareas",
"enable_tasks_desc": "Mostrar una vista de tareas en el calendario para gestionar tareas pendientes",
"show_tasks_on_calendar": "Mostrar tareas en el calendario",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Retraso de 1 segundo",
"hover_preview_delay_2s": "Retraso de 2 segundos",
"hover_preview_off": "Desactivado",
"organizer_identity": "Organizar invitaciones como",
"organizer_identity_desc": "La dirección de calendario usada como organizador al invitar a personas a eventos.",
"organizer_identity_failed": "No se pudo cambiar la dirección del organizador",
"show_birthday_calendar": "Calendario de cumpleaños",
"show_birthday_calendar_desc": "Mostrar un calendario virtual con los cumpleaños de tus contactos"
},
@@ -2947,6 +3017,9 @@
"alert_now": "Comienza ahora",
"alert_in_minutes": "En {count} min",
"invitation_sent": "Invitaciones enviadas",
"invitations_denied_title": "No se pueden enviar las invitaciones",
"invitations_denied": "El servidor rechazó enviar las invitaciones: {reason}. ¿Guardar el evento sin enviarlas?",
"save_without_invitations": "Guardar sin invitaciones",
"rsvp_updated": "Respuesta actualizada",
"rsvp_error": "Error al actualizar la respuesta",
"event_duplicated": "Evento duplicado",
@@ -2975,7 +3048,7 @@
"supported_formats": "Archivos iCalendar (.ics) compatibles",
"url_description": "Introduce la URL de un feed iCalendar (.ics) externo para importar eventos.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Compatible con URLs CalDAV e iCalendar (.ics)",
"url_hint": "Compatible con URLs iCalendar (.ics)",
"fetch": "Obtener",
"invalid_url": "Introduce una URL válida",
"url_fetch_failed": "No se pudo obtener el calendario desde la URL",
@@ -3164,6 +3237,8 @@
"has_attachment": "Adjuntos",
"date_after": "Después de",
"date_before": "Antes de",
"size_min": "Mayor que (KB)",
"size_max": "Menor que (KB)",
"starred": "Destacados",
"unread": "No leídos",
"read": "Leídos",
@@ -3205,6 +3280,10 @@
"hint": "Haz clic en un correo a la izquierda para empezar, o inicia el tour."
},
"files": {
"office_edit": "Editar documento",
"office_loading": "Abriendo el editor de documentos…",
"office_error": "No se pudo abrir el editor de documentos.",
"office_close": "Cerrar editor",
"open_in_new_tab": "Abrir en una pestaña nueva",
"title": "Archivos",
"search_placeholder": "Buscar archivos...",
@@ -3217,6 +3296,8 @@
"rename": "Renombrar",
"rename_title": "Renombrar",
"new_name": "Nuevo nombre",
"name_forbidden_chars": "El servidor no permite estos caracteres en los nombres: {chars}",
"name_reserved": "Este nombre está reservado en el servidor.",
"delete": "Eliminar",
"delete_confirm_title": "Eliminar recurso",
"delete_confirm_message": "¿Está seguro de que desea eliminar \"{name}\"? Esta acción no se puede deshacer.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "La búsqueda no está disponible en la vista unificada"
},
"calendar_event_notifications": {
"someone": "Alguien",
"invited": "{name} te ha invitado a \"{title}\"",
"updated": "{name} ha actualizado \"{title}\"",
"cancelled": "{name} ha cancelado \"{title}\"",
"untitled": "Evento sin título"
},
"share_notifications": {
"someone": "Alguien",
"shared": "{name} ha compartido contigo {kind} \"{object}\"",
"changed": "{name} ha cambiado tu acceso a {kind} \"{object}\"",
"revoked": "{name} ha retirado tu acceso a {kind} \"{object}\"",
"object": {
"folder": "la carpeta",
"calendar": "el calendario",
"address_book": "la libreta de direcciones",
"files": "la carpeta de archivos"
}
},
"sharing": {
"title": "Compartir «{name}»",
"description": "Concede acceso a otros usuarios o grupos de este servidor. Los cambios surten efecto inmediatamente.",
@@ -3501,5 +3601,45 @@
"copy_event": "Copiar enlace del evento",
"copied": "Enlace copiado",
"copy_failed": "No se pudo copiar el enlace"
},
"plugins": {
"oauth_callback": {
"completing": "Completando la autorización…",
"completing_message": "Por favor, espera mientras se establece la conexión.",
"received_title": "Autorización recibida",
"received_message": "Puedes volver a la pestaña donde iniciaste la conexión.",
"failed_title": "Error de autorización",
"denied": "Se denegó la autorización.",
"failed_detail": "Error de autorización: {error}",
"missing_params": "Falta el código de autorización o el estado.",
"delivery_failed": "No se pudo entregar el resultado de la autorización a la pestaña de la aplicación. Cierre esta ventana e inténtelo de nuevo."
}
},
"global_search": {
"title": "Buscar en todo",
"placeholder": "Buscar correo, contactos, calendario, archivos...",
"scope_label": "Buscar en",
"account_label": "Cuenta",
"scope_all": "Todo",
"scope_mail": "Correo",
"scope_contacts": "Contactos",
"scope_calendar": "Calendario",
"scope_files": "Archivos",
"all_accounts": "Todas las cuentas",
"show_all": "Mostrar todos los resultados",
"no_results": "Sin resultados para “{query}”",
"server_hint": "Las palabras completas buscan en el servidor; las parciales coinciden con lo ya cargado",
"searching": "Buscando...",
"account_error": "No se pudo buscar en {account}",
"recent_searches": "Búsquedas recientes",
"recurring": "Periódico",
"no_subject": "(no subject)",
"empty_hint": "Busca correo, contactos, eventos y archivos en todas las cuentas. Prueba from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Plano por fecha",
"grouped": "Agrupado",
"load_more": "Cargar más",
"preview_empty": "Selecciona un resultado para previsualizarlo",
"open_full": "Abrir",
"view_label": "Vista"
}
}
+152 -12
View File
@@ -225,6 +225,10 @@
"and_others": "و {count} نفر دیگر",
"draft": "پیش‌نویس",
"starred": "ستاره‌دار",
"pinned": "سنجاق‌شده",
"replied": "پاسخ داده شده",
"forwarded": "بازارسال شده",
"has_attachment": "دارای پیوست",
"conversations_count": "{count} از {total} مکالمه",
"conversations_count_plus": "+{count} مکالمه",
"conversations_count_simple": "{count} مکالمه",
@@ -292,6 +296,7 @@
"forward": "ارسال مجدد",
"delete": "حذف",
"archive": "بایگانی",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "ستاره‌دار کردن",
"unstar": "حذف ستاره",
"mark_unread": "علامت‌گذاری خوانده نشده",
@@ -303,12 +308,10 @@
"move": "انتقال",
"print": "چاپ",
"view_source": "مشاهده منبع",
"fullscreen": "تمام‌صفحه",
"exit_fullscreen": "خروج از تمام‌صفحه",
"export_email": "خروجی .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "ارسال به‌عنوان پیوست",
"import_email": "وارد کردن .eml یا .zip",
"keyboard_shortcuts": "میانبرهای صفحه کلید (?)",
"email_source": "منبع ایمیل",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "درخواست تأیید خواندن فعال (کلیک برای غیرفعال کردن)",
"read_receipt_off": "درخواست تأیید خواندن",
"dsn_on": "اعلان تحویل درخواست شد (برای غیرفعال کردن کلیک کنید)",
"dsn_off": "درخواست اعلان تحویل",
"format_plain_text": "تغییر به متن ساده",
"format_rich_text": "تغییر به متن غنی (HTML)",
"require_tls_on": "تحویل رمزگذاری‌شده الزامی است (برای غیرفعال کردن کلیک کنید)",
"require_tls_off": "الزام تحویل رمزگذاری‌شده (TLS)",
"new_message": "پیام جدید",
"signature_edit_hint": "برای ویرایش امضا دوبار کلیک کنید",
"reply": "پاسخ",
@@ -638,7 +647,8 @@
"name_label": "نام فرستنده",
"name_placeholder": "نام",
"email_label": "آدرس ایمیل فرستنده",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "اگر سرور شما این آدرس را به‌عنوان فرستندهٔ پاکت نپذیرد، به‌جای آن {identity} استفاده می‌شود و گیرندگان می‌توانند آن را در سرآیند Return-Path ببینند."
},
"use_template": "قالب",
"save_as_template": "ذخیره به عنوان قالب",
@@ -651,6 +661,8 @@
"upload_progress": "در حال بارگذاری {uploaded} / {total}",
"upload_cancel": "لغو بارگذاری",
"upload_failed": "بارگذاری {filename} ناموفق بود",
"attachment_too_large": "«{name}» بزرگ‌تر از حدی است که سرور اجازه می‌دهد ({max} برای هر فایل)",
"attachments_total_too_large": "پیوست‌ها از سقف {max} سرور برای هر پیام بیشتر می‌شوند",
"drop_files": "فایل‌ها را اینجا رها کنید",
"show_less": "نمایش کمتر",
"send_failed": "ارسال ایمیل ناموفق بود",
@@ -726,7 +738,11 @@
"delete_column": "حذف ستون",
"toggle_header_row": "تغییر وضعیت ردیف سرصفحه",
"delete_table": "حذف جدول",
"pick_size": "انتخاب اندازه"
"pick_size": "انتخاب اندازه",
"font_size": "اندازه فونت",
"font_size_default": "پیش‌فرض",
"background_color": "رنگ پس‌زمینه",
"remove_background_color": "حذف رنگ پس‌زمینه"
},
"send_filing_warning": "ارسال شد - اما پاک‌سازی پس از ارسال ناموفق بود، ممکن است پیش‌نویس قدیمی باقی بماند."
},
@@ -765,6 +781,10 @@
"email_sent": "ایمیل با موفقیت ارسال شد",
"email_deleted": "ایمیل حذف شد",
"email_archived": "ایمیل بایگانی شد",
"emails_deleted": "{count, plural, other {# ایمیل حذف شد}}",
"emails_archived": "{count, plural, other {# ایمیل بایگانی شد}}",
"error_archiving": "بایگانی ایمیل ناموفق بود",
"error_updating": "به‌روزرسانی ایمیل ناموفق بود",
"email_starred": "ایمیل ستاره‌دار شد",
"email_unstarred": "ستاره ایمیل برداشته شد",
"email_marked_read": "ایمیل خوانده شده علامت‌گذاری شد",
@@ -1144,6 +1164,7 @@
"status_active": "فعال روی این دستگاه",
"status_inactive": "فعال نشده",
"status_unsupported": "مرورگر پشتیبانی نمی‌کند",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "در حال کار…",
"enable": "فعال کردن",
"reenable": "ثبت مجدد",
@@ -1181,7 +1202,9 @@
"enabled": "اعلان‌های ایمیل",
"enabled_desc": "نمایش اعلان هنگام ایمیل جدید",
"sound": "صدای اعلان",
"sound_desc": "پخش هشدار صوتی"
"sound_desc": "پخش هشدار صوتی",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "اعلان‌های تقویم",
@@ -1241,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "پاک کردن جستجو هنگام تغییر پوشه",
"description": "کلیک روی یک پوشه (صندوق ورودی، ارسال‌شده…) جستجوی فعال را پاک می‌کند و آن پوشه را نشان می‌دهد. وقتی خاموش باشد، جستجو باقی می‌ماند و در پوشهٔ جدید دوباره اجرا می‌شود."
},
"request_read_receipt": {
"label": "درخواست تأیید خواندن",
"description": "پیش‌فعال کردن تأیید خواندن"
@@ -1324,6 +1351,12 @@
"label": "پاسخ از آدرس دریافتی",
"description": "ارسال با آدرس دریافت شده"
},
"reply_identity_match": {
"label": "تطبیق نشانی دریافت",
"description": "کدام نشانی‌های دریافت به‌عنوان نشانی شما در نظر گرفته شوند. «فقط نشانی دقیق» تنها یکی از هویت‌های پیکربندی‌شدهٔ شما را انتخاب می‌کند؛ «همان دامنه» هر نشانی دیگری در دامنه‌های هویت شما را نیز نام مستعار catch-all در نظر می‌گیرد و سرآیند فرستنده را با آن بازنویسی می‌کند. اگر این نشانی‌ها فهرست توزیع هستند، نشانی دقیق را انتخاب کنید.",
"exact": "فقط نشانی دقیق",
"domain": "هر نشانی در دامنه‌های من"
},
"signature_position": {
"label": "موقعیت امضا",
"description": "محل درج امضا",
@@ -1753,7 +1786,9 @@
"warnings": {
"end_before_start": "تاریخ پایان باید بعد از تاریخ شروع باشد",
"start_in_past": "تاریخ شروع در گذشته است",
"empty_body": "بدنه پیام خالی است - گیرندگان پاسخ خالی دریافت می‌کنند"
"empty_body": "بدنه پیام خالی است - گیرندگان پاسخ خالی دریافت می‌کنند",
"subject_too_long": "موضوع برای سرور بیش از حد طولانی است (حداکثر {max} بایت).",
"body_too_long": "پیام برای سرور بیش از حد طولانی است (حداکثر {max} بایت، همراه با قالب‌بندی)."
}
},
"folders": {
@@ -1770,6 +1805,8 @@
"role_trash": "زباله‌دان",
"role_junk": "هرزنامه",
"role_archive": "بایگانی",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "هیچکدام",
"create_folder": "ایجاد پوشه",
"create_subfolder": "ایجاد زیرپوشه",
@@ -1879,6 +1916,8 @@
"title": "مخاطبین",
"description": "وارد کردن و خروجی مخاطبین",
"group_by_letter_label": "گروه‌بندی بر اساس حرف اول",
"sort_by_last_name_label": "مرتب‌سازی بر اساس نام خانوادگی",
"sort_by_last_name_description": "لیست مخاطبین را بر اساس نام خانوادگی مرتب کنید تا اعضای خانواده کنار هم نمایش داده شوند",
"group_by_letter_description": "نمایش هدرهای الفبایی در لیست مخاطبین",
"import_label": "وارد کردن مخاطبین",
"import_description": "وارد کردن مخاطبین از فایل vCard (.vcf)",
@@ -1970,6 +2009,7 @@
"actions": "عملیات‌ها",
"add_action": "افزودن عملیات",
"stop_processing": "توقف پردازش قوانین بعدی",
"include_spam": "پیام‌های علامت‌خورده به‌عنوان هرزنامه هم منتقل شوند",
"attachment_type_placeholder": "مثال: pdf, doc, jpg",
"value_placeholder_multi": "مقدار (چندتا با کاما جدا شوند)",
"condition_fields": {
@@ -2011,6 +2051,8 @@
"copy_to_folder": "انتخاب پوشه",
"forward_to": "ارسال به آدرس ایمیل",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "نگه‌داشتن یک نسخه",
"forward_limit": "سقف بازارسال برای هر پیام در این سرور: {count}. بازارسال‌های اضافه نادیده گرفته می‌شوند.",
"reject_message": "پیام رد",
"reject_placeholder": "ایمیل شما رد شد",
"label_name": "نام برچسب",
@@ -2207,6 +2249,7 @@
"new_subfolder": "زیرپوشه جدید...",
"new_folder": "پوشه جدید...",
"rename": "تغییر نام...",
"share": "اشتراک‌گذاری...",
"import_email": "وارد کردن .eml/.zip...",
"empty_folder": "خالی کردن پوشه",
"empty_folder_generic": "خالی کردن پوشه",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "خطای تغییر نام",
"toast_error_delete": "خطای حذف",
"toast_error_delete_has_children": "زیرپوشه دارد",
"toast_error_delete_has_email": "خالی نیست"
"toast_error_delete_has_email": "خالی نیست",
"delete_with_contents_title": "پوشه خالی نیست",
"delete_with_contents_message": "پوشه «{name}» هنوز شامل {count, plural, one {۱ پیام} other {# پیام}} است. پوشه و همه پیام‌های آن حذف شود؟ این عمل قابل بازگشت نیست.",
"delete_with_contents_confirm": "حذف پوشه و پیام‌ها"
},
"shortcuts": {
"title": "میانبرهای صفحه کلید",
@@ -2270,7 +2316,8 @@
"search": "تمرکز جستجو",
"help": "نمایش میانبرها",
"refresh": "بازنشانی ایمیل‌ها",
"select_all": "انتخاب همه"
"select_all": "انتخاب همه",
"global_search": "جستجو در همه جا"
},
"threads": {
"expand_collapse": "باز/بسته کردن رشته"
@@ -2722,7 +2769,10 @@
"any_month": "هر ماه",
"has_email": "دارای ایمیل",
"has_phone": "دارای تلفن",
"has_photo": "دارای عکس"
"has_photo": "دارای عکس",
"sort_by": "مرتب‌سازی بر اساس",
"sort_first_name": "نام",
"sort_last_name": "نام خانوادگی"
}
},
"calendar": {
@@ -2758,6 +2808,10 @@
"duplicate": "کپی",
"today_header": "امروز",
"tomorrow_header": "فردا",
"agenda_show_earlier": "نمایش رویدادهای قبلی",
"agenda_loading": "در حال بارگذاری…",
"agenda_range_start": "رویدادها از {date}",
"agenda_range_end": "رویدادها تا {date}",
"export_ics": "خروجی .ics",
"copy_title": "کپی عنوان",
"copy_link": "کپی لینک جلسه",
@@ -2793,6 +2847,14 @@
"color": "رنگ"
},
"participants": {
"availability": {
"title": "در دسترس بودن",
"free": "در دسترس",
"busy": "مشغول",
"tentative": "احتمالی",
"unknown": "کاربر این سرور نیست",
"checking": "در حال بررسی…"
},
"title": "شرکت‌کنندگان",
"add": "افزودن شرکت‌کننده",
"organizer": "برگزارکننده",
@@ -2867,7 +2929,10 @@
"all_events": "همه رویدادها",
"cancel": "انصراف",
"save": "ذخیره",
"delete": "حذف"
"delete": "حذف",
"rsvp_title": "پاسخ به رویداد تکراری",
"rsvp_description": "این یک رویداد تکراری است. پاسخ شما برای کدام رویدادها اعمال شود؟",
"respond": "پاسخ"
},
"alerts": {
"title": "یادآورها",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "نمایش زمان رویدادها در نمای ماه تقویم",
"show_week_numbers": "نمایش شماره هفته",
"show_week_numbers_desc": "نمایش شماره هفته در تقویم کوچک",
"calendar_free_scroll": "پیمایش آزاد",
"calendar_free_scroll_desc": "پیمایش پیوسته در ماه‌ها، هفته‌ها و روزها به جای یک بازه در هر بار",
"enable_tasks": "فعال کردن وظایف",
"enable_tasks_desc": "نمایش نمای وظایف در تقویم برای مدیریت کارها",
"show_tasks_on_calendar": "نمایش وظایف روی تقویم",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "تأخیر ۱ ثانیه",
"hover_preview_delay_2s": "تأخیر ۲ ثانیه",
"hover_preview_off": "غیرفعال",
"organizer_identity": "سازمان‌دهی دعوت‌ها به‌عنوان",
"organizer_identity_desc": "آدرس تقویمی که هنگام دعوت افراد به رویدادها به‌عنوان برگزارکننده استفاده می‌شود.",
"organizer_identity_failed": "تغییر آدرس برگزارکننده ناموفق بود",
"show_birthday_calendar": "تقویم تولد مخاطبین",
"show_birthday_calendar_desc": "نمایش تقویم مجازی با تولدهای مخاطبین"
},
@@ -2973,6 +3043,9 @@
"alert_now": "همین الان شروع می‌شود",
"alert_in_minutes": "{count} دقیقه دیگر",
"invitation_sent": "دعوت‌نامه‌ها ارسال شد",
"invitations_denied_title": "ارسال دعوت‌نامه‌ها ممکن نیست",
"invitations_denied": "سرور از ارسال دعوت‌نامه‌ها خودداری کرد: {reason}. رویداد بدون ارسال آن‌ها ذخیره شود؟",
"save_without_invitations": "ذخیره بدون دعوت‌نامه",
"rsvp_updated": "پاسخ به‌روزرسانی شد",
"rsvp_error": "به‌روزرسانی پاسخ ناموفق بود",
"event_duplicated": "رویداد کپی شد",
@@ -3002,7 +3075,7 @@
"supported_formats": "پشتیبانی از فایل‌های iCalendar (.ics)",
"url_description": "آدرس فید خارجی iCalendar (.ics) را برای وارد کردن رویدادها وارد کنید.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "پشتیبانی از آدرس‌های CalDAV و iCalendar (.ics)",
"url_hint": "پشتیبانی از آدرس‌های iCalendar (.ics)",
"fetch": "دریافت",
"invalid_url": "لطفاً یک آدرس معتبر وارد کنید",
"url_fetch_failed": "دریافت تقویم از آدرس ناموفق بود",
@@ -3149,6 +3222,25 @@
"overdue": "عقب‌افتاده"
}
},
"calendar_event_notifications": {
"someone": "کسی",
"invited": "{name} شما را به «{title}» دعوت کرد",
"updated": "{name} «{title}» را به‌روزرسانی کرد",
"cancelled": "{name} «{title}» را لغو کرد",
"untitled": "رویداد بدون عنوان"
},
"share_notifications": {
"someone": "کسی",
"shared": "{name} {kind} «{object}» را با شما به اشتراک گذاشت",
"changed": "{name} دسترسی شما به {kind} «{object}» را تغییر داد",
"revoked": "{name} دسترسی شما به {kind} «{object}» را حذف کرد",
"object": {
"folder": "پوشه",
"calendar": "تقویم",
"address_book": "دفترچه آدرس",
"files": "پوشه فایل"
}
},
"sharing": {
"title": "اشتراک‌گذاری \"{name}\"",
"description": "دسترسی به سایر کاربران یا گروه‌های این سرور را فراهم کنید.",
@@ -3187,6 +3279,8 @@
"has_attachment": "پیوست",
"date_after": "بعد از",
"date_before": "قبل از",
"size_min": "بزرگ‌تر از (کیلوبایت)",
"size_max": "کوچک‌تر از (کیلوبایت)",
"starred": "ستاره‌دار",
"unread": "خوانده نشده",
"read": "خوانده شده",
@@ -3228,6 +3322,10 @@
"hint": "روی هر ایمیل در سمت چپ کلیک کنید یا تور را ببینید."
},
"files": {
"office_edit": "ویرایش سند",
"office_loading": "در حال باز کردن ویرایشگر سند…",
"office_error": "ویرایشگر سند باز نشد.",
"office_close": "بستن ویرایشگر",
"open_in_new_tab": "باز کردن در تب جدید",
"title": "فایل‌ها",
"search_placeholder": "جستجوی فایل‌ها...",
@@ -3240,6 +3338,8 @@
"rename": "تغییر نام",
"rename_title": "تغییر نام",
"new_name": "نام جدید",
"name_forbidden_chars": "سرور این نویسه‌ها را در نام‌ها مجاز نمی‌داند: {chars}",
"name_reserved": "این نام در سرور رزرو شده است.",
"delete": "حذف",
"delete_confirm_title": "حذف منبع",
"delete_confirm_message": "آیا از حذف \"{name}\" اطمینان دارید؟ این عملیات قابل بازگشت نیست.",
@@ -3501,5 +3601,45 @@
"copy_event": "کپی پیوند رویداد",
"copied": "پیوند کپی شد",
"copy_failed": "کپی پیوند ممکن نشد"
},
"plugins": {
"oauth_callback": {
"completing": "در حال تکمیل احراز هویت…",
"completing_message": "لطفاً تا برقراری اتصال منتظر بمانید.",
"received_title": "احراز هویت دریافت شد",
"received_message": "می‌توانید به برگه‌ای که اتصال را در آن آغاز کردید بازگردید.",
"failed_title": "احراز هویت ناموفق بود",
"denied": "احراز هویت رد شد.",
"failed_detail": "احراز هویت ناموفق بود: {error}",
"missing_params": "کد احراز هویت یا حالت وجود ندارد.",
"delivery_failed": "نتیجه احراز هویت به برگه برنامه ارسال نشد. لطفاً این پنجره را ببندید و دوباره تلاش کنید."
}
},
"global_search": {
"title": "جستجو در همه جا",
"placeholder": "جستجو در ایمیل، مخاطبین، تقویم، فایل‌ها...",
"scope_label": "جستجو در",
"account_label": "حساب",
"scope_all": "همه",
"scope_mail": "ایمیل",
"scope_contacts": "مخاطبین",
"scope_calendar": "تقویم",
"scope_files": "فایل‌ها",
"all_accounts": "همه حساب‌ها",
"show_all": "نمایش همه نتایج",
"no_results": "نتیجه‌ای برای “{query}” یافت نشد",
"server_hint": "کلمات کامل در سرور جستجو می‌شوند؛ بخشی از کلمات با موارد بارگذاری‌شده مطابقت دارد",
"searching": "در حال جستجو...",
"account_error": "جستجو در {account} ممکن نشد",
"recent_searches": "جستجوهای اخیر",
"recurring": "تکرارشونده",
"no_subject": "(no subject)",
"empty_hint": "در ایمیل، مخاطبین، رویدادها و فایل‌های همه حساب‌ها جستجو کنید. امتحان کنید: from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "تخت بر اساس تاریخ",
"grouped": "گروه‌بندی‌شده",
"load_more": "بارگذاری بیشتر",
"preview_empty": "برای پیش‌نمایش یک نتیجه انتخاب کنید",
"open_full": "باز کردن",
"view_label": "نما"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "et {count} autres",
"draft": "Brouillon",
"starred": "Favori",
"pinned": "Épinglé",
"replied": "Répondu",
"forwarded": "Transféré",
"has_attachment": "Avec pièce jointe",
"conversations_count": "{count} sur {total} conversations",
"conversations_count_plus": "{count}+ conversations",
"conversations_count_simple": "{count} conversations",
@@ -292,6 +296,7 @@
"forward": "Transférer",
"delete": "Supprimer",
"archive": "Archiver",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Marquer comme favori",
"unstar": "Retirer des favoris",
"mark_unread": "Marquer comme non lu",
@@ -303,12 +308,10 @@
"move": "Déplacer",
"print": "Imprimer",
"view_source": "Voir la source",
"fullscreen": "Plein écran",
"exit_fullscreen": "Quitter le plein écran",
"export_email": "Exporter en .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Transférer en pièce jointe",
"import_email": "Importer .eml ou .zip",
"keyboard_shortcuts": "Raccourcis clavier (?)",
"email_source": "Source de l'email",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Accusé de lecture demandé (cliquez pour désactiver)",
"read_receipt_off": "Demander un accusé de lecture",
"dsn_on": "Notification de remise demandée (cliquer pour désactiver)",
"dsn_off": "Demander une notification de remise",
"format_plain_text": "Passer en texte brut",
"format_rich_text": "Passer en texte enrichi (HTML)",
"require_tls_on": "Remise chiffrée exigée (cliquer pour désactiver)",
"require_tls_off": "Exiger une remise chiffrée (TLS)",
"new_message": "Nouveau message",
"signature_edit_hint": "Double-cliquez pour modifier la signature",
"reply": "Répondre",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Remplacer",
"toggle_on": "Annuler le remplacement",
"toggle_tooltip": "Modifiez librement le nom et l'adresse d'expéditeur. Le courrier est toujours envoyé via votre identité - seul l'en-tête De visible change.",
"toggle_tooltip": "Modifiez librement le nom et l'adresse d'expéditeur. Le courrier est toujours envoyé via votre identité.",
"name_label": "Nom de l'expéditeur",
"name_placeholder": "Nom",
"email_label": "Adresse e-mail de l'expéditeur",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Si votre serveur n'accepte pas cette adresse comme expéditeur d'enveloppe, {identity} est utilisée à la place, et les destinataires peuvent la voir dans l'en-tête Return-Path."
},
"use_template": "Modèle",
"save_as_template": "Enregistrer comme modèle",
@@ -648,6 +658,8 @@
"upload_progress": "Envoi {uploaded} / {total}",
"upload_cancel": "Annuler l'envoi",
"upload_failed": "Échec du téléversement de {filename}",
"attachment_too_large": "« {name} » dépasse la taille autorisée par le serveur ({max} par fichier)",
"attachments_total_too_large": "Les pièces jointes dépasseraient la limite du serveur de {max} par message",
"send_failed": "Échec de l'envoi de l'e-mail",
"smime_sign_on": "Signature S/MIME activée",
"smime_sign_off": "Signature S/MIME désactivée",
@@ -726,7 +738,11 @@
"delete_column": "Supprimer la colonne",
"toggle_header_row": "Basculer la ligne d'en-tête",
"delete_table": "Supprimer le tableau",
"pick_size": "Choisir la taille"
"pick_size": "Choisir la taille",
"font_size": "Taille de police",
"font_size_default": "Par défaut",
"background_color": "Couleur d'arrière-plan",
"remove_background_color": "Supprimer la couleur d'arrière-plan"
},
"send_filing_warning": "Envoyé - mais le nettoyage après envoi a échoué, un ancien brouillon peut subsister."
},
@@ -765,6 +781,10 @@
"email_sent": "Email envoyé avec succès",
"email_deleted": "Email supprimé",
"email_archived": "Email archivé",
"emails_deleted": "{count, plural, one {# e-mail supprimé} other {# e-mails supprimés}}",
"emails_archived": "{count, plural, one {# e-mail archivé} other {# e-mails archivés}}",
"error_archiving": "Impossible d'archiver l'e-mail",
"error_updating": "Impossible de mettre à jour l'e-mail",
"email_starred": "Email ajouté aux favoris",
"email_unstarred": "Email retiré des favoris",
"email_marked_read": "Email marqué comme lu",
@@ -1143,7 +1163,9 @@
"enabled": "Notifications par e-mail",
"enabled_desc": "Afficher des notifications à l'arrivée de nouveaux e-mails",
"sound": "Son de notification",
"sound_desc": "Jouer un son à l'arrivée de nouveaux e-mails"
"sound_desc": "Jouer un son à l'arrivée de nouveaux e-mails",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Notifications de calendrier",
@@ -1172,6 +1194,7 @@
"status_busy": "Traitement en cours…",
"status_inactive": "Non activé sur cet appareil",
"status_unsupported": "Ce navigateur ne prend pas en charge Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Notifications en arrière-plan",
"devices_title": "Appareils enregistrés",
"devices_desc": "Tous les appareils enregistrés pour recevoir les notifications en arrière-plan de ce compte. Révoquer un appareil l’empêche immédiatement d’en recevoir.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Effacer la recherche lors du changement de dossier",
"description": "Cliquer sur un dossier (Boîte de réception, Envoyés…) efface la recherche active et affiche ce dossier. Désactivé, la recherche reste appliquée et est relancée dans le dossier choisi."
},
"request_read_receipt": {
"label": "Demander un accusé de lecture par défaut",
"description": "Activer au préalable la demande d''accusé de lecture lors de la rédaction d''un nouveau message."
@@ -1316,6 +1343,12 @@
"label": "Sélection automatique de l'adresse de réponse",
"description": "Lors d'une réponse, bascule automatiquement l'adresse d'expédition vers l'identité qui a reçu le message d'origine"
},
"reply_identity_match": {
"label": "Correspondance de l'adresse de réception",
"description": "Quelles adresses de réception comptent comme les vôtres. « Adresse exacte uniquement » ne choisit qu'une de vos identités configurées ; « même domaine » traite aussi toute autre adresse de vos domaines d'identité comme un alias catch-all et y réécrit l'en-tête De. Choisissez l'adresse exacte si ces adresses sont des listes de diffusion.",
"exact": "Adresse exacte uniquement",
"domain": "Toute adresse de mes domaines"
},
"signature_position": {
"label": "Position de la signature",
"description": "Où insérer votre signature dans les réponses et les transferts. Au-dessus du texte cité, elle se lit naturellement comme la conclusion de la réponse ; en dessous, elle garde le message d'origine contigu.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "La date de fin doit être postérieure à la date de début",
"start_in_past": "La date de début est dans le passé",
"empty_body": "Le corps du message est vide - les destinataires recevront une réponse vide"
"empty_body": "Le corps du message est vide - les destinataires recevront une réponse vide",
"subject_too_long": "L'objet est trop long pour le serveur ({max} octets au maximum).",
"body_too_long": "Le message est trop long pour le serveur ({max} octets au maximum, mise en forme comprise)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Corbeille",
"role_junk": "Spam / Indésirables",
"role_archive": "Archives",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Aucun",
"create_folder": "Créer un dossier",
"create_subfolder": "Créer un sous-dossier",
@@ -1882,7 +1919,9 @@
"categories_description": "Renommer les catégories de contacts",
"no_categories": "Aucune catégorie trouvée",
"group_by_letter_description": "Afficher des en-têtes alphabétiques dans la liste de contacts",
"group_by_letter_label": "Grouper par première lettre"
"group_by_letter_label": "Grouper par première lettre",
"sort_by_last_name_label": "Trier par nom de famille",
"sort_by_last_name_description": "Classer la liste des contacts par nom de famille pour regrouper les membres d'une même famille"
},
"downloads": {
"title": "Téléchargements",
@@ -1963,6 +2002,7 @@
"actions": "Actions",
"add_action": "Ajouter une action",
"stop_processing": "Arrêter le traitement des règles suivantes",
"include_spam": "Déplacer aussi les messages marqués comme spam",
"attachment_type_placeholder": "p. ex. pdf, doc, jpg",
"value_placeholder_multi": "Valeur (plusieurs séparées par des virgules)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Sélectionner le dossier",
"forward_to": "Transférer à l'adresse e-mail",
"forward_placeholder": "email@exemple.com",
"forward_keep_copy": "Garder une copie",
"forward_limit": "Limite de transferts par message sur ce serveur : {count}. Les transferts en trop sont ignorés.",
"reject_message": "Message de rejet",
"reject_placeholder": "Votre e-mail a été rejeté",
"label_name": "Nom de l'étiquette",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Nouveau sous-dossier...",
"new_folder": "Nouveau dossier...",
"rename": "Renommer...",
"share": "Partager...",
"import_email": "Importer .eml ou .zip...",
"empty_folder": "Vider le dossier",
"empty_folder_generic": "Vider le dossier",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Échec de la suppression du dossier",
"toast_error_delete_has_children": "Le dossier contient des sous-dossiers. Supprimez-les d'abord.",
"toast_error_delete_has_email": "Le dossier n'est pas vide. Videz-le d'abord.",
"delete_with_contents_title": "Le dossier n'est pas vide",
"delete_with_contents_message": "Le dossier « {name} » contient encore {count, plural, one {1 message} other {# messages}}. Supprimer le dossier et tous ses messages ? Cette action est irréversible.",
"delete_with_contents_confirm": "Supprimer le dossier et les messages",
"placeholder_folder_name": "Nom du dossier",
"create": "Créer",
"rename_confirm": "Renommer"
@@ -2270,7 +2316,8 @@
"search": "Rechercher",
"help": "Afficher les raccourcis",
"refresh": "Actualiser les emails",
"select_all": "Tout sélectionner"
"select_all": "Tout sélectionner",
"global_search": "Tout rechercher"
},
"threads": {
"expand_collapse": "Développer/réduire la conversation"
@@ -2721,7 +2768,10 @@
"any_month": "Tous les mois",
"has_email": "Avec e-mail",
"has_phone": "Avec téléphone",
"has_photo": "Avec photo"
"has_photo": "Avec photo",
"sort_by": "Trier par",
"sort_first_name": "Prénom",
"sort_last_name": "Nom de famille"
},
"open_categories": "Ouvrir les catégories"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Dupliquer",
"today_header": "Aujourd'hui",
"tomorrow_header": "Demain",
"agenda_show_earlier": "Afficher les événements précédents",
"agenda_loading": "Chargement…",
"agenda_range_start": "Événements à partir du {date}",
"agenda_range_end": "Événements jusqu'au {date}",
"export_ics": "Exporter en .ics",
"copy_title": "Copier le titre",
"copy_link": "Copier le lien de réunion",
@@ -2793,6 +2847,14 @@
"color": "Couleur"
},
"participants": {
"availability": {
"title": "Disponibilité",
"free": "Disponible",
"busy": "Occupé",
"tentative": "Provisoire",
"unknown": "Pas un utilisateur de ce serveur",
"checking": "Vérification…"
},
"title": "Participants",
"add": "Ajouter un participant",
"organizer": "Organisateur",
@@ -2867,7 +2929,10 @@
"all_events": "Tous les événements",
"cancel": "Annuler",
"save": "Enregistrer",
"delete": "Supprimer"
"delete": "Supprimer",
"rsvp_title": "Répondre à l'événement récurrent",
"rsvp_description": "Ceci est un événement récurrent. À quels événements votre réponse s'applique-t-elle ?",
"respond": "Répondre"
},
"alerts": {
"title": "Rappel",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Afficher les heures des événements dans la vue mensuelle du calendrier",
"show_week_numbers": "Afficher les numéros de semaine",
"show_week_numbers_desc": "Afficher les numéros de semaine dans le mini-calendrier",
"calendar_free_scroll": "Défilement libre",
"calendar_free_scroll_desc": "Faire défiler les mois, semaines et jours en continu au lieu d'une période à la fois",
"enable_tasks": "Activer les tâches",
"enable_tasks_desc": "Afficher une vue des tâches dans le calendrier pour gérer les choses à faire",
"show_tasks_on_calendar": "Afficher les tâches dans le calendrier",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Délai de 1 seconde",
"hover_preview_delay_2s": "Délai de 2 secondes",
"hover_preview_off": "Désactivé",
"organizer_identity": "Organiser les invitations en tant que",
"organizer_identity_desc": "L'adresse d'agenda utilisée comme organisateur lorsque vous invitez des personnes à des événements.",
"organizer_identity_failed": "Impossible de modifier l'adresse de l'organisateur",
"show_birthday_calendar": "Calendrier des anniversaires",
"show_birthday_calendar_desc": "Afficher un calendrier virtuel avec les anniversaires de vos contacts"
},
@@ -2973,6 +3043,9 @@
"alert_now": "Commence maintenant",
"alert_in_minutes": "Dans {count} min",
"invitation_sent": "Invitations envoyées",
"invitations_denied_title": "Impossible d'envoyer les invitations",
"invitations_denied": "Le serveur a refusé d'envoyer les invitations : {reason}. Enregistrer l'événement sans les envoyer ?",
"save_without_invitations": "Enregistrer sans invitations",
"rsvp_updated": "Réponse mise à jour",
"rsvp_error": "Échec de la mise à jour de la réponse",
"event_duplicated": "Événement dupliqué",
@@ -3001,7 +3074,7 @@
"supported_formats": "Fichiers iCalendar (.ics) supportés",
"url_description": "Entrez l'URL d'un flux iCalendar (.ics) externe pour importer des événements.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Prend en charge les URLs CalDAV et iCalendar (.ics)",
"url_hint": "Prend en charge les URLs iCalendar (.ics)",
"fetch": "Récupérer",
"invalid_url": "Veuillez entrer une URL valide",
"url_fetch_failed": "Impossible de récupérer le calendrier depuis l'URL",
@@ -3164,6 +3237,8 @@
"has_attachment": "Pièces jointes",
"date_after": "Après le",
"date_before": "Avant le",
"size_min": "Plus grand que (Ko)",
"size_max": "Plus petit que (Ko)",
"starred": "Suivis",
"unread": "Non lus",
"read": "Lus",
@@ -3205,6 +3280,10 @@
"hint": "Cliquez sur un e-mail à gauche pour commencer, ou lancez la visite."
},
"files": {
"office_edit": "Modifier le document",
"office_loading": "Ouverture de l'éditeur de documents…",
"office_error": "L'éditeur de documents n'a pas pu être ouvert.",
"office_close": "Fermer l'éditeur",
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
"title": "Fichiers",
"search_placeholder": "Rechercher des fichiers...",
@@ -3217,6 +3296,8 @@
"rename": "Renommer",
"rename_title": "Renommer",
"new_name": "Nouveau nom",
"name_forbidden_chars": "Le serveur n'autorise pas ces caractères dans les noms : {chars}",
"name_reserved": "Ce nom est réservé sur le serveur.",
"delete": "Supprimer",
"delete_confirm_title": "Supprimer la ressource",
"delete_confirm_message": "Êtes-vous sûr de vouloir supprimer \"{name}\" ? Cette action est irréversible.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "La recherche n'est pas disponible dans la vue unifiée"
},
"calendar_event_notifications": {
"someone": "Quelqu'un",
"invited": "{name} vous a invité à « {title} »",
"updated": "{name} a mis à jour « {title} »",
"cancelled": "{name} a annulé « {title} »",
"untitled": "Événement sans titre"
},
"share_notifications": {
"someone": "Quelqu'un",
"shared": "{name} a partagé {kind} « {object} » avec vous",
"changed": "{name} a modifié votre accès à {kind} « {object} »",
"revoked": "{name} a retiré votre accès à {kind} « {object} »",
"object": {
"folder": "le dossier",
"calendar": "l'agenda",
"address_book": "le carnet d'adresses",
"files": "le dossier de fichiers"
}
},
"sharing": {
"title": "Partager « {name} »",
"description": "Accordez l'accès à d'autres utilisateurs ou groupes de ce serveur. Les modifications sont immédiates.",
@@ -3501,5 +3601,45 @@
"copy_event": "Copier le lien de l'événement",
"copied": "Lien copié",
"copy_failed": "Impossible de copier le lien"
},
"plugins": {
"oauth_callback": {
"completing": "Finalisation de l'autorisation…",
"completing_message": "Veuillez patienter pendant l'établissement de la connexion.",
"received_title": "Autorisation reçue",
"received_message": "Vous pouvez revenir à l'onglet où vous avez commencé la connexion.",
"failed_title": "Échec de l'autorisation",
"denied": "Lautorisation a été refusée.",
"failed_detail": "Échec de lautorisation : {error}",
"missing_params": "Code dautorisation ou état manquant.",
"delivery_failed": "Le résultat de lautorisation na pas pu être transmis à longlet de lapplication. Fermez cette fenêtre et réessayez."
}
},
"global_search": {
"title": "Tout rechercher",
"placeholder": "Rechercher courriels, contacts, calendrier, fichiers...",
"scope_label": "Rechercher dans",
"account_label": "Compte",
"scope_all": "Tout",
"scope_mail": "Messagerie",
"scope_contacts": "Contacts",
"scope_calendar": "Calendrier",
"scope_files": "Fichiers",
"all_accounts": "Tous les comptes",
"show_all": "Afficher tous les résultats",
"no_results": "Aucun résultat pour « {query} »",
"server_hint": "Les mots complets interrogent le serveur ; les mots partiels correspondent aux éléments déjà chargés",
"searching": "Recherche...",
"account_error": "Impossible de rechercher dans {account}",
"recent_searches": "Recherches récentes",
"recurring": "Récurrent",
"no_subject": "(no subject)",
"empty_hint": "Recherchez courriels, contacts, événements et fichiers sur tous les comptes. Essayez from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "À plat par date",
"grouped": "Groupé",
"load_more": "Charger plus",
"preview_empty": "Sélectionnez un résultat pour l'aperçu",
"open_full": "Ouvrir",
"view_label": "Affichage"
}
}
+153 -13
View File
@@ -190,6 +190,10 @@
"and_others": "ו-{count}אחרים",
"draft": "טְיוּטָה",
"starred": "מסומן בכוכב",
"pinned": "מוצמד",
"replied": "נענה",
"forwarded": "הועבר",
"has_attachment": "כולל קובץ מצורף",
"conversations_count": "{count}מתוך שיחות{total}",
"conversations_count_plus": "{count}+ שיחות",
"conversations_count_simple": "שיחות{count}",
@@ -244,18 +248,17 @@
"forward": "העבר",
"delete": "מחיקה",
"archive": "ארכיון",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "כוכב",
"unstar": "הסר כוכב",
"mark_unread": "סמן כלא נקרא",
"mark_read": "סמן כנקרא",
"print": "הדפס",
"view_source": "צפה במקור",
"fullscreen": "מסך מלא",
"exit_fullscreen": "יציאה ממסך מלא",
"export_email": "ייצא כ-.eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "העבר כקובץ מצורף",
"import_email": "ייבוא .eml",
"keyboard_shortcuts": "קיצורי מקשים (?)",
"email_source": "מקור דוא\"ל",
@@ -600,6 +603,8 @@
"upload_progress": "מעלה את{uploaded}/{total}",
"upload_cancel": "בטל העלאה",
"upload_failed": "העלאת{filename}נכשלה",
"attachment_too_large": "\"{name}\" גדול ממה שהשרת מאפשר ({max} לקובץ)",
"attachments_total_too_large": "הקבצים המצורפים יחרגו ממגבלת השרת של {max} להודעה",
"drop_files": "שחרר קבצים לצירוף",
"show_less": "הצג פחות",
"send_failed": "שליחת האימייל נכשלה",
@@ -618,6 +623,12 @@
"smime_passphrase_placeholder": "ביטוי סיסמה",
"read_receipt_on": "קבלת דואר מבוקשת (לחץ כדי להשבית)",
"read_receipt_off": "בקש קבלת דואר",
"dsn_on": "התבקש אישור מסירה (לחצו לביטול)",
"dsn_off": "בקשת אישור מסירה",
"format_plain_text": "מעבר לטקסט רגיל",
"format_rich_text": "מעבר לטקסט עשיר (HTML)",
"require_tls_on": "נדרשת מסירה מוצפנת (לחצו לביטול)",
"require_tls_off": "דרישת מסירה מוצפנת (TLS)",
"attach_photos": "תמונות וסרטונים",
"attach_files": "קבצים",
"sending": "שולח…",
@@ -626,11 +637,12 @@
"from_override": {
"toggle_off": "עקוף",
"toggle_on": "בטל עקיפה",
"toggle_tooltip": "ערוך את שם ההַשגחה וכתובת בחופשיות. הדוא״ל עדיין נשלח דרך הזהות שלך - רק כותרת ההַשגחה הנראית משתנה.",
"toggle_tooltip": "ערוך בחופשיות את שם וכתובת השולח. הדוא״ל עדיין נשלח דרך הזהות שלך.",
"name_label": "שם ממנו",
"name_placeholder": "שם",
"email_label": "כתובת דוא״ל ממנו",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "אם השרת שלך לא מקבל את הכתובת הזו כשולח המעטפה, ייעשה שימוש ב-{identity} במקומה, והנמענים יכולים לראות אותה בכותרת Return-Path."
},
"forgot_attachment": {
"title": "נשכחת קובץ מצורף?",
@@ -691,7 +703,11 @@
"delete_column": "מחיקת עמודה",
"toggle_header_row": "החלפת שורת כותרת",
"delete_table": "מחיקת טבלה",
"pick_size": "בחירת גודל"
"pick_size": "בחירת גודל",
"font_size": "גודל גופן",
"font_size_default": "ברירת מחדל",
"background_color": "צבע רקע",
"remove_background_color": "הסר צבע רקע"
},
"send_filing_warning": "נשלח - אך הניקוי שלאחר השליחה נכשל, ייתכן שתישאר טיוטה ישנה."
},
@@ -730,6 +746,10 @@
"email_sent": "האימייל נשלח בהצלחה",
"email_deleted": "האימייל נמחק",
"email_archived": "האימייל הועבר לארכיון",
"emails_deleted": "{count, plural, one {הודעה אחת נמחקה} other {# הודעות נמחקו}}",
"emails_archived": "{count, plural, one {הודעה אחת הועברה לארכיון} other {# הודעות הועברו לארכיון}}",
"error_archiving": "העברת ההודעה לארכיון נכשלה",
"error_updating": "עדכון ההודעה נכשל",
"email_starred": "אימייל מסומן בכוכב",
"email_unstarred": "דוא\"ל בוטל בכוכב",
"email_marked_read": "אימייל סומן כנקראה",
@@ -1108,7 +1128,9 @@
"enabled": "התראות באימייל",
"enabled_desc": "הצג התראות כשמגיעות אימיילים חדשים",
"sound": "צליל התראה",
"sound_desc": "הפעל התראת שמע כשמגיעות הודעות דוא\"ל חדשות"
"sound_desc": "הפעל התראת שמע כשמגיעות הודעות דוא\"ל חדשות",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "הודעות לוח שנה",
@@ -1130,6 +1152,7 @@
"status_active": "פעיל בהתקן זה",
"status_inactive": "לא מופעל בהתקן זה",
"status_unsupported": "דפדפן זה אינו תומך בדחיפה ברשת",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "עובד…",
"enable": "הפעל",
"reenable": "רשום מחדש",
@@ -1203,6 +1226,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "נקה את החיפוש בעת מעבר בין תיקיות",
"description": "לחיצה על תיקייה (דואר נכנס, נשלח…) מנקה את החיפוש הפעיל ומציגה את התיקייה. כשהאפשרות כבויה, החיפוש נשאר ומופעל מחדש בתיקייה שאליה עברת."
},
"title": "התנהגות אימייל",
"description": "קבע את אופן הטיפול בהודעות דוא\"ל",
"mark_read": {
@@ -1275,6 +1302,12 @@
"label": "בחירה אוטומטית בכתובת תשובה",
"description": "בעת תשובה, העבר אוטומטית את הכתובת מאת לזהות שקיבלה את ההודעה במקור"
},
"reply_identity_match": {
"label": "התאמת כתובת הקבלה",
"description": "אילו כתובות קבלה נחשבות לשלך. „כתובת מדויקת בלבד“ בוחרת רק אחת מהזהויות שהגדרת; „אותו דומיין“ מתייחסת גם לכל כתובת אחרת בדומייני הזהויות שלך ככינוי catch-all ומשכתבת אליה את כותרת השולח. בחר בכתובת מדויקת אם כתובות אלה הן רשימות תפוצה.",
"exact": "כתובת מדויקת בלבד",
"domain": "כל כתובת בדומיינים שלי"
},
"attachment_click_action": {
"label": "קובץ מצורף לחץ על פעולה",
"description": "בחר אם לחיצה על קובץ מצורף תציג אותו בתצוגה מקדימה או תוריד אותו מיד",
@@ -1711,7 +1744,9 @@
"warnings": {
"end_before_start": "תאריך הסיום חייב להיות אחרי תאריך ההתחלה",
"start_in_past": "תאריך ההתחלה הוא בעבר",
"empty_body": "גוף ההודעה ריק - הנמענים יקבלו תשובה ריקה"
"empty_body": "גוף ההודעה ריק - הנמענים יקבלו תשובה ריקה",
"subject_too_long": "הנושא ארוך מדי עבור השרת (לכל היותר {max} בתים).",
"body_too_long": "ההודעה ארוכה מדי עבור השרת (לכל היותר {max} בתים, כולל עיצוב)."
}
},
"folders": {
@@ -1727,6 +1762,8 @@
"role_trash": "אַשׁפָּה",
"role_junk": "ספאם / זבל",
"role_archive": "ארכיון",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "אין",
"create_folder": "צור תיקיה",
"create_subfolder": "צור תיקיית משנה",
@@ -1848,6 +1885,8 @@
"categories_description": "שנה שם של קטגוריות אנשי קשר",
"no_categories": "לא נמצאו קטגוריות",
"group_by_letter_label": "קבוצה לפי אות ראשונה",
"sort_by_last_name_label": "מיון לפי שם משפחה",
"sort_by_last_name_description": "סידור רשימת אנשי הקשר לפי שם משפחה כך שבני אותה משפחה יופיעו יחד",
"group_by_letter_description": "הצג כותרות חלקים אלפביתיים ברשימת אנשי הקשר"
},
"filters": {
@@ -1885,6 +1924,7 @@
"actions": "פעולות",
"add_action": "הוסף פעולה",
"stop_processing": "הפסק לעבד את הכללים הבאים",
"include_spam": "להעביר גם הודעות שסומנו כדואר זבל",
"condition_fields": {
"from": "מ",
"to": "ל",
@@ -1924,6 +1964,8 @@
"copy_to_folder": "בחר תיקייה",
"forward_to": "העבר לכתובת המייל",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "שמור עותק",
"forward_limit": "מגבלת העברות לכל הודעה בשרת זה: {count}. העברות נוספות מדולגות.",
"reject_message": "הודעת דחייה",
"reject_placeholder": "האימייל שלך נדחה",
"label_name": "שם התווית",
@@ -2198,7 +2240,8 @@
"search": "התמקד בחיפוש",
"help": "הצג קיצורי דרך",
"refresh": "רענן מיילים",
"select_all": "בחר הכל"
"select_all": "בחר הכל",
"global_search": "חיפוש בכל מקום"
},
"threads": {
"expand_collapse": "הרחב/כווץ את השרשור"
@@ -2650,7 +2693,10 @@
"any_month": "כל חודש",
"has_email": "יש דוא״ל",
"has_phone": "יש טלפון",
"has_photo": "יש תמונה"
"has_photo": "יש תמונה",
"sort_by": "מיון לפי",
"sort_first_name": "שם פרטי",
"sort_last_name": "שם משפחה"
}
},
"calendar": {
@@ -2686,6 +2732,10 @@
"duplicate": "שכפול",
"today_header": "היום",
"tomorrow_header": "מחר",
"agenda_show_earlier": "הצגת אירועים קודמים",
"agenda_loading": "טוען…",
"agenda_range_start": "אירועים מ-{date}",
"agenda_range_end": "אירועים עד {date}",
"export_ics": "יצא כ .ics",
"copy_title": "עותק כותרת",
"copy_link": "עותק קישור פגישה",
@@ -2721,6 +2771,14 @@
"color": "צבע"
},
"participants": {
"availability": {
"title": "זמינות",
"free": "פנוי",
"busy": "עסוק",
"tentative": "טנטטיבי",
"unknown": "לא משתמש בשרת זה",
"checking": "בודק…"
},
"title": "משתתפים",
"add": "הוסף משתתף",
"organizer": "מארגן",
@@ -2795,7 +2853,10 @@
"all_events": "כל האירועים",
"cancel": "לְבַטֵל",
"save": "שמור",
"delete": "לִמְחוֹק"
"delete": "לִמְחוֹק",
"rsvp_title": "השב לאירוע חוזר",
"rsvp_description": "זהו אירוע שחוזר על עצמו. על אילו אירועים חלה התשובה שלך?",
"respond": "השב"
},
"alerts": {
"title": "תזכורת",
@@ -2833,6 +2894,8 @@
"show_time_in_month_view_desc": "הצג זמני אירועים בתצוגת לוח השנה של החודש",
"show_week_numbers": "הצג מספרי שבוע",
"show_week_numbers_desc": "הצג מספרי שבוע במיני-לוח שנה",
"calendar_free_scroll": "גלילה חופשית",
"calendar_free_scroll_desc": "גלילה רציפה בין חודשים, שבועות וימים במקום תקופה אחת בכל פעם",
"enable_tasks": "אפשר משימות",
"enable_tasks_desc": "הצג תצוגת משימות בלוח השנה לניהול מטלות",
"show_tasks_on_calendar": "הצג משימות בלוח השנה",
@@ -2844,6 +2907,9 @@
"hover_preview_delay_1s": "עיכוב של שנייה",
"hover_preview_delay_2s": "עיכוב של 2 שניות",
"hover_preview_off": "מושבת",
"organizer_identity": "ארגון הזמנות בתור",
"organizer_identity_desc": "כתובת היומן שתשמש כמארגן כשמזמינים אנשים לאירועים.",
"organizer_identity_failed": "שינוי כתובת המארגן נכשל",
"show_birthday_calendar": "צור קשר עם יומן יום הולדת",
"show_birthday_calendar_desc": "הצג לוח שנה וירטואלי עם ימי הולדת מאנשי הקשר שלך"
},
@@ -2875,6 +2941,9 @@
"alert_now": "מתחיל עכשיו",
"alert_in_minutes": "ב-{count}דקות",
"invitation_sent": "נשלחו הזמנות",
"invitations_denied_title": "לא ניתן לשלוח את ההזמנות",
"invitations_denied": "השרת סירב לשלוח את ההזמנות: {reason}. לשמור את האירוע בלי לשלוח אותן?",
"save_without_invitations": "שמור בלי הזמנות",
"rsvp_updated": "התגובה עודכנה",
"rsvp_error": "עדכון התגובה נכשל",
"event_duplicated": "האירוע שוכפל",
@@ -2903,7 +2972,7 @@
"supported_formats": "תומך בקבצי iCalendar (.ics).",
"url_description": "הזן את כתובת האתר של עדכון iCalendar (.ics) חיצוני כדי לייבא אירועים.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "תומך בכתובות URL של CalDAV ו-iCalendar (.ics).",
"url_hint": "תומך בכתובות URL של iCalendar (.ics).",
"fetch": "אחזר",
"invalid_url": "אנא הזן כתובת אתר חוקית",
"url_fetch_failed": "אחזור היומן מ-URL נכשל",
@@ -3092,6 +3161,8 @@
"has_attachment": "קבצים מצורפים",
"date_after": "אחרי",
"date_before": "לפני",
"size_min": "גדול מ־(KB)",
"size_max": "קטן מ־(KB)",
"starred": "מסומן בכוכב",
"unread": "לא נקרא",
"read": "קרא",
@@ -3133,6 +3204,10 @@
"hint": "לחץ על אימייל כלשהו מימין כדי להתחיל, או צא לסיור למטה."
},
"files": {
"office_edit": "עריכת מסמך",
"office_loading": "פותח את עורך המסמכים…",
"office_error": "לא ניתן היה לפתוח את עורך המסמכים.",
"office_close": "סגירת העורך",
"title": "קבצים",
"search_placeholder": "חפש קבצים...",
"empty_state_title": "אין עדיין קבצים",
@@ -3144,6 +3219,8 @@
"rename": "שנה שם",
"rename_title": "שנה שם",
"new_name": "שם חדש",
"name_forbidden_chars": "השרת אינו מתיר את התווים האלה בשמות: {chars}",
"name_reserved": "השם הזה שמור בשרת.",
"delete": "לִמְחוֹק",
"delete_confirm_title": "מחק משאב",
"delete_confirm_message": "האם אתה בטוח שברצונך למחוק את \"{name}\"? לא ניתן לבטל זאת.",
@@ -3418,6 +3495,7 @@
"new_subfolder": "תת־תיקייה חדשה…",
"new_folder": "תיקייה חדשה…",
"rename": "שנה שם…",
"share": "שיתוף...",
"import_email": "ייבא .eml או .zip…",
"empty_folder": "תיקייה ריקה",
"empty_folder_generic": "תיקייה ריקה",
@@ -3447,7 +3525,29 @@
"toast_error_rename": "נכשל בשינוי שם תיקייה",
"toast_error_delete": "נכשל במחיקת תיקייה",
"toast_error_delete_has_children": "לתיקייה יש תת־תיקיות. הסר אותן קודם.",
"toast_error_delete_has_email": "התיקייה אינה ריקה. תרוקנה קודם."
"toast_error_delete_has_email": "התיקייה אינה ריקה. תרוקנה קודם.",
"delete_with_contents_title": "התיקייה אינה ריקה",
"delete_with_contents_message": "התיקייה \"{name}\" עדיין מכילה {count, plural, one {הודעה אחת} other {# הודעות}}. למחוק את התיקייה ואת כל ההודעות שבה? לא ניתן לבטל פעולה זו.",
"delete_with_contents_confirm": "מחיקת התיקייה וההודעות"
},
"calendar_event_notifications": {
"someone": "מישהו",
"invited": "{name} הזמין/ה אותך ל\"{title}\"",
"updated": "{name} עדכן/ה את \"{title}\"",
"cancelled": "{name} ביטל/ה את \"{title}\"",
"untitled": "אירוע ללא כותרת"
},
"share_notifications": {
"someone": "מישהו",
"shared": "{name} שיתף/ה איתך את {kind} \"{object}\"",
"changed": "{name} שינה/תה את הגישה שלך ל{kind} \"{object}\"",
"revoked": "{name} הסיר/ה את הגישה שלך ל{kind} \"{object}\"",
"object": {
"folder": "התיקייה",
"calendar": "היומן",
"address_book": "פנקס הכתובות",
"files": "תיקיית הקבצים"
}
},
"sharing": {
"title": "שתף \"{name}\"",
@@ -3502,5 +3602,45 @@
"copy_event": "העתק קישור לאירוע",
"copied": "הקישור הועתק",
"copy_failed": "לא ניתן להעתיק את הקישור"
},
"plugins": {
"oauth_callback": {
"completing": "משלים אימות…",
"completing_message": "אנא המתן בזמן יצירת החיבור.",
"received_title": "האימות התקבל",
"received_message": "באפשרותך לחזור לכרטיסייה שבה התחלת את החיבור.",
"failed_title": "האימות נכשל",
"denied": "האימות נדחה.",
"failed_detail": "האימות נכשל: {error}",
"missing_params": "חסר קוד אימות או מצב.",
"delivery_failed": "לא ניתן היה למסור את תוצאת האימות לכרטיסיית האפליקציה. סגרו חלון זה ונסו שוב."
}
},
"global_search": {
"title": "חיפוש בכל מקום",
"placeholder": "חיפוש בדואר, אנשי קשר, יומן, קבצים...",
"scope_label": "חיפוש ב",
"account_label": "חשבון",
"scope_all": "הכל",
"scope_mail": "דוֹאַר",
"scope_contacts": "אנשי קשר",
"scope_calendar": "לוּחַ שָׁנָה",
"scope_files": "קבצים",
"all_accounts": "כל החשבונות",
"show_all": "הצגת כל התוצאות",
"no_results": "אין תוצאות עבור “{query}”",
"server_hint": "מילים שלמות מחפשות בשרת; חלקי מילים תואמים פריטים שכבר נטענו",
"searching": "מחפש...",
"account_error": "לא ניתן היה לחפש ב-{account}",
"recent_searches": "חיפושים אחרונים",
"recurring": "חוזר",
"no_subject": "(no subject)",
"empty_hint": "חפשו דואר, אנשי קשר, אירועים וקבצים בכל החשבונות. נסו: from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "שטוח לפי תאריך",
"grouped": "מקובץ",
"load_more": "טען עוד",
"preview_empty": "בחרו תוצאה לתצוגה מקדימה",
"open_full": "פתיחה",
"view_label": "תצוגה"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "és {count} további",
"draft": "Piszkozat",
"starred": "Csillagozott",
"pinned": "Kitűzve",
"replied": "Megválaszolva",
"forwarded": "Továbbítva",
"has_attachment": "Melléklettel",
"conversations_count": "{count} / {total} beszélgetés",
"conversations_count_plus": "{count}+ beszélgetés",
"conversations_count_simple": "{count} beszélgetés",
@@ -292,6 +296,7 @@
"forward": "Továbbítás",
"delete": "Törlés",
"archive": "Archiválás",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Csillagozás",
"unstar": "Csillagozás megszüntetése",
"mark_unread": "Megjelölés olvasatlanként",
@@ -303,12 +308,10 @@
"move": "Áthelyezés",
"print": "Nyomtatás",
"view_source": "Forrás megtekintése",
"fullscreen": "Teljes képernyő",
"exit_fullscreen": "Kilépés a teljes képernyőből",
"export_email": "Exportálás .eml-ként",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Továbbítás mellékletként",
"import_email": "Importálás .eml vagy .zip fájlból",
"keyboard_shortcuts": "Billentyűparancsok (?)",
"email_source": "E-mail forrás",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Olvasási visszaigazolás kérve (kattints a letiltáshoz)",
"read_receipt_off": "Olvasási visszaigazolás kérése",
"dsn_on": "Kézbesítési értesítés kérve (kattints a kikapcsoláshoz)",
"dsn_off": "Kézbesítési értesítés kérése",
"format_plain_text": "Váltás egyszerű szövegre",
"format_rich_text": "Váltás formázott szövegre (HTML)",
"require_tls_on": "Titkosított kézbesítés kötelező (kattints a kikapcsoláshoz)",
"require_tls_off": "Titkosított kézbesítés megkövetelése (TLS)",
"new_message": "Új üzenet",
"signature_edit_hint": "Kattintson duplán az aláírás szerkesztéséhez",
"reply": "Válasz",
@@ -634,11 +643,12 @@
"from_override": {
"toggle_off": "Felülbírálás",
"toggle_on": "Felülbírálás megszüntetése",
"toggle_tooltip": "A Feladó név és cím szabad szerkesztése. A levél továbbra is az identitásodon keresztül kerül elküldésre - csak a látható Feladó fejléc változik.",
"toggle_tooltip": "A Feladó név és cím szabad szerkesztése. A levél továbbra is az identitásodon keresztül kerül elküldésre.",
"name_label": "Feladó neve",
"name_placeholder": "Név",
"email_label": "Feladó e-mail címe",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Ha a szervered nem fogadja el ezt a címet borítékfeladóként, helyette a(z) {identity} cím kerül oda, és a címzettek láthatják a Return-Path fejlécben."
},
"use_template": "Sablon",
"save_as_template": "Mentés sablonként",
@@ -651,6 +661,8 @@
"upload_progress": "Feltöltés {uploaded} / {total}",
"upload_cancel": "Feltöltés megszakítása",
"upload_failed": "Nem sikerült feltölteni: {filename}",
"attachment_too_large": "A(z) „{name}” nagyobb, mint amit a kiszolgáló enged (fájlonként {max})",
"attachments_total_too_large": "A mellékletek túllépnék a kiszolgáló üzenetenkénti {max} korlátját",
"drop_files": "Fájlok ejtése a csatoláshoz",
"show_less": "Kevesebb mutatása",
"send_failed": "Nem sikerült elküldeni az e-mailt",
@@ -726,7 +738,11 @@
"delete_column": "Oszlop törlése",
"toggle_header_row": "Fejlécsor váltása",
"delete_table": "Táblázat törlése",
"pick_size": "Méret kiválasztása"
"pick_size": "Méret kiválasztása",
"font_size": "Betűméret",
"font_size_default": "Alapértelmezett",
"background_color": "Háttérszín",
"remove_background_color": "Háttérszín eltávolítása"
},
"send_filing_warning": "Elküldve - de az utólagos rendrakás nem sikerült, egy elavult piszkozat megmaradhat."
},
@@ -765,6 +781,10 @@
"email_sent": "E-mail sikeresen elküldve",
"email_deleted": "E-mail törölve",
"email_archived": "E-mail archiválva",
"emails_deleted": "{count, plural, other {# e-mail törölve}}",
"emails_archived": "{count, plural, other {# e-mail archiválva}}",
"error_archiving": "Nem sikerült archiválni az e-mailt",
"error_updating": "Nem sikerült frissíteni az e-mailt",
"email_starred": "E-mail csillagozva",
"email_unstarred": "E-mail csillagozás megszüntetve",
"email_marked_read": "E-mail olvasottnak jelölve",
@@ -1144,6 +1164,7 @@
"status_active": "Aktív ezen az eszközön",
"status_inactive": "Nincs engedélyezve ezen az eszközön",
"status_unsupported": "Ez a böngésző nem támogatja a Web Push-t",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "Munka...",
"enable": "Engedélyezés",
"reenable": "Újraregisztráció",
@@ -1181,7 +1202,9 @@
"enabled": "E-mail értesítések",
"enabled_desc": "Értesítések megjelenítése új e-mailek érkezésekor",
"sound": "Értesítési hang",
"sound_desc": "Hangjelzés lejátszása új e-mailek érkezésekor"
"sound_desc": "Hangjelzés lejátszása új e-mailek érkezésekor",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Naptár értesítések",
@@ -1241,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Keresés törlése mappaváltáskor",
"description": "Egy mappára kattintva (Beérkezett, Elküldött…) törlődik az aktív keresés, és a mappa jelenik meg. Kikapcsolva a keresés megmarad, és újra lefut a választott mappában."
},
"request_read_receipt": {
"label": "Olvasási visszaigazolás kérése alapértelmezés szerint",
"description": "Az olvasási visszaigazolás kérésének előre bekapcsolása új üzenet írásakor."
@@ -1324,6 +1351,12 @@
"label": "Válasz a fogadott címről",
"description": "Válaszküldéskor arról a címről küldjön, amelyre az üzenet eredetileg érkezett. Először identitásokat keres; tartományi catch-all kézbesítések esetén átírja a Feladó fejlécet az aliasra, miközben az elsődleges identitáson keresztül küld."
},
"reply_identity_match": {
"label": "Fogadó cím egyeztetése",
"description": "Mely fogadó címek számítanak a sajátjának. A „Csak pontos cím” kizárólag a beállított identitásai közül választ; az „azonos domain” az identitás-domainjein lévő bármely más címet is catch-all aliasként kezeli, és arra írja át a Feladó fejlécet. Válassza a pontos címet, ha ezek a címek terjesztési listák.",
"exact": "Csak pontos cím",
"domain": "Bármely cím a domainjeimen"
},
"signature_position": {
"label": "Aláírás pozíciója",
"description": "Hova kerüljön az aláírás a válaszokban és továbbításokban. Az idézett szöveg felett természetes lezárásként olvasható; alatta az eredeti üzenet marad egybefüggő.",
@@ -1749,7 +1782,9 @@
"warnings": {
"end_before_start": "A befejező dátumnak a kezdő dátum után kell lennie",
"start_in_past": "A kezdő dátum a múltban van",
"empty_body": "Az üzenet szövege üres - a címzettek üres választ kapnak"
"empty_body": "Az üzenet szövege üres - a címzettek üres választ kapnak",
"subject_too_long": "A tárgy túl hosszú a kiszolgálónak (legfeljebb {max} bájt).",
"body_too_long": "Az üzenet túl hosszú a kiszolgálónak (legfeljebb {max} bájt a formázással együtt)."
}
},
"folders": {
@@ -1766,6 +1801,8 @@
"role_trash": "Kuka",
"role_junk": "Spam",
"role_archive": "Archívum",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Nincs",
"create_folder": "Mappa létrehozása",
"create_subfolder": "Almappa létrehozása",
@@ -1875,6 +1912,8 @@
"title": "Névjegyek",
"description": "Névjegyek importálása és exportálása",
"group_by_letter_label": "Csoportosítás kezdőbetű szerint",
"sort_by_last_name_label": "Rendezés vezetéknév szerint",
"sort_by_last_name_description": "A névjegylista vezetéknév szerinti rendezése, hogy a családtagok egymás mellett legyenek",
"group_by_letter_description": "ABC sorrendű fejléc mutatása a névjegy listában",
"import_label": "Névjegyek importálása",
"import_description": "Névjegyek importálása vCard (.vcf) fájlból",
@@ -1966,6 +2005,7 @@
"actions": "Műveletek",
"add_action": "Művelet hozzáadása",
"stop_processing": "További szabályok feldolgozásának leállítása",
"include_spam": "A levélszemétnek jelölt üzenetek áthelyezése is",
"attachment_type_placeholder": "pl. pdf, doc, jpg",
"value_placeholder_multi": "Érték (több érték vesszővel elválasztva)",
"condition_fields": {
@@ -2007,6 +2047,8 @@
"copy_to_folder": "Mappa kiválasztása",
"forward_to": "Továbbítás e-mail címre",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Másolat megtartása",
"forward_limit": "Továbbítási korlát üzenetenként ezen a kiszolgálón: {count}. A további továbbítások kimaradnak.",
"reject_message": "Elutasító üzenet",
"reject_placeholder": "Az e-mailed elutasításra került",
"label_name": "Címke neve",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Új almappa...",
"new_folder": "Új mappa...",
"rename": "Átnevezés...",
"share": "Megosztás...",
"import_email": "Importálás .eml vagy .zip fájlból...",
"empty_folder": "Mappa ürítése",
"empty_folder_generic": "Mappa ürítése",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "Nem sikerült átnevezni a mappát",
"toast_error_delete": "Nem sikerült törölni a mappát",
"toast_error_delete_has_children": "A mappának almappái vannak. Távolítsd el azokat először.",
"toast_error_delete_has_email": "A mappa nem üres. Ürítsd ki először."
"toast_error_delete_has_email": "A mappa nem üres. Ürítsd ki először.",
"delete_with_contents_title": "A mappa nem üres",
"delete_with_contents_message": "A(z) „{name}” mappa még {count, plural, one {1 üzenetet} other {# üzenetet}} tartalmaz. Törlöd a mappát az összes üzenetével együtt? Ez nem vonható vissza.",
"delete_with_contents_confirm": "Mappa és üzenetek törlése"
},
"shortcuts": {
"title": "Billentyűparancsok",
@@ -2270,7 +2316,8 @@
"search": "Keresés fókusz",
"help": "Parancsok mutatása",
"refresh": "E-mailek frissítése",
"select_all": "Összes kijelölése"
"select_all": "Összes kijelölése",
"global_search": "Keresés mindenben"
},
"threads": {
"expand_collapse": "Beszélgetés kibontása/összecsukása"
@@ -2722,7 +2769,10 @@
"any_month": "Bármely hónap",
"has_email": "Van e-mail",
"has_phone": "Van telefon",
"has_photo": "Van fotó"
"has_photo": "Van fotó",
"sort_by": "Rendezés",
"sort_first_name": "Keresztnév",
"sort_last_name": "Vezetéknév"
}
},
"calendar": {
@@ -2758,6 +2808,10 @@
"duplicate": "Duplikálás",
"today_header": "Ma",
"tomorrow_header": "Holnap",
"agenda_show_earlier": "Korábbi események megjelenítése",
"agenda_loading": "Betöltés…",
"agenda_range_start": "Események ettől: {date}",
"agenda_range_end": "Események eddig: {date}",
"export_ics": "Exportálás .ics-ként",
"copy_title": "Cím másolása",
"copy_link": "Találkozó link másolása",
@@ -2793,6 +2847,14 @@
"color": "Szín"
},
"participants": {
"availability": {
"title": "Elérhetőség",
"free": "Szabad",
"busy": "Foglalt",
"tentative": "Feltételes",
"unknown": "Nem felhasználó ezen a kiszolgálón",
"checking": "Ellenőrzés…"
},
"title": "Résztvevők",
"add": "Résztvevő hozzáadása",
"organizer": "Szervező",
@@ -2867,7 +2929,10 @@
"all_events": "Minden esemény",
"cancel": "Mégse",
"save": "Mentés",
"delete": "Törlés"
"delete": "Törlés",
"rsvp_title": "Válasz ismétlődő eseményre",
"rsvp_description": "Ez egy ismétlődő esemény. Mely eseményekre vonatkozzon a válaszod?",
"respond": "Válasz"
},
"alerts": {
"title": "Emlékeztetők",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Események időpontjának mutatása a havi naptár nézetben",
"show_week_numbers": "Hétszámok mutatása",
"show_week_numbers_desc": "Hétszámok mutatása a mini naptárban",
"calendar_free_scroll": "Szabad görgetés",
"calendar_free_scroll_desc": "Folyamatos görgetés hónapok, hetek és napok között egyetlen időszak helyett",
"enable_tasks": "Feladatok engedélyezése",
"enable_tasks_desc": "Feladatnézet mutatása a naptárban a teendők kezeléséhez",
"show_tasks_on_calendar": "Feladatok mutatása a naptárban",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1 másodperc késleltetés",
"hover_preview_delay_2s": "2 másodperc késleltetés",
"hover_preview_off": "Letiltva",
"organizer_identity": "Meghívók szervezése mint",
"organizer_identity_desc": "A szervezőként használt naptárcím, amikor embereket hívsz meg eseményekre.",
"organizer_identity_failed": "Nem sikerült módosítani a szervező címét",
"show_birthday_calendar": "Névjegy születésnap naptár",
"show_birthday_calendar_desc": "Virtuális naptár mutatása a névjegyeid születésnapjaival"
},
@@ -2973,6 +3043,9 @@
"alert_now": "Most kezdődik",
"alert_in_minutes": "{count} perc múlva",
"invitation_sent": "Meghívók elküldve",
"invitations_denied_title": "A meghívók nem küldhetők el",
"invitations_denied": "A kiszolgáló nem küldte el a meghívókat: {reason}. Mentsük az eseményt a meghívók nélkül?",
"save_without_invitations": "Mentés meghívók nélkül",
"rsvp_updated": "Válasz frissítve",
"rsvp_error": "Nem sikerült frissíteni a választ",
"event_duplicated": "Esemény duplikálva",
@@ -3002,7 +3075,7 @@
"supported_formats": "iCalendar (.ics) fájlok támogatottak",
"url_description": "Add meg egy külső iCalendar (.ics) feed URL-jét az események importálásához.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "CalDAV és iCalendar (.ics) URL-ek támogatottak",
"url_hint": "iCalendar (.ics) URL-ek támogatottak",
"fetch": "Lekérés",
"invalid_url": "Kérjük, adj meg egy érvényes URL-t",
"url_fetch_failed": "Nem sikerült lekérni a naptárt az URL-ről",
@@ -3149,6 +3222,25 @@
"overdue": "Lejárt"
}
},
"calendar_event_notifications": {
"someone": "Valaki",
"invited": "{name} meghívott a(z) „{title}” eseményre",
"updated": "{name} frissítette a(z) „{title}” eseményt",
"cancelled": "{name} lemondta a(z) „{title}” eseményt",
"untitled": "Névtelen esemény"
},
"share_notifications": {
"someone": "Valaki",
"shared": "{name} megosztotta veled a(z) {kind} „{object}” elemet",
"changed": "{name} módosította a hozzáférésedet a(z) {kind} „{object}” elemhez",
"revoked": "{name} visszavonta a hozzáférésedet a(z) {kind} „{object}” elemhez",
"object": {
"folder": "mappa",
"calendar": "naptár",
"address_book": "címjegyzék",
"files": "fájlmappa"
}
},
"sharing": {
"title": "\"{name}\" megosztása",
"description": "Hozzáférés biztosítása más felhasználók vagy csoportok számára ezen a szerveren. A módosítások azonnal életbe lépnek.",
@@ -3187,6 +3279,8 @@
"has_attachment": "Csatolmányok",
"date_after": "Ettől",
"date_before": "Eddig",
"size_min": "Nagyobb mint (KB)",
"size_max": "Kisebb mint (KB)",
"starred": "Csillagozott",
"unread": "Olvasatlan",
"read": "Olvasott",
@@ -3228,6 +3322,10 @@
"hint": "Kattints bármely e-mailre a bal oldalon a kezdéshez, vagy nézd meg az alábbi bemutatót."
},
"files": {
"office_edit": "Dokumentum szerkesztése",
"office_loading": "Dokumentumszerkesztő megnyitása…",
"office_error": "A dokumentumszerkesztőt nem sikerült megnyitni.",
"office_close": "Szerkesztő bezárása",
"open_in_new_tab": "Megnyitás új lapon",
"title": "Fájlok",
"search_placeholder": "Fájlok keresése...",
@@ -3240,6 +3338,8 @@
"rename": "Átnevezés",
"rename_title": "Átnevezés",
"new_name": "Új név",
"name_forbidden_chars": "A kiszolgáló nem engedi ezeket a karaktereket a nevekben: {chars}",
"name_reserved": "Ez a név a kiszolgálón foglalt.",
"delete": "Törlés",
"delete_confirm_title": "Erőforrás törlése",
"delete_confirm_message": "Biztosan törölni szeretnéd a \"{name}\" elemet? Ez nem vonható vissza.",
@@ -3501,5 +3601,45 @@
"copy_event": "Esemény linkjének másolása",
"copied": "Link másolva",
"copy_failed": "Nem sikerült másolni a linket"
},
"plugins": {
"oauth_callback": {
"completing": "Hitelesítés befejezése…",
"completing_message": "Kérjük, várjon, amíg a kapcsolat létrejön.",
"received_title": "Hitelesítés fogadva",
"received_message": "Visszatérhet arra a lapra, ahol elindította a kapcsolódást.",
"failed_title": "A hitelesítés sikertelen",
"denied": "A hitelesítés megtagadva.",
"failed_detail": "A hitelesítés sikertelen: {error}",
"missing_params": "Hiányzik a hitelesítési kód vagy az állapot.",
"delivery_failed": "A hitelesítés eredményét nem sikerült átadni az alkalmazás lapjának. Zárja be ezt az ablakot, és próbálja újra."
}
},
"global_search": {
"title": "Keresés mindenben",
"placeholder": "Keresés a levélben, névjegyekben, naptárban, fájlokban...",
"scope_label": "Keresés itt",
"account_label": "Fiók",
"scope_all": "Minden",
"scope_mail": "Levelek",
"scope_contacts": "Névjegyek",
"scope_calendar": "Naptár",
"scope_files": "Fájlok",
"all_accounts": "Minden fiók",
"show_all": "Összes találat megjelenítése",
"no_results": "Nincs találat erre: „{query}”",
"server_hint": "A teljes szavak a szerveren keresnek; a szórészletek a már betöltött elemekre illeszkednek",
"searching": "Keresés...",
"account_error": "Nem sikerült keresni itt: {account}",
"recent_searches": "Legutóbbi keresések",
"recurring": "Ismétlődő",
"no_subject": "(no subject)",
"empty_hint": "Keressen levelet, névjegyet, eseményt és fájlt minden fiókban. Próbálja ki: from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Lapos, dátum szerint",
"grouped": "Csoportosított",
"load_more": "Továbbiak betöltése",
"preview_empty": "Válasszon egy találatot az előnézethez",
"open_full": "Megnyitás",
"view_label": "Nézet"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "e altri {count}",
"draft": "Bozza",
"starred": "Speciale",
"pinned": "Fissato",
"replied": "Risposto",
"forwarded": "Inoltrato",
"has_attachment": "Con allegato",
"conversations_count": "{count} di {total} conversazioni",
"conversations_count_plus": "{count}+ conversazioni",
"conversations_count_simple": "{count} conversazioni",
@@ -292,6 +296,7 @@
"forward": "Inoltra",
"delete": "Elimina",
"archive": "Archivia",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Aggiungi stella",
"unstar": "Rimuovi stella",
"mark_unread": "Segna come non letto",
@@ -303,12 +308,10 @@
"move": "Sposta",
"print": "Stampa",
"view_source": "Visualizza sorgente",
"fullscreen": "Schermo intero",
"exit_fullscreen": "Esci da schermo intero",
"export_email": "Esporta come .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Inoltra come allegato",
"import_email": "Importa .eml o .zip",
"keyboard_shortcuts": "Scorciatoie da tastiera (?)",
"email_source": "Sorgente del messaggio",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Conferma di lettura richiesta (clicca per disattivare)",
"read_receipt_off": "Richiedi una conferma di lettura",
"dsn_on": "Notifica di consegna richiesta (clic per disattivare)",
"dsn_off": "Richiedi una notifica di consegna",
"format_plain_text": "Passa a testo semplice",
"format_rich_text": "Passa a testo formattato (HTML)",
"require_tls_on": "Consegna cifrata obbligatoria (clic per disattivare)",
"require_tls_off": "Richiedi consegna cifrata (TLS)",
"new_message": "Nuovo messaggio",
"signature_edit_hint": "Doppio clic per modificare la firma",
"reply": "Rispondi",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Sovrascrivi",
"toggle_on": "Annulla sovrascrittura",
"toggle_tooltip": "Modifica liberamente nome e indirizzo del mittente. La posta viene comunque inviata tramite la tua identità - cambia solo l'intestazione Da visibile.",
"toggle_tooltip": "Modifica liberamente nome e indirizzo del mittente. La posta viene comunque inviata tramite la tua identità.",
"name_label": "Nome mittente",
"name_placeholder": "Nome",
"email_label": "Indirizzo email del mittente",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Se il tuo server non accetta questo indirizzo come mittente della busta, viene usato {identity} al suo posto, e i destinatari possono vederlo nell'intestazione Return-Path."
},
"use_template": "Modello",
"save_as_template": "Salva come modello",
@@ -648,6 +658,8 @@
"upload_progress": "Caricamento {uploaded} / {total}",
"upload_cancel": "Annulla caricamento",
"upload_failed": "Caricamento di {filename} non riuscito",
"attachment_too_large": "\"{name}\" supera la dimensione consentita dal server ({max} per file)",
"attachments_total_too_large": "Gli allegati supererebbero il limite del server di {max} per messaggio",
"send_failed": "Invio dell'e-mail non riuscito",
"smime_sign_on": "Firma S/MIME attivata",
"smime_sign_off": "Firma S/MIME disattivata",
@@ -726,7 +738,11 @@
"delete_column": "Elimina colonna",
"toggle_header_row": "Attiva/disattiva riga di intestazione",
"delete_table": "Elimina tabella",
"pick_size": "Scegli dimensione"
"pick_size": "Scegli dimensione",
"font_size": "Dimensione carattere",
"font_size_default": "Predefinita",
"background_color": "Colore di sfondo",
"remove_background_color": "Rimuovi colore di sfondo"
},
"send_filing_warning": "Inviato - ma la pulizia successiva non è riuscita, potrebbe restare una bozza obsoleta."
},
@@ -765,6 +781,10 @@
"email_sent": "Messaggio inviato con successo",
"email_deleted": "Messaggio eliminato",
"email_archived": "Messaggio archiviato",
"emails_deleted": "{count, plural, one {# email eliminata} other {# email eliminate}}",
"emails_archived": "{count, plural, one {# email archiviata} other {# email archiviate}}",
"error_archiving": "Impossibile archiviare l'email",
"error_updating": "Impossibile aggiornare l'email",
"email_starred": "Stella aggiunta al messaggio",
"email_unstarred": "Stella rimossa dal messaggio",
"email_marked_read": "Messaggio segnato come letto",
@@ -1143,7 +1163,9 @@
"enabled": "Notifiche e-mail",
"enabled_desc": "Mostra notifiche all'arrivo di nuove e-mail",
"sound": "Suono di notifica",
"sound_desc": "Riproduci un suono all'arrivo di nuove e-mail"
"sound_desc": "Riproduci un suono all'arrivo di nuove e-mail",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Notifiche calendario",
@@ -1172,6 +1194,7 @@
"status_busy": "In elaborazione…",
"status_inactive": "Non abilitato su questo dispositivo",
"status_unsupported": "Questo browser non supporta Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Notifiche in background",
"devices_title": "Dispositivi registrati",
"devices_desc": "Tutti i dispositivi registrati per ricevere le notifiche in background di questo account. Revocandone uno, smette subito di riceverle.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Cancella la ricerca quando cambi cartella",
"description": "Facendo clic su una cartella (Posta in arrivo, Inviati…) la ricerca attiva viene cancellata e si apre quella cartella. Se disattivato, la ricerca resta attiva e viene rieseguita nella cartella scelta."
},
"request_read_receipt": {
"label": "Richiedi conferme di lettura per impostazione predefinita",
"description": "Attiva in anticipo la richiesta di conferma di lettura durante la composizione di un nuovo messaggio."
@@ -1316,6 +1343,12 @@
"label": "Seleziona automaticamente l'indirizzo di risposta",
"description": "Quando rispondi, passa automaticamente l'indirizzo mittente all'identità che ha ricevuto il messaggio originale"
},
"reply_identity_match": {
"label": "Corrispondenza dell'indirizzo di ricezione",
"description": "Quali indirizzi di ricezione contano come tuoi. «Solo indirizzo esatto» sceglie soltanto una delle tue identità configurate; «stesso dominio» tratta anche qualsiasi altro indirizzo dei tuoi domini di identità come alias catch-all e riscrive con esso l'intestazione Da. Scegli l'indirizzo esatto se quegli indirizzi sono liste di distribuzione.",
"exact": "Solo indirizzo esatto",
"domain": "Qualsiasi indirizzo dei miei domini"
},
"signature_position": {
"label": "Posizione della firma",
"description": "Dove inserire la tua firma nelle risposte e negli inoltri. Sopra il testo citato si legge in modo naturale come chiusura della risposta; sotto mantiene il messaggio originale contiguo.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "La data di fine deve essere successiva alla data di inizio",
"start_in_past": "La data di inizio è nel passato",
"empty_body": "Il corpo del messaggio è vuoto - i destinatari riceveranno una risposta vuota"
"empty_body": "Il corpo del messaggio è vuoto - i destinatari riceveranno una risposta vuota",
"subject_too_long": "L'oggetto è troppo lungo per il server (al massimo {max} byte).",
"body_too_long": "Il messaggio è troppo lungo per il server (al massimo {max} byte, formattazione inclusa)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Cestino",
"role_junk": "Spam / Indesiderata",
"role_archive": "Archivio",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Nessuno",
"create_folder": "Crea cartella",
"create_subfolder": "Crea sottocartella",
@@ -1882,7 +1919,9 @@
"categories_description": "Rinomina le categorie dei contatti",
"no_categories": "Nessuna categoria trovata",
"group_by_letter_description": "Mostra intestazioni alfabetiche nell'elenco dei contatti",
"group_by_letter_label": "Raggruppa per prima lettera"
"group_by_letter_label": "Raggruppa per prima lettera",
"sort_by_last_name_label": "Ordina per cognome",
"sort_by_last_name_description": "Ordina l'elenco dei contatti per cognome così i familiari compaiono insieme"
},
"downloads": {
"title": "Download",
@@ -1963,6 +2002,7 @@
"actions": "Azioni",
"add_action": "Aggiungi azione",
"stop_processing": "Interrompere l'elaborazione delle regole successive",
"include_spam": "Sposta anche i messaggi contrassegnati come spam",
"attachment_type_placeholder": "es. pdf, doc, jpg",
"value_placeholder_multi": "Valore (più valori separati da virgole)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Seleziona cartella",
"forward_to": "Inoltra a indirizzo email",
"forward_placeholder": "email@esempio.com",
"forward_keep_copy": "Conserva una copia",
"forward_limit": "Limite di inoltri per messaggio su questo server: {count}. Gli inoltri in eccesso vengono ignorati.",
"reject_message": "Messaggio di rifiuto",
"reject_placeholder": "La tua email è stata rifiutata",
"label_name": "Nome dell'etichetta",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Nuova sottocartella...",
"new_folder": "Nuova cartella...",
"rename": "Rinomina...",
"share": "Condividi...",
"import_email": "Importa .eml o .zip...",
"empty_folder": "Svuota cartella",
"empty_folder_generic": "Svuota cartella",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Impossibile eliminare la cartella",
"toast_error_delete_has_children": "La cartella contiene sottocartelle. Rimuoverle prima.",
"toast_error_delete_has_email": "La cartella non è vuota. Svuotarla prima.",
"delete_with_contents_title": "La cartella non è vuota",
"delete_with_contents_message": "La cartella \"{name}\" contiene ancora {count, plural, one {1 messaggio} other {# messaggi}}. Eliminare la cartella e tutti i suoi messaggi? L'operazione non può essere annullata.",
"delete_with_contents_confirm": "Elimina cartella e messaggi",
"placeholder_folder_name": "Nome cartella",
"create": "Crea",
"rename_confirm": "Rinomina"
@@ -2270,7 +2316,8 @@
"search": "Cerca",
"help": "Mostra scorciatoie",
"refresh": "Aggiorna messaggi",
"select_all": "Seleziona tutto"
"select_all": "Seleziona tutto",
"global_search": "Cerca ovunque"
},
"threads": {
"expand_collapse": "Espandi/comprimi conversazione"
@@ -2721,7 +2768,10 @@
"any_month": "Qualsiasi mese",
"has_email": "Con email",
"has_phone": "Con telefono",
"has_photo": "Con foto"
"has_photo": "Con foto",
"sort_by": "Ordina per",
"sort_first_name": "Nome",
"sort_last_name": "Cognome"
},
"open_categories": "Apri categorie"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Duplica",
"today_header": "Oggi",
"tomorrow_header": "Domani",
"agenda_show_earlier": "Mostra eventi precedenti",
"agenda_loading": "Caricamento…",
"agenda_range_start": "Eventi dal {date}",
"agenda_range_end": "Eventi fino al {date}",
"export_ics": "Esporta come .ics",
"copy_title": "Copia titolo",
"copy_link": "Copia link riunione",
@@ -2793,6 +2847,14 @@
"color": "Colore"
},
"participants": {
"availability": {
"title": "Disponibilità",
"free": "Disponibile",
"busy": "Occupato",
"tentative": "Provvisorio",
"unknown": "Non è un utente di questo server",
"checking": "Verifica in corso…"
},
"title": "Partecipanti",
"add": "Aggiungi partecipante",
"organizer": "Organizzatore",
@@ -2867,7 +2929,10 @@
"all_events": "Tutti gli eventi",
"cancel": "Annulla",
"save": "Salva",
"delete": "Elimina"
"delete": "Elimina",
"rsvp_title": "Rispondi a evento ricorrente",
"rsvp_description": "Questo è un evento ricorrente. A quali eventi si applica la tua risposta?",
"respond": "Rispondi"
},
"alerts": {
"title": "Promemoria",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Visualizza gli orari degli eventi nella vista mensile del calendario",
"show_week_numbers": "Mostra numeri di settimana",
"show_week_numbers_desc": "Mostra i numeri di settimana nel mini-calendario",
"calendar_free_scroll": "Scorrimento libero",
"calendar_free_scroll_desc": "Scorri in modo continuo tra mesi, settimane e giorni invece di un periodo alla volta",
"enable_tasks": "Abilita attività",
"enable_tasks_desc": "Mostra una vista attività nel calendario per gestire le cose da fare",
"show_tasks_on_calendar": "Mostra attività nel calendario",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Ritardo di 1 secondo",
"hover_preview_delay_2s": "Ritardo di 2 secondi",
"hover_preview_off": "Disabilitato",
"organizer_identity": "Organizza gli inviti come",
"organizer_identity_desc": "L'indirizzo del calendario usato come organizzatore quando inviti persone agli eventi.",
"organizer_identity_failed": "Impossibile cambiare l'indirizzo dell'organizzatore",
"show_birthday_calendar": "Calendario compleanni",
"show_birthday_calendar_desc": "Mostra un calendario virtuale con i compleanni dei tuoi contatti"
},
@@ -2947,6 +3017,9 @@
"alert_now": "Inizia ora",
"alert_in_minutes": "Tra {count} min",
"invitation_sent": "Inviti inviati",
"invitations_denied_title": "Impossibile inviare gli inviti",
"invitations_denied": "Il server ha rifiutato di inviare gli inviti: {reason}. Salvare l'evento senza inviarli?",
"save_without_invitations": "Salva senza inviti",
"rsvp_updated": "Risposta aggiornata",
"rsvp_error": "Impossibile aggiornare la risposta",
"event_duplicated": "Evento duplicato",
@@ -2975,7 +3048,7 @@
"supported_formats": "File iCalendar (.ics) supportati",
"url_description": "Inserisci l'URL di un feed iCalendar (.ics) esterno per importare eventi.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Supporta URL CalDAV e iCalendar (.ics)",
"url_hint": "Supporta URL iCalendar (.ics)",
"fetch": "Recupera",
"invalid_url": "Inserisci un URL valido",
"url_fetch_failed": "Impossibile recuperare il calendario dall'URL",
@@ -3164,6 +3237,8 @@
"has_attachment": "Allegati",
"date_after": "Dopo il",
"date_before": "Prima del",
"size_min": "Maggiore di (KB)",
"size_max": "Minore di (KB)",
"starred": "Preferiti",
"unread": "Non letti",
"read": "Letti",
@@ -3205,6 +3280,10 @@
"hint": "Clicca su un'email a sinistra per iniziare, oppure fai il tour."
},
"files": {
"office_edit": "Modifica documento",
"office_loading": "Apertura dell'editor di documenti…",
"office_error": "Impossibile aprire l'editor di documenti.",
"office_close": "Chiudi editor",
"open_in_new_tab": "Apri in una nuova scheda",
"title": "File",
"search_placeholder": "Cerca file...",
@@ -3217,6 +3296,8 @@
"rename": "Rinomina",
"rename_title": "Rinomina",
"new_name": "Nuovo nome",
"name_forbidden_chars": "Il server non consente questi caratteri nei nomi: {chars}",
"name_reserved": "Questo nome è riservato sul server.",
"delete": "Elimina",
"delete_confirm_title": "Elimina risorsa",
"delete_confirm_message": "Sei sicuro di voler eliminare \"{name}\"? Questa azione non può essere annullata.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "La ricerca non è disponibile nella vista unificata"
},
"calendar_event_notifications": {
"someone": "Qualcuno",
"invited": "{name} ti ha invitato a \"{title}\"",
"updated": "{name} ha aggiornato \"{title}\"",
"cancelled": "{name} ha annullato \"{title}\"",
"untitled": "Evento senza titolo"
},
"share_notifications": {
"someone": "Qualcuno",
"shared": "{name} ha condiviso con te {kind} \"{object}\"",
"changed": "{name} ha modificato il tuo accesso a {kind} \"{object}\"",
"revoked": "{name} ha rimosso il tuo accesso a {kind} \"{object}\"",
"object": {
"folder": "la cartella",
"calendar": "il calendario",
"address_book": "la rubrica",
"files": "la cartella dei file"
}
},
"sharing": {
"title": "Condividi \"{name}\"",
"description": "Concedi l'accesso ad altri utenti o gruppi su questo server. Le modifiche hanno effetto immediato.",
@@ -3501,5 +3601,45 @@
"copy_event": "Copia link all'evento",
"copied": "Link copiato",
"copy_failed": "Impossibile copiare il link"
},
"plugins": {
"oauth_callback": {
"completing": "Completamento dell'autorizzazione…",
"completing_message": "Attendi mentre viene stabilita la connessione.",
"received_title": "Autorizzazione ricevuta",
"received_message": "Puoi tornare alla scheda in cui hai avviato la connessione.",
"failed_title": "Autorizzazione non riuscita",
"denied": "Lautorizzazione è stata negata.",
"failed_detail": "Autorizzazione non riuscita: {error}",
"missing_params": "Codice di autorizzazione o stato mancante.",
"delivery_failed": "Impossibile consegnare il risultato dellautorizzazione alla scheda dellapp. Chiudi questa finestra e riprova."
}
},
"global_search": {
"title": "Cerca ovunque",
"placeholder": "Cerca email, contatti, calendario, file...",
"scope_label": "Cerca in",
"account_label": "Account",
"scope_all": "Tutto",
"scope_mail": "Posta",
"scope_contacts": "Contatti",
"scope_calendar": "Calendario",
"scope_files": "File",
"all_accounts": "Tutti gli account",
"show_all": "Mostra tutti i risultati",
"no_results": "Nessun risultato per “{query}”",
"server_hint": "Le parole complete cercano sul server; quelle parziali corrispondono agli elementi già caricati",
"searching": "Ricerca in corso...",
"account_error": "Impossibile cercare in {account}",
"recent_searches": "Ricerche recenti",
"recurring": "Ricorrente",
"no_subject": "(no subject)",
"empty_hint": "Cerca email, contatti, eventi e file in tutti gli account. Prova from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Piatto per data",
"grouped": "Raggruppato",
"load_more": "Carica altri",
"preview_empty": "Seleziona un risultato per l'anteprima",
"open_full": "Apri",
"view_label": "Vista"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "他{count}人",
"draft": "下書き",
"starred": "スター付き",
"pinned": "ピン留め",
"replied": "返信済み",
"forwarded": "転送済み",
"has_attachment": "添付ファイルあり",
"conversations_count": "{total}件中{count}件の会話",
"conversations_count_plus": "{count}件以上の会話",
"conversations_count_simple": "{count}件の会話",
@@ -292,6 +296,7 @@
"forward": "転送",
"delete": "削除",
"archive": "アーカイブ",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "スターを付ける",
"unstar": "スターを外す",
"mark_unread": "未読にする",
@@ -303,12 +308,10 @@
"move": "移動",
"print": "印刷",
"view_source": "ソースを表示",
"fullscreen": "全画面表示",
"exit_fullscreen": "全画面表示を終了",
"export_email": ".emlとしてエクスポート",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "添付ファイルとして転送",
"import_email": ".eml または .zip をインポート",
"keyboard_shortcuts": "キーボードショートカット (?)",
"email_source": "メールソース",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "開封確認を要求中(クリックで無効化)",
"read_receipt_off": "開封確認を要求する",
"dsn_on": "配信通知を要求中(クリックで解除)",
"dsn_off": "配信通知を要求する",
"format_plain_text": "プレーンテキストに切り替え",
"format_rich_text": "リッチテキスト (HTML) に切り替え",
"require_tls_on": "暗号化配信を必須にしています(クリックで解除)",
"require_tls_off": "暗号化配信を必須にする(TLS)",
"new_message": "新規メッセージ",
"signature_edit_hint": "ダブルクリックで署名を編集",
"reply": "返信",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "上書き",
"toggle_on": "上書きを取り消す",
"toggle_tooltip": "差出人名とアドレスを自由に編集できます。メールは引き続きあなたのアイデンティティ経由で送信されます - 表示される差出人ヘッダーのみが変更されます。",
"toggle_tooltip": "差出人名とアドレスを自由に編集できます。メールは引き続きあなたのアイデンティティ経由で送信されます。",
"name_label": "差出人名",
"name_placeholder": "名前",
"email_label": "差出人メールアドレス",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "サーバーがこのアドレスをエンベロープ送信者として受け付けない場合は、代わりに {identity} が使われ、受信者は Return-Path ヘッダーでそのアドレスを確認できます。"
},
"use_template": "テンプレート",
"save_as_template": "テンプレートとして保存",
@@ -648,6 +658,8 @@
"upload_progress": "アップロード中 {uploaded} / {total}",
"upload_cancel": "アップロードをキャンセル",
"upload_failed": "{filename} のアップロードに失敗しました",
"attachment_too_large": "「{name}」はサーバーの上限を超えています(1 ファイルあたり {max})",
"attachments_total_too_large": "添付ファイルがサーバーの上限(1 通あたり {max})を超えます",
"send_failed": "メールの送信に失敗しました",
"smime_sign_on": "S/MIME 署名を有効化しました",
"smime_sign_off": "S/MIME 署名を無効化しました",
@@ -726,7 +738,11 @@
"delete_column": "列を削除",
"toggle_header_row": "ヘッダー行の切り替え",
"delete_table": "表を削除",
"pick_size": "サイズを選択"
"pick_size": "サイズを選択",
"font_size": "文字サイズ",
"font_size_default": "デフォルト",
"background_color": "背景色",
"remove_background_color": "背景色を削除"
},
"send_filing_warning": "送信されましたが、送信後の整理に失敗しました。古い下書きが残る場合があります。"
},
@@ -765,6 +781,10 @@
"email_sent": "メールを送信しました",
"email_deleted": "メールを削除しました",
"email_archived": "メールをアーカイブしました",
"emails_deleted": "{count, plural, other {#件のメールを削除しました}}",
"emails_archived": "{count, plural, other {#件のメールをアーカイブしました}}",
"error_archiving": "メールをアーカイブできませんでした",
"error_updating": "メールを更新できませんでした",
"email_starred": "スターを付けました",
"email_unstarred": "スターを外しました",
"email_marked_read": "既読にしました",
@@ -1143,7 +1163,9 @@
"enabled": "メール通知",
"enabled_desc": "新しいメールが届いたときに通知を表示",
"sound": "通知音",
"sound_desc": "新しいメールが届いたときに音を鳴らす"
"sound_desc": "新しいメールが届いたときに音を鳴らす",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "カレンダー通知",
@@ -1172,6 +1194,7 @@
"status_busy": "処理中…",
"status_inactive": "このデバイスでは有効になっていません",
"status_unsupported": "このブラウザは Web Push をサポートしていません",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "バックグラウンド通知",
"devices_title": "登録済みのデバイス",
"devices_desc": "このアカウントのバックグラウンド通知を受け取るよう登録されているデバイスの一覧です。取り消すと、そのデバイスはすぐに受信を停止します。",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "フォルダー切り替え時に検索をクリア",
"description": "フォルダー(受信トレイ、送信済みなど)をクリックすると、有効な検索をクリアしてそのフォルダーを表示します。オフの場合、検索は保持され、切り替え先のフォルダーで再実行されます。"
},
"request_read_receipt": {
"label": "既定で開封確認を要求する",
"description": "新しいメッセージの作成時に、開封確認の要求をあらかじめ有効にします。"
@@ -1316,6 +1343,12 @@
"label": "返信元アドレスを自動選択",
"description": "返信時に、元のメッセージを受信したIDへ差出人アドレスを自動的に切り替えます"
},
"reply_identity_match": {
"label": "受信アドレスの照合",
"description": "どの受信アドレスを自分のものとみなすか。「完全一致のみ」は設定済みのアイデンティティからのみ選択します。「同じドメイン」はアイデンティティのドメイン上の他のアドレスもキャッチオールのエイリアスとして扱い、From ヘッダーをそのアドレスに書き換えます。それらのアドレスが配布リストの場合は完全一致を選択してください。",
"exact": "完全一致のみ",
"domain": "自分のドメイン上の任意のアドレス"
},
"signature_position": {
"label": "署名の位置",
"description": "返信や転送で署名を挿入する位置。引用テキストの上は返信の締めとして自然に読めます。下は元のメッセージを続けて表示します。",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "終了日は開始日より後に設定してください",
"start_in_past": "開始日が過去の日付です",
"empty_body": "メッセージ本文が空です - 受信者には空の返信が届きます"
"empty_body": "メッセージ本文が空です - 受信者には空の返信が届きます",
"subject_too_long": "件名がサーバーの上限を超えています(最大 {max} バイト)。",
"body_too_long": "メッセージがサーバーの上限を超えています(書式を含めて最大 {max} バイト)。"
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "ゴミ箱",
"role_junk": "迷惑メール",
"role_archive": "アーカイブ",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "なし",
"create_folder": "フォルダーを作成",
"create_subfolder": "サブフォルダーを作成",
@@ -1882,7 +1919,9 @@
"categories_description": "連絡先カテゴリの名前を変更",
"no_categories": "カテゴリが見つかりません",
"group_by_letter_description": "連絡先リストにアルファベット順のセクション見出しを表示",
"group_by_letter_label": "頭文字でグループ化"
"group_by_letter_label": "頭文字でグループ化",
"sort_by_last_name_label": "姓で並べ替え",
"sort_by_last_name_description": "家族が並んで表示されるように、連絡先一覧を姓の順に並べ替えます"
},
"downloads": {
"title": "ダウンロード",
@@ -1963,6 +2002,7 @@
"actions": "アクション",
"add_action": "アクションを追加",
"stop_processing": "以降のルールの処理を停止",
"include_spam": "迷惑メールと判定されたメッセージも移動する",
"attachment_type_placeholder": "例: pdf, doc, jpg",
"value_placeholder_multi": "値(カンマで複数指定可)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "フォルダーを選択",
"forward_to": "転送先メールアドレス",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "コピーを残す",
"forward_limit": "このサーバーでの1通あたりの転送上限: {count}。それを超える転送はスキップされます。",
"reject_message": "拒否メッセージ",
"reject_placeholder": "あなたのメールは拒否されました",
"label_name": "ラベル名",
@@ -2207,6 +2249,7 @@
"new_subfolder": "新しいサブフォルダー...",
"new_folder": "新しいフォルダー...",
"rename": "名前を変更...",
"share": "共有...",
"import_email": ".eml または .zip をインポート...",
"empty_folder": "フォルダーを空にする",
"empty_folder_generic": "フォルダーを空にする",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "フォルダーを削除できませんでした",
"toast_error_delete_has_children": "フォルダーにサブフォルダーがあります。先に削除してください。",
"toast_error_delete_has_email": "フォルダーが空ではありません。先に空にしてください。",
"delete_with_contents_title": "フォルダーが空ではありません",
"delete_with_contents_message": "フォルダー「{name}」にはまだ {count} 件のメッセージがあります。フォルダーとすべてのメッセージを削除しますか?この操作は元に戻せません。",
"delete_with_contents_confirm": "フォルダーとメッセージを削除",
"placeholder_folder_name": "フォルダー名",
"create": "作成",
"rename_confirm": "名前を変更"
@@ -2270,7 +2316,8 @@
"search": "検索にフォーカス",
"help": "ショートカットを表示",
"refresh": "メールを更新",
"select_all": "すべて選択"
"select_all": "すべて選択",
"global_search": "すべてを検索"
},
"threads": {
"expand_collapse": "スレッドの展開/折りたたみ"
@@ -2721,7 +2768,10 @@
"any_month": "すべての月",
"has_email": "メールあり",
"has_phone": "電話あり",
"has_photo": "写真あり"
"has_photo": "写真あり",
"sort_by": "並べ替え",
"sort_first_name": "名",
"sort_last_name": "姓"
},
"open_categories": "カテゴリを開く"
},
@@ -2758,6 +2808,10 @@
"duplicate": "複製",
"today_header": "今日",
"tomorrow_header": "明日",
"agenda_show_earlier": "以前の予定を表示",
"agenda_loading": "読み込み中…",
"agenda_range_start": "{date}以降の予定",
"agenda_range_end": "{date}までの予定",
"export_ics": ".icsとしてエクスポート",
"copy_title": "タイトルをコピー",
"copy_link": "会議リンクをコピー",
@@ -2793,6 +2847,14 @@
"color": "色"
},
"participants": {
"availability": {
"title": "空き状況",
"free": "空き",
"busy": "予定あり",
"tentative": "仮",
"unknown": "このサーバーのユーザーではありません",
"checking": "確認中…"
},
"title": "参加者",
"add": "参加者を追加",
"organizer": "主催者",
@@ -2867,7 +2929,10 @@
"all_events": "すべてのイベント",
"cancel": "キャンセル",
"save": "保存",
"delete": "削除"
"delete": "削除",
"rsvp_title": "繰り返しイベントに返信",
"rsvp_description": "これは繰り返しイベントです。どのイベントに返信しますか?",
"respond": "返信"
},
"alerts": {
"title": "リマインダー",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "月カレンダー表示でイベントの時刻を表示する",
"show_week_numbers": "週番号を表示",
"show_week_numbers_desc": "ミニカレンダーに週番号を表示する",
"calendar_free_scroll": "自由スクロール",
"calendar_free_scroll_desc": "1つの期間ずつではなく、月・週・日を連続してスクロールします",
"enable_tasks": "タスクを有効化",
"enable_tasks_desc": "カレンダーにタスク表示を追加して、ToDoを管理します",
"show_tasks_on_calendar": "カレンダーにタスクを表示",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1秒遅延",
"hover_preview_delay_2s": "2秒遅延",
"hover_preview_off": "無効",
"organizer_identity": "招待の主催者アドレス",
"organizer_identity_desc": "イベントに人を招待する際に主催者として使用するカレンダーアドレスです。",
"organizer_identity_failed": "主催者アドレスを変更できませんでした",
"show_birthday_calendar": "誕生日カレンダー",
"show_birthday_calendar_desc": "連絡先の誕生日を表示する仮想カレンダーを表示します"
},
@@ -2947,6 +3017,9 @@
"alert_now": "まもなく開始",
"alert_in_minutes": "{count}分後",
"invitation_sent": "招待を送信しました",
"invitations_denied_title": "招待状を送信できません",
"invitations_denied": "サーバーが招待状の送信を拒否しました: {reason}。招待状を送らずに予定を保存しますか?",
"save_without_invitations": "招待状なしで保存",
"rsvp_updated": "回答を更新しました",
"rsvp_error": "回答の更新に失敗しました",
"event_duplicated": "予定を複製しました",
@@ -2975,7 +3048,7 @@
"supported_formats": "iCalendar (.ics) ファイルに対応",
"url_description": "外部のiCalendar (.ics) フィードのURLを入力してイベントをインポートします。",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "CalDAVおよびiCalendar (.ics) URLに対応",
"url_hint": "iCalendar (.ics) URLに対応",
"fetch": "取得",
"invalid_url": "有効なURLを入力してください",
"url_fetch_failed": "URLからカレンダーを取得できませんでした",
@@ -3164,6 +3237,8 @@
"has_attachment": "添付ファイル",
"date_after": "以降",
"date_before": "以前",
"size_min": "次のサイズ以上 (KB)",
"size_max": "次のサイズ未満 (KB)",
"starred": "スター付き",
"unread": "未読",
"read": "既読",
@@ -3205,6 +3280,10 @@
"hint": "左のメールをクリックして始めるか、ツアーを開始してください。"
},
"files": {
"office_edit": "ドキュメントを編集",
"office_loading": "ドキュメントエディターを開いています…",
"office_error": "ドキュメントエディターを開けませんでした。",
"office_close": "エディターを閉じる",
"open_in_new_tab": "新しいタブで開く",
"title": "ファイル",
"search_placeholder": "ファイルを検索...",
@@ -3217,6 +3296,8 @@
"rename": "名前を変更",
"rename_title": "名前を変更",
"new_name": "新しい名前",
"name_forbidden_chars": "サーバーでは名前に次の文字を使用できません: {chars}",
"name_reserved": "この名前はサーバーで予約されています。",
"delete": "削除",
"delete_confirm_title": "リソースを削除",
"delete_confirm_message": "\"{name}\"を削除してもよろしいですか?この操作は元に戻せません。",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "統合ビューでは検索を利用できません"
},
"calendar_event_notifications": {
"someone": "誰か",
"invited": "{name} さんが「{title}」に招待しました",
"updated": "{name} さんが「{title}」を更新しました",
"cancelled": "{name} さんが「{title}」をキャンセルしました",
"untitled": "無題の予定"
},
"share_notifications": {
"someone": "誰か",
"shared": "{name} さんが{kind}「{object}」を共有しました",
"changed": "{name} さんが{kind}「{object}」へのアクセス権を変更しました",
"revoked": "{name} さんが{kind}「{object}」へのアクセス権を削除しました",
"object": {
"folder": "フォルダー",
"calendar": "カレンダー",
"address_book": "アドレス帳",
"files": "ファイルフォルダー"
}
},
"sharing": {
"title": "「{name}」を共有",
"description": "このサーバー上の他のユーザーまたはグループにアクセス権を付与します。変更はすぐに反映されます。",
@@ -3501,5 +3601,45 @@
"copy_event": "予定のリンクをコピー",
"copied": "リンクをコピーしました",
"copy_failed": "リンクをコピーできませんでした"
},
"plugins": {
"oauth_callback": {
"completing": "認証を完了しています…",
"completing_message": "接続が確立されるまでお待ちください。",
"received_title": "認証を受信しました",
"received_message": "接続を開始したタブに戻ることができます。",
"failed_title": "認証に失敗しました",
"denied": "認証が拒否されました。",
"failed_detail": "認証に失敗しました: {error}",
"missing_params": "認証コードまたはステートがありません。",
"delivery_failed": "認証結果をアプリのタブに渡せませんでした。このウィンドウを閉じて、もう一度お試しください。"
}
},
"global_search": {
"title": "すべてを検索",
"placeholder": "メール、連絡先、カレンダー、ファイルを検索...",
"scope_label": "検索対象",
"account_label": "アカウント",
"scope_all": "すべて",
"scope_mail": "メール",
"scope_contacts": "連絡先",
"scope_calendar": "カレンダー",
"scope_files": "ファイル",
"all_accounts": "すべてのアカウント",
"show_all": "すべての結果を表示",
"no_results": "「{query}」に一致する結果はありません",
"server_hint": "完全な単語はサーバーを検索します。部分一致は読み込み済みの項目に適用されます",
"searching": "検索中...",
"account_error": "{account} を検索できませんでした",
"recent_searches": "最近の検索",
"recurring": "繰り返し",
"no_subject": "(no subject)",
"empty_hint": "すべてのアカウントのメール、連絡先、予定、ファイルを検索できます。from:、subject:、has:attachment、after:2026-01-01 なども試せます。",
"flat_by_date": "日付順のフラット表示",
"grouped": "グループ表示",
"load_more": "さらに読み込む",
"preview_empty": "プレビューする結果を選択してください",
"open_full": "開く",
"view_label": "表示"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "외 {count}명",
"draft": "임시보관",
"starred": "별표 표시됨",
"pinned": "고정됨",
"replied": "답장함",
"forwarded": "전달함",
"has_attachment": "첨부 파일 있음",
"conversations_count": "전체 {total}개 중 {count}개의 대화",
"conversations_count_plus": "{count}개 이상의 대화",
"conversations_count_simple": "{count}개의 대화",
@@ -292,6 +296,7 @@
"forward": "전달",
"delete": "삭제",
"archive": "보관",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "별표 달기",
"unstar": "별표 해제",
"mark_unread": "읽지 않은 상태로 표시",
@@ -303,12 +308,10 @@
"move": "이동",
"print": "인쇄",
"view_source": "원본 보기",
"fullscreen": "전체 화면",
"exit_fullscreen": "전체 화면 종료",
"export_email": ".eml 파일로 내보내기",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "첨부 파일로 전달",
"import_email": ".eml 또는 .zip 가져오기",
"keyboard_shortcuts": "단축키 (?)",
"email_source": "메일 원본",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "읽음 확인 요청됨 (클릭하여 해제)",
"read_receipt_off": "읽음 확인 요청",
"dsn_on": "배달 알림 요청됨 (클릭하여 해제)",
"dsn_off": "배달 알림 요청",
"format_plain_text": "일반 텍스트로 전환",
"format_rich_text": "서식 있는 텍스트(HTML)로 전환",
"require_tls_on": "암호화 전송 필수 (클릭하여 해제)",
"require_tls_off": "암호화 전송 필수 (TLS)",
"new_message": "새 메시지",
"signature_edit_hint": "서명을 편집하려면 두 번 클릭하세요",
"reply": "답장",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "재정의",
"toggle_on": "재정의 취소",
"toggle_tooltip": "보낸 사람 이름과 주소를 자유롭게 편집하세요. 메일은 여전히 사용자의 ID를 통해 전송되며 - 표시되는 보낸 사람 헤더만 변경됩니다.",
"toggle_tooltip": "보낸 사람 이름과 주소를 자유롭게 편집하세요. 메일은 여전히 사용자의 ID를 통해 전송됩니다.",
"name_label": "보낸 사람 이름",
"name_placeholder": "이름",
"email_label": "보낸 사람 이메일 주소",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "서버가 이 주소를 봉투 발신자로 허용하지 않으면 대신 {identity}이(가) 사용되며, 수신자는 Return-Path 헤더에서 이 주소를 볼 수 있습니다."
},
"use_template": "템플릿",
"save_as_template": "템플릿으로 저장",
@@ -648,6 +658,8 @@
"upload_progress": "{uploaded} / {total} 업로드 중",
"upload_cancel": "업로드 취소",
"upload_failed": "{filename} 파일 업로드에 실패했어요",
"attachment_too_large": "\"{name}\"이(가) 서버 허용 크기보다 커요(파일당 {max})",
"attachments_total_too_large": "첨부 파일이 서버의 메시지당 한도({max})를 넘어요",
"drop_files": "첨부할 파일을 여기에 놓아주세요",
"show_less": "간략히 보기",
"send_failed": "메일 전송에 실패했어요",
@@ -726,7 +738,11 @@
"delete_column": "열 삭제",
"toggle_header_row": "머리글 행 전환",
"delete_table": "표 삭제",
"pick_size": "크기 선택"
"pick_size": "크기 선택",
"font_size": "글자 크기",
"font_size_default": "기본값",
"background_color": "배경색",
"remove_background_color": "배경색 제거"
},
"send_filing_warning": "보냈지만 전송 후 정리에 실패했습니다. 오래된 임시 보관 메일이 남아 있을 수 있습니다."
},
@@ -765,6 +781,10 @@
"email_sent": "메일이 전송되었어요",
"email_deleted": "메일이 삭제되었어요",
"email_archived": "메일이 보관되었어요",
"emails_deleted": "{count, plural, other {이메일 #개 삭제됨}}",
"emails_archived": "{count, plural, other {이메일 #개 보관됨}}",
"error_archiving": "이메일을 보관하지 못했습니다",
"error_updating": "이메일을 업데이트하지 못했습니다",
"email_starred": "별표가 추가되었어요",
"email_unstarred": "별표가 해제되었어요",
"email_marked_read": "읽은 상태로 변경되었어요",
@@ -1143,7 +1163,9 @@
"enabled": "이메일 알림",
"enabled_desc": "새 이메일이 도착하면 알림을 보여줘요",
"sound": "알림음",
"sound_desc": "새 이메일이 도착하면 소리로 알려줘요"
"sound_desc": "새 이메일이 도착하면 소리로 알려줘요",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "캘린더 알림",
@@ -1172,6 +1194,7 @@
"status_busy": "처리 중…",
"status_inactive": "이 기기에서 활성화되지 않음",
"status_unsupported": "이 브라우저는 Web Push를 지원하지 않습니다",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "백그라운드 알림",
"devices_title": "등록된 기기",
"devices_desc": "이 계정의 백그라운드 알림을 받도록 등록된 모든 기기입니다. 해지하면 해당 기기는 즉시 수신을 멈추니다.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "폴더 전환 시 검색 지우기",
"description": "폴더(받은편지함, 보낸편지함 등)를 클릭하면 활성 검색을 지우고 해당 폴더를 표시합니다. 꺼져 있으면 검색이 유지되고 이동한 폴더에서 다시 실행됩니다."
},
"request_read_receipt": {
"label": "기본적으로 읽음 확인 요청",
"description": "새 메시지를 작성할 때 읽음 확인 요청을 미리 켭니다."
@@ -1321,6 +1348,12 @@
"label": "답장 시 보내는 사람 자동 선택",
"description": "답장할 때 메일을 받았던 주소로 보내는 사람을 자동으로 변경해요"
},
"reply_identity_match": {
"label": "수신 주소 일치 방식",
"description": "어떤 수신 주소를 내 주소로 간주할지 정합니다. '정확한 주소만'은 설정된 ID 중에서만 선택합니다. '같은 도메인'은 ID 도메인의 다른 주소도 캐치올 별칭으로 간주하여 보낸 사람 헤더를 해당 주소로 바꿉니다. 해당 주소가 배포 목록이라면 정확한 주소를 선택하세요.",
"exact": "정확한 주소만",
"domain": "내 도메인의 모든 주소"
},
"signature_position": {
"label": "서명 위치",
"description": "답장과 전달에서 서명을 삽입할 위치. 인용된 텍스트 위에 두면 답장의 마무리처럼 자연스럽게 읽히고, 아래에 두면 원본 메시지가 이어져 보입니다.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "종료일은 시작일보다 나중이어야 해요",
"start_in_past": "시작일이 이미 지났어요",
"empty_body": "메시지 내용이 비어있어요. 상대방에게 빈 메일이 가게 돼요"
"empty_body": "메시지 내용이 비어있어요. 상대방에게 빈 메일이 가게 돼요",
"subject_too_long": "제목이 서버에 비해 너무 깁니다(최대 {max}바이트).",
"body_too_long": "메시지가 서버에 비해 너무 깁니다(서식 포함 최대 {max}바이트)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "휴지통",
"role_junk": "스팸/정크함",
"role_archive": "보관함",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "없음",
"create_folder": "폴더 만들기",
"create_subfolder": "하위 폴더 만들기",
@@ -1882,7 +1919,9 @@
"categories_description": "연락처 카테고리 이름 변경",
"no_categories": "카테고리를 찾을 수 없음",
"group_by_letter_description": "연락처 목록에 알파벳순 섹션 헤더 표시",
"group_by_letter_label": "첫 글자로 그룹화"
"group_by_letter_label": "첫 글자로 그룹화",
"sort_by_last_name_label": "성으로 정렬",
"sort_by_last_name_description": "가족 구성원이 함께 표시되도록 연락처 목록을 성 기준으로 정렬합니다"
},
"downloads": {
"title": "다운로드",
@@ -1963,6 +2002,7 @@
"actions": "동작",
"add_action": "동작 추가",
"stop_processing": "이후 규칙 무시하기",
"include_spam": "스팸으로 표시된 메시지도 이동",
"attachment_type_placeholder": "예: pdf, doc, jpg",
"value_placeholder_multi": "값 (쉼표로 여러 개 구분)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "폴더 선택",
"forward_to": "전달할 이메일 주소",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "사본 보관",
"forward_limit": "이 서버의 메시지당 전달 한도: {count}. 초과된 전달은 건너뜁니다.",
"reject_message": "거부 메시지",
"reject_placeholder": "메일 수신이 거부되었습니다",
"label_name": "태그 이름",
@@ -2207,6 +2249,7 @@
"new_subfolder": "새 하위 폴더...",
"new_folder": "새 폴더...",
"rename": "이름 바꾸기...",
"share": "공유...",
"import_email": ".eml 또는 .zip 가져오기...",
"empty_folder": "폴더 비우기",
"empty_folder_generic": "폴더 비우기",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "폴더를 삭제하지 못했습니다",
"toast_error_delete_has_children": "폴더에 하위 폴더가 있습니다. 먼저 제거하세요.",
"toast_error_delete_has_email": "폴더가 비어 있지 않습니다. 먼저 비우세요.",
"delete_with_contents_title": "폴더가 비어 있지 않습니다",
"delete_with_contents_message": "\"{name}\" 폴더에 아직 {count}개의 메시지가 있습니다. 폴더와 모든 메시지를 삭제할까요? 이 작업은 취소할 수 없습니다.",
"delete_with_contents_confirm": "폴더와 메시지 삭제",
"placeholder_folder_name": "폴더 이름",
"create": "만들기",
"rename_confirm": "이름 바꾸기"
@@ -2270,7 +2316,8 @@
"search": "검색창 이동",
"help": "단축키 보기",
"refresh": "메일 새로고침",
"select_all": "모두 선택"
"select_all": "모두 선택",
"global_search": "전체 검색"
},
"threads": {
"expand_collapse": "대화 펼치기/접기"
@@ -2721,7 +2768,10 @@
"any_month": "모든 달",
"has_email": "이메일 있음",
"has_phone": "전화번호 있음",
"has_photo": "사진 있음"
"has_photo": "사진 있음",
"sort_by": "정렬 기준",
"sort_first_name": "이름",
"sort_last_name": "성"
},
"open_categories": "카테고리 열기"
},
@@ -2758,6 +2808,10 @@
"duplicate": "복제",
"today_header": "오늘",
"tomorrow_header": "내일",
"agenda_show_earlier": "이전 일정 보기",
"agenda_loading": "불러오는 중…",
"agenda_range_start": "{date}부터의 일정",
"agenda_range_end": "{date}까지의 일정",
"export_ics": ".ics로 내보내기",
"copy_title": "제목 복사",
"copy_link": "회의 링크 복사",
@@ -2793,6 +2847,14 @@
"color": "색상"
},
"participants": {
"availability": {
"title": "일정 상태",
"free": "가능",
"busy": "바쁨",
"tentative": "미정",
"unknown": "이 서버의 사용자가 아님",
"checking": "확인 중…"
},
"title": "참석자",
"add": "참석자 추가",
"organizer": "주최자",
@@ -2867,7 +2929,10 @@
"all_events": "모든 일정",
"cancel": "취소",
"save": "저장",
"delete": "삭제"
"delete": "삭제",
"rsvp_title": "반복 일정에 응답",
"rsvp_description": "반복되는 일정이에요. 어떤 일정에 응답할까요?",
"respond": "응답"
},
"alerts": {
"title": "알림",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "월간 캘린더에서도 일정 시간을 표시해요",
"show_week_numbers": "주 차 번호 표시",
"show_week_numbers_desc": "미니 캘린더에 1년 중 몇 주 차인지 표시해요",
"calendar_free_scroll": "자유 스크롤",
"calendar_free_scroll_desc": "한 번에 한 기간씩 보는 대신 월, 주, 일을 연속으로 스크롤합니다",
"enable_tasks": "할 일 사용",
"enable_tasks_desc": "캘린더에 할 일을 관리할 수 있는 메뉴를 추가해요",
"show_tasks_on_calendar": "캘린더에 할 일 표시",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1초 지연",
"hover_preview_delay_2s": "2초 지연",
"hover_preview_off": "사용 안 함",
"organizer_identity": "초대 주최자 주소",
"organizer_identity_desc": "이벤트에 사람들을 초대할 때 주최자로 사용되는 캘린더 주소입니다.",
"organizer_identity_failed": "주최자 주소를 변경하지 못했습니다",
"show_birthday_calendar": "연락처 생일 캘린더",
"show_birthday_calendar_desc": "연락처에 등록된 생일을 가상 캘린더로 보여줘요"
},
@@ -2947,6 +3017,9 @@
"alert_now": "지금 시작해요",
"alert_in_minutes": "{count}분 후 시작",
"invitation_sent": "초대장이 전송되었어요",
"invitations_denied_title": "초대장을 보낼 수 없어요",
"invitations_denied": "서버가 초대장 발송을 거부했어요: {reason}. 초대장 없이 일정을 저장할까요?",
"save_without_invitations": "초대장 없이 저장",
"rsvp_updated": "응답이 업데이트되었어요",
"rsvp_error": "응답을 업데이트하지 못했어요",
"event_duplicated": "일정이 복제되었어요",
@@ -2975,7 +3048,7 @@
"supported_formats": "iCalendar (.ics) 파일을 지원해요",
"url_description": "외부 iCalendar (.ics) 피드의 URL을 입력해서 일정을 가져와요.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "CalDAV 및 iCalendar (.ics) URL을 지원해요",
"url_hint": "iCalendar (.ics) URL을 지원해요",
"fetch": "가져오기",
"invalid_url": "올바른 URL을 입력해 주세요",
"url_fetch_failed": "URL에서 캘린더를 가져오지 못했어요",
@@ -3164,6 +3237,8 @@
"has_attachment": "첨부파일 있음",
"date_after": "이후",
"date_before": "이전",
"size_min": "다음 크기 이상 (KB)",
"size_max": "다음 크기 미만 (KB)",
"starred": "별표 있음",
"unread": "읽지 않음",
"read": "읽음",
@@ -3205,6 +3280,10 @@
"hint": "왼쪽에서 메일을 클릭해서 시작하거나, 아래의 둘러보기를 클릭해 보세요."
},
"files": {
"office_edit": "문서 편집",
"office_loading": "문서 편집기를 여는 중…",
"office_error": "문서 편집기를 열 수 없습니다.",
"office_close": "편집기 닫기",
"open_in_new_tab": "새 탭에서 열기",
"title": "파일",
"search_placeholder": "파일 검색...",
@@ -3217,6 +3296,8 @@
"rename": "이름 바꾸기",
"rename_title": "이름 바꾸기",
"new_name": "새 이름",
"name_forbidden_chars": "서버에서 이름에 다음 문자를 허용하지 않습니다: {chars}",
"name_reserved": "이 이름은 서버에서 예약되어 있습니다.",
"delete": "삭제",
"delete_confirm_title": "항목 삭제",
"delete_confirm_message": "정말 \"{name}\"을(를) 삭제할까요? 이 작업은 되돌릴 수 없어요.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "통합 보기에서는 검색을 사용할 수 없습니다"
},
"calendar_event_notifications": {
"someone": "누군가",
"invited": "{name}님이 \"{title}\"에 초대했습니다",
"updated": "{name}님이 \"{title}\"을(를) 업데이트했습니다",
"cancelled": "{name}님이 \"{title}\"을(를) 취소했습니다",
"untitled": "제목 없는 일정"
},
"share_notifications": {
"someone": "누군가",
"shared": "{name}님이 {kind} \"{object}\"을(를) 공유했습니다",
"changed": "{name}님이 {kind} \"{object}\"에 대한 접근 권한을 변경했습니다",
"revoked": "{name}님이 {kind} \"{object}\"에 대한 접근 권한을 제거했습니다",
"object": {
"folder": "폴더",
"calendar": "캘린더",
"address_book": "주소록",
"files": "파일 폴더"
}
},
"sharing": {
"title": "\"{name}\" 공유",
"description": "이 서버의 다른 사용자나 그룹에 액세스 권한을 부여합니다. 변경 사항은 즉시 적용됩니다.",
@@ -3501,5 +3601,45 @@
"copy_event": "일정 링크 복사",
"copied": "링크가 복사되었습니다",
"copy_failed": "링크를 복사하지 못했습니다"
},
"plugins": {
"oauth_callback": {
"completing": "인증 완료 중…",
"completing_message": "연결이 설정되는 동안 잠시 기다려 주세요.",
"received_title": "인증 수신됨",
"received_message": "연결을 시작했던 탭으로 돌아갈 수 있습니다.",
"failed_title": "인증 실패",
"denied": "인증이 거부되었습니다.",
"failed_detail": "인증 실패: {error}",
"missing_params": "인증 코드 또는 상태가 누락되었습니다.",
"delivery_failed": "인증 결과를 앱 탭에 전달하지 못했습니다. 이 창을 닫고 다시 시도해 주세요."
}
},
"global_search": {
"title": "전체 검색",
"placeholder": "메일, 연락처, 캘린더, 파일 검색...",
"scope_label": "검색 범위",
"account_label": "계정",
"scope_all": "전체",
"scope_mail": "메일",
"scope_contacts": "연락처",
"scope_calendar": "캘린더",
"scope_files": "파일",
"all_accounts": "모든 계정",
"show_all": "모든 결과 보기",
"no_results": "“{query}”에 대한 결과가 없습니다",
"server_hint": "완전한 단어는 서버에서 검색되며, 단어 일부는 이미 불러온 항목과 일치합니다",
"searching": "검색 중...",
"account_error": "{account}을(를) 검색할 수 없습니다",
"recent_searches": "최근 검색",
"recurring": "반복",
"no_subject": "(no subject)",
"empty_hint": "모든 계정의 메일, 연락처, 일정, 파일을 검색하세요. from:, subject:, has:attachment, after:2026-01-01 도 사용할 수 있습니다.",
"flat_by_date": "날짜순 평뱀 보기",
"grouped": "그룹 보기",
"load_more": "더 불러오기",
"preview_empty": "미리 볼 결과를 선택하세요",
"open_full": "열기",
"view_label": "보기"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "un vēl {count}",
"draft": "Melnraksts",
"starred": "Ar zvaigznīti",
"pinned": "Piesprausts",
"replied": "Atbildēts",
"forwarded": "Pārsūtīts",
"has_attachment": "Ar pielikumu",
"conversations_count": "{count} no {total} sarunām",
"conversations_count_plus": "{count}+ sarunas",
"conversations_count_simple": "{count} sarunas",
@@ -292,6 +296,7 @@
"forward": "Pārsūtīt",
"delete": "Dzēst",
"archive": "Arhivēt",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Pievienot zvaigznīti",
"unstar": "Noņemt zvaigznīti",
"mark_unread": "Atzīmēt kā nelasītu",
@@ -303,12 +308,10 @@
"move": "Pārvietot",
"print": "Drukāt",
"view_source": "Skatīt avota kodu",
"fullscreen": "Pilnekrāna režīms",
"exit_fullscreen": "Iziet no pilnekrāna režīma",
"export_email": "Eksportēt kā .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Pārsūtīt kā pielikumu",
"import_email": "Importēt .eml vai .zip",
"keyboard_shortcuts": "Īsinājumtaustiņi (?)",
"email_source": "Vēstules avota kods",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Pieprasīts lasīšanas apstiprinājums (noklikšķiniet, lai atspējotu)",
"read_receipt_off": "Pieprasīt lasīšanas apstiprinājumu",
"dsn_on": "Piegādes paziņojums pieprasīts (noklikšķiniet, lai atspējotu)",
"dsn_off": "Pieprasīt piegādes paziņojumu",
"format_plain_text": "Pārslēgt uz vienkāršu tekstu",
"format_rich_text": "Pārslēgt uz formatētu tekstu (HTML)",
"require_tls_on": "Nepieciešama šifrēta piegāde (noklikšķiniet, lai atspējotu)",
"require_tls_off": "Pieprasīt šifrētu piegādi (TLS)",
"new_message": "Jauns ziņojums",
"signature_edit_hint": "Veiciet dubultklikšķi, lai rediģētu parakstu",
"reply": "Atbildēt",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Pārrakstīt",
"toggle_on": "Atcelt pārrakstīšanu",
"toggle_tooltip": "Brīvi rediģējiet sūtītāja vārdu un adresi. Pasts joprojām tiek sūtīts caur jūsu identitāti - mainās tikai redzamais No galvenes ieraksts.",
"toggle_tooltip": "Brīvi rediģējiet sūtītāja vārdu un adresi. Pasts joprojām tiek sūtīts caur jūsu identitāti.",
"name_label": "Sūtītāja vārds",
"name_placeholder": "Vārds",
"email_label": "Sūtītāja e-pasta adrese",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Ja jūsu serveris nepieņem šo adresi kā aploksnes sūtītāju, tās vietā tiek izmantota {identity}, un adresāti to var redzēt galvenē Return-Path."
},
"use_template": "Veidne",
"save_as_template": "Saglabāt kā veidni",
@@ -648,6 +658,8 @@
"upload_progress": "Augšupielāde {uploaded} / {total}",
"upload_cancel": "Atcelt augšupielādi",
"upload_failed": "Neizdevās augšupielādēt {filename}",
"attachment_too_large": "“{name}” ir lielāks, nekā serveris atļauj ({max} vienam failam)",
"attachments_total_too_large": "Pielikumi pārsniegtu servera ierobežojumu – {max} vienai ziņai",
"drop_files": "Velciet failus šeit, lai pievienotu",
"show_less": "Rādīt mazāk",
"send_failed": "Neizdevās nosūtīt vēstuli",
@@ -726,7 +738,11 @@
"delete_column": "Dzēst kolonnu",
"toggle_header_row": "Pārslēgt galvenes rindu",
"delete_table": "Dzēst tabulu",
"pick_size": "Izvēlēties izmēru"
"pick_size": "Izvēlēties izmēru",
"font_size": "Fonta izmērs",
"font_size_default": "Noklusējums",
"background_color": "Fona krāsa",
"remove_background_color": "Noņemt fona krāsu"
},
"send_filing_warning": "Nosūtīts - bet pēcapstrāde neizdevās, var palikt novecojis melnraksts."
},
@@ -765,6 +781,10 @@
"email_sent": "Vēstule veiksmīgi nosūtīta",
"email_deleted": "Vēstule izdzēsta",
"email_archived": "Vēstule arhivēta",
"emails_deleted": "{count, plural, zero {# e-pasti dzēsti} one {# e-pasts dzēsts} other {# e-pasti dzēsti}}",
"emails_archived": "{count, plural, zero {# e-pasti arhivēti} one {# e-pasts arhivēts} other {# e-pasti arhivēti}}",
"error_archiving": "Neizdevās arhivēt e-pastu",
"error_updating": "Neizdevās atjaunināt e-pastu",
"email_starred": "Vēstulei pievienota zvaigznīte",
"email_unstarred": "Zvaigznīte noņemta",
"email_marked_read": "Vēstule atzīmēta kā izlasīta",
@@ -1143,7 +1163,9 @@
"enabled": "E-pasta paziņojumi",
"enabled_desc": "Rādīt paziņojumus, saņemot jaunas vēstules",
"sound": "Paziņojuma skaņa",
"sound_desc": "Atskaņot skaņas signālu, saņemot jaunas vēstules"
"sound_desc": "Atskaņot skaņas signālu, saņemot jaunas vēstules",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Kalendāra paziņojumi",
@@ -1172,6 +1194,7 @@
"status_busy": "Notiek darbs…",
"status_inactive": "Nav iespējots šajā ierīcē",
"status_unsupported": "Šī pārlūkprogramma neatbalsta Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Fona paziņojumi",
"devices_title": "Reģistrētās ierīces",
"devices_desc": "Visas ierīces, kas reģistrētas šī konta fona paziņojumu saņemšanai. Atsaucot ierīci, tā nekavējoties pārstāj tās saņemt.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Notīrīt meklēšanu, mainot mapi",
"description": "Noklikšķinot uz mapes (Iesūtne, Nosūtītie…), aktīvā meklēšana tiek notīrīta un tiek atvērta šī mape. Kad izslēgts, meklēšana paliek un tiek atkārtota izvēlētajā mapē."
},
"request_read_receipt": {
"label": "Pēc noklusējuma pieprasīt lasīšanas apstiprinājumus",
"description": "Sastādot jaunu ziņojumu, iepriekš ieslēgt lasīšanas apstiprinājuma pieprasījumu."
@@ -1316,6 +1343,12 @@
"label": "Automātiski izvēlēties atbildes adresi",
"description": "Atbildot automātiski izmantot to kontu, uz kuru vēstule tika saņemta"
},
"reply_identity_match": {
"label": "Saņemšanas adreses atbilstība",
"description": "Kuras saņemšanas adreses tiek uzskatītas par jūsu. „Tikai precīza adrese“ izvēlas vienīgi kādu no jūsu konfigurētajām identitātēm; „tas pats domēns“ arī jebkuru citu adresi jūsu identitāšu domēnos uzskata par catch-all aizstājvārdu un pārraksta ar to galveni „No“. Izvēlieties precīzu adresi, ja šīs adreses ir izplatīšanas saraksti.",
"exact": "Tikai precīza adrese",
"domain": "Jebkura adrese manos domēnos"
},
"signature_position": {
"label": "Paraksta novietojums",
"description": "Kur ievietot jūsu parakstu atbildēs un pārsūtīšanā. Virs citētā teksta tas dabiski lasās kā atbildes noslēgums; zem tā saglabā oriģinālo ziņojumu vienkopus.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "Beigu datumam jābūt vēlāk par sākuma datumu",
"start_in_past": "Sākuma datums ir pagātnē",
"empty_body": "Ziņojuma teksts ir tukšs - saņēmēji saņems tukšu atbildi"
"empty_body": "Ziņojuma teksts ir tukšs - saņēmēji saņems tukšu atbildi",
"subject_too_long": "Temats serverim ir pārāk garš (ne vairāk kā {max} baiti).",
"body_too_long": "Ziņa serverim ir pārāk gara (ne vairāk kā {max} baiti, ieskaitot formatējumu)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Atkritne",
"role_junk": "Mēstules",
"role_archive": "Arhīvs",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Nav",
"create_folder": "Izveidot mapi",
"create_subfolder": "Izveidot apakšmapi",
@@ -1882,7 +1919,9 @@
"categories_description": "Pārdēvēt kontaktu kategorijas",
"no_categories": "Kategorijas nav atrastas",
"group_by_letter_description": "Rādīt alfabētiskos sadaļu virsrakstus kontaktu sarakstā",
"group_by_letter_label": "Grupēt pēc pirmā burta"
"group_by_letter_label": "Grupēt pēc pirmā burta",
"sort_by_last_name_label": "Kārtot pēc uzvārda",
"sort_by_last_name_description": "Kārtot kontaktu sarakstu pēc uzvārda, lai ģimenes locekļi būtu kopā"
},
"downloads": {
"title": "Lejupielādes",
@@ -1963,6 +2002,7 @@
"actions": "Darbības",
"add_action": "Pievienot darbību",
"stop_processing": "Pārtraukt nākamo noteikumu apstrādi",
"include_spam": "Pārvietot arī ziņas, kas atzīmētas kā mēstules",
"attachment_type_placeholder": "piem. pdf, doc, jpg",
"value_placeholder_multi": "Vērtība (vairākas atdalītas ar komatu)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Izvēlēties mapi",
"forward_to": "Pārsūtīt uz e-pastu",
"forward_placeholder": "lietotajs@piemers.lv",
"forward_keep_copy": "Saglabāt kopiju",
"forward_limit": "Pārsūtīšanas ierobežojums vienai ziņai šajā serverī: {count}. Papildu pārsūtīšanas tiek izlaistas.",
"reject_message": "Noraidīšanas ziņojums",
"reject_placeholder": "Jūsu e-pasts tika noraidīts",
"label_name": "Taga nosaukums",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Jauna apakšmape...",
"new_folder": "Jauna mape...",
"rename": "Pārsaukt...",
"share": "Kopīgot...",
"import_email": "Importēt .eml vai .zip...",
"empty_folder": "Iztukšot mapi",
"empty_folder_generic": "Iztukšot mapi",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Neizdevās dzēst mapi",
"toast_error_delete_has_children": "Mapei ir apakšmapes. Vispirms noņemiet tās.",
"toast_error_delete_has_email": "Mape nav tukša. Vispirms iztukšojiet to.",
"delete_with_contents_title": "Mape nav tukša",
"delete_with_contents_message": "Mapē “{name}” joprojām ir {count, plural, one {1 ziņa} other {# ziņas}}. Dzēst mapi un visas tās ziņas? To nevar atsaukt.",
"delete_with_contents_confirm": "Dzēst mapi un ziņas",
"placeholder_folder_name": "Mapes nosaukums",
"create": "Izveidot",
"rename_confirm": "Pārsaukt"
@@ -2270,7 +2316,8 @@
"search": "Iet uz meklēšanu",
"help": "Rādīt īsinājumtaustiņus",
"refresh": "Atsvaidzināt vēstules",
"select_all": "Atlasīt visas"
"select_all": "Atlasīt visas",
"global_search": "Meklēt visā"
},
"threads": {
"expand_collapse": "Izvērst/sairt sarunu"
@@ -2721,7 +2768,10 @@
"any_month": "Jebkurš mēnesis",
"has_email": "Ar e-pastu",
"has_phone": "Ar tālruni",
"has_photo": "Ar foto"
"has_photo": "Ar foto",
"sort_by": "Kārtot pēc",
"sort_first_name": "Vārds",
"sort_last_name": "Uzvārds"
},
"open_categories": "Atvērt kategorijas"
},
@@ -2757,6 +2807,10 @@
"duplicate": "Dublēt",
"today_header": "Šodien",
"tomorrow_header": "Rīt",
"agenda_show_earlier": "Rādīt agrākos notikumus",
"agenda_loading": "Ielādē…",
"agenda_range_start": "Notikumi no {date}",
"agenda_range_end": "Notikumi līdz {date}",
"export_ics": "Eksportēt kā .ics",
"copy_title": "Kopēt nosaukumu",
"copy_link": "Kopēt sapulces saiti",
@@ -2792,6 +2846,14 @@
"color": "Krāsa"
},
"participants": {
"availability": {
"title": "Pieejamība",
"free": "Pieejams",
"busy": "Aizņemts",
"tentative": "Provizoriski",
"unknown": "Nav šī servera lietotājs",
"checking": "Pārbauda…"
},
"title": "Dalībnieki",
"add": "Pievienot dalībnieku",
"organizer": "Organizators",
@@ -2866,7 +2928,10 @@
"all_events": "Visus pasākumus",
"cancel": "Atcelt",
"save": "Saglabāt",
"delete": "Dzēst"
"delete": "Dzēst",
"rsvp_title": "Atbildēt uz atkārtotu pasākumu",
"rsvp_description": "Šis ir atkārtots pasākums. Uz kurām instancēm attiecas jūsu atbilde?",
"respond": "Atbildēt"
},
"alerts": {
"title": "Atgādinājums",
@@ -2904,6 +2969,8 @@
"show_time_in_month_view_desc": "Rādīt pasākuma laiku mēneša kalendārā",
"show_week_numbers": "Rādīt nedēļu numurus",
"show_week_numbers_desc": "Rādīt nedēļu numurus mini kalendārā",
"calendar_free_scroll": "Brīva ritināšana",
"calendar_free_scroll_desc": "Nepārtraukti ritināt cauri mēnešiem, nedēļām un dienām, nevis pa vienam periodam",
"enable_tasks": "Iespējot uzdevumus",
"enable_tasks_desc": "Rādīt uzdevumu sadaļu kalendārā",
"show_tasks_on_calendar": "Rādīt uzdevumus kalendārā",
@@ -2915,6 +2982,9 @@
"hover_preview_delay_1s": "1 sekundes aizture",
"hover_preview_delay_2s": "2 sekunžu aizture",
"hover_preview_off": "Izslēgts",
"organizer_identity": "Organizēt ielūgumus kā",
"organizer_identity_desc": "Kalendāra adrese, ko izmanto kā organizatoru, uzaicinot cilvēkus uz notikumiem.",
"organizer_identity_failed": "Neizdevās mainīt organizatora adresi",
"show_birthday_calendar": "Kontaktu dzimšanas dienu kalendārs",
"show_birthday_calendar_desc": "Rādīt virtuālu kalendāru ar kontaktu dzimšanas dienām"
},
@@ -2946,6 +3016,9 @@
"alert_now": "Sākas tagad",
"alert_in_minutes": "Pēc {count} min",
"invitation_sent": "Uzaicinājumi nosūtīti",
"invitations_denied_title": "Ielūgumus nevar nosūtīt",
"invitations_denied": "Serveris atteicās nosūtīt ielūgumus: {reason}. Saglabāt notikumu, tos nenosūtot?",
"save_without_invitations": "Saglabāt bez ielūgumiem",
"rsvp_updated": "Atbilde atjaunināta",
"rsvp_error": "Neizdevās atjaunināt atbildi",
"event_duplicated": "Pasākums dublēts",
@@ -2974,7 +3047,7 @@
"supported_formats": "Atbalsta iCalendar (.ics) failus",
"url_description": "Ievadiet ārējas iCalendar plūsmas URL.",
"url_placeholder": "https://piemers.lv/kalendars.ics",
"url_hint": "Atbalsta CalDAV un iCalendar (.ics) saites",
"url_hint": "Atbalsta iCalendar (.ics) saites",
"fetch": "Iegūt",
"invalid_url": "Ievadiet derīgu URL",
"url_fetch_failed": "Neizdevās iegūt kalendāru no saites",
@@ -3164,6 +3237,8 @@
"has_attachment": "Ar pielikumu",
"date_after": "Pēc",
"date_before": "Pirms",
"size_min": "Lielāks par (KB)",
"size_max": "Mazāks par (KB)",
"starred": "Ar zvaigznīti",
"unread": "Nelasītas",
"read": "Izlasītas",
@@ -3205,6 +3280,10 @@
"hint": "Noklikšķiniet uz jebkuru vēstuli kreisajā pusē, lai sāktu."
},
"files": {
"office_edit": "Rediģēt dokumentu",
"office_loading": "Atver dokumentu redaktoru…",
"office_error": "Neizdevās atvērt dokumentu redaktoru.",
"office_close": "Aizvērt redaktoru",
"open_in_new_tab": "Atvērt jaunā cilnē",
"title": "Faili",
"search_placeholder": "Meklēt failus...",
@@ -3217,6 +3296,8 @@
"rename": "Pārdēvēt",
"rename_title": "Pārdēvēt",
"new_name": "Jauns nosaukums",
"name_forbidden_chars": "Serveris neatļauj šīs rakstzīmes nosaukumos: {chars}",
"name_reserved": "Šis nosaukums serverī ir rezervēts.",
"delete": "Dzēst",
"delete_confirm_title": "Dzēst resursu",
"delete_confirm_message": "Vai tiešām vēlaties dzēst «{name}»? Šo darbību nevar atcelt.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "Meklēšana nav pieejama apvienotajā skatā"
},
"calendar_event_notifications": {
"someone": "Kāds",
"invited": "{name} uzaicināja jūs uz “{title}”",
"updated": "{name} atjaunināja “{title}”",
"cancelled": "{name} atcēla “{title}”",
"untitled": "Notikums bez nosaukuma"
},
"share_notifications": {
"someone": "Kāds",
"shared": "{name} kopīgoja ar jums {kind} “{object}”",
"changed": "{name} mainīja jūsu piekļuvi {kind} “{object}”",
"revoked": "{name} noņēma jūsu piekļuvi {kind} “{object}”",
"object": {
"folder": "mapi",
"calendar": "kalendāru",
"address_book": "adrešu grāmatu",
"files": "failu mapi"
}
},
"sharing": {
"title": "Kopīgot \"{name}\"",
"description": "Piešķiriet piekļuvi citiem lietotājiem vai grupām šajā serverī. Izmaiņas stājas spēkā nekavējoties.",
@@ -3501,5 +3601,45 @@
"copy_event": "Kopēt saiti uz notikumu",
"copied": "Saite nokopēta",
"copy_failed": "Neizdevās nokopēt saiti"
},
"plugins": {
"oauth_callback": {
"completing": "Pabeidz autorizāciju…",
"completing_message": "Lūdzu, uzgaidiet, kamēr tiek izveidots savienojums.",
"received_title": "Autorizācija saņemta",
"received_message": "Varat atgriezties cilnē, kurā sākāt savienojumu.",
"failed_title": "Autorizācija neizdevās",
"denied": "Autorizācija tika atteikta.",
"failed_detail": "Autorizācija neizdevās: {error}",
"missing_params": "Trūkst autorizācijas kods vai stāvoklis.",
"delivery_failed": "Autorizācijas rezultātu neizdevās nodot lietotnes cilnei. Aizveriet šo logu un mēģiniet vēlreiz."
}
},
"global_search": {
"title": "Meklēt visā",
"placeholder": "Meklēt pastā, kontaktos, kalendārā, failos...",
"scope_label": "Meklēt",
"account_label": "Konts",
"scope_all": "Viss",
"scope_mail": "Pasts",
"scope_contacts": "Kontakti",
"scope_calendar": "Kalendārs",
"scope_files": "Faili",
"all_accounts": "Visi konti",
"show_all": "Rādīt visus rezultātus",
"no_results": "Nav rezultātu vaicājumam “{query}”",
"server_hint": "Pilni vārdi meklē serverī; vārdu daļas atbilst jau ielādētajiem vienumiem",
"searching": "Meklē...",
"account_error": "Neizdevās meklēt kontā {account}",
"recent_searches": "Pēdējie meklējumi",
"recurring": "Period isks",
"no_subject": "(no subject)",
"empty_hint": "Meklējiet pastu, kontaktus, notikumus un failus visos kontos. Izmēģiniet from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Plakani pēc datuma",
"grouped": "Grupēts",
"load_more": "Ielādēt vairāk",
"preview_empty": "Izvēlieties rezultātu, lai to priekšskatītu",
"open_full": "Atvērt",
"view_label": "Skats"
}
}
+152 -12
View File
@@ -225,6 +225,10 @@
"and_others": "болон {count} бусад",
"draft": "Ноорог",
"starred": "Одтой",
"pinned": "Бэхэлсэн",
"replied": "Хариулсан",
"forwarded": "Дамжуулсан",
"has_attachment": "Хавсралттай",
"conversations_count": "{total} харилцан ярианы {count}",
"conversations_count_plus": "{count}+ харилцан яриа",
"conversations_count_simple": "{count} харилцан яриа",
@@ -292,6 +296,7 @@
"forward": "Урагшаа",
"delete": "Устгах",
"archive": "Архив",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "од",
"unstar": "Одгүй болгох",
"mark_unread": "Уншаагүй гэж тэмдэглэх",
@@ -303,9 +308,7 @@
"move": "Хөдөлгөөн",
"print": "Хэвлэх",
"view_source": "Эх сурвалжийг харах",
"fullscreen": "Бүтэн дэлгэц",
"exit_fullscreen": "Бүтэн дэлгэцээс гарах",
"export_email": ".eml",
"forward_as_attachment": "Хавсралт болгон дамжуулах",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Уншсан баримт хүссэн (идэвхгүй болгохын тулд дарна уу)",
"read_receipt_off": "Уншсан баримт хүсэх",
"dsn_on": "Хүргэлтийн мэдэгдэл хүссэн (идэвхгүй болгох бол дарна уу)",
"dsn_off": "Хүргэлтийн мэдэгдэл хүсэх",
"format_plain_text": "Энгийн текст рүү шилжих",
"format_rich_text": "Форматтай текст (HTML) рүү шилжих",
"require_tls_on": "Шифрлэгдсэн хүргэлт шаардлагатай (идэвхгүй болгох бол дарна уу)",
"require_tls_off": "Шифрлэгдсэн хүргэлт шаардах (TLS)",
"new_message": "Шинэ мессеж",
"signature_edit_hint": "Гарын үсгийг засахын тулд давхар товшино уу",
"reply": "Хариулах",
@@ -634,11 +643,12 @@
"from_override": {
"toggle_off": "Дараах",
"toggle_on": "Дараахыг цуцлах",
"toggle_tooltip": "From нэр хаягийг чөлөөтэй засварлана. Таны хувийн мэдээллээр шуудан илгээгдсэн хэвээр байна - зөвхөн харагдах \"From\" гарчиг өөрчлөгдөнө.",
"toggle_tooltip": "From нэр хаягийг чөлөөтэй засварлана. Таны хувийн мэдээллээр шуудан илгээгдсэн хэвээр байна.",
"name_label": "Нэрнээс",
"name_placeholder": "Нэр",
"email_label": "Имэйл хаягаас",
"email_placeholder": "user@example.com"
"email_placeholder": "user@example.com",
"envelope_notice": "Хэрэв таны сервер энэ хаягийг дугтуйн илгээгчээр хүлээн авахгүй бол оронд нь {identity} ашиглагдах бөгөөд хүлээн авагчид үүнийг Return-Path гарчигт харж болно."
},
"use_template": "Загвар",
"save_as_template": "Загвар болгон хадгалах",
@@ -651,6 +661,8 @@
"upload_progress": "{uploaded} / {total}",
"upload_cancel": "Байршуулалтыг цуцлах",
"upload_failed": "{filename}",
"attachment_too_large": "\"{name}\" сервер зөвшөөрөхөөс том байна (нэг файлд {max})",
"attachments_total_too_large": "Хавсралтууд серверийн нэг захидалд ногдох {max} хязгаараас хэтэрнэ",
"drop_files": "Файлуудыг хавсаргах",
"show_less": "Бага харуулах",
"send_failed": "Имэйл илгээж чадсангүй",
@@ -726,7 +738,11 @@
"delete_column": "Баганыг устгах",
"toggle_header_row": "Толгойн мөрийг сэлгэх",
"delete_table": "Хүснэгт устгах",
"pick_size": "Хэмжээ сонгох"
"pick_size": "Хэмжээ сонгох",
"font_size": "Фонтын хэмжээ",
"font_size_default": "Үндсэн",
"background_color": "Дэвсгэр өнгө",
"remove_background_color": "Дэвсгэр өнгийг арилгах"
},
"send_filing_warning": "Илгээсэн - гэхдээ илгээсний дараах цэвэрлэгээ амжилтгүй болсон тул хуучирсан ноорог үлдэж болзошгүй."
},
@@ -765,6 +781,10 @@
"email_sent": "Имэйл амжилттай илгээгдсэн",
"email_deleted": "Имэйл устгасан",
"email_archived": "Имэйл архивлагдсан",
"emails_deleted": "{count, plural, other {# имэйл устгагдлаа}}",
"emails_archived": "{count, plural, other {# имэйл архивлагдлаа}}",
"error_archiving": "Имэйлийг архивлаж чадсангүй",
"error_updating": "Имэйлийг шинэчилж чадсангүй",
"email_starred": "И-мэйл одоор тэмдэглэгдсэн",
"email_unstarred": "И-мэйл одыг арилгасан",
"email_marked_read": "И-мэйл уншсан гэж тэмдэглэсэн",
@@ -1144,6 +1164,7 @@
"status_active": "Энэ төхөөрөмжид идэвхтэй байна",
"status_inactive": "Энэ төхөөрөмж дээр идэвхжээгүй",
"status_unsupported": "Энэ хөтөч Web Push-ийг дэмждэггүй",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "Ажиллаж байна…",
"enable": "Идэвхжүүлэх",
"reenable": "Дахин бүртгүүлэх",
@@ -1181,7 +1202,9 @@
"enabled": "И-мэйл мэдэгдэл",
"enabled_desc": "Шинэ имэйл ирэх үед мэдэгдлийг харуулах",
"sound": "Мэдэгдэлийн дуу",
"sound_desc": "Шинэ имэйл ирэх үед аудио дохиог тоглуулах"
"sound_desc": "Шинэ имэйл ирэх үед аудио дохиог тоглуулах",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Календарийн мэдэгдэл",
@@ -1241,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Хавтас солих үед хайлтыг арилгах",
"description": "Хавтас дээр дарахад (Ирсэн, Илгээсэн…) идэвхтэй хайлт арилж, тухайн хавтас нээгдэнэ. Унтраасан үед хайлт хадгалагдаж, шилжсэн хавтаст дахин ажиллана."
},
"request_read_receipt": {
"label": "Анхдагчаар уншсан баримтыг хүсэх",
"description": "Шинэ мессеж бичихдээ уншсан хүлээн авах хүсэлтийг урьдчилан идэвхжүүлнэ."
@@ -1324,6 +1351,12 @@
"label": "Хүлээн авсан хаягаас хариу бичих",
"description": "Хариу бичихдээ мессежийг анх илгээсэн хаягаар нь илгээнэ үү. Эхлээд таних тэмдэгтэй таарах; Домэйн бүх хүргэлтийн хувьд, таны үндсэн таниулбараар дамжуулан илгээхдээ From толгой хэсгийг өөр нэр рүү дахин бичнэ."
},
"reply_identity_match": {
"label": "Хүлээн авах хаягийн тохирол",
"description": "Хүлээн авах ямар хаягуудыг таных гэж үзэх. „Зөвхөн яг таарах хаяг“ нь зөвхөн таны тохируулсан identity-уудаас сонгоно; „ижил домэйн“ нь таны identity-ийн домэйн дээрх бусад хаягийг ч catch-all нэр болгон үзэж, Илгээгч толгойг тэр хаягаар солино. Эдгээр хаяг нь түгээлтийн жагсаалт бол яг таарах хаягийг сонгоно уу.",
"exact": "Зөвхөн яг таарах хаяг",
"domain": "Миний домэйн дээрх дурын хаяг"
},
"signature_position": {
"label": "Гарын үсэг зурах албан тушаал",
"description": "Хариулт болон дамжуулалтад гарын үсгээ хаана оруулах вэ. Иш татсан бичвэрийн дээрх хариултыг хаах нь ойлгомжтой; доор нь эх мессежийг залгаж хадгална.",
@@ -1753,7 +1786,9 @@
"warnings": {
"end_before_start": "Төгсгөлийн огноо нь эхлэх огнооноос хойш байх ёстой",
"start_in_past": "Эхлэх огноо өнгөрсөн байна",
"empty_body": "Зурвасны хэсэг хоосон байна - хүлээн авагчид хоосон хариу ирэх болно"
"empty_body": "Зурвасны хэсэг хоосон байна - хүлээн авагчид хоосон хариу ирэх болно",
"subject_too_long": "Гарчиг сервертэй харьцуулахад хэт урт байна (дээд тал нь {max} байт).",
"body_too_long": "Захидал сервертэй харьцуулахад хэт урт байна (форматыг оруулаад дээд тал нь {max} байт)."
}
},
"folders": {
@@ -1770,6 +1805,8 @@
"role_trash": "Хогийн сав",
"role_junk": "Спам / Хог",
"role_archive": "Архив",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Байхгүй",
"create_folder": "Хавтас үүсгэх",
"create_subfolder": "Дэд хавтас үүсгэх",
@@ -1879,6 +1916,8 @@
"title": "Харилцах утас",
"description": "Харилцагчаа импортлох, экспортлох",
"group_by_letter_label": "Эхний үсгээр бүлэглэх",
"sort_by_last_name_label": "Овгоор эрэмбэлэх",
"sort_by_last_name_description": "Гэр бүлийн гишүүд хамт харагдахаар харилцах хүмүүсийн жагсаалтыг овгоор эрэмбэлэх",
"group_by_letter_description": "Харилцах хүмүүсийн жагсаалтад цагаан толгойн үсгийн дарааллаар хэсгийн толгойг харуулах",
"import_label": "Харилцагчдыг импортлох",
"import_description": "vCard (.vcf) файлаас харилцагчдыг импортлох",
@@ -1970,6 +2009,7 @@
"actions": "Үйлдэл",
"add_action": "Үйлдэл нэмэх",
"stop_processing": "Дараагийн дүрмийг боловсруулахаа зогсоох",
"include_spam": "Спам гэж тэмдэглэсэн захидлыг мөн зөөх",
"attachment_type_placeholder": "жишээ нь pdf, doc, jpg",
"value_placeholder_multi": "Утга (олон тоог таслалаар тусгаарласан)",
"condition_fields": {
@@ -2011,6 +2051,8 @@
"copy_to_folder": "Фолдер сонгох",
"forward_to": "Имэйл хаяг руу шилжүүлэх",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Хуулбар хадгалах",
"forward_limit": "Энэ сервер дээрх нэг захидлын дамжуулах хязгаар: {count}. Илүү дамжуулалтыг алгасна.",
"reject_message": "Татгалзсан мессеж",
"reject_placeholder": "Таны имэйл татгалзсан байна",
"label_name": "Тагийн нэр",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Шинэ дэд хавтас...",
"new_folder": "Шинэ хавтас...",
"rename": "Нэр өөрчлөх...",
"share": "Хуваалцах...",
"import_email": "Импорт .eml эсвэл .zip...",
"empty_folder": "Хоосон хавтас",
"empty_folder_generic": "Хоосон хавтас",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "Хавтсын нэрийг өөрчилж чадсангүй",
"toast_error_delete": "Хавтсыг устгаж чадсангүй",
"toast_error_delete_has_children": "Хавтас дэд хавтастай байна. Эхлээд тэдгээрийг устгана уу.",
"toast_error_delete_has_email": "Хавтас хоосон биш байна. Эхлээд үүнийг хоосло."
"toast_error_delete_has_email": "Хавтас хоосон биш байна. Эхлээд үүнийг хоосло.",
"delete_with_contents_title": "Хавтас хоосон биш байна",
"delete_with_contents_message": "\"{name}\" хавтсанд одоо ч {count} зурвас байна. Хавтас болон бүх зурвасыг устгах уу? Үүнийг буцаах боломжгүй.",
"delete_with_contents_confirm": "Хавтас болон зурвасыг устгах"
},
"shortcuts": {
"title": "Гарын товчлол",
@@ -2270,7 +2316,8 @@
"search": "Хайлт руу шилжих",
"help": "Товчлол харуулах",
"refresh": "Имэйл сэргээх",
"select_all": "Бүгдийг сонго"
"select_all": "Бүгдийг сонго",
"global_search": "Бүгдийг хайх"
},
"threads": {
"expand_collapse": "Хэлхээсийг дэлгэх/хумих"
@@ -2722,7 +2769,10 @@
"any_month": "Ямар ч сар",
"has_email": "Имэйл хаягтай",
"has_phone": "Утастай",
"has_photo": "Зурагтай"
"has_photo": "Зурагтай",
"sort_by": "Эрэмбэлэх",
"sort_first_name": "Нэр",
"sort_last_name": "Овог"
}
},
"calendar": {
@@ -2758,6 +2808,10 @@
"duplicate": "Давхардсан",
"today_header": "Өнөөдөр",
"tomorrow_header": "Маргааш",
"agenda_show_earlier": "Өмнөх үйл явдлуудыг харуулах",
"agenda_loading": "Ачаалж байна…",
"agenda_range_start": "{date}-с хойшхи үйл явдлууд",
"agenda_range_end": "{date} хүртэлх үйл явдлууд",
"export_ics": ".ics",
"copy_title": "Гарчиг хуулах",
"copy_link": "Уулзалтын холбоосыг хуулах",
@@ -2793,6 +2847,14 @@
"color": "Өнгө"
},
"participants": {
"availability": {
"title": "Боломжтой эсэх",
"free": "Боломжтой",
"busy": "Завгүй",
"tentative": "Урьдчилсан",
"unknown": "Энэ серверийн хэрэглэгч биш",
"checking": "Шалгаж байна…"
},
"title": "Оролцогчид",
"add": "Оролцогч нэмэх",
"organizer": "Зохион байгуулагч",
@@ -2867,7 +2929,10 @@
"all_events": "Бүх арга хэмжээ",
"cancel": "Цуцлах",
"save": "Хадгалах",
"delete": "Устгах"
"delete": "Устгах",
"rsvp_title": "Давтагдах үйл явдалд хариулах",
"rsvp_description": "Энэ бол давтагдах үйл явдал. Таны хариу аль үйл явдлуудад хамаарах вэ?",
"respond": "Хариулах"
},
"alerts": {
"title": "Сануулгууд",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Сарын хуанлийн харагдац дээр үйл явдлын цагийг харуулах. Жижиг дэлгэц дээр энэ нь цэгийн оронд үйл явдлын бүрэн оруулгуудыг харуулдаг.",
"show_week_numbers": "Долоо хоногийн дугаарыг харуулах",
"show_week_numbers_desc": "Ми-календарт долоо хоногийн тоог харуулах",
"calendar_free_scroll": "Чөлөөт гүйлгэлт",
"calendar_free_scroll_desc": "Нэг үеийг харуулахын оронд сар, долоо хоног, өдрүүдийг тасралтгүй гүйлгэх",
"enable_tasks": "Даалгавруудыг идэвхжүүлэх",
"enable_tasks_desc": "Хийх ажлуудыг удирдахын тулд календарьт ажлын харагдах байдлыг харуулах",
"show_tasks_on_calendar": "Хуанли дээр даалгавруудыг харуулах",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1 секундын саатал",
"hover_preview_delay_2s": "2 секундын саатал",
"hover_preview_off": "Хөгжлийн бэрхшээлтэй",
"organizer_identity": "Урилгыг зохион байгуулагч",
"organizer_identity_desc": "Хүмүүсийг арга хэмжээнд урихад зохион байгуулагчаар ашиглах хуанлийн хаяг.",
"organizer_identity_failed": "Зохион байгуулагчийн хаягийг өөрчилж чадсангүй",
"show_birthday_calendar": "Холбоо барих төрсөн өдрийн хуанли",
"show_birthday_calendar_desc": "Харилцагчдаас төрсөн өдөр бүхий виртуал хуанли харуулах"
},
@@ -2973,6 +3043,9 @@
"alert_now": "Одооноос эхлэн",
"alert_in_minutes": "{count} мин",
"invitation_sent": "Урилга илгээсэн",
"invitations_denied_title": "Урилга илгээх боломжгүй",
"invitations_denied": "Сервер урилга илгээхээс татгалзлаа: {reason}. Урилга илгээхгүйгээр үйл явдлыг хадгалах уу?",
"save_without_invitations": "Урилгагүйгээр хадгалах",
"rsvp_updated": "Хариулт шинэчлэгдсэн",
"rsvp_error": "Хариултыг шинэчилж чадсангүй",
"event_duplicated": "Үйл явдал давхардсан",
@@ -3002,7 +3075,7 @@
"supported_formats": "iCalendar (.ics) файлуудыг дэмждэг",
"url_description": "Арга хэмжээг импортлохын тулд гадаад iCalendar (.ics) хангамжийн URL хаягийг оруулна уу.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "CalDAV болон iCalendar (.ics) URL-г дэмждэг",
"url_hint": "iCalendar (.ics) URL-г дэмждэг",
"fetch": "Авах",
"invalid_url": "Хүчинтэй URL оруулна уу",
"url_fetch_failed": "URL",
@@ -3149,6 +3222,25 @@
"overdue": "Хугацаа хэтэрсэн"
}
},
"calendar_event_notifications": {
"someone": "Хэн нэгэн",
"invited": "{name} таныг \"{title}\"-д урилаа",
"updated": "{name} \"{title}\"-г шинэчиллээ",
"cancelled": "{name} \"{title}\"-г цуцаллаа",
"untitled": "Нэргүй арга хэмжээ"
},
"share_notifications": {
"someone": "Хэн нэгэн",
"shared": "{name} танд {kind} \"{object}\"-г хуваалцлаа",
"changed": "{name} таны {kind} \"{object}\"-д хандах эрхийг өөрчиллөө",
"revoked": "{name} таны {kind} \"{object}\"-д хандах эрхийг хаслаа",
"object": {
"folder": "хавтас",
"calendar": "хуанли",
"address_book": "хаягийн дэвтэр",
"files": "файлын хавтас"
}
},
"sharing": {
"title": "\"{name}\"-г хуваалцаарай",
"description": "Энэ сервер дээрх бусад хэрэглэгчид эсвэл бүлгүүдэд хандах эрх олгох. Өөрчлөлтүүд шууд хүчин төгөлдөр болно.",
@@ -3187,6 +3279,8 @@
"has_attachment": "Хавсралт",
"date_after": "Дараа",
"date_before": "Өмнө нь",
"size_min": "Ихээс (KB)",
"size_max": "Багаас (KB)",
"starred": "Одтой",
"unread": "Уншаагүй",
"read": "унших",
@@ -3228,6 +3322,10 @@
"hint": "Эхлэхийн тулд зүүн талд байгаа дурын имэйл дээр дарж эсвэл доорх аялалд хамрагдаарай."
},
"files": {
"office_edit": "Баримт засах",
"office_loading": "Баримтын засварлагчийг нээж байна…",
"office_error": "Баримтын засварлагчийг нээж чадсангүй.",
"office_close": "Засварлагчийг хаах",
"open_in_new_tab": "Шинэ табаар нээх",
"title": "Файл",
"search_placeholder": "Файл хайх...",
@@ -3240,6 +3338,8 @@
"rename": "Нэрээ өөрчлөх",
"rename_title": "Нэрээ өөрчлөх",
"new_name": "Шинэ нэр",
"name_forbidden_chars": "Сервер нэрэнд эдгээр тэмдэгтийг зөвшөөрдөггүй: {chars}",
"name_reserved": "Энэ нэр сервер дээр нөөцлөгдсөн.",
"delete": "Устгах",
"delete_confirm_title": "Нөөцийг устгах",
"delete_confirm_message": "Та \"{name}\"-г устгахдаа итгэлтэй байна уу? Үүнийг буцаах боломжгүй.",
@@ -3501,5 +3601,45 @@
"copy_event": "Арга хэмжээний холбоосыг хуулах",
"copied": "Холбоос хуулагдлаа",
"copy_failed": "Холбоосыг хуулж чадсангүй"
},
"plugins": {
"oauth_callback": {
"completing": "Зөвшөөрлийг дуусгаж байна…",
"completing_message": "Холболт үүсэх хүртэл түр хүлээнэ үү.",
"received_title": "Зөвшөөрөл хүлээн авсан",
"received_message": "Та холболт эхлүүлсэн таб руугаа буцаж болно.",
"failed_title": "Зөвшөөрөл амжилтгүй болсон",
"denied": "Зөвшөөрөл олгохыг татгалзсан.",
"failed_detail": "Зөвшөөрөл амжилтгүй болсон: {error}",
"missing_params": "Зөвшөөрлийн код эсвэл төлөв байхгүй байна.",
"delivery_failed": "Зөвшөөрлийн үр дүнг аппын таб руу дамжуулж чадсангүй. Энэ цонхыг хаагаад дахин оролдоно уу."
}
},
"global_search": {
"title": "Бүгдийг хайх",
"placeholder": "Шуудан, харилцагч, хуанли, файлаас хайх...",
"scope_label": "Хайх хүрээ",
"account_label": "Бүртгэл",
"scope_all": "Бүгд",
"scope_mail": "Мэйл",
"scope_contacts": "Харилцах утас",
"scope_calendar": "Хуанли",
"scope_files": "Файл",
"all_accounts": "Бүх бүртгэл",
"show_all": "Бүх үр дүнг харуулах",
"no_results": "“{query}”-д үр дүн олдсонгүй",
"server_hint": "Бүтэн үгс серверээс хайна; үгийн хэсэг ачаалагдсан зүйлстэй таарна",
"searching": "Хайж байна...",
"account_error": "{account}-аас хайж чадсангүй",
"recent_searches": "Сүүлийн хайлтууд",
"recurring": "Давтагдах",
"no_subject": "(no subject)",
"empty_hint": "Бүх бүртгэлийн шуудан, харилцагч, үйл явдал, файлаас хайгаарай. from:, subject:, has:attachment, after:2026-01-01 гэж туршиж үзээрэй.",
"flat_by_date": "Огноогоор жагсаах",
"grouped": "Бүлэглэсэн",
"load_more": "Цааш ачаалах",
"preview_empty": "Урьдчилан харах үр дүнгээ сонгоно уу",
"open_full": "Нээх",
"view_label": "Харагдац"
}
}
+152 -10
View File
@@ -225,6 +225,10 @@
"and_others": "og {count} andre",
"draft": "Utkast",
"starred": "Stjernemerket",
"pinned": "Festet",
"replied": "Besvart",
"forwarded": "Videresendt",
"has_attachment": "Har vedlegg",
"conversations_count": "{count} av {total} samtaler",
"conversations_count_plus": "{count}+ samtaler",
"conversations_count_simple": "{count} samtaler",
@@ -292,6 +296,7 @@
"forward": "Videresend",
"delete": "Slett",
"archive": "Arkiver",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Stjernemerk",
"unstar": "Fjern stjerne",
"mark_unread": "Merk som ulest",
@@ -578,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Ber om lesebekreftelse (klikk for å slå av)",
"read_receipt_off": "Be om lesebekreftelse",
"dsn_on": "Leveringsvarsel forespurt (klikk for å slå av)",
"dsn_off": "Be om leveringsvarsel",
"format_plain_text": "Bytt til ren tekst",
"format_rich_text": "Bytt til formatert tekst (HTML)",
"require_tls_on": "Kryptert levering kreves (klikk for å slå av)",
"require_tls_off": "Krev kryptert levering (TLS)",
"new_message": "Ny melding",
"signature_edit_hint": "Dobbeltklikk for å redigere signaturen",
"reply": "Svar",
@@ -632,11 +643,12 @@
"from_override": {
"toggle_off": "Endre avsender",
"toggle_on": "Avbryt endringen",
"toggle_tooltip": "Rediger Fra-navnet og -adressen fritt. E-posten sendes fortsatt gjennom identiteten din – bare den synlige Fra-overskriften endres.",
"toggle_tooltip": "Rediger Fra-navnet og -adressen fritt. E-posten sendes fortsatt gjennom identiteten din.",
"name_label": "Navn i Fra-feltet",
"name_placeholder": "Navn",
"email_label": "E-postadresse i Fra-feltet",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Hvis serveren din ikke godtar denne adressen som konvoluttavsender, brukes {identity} i stedet, og mottakerne kan se den i Return-Path-overskriften."
},
"use_template": "Mal",
"save_as_template": "Lagre som mal",
@@ -649,6 +661,8 @@
"upload_progress": "Laster opp {uploaded} / {total}",
"upload_cancel": "Avbryt opplasting",
"upload_failed": "Kunne ikke laste opp {filename}",
"attachment_too_large": "«{name}» er større enn serveren tillater ({max} per fil)",
"attachments_total_too_large": "Vedleggene ville overskride serverens grense på {max} per melding",
"drop_files": "Slipp filer for å legge ved",
"show_less": "Vis færre",
"send_failed": "Kunne ikke sende e-post",
@@ -724,7 +738,11 @@
"delete_column": "Slett kolonne",
"toggle_header_row": "Slå overskriftsraden av eller på",
"delete_table": "Slett tabell",
"pick_size": "Velg størrelse"
"pick_size": "Velg størrelse",
"font_size": "Skriftstørrelse",
"font_size_default": "Standard",
"background_color": "Bakgrunnsfarge",
"remove_background_color": "Fjern bakgrunnsfarge"
},
"send_filing_warning": "Meldingen ble sendt, men oppryddingen etterpå mislyktes. Et gammelt utkast kan bli liggende."
},
@@ -763,6 +781,10 @@
"email_sent": "E-posten er sendt",
"email_deleted": "E-posten er slettet",
"email_archived": "E-posten er arkivert",
"emails_deleted": "{count, plural, one {# e-post slettet} other {# e-poster slettet}}",
"emails_archived": "{count, plural, one {# e-post arkivert} other {# e-poster arkivert}}",
"error_archiving": "Kunne ikke arkivere e-posten",
"error_updating": "Kunne ikke oppdatere e-posten",
"email_starred": "E-posten er stjernemerket",
"email_unstarred": "E-posten er ikke lenger stjernemerket",
"email_marked_read": "E-posten er merket som lest",
@@ -1142,6 +1164,7 @@
"status_active": "Aktiv på denne enheten",
"status_inactive": "Ikke aktivert på denne enheten",
"status_unsupported": "Denne nettleseren støtter ikke Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "Behandler...",
"enable": "Aktiver",
"reenable": "Registrer på nytt",
@@ -1179,7 +1202,9 @@
"enabled": "E-postvarsler",
"enabled_desc": "Vis varsler når nye e-poster kommer",
"sound": "Varslingslyd",
"sound_desc": "Spill av et lydvarsel når nye e-poster kommer"
"sound_desc": "Spill av et lydvarsel når nye e-poster kommer",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Kalendervarsler",
@@ -1239,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Tøm søket ved bytte av mappe",
"description": "Å klikke på en mappe (Innboks, Sendt …) tømmer det aktive søket og viser mappen. Når det er av, beholdes søket og kjøres på nytt i mappen du bytter til."
},
"request_read_receipt": {
"label": "Be om lesebekreftelser som standard",
"description": "Be automatisk om lesebekreftelse når du skriver en ny melding."
@@ -1322,6 +1351,12 @@
"label": "Svar fra mottatt adresse",
"description": "Når du svarer, bruker Bulwark adressen meldingen opprinnelig ble sendt til. Programmet ser først etter en samsvarende identitet. For meldinger til et oppsamlingsdomene brukes aliaset i Fra-feltet, mens sendingen går gjennom hovedidentiteten din."
},
"reply_identity_match": {
"label": "Samsvar for mottaksadresse",
"description": "Hvilke mottaksadresser som regnes som dine. «Kun nøyaktig adresse» velger bare en av dine oppsatte identiteter; «samme domene» behandler i tillegg enhver annen adresse på identitetsdomenene dine som et catch-all-alias og skriver om Fra-headeren til den. Velg nøyaktig adresse hvis disse adressene er distribusjonslister.",
"exact": "Kun nøyaktig adresse",
"domain": "Enhver adresse på mine domener"
},
"signature_position": {
"label": "Signaturposisjon",
"description": "Velg hvor signaturen skal settes inn i svar og videresendinger. Over den siterte teksten fungerer den som en naturlig avslutning på svaret. Under den siterte teksten holdes den opprinnelige meldingen samlet.",
@@ -1751,7 +1786,9 @@
"warnings": {
"end_before_start": "Sluttdatoen må være etter startdatoen",
"start_in_past": "Startdatoen er i fortiden",
"empty_body": "Meldingsteksten er tom – mottakerne får et tomt svar"
"empty_body": "Meldingsteksten er tom – mottakerne får et tomt svar",
"subject_too_long": "Emnet er for langt for serveren (maks. {max} byte).",
"body_too_long": "Meldingen er for lang for serveren (maks. {max} byte inkludert formatering)."
}
},
"folders": {
@@ -1768,6 +1805,8 @@
"role_trash": "Papirkurv",
"role_junk": "Søppelpost",
"role_archive": "Arkiv",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Ingen",
"create_folder": "Opprett mappe",
"create_subfolder": "Opprett undermappe",
@@ -1877,6 +1916,8 @@
"title": "Kontakter",
"description": "Importer og eksporter kontaktene dine",
"group_by_letter_label": "Grupper etter første bokstav",
"sort_by_last_name_label": "Sorter etter etternavn",
"sort_by_last_name_description": "Sorter kontaktlisten etter etternavn slik at familiemedlemmer står samlet",
"group_by_letter_description": "Vis alfabetiske overskrifter i kontaktlisten",
"import_label": "Importer kontakter",
"import_description": "Importer kontakter fra en vCard-fil (.vcf).",
@@ -1968,6 +2009,7 @@
"actions": "Handlinger",
"add_action": "Legg til handling",
"stop_processing": "Ikke behandle flere regler",
"include_spam": "Flytt også meldinger som er merket som søppelpost",
"attachment_type_placeholder": "f.eks. pdf, doc, jpg",
"value_placeholder_multi": "Verdi (skill flere verdier med komma)",
"condition_fields": {
@@ -2009,6 +2051,8 @@
"copy_to_folder": "Velg mappe",
"forward_to": "Videresend til e-postadresse",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Behold en kopi",
"forward_limit": "Grense for videresendinger per melding på denne serveren: {count}. Flere videresendinger hoppes over.",
"reject_message": "Avvisningsmelding",
"reject_placeholder": "Meldingen din er avvist",
"label_name": "Etikettnavn",
@@ -2205,6 +2249,7 @@
"new_subfolder": "Ny undermappe...",
"new_folder": "Ny mappe...",
"rename": "Gi nytt navn...",
"share": "Del...",
"import_email": "Importer .eml eller .zip...",
"empty_folder": "Tøm mappe",
"empty_folder_generic": "Tøm mappe",
@@ -2234,7 +2279,10 @@
"toast_error_rename": "Kunne ikke gi nytt navn til mappen",
"toast_error_delete": "Kunne ikke slette mappen",
"toast_error_delete_has_children": "Mappen har undermapper. Fjern dem først.",
"toast_error_delete_has_email": "Mappen er ikke tom. Tøm den først."
"toast_error_delete_has_email": "Mappen er ikke tom. Tøm den først.",
"delete_with_contents_title": "Mappen er ikke tom",
"delete_with_contents_message": "Mappen «{name}» inneholder fortsatt {count, plural, one {1 melding} other {# meldinger}}. Slette mappen og alle meldingene i den? Dette kan ikke angres.",
"delete_with_contents_confirm": "Slett mappe og meldinger"
},
"shortcuts": {
"title": "Tastatursnarveier",
@@ -2268,7 +2316,8 @@
"search": "Aktiver søkefeltet",
"help": "Vis tastatursnarveier",
"refresh": "Oppdater e-poster",
"select_all": "Velg alle"
"select_all": "Velg alle",
"global_search": "Søk i alt"
},
"threads": {
"expand_collapse": "Utvid eller fold sammen tråden"
@@ -2720,7 +2769,10 @@
"any_month": "Alle måneder",
"has_email": "Har e-postadresse",
"has_phone": "Har telefonnummer",
"has_photo": "Har bilde"
"has_photo": "Har bilde",
"sort_by": "Sorter etter",
"sort_first_name": "Fornavn",
"sort_last_name": "Etternavn"
}
},
"calendar": {
@@ -2756,6 +2808,10 @@
"duplicate": "Dupliser",
"today_header": "I dag",
"tomorrow_header": "I morgen",
"agenda_show_earlier": "Vis tidligere hendelser",
"agenda_loading": "Laster…",
"agenda_range_start": "Hendelser fra {date}",
"agenda_range_end": "Hendelser til {date}",
"export_ics": "Eksporter som .ics",
"copy_title": "Kopier tittel",
"copy_link": "Kopier møtelenken",
@@ -2791,6 +2847,14 @@
"color": "Farge"
},
"participants": {
"availability": {
"title": "Tilgjengelighet",
"free": "Ledig",
"busy": "Opptatt",
"tentative": "Foreløpig",
"unknown": "Ikke en bruker på denne serveren",
"checking": "Sjekker…"
},
"title": "Deltakere",
"add": "Legg til deltaker",
"organizer": "Arrangør",
@@ -2865,7 +2929,10 @@
"all_events": "Alle hendelser",
"cancel": "Avbryt",
"save": "Lagre",
"delete": "Slett"
"delete": "Slett",
"rsvp_title": "Svar på gjentakende hendelse",
"rsvp_description": "Dette er en gjentakende hendelse. Hvilke hendelser gjelder svaret ditt for?",
"respond": "Svar"
},
"alerts": {
"title": "Påminnelser",
@@ -2903,6 +2970,8 @@
"show_time_in_month_view_desc": "Vis klokkeslettet for hendelser i månedsvisningen. På små skjermer vises hele hendelsesoppføringen i stedet for en prikk.",
"show_week_numbers": "Vis ukenummer",
"show_week_numbers_desc": "Vis ukenummer i minikalenderen",
"calendar_free_scroll": "Fri rulling",
"calendar_free_scroll_desc": "Rull fortløpende gjennom måneder, uker og dager i stedet for én periode om gangen",
"enable_tasks": "Aktiver oppgaver",
"enable_tasks_desc": "Vis en oppgavevisning i kalenderen for å administrere gjøremål",
"show_tasks_on_calendar": "Vis oppgaver i kalenderen",
@@ -2914,6 +2983,9 @@
"hover_preview_delay_1s": "Forsinkelse på 1 sekund",
"hover_preview_delay_2s": "Forsinkelse på 2 sekunder",
"hover_preview_off": "Deaktivert",
"organizer_identity": "Organiser invitasjoner som",
"organizer_identity_desc": "Kalenderadressen som brukes som arrangør når du inviterer folk til hendelser.",
"organizer_identity_failed": "Kunne ikke endre arrangøradressen",
"show_birthday_calendar": "Fødselsdager fra kontakter",
"show_birthday_calendar_desc": "Vis en virtuell kalender med bursdager fra kontaktene dine"
},
@@ -2971,6 +3043,9 @@
"alert_now": "Starter nå",
"alert_in_minutes": "Om {count} min",
"invitation_sent": "Invitasjonene er sendt",
"invitations_denied_title": "Invitasjonene kan ikke sendes",
"invitations_denied": "Serveren nektet å sende invitasjonene: {reason}. Lagre hendelsen uten å sende dem?",
"save_without_invitations": "Lagre uten invitasjoner",
"rsvp_updated": "Svaret er oppdatert",
"rsvp_error": "Kunne ikke oppdatere svaret",
"event_duplicated": "Hendelsen er duplisert",
@@ -3000,7 +3075,7 @@
"supported_formats": "Støtter iCalendar (.ics)-filer",
"url_description": "Skriv inn URL-en til en ekstern iCalendar-strøm (.ics) for å importere hendelser.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Støtter CalDAV- og iCalendar-URL-er (.ics)",
"url_hint": "Støtter iCalendar-URL-er (.ics)",
"fetch": "Hent",
"invalid_url": "Skriv inn en gyldig URL",
"url_fetch_failed": "Kunne ikke hente kalenderen fra nettadressen",
@@ -3147,6 +3222,25 @@
"overdue": "Forfalt"
}
},
"calendar_event_notifications": {
"someone": "Noen",
"invited": "{name} har invitert deg til «{title}»",
"updated": "{name} har oppdatert «{title}»",
"cancelled": "{name} har avlyst «{title}»",
"untitled": "Hendelse uten tittel"
},
"share_notifications": {
"someone": "Noen",
"shared": "{name} har delt {kind} «{object}» med deg",
"changed": "{name} har endret tilgangen din til {kind} «{object}»",
"revoked": "{name} har fjernet tilgangen din til {kind} «{object}»",
"object": {
"folder": "mappen",
"calendar": "kalenderen",
"address_book": "adresseboken",
"files": "filmappen"
}
},
"sharing": {
"title": "Del «{name}»",
"description": "Gi andre brukere eller grupper på denne serveren tilgang. Endringer trer i kraft umiddelbart.",
@@ -3185,6 +3279,8 @@
"has_attachment": "Vedlegg",
"date_after": "Etter",
"date_before": "Før",
"size_min": "Større enn (KB)",
"size_max": "Mindre enn (KB)",
"starred": "Stjernemerket",
"unread": "Ulest",
"read": "Lest",
@@ -3226,6 +3322,10 @@
"hint": "Klikk på en av e-postene til venstre for å komme i gang, eller start omvisningen nedenfor."
},
"files": {
"office_edit": "Rediger dokument",
"office_loading": "Åpner dokumentredigering…",
"office_error": "Dokumentredigeringen kunne ikke åpnes.",
"office_close": "Lukk redigering",
"open_in_new_tab": "Åpne i ny fane",
"title": "Filer",
"search_placeholder": "Søk etter filer...",
@@ -3238,6 +3338,8 @@
"rename": "Gi nytt navn",
"rename_title": "Gi nytt navn",
"new_name": "Nytt navn",
"name_forbidden_chars": "Serveren tillater ikke disse tegnene i navn: {chars}",
"name_reserved": "Dette navnet er reservert på serveren.",
"delete": "Slett",
"delete_confirm_title": "Slett fil eller mappe",
"delete_confirm_message": "Er du sikker på at du vil slette «{name}»? Dette kan ikke angres.",
@@ -3499,5 +3601,45 @@
"copy_event": "Kopier lenken til hendelsen",
"copied": "Lenken er kopiert",
"copy_failed": "Kunne ikke kopiere lenken"
},
"plugins": {
"oauth_callback": {
"completing": "Fullfører autorisering…",
"completing_message": "Vennligst vent mens tilkoblingen opprettes.",
"received_title": "Autorisering mottatt",
"received_message": "Du kan gå tilbake til fanen der du startet tilkoblingen.",
"failed_title": "Autorisering mislyktes",
"denied": "Autorisering ble avslått.",
"failed_detail": "Autorisering mislyktes: {error}",
"missing_params": "Mangler autorisasjonskode eller tilstand.",
"delivery_failed": "Autoriseringsresultatet kunne ikke leveres til app-fanen. Lukk dette vinduet og prøv igjen."
}
},
"global_search": {
"title": "Søk i alt",
"placeholder": "Søk i e-post, kontakter, kalender, filer...",
"scope_label": "Søk i",
"account_label": "Konto",
"scope_all": "Alt",
"scope_mail": "E-post",
"scope_contacts": "Kontakter",
"scope_calendar": "Kalender",
"scope_files": "Filer",
"all_accounts": "Alle kontoer",
"show_all": "Vis alle resultater",
"no_results": "Ingen resultater for “{query}”",
"server_hint": "Hele ord søker på serveren; deler av ord treffer allerede innlastede elementer",
"searching": "Søker...",
"account_error": "Kunne ikke søke i {account}",
"recent_searches": "Nylige søk",
"recurring": "Gjentakende",
"no_subject": "(no subject)",
"empty_hint": "Søk i e-post, kontakter, hendelser og filer på alle kontoer. Prøv from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Flatt etter dato",
"grouped": "Gruppert",
"load_more": "Last inn flere",
"preview_empty": "Velg et resultat for å forhåndsvise det",
"open_full": "Åpne",
"view_label": "Visning"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "en {count} anderen",
"draft": "Concept",
"starred": "Met ster",
"pinned": "Vastgezet",
"replied": "Beantwoord",
"forwarded": "Doorgestuurd",
"has_attachment": "Met bijlage",
"conversations_count": "{count} van {total} gesprekken",
"conversations_count_plus": "{count}+ gesprekken",
"conversations_count_simple": "{count} gesprekken",
@@ -292,6 +296,7 @@
"forward": "Doorsturen",
"delete": "Verwijderen",
"archive": "Archiveren",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Ster toevoegen",
"unstar": "Ster verwijderen",
"mark_unread": "Markeren als ongelezen",
@@ -303,12 +308,10 @@
"move": "Verplaatsen",
"print": "Afdrukken",
"view_source": "Bron bekijken",
"fullscreen": "Volledig scherm",
"exit_fullscreen": "Volledig scherm sluiten",
"export_email": "Exporteren als .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Doorsturen als bijlage",
"import_email": ".eml of .zip importeren",
"keyboard_shortcuts": "Sneltoetsen (?)",
"email_source": "E-mailbron",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Leesbevestiging aangevraagd (klik om uit te schakelen)",
"read_receipt_off": "Leesbevestiging aanvragen",
"dsn_on": "Bezorgmelding aangevraagd (klik om uit te schakelen)",
"dsn_off": "Bezorgmelding aanvragen",
"format_plain_text": "Overschakelen naar platte tekst",
"format_rich_text": "Overschakelen naar opgemaakte tekst (HTML)",
"require_tls_on": "Versleutelde bezorging vereist (klik om uit te schakelen)",
"require_tls_off": "Versleutelde bezorging vereisen (TLS)",
"new_message": "Nieuw bericht",
"signature_edit_hint": "Dubbelklik om de handtekening te bewerken",
"reply": "Beantwoorden",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Overschrijven",
"toggle_on": "Overschrijven annuleren",
"toggle_tooltip": "Bewerk de naam en het adres van de afzender vrij. E-mail wordt nog steeds via je identiteit verzonden - alleen de zichtbare Van-koptekst verandert.",
"toggle_tooltip": "Bewerk de naam en het adres van de afzender vrij. E-mail wordt nog steeds via je identiteit verzonden.",
"name_label": "Afzendernaam",
"name_placeholder": "Naam",
"email_label": "E-mailadres afzender",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Als je server dit adres niet accepteert als envelopafzender, wordt daar {identity} gebruikt, en ontvangers kunnen dat zien in de Return-Path-header."
},
"use_template": "Sjabloon",
"save_as_template": "Opslaan als sjabloon",
@@ -648,6 +658,8 @@
"upload_progress": "Uploaden {uploaded} / {total}",
"upload_cancel": "Upload annuleren",
"upload_failed": "Uploaden van {filename} mislukt",
"attachment_too_large": "\"{name}\" is groter dan de server toestaat ({max} per bestand)",
"attachments_total_too_large": "De bijlagen zouden de serverlimiet van {max} per bericht overschrijden",
"send_failed": "E-mail verzenden mislukt",
"smime_sign_on": "S/MIME-ondertekening ingeschakeld",
"smime_sign_off": "S/MIME-ondertekening uitgeschakeld",
@@ -726,7 +738,11 @@
"delete_column": "Kolom verwijderen",
"toggle_header_row": "Koprij aan/uit",
"delete_table": "Tabel verwijderen",
"pick_size": "Grootte kiezen"
"pick_size": "Grootte kiezen",
"font_size": "Lettergrootte",
"font_size_default": "Standaard",
"background_color": "Achtergrondkleur",
"remove_background_color": "Achtergrondkleur verwijderen"
},
"send_filing_warning": "Verzonden - maar het opruimen daarna is mislukt, mogelijk blijft een oud concept staan."
},
@@ -765,6 +781,10 @@
"email_sent": "E-mail succesvol verzonden",
"email_deleted": "E-mail verwijderd",
"email_archived": "E-mail gearchiveerd",
"emails_deleted": "{count, plural, one {# e-mail verwijderd} other {# e-mails verwijderd}}",
"emails_archived": "{count, plural, one {# e-mail gearchiveerd} other {# e-mails gearchiveerd}}",
"error_archiving": "E-mail kon niet worden gearchiveerd",
"error_updating": "E-mail kon niet worden bijgewerkt",
"email_starred": "Ster toegevoegd aan e-mail",
"email_unstarred": "Ster verwijderd van e-mail",
"email_marked_read": "E-mail gemarkeerd als gelezen",
@@ -1143,7 +1163,9 @@
"enabled": "E-mailmeldingen",
"enabled_desc": "Meldingen tonen wanneer nieuwe e-mails binnenkomen",
"sound": "Meldingsgeluid",
"sound_desc": "Een geluid afspelen wanneer nieuwe e-mails binnenkomen"
"sound_desc": "Een geluid afspelen wanneer nieuwe e-mails binnenkomen",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Agendameldingen",
@@ -1172,6 +1194,7 @@
"status_busy": "Bezig…",
"status_inactive": "Niet ingeschakeld op dit apparaat",
"status_unsupported": "Deze browser ondersteunt geen Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Achtergrondmeldingen",
"devices_title": "Geregistreerde apparaten",
"devices_desc": "Alle apparaten die achtergrondmeldingen voor dit account ontvangen. Trek je er een in, dan stopt de ontvangst meteen.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Zoekopdracht wissen bij wisselen van map",
"description": "Op een map klikken (Postvak IN, Verzonden…) wist de actieve zoekopdracht en toont die map. Uitgeschakeld blijft de zoekopdracht staan en wordt opnieuw uitgevoerd in de gekozen map."
},
"request_read_receipt": {
"label": "Standaard om leesbevestiging vragen",
"description": "De aanvraag voor een leesbevestiging vooraf inschakelen bij het opstellen van een nieuw bericht."
@@ -1316,6 +1343,12 @@
"label": "Antwoordadres automatisch selecteren",
"description": "Schakel bij het beantwoorden automatisch het Van-adres om naar de identiteit die het oorspronkelijke bericht ontving"
},
"reply_identity_match": {
"label": "Overeenkomst van ontvangstadres",
"description": "Welke ontvangstadressen als de uwe gelden. „Alleen exact adres“ kiest uitsluitend een van uw ingestelde identiteiten; „zelfde domein“ behandelt ook elk ander adres op uw identiteitsdomeinen als catch-all-alias en herschrijft de Van-header ernaar. Kies exact adres als die adressen distributielijsten zijn.",
"exact": "Alleen exact adres",
"domain": "Elk adres op mijn domeinen"
},
"signature_position": {
"label": "Positie van handtekening",
"description": "Waar je handtekening in antwoorden en doorgestuurde berichten moet worden ingevoegd. Boven de geciteerde tekst leest natuurlijk als afsluiting van het antwoord; eronder houdt het originele bericht aaneengesloten.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "De einddatum moet na de startdatum liggen",
"start_in_past": "De startdatum ligt in het verleden",
"empty_body": "De berichttekst is leeg - ontvangers krijgen een leeg antwoord"
"empty_body": "De berichttekst is leeg - ontvangers krijgen een leeg antwoord",
"subject_too_long": "Het onderwerp is te lang voor de server (maximaal {max} bytes).",
"body_too_long": "Het bericht is te lang voor de server (maximaal {max} bytes, inclusief opmaak)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Prullenbak",
"role_junk": "Spam / Ongewenst",
"role_archive": "Archief",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Geen",
"create_folder": "Map maken",
"create_subfolder": "Submap maken",
@@ -1882,7 +1919,9 @@
"categories_description": "Contactcategorieën hernoemen",
"no_categories": "Geen categorieën gevonden",
"group_by_letter_description": "Toon alfabetische sectiekoppen in de contactenlijst",
"group_by_letter_label": "Groeperen op eerste letter"
"group_by_letter_label": "Groeperen op eerste letter",
"sort_by_last_name_label": "Sorteren op achternaam",
"sort_by_last_name_description": "Sorteer de contactenlijst op achternaam zodat gezinsleden bij elkaar staan"
},
"downloads": {
"title": "Downloads",
@@ -1963,6 +2002,7 @@
"actions": "Acties",
"add_action": "Actie toevoegen",
"stop_processing": "Verwerking van volgende regels stoppen",
"include_spam": "Ook berichten verplaatsen die als spam zijn gemarkeerd",
"attachment_type_placeholder": "bijv. pdf, doc, jpg",
"value_placeholder_multi": "Waarde (meerdere met komma's gescheiden)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Map selecteren",
"forward_to": "Doorsturen naar e-mailadres",
"forward_placeholder": "email@voorbeeld.nl",
"forward_keep_copy": "Kopie bewaren",
"forward_limit": "Maximaal aantal doorsturingen per bericht op deze server: {count}. Extra doorsturingen worden overgeslagen.",
"reject_message": "Afwijzingsbericht",
"reject_placeholder": "Uw e-mail is afgewezen",
"label_name": "Labelnaam",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Nieuwe submap...",
"new_folder": "Nieuwe map...",
"rename": "Hernoemen...",
"share": "Delen...",
"import_email": ".eml of .zip importeren...",
"empty_folder": "Map leegmaken",
"empty_folder_generic": "Map leegmaken",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Map verwijderen mislukt",
"toast_error_delete_has_children": "Map bevat submappen. Verwijder deze eerst.",
"toast_error_delete_has_email": "Map is niet leeg. Maak deze eerst leeg.",
"delete_with_contents_title": "Map is niet leeg",
"delete_with_contents_message": "De map \"{name}\" bevat nog {count, plural, one {1 bericht} other {# berichten}}. De map en al haar berichten verwijderen? Dit kan niet ongedaan worden gemaakt.",
"delete_with_contents_confirm": "Map en berichten verwijderen",
"placeholder_folder_name": "Mapnaam",
"create": "Aanmaken",
"rename_confirm": "Hernoemen"
@@ -2270,7 +2316,8 @@
"search": "Focus op zoeken",
"help": "Sneltoetsen tonen",
"refresh": "E-mails vernieuwen",
"select_all": "Alles selecteren"
"select_all": "Alles selecteren",
"global_search": "Alles doorzoeken"
},
"threads": {
"expand_collapse": "Gesprek uitklappen/inklappen"
@@ -2721,7 +2768,10 @@
"any_month": "Elke maand",
"has_email": "Met e-mail",
"has_phone": "Met telefoon",
"has_photo": "Met foto"
"has_photo": "Met foto",
"sort_by": "Sorteren op",
"sort_first_name": "Voornaam",
"sort_last_name": "Achternaam"
},
"open_categories": "Categorieën openen"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Dupliceren",
"today_header": "Vandaag",
"tomorrow_header": "Morgen",
"agenda_show_earlier": "Eerdere afspraken tonen",
"agenda_loading": "Laden…",
"agenda_range_start": "Afspraken vanaf {date}",
"agenda_range_end": "Afspraken tot {date}",
"export_ics": "Exporteren als .ics",
"copy_title": "Titel kopiëren",
"copy_link": "Vergaderlink kopiëren",
@@ -2793,6 +2847,14 @@
"color": "Kleur"
},
"participants": {
"availability": {
"title": "Beschikbaarheid",
"free": "Beschikbaar",
"busy": "Bezet",
"tentative": "Voorlopig",
"unknown": "Geen gebruiker op deze server",
"checking": "Controleren…"
},
"title": "Deelnemers",
"add": "Deelnemer toevoegen",
"organizer": "Organisator",
@@ -2867,7 +2929,10 @@
"all_events": "Alle evenementen",
"cancel": "Annuleren",
"save": "Opslaan",
"delete": "Verwijderen"
"delete": "Verwijderen",
"rsvp_title": "Reageren op terugkerend evenement",
"rsvp_description": "Dit is een terugkerend evenement. Voor welke evenementen geldt uw reactie?",
"respond": "Reageren"
},
"alerts": {
"title": "Herinnering",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Evenementtijden weergeven in de maandkalenderweergave",
"show_week_numbers": "Weeknummers weergeven",
"show_week_numbers_desc": "Weeknummers weergeven in de minikalender",
"calendar_free_scroll": "Vrij scrollen",
"calendar_free_scroll_desc": "Doorlopend door maanden, weken en dagen scrollen in plaats van één periode tegelijk",
"enable_tasks": "Taken inschakelen",
"enable_tasks_desc": "Een takenweergave in de agenda tonen om to-do's te beheren",
"show_tasks_on_calendar": "Taken in agenda tonen",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1 seconde vertraging",
"hover_preview_delay_2s": "2 seconden vertraging",
"hover_preview_off": "Uitgeschakeld",
"organizer_identity": "Uitnodigingen organiseren als",
"organizer_identity_desc": "Het agenda-adres dat als organisator wordt gebruikt wanneer je mensen uitnodigt voor afspraken.",
"organizer_identity_failed": "Kon het organisatoradres niet wijzigen",
"show_birthday_calendar": "Verjaardagskalender",
"show_birthday_calendar_desc": "Toon een virtuele kalender met verjaardagen van je contacten"
},
@@ -2947,6 +3017,9 @@
"alert_now": "Begint nu",
"alert_in_minutes": "Over {count} min",
"invitation_sent": "Uitnodigingen verzonden",
"invitations_denied_title": "Uitnodigingen kunnen niet worden verzonden",
"invitations_denied": "De server weigerde de uitnodigingen te verzenden: {reason}. Het evenement opslaan zonder ze te verzenden?",
"save_without_invitations": "Opslaan zonder uitnodigingen",
"rsvp_updated": "Reactie bijgewerkt",
"rsvp_error": "Reactie kon niet worden bijgewerkt",
"event_duplicated": "Evenement gedupliceerd",
@@ -2975,7 +3048,7 @@
"supported_formats": "iCalendar (.ics) bestanden worden ondersteund",
"url_description": "Voer de URL in van een externe iCalendar (.ics) feed om evenementen te importeren.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Ondersteunt CalDAV en iCalendar (.ics) URLs",
"url_hint": "Ondersteunt iCalendar (.ics) URLs",
"fetch": "Ophalen",
"invalid_url": "Voer een geldige URL in",
"url_fetch_failed": "Kan agenda niet ophalen van URL",
@@ -3164,6 +3237,8 @@
"has_attachment": "Bijlagen",
"date_after": "Na",
"date_before": "Voor",
"size_min": "Groter dan (KB)",
"size_max": "Kleiner dan (KB)",
"starred": "Met ster",
"unread": "Ongelezen",
"read": "Gelezen",
@@ -3205,6 +3280,10 @@
"hint": "Klik links op een e-mail om te beginnen, of start de tour."
},
"files": {
"office_edit": "Document bewerken",
"office_loading": "Documenteditor wordt geopend…",
"office_error": "De documenteditor kon niet worden geopend.",
"office_close": "Editor sluiten",
"open_in_new_tab": "Openen in nieuw tabblad",
"title": "Bestanden",
"search_placeholder": "Bestanden zoeken...",
@@ -3217,6 +3296,8 @@
"rename": "Hernoemen",
"rename_title": "Hernoemen",
"new_name": "Nieuwe naam",
"name_forbidden_chars": "De server staat deze tekens niet toe in namen: {chars}",
"name_reserved": "Deze naam is gereserveerd op de server.",
"delete": "Verwijderen",
"delete_confirm_title": "Bron verwijderen",
"delete_confirm_message": "Weet u zeker dat u \"{name}\" wilt verwijderen? Dit kan niet ongedaan worden gemaakt.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "Zoeken is niet beschikbaar in de gecombineerde weergave"
},
"calendar_event_notifications": {
"someone": "Iemand",
"invited": "{name} heeft je uitgenodigd voor \"{title}\"",
"updated": "{name} heeft \"{title}\" bijgewerkt",
"cancelled": "{name} heeft \"{title}\" geannuleerd",
"untitled": "Afspraak zonder titel"
},
"share_notifications": {
"someone": "Iemand",
"shared": "{name} heeft {kind} \"{object}\" met je gedeeld",
"changed": "{name} heeft je toegang tot {kind} \"{object}\" gewijzigd",
"revoked": "{name} heeft je toegang tot {kind} \"{object}\" ingetrokken",
"object": {
"folder": "de map",
"calendar": "de agenda",
"address_book": "het adresboek",
"files": "de bestandsmap"
}
},
"sharing": {
"title": "\"{name}\" delen",
"description": "Geef andere gebruikers of groepen op deze server toegang. Wijzigingen zijn direct van kracht.",
@@ -3501,5 +3601,45 @@
"copy_event": "Link naar afspraak kopiëren",
"copied": "Link gekopieerd",
"copy_failed": "Kon de link niet kopiëren"
},
"plugins": {
"oauth_callback": {
"completing": "Autorisatie voltooien…",
"completing_message": "Even geduld terwijl de verbinding tot stand wordt gebracht.",
"received_title": "Autorisatie ontvangen",
"received_message": "Je kunt terugkeren naar het tabblad waar je de verbinding bent gestart.",
"failed_title": "Autorisatie mislukt",
"denied": "Autorisatie geweigerd.",
"failed_detail": "Autorisatie mislukt: {error}",
"missing_params": "Autorisatiecode of status ontbreekt.",
"delivery_failed": "Het autorisatieresultaat kon niet worden afgeleverd bij het app-tabblad. Sluit dit venster en probeer het opnieuw."
}
},
"global_search": {
"title": "Alles doorzoeken",
"placeholder": "Zoek in mail, contacten, agenda, bestanden...",
"scope_label": "Zoeken in",
"account_label": "Account",
"scope_all": "Alles",
"scope_mail": "E-mail",
"scope_contacts": "Contacten",
"scope_calendar": "Agenda",
"scope_files": "Bestanden",
"all_accounts": "Alle accounts",
"show_all": "Alle resultaten tonen",
"no_results": "Geen resultaten voor “{query}”",
"server_hint": "Volledige woorden doorzoeken de server; woorddelen vinden al geladen items",
"searching": "Zoeken...",
"account_error": "Kon niet zoeken in {account}",
"recent_searches": "Recente zoekopdrachten",
"recurring": "Terugkerend",
"no_subject": "(no subject)",
"empty_hint": "Doorzoek mail, contacten, afspraken en bestanden van alle accounts. Probeer from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Plat op datum",
"grouped": "Gegroepeerd",
"load_more": "Meer laden",
"preview_empty": "Selecteer een resultaat voor een voorbeeld",
"open_full": "Openen",
"view_label": "Weergave"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "i {count} innych",
"draft": "Szkic",
"starred": "Oznaczone gwiazdką",
"pinned": "Przypięte",
"replied": "Odpowiedziano",
"forwarded": "Przekazano",
"has_attachment": "Z załącznikiem",
"conversations_count": "{count} z {total} konwersacji",
"conversations_count_plus": "{count}+ konwersacji",
"conversations_count_simple": "{count} konwersacji",
@@ -292,6 +296,7 @@
"forward": "Przekaż dalej",
"delete": "Usuń",
"archive": "Archiwizuj",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Oznacz gwiazdką",
"unstar": "Usuń gwiazdkę",
"mark_unread": "Oznacz jako nieprzeczytane",
@@ -303,12 +308,10 @@
"move": "Przenieś",
"print": "Drukuj",
"view_source": "Pokaż źródło",
"fullscreen": "Pełny ekran",
"exit_fullscreen": "Zamknij pełny ekran",
"export_email": "Eksportuj jako .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Prześlij dalej jako załącznik",
"import_email": "Importuj .eml lub .zip",
"keyboard_shortcuts": "Skróty klawiszowe (?)",
"email_source": "Źródło wiadomości",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Zażądano potwierdzenia przeczytania (kliknij, aby wyłączyć)",
"read_receipt_off": "Zażądaj potwierdzenia przeczytania",
"dsn_on": "Zażądano powiadomienia o dostarczeniu (kliknij, aby wyłączyć)",
"dsn_off": "Zażądaj powiadomienia o dostarczeniu",
"format_plain_text": "Przełącz na zwykły tekst",
"format_rich_text": "Przełącz na tekst sformatowany (HTML)",
"require_tls_on": "Wymagane szyfrowane dostarczenie (kliknij, aby wyłączyć)",
"require_tls_off": "Wymagaj szyfrowanego dostarczenia (TLS)",
"new_message": "Nowa wiadomość",
"signature_edit_hint": "Kliknij dwukrotnie, aby edytować podpis",
"reply": "Odpowiedz",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Zastąp",
"toggle_on": "Anuluj zastąpienie",
"toggle_tooltip": "Swobodnie edytuj nazwę i adres nadawcy. Poczta jest nadal wysyłana przez twoją tożsamość - zmienia się tylko widoczny nagłówek Od.",
"toggle_tooltip": "Swobodnie edytuj nazwę i adres nadawcy. Poczta jest nadal wysyłana przez twoją tożsamość.",
"name_label": "Nazwa nadawcy",
"name_placeholder": "Nazwa",
"email_label": "Adres e-mail nadawcy",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Jeśli twój serwer nie akceptuje tego adresu jako nadawcy koperty, zostanie użyty {identity}, a odbiorcy zobaczą go w nagłówku Return-Path."
},
"use_template": "Szablon",
"save_as_template": "Zapisz jako szablon",
@@ -648,6 +658,8 @@
"upload_progress": "Przesyłanie {uploaded} / {total}",
"upload_cancel": "Anuluj przesyłanie",
"upload_failed": "Nie udało się przesłać {filename}",
"attachment_too_large": "„{name}” jest większy, niż pozwala serwer ({max} na plik)",
"attachments_total_too_large": "Załączniki przekroczyłyby limit serwera {max} na wiadomość",
"drop_files": "Upuść pliki, aby załączyć",
"show_less": "Pokaż mniej",
"send_failed": "Nie udało się wysłać wiadomości",
@@ -726,7 +738,11 @@
"delete_column": "Usuń kolumnę",
"toggle_header_row": "Przełącz wiersz nagłówka",
"delete_table": "Usuń tabelę",
"pick_size": "Wybierz rozmiar"
"pick_size": "Wybierz rozmiar",
"font_size": "Rozmiar czcionki",
"font_size_default": "Domyślny",
"background_color": "Kolor tła",
"remove_background_color": "Usuń kolor tła"
},
"send_filing_warning": "Wysłano - ale późniejsze porządkowanie nie powiodło się, może pozostać nieaktualna wersja robocza."
},
@@ -765,6 +781,10 @@
"email_sent": "Wiadomość wysłana pomyślnie",
"email_deleted": "Wiadomość usunięta",
"email_archived": "Wiadomość zarchiwizowana",
"emails_deleted": "{count, plural, one {# wiadomość usunięta} few {# wiadomości usunięte} many {# wiadomości usuniętych} other {# wiadomości usuniętych}}",
"emails_archived": "{count, plural, one {# wiadomość zarchiwizowana} few {# wiadomości zarchiwizowane} many {# wiadomości zarchiwizowanych} other {# wiadomości zarchiwizowanych}}",
"error_archiving": "Nie udało się zarchiwizować wiadomości",
"error_updating": "Nie udało się zaktualizować wiadomości",
"email_starred": "Wiadomość oznaczona gwiazdką",
"email_unstarred": "Gwiazdka usunięta",
"email_marked_read": "Wiadomość oznaczona jako przeczytana",
@@ -1143,7 +1163,9 @@
"enabled": "Powiadomienia e-mail",
"enabled_desc": "Pokazuj powiadomienia po nadejściu nowych wiadomości e-mail",
"sound": "Dźwięk powiadomienia",
"sound_desc": "Odtwarzaj sygnał dźwiękowy po nadejściu nowych wiadomości e-mail"
"sound_desc": "Odtwarzaj sygnał dźwiękowy po nadejściu nowych wiadomości e-mail",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Powiadomienia kalendarza",
@@ -1172,6 +1194,7 @@
"status_busy": "Przetwarzanie…",
"status_inactive": "Niewłączone na tym urządzeniu",
"status_unsupported": "Ta przeglądarka nie obsługuje Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Powiadomienia w tle",
"devices_title": "Zarejestrowane urządzenia",
"devices_desc": "Wszystkie urządzenia zarejestrowane do odbioru powiadomień w tle dla tego konta. Unieważnienie natychmiast wstrzymuje ich odbiór.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Czyść wyszukiwanie przy zmianie folderu",
"description": "Kliknięcie folderu (Odebrane, Wysłane…) czyści aktywne wyszukiwanie i otwiera ten folder. Gdy wyłączone, wyszukiwanie pozostaje i jest ponawiane w wybranym folderze."
},
"request_read_receipt": {
"label": "Domyślnie żądaj potwierdzeń przeczytania",
"description": "Włącz wcześniej żądanie potwierdzenia przeczytania podczas tworzenia nowej wiadomości."
@@ -1321,6 +1348,12 @@
"label": "Automatycznie wybieraj adres odpowiedzi",
"description": "Podczas odpowiadania automatycznie przełączaj adres nadawcy na tożsamość, która pierwotnie otrzymała wiadomość"
},
"reply_identity_match": {
"label": "Dopasowanie adresu odbioru",
"description": "Które adresy odbioru są uznawane za Twoje. „Tylko dokładny adres” wybiera wyłącznie jedną ze skonfigurowanych tożsamości; „ta sama domena” traktuje dodatkowo każdy inny adres w domenach Twoich tożsamości jako alias catch-all i przepisuje na niego nagłówek Od. Wybierz dokładny adres, jeśli te adresy to listy dystrybucyjne.",
"exact": "Tylko dokładny adres",
"domain": "Dowolny adres w moich domenach"
},
"signature_position": {
"label": "Pozycja podpisu",
"description": "Gdzie wstawić podpis w odpowiedziach i wiadomościach przekazanych dalej. Nad cytowanym tekstem brzmi naturalnie jako zakończenie odpowiedzi; pod nim zachowuje oryginalną wiadomość w całości.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "Data zakończenia musi być późniejsza niż data rozpoczęcia",
"start_in_past": "Data rozpoczęcia jest w przeszłości",
"empty_body": "Treść wiadomości jest pusta - odbiorcy otrzymają pustą odpowiedź"
"empty_body": "Treść wiadomości jest pusta - odbiorcy otrzymają pustą odpowiedź",
"subject_too_long": "Temat jest za długi dla serwera (maksymalnie {max} bajtów).",
"body_too_long": "Wiadomość jest za długa dla serwera (maksymalnie {max} bajtów wraz z formatowaniem)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Kosz",
"role_junk": "Spam / Niechciane",
"role_archive": "Archiwum",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Brak",
"create_folder": "Utwórz folder",
"create_subfolder": "Utwórz podfolder",
@@ -1882,7 +1919,9 @@
"categories_description": "Zmień nazwy kategorii kontaktów",
"no_categories": "Nie znaleziono kategorii",
"group_by_letter_description": "Pokaż alfabetyczne nagłówki sekcji na liście kontaktów",
"group_by_letter_label": "Grupuj według pierwszej litery"
"group_by_letter_label": "Grupuj według pierwszej litery",
"sort_by_last_name_label": "Sortuj według nazwiska",
"sort_by_last_name_description": "Uporządkuj listę kontaktów według nazwiska, aby członkowie rodziny byli obok siebie"
},
"downloads": {
"title": "Pobrane",
@@ -1963,6 +2002,7 @@
"actions": "Akcje",
"add_action": "Dodaj akcję",
"stop_processing": "Zatrzymaj przetwarzanie kolejnych reguł",
"include_spam": "Przenoś również wiadomości oznaczone jako spam",
"attachment_type_placeholder": "np. pdf, doc, jpg",
"value_placeholder_multi": "Wartość (kilka oddzielonych przecinkami)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Wybierz folder",
"forward_to": "Prześlij dalej na adres e-mail",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Zachowaj kopię",
"forward_limit": "Limit przekazań na wiadomość na tym serwerze: {count}. Dodatkowe przekazania są pomijane.",
"reject_message": "Wiadomość odrzucenia",
"reject_placeholder": "Twoja wiadomość e-mail została odrzucona",
"label_name": "Nazwa etykiety",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Nowy podfolder...",
"new_folder": "Nowy folder...",
"rename": "Zmień nazwę...",
"share": "Udostępnij...",
"import_email": "Importuj .eml lub .zip...",
"empty_folder": "Opróżnij folder",
"empty_folder_generic": "Opróżnij folder",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Nie udało się usunąć folderu",
"toast_error_delete_has_children": "Folder zawiera podfoldery. Najpierw je usuń.",
"toast_error_delete_has_email": "Folder nie jest pusty. Najpierw go opróżnij.",
"delete_with_contents_title": "Folder nie jest pusty",
"delete_with_contents_message": "Folder „{name}” nadal zawiera {count, plural, one {1 wiadomość} few {# wiadomości} other {# wiadomości}}. Usunąć folder wraz ze wszystkimi wiadomościami? Tej operacji nie można cofnąć.",
"delete_with_contents_confirm": "Usuń folder i wiadomości",
"placeholder_folder_name": "Nazwa folderu",
"create": "Utwórz",
"rename_confirm": "Zmień nazwę"
@@ -2270,7 +2316,8 @@
"search": "Przejdź do wyszukiwania",
"help": "Pokaż skróty",
"refresh": "Odśwież wiadomości",
"select_all": "Zaznacz wszystko"
"select_all": "Zaznacz wszystko",
"global_search": "Szukaj wszędzie"
},
"threads": {
"expand_collapse": "Rozwiń/zwiń wątek"
@@ -2721,7 +2768,10 @@
"any_month": "Dowolny miesiąc",
"has_email": "Z e-mailem",
"has_phone": "Z telefonem",
"has_photo": "Ze zdjęciem"
"has_photo": "Ze zdjęciem",
"sort_by": "Sortuj według",
"sort_first_name": "Imię",
"sort_last_name": "Nazwisko"
},
"open_categories": "Otwórz kategorie"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Duplikuj",
"today_header": "Dzisiaj",
"tomorrow_header": "Jutro",
"agenda_show_earlier": "Pokaż wcześniejsze wydarzenia",
"agenda_loading": "Ładowanie…",
"agenda_range_start": "Wydarzenia od {date}",
"agenda_range_end": "Wydarzenia do {date}",
"export_ics": "Eksportuj jako .ics",
"copy_title": "Kopiuj tytuł",
"copy_link": "Kopiuj link do spotkania",
@@ -2793,6 +2847,14 @@
"color": "Kolor"
},
"participants": {
"availability": {
"title": "Dostępność",
"free": "Dostępny",
"busy": "Zajęty",
"tentative": "Wstępnie",
"unknown": "Nie jest użytkownikiem tego serwera",
"checking": "Sprawdzanie…"
},
"title": "Uczestnicy",
"add": "Dodaj uczestnika",
"organizer": "Organizator",
@@ -2867,7 +2929,10 @@
"all_events": "Wszystkie wydarzenia",
"cancel": "Anuluj",
"save": "Zapisz",
"delete": "Usuń"
"delete": "Usuń",
"rsvp_title": "Odpowiedz na wydarzenie cykliczne",
"rsvp_description": "To jest wydarzenie cykliczne. Których wydarzeń dotyczy Twoja odpowiedź?",
"respond": "Odpowiedz"
},
"alerts": {
"title": "Przypomnienie",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Wyświetlaj godziny wydarzeń w miesięcznym widoku kalendarza",
"show_week_numbers": "Pokaż numery tygodni",
"show_week_numbers_desc": "Wyświetlaj numery tygodni w mini-kalendarzu",
"calendar_free_scroll": "Swobodne przewijanie",
"calendar_free_scroll_desc": "Przewijaj płynnie miesiące, tygodnie i dni zamiast jednego okresu naraz",
"enable_tasks": "Włącz zadania",
"enable_tasks_desc": "Pokaż widok zadań w kalendarzu do zarządzania zadaniami",
"show_tasks_on_calendar": "Pokaż zadania w kalendarzu",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Opóźnienie 1 sekundy",
"hover_preview_delay_2s": "Opóźnienie 2 sekund",
"hover_preview_off": "Wyłączono",
"organizer_identity": "Organizuj zaproszenia jako",
"organizer_identity_desc": "Adres kalendarza używany jako organizator, gdy zapraszasz osoby na wydarzenia.",
"organizer_identity_failed": "Nie udało się zmienić adresu organizatora",
"show_birthday_calendar": "Kalendarz urodzin kontaktów",
"show_birthday_calendar_desc": "Pokaż wirtualny kalendarz z urodzinami z kontaktów"
},
@@ -2947,6 +3017,9 @@
"alert_now": "Zaczyna się teraz",
"alert_in_minutes": "Za {count} min",
"invitation_sent": "Zaproszenia wysłano",
"invitations_denied_title": "Nie można wysłać zaproszeń",
"invitations_denied": "Serwer odmówił wysłania zaproszeń: {reason}. Zapisać wydarzenie bez ich wysyłania?",
"save_without_invitations": "Zapisz bez zaproszeń",
"rsvp_updated": "Odpowiedź zaktualizowano",
"rsvp_error": "Nie udało się zaktualizować odpowiedzi",
"event_duplicated": "Wydarzenie zduplikowano",
@@ -2975,7 +3048,7 @@
"supported_formats": "Obsługuje pliki iCalendar (.ics)",
"url_description": "Wprowadź adres URL zewnętrznego kanału iCalendar (.ics), aby zaimportować wydarzenia.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Obsługuje adresy URL CalDAV i iCalendar (.ics)",
"url_hint": "Obsługuje adresy URL iCalendar (.ics)",
"fetch": "Pobierz",
"invalid_url": "Wprowadź prawidłowy adres URL",
"url_fetch_failed": "Nie udało się pobrać kalendarza z adresu URL",
@@ -3164,6 +3237,8 @@
"has_attachment": "Załączniki",
"date_after": "Po",
"date_before": "Przed",
"size_min": "Większe niż (KB)",
"size_max": "Mniejsze niż (KB)",
"starred": "Z gwiazdką",
"unread": "Nieprzeczytane",
"read": "Przeczytane",
@@ -3205,6 +3280,10 @@
"hint": "Kliknij dowolną wiadomość po lewej, aby zacząć, albo skorzystaj z przewodnika poniżej."
},
"files": {
"office_edit": "Edytuj dokument",
"office_loading": "Otwieranie edytora dokumentów…",
"office_error": "Nie udało się otworzyć edytora dokumentów.",
"office_close": "Zamknij edytor",
"open_in_new_tab": "Otwórz w nowej karcie",
"title": "Pliki",
"search_placeholder": "Szukaj plików...",
@@ -3217,6 +3296,8 @@
"rename": "Zmień nazwę",
"rename_title": "Zmień nazwę",
"new_name": "Nowa nazwa",
"name_forbidden_chars": "Serwer nie zezwala na te znaki w nazwach: {chars}",
"name_reserved": "Ta nazwa jest zarezerwowana na serwerze.",
"delete": "Usuń",
"delete_confirm_title": "Usuń zasób",
"delete_confirm_message": "Czy na pewno chcesz usunąć \"{name}\"? Tej operacji nie można cofnąć.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "Wyszukiwanie jest niedostępne w widoku ujednoliconym"
},
"calendar_event_notifications": {
"someone": "Ktoś",
"invited": "{name} zaprosił(a) Cię na „{title}”",
"updated": "{name} zaktualizował(a) „{title}”",
"cancelled": "{name} odwołał(a) „{title}”",
"untitled": "Wydarzenie bez tytułu"
},
"share_notifications": {
"someone": "Ktoś",
"shared": "{name} udostępnił(a) Ci {kind} „{object}”",
"changed": "{name} zmienił(a) Twój dostęp do {kind} „{object}”",
"revoked": "{name} odebrał(a) Ci dostęp do {kind} „{object}”",
"object": {
"folder": "folder",
"calendar": "kalendarz",
"address_book": "książkę adresową",
"files": "folder plików"
}
},
"sharing": {
"title": "Udostępnij „{name}\"",
"description": "Udziel dostępu innym użytkownikom lub grupom na tym serwerze. Zmiany są natychmiastowe.",
@@ -3501,5 +3601,45 @@
"copy_event": "Kopiuj link do wydarzenia",
"copied": "Link skopiowany",
"copy_failed": "Nie udało się skopiować linku"
},
"plugins": {
"oauth_callback": {
"completing": "Kończenie autoryzacji…",
"completing_message": "Proszę czekać na nawiązanie połączenia.",
"received_title": "Otrzymano autoryzację",
"received_message": "Możesz wrócić do karty, w której rozpoczęto nawiązywanie połączenia.",
"failed_title": "Autoryzacja nie powiodła się",
"denied": "Autoryzacja została odrzucona.",
"failed_detail": "Autoryzacja nie powiodła się: {error}",
"missing_params": "Brak kodu autoryzacji lub stanu.",
"delivery_failed": "Nie udało się przekazać wyniku autoryzacji do karty aplikacji. Zamknij to okno i spróbuj ponownie."
}
},
"global_search": {
"title": "Szukaj wszędzie",
"placeholder": "Szukaj w poczcie, kontaktach, kalendarzu, plikach...",
"scope_label": "Szukaj w",
"account_label": "Konto",
"scope_all": "Wszystko",
"scope_mail": "Poczta",
"scope_contacts": "Kontakty",
"scope_calendar": "Kalendarz",
"scope_files": "Pliki",
"all_accounts": "Wszystkie konta",
"show_all": "Pokaż wszystkie wyniki",
"no_results": "Brak wyników dla „{query}”",
"server_hint": "Całe słowa przeszukują serwer; części słów pasują do już wczytanych elementów",
"searching": "Szukanie...",
"account_error": "Nie udało się przeszukać {account}",
"recent_searches": "Ostatnie wyszukiwania",
"recurring": "Cykliczne",
"no_subject": "(no subject)",
"empty_hint": "Szukaj poczty, kontaktów, wydarzeń i plików na wszystkich kontach. Wypróbuj from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Płasko według daty",
"grouped": "Pogrupowane",
"load_more": "Wczytaj więcej",
"preview_empty": "Wybierz wynik, aby zobaczyć podgląd",
"open_full": "Otwórz",
"view_label": "Widok"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "e {count} outros",
"draft": "Rascunho",
"starred": "Com Estrela",
"pinned": "Fixado",
"replied": "Respondido",
"forwarded": "Encaminhado",
"has_attachment": "Com anexo",
"conversations_count": "{count} de {total} conversas",
"conversations_count_plus": "{count}+ conversas",
"conversations_count_simple": "{count} conversas",
@@ -292,6 +296,7 @@
"forward": "Encaminhar",
"delete": "Excluir",
"archive": "Arquivar",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Adicionar Estrela",
"unstar": "Remover Estrela",
"mark_unread": "Marcar como não lido",
@@ -303,12 +308,10 @@
"move": "Mover",
"print": "Imprimir",
"view_source": "Ver código-fonte",
"fullscreen": "Tela cheia",
"exit_fullscreen": "Sair da tela cheia",
"export_email": "Exportar como .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Encaminhar como anexo",
"import_email": "Importar .eml ou .zip",
"keyboard_shortcuts": "Atalhos de teclado (?)",
"email_source": "Código-fonte do E-mail",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Confirmação de leitura solicitada (clique para desativar)",
"read_receipt_off": "Solicitar confirmação de leitura",
"dsn_on": "Notificação de entrega pedida (clique para desativar)",
"dsn_off": "Pedir notificação de entrega",
"format_plain_text": "Mudar para texto simples",
"format_rich_text": "Mudar para texto formatado (HTML)",
"require_tls_on": "Entrega cifrada obrigatória (clique para desativar)",
"require_tls_off": "Exigir entrega cifrada (TLS)",
"new_message": "Nova Mensagem",
"signature_edit_hint": "Clique duas vezes para editar a assinatura",
"reply": "Responder",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Substituir",
"toggle_on": "Cancelar substituição",
"toggle_tooltip": "Edite livremente o nome e o endereço do remetente. O email ainda é enviado através da sua identidade - apenas o cabeçalho De visível muda.",
"toggle_tooltip": "Edite livremente o nome e o endereço do remetente. O email ainda é enviado através da sua identidade.",
"name_label": "Nome do remetente",
"name_placeholder": "Nome",
"email_label": "Endereço de email do remetente",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Se o seu servidor não aceitar este endereço como remetente do envelope, {identity} é usado em seu lugar, e os destinatários podem vê-lo no cabeçalho Return-Path."
},
"use_template": "Modelo",
"save_as_template": "Salvar como modelo",
@@ -648,6 +658,8 @@
"upload_progress": "Enviando {uploaded} / {total}",
"upload_cancel": "Cancelar envio",
"upload_failed": "Falha ao enviar {filename}",
"attachment_too_large": "\"{name}\" é maior do que o servidor permite ({max} por ficheiro)",
"attachments_total_too_large": "Os anexos excederiam o limite do servidor de {max} por mensagem",
"send_failed": "Falha ao enviar o e-mail",
"smime_sign_on": "Assinatura S/MIME ativada",
"smime_sign_off": "Assinatura S/MIME desativada",
@@ -726,7 +738,11 @@
"delete_column": "Excluir coluna",
"toggle_header_row": "Alternar linha de cabeçalho",
"delete_table": "Excluir tabela",
"pick_size": "Escolher tamanho"
"pick_size": "Escolher tamanho",
"font_size": "Tamanho da fonte",
"font_size_default": "Padrão",
"background_color": "Cor de fundo",
"remove_background_color": "Remover cor de fundo"
},
"send_filing_warning": "Enviado - mas a limpeza posterior falhou, um rascunho antigo pode permanecer."
},
@@ -765,6 +781,10 @@
"email_sent": "E-mail enviado com sucesso",
"email_deleted": "E-mail excluído",
"email_archived": "E-mail arquivado",
"emails_deleted": "{count, plural, one {# e-mail excluído} other {# e-mails excluídos}}",
"emails_archived": "{count, plural, one {# e-mail arquivado} other {# e-mails arquivados}}",
"error_archiving": "Falha ao arquivar o e-mail",
"error_updating": "Falha ao atualizar o e-mail",
"email_starred": "E-mail marcado com estrela",
"email_unstarred": "Estrela removida do e-mail",
"email_marked_read": "E-mail marcado como lido",
@@ -1143,7 +1163,9 @@
"enabled": "Notificações de e-mail",
"enabled_desc": "Mostrar notificações quando novos e-mails chegarem",
"sound": "Som de notificação",
"sound_desc": "Reproduzir um som quando novos e-mails chegarem"
"sound_desc": "Reproduzir um som quando novos e-mails chegarem",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Notificações de calendário",
@@ -1172,6 +1194,7 @@
"status_busy": "Processando…",
"status_inactive": "Não ativado neste dispositivo",
"status_unsupported": "Este navegador não oferece suporte a Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Notificações em segundo plano",
"devices_title": "Dispositivos registrados",
"devices_desc": "Todos os dispositivos registrados para receber notificações em segundo plano desta conta. Revogar um deles interrompe o recebimento imediatamente.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Limpar a pesquisa ao mudar de pasta",
"description": "Clicar numa pasta (Caixa de entrada, Enviados…) limpa a pesquisa ativa e mostra essa pasta. Se desativado, a pesquisa mantém-se e volta a ser executada na pasta para a qual mudar."
},
"request_read_receipt": {
"label": "Solicitar confirmações de leitura por padrão",
"description": "Ativar previamente a solicitação de confirmação de leitura ao redigir uma nova mensagem."
@@ -1316,6 +1343,12 @@
"label": "Selecionar automaticamente o endereço de resposta",
"description": "Ao responder, muda automaticamente o endereço do remetente para a identidade que recebeu a mensagem original"
},
"reply_identity_match": {
"label": "Correspondência do endereço de receção",
"description": "Quais endereços de receção contam como seus. «Apenas endereço exato» escolhe somente uma das suas identidades configuradas; «mesmo domínio» trata também qualquer outro endereço dos seus domínios de identidade como alias catch-all e reescreve o cabeçalho De com ele. Escolha o endereço exato se esses endereços forem listas de distribuição.",
"exact": "Apenas endereço exato",
"domain": "Qualquer endereço dos meus domínios"
},
"signature_position": {
"label": "Posição da assinatura",
"description": "Onde inserir a sua assinatura em respostas e encaminhamentos. Acima do texto citado lê-se naturalmente como fecho da resposta; abaixo mantém a mensagem original contígua.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "A data de fim deve ser posterior à data de início",
"start_in_past": "A data de início está no passado",
"empty_body": "O corpo da mensagem está vazio - os destinatários receberão uma resposta em branco"
"empty_body": "O corpo da mensagem está vazio - os destinatários receberão uma resposta em branco",
"subject_too_long": "O assunto é demasiado longo para o servidor (no máximo {max} bytes).",
"body_too_long": "A mensagem é demasiado longa para o servidor (no máximo {max} bytes, incluindo a formatação)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Lixeira",
"role_junk": "Spam / Lixo eletrônico",
"role_archive": "Arquivo",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Nenhum",
"create_folder": "Criar pasta",
"create_subfolder": "Criar subpasta",
@@ -1882,7 +1919,9 @@
"categories_description": "Renomear categorias de contatos",
"no_categories": "Nenhuma categoria encontrada",
"group_by_letter_description": "Mostrar cabeçalhos de seção alfabéticos na lista de contatos",
"group_by_letter_label": "Agrupar pela primeira letra"
"group_by_letter_label": "Agrupar pela primeira letra",
"sort_by_last_name_label": "Ordenar por sobrenome",
"sort_by_last_name_description": "Ordenar a lista de contatos por sobrenome para que os membros da família apareçam juntos"
},
"downloads": {
"title": "Downloads",
@@ -1963,6 +2002,7 @@
"actions": "Ações",
"add_action": "Adicionar ação",
"stop_processing": "Parar o processamento das regras seguintes",
"include_spam": "Mover também as mensagens marcadas como spam",
"attachment_type_placeholder": "ex.: pdf, doc, jpg",
"value_placeholder_multi": "Valor (vários separados por vírgulas)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Selecionar pasta",
"forward_to": "Encaminhar para endereço de e-mail",
"forward_placeholder": "email@exemplo.com",
"forward_keep_copy": "Manter uma cópia",
"forward_limit": "Limite de reencaminhamentos por mensagem neste servidor: {count}. Os restantes são ignorados.",
"reject_message": "Mensagem de rejeição",
"reject_placeholder": "Seu e-mail foi rejeitado",
"label_name": "Nome da etiqueta",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Nova subpasta...",
"new_folder": "Nova pasta...",
"rename": "Renomear...",
"share": "Partilhar...",
"import_email": "Importar .eml ou .zip...",
"empty_folder": "Esvaziar pasta",
"empty_folder_generic": "Esvaziar pasta",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Falha ao excluir a pasta",
"toast_error_delete_has_children": "A pasta contém subpastas. Remova-as primeiro.",
"toast_error_delete_has_email": "A pasta não está vazia. Esvazie-a primeiro.",
"delete_with_contents_title": "A pasta não está vazia",
"delete_with_contents_message": "A pasta \"{name}\" ainda contém {count, plural, one {1 mensagem} other {# mensagens}}. Eliminar a pasta e todas as suas mensagens? Esta ação não pode ser anulada.",
"delete_with_contents_confirm": "Eliminar pasta e mensagens",
"placeholder_folder_name": "Nome da pasta",
"create": "Criar",
"rename_confirm": "Renomear"
@@ -2270,7 +2316,8 @@
"search": "Focar busca",
"help": "Mostrar atalhos",
"refresh": "Atualizar e-mails",
"select_all": "Selecionar todos"
"select_all": "Selecionar todos",
"global_search": "Pesquisar em tudo"
},
"threads": {
"expand_collapse": "Expandir/recolher conversa"
@@ -2721,7 +2768,10 @@
"any_month": "Qualquer mês",
"has_email": "Com e-mail",
"has_phone": "Com telefone",
"has_photo": "Com foto"
"has_photo": "Com foto",
"sort_by": "Ordenar por",
"sort_first_name": "Nome",
"sort_last_name": "Sobrenome"
},
"open_categories": "Abrir categorias"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Duplicar",
"today_header": "Hoje",
"tomorrow_header": "Amanhã",
"agenda_show_earlier": "Mostrar eventos anteriores",
"agenda_loading": "A carregar…",
"agenda_range_start": "Eventos a partir de {date}",
"agenda_range_end": "Eventos até {date}",
"export_ics": "Exportar como .ics",
"copy_title": "Copiar título",
"copy_link": "Copiar link da reunião",
@@ -2793,6 +2847,14 @@
"color": "Cor"
},
"participants": {
"availability": {
"title": "Disponibilidade",
"free": "Disponível",
"busy": "Ocupado",
"tentative": "Provisório",
"unknown": "Não é um utilizador deste servidor",
"checking": "A verificar…"
},
"title": "Participantes",
"add": "Adicionar participante",
"organizer": "Organizador",
@@ -2867,7 +2929,10 @@
"all_events": "Todos os eventos",
"cancel": "Cancelar",
"save": "Salvar",
"delete": "Excluir"
"delete": "Excluir",
"rsvp_title": "Responder a evento recorrente",
"rsvp_description": "Este é um evento recorrente. A quais eventos sua resposta se aplica?",
"respond": "Responder"
},
"alerts": {
"title": "Lembrete",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Exibir horários dos eventos na visualização mensal do calendário",
"show_week_numbers": "Mostrar números da semana",
"show_week_numbers_desc": "Exibir números da semana no minicalendário",
"calendar_free_scroll": "Deslocamento livre",
"calendar_free_scroll_desc": "Percorrer meses, semanas e dias de forma contínua em vez de um período de cada vez",
"enable_tasks": "Ativar tarefas",
"enable_tasks_desc": "Mostrar uma visualização de tarefas no calendário para gerenciar pendências",
"show_tasks_on_calendar": "Mostrar tarefas no calendário",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Atraso de 1 segundo",
"hover_preview_delay_2s": "Atraso de 2 segundos",
"hover_preview_off": "Desativado",
"organizer_identity": "Organizar convites como",
"organizer_identity_desc": "O endereço de calendário usado como organizador quando convida pessoas para eventos.",
"organizer_identity_failed": "Não foi possível alterar o endereço do organizador",
"show_birthday_calendar": "Calendário de aniversários",
"show_birthday_calendar_desc": "Mostrar um calendário virtual com os aniversários dos seus contatos"
},
@@ -2973,6 +3043,9 @@
"alert_now": "Começa agora",
"alert_in_minutes": "Em {count} min",
"invitation_sent": "Convites enviados",
"invitations_denied_title": "Não é possível enviar os convites",
"invitations_denied": "O servidor recusou enviar os convites: {reason}. Guardar o evento sem os enviar?",
"save_without_invitations": "Guardar sem convites",
"rsvp_updated": "Resposta atualizada",
"rsvp_error": "Falha ao atualizar resposta",
"event_duplicated": "Evento duplicado",
@@ -3001,7 +3074,7 @@
"supported_formats": "Arquivos iCalendar (.ics) suportados",
"url_description": "Insira a URL de um feed iCalendar (.ics) externo para importar eventos.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Suporta URLs CalDAV e iCalendar (.ics)",
"url_hint": "Suporta URLs iCalendar (.ics)",
"fetch": "Buscar",
"invalid_url": "Insira uma URL válida",
"url_fetch_failed": "Não foi possível buscar o calendário da URL",
@@ -3164,6 +3237,8 @@
"has_attachment": "Anexos",
"date_after": "Após",
"date_before": "Antes de",
"size_min": "Maior que (KB)",
"size_max": "Menor que (KB)",
"starred": "Favoritos",
"unread": "Não lidos",
"read": "Lidos",
@@ -3205,6 +3280,10 @@
"hint": "Clique num e-mail à esquerda para começar, ou inicie o tour."
},
"files": {
"office_edit": "Editar documento",
"office_loading": "A abrir o editor de documentos…",
"office_error": "Não foi possível abrir o editor de documentos.",
"office_close": "Fechar editor",
"open_in_new_tab": "Abrir num novo separador",
"title": "Arquivos",
"search_placeholder": "Pesquisar arquivos...",
@@ -3217,6 +3296,8 @@
"rename": "Renomear",
"rename_title": "Renomear",
"new_name": "Novo nome",
"name_forbidden_chars": "O servidor não permite estes caracteres nos nomes: {chars}",
"name_reserved": "Este nome está reservado no servidor.",
"delete": "Eliminar",
"delete_confirm_title": "Eliminar recurso",
"delete_confirm_message": "Tem a certeza de que deseja eliminar \"{name}\"? Esta ação não pode ser desfeita.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "A pesquisa não está disponível na vista unificada"
},
"calendar_event_notifications": {
"someone": "Alguém",
"invited": "{name} convidou-o para \"{title}\"",
"updated": "{name} atualizou \"{title}\"",
"cancelled": "{name} cancelou \"{title}\"",
"untitled": "Evento sem título"
},
"share_notifications": {
"someone": "Alguém",
"shared": "{name} partilhou consigo {kind} \"{object}\"",
"changed": "{name} alterou o seu acesso a {kind} \"{object}\"",
"revoked": "{name} removeu o seu acesso a {kind} \"{object}\"",
"object": {
"folder": "a pasta",
"calendar": "o calendário",
"address_book": "o livro de endereços",
"files": "a pasta de ficheiros"
}
},
"sharing": {
"title": "Compartilhar \"{name}\"",
"description": "Conceda acesso a outros usuários ou grupos neste servidor. As alterações têm efeito imediato.",
@@ -3501,5 +3601,45 @@
"copy_event": "Copiar link do evento",
"copied": "Link copiado",
"copy_failed": "Não foi possível copiar o link"
},
"plugins": {
"oauth_callback": {
"completing": "A concluir a autorização…",
"completing_message": "Aguarde enquanto a ligação é estabelecida.",
"received_title": "Autorização recebida",
"received_message": "Pode voltar ao separador onde iniciou a ligação.",
"failed_title": "Falha na autorização",
"denied": "A autorização foi negada.",
"failed_detail": "Falha na autorização: {error}",
"missing_params": "Falta o código de autorização ou o estado.",
"delivery_failed": "Não foi possível entregar o resultado da autorização ao separador da aplicação. Feche esta janela e tente novamente."
}
},
"global_search": {
"title": "Pesquisar em tudo",
"placeholder": "Pesquisar e-mail, contactos, calendário, ficheiros...",
"scope_label": "Pesquisar em",
"account_label": "Conta",
"scope_all": "Tudo",
"scope_mail": "E-mail",
"scope_contacts": "Contatos",
"scope_calendar": "Calendário",
"scope_files": "Arquivos",
"all_accounts": "Todas as contas",
"show_all": "Mostrar todos os resultados",
"no_results": "Sem resultados para “{query}”",
"server_hint": "Palavras completas pesquisam no servidor; palavras parciais correspondem ao que já está carregado",
"searching": "A pesquisar...",
"account_error": "Não foi possível pesquisar em {account}",
"recent_searches": "Pesquisas recentes",
"recurring": "Recorrente",
"no_subject": "(no subject)",
"empty_hint": "Pesquise e-mail, contactos, eventos e ficheiros em todas as contas. Experimente from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Plano por data",
"grouped": "Agrupado",
"load_more": "Carregar mais",
"preview_empty": "Selecione um resultado para pré-visualizar",
"open_full": "Abrir",
"view_label": "Vista"
}
}
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+310 -19
View File
@@ -6,9 +6,12 @@
"not_starred": "Not starred"
},
"calendar": {
"agenda": {
"days_clear": "The next {count} days are clear.",
"no_upcoming": "No upcoming events"
"alerts": {
"minutes_after": "{count, plural, one {# minute after} other {# minutes after}}"
},
"colors": {
"indigo": "Indigo",
"teal": "Teal"
},
"drawer": {
"my_calendars": "My calendars",
@@ -20,6 +23,8 @@
"participant_placeholder": "Add participant by email"
},
"events": {
"copy_link_short": "Link",
"export_ics_short": ".ics",
"tap_to_create": "Tap + to create one"
},
"form": {
@@ -27,6 +32,18 @@
"location_placeholder": "Place or address",
"time": "Time"
},
"import": {
"file_too_large_max": "That file is too large (max {size} MB).",
"imported_count": "{count, plural, one {Imported # event.} other {Imported # events.}}",
"no_calendar": "Create a calendar before importing events.",
"no_calendar_title": "No calendar",
"picker_error": "Could not open the file picker",
"picker_unavailable": "The file picker is not installed in this build. Rebuild the app to enable importing.",
"picker_unavailable_title": "Import unavailable",
"refused_count": "{count, plural, one {# event could not be imported:} other {# events could not be imported:}}",
"refused_message": "{count, plural, =1 {The event could not be imported: {reason}} one {# event could not be imported: {reason}} other {# events could not be imported: {reason}}}",
"selected_count": "{count} selected"
},
"invitation": {
"accept": "Yes",
"add_error": "Could not add the event",
@@ -44,6 +61,10 @@
"response_sent": "Response sent",
"tentative": "Maybe",
"title": "Calendar invitation",
"trust_authentication_failed": "This message failed sender authentication (SPF/DKIM/DMARC).",
"trust_authentication_missing": "The sender could not be verified.",
"trust_sender_mismatch": "The sender differs from the event organizer.",
"trust_sender_mismatch_unverified": "The sender does not match the organizer and the message is not authenticated.",
"trust_verified": "Sender verified"
},
"management": {
@@ -56,10 +77,26 @@
"edit_calendar": "Edit calendar",
"new_calendar": "New calendar"
},
"notifications": {
"channel_name": "Calendar reminders",
"starts_in_days": "{count, plural, one {Starts in # day} other {Starts in # days}}",
"starts_in_hours": "{count, plural, one {Starts in # hour} other {Starts in # hours}}",
"starts_in_minutes": "{count, plural, one {Starts in # minute} other {Starts in # minutes}}"
},
"recurrence": {
"editor_occurrences_count": "{count, plural, one {occurrence} other {occurrences}}",
"every_count_days": "{count, plural, one {Every # day} other {Every # days}}",
"every_count_months": "{count, plural, one {Every # month} other {Every # months}}",
"every_count_weeks": "{count, plural, one {Every # week} other {Every # weeks}}",
"every_count_years": "{count, plural, one {Every # year} other {Every # years}}",
"occurrence_count": "{count, plural, one {# occurrence} other {# occurrences}}",
"until_date": "Until {date}"
},
"settings": {
"show_week_numbers_mobile_desc": "Display week numbers in the month view.",
"time_zone": "Time zone",
"time_zone_auto": "Device time zone",
"time_zone_auto_zone": "Device time zone ({zone})",
"time_zone_desc": "Zone used for new events and for interpreting the calendar. \"Device\" follows the phone."
},
"share": {
@@ -67,10 +104,27 @@
"error": "Failed to update sharing",
"no_matches": "No matches",
"no_principals": "Sharing is not available on this server.",
"role_custom": "Custom",
"search_placeholder": "Search by name or email",
"shared_with": "Shared with",
"title": "Share"
"shared_with": "Shared with"
},
"subscription": {
"add_feed": "Add a feed",
"feed_too_large": "Feed is too large",
"fetch_failed": "Could not fetch feed (HTTP {status})",
"name_and_url_required": "Enter a name and a feed URL.",
"not_a_feed": "That URL did not return an iCalendar feed",
"not_synced": "Not synced yet",
"url_scheme_invalid": "URL must start with https://, http://, webcal:// or webcals://",
"your_subscriptions": "Your subscriptions"
},
"tasks": {
"add_placeholder": "Add a task…",
"add_time": "Add time",
"clear_due_date": "Clear due date",
"delete_error": "Failed to delete task",
"not_found": "This task is no longer available.",
"save_error": "Failed to save task",
"update_error": "Failed to update task"
}
},
"common": {
@@ -80,11 +134,117 @@
"create": "Create",
"dismiss": "Dismiss",
"done": "Done",
"open": "Open",
"open_link_message": "This will open {host} outside the app.",
"open_link_title": "Open link?",
"remove": "Remove",
"retry": "Retry",
"undo": "Undo",
"update": "Update"
},
"contacts": {
"address_books": {
"confirm_delete_count": "{count, plural, =0 {Delete \"{name}\"? This cannot be undone.} one {Delete \"{name}\"? Its # contact will be deleted with it. This cannot be undone.} other {Delete \"{name}\"? Its # contacts will be deleted with it. This cannot be undone.}}"
},
"all_contacts": "All Contacts",
"bulk": {
"add_tag": "Add tag",
"delete_confirm_mobile": "{count, plural, one {Delete # contact? This cannot be undone.} other {Delete # contacts? This cannot be undone.}}",
"move_to_address_book": "Move to address book",
"tag_failed": "Tagging failed"
},
"contact": "Contact",
"create_hint_mobile": "Long-press to create a group instead",
"detail": {
"add_to_group_failed": "Add to group failed",
"copy_name": "{name} (Copy)",
"created_on": "Created {date}",
"duplicate_failed": "Duplicate failed",
"duplicate_no_book": "Cannot duplicate without an address book.",
"no_address_book": "No address book",
"not_found": "Contact not found",
"open_dialer_failed": "Cannot open dialer",
"open_link_failed": "Cannot open link",
"open_maps_failed": "Cannot open maps",
"open_sms_failed": "Cannot open messaging app",
"share": "Share",
"share_title": "Share {name}",
"sms": "SMS",
"timezone_value": "Timezone: {zone}",
"updated_on": "Updated {date}"
},
"empty_state_hint_mobile": "Tap + to add one",
"export": {
"failed": "Export failed"
},
"form": {
"add_nickname": "Add nickname",
"add_note": "Add note",
"add_organization": "Add organization",
"address_book_required": "Select an address book to save this contact.",
"categories_placeholder_mobile": "Add tag and press Enter",
"context_none": "None",
"date_placeholder": "YYYY-MM-DD, YYYY-MM, YYYY or --MM-DD",
"display_name_optional": "Display name (optional)",
"display_name_placeholder": "Custom display name",
"edit_named": "Edit {name}",
"gender_unspecified": "Unspecified",
"grammatical_gender": "Grammatical gender",
"invalid_date_message": "\"{date}\" is not a date. Use YYYY-MM-DD, YYYY-MM, YYYY or --MM-DD.",
"invalid_date_title": "Invalid date",
"invalid_email_message": "\"{address}\" is not a valid email address.",
"invalid_email_title": "Invalid email",
"label": "Label",
"label_placeholder": "Work, Personal, …",
"missing_address_book": "Missing address book",
"missing_info": "Missing info",
"name_or_email_required": "Add a name or at least one email.",
"new_organization": "New organization",
"org_name_required": "Add the organization name.",
"photo_hint_mobile": "Tap photo to choose an image",
"photo_permission_message": "Grant photo library permission to pick a contact photo.",
"photo_permission_title": "Photo access needed",
"pick_date": "Pick a date",
"pronouns": "Pronouns",
"pronouns_placeholder": "they/them",
"url": "URL"
},
"group": "Group",
"groups": {
"add_member": "Add member",
"add_members": "Add members",
"add_members_failed": "Failed to add members",
"create_ellipsis": "New group…",
"delete_confirm_named": "Delete \"{name}\"? Members will not be deleted.",
"delete_failed": "Failed to delete group",
"email_all": "Email all",
"members_with_count": "Members ({count})",
"no_emails": "No emails",
"no_members_hint": "Tap \"Add member\" to get started",
"not_found": "Group not found",
"remove_member": "Remove member",
"remove_member_confirm": "Remove \"{name}\" from this group?",
"remove_member_failed": "Failed to remove member"
},
"import": {
"change_target": "Change",
"choose_file_mobile": "Choose a vCard file",
"failed_count": "{count, plural, one {# contact could not be imported.} other {# contacts could not be imported.}}",
"import_count": "Import {count}",
"into_address_book": "Import into address book",
"into_named": "Import into {name}",
"no_address_book": "Create an address book before importing contacts.",
"picker_failed": "Could not open the file picker"
},
"no_tags_hint": "No tags yet. Type one above.",
"picker": {
"add_count": "{count, plural, =0 {Add} other {Add (#)}}",
"title": "Add Contact"
},
"rename_tag_hint_mobile": "Long-press a tag to rename it",
"tag_name_placeholder": "New tag name",
"unnamed": "Unnamed"
},
"context_menu": {
"color_tag": "Tag",
"remove_color": "Remove tag"
@@ -96,6 +256,10 @@
"attachment_too_large": "This file is larger than the server allows ({size} > {max}).",
"attachments_too_large_total": "The attachments would exceed the {max} this server allows per message.",
"editor_unavailable": "Could not read the message content. Copy your text, then close and reopen the composer.",
"format_plain_text": "Switch to plain text",
"format_rich_text": "Switch to rich text (HTML)",
"from_options": "Sender options",
"group_chip": "{name} ({count})",
"identity_unavailable": "Identity unavailable",
"image_load_failed": "Could not load image",
"no_recipient": "(no recipient)",
@@ -106,7 +270,10 @@
"recipient_copy": "Copy address",
"recipient_move_to": "Move to {field}",
"recipient_remove": "Remove",
"remove_attachment": "Remove attachment",
"save_draft": "Save Draft",
"save_failed_no_drafts": "There is no Drafts folder to save this draft to.",
"save_failed_no_identity": "Your sender identities could not be loaded, so this draft cannot be saved. Check your connection, or copy your text before closing.",
"schedule_custom": "Pick date & time…",
"schedule_in_1h": "In 1 hour",
"schedule_in_3h": "In 3 hours",
@@ -120,12 +287,17 @@
"schedule_unsupported_title": "Scheduling unavailable",
"scheduled_body": "Your message will be sent at {time}.",
"scheduled_title": "Scheduled",
"send_failed": "Failed to send",
"send_now": "Send now",
"send_timeout_body": "The message may already have gone out. Check your Sent folder before sending it again.",
"send_timeout_title": "No answer from the server",
"toolbar": {
"background_color": "Background color",
"remove_background_color": "Remove background color"
},
"undo_send_label": "Sending in {seconds}s…",
"upload_failed_short": "Upload failed",
"uploading": "Uploading...",
"uploading_pct": "Uploading {pct}%",
"validation": {
"subject_required": "Add a subject"
}
@@ -135,6 +307,15 @@
"emails_archived_count": "{count, plural, one {# email} other {# emails}} archived",
"emails_moved_to": "{count, plural, one {# email} other {# emails}} moved to {mailbox}",
"error": "Error",
"filter_chip": "{field}: {value}",
"filter_title": "Filter emails",
"folder_counts": "{total} total · {unread} unread",
"import": {
"none": "No messages were imported.",
"partial": "{imported} imported, {failed} failed.",
"success": "{count, plural, one {# message imported.} other {# messages imported.}}",
"title": "Import"
},
"marked_as_spam": "Marked as spam",
"marked_as_spam_count": "{count, plural, one {# email} other {# emails}} marked as spam",
"marked_not_spam": "Marked as not spam",
@@ -154,10 +335,12 @@
"outbox_discard": "Discard",
"outbox_failed": "{count} changes could not be saved to the server.",
"recent_searches": "Recent searches",
"remove_filter": "Remove filter {label}",
"scope_all_folders": "All folders",
"scope_this_folder": "This folder",
"search_placeholder": "Search mail...",
"showing_cached": "Showing cached mail",
"sort_toggle_hint": "Reverses the mail sort order",
"swipe_move": "Move",
"swipe_spam": "Spam",
"total": "total",
@@ -182,6 +365,7 @@
"authentication": {
"spoof_warning": "The sender address could not be verified - this message may be spoofed."
},
"body_truncated": "This message is too large to show in full.",
"bundle_too_large": "The attachments are too large to bundle.",
"color_tag": {
"blue": "Blue",
@@ -196,6 +380,11 @@
},
"delete_permanently_message": "This message will be deleted permanently and cannot be recovered.",
"delete_permanently_title": "Delete permanently?",
"details": {
"attachment_count_size": "{count, plural, one {# file} other {# files}} · {size}",
"received_after_delay": "{date} · {delay} delivery time",
"spam_score_value": "{score} ({status})"
},
"export_failed": "Export failed",
"forwarded": "Forwarded",
"load_failed": "Failed to load email",
@@ -221,17 +410,34 @@
"view_light_mode": "View in light mode"
},
"files": {
"delete_cascade_hint": "Folders are deleted along with everything inside them. This can't be undone.",
"delete_item_title": "Delete this item?",
"delete_items_title": "{count, plural, one {Delete # item?} other {Delete # items?}}",
"done": "Done",
"download_failed_status": "Download failed (HTTP {status})",
"folder_empty": "This folder is empty",
"invalid_name": "Invalid name",
"invalid_name_slash": "Names cannot contain \"/\".",
"move_to": "Move to…",
"open_error": "Could not open the file",
"open_with": "Open with…",
"preview_too_large": "This file is too large to preview.",
"selected_count": "{count, plural, one {# selected} other {# selected}}",
"share_add_people": "Add people",
"share_empty": "Not shared with anyone yet",
"share_error": "Failed to update sharing",
"share_file_error": "Could not share the file",
"share_folder_hint": "Sharing a folder shares everything inside it.",
"share_no_matches": "No matches",
"share_no_users": "No other users found",
"share_remove": "Remove access",
"share_role_custom": "Custom",
"share_search_placeholder": "Search by name or email",
"shared_with": "Shared with"
"share_unavailable": "Sharing is not available on this device",
"shared_with": "Shared with",
"sharing_access": "Sharing & access",
"upload_unavailable": "The file picker is not installed in this build. Rebuild the app to enable uploads.",
"uploading_progress": "Uploading {current}/{total} · {name}"
},
"identities": {
"badge": {
@@ -291,7 +497,7 @@
"looking_up": "Looking up your server…",
"manual": "Enter server details manually",
"notice_bad_qr": "That code isn't a Bulwark sign-in code",
"notice_bad_qr_detail": "Open Bulwark on the web, then Settings → Security → Link device to show one.",
"notice_bad_qr_detail": "Open Bulwark on the web, then Settings → Security → Link Mobile App to show one.",
"notice_bad_server": "That doesn't look like a server address",
"notice_bad_server_detail": "Try the address you use for webmail, like mail.example.com.",
"notice_full_email": "Enter a full email address",
@@ -308,7 +514,7 @@
"password_title": "Sign in to {host}",
"provider_detail": "You'll type it on their page. Bulwark only stores a sign-in token, which you can revoke at any time.",
"provider_title": "Your password stays with your provider",
"qr_hint": "Settings → Security → Link device shows a code. Point the camera at it.",
"qr_hint": "Settings → Security → Link Mobile App shows a code. Point the camera at it.",
"qr_hint_title": "Open Bulwark on your computer",
"qr_title": "Sign-in code",
"scan_desc": "The fastest way in if you're signed in on the web",
@@ -328,7 +534,18 @@
},
"offline": {
"banner": "You are offline",
"queued_changes": "{count, plural, one {# change} other {# changes}} will sync when you reconnect"
"queued_changes": "{count, plural, one {# change} other {# changes}} will sync when you reconnect",
"sync": {
"cached_count": "{count, plural, one {# new message cached} other {# new messages cached}} • {size}",
"cancelled": "Sync cancelled",
"failed": "Offline sync failed",
"processed": "{completed}/{total} processed",
"ready": "Offline mail ready",
"syncing": "Syncing offline mail",
"title": "Offline sync",
"unable_to_download": "Unable to download",
"up_to_date": "Already up to date"
}
},
"push_prompt": {
"body": "Turn on background notifications so new messages reach you while the app is closed.",
@@ -351,7 +568,7 @@
"limit": "Maximum of {count} accounts reached."
},
"shared_accounts": {
"description_mobile": "Group and shared accounts you can read through this account. Their filters and vacation responder are managed from the webmail."
"description_manage": "Group and shared accounts you can read through this account. Select one to manage its filters and vacation responder."
},
"unknown": "Unknown"
},
@@ -384,6 +601,15 @@
"not_implemented": "Not implemented",
"unavailable": "Unavailable"
},
"contacts": {
"book_count": "{count, plural, one {# contact} other {# contacts}}",
"default_book": "Default address book",
"description_mobile": "Display preferences and address-book tools.",
"export_description_count": "{count, plural, one {Export # contact as a single vCard file.} other {Export # contacts as a single vCard file.}}",
"export_empty": "No contacts to export",
"import_description_mobile": "Import contacts from a vCard (.vcf) file with duplicate detection.",
"manage_description_mobile": "Create, rename, and remove the address books that organize your contacts."
},
"downloads": {
"attachment_template": {
"description_mobile": "Template used when saving or sharing an attachment."
@@ -407,11 +633,23 @@
"description_mobile": "What happens when you tap an attachment."
},
"auto_select_reply_identity": {
"description_mobile": "When replying, send from the address the message was originally sent to."
"description_mobile": "Replies always come from the identity a message was sent to. With this on, a reply to another address on your domains (a catch-all alias) is sent from that address too."
},
"content_senders_description_mobile": "Control how external resources and tracking pixels are handled.",
"external_content": {
"allow_mobile": "Allow",
"ask_mobile": "Ask",
"block_mobile": "Block"
},
"plain_text_mode": {
"description_mobile": "Compose and read in plain text only."
},
"reply_identity_match": {
"description": "Which received addresses count as yours. Exact address only picks one of your configured identities. Same domain also treats any other address on one of your identity domains as a catch-all alias and rewrites the From header to it. Choose exact address if those addresses are distribution lists rather than aliases.",
"domain": "Any address on my domains",
"exact": "Exact address only",
"label": "Received Address Matching"
},
"send_delay": {
"description_mobile": "Hold outgoing mail for a few seconds so you can cancel it. Requires server support."
},
@@ -426,11 +664,25 @@
"swipe_right_action": {
"description_mobile": "Action when you drag a row from its left edge towards the right.",
"label_mobile": "Swipe right (left → right)"
},
"trusted_senders": {
"description_mobile": "External content always loads for senders on this list.",
"description_synced_mobile": "External content always loads for senders on this list. {count} synced via your address book.",
"sync_add_failed": "Could not sync to address book",
"sync_remove_failed": "Could not remove from address book",
"synced_badge": "Synced via address book"
}
},
"experimental": {
"title": "Experimental Feature"
},
"filters": {
"remove_action": "Remove action",
"remove_condition": "Remove condition",
"sieve_editor": {
"line_count": "{count, plural, one {# line} other {# lines}}"
}
},
"folders": {
"account": "Account",
"cannot_delete": "Cannot delete",
@@ -461,6 +713,7 @@
"delete_title": "Delete identity",
"description_mobile": "Manage sender names, email addresses, and signatures. Tap a row to edit.",
"display_name": "Display name",
"display_name_placeholder": "Jane Doe",
"edit": "Edit identity",
"email_address": "Email address",
"email_locked": "JMAP does not allow changing an identity's email; create a new one instead.",
@@ -502,7 +755,9 @@
"show_folder_total_count": "Show total message count on folders",
"show_folder_total_count_description": "Show the total next to the unread count in the folder list.",
"swipe_mode": {
"description": "Pick instant (swipe past the threshold to fire the action) or reveal (swipe to expose an action band, then tap to confirm).",
"instant": "Instant (swipe = action)",
"label": "Swipe behavior",
"reveal": "Reveal (swipe, then tap)"
},
"unified_cross_account": "Unified views across accounts",
@@ -523,22 +778,46 @@
},
"notifications": {
"email": {
"app_icon_badge": "Unread count on app icon",
"app_icon_badge_desc": "Show the inbox unread count as a badge on the app icon.",
"sound_channel_hint": "Sound and vibration for mail alerts are controlled by the Android notification channel: long-press a notification or open the system app settings."
},
"push": {
"confirm_disable_message_mobile": "This device will stop receiving new-mail alerts for this account.",
"description_mobile": "Receive system notifications for new mail when the app is closed. Delivered via the Bulwark push relay; the relay never sees mail content.",
"err_android_only": "Push notifications are only available on Android.",
"err_fcm_empty": "Firebase returned an empty device token.",
"err_fcm_token": "Firebase could not issue a device token ({detail}). Push over FCM needs Google Play services on this device.",
"err_fcm_token_up": "Firebase could not issue a device token ({detail}). Push over FCM needs Google Play services on this device. This device has a UnifiedPush distributor installed - switch the delivery method to UnifiedPush instead.",
"err_jmap_refused": "The mail server refused the push subscription: {detail}",
"err_no_account": "No account loaded - cannot set up push.",
"err_permission": "Notification permission was not granted.",
"err_relay_https": "The relay URL must use https://.",
"err_relay_rejected": "The push relay rejected the registration: {detail}",
"err_relay_unreachable": "Could not reach the push relay at {url} ({detail}).",
"err_up_android_only": "UnifiedPush is only available on Android.",
"err_verify_rejected": "The mail server rejected the verification code: {detail}",
"err_verify_relay": "The relay never received the verification code from the mail server (last relay response: {detail}).",
"err_verify_timeout": "The mail server did not send a verification code to the relay within 75 s. Check that the server can reach the relay URL.",
"phase_account": "Account",
"phase_distributor": "UnifiedPush distributor",
"phase_jmap": "Mail server subscription",
"phase_permission": "Notification permission",
"phase_platform": "Platform",
"phase_relay": "Relay registration",
"phase_token": "Device token (Firebase)",
"phase_token": "Device token",
"phase_verify": "Verification",
"relay_desc_mobile": "The server that delivers your notifications. Defaults to the hosted Bulwark relay; change only if you self-host. Must use https.",
"relay_invalid": "Enter a valid https:// URL",
"setup_failed": "Setup failed",
"status_android_only": "Background notifications are available on Android only."
"status_android_only": "Background notifications are available on Android only.",
"transport_desc": "How notifications reach this device: Google’s Firebase Cloud Messaging, or a UnifiedPush distributor app (such as ntfy) for Google-free delivery.",
"transport_fcm": "Google (FCM)",
"transport_label": "Delivery method",
"transport_unifiedpush": "UnifiedPush",
"up_distributor_desc": "Several UnifiedPush distributor apps are installed - pick the one that should deliver notifications.",
"up_distributor_label": "Distributor",
"up_no_distributor": "No UnifiedPush distributor app is installed on this device. Install one (for example ntfy) and try again."
}
},
"offline": {
@@ -675,10 +954,21 @@
"mobile_explainer": "S/MIME is not available in the mobile app yet. Manage keys and certificates in the webmail; signed messages are shown without verification here."
},
"tabs": {
"plugins": "Plugins"
"plugins": "Plugins",
"themes": "Themes",
"updates": "Updates"
},
"templates": {
"delete_title": "Delete template"
"delete_confirm": "Permanently delete \"{name}\"?",
"delete_title": "Delete template",
"description_mobile": "Reusable snippets for common replies. Tap one to edit; long-press to delete.",
"export_failed": "Export failed",
"import_failed": "Import failed",
"imported": "Imported",
"imported_count": "{count, plural, one {# template} other {# templates}} added.",
"is_html": "Body is HTML",
"placeholders_hint": "Placeholders: {{recipient_name}}, {{company}}, {{date}}, {{day_of_week}}, {{sender_name}}",
"share_unavailable": "Unable to share"
},
"themes": {
"default_description": "The default light and dark palettes.",
@@ -697,10 +987,11 @@
"remove_account_hint": "Long-press to remove this account",
"remove_account_message": "Sign out of {account} on this device. Your mail stays on the server.",
"remove_account_title": "Remove account?",
"show_unread": "Show unread"
"show_unread": "Show unread",
"tab_unavailable": "{name} (unavailable)"
},
"unified_mailbox": {
"accounts_failed": "{count} account(s) could not be loaded"
"accounts_failed": "{count, plural, one {# account could not be loaded} other {# accounts could not be loaded}}"
},
"updates": {
"advisory": "Security advisory",
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1029
View File
File diff suppressed because it is too large Load Diff
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "și {count} altele",
"draft": "Ciorne",
"starred": "Cu stea",
"pinned": "Fixat",
"replied": "Răspuns",
"forwarded": "Redirecționat",
"has_attachment": "Cu atașament",
"conversations_count": "{count} din conversațiile de pe {total}",
"conversations_count_plus": "{count}+ conversations",
"conversations_count_simple": "{count} conversații",
@@ -292,6 +296,7 @@
"forward": "Redirecționează",
"delete": "Șterge",
"archive": "Arhivează",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Stea",
"unstar": "Anulează marcarea cu stea",
"mark_unread": "Marcați ca necitit",
@@ -303,12 +308,10 @@
"move": "Mutare",
"print": "Imprimare",
"view_source": "Vizualizați sursa",
"fullscreen": "Ecran complet",
"exit_fullscreen": "Ieșire din ecran complet",
"export_email": "Exportați ca fișier .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Redirecționează ca atașament",
"import_email": "Importați fișiere .eml sau .zip",
"keyboard_shortcuts": "Comenzi rapide de la tastatură (?)",
"email_source": "Sursa e-mailului",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Solicitare confirmare de citire (faceți clic pentru a dezactiva)",
"read_receipt_off": "Solicitați o confirmare de citire",
"dsn_on": "Notificare de livrare solicitată (clic pentru a dezactiva)",
"dsn_off": "Solicită o notificare de livrare",
"format_plain_text": "Comută la text simplu",
"format_rich_text": "Comută la text formatat (HTML)",
"require_tls_on": "Livrare criptată obligatorie (clic pentru a dezactiva)",
"require_tls_off": "Impune livrare criptată (TLS)",
"new_message": "Mesaj nou",
"signature_edit_hint": "Dublu clic pentru a edita semnătura",
"reply": "Răspunde",
@@ -634,11 +643,12 @@
"from_override": {
"toggle_off": "Suprascrieți",
"toggle_on": "Anulare suprascriere",
"toggle_tooltip": "Puteți modifica liber numele și adresa din câmpul „De la”. E-mailul este trimis în continuare sub identitatea dvs. – se modifică doar antetul vizibil „De la”.",
"toggle_tooltip": "Puteți modifica liber numele și adresa din câmpul „De la”. E-mailul este trimis în continuare sub identitatea dvs.",
"name_label": "Numele expeditorului",
"name_placeholder": "Nume",
"email_label": "De la adresa de e-mail",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Dacă serverul dvs. nu acceptă această adresă ca expeditor al plicului, se folosește în schimb {identity}, iar destinatarii o pot vedea în antetul Return-Path."
},
"use_template": "Șablon",
"save_as_template": "Salvează ca șablon",
@@ -651,6 +661,8 @@
"upload_progress": "Încărcare {uploaded} / {total}",
"upload_cancel": "Anulează încărcarea",
"upload_failed": "Nu s-a putut încărca {filename}",
"attachment_too_large": "„{name}” depășește dimensiunea permisă de server ({max} per fișier)",
"attachments_total_too_large": "Atașamentele ar depăși limita serverului de {max} per mesaj",
"drop_files": "Trageți fișierele pentru a le atașa",
"show_less": "Afișați mai puțin",
"send_failed": "Nu s-a putut trimite e-mailul",
@@ -726,7 +738,11 @@
"delete_column": "Șterge coloana",
"toggle_header_row": "Comută rândul de antet",
"delete_table": "Șterge tabelul",
"pick_size": "Alege dimensiunea"
"pick_size": "Alege dimensiunea",
"font_size": "Dimensiune font",
"font_size_default": "Implicit",
"background_color": "Culoarea fundalului",
"remove_background_color": "Elimină culoarea fundalului"
},
"send_filing_warning": "Trimis - dar curățarea ulterioară a eșuat, poate rămâne o ciornă veche."
},
@@ -765,6 +781,10 @@
"email_sent": "E-mailul a fost trimis cu succes",
"email_deleted": "E-mail șters",
"email_archived": "E-mail arhivat",
"emails_deleted": "{count, plural, one {# e-mail șters} few {# e-mailuri șterse} other {# de e-mailuri șterse}}",
"emails_archived": "{count, plural, one {# e-mail arhivat} few {# e-mailuri arhivate} other {# de e-mailuri arhivate}}",
"error_archiving": "Arhivarea e-mailului a eșuat",
"error_updating": "Actualizarea e-mailului a eșuat",
"email_starred": "E-mail marcat cu stea",
"email_unstarred": "E-mail fără stea",
"email_marked_read": "E-mail marcat ca citit",
@@ -1144,6 +1164,7 @@
"status_active": "Activ pe acest dispozitiv",
"status_inactive": "Nu este activat pe acest dispozitiv",
"status_unsupported": "Acest browser nu acceptă Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "Se lucrează…",
"enable": "Activați",
"reenable": "Reînregistrează-te",
@@ -1181,7 +1202,9 @@
"enabled": "Notificări prin e-mail",
"enabled_desc": "Afișați notificări la primirea de e-mailuri noi",
"sound": "Sunet de notificare",
"sound_desc": "Redă o alertă audio la sosirea unor e-mailuri noi"
"sound_desc": "Redă o alertă audio la sosirea unor e-mailuri noi",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Notificări din calendar",
@@ -1241,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Șterge căutarea la schimbarea folderului",
"description": "Un clic pe un folder (Inbox, Trimise…) șterge căutarea activă și afișează acel folder. Dezactivat, căutarea rămâne și se reia în folderul ales."
},
"request_read_receipt": {
"label": "Solicitați confirmări de citire în mod implicit",
"description": "Activați în prealabil solicitarea de confirmare de citire atunci când redactați un mesaj nou."
@@ -1324,6 +1351,12 @@
"label": "Răspunde de la adresa de la care a fost primit mesajul",
"description": "Când răspundeți, trimiteți mesajul de la adresa la care a fost trimis inițial. Se potrivesc mai întâi identitățile; pentru livrările de tip „catch-all” ale domeniului, antetul „De la” este rescris cu aliasul, în timp ce trimiterea se face prin identitatea dvs. principală."
},
"reply_identity_match": {
"label": "Potrivirea adresei de primire",
"description": "Care adrese de primire sunt considerate ale tale. „Doar adresa exactă” alege numai una dintre identitățile configurate; „același domeniu” tratează și orice altă adresă de pe domeniile identităților tale ca alias catch-all și rescrie antetul De la cu ea. Alege adresa exactă dacă aceste adrese sunt liste de distribuție.",
"exact": "Doar adresa exactă",
"domain": "Orice adresă de pe domeniile mele"
},
"signature_position": {
"label": "Poziția semnăturii",
"description": "Unde să inserați semnătura în răspunsuri și redirecționări. Deasupra textului citat, semnătura se citește în mod natural ca o încheiere a răspunsului; sub text, mesajul original rămâne contiguu.",
@@ -1753,7 +1786,9 @@
"warnings": {
"end_before_start": "Data de încheiere trebuie să fie ulterioară datei de începere",
"start_in_past": "Data de începere este în trecut",
"empty_body": "Corpul mesajului este gol – destinatarii vor primi un răspuns gol"
"empty_body": "Corpul mesajului este gol – destinatarii vor primi un răspuns gol",
"subject_too_long": "Subiectul este prea lung pentru server (cel mult {max} octeți).",
"body_too_long": "Mesajul este prea lung pentru server (cel mult {max} octeți, inclusiv formatarea)."
}
},
"folders": {
@@ -1770,6 +1805,8 @@
"role_trash": "Coș de gunoi",
"role_junk": "Spam / Mesaje nedorite",
"role_archive": "Arhivă",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Niciunul",
"create_folder": "Creați un dosar",
"create_subfolder": "Creați un subfolder",
@@ -1879,6 +1916,8 @@
"title": "Contacte",
"description": "Importați și exportați contactele",
"group_by_letter_label": "Grupați după prima literă",
"sort_by_last_name_label": "Sortare după nume de familie",
"sort_by_last_name_description": "Ordonați lista de contacte după numele de familie, astfel încât membrii familiei să apară împreună",
"group_by_letter_description": "Afișați titlurile secțiunilor în ordine alfabetică în lista de contacte",
"import_label": "Importați contactele",
"import_description": "Importați contactele dintr-un fișier „vCard” (.vcf)",
@@ -1970,6 +2009,7 @@
"actions": "Acțiuni",
"add_action": "Adăugare acțiune",
"stop_processing": "Opriți procesarea regulilor următoare",
"include_spam": "Mută și mesajele marcate ca spam",
"attachment_type_placeholder": "de ex. pdf, doc, jpg",
"value_placeholder_multi": "Valoare (mai multe valori separate prin virgule)",
"condition_fields": {
@@ -2011,6 +2051,8 @@
"copy_to_folder": "Selectează folderul",
"forward_to": "Redirecționează către adresa de e-mail",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Păstrează o copie",
"forward_limit": "Limita de redirecționări per mesaj pe acest server: {count}. Redirecționările în plus sunt omise.",
"reject_message": "Mesaj de respingere",
"reject_placeholder": "E-mailul dvs. a fost respins",
"label_name": "Numele etichetei",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Subfolder nou...",
"new_folder": "Dosar nou...",
"rename": "Redenumire...",
"share": "Partajează...",
"import_email": "Importați fișiere .eml sau .zip...",
"empty_folder": "Folder gol",
"empty_folder_generic": "Folder gol",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "Nu s-a putut redenumi folderul",
"toast_error_delete": "Nu s-a putut șterge folderul",
"toast_error_delete_has_children": "Dosarul conține subdosare. Ștergeți-le mai întâi.",
"toast_error_delete_has_email": "Dosarul nu este gol. Goliți-l mai întâi."
"toast_error_delete_has_email": "Dosarul nu este gol. Goliți-l mai întâi.",
"delete_with_contents_title": "Dosarul nu este gol",
"delete_with_contents_message": "Dosarul „{name}” încă mai conține {count, plural, one {1 mesaj} other {# mesaje}}. Ștergi dosarul și toate mesajele sale? Acțiunea nu poate fi anulată.",
"delete_with_contents_confirm": "Șterge dosarul și mesajele"
},
"shortcuts": {
"title": "Comenzi rapide de la tastatură",
@@ -2270,7 +2316,8 @@
"search": "Căutare focalizată",
"help": "Afișați comenzile rapide",
"refresh": "Reîmprospătați e-mailurile",
"select_all": "Selectează tot"
"select_all": "Selectează tot",
"global_search": "Caută în tot"
},
"threads": {
"expand_collapse": "Extindeți/reduceți firul de discuție"
@@ -2722,7 +2769,10 @@
"any_month": "Orice lună",
"has_email": "Are e-mail",
"has_phone": "Are telefon",
"has_photo": "Are fotografie"
"has_photo": "Are fotografie",
"sort_by": "Sortare după",
"sort_first_name": "Prenume",
"sort_last_name": "Nume de familie"
}
},
"calendar": {
@@ -2758,6 +2808,10 @@
"duplicate": "Duplicare",
"today_header": "Astăzi",
"tomorrow_header": "Mâine",
"agenda_show_earlier": "Afișează evenimentele anterioare",
"agenda_loading": "Se încarcă…",
"agenda_range_start": "Evenimente de la {date}",
"agenda_range_end": "Evenimente până la {date}",
"export_ics": "Exportați ca fișier .ics",
"copy_title": "Copiați titlul",
"copy_link": "Copiați linkul întâlnirii",
@@ -2793,6 +2847,14 @@
"color": "Culoare"
},
"participants": {
"availability": {
"title": "Disponibilitate",
"free": "Disponibil",
"busy": "Ocupat",
"tentative": "Provizoriu",
"unknown": "Nu este un utilizator al acestui server",
"checking": "Se verifică…"
},
"title": "Participanți",
"add": "Adăugați un participant",
"organizer": "Organizator",
@@ -2867,7 +2929,10 @@
"all_events": "Toate evenimentele",
"cancel": "Anulează",
"save": "Salvare",
"delete": "Șterge"
"delete": "Șterge",
"rsvp_title": "Răspundeți la evenimentul recurent",
"rsvp_description": "Acesta este un eveniment recurent. Pentru ce evenimente se aplică răspunsul dvs.?",
"respond": "Răspundeți"
},
"alerts": {
"title": "Memento-uri",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Afișați orele evenimentelor în vizualizarea calendarului lunar",
"show_week_numbers": "Afișați numerele săptămânilor",
"show_week_numbers_desc": "Afișați numerele săptămânilor în mini-calendar",
"calendar_free_scroll": "Derulare liberă",
"calendar_free_scroll_desc": "Derulează continuu prin luni, săptămâni și zile în loc de o singură perioadă odată",
"enable_tasks": "Activați sarcinile",
"enable_tasks_desc": "Afișați o vizualizare a sarcinilor în calendar pentru gestionarea sarcinilor de îndeplinit",
"show_tasks_on_calendar": "Afișați sarcinile în calendar",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Întârziere de 1 secundă",
"hover_preview_delay_2s": "Întârziere de 2 secunde",
"hover_preview_off": "Dezactivat",
"organizer_identity": "Organizează invitațiile ca",
"organizer_identity_desc": "Adresa de calendar folosită ca organizator când inviți persoane la evenimente.",
"organizer_identity_failed": "Nu s-a putut schimba adresa organizatorului",
"show_birthday_calendar": "Calendarul zilelor de naștere ale contactelor",
"show_birthday_calendar_desc": "Afișați un calendar virtual cu zilele de naștere ale contactelor dvs."
},
@@ -2973,6 +3043,9 @@
"alert_now": "Începând de acum",
"alert_in_minutes": "În {count} min",
"invitation_sent": "Invitații trimise",
"invitations_denied_title": "Invitațiile nu pot fi trimise",
"invitations_denied": "Serverul a refuzat trimiterea invitațiilor: {reason}. Salvezi evenimentul fără a le trimite?",
"save_without_invitations": "Salvează fără invitații",
"rsvp_updated": "Răspuns actualizat",
"rsvp_error": "Nu s-a putut actualiza răspunsul",
"event_duplicated": "Eveniment duplicat",
@@ -3002,7 +3075,7 @@
"supported_formats": "Suportă fișiere de tip „iCalendar” (.ics)",
"url_description": "Introduceți adresa URL a unui flux extern iCalendar (.ics) pentru a importa evenimente.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Suportă adresele URL de tip CalDAV și iCalendar (.ics)",
"url_hint": "Suportă adresele URL de tip iCalendar (.ics)",
"fetch": "Preluare",
"invalid_url": "Vă rugăm să introduceți o adresă de e-mail validă URL",
"url_fetch_failed": "Nu s-a putut prelua calendarul de la URL",
@@ -3149,6 +3222,25 @@
"overdue": "Restant"
}
},
"calendar_event_notifications": {
"someone": "Cineva",
"invited": "{name} te-a invitat la „{title}”",
"updated": "{name} a actualizat „{title}”",
"cancelled": "{name} a anulat „{title}”",
"untitled": "Eveniment fără titlu"
},
"share_notifications": {
"someone": "Cineva",
"shared": "{name} a partajat cu tine {kind} „{object}”",
"changed": "{name} ți-a modificat accesul la {kind} „{object}”",
"revoked": "{name} ți-a retras accesul la {kind} „{object}”",
"object": {
"folder": "dosarul",
"calendar": "calendarul",
"address_book": "agenda de contacte",
"files": "dosarul de fișiere"
}
},
"sharing": {
"title": "Distribuie „{name}”",
"description": "Acordați acces altor utilizatori sau grupuri de pe acest server. Modificările intră în vigoare imediat.",
@@ -3187,6 +3279,8 @@
"has_attachment": "Anexe",
"date_after": "După",
"date_before": "Înainte",
"size_min": "Mai mare de (KB)",
"size_max": "Mai mic de (KB)",
"starred": "Cu stea",
"unread": "Necitit",
"read": "Citiți",
@@ -3228,6 +3322,10 @@
"hint": "Faceți clic pe orice e-mail din partea stângă pentru a începe sau parcurgeți turul de mai jos."
},
"files": {
"office_edit": "Editează documentul",
"office_loading": "Se deschide editorul de documente…",
"office_error": "Editorul de documente nu a putut fi deschis.",
"office_close": "Închide editorul",
"open_in_new_tab": "Deschide într-o filă nouă",
"title": "Fișiere",
"search_placeholder": "Căutare fișiere...",
@@ -3240,6 +3338,8 @@
"rename": "Redenumire",
"rename_title": "Redenumire",
"new_name": "Nume nou",
"name_forbidden_chars": "Serverul nu permite aceste caractere în nume: {chars}",
"name_reserved": "Acest nume este rezervat pe server.",
"delete": "Șterge",
"delete_confirm_title": "Ștergeți resursa",
"delete_confirm_message": "Sunteți sigur că doriți să ștergeți „{name}”? Această acțiune nu poate fi anulată.",
@@ -3501,5 +3601,45 @@
"copy_event": "Copiați linkul evenimentului",
"copied": "Link copiat",
"copy_failed": "Linkul nu a putut fi copiat"
},
"plugins": {
"oauth_callback": {
"completing": "Se finalizează autorizarea…",
"completing_message": "Vă rugăm să așteptați stabilirea conexiunii.",
"received_title": "Autorizare primită",
"received_message": "Vă puteți întoarce la fila în care ați inițiat conexiunea.",
"failed_title": "Autorizarea a eșuat",
"denied": "Autorizarea a fost refuzată.",
"failed_detail": "Autorizarea a eșuat: {error}",
"missing_params": "Lipsește codul de autorizare sau starea.",
"delivery_failed": "Rezultatul autorizării nu a putut fi transmis filei aplicației. Închideți această fereastră și încercați din nou."
}
},
"global_search": {
"title": "Caută în tot",
"placeholder": "Caută în mail, contacte, calendar, fișiere...",
"scope_label": "Caută în",
"account_label": "Cont",
"scope_all": "Tot",
"scope_mail": "E-mail",
"scope_contacts": "Contacte",
"scope_calendar": "Calendar",
"scope_files": "Fișiere",
"all_accounts": "Toate conturile",
"show_all": "Afișează toate rezultatele",
"no_results": "Niciun rezultat pentru „{query}”",
"server_hint": "Cuvintele complete caută pe server; fragmentele de cuvinte se potrivesc cu elementele deja încărcate",
"searching": "Se caută...",
"account_error": "Nu s-a putut căuta în {account}",
"recent_searches": "Căutări recente",
"recurring": "Recurent",
"no_subject": "(no subject)",
"empty_hint": "Caută mail, contacte, evenimente și fișiere în toate conturile. Încearcă from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Plat după dată",
"grouped": "Grupat",
"load_more": "Încarcă mai multe",
"preview_empty": "Selectează un rezultat pentru previzualizare",
"open_full": "Deschide",
"view_label": "Vizualizare"
}
}
+155 -15
View File
@@ -225,6 +225,10 @@
"and_others": "и ещё {count}",
"draft": "Черновик",
"starred": "Помечено",
"pinned": "Закреплено",
"replied": "Отвечено",
"forwarded": "Переслано",
"has_attachment": "С вложением",
"conversations_count": "{count} из {total} бесед",
"conversations_count_plus": "{count}+ бесед",
"conversations_count_simple": "{count} бесед",
@@ -292,6 +296,7 @@
"forward": "Переслать",
"delete": "Удалить",
"archive": "Архивировать",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Пометить",
"unstar": "Снять пометку",
"mark_unread": "Отметить непрочитанным",
@@ -303,12 +308,10 @@
"move": "Переместить",
"print": "Распечатать",
"view_source": "Просмотреть исходный код",
"fullscreen": "Во весь экран",
"exit_fullscreen": "Выйти из полноэкранного режима",
"export_email": "Экспортировать как .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Переслать как вложение",
"import_email": "Импортировать .eml или .zip",
"keyboard_shortcuts": "Сочетания клавиш (?)",
"email_source": "Исходный код письма",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Запрошено уведомление о прочтении (нажмите, чтобы отключить)",
"read_receipt_off": "Запросить уведомление о прочтении",
"dsn_on": "Запрошено уведомление о доставке (нажмите, чтобы отключить)",
"dsn_off": "Запросить уведомление о доставке",
"format_plain_text": "Переключить на простой текст",
"format_rich_text": "Переключить на форматированный текст (HTML)",
"require_tls_on": "Требуется зашифрованная доставка (нажмите, чтобы отключить)",
"require_tls_off": "Требовать зашифрованную доставку (TLS)",
"new_message": "Новое письмо",
"signature_edit_hint": "Дважды щёлкните, чтобы изменить подпись",
"reply": "Ответить",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Переопределить",
"toggle_on": "Отменить переопределение",
"toggle_tooltip": "Свободно редактируйте имя и адрес отправителя. Письмо по-прежнему отправляется через вашу учётную запись - меняется только видимый заголовок От.",
"toggle_tooltip": "Свободно редактируйте имя и адрес отправителя. Письмо по-прежнему отправляется через вашу учётную запись.",
"name_label": "Имя отправителя",
"name_placeholder": "Имя",
"email_label": "Email отправителя",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Если ваш сервер не принимает этот адрес как отправителя конверта, вместо него используется {identity}, и получатели увидят его в заголовке Return-Path."
},
"use_template": "Шаблон",
"save_as_template": "Сохранить как шаблон",
@@ -648,6 +658,8 @@
"upload_progress": "Загрузка {uploaded} / {total}",
"upload_cancel": "Отменить загрузку",
"upload_failed": "Не удалось загрузить {filename}",
"attachment_too_large": "«{name}» больше, чем разрешает сервер ({max} на файл)",
"attachments_total_too_large": "Вложения превысят лимит сервера в {max} на письмо",
"drop_files": "Перетащите файлы для прикрепления",
"show_less": "Свернуть",
"send_failed": "Не удалось отправить письмо",
@@ -726,7 +738,11 @@
"delete_column": "Удалить столбец",
"toggle_header_row": "Переключить строку заголовка",
"delete_table": "Удалить таблицу",
"pick_size": "Выбрать размер"
"pick_size": "Выбрать размер",
"font_size": "Размер шрифта",
"font_size_default": "По умолчанию",
"background_color": "Цвет фона",
"remove_background_color": "Убрать цвет фона"
},
"send_filing_warning": "Отправлено - но последующая очистка не удалась, может остаться устаревший черновик."
},
@@ -765,6 +781,10 @@
"email_sent": "Письмо успешно отправлено",
"email_deleted": "Письмо удалено",
"email_archived": "Письмо архивировано",
"emails_deleted": "{count, plural, one {# письмо удалено} few {# письма удалены} many {# писем удалено} other {# письма удалено}}",
"emails_archived": "{count, plural, one {# письмо архивировано} few {# письма архивированы} many {# писем архивировано} other {# письма архивировано}}",
"error_archiving": "Не удалось архивировать письмо",
"error_updating": "Не удалось обновить письмо",
"email_starred": "Письмо помечено",
"email_unstarred": "Пометка снята",
"email_marked_read": "Письмо отмечено прочитанным",
@@ -1055,8 +1075,8 @@
"description": "Показывает общее количество сообщений рядом с папками и метками, наряду с количеством непрочитанных. Отключите, чтобы показывать только непрочитанные."
},
"favicon_unread_badge": {
"label": "Unread Count on Tab Icon",
"description": "Show the inbox unread count as a badge on the browser tab icon, so new mail is visible when the tab is not focused. Disable to keep the icon unbadged."
"label": "Счётчик непрочитанных на иконке вкладки и приложения",
"description": "Показывает количество непрочитанных писем значком на иконке вкладки браузера и, если поддерживается, на иконке установленного приложения — чтобы новая почта была видна без переключения на вкладку. Отключите, чтобы обе иконки оставались без значка."
},
"pro_interface": {
"label": "Pro-интерфейс (экспериментальный)",
@@ -1143,7 +1163,9 @@
"enabled": "Уведомления о почте",
"enabled_desc": "Показывать уведомления при получении новых писем",
"sound": "Звук уведомления",
"sound_desc": "Воспроизводить звуковой сигнал при получении новых писем"
"sound_desc": "Воспроизводить звуковой сигнал при получении новых писем",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Уведомления календаря",
@@ -1172,6 +1194,7 @@
"status_busy": "Выполняется…",
"status_inactive": "Не включено на этом устройстве",
"status_unsupported": "Этот браузер не поддерживает Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Фоновые уведомления",
"devices_title": "Зарегистрированные устройства",
"devices_desc": "Все устройства, зарегистрированные для получения фоновых уведомлений этой учётной записи. После отзыва устройство сразу перестаёт их получать.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Очищать поиск при переходе между папками",
"description": "Щелчок по папке («Входящие», «Отправленные»…) очищает активный поиск и открывает эту папку. Если выключено, поиск сохраняется и выполняется заново в выбранной папке."
},
"request_read_receipt": {
"label": "Запрашивать уведомления о прочтении по умолчанию",
"description": "Заранее включать запрос уведомления о прочтении при создании нового сообщения."
@@ -1316,6 +1343,12 @@
"label": "Автоматически выбирать адрес для ответа",
"description": "При ответе автоматически переключать адрес отправителя на ту учетную запись, которая получила исходное сообщение"
},
"reply_identity_match": {
"label": "Сопоставление адреса получения",
"description": "Какие адреса получения считать вашими. «Только точный адрес» выбирает исключительно одну из настроенных личностей; «тот же домен» также считает любой другой адрес на доменах ваших личностей catch-all-псевдонимом и подставляет его в заголовок «От». Выберите точный адрес, если такие адреса являются списками рассылки.",
"exact": "Только точный адрес",
"domain": "Любой адрес на моих доменах"
},
"signature_position": {
"label": "Положение подписи",
"description": "Куда вставлять подпись в ответах и пересылке. Над цитируемым текстом она читается естественно как завершение ответа; под ним сохраняет целостность исходного сообщения.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "Дата окончания должна быть позже даты начала",
"start_in_past": "Дата начала в прошлом",
"empty_body": "Текст сообщения пуст - получатели получат пустой ответ"
"empty_body": "Текст сообщения пуст - получатели получат пустой ответ",
"subject_too_long": "Тема слишком длинная для сервера (не более {max} байт).",
"body_too_long": "Сообщение слишком длинное для сервера (не более {max} байт с учётом форматирования)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Корзина",
"role_junk": "Спам",
"role_archive": "Архив",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Нет",
"create_folder": "Создать папку",
"create_subfolder": "Создать подпапку",
@@ -1882,7 +1919,9 @@
"categories_description": "Переименование категорий контактов",
"no_categories": "Категории не найдены",
"group_by_letter_description": "Показывать алфавитные заголовки разделов в списке контактов",
"group_by_letter_label": "Группировать по первой букве"
"group_by_letter_label": "Группировать по первой букве",
"sort_by_last_name_label": "Сортировать по фамилии",
"sort_by_last_name_description": "Упорядочить список контактов по фамилии, чтобы члены семьи шли подряд"
},
"downloads": {
"title": "Загрузки",
@@ -1963,6 +2002,7 @@
"actions": "Действия",
"add_action": "Добавить действие",
"stop_processing": "Прекратить обработку последующих правил",
"include_spam": "Перемещать также письма, помеченные как спам",
"attachment_type_placeholder": "напр. pdf, doc, jpg",
"value_placeholder_multi": "Значение (несколько через запятую)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Выбрать папку",
"forward_to": "Переслать на адрес электронной почты",
"forward_placeholder": "user@пример.рф",
"forward_keep_copy": "Сохранить копию",
"forward_limit": "Лимит пересылок на одно письмо на этом сервере: {count}. Лишние пересылки пропускаются.",
"reject_message": "Сообщение об отклонении",
"reject_placeholder": "Ваше письмо было отклонено",
"label_name": "Название тега",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Новая вложенная папка...",
"new_folder": "Новая папка...",
"rename": "Переименовать...",
"share": "Поделиться...",
"import_email": "Импортировать .eml или .zip...",
"empty_folder": "Очистить папку",
"empty_folder_generic": "Очистить папку",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Не удалось удалить папку",
"toast_error_delete_has_children": "Папка содержит вложенные папки. Сначала удалите их.",
"toast_error_delete_has_email": "Папка не пуста. Сначала очистите её.",
"delete_with_contents_title": "Папка не пуста",
"delete_with_contents_message": "Папка «{name}» всё ещё содержит {count, plural, one {# сообщение} few {# сообщения} other {# сообщений}}. Удалить папку вместе со всеми сообщениями? Это действие нельзя отменить.",
"delete_with_contents_confirm": "Удалить папку и сообщения",
"placeholder_folder_name": "Имя папки",
"create": "Создать",
"rename_confirm": "Переименовать"
@@ -2270,7 +2316,8 @@
"search": "Перейти к поиску",
"help": "Показать сочетания клавиш",
"refresh": "Обновить письма",
"select_all": "Выбрать все"
"select_all": "Выбрать все",
"global_search": "Искать везде"
},
"threads": {
"expand_collapse": "Развернуть/свернуть цепочку"
@@ -2721,7 +2768,10 @@
"any_month": "Любой месяц",
"has_email": "С эл. почтой",
"has_phone": "С телефоном",
"has_photo": "С фото"
"has_photo": "С фото",
"sort_by": "Сортировать по",
"sort_first_name": "Имя",
"sort_last_name": "Фамилия"
},
"open_categories": "Открыть категории"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Дублировать",
"today_header": "Сегодня",
"tomorrow_header": "Завтра",
"agenda_show_earlier": "Показать более ранние события",
"agenda_loading": "Загрузка…",
"agenda_range_start": "События с {date}",
"agenda_range_end": "События до {date}",
"export_ics": "Экспорт в .ics",
"copy_title": "Скопировать название",
"copy_link": "Скопировать ссылку встречи",
@@ -2793,6 +2847,14 @@
"color": "Цвет"
},
"participants": {
"availability": {
"title": "Доступность",
"free": "Свободен",
"busy": "Занят",
"tentative": "Предварительно",
"unknown": "Не пользователь этого сервера",
"checking": "Проверка…"
},
"title": "Участники",
"add": "Добавить участника",
"organizer": "Организатор",
@@ -2867,7 +2929,10 @@
"all_events": "Все события",
"cancel": "Отмена",
"save": "Сохранить",
"delete": "Удалить"
"delete": "Удалить",
"rsvp_title": "Ответить на повторяющееся событие",
"rsvp_description": "Это повторяющееся событие. К каким событиям относится ваш ответ?",
"respond": "Ответить"
},
"alerts": {
"title": "Напоминание",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Отображать время события в виде месячного календаря",
"show_week_numbers": "Показывать номера недель",
"show_week_numbers_desc": "Отображать номера недель в мини-календаре",
"calendar_free_scroll": "Свободная прокрутка",
"calendar_free_scroll_desc": "Непрерывно прокручивать месяцы, недели и дни вместо одного периода за раз",
"enable_tasks": "Включить задачи",
"enable_tasks_desc": "Показывать представление задач в календаре для управления делами",
"show_tasks_on_calendar": "Показывать задачи в календаре",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Задержка 1 секунда",
"hover_preview_delay_2s": "Задержка 2 секунды",
"hover_preview_off": "Отключено",
"organizer_identity": "Организовывать приглашения как",
"organizer_identity_desc": "Адрес календаря, используемый как организатор при приглашении людей на события.",
"organizer_identity_failed": "Не удалось изменить адрес организатора",
"show_birthday_calendar": "Календарь дней рождения",
"show_birthday_calendar_desc": "Показать виртуальный календарь с днями рождения из ваших контактов"
},
@@ -2947,6 +3017,9 @@
"alert_now": "Начинается сейчас",
"alert_in_minutes": "Через {count} мин",
"invitation_sent": "Приглашения отправлены",
"invitations_denied_title": "Не удаётся отправить приглашения",
"invitations_denied": "Сервер отказался отправить приглашения: {reason}. Сохранить событие без их отправки?",
"save_without_invitations": "Сохранить без приглашений",
"rsvp_updated": "Ответ обновлён",
"rsvp_error": "Не удалось обновить ответ",
"event_duplicated": "Событие дублировано",
@@ -2975,7 +3048,7 @@
"supported_formats": "Поддерживает файлы iCalendar (.ics)",
"url_description": "Введите URL внешней ленты iCalendar (.ics) для импорта событий.",
"url_placeholder": "https://пример.рф/календарь.ics",
"url_hint": "Поддерживает URL CalDAV и iCalendar (.ics)",
"url_hint": "Поддерживает URL iCalendar (.ics)",
"fetch": "Получить",
"invalid_url": "Введите корректный URL",
"url_fetch_failed": "Не удалось получить календарь по URL",
@@ -3164,6 +3237,8 @@
"has_attachment": "Вложения",
"date_after": "После",
"date_before": "До",
"size_min": "Больше чем (КБ)",
"size_max": "Меньше чем (КБ)",
"starred": "Со звёздочкой",
"unread": "Непрочитанные",
"read": "Прочитанные",
@@ -3205,6 +3280,10 @@
"hint": "Нажмите на любое письмо слева, чтобы начать, или пройдите тур ниже."
},
"files": {
"office_edit": "Редактировать документ",
"office_loading": "Открытие редактора документов…",
"office_error": "Не удалось открыть редактор документов.",
"office_close": "Закрыть редактор",
"open_in_new_tab": "Открыть в новой вкладке",
"title": "Файлы",
"search_placeholder": "Поиск файлов...",
@@ -3217,6 +3296,8 @@
"rename": "Переименовать",
"rename_title": "Переименовать",
"new_name": "Новое название",
"name_forbidden_chars": "Сервер не допускает эти символы в именах: {chars}",
"name_reserved": "Это имя зарезервировано на сервере.",
"delete": "Удалить",
"delete_confirm_title": "Удалить ресурс",
"delete_confirm_message": "Вы уверены, что хотите удалить «{name}»? Это действие нельзя отменить.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "Поиск недоступен в объединённом представлении"
},
"calendar_event_notifications": {
"someone": "Кто-то",
"invited": "{name} пригласил(а) вас на «{title}»",
"updated": "{name} обновил(а) «{title}»",
"cancelled": "{name} отменил(а) «{title}»",
"untitled": "Событие без названия"
},
"share_notifications": {
"someone": "Кто-то",
"shared": "{name} поделился(ась) с вами {kind} «{object}»",
"changed": "{name} изменил(а) ваш доступ к {kind} «{object}»",
"revoked": "{name} отозвал(а) ваш доступ к {kind} «{object}»",
"object": {
"folder": "папкой",
"calendar": "календарём",
"address_book": "адресной книгой",
"files": "папкой файлов"
}
},
"sharing": {
"title": "Поделиться «{name}»",
"description": "Предоставьте доступ другим пользователям или группам на этом сервере. Изменения вступают в силу немедленно.",
@@ -3501,5 +3601,45 @@
"copy_event": "Скопировать ссылку на событие",
"copied": "Ссылка скопирована",
"copy_failed": "Не удалось скопировать ссылку"
},
"plugins": {
"oauth_callback": {
"completing": "Завершение авторизации…",
"completing_message": "Пожалуйста, подождите, пока устанавливается соединение.",
"received_title": "Авторизация получена",
"received_message": "Вы можете вернуться на вкладку, где начали подключение.",
"failed_title": "Ошибка авторизации",
"denied": "В авторизации отказано.",
"failed_detail": "Ошибка авторизации: {error}",
"missing_params": "Отсутствует код авторизации или состояние.",
"delivery_failed": "Не удалось передать результат авторизации на вкладку приложения. Закройте это окно и попробуйте снова."
}
},
"global_search": {
"title": "Искать везде",
"placeholder": "Поиск по почте, контактам, календарю, файлам...",
"scope_label": "Искать в",
"account_label": "Аккаунт",
"scope_all": "Всё",
"scope_mail": "Почта",
"scope_contacts": "Контакты",
"scope_calendar": "Календарь",
"scope_files": "Файлы",
"all_accounts": "Все аккаунты",
"show_all": "Показать все результаты",
"no_results": "Ничего не найдено по запросу «{query}»",
"server_hint": "Целые слова ищутся на сервере; части слов — среди уже загруженного",
"searching": "Поиск...",
"account_error": "Не удалось выполнить поиск в {account}",
"recent_searches": "Недавние запросы",
"recurring": "Повторяющееся",
"no_subject": "(no subject)",
"empty_hint": "Ищите почту, контакты, события и файлы во всех аккаунтах. Попробуйте from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Списком по дате",
"grouped": "По группам",
"load_more": "Загрузить ещё",
"preview_empty": "Выберите результат для предпросмотра",
"open_full": "Открыть",
"view_label": "Вид"
}
}
+152 -12
View File
@@ -225,6 +225,10 @@
"and_others": "a {count} ďalších",
"draft": "Koncept",
"starred": "S hviezdičkou",
"pinned": "Pripnuté",
"replied": "Zodpovedané",
"forwarded": "Preposlané",
"has_attachment": "Obsahuje prílohu",
"conversations_count": "{count} z {total} konverzácií",
"conversations_count_plus": "{count}+ konverzácií",
"conversations_count_simple": "{count} konverzácií",
@@ -292,6 +296,7 @@
"forward": "Preposlať",
"delete": "Odstrániť",
"archive": "Archivovať",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Označiť hviezdičkou",
"unstar": "Odstrániť hviezdičku",
"mark_unread": "Označiť ako neprečítané",
@@ -303,12 +308,10 @@
"move": "Presunúť",
"print": "Tlačiť",
"view_source": "Zobraziť zdrojový kód",
"fullscreen": "Celá obrazovka",
"exit_fullscreen": "Ukončiť režim celej obrazovky",
"export_email": "Exportovať ako .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Preposlať ako prílohu",
"import_email": "Importovať .eml alebo .zip",
"keyboard_shortcuts": "Klávesové skratky (?)",
"email_source": "Zdrojový kód e-mailu",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Potvrdenie o prečítaní požiadané (kliknutím vypnete)",
"read_receipt_off": "Požiadať o potvrdenie o prečítaní",
"dsn_on": "Oznámenie o doručení vyžiadané (kliknutím vypnete)",
"dsn_off": "Vyžiadať oznámenie o doručení",
"format_plain_text": "Prepnúť na obyčajný text",
"format_rich_text": "Prepnúť na formátovaný text (HTML)",
"require_tls_on": "Šifrované doručenie vyžadované (kliknutím vypnete)",
"require_tls_off": "Vyžadovať šifrované doručenie (TLS)",
"new_message": "Nová správa",
"signature_edit_hint": "Dvojklikom upravíte podpis",
"reply": "Odpovedať",
@@ -638,7 +647,8 @@
"name_label": "Meno odosielateľa",
"name_placeholder": "Meno",
"email_label": "E-mailová adresa odosielateľa",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Ak váš server túto adresu neprijme ako odosielateľa obálky, použije sa namiesto nej {identity} a príjemcovia ju uvidia v hlavičke Return-Path."
},
"use_template": "Šablóna",
"save_as_template": "Uložiť ako šablónu",
@@ -651,6 +661,8 @@
"upload_progress": "Nahrávanie {uploaded} / {total}",
"upload_cancel": "Zrušiť nahrávanie",
"upload_failed": "Súbor {filename} sa nepodarilo nahrať",
"attachment_too_large": "„{name}“ je väčší, než server povoľuje ({max} na súbor)",
"attachments_total_too_large": "Prílohy by prekročili limit servera {max} na správu",
"drop_files": "Presuňte súbory sem na priloženie",
"show_less": "Zobraziť menej",
"send_failed": "Odoslanie e-mailu zlyhalo",
@@ -726,7 +738,11 @@
"delete_column": "Odstrániť stĺpec",
"toggle_header_row": "Prepnúť riadok záhlavia",
"delete_table": "Odstrániť tabuľku",
"pick_size": "Vybrať veľkosť"
"pick_size": "Vybrať veľkosť",
"font_size": "Veľkosť písma",
"font_size_default": "Predvolené",
"background_color": "Farba pozadia",
"remove_background_color": "Odstrániť farbu pozadia"
},
"send_filing_warning": "Odoslané - ale následné upratovanie zlyhalo, môže zostať zastaraný koncept."
},
@@ -765,6 +781,10 @@
"email_sent": "E-mail bol úspešne odoslaný",
"email_deleted": "E-mail bol odstránený",
"email_archived": "E-mail bol archivovaný",
"emails_deleted": "{count, plural, one {# e-mail odstránený} few {# e-maily odstránené} other {# e-mailov odstránených}}",
"emails_archived": "{count, plural, one {# e-mail archivovaný} few {# e-maily archivované} other {# e-mailov archivovaných}}",
"error_archiving": "E-mail sa nepodarilo archivovať",
"error_updating": "E-mail sa nepodarilo aktualizovať",
"email_starred": "E-mail bol označený hviezdičkou",
"email_unstarred": "Hviezdička bola odstránená",
"email_marked_read": "E-mail bol označený ako prečítaný",
@@ -1146,7 +1166,9 @@
"enabled": "E-mailové oznámenia",
"enabled_desc": "Zobrazovať oznámenia pri príchode nových e-mailov",
"sound": "Zvuk oznámenia",
"sound_desc": "Prehrať zvukové upozornenie pri príchode nových e-mailov"
"sound_desc": "Prehrať zvukové upozornenie pri príchode nových e-mailov",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Oznámenia kalendára",
@@ -1168,6 +1190,7 @@
"status_active": "Aktívne na tomto zariadení",
"status_inactive": "Nie je povolené na tomto zariadení",
"status_unsupported": "Tento prehliadač nepodporuje Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "Pracujem…",
"enable": "Povoliť",
"reenable": "Znovu zaregistrovať",
@@ -1241,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Vymazať hľadanie pri prepnutí priečinka",
"description": "Kliknutie na priečinok (Doručená pošta, Odoslaná…) vymaže aktívne hľadanie a zobrazí priečinok. Keď je vypnuté, hľadanie zostáva a spustí sa znova vo zvolenom priečinku."
},
"title": "Správanie e-mailov",
"description": "Nakonfigurujte spôsob spracovania e-mailov",
"request_read_receipt": {
@@ -1324,6 +1351,12 @@
"label": "Odpovedať z prijatej adresy",
"description": "Pri odpovedaní automaticky prepnúť adresu odosielateľa na identitu, ktorá pôvodne prijala správu"
},
"reply_identity_match": {
"label": "Porovnávanie prijímacej adresy",
"description": "Ktoré prijímacie adresy sa považujú za vaše. „Iba presná adresa“ vyberie len jednu z vašich nastavených identít; „rovnaká doména“ navyše považuje akúkoľvek inú adresu na doménach vašich identít za catch-all alias a prepíše na ňu hlavičku Od. Ak sú tieto adresy distribučné zoznamy, zvoľte presnú adresu.",
"exact": "Iba presná adresa",
"domain": "Akákoľvek adresa na mojich doménach"
},
"signature_position": {
"label": "Pozícia podpisu",
"description": "Kam vložiť podpis v odpovediach a preposlaniach.",
@@ -1753,7 +1786,9 @@
"warnings": {
"end_before_start": "Dátum ukončenia musí byť po dátume začiatku",
"start_in_past": "Dátum začiatku je v minulosti",
"empty_body": "Telo správy je prázdne – príjemcovia dostanú prázdnu odpoveď"
"empty_body": "Telo správy je prázdne – príjemcovia dostanú prázdnu odpoveď",
"subject_too_long": "Predmet je pre server príliš dlhý (najviac {max} bajtov).",
"body_too_long": "Správa je pre server príliš dlhá (najviac {max} bajtov vrátane formátovania)."
}
},
"folders": {
@@ -1770,6 +1805,8 @@
"role_trash": "Kôš",
"role_junk": "Spam / Nevyžiadaná pošta",
"role_archive": "Archív",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Žiadna",
"create_folder": "Vytvoriť priečinok",
"create_subfolder": "Vytvoriť podpriečinok",
@@ -1879,6 +1916,8 @@
"title": "Kontakty",
"description": "Import a export kontaktov",
"group_by_letter_label": "Zoskupiť podľa prvého písmena",
"sort_by_last_name_label": "Zoradiť podľa priezviska",
"sort_by_last_name_description": "Zoradiť zoznam kontaktov podľa priezviska, aby členovia rodiny boli pri sebe",
"group_by_letter_description": "Zobraziť abecedné záhlavia sekcií v zozname kontaktov",
"import_label": "Importovať kontakty",
"import_description": "Importovať kontakty zo súboru vCard (.vcf)",
@@ -1970,6 +2009,7 @@
"actions": "Akcie",
"add_action": "Pridať akciu",
"stop_processing": "Zastaviť spracovanie ďalších pravidiel",
"include_spam": "Presúvať aj správy označené ako spam",
"attachment_type_placeholder": "napr. pdf, doc, jpg",
"value_placeholder_multi": "Hodnota (viac oddelených čiarkami)",
"condition_fields": {
@@ -2011,6 +2051,8 @@
"copy_to_folder": "Vybrať priečinok",
"forward_to": "Preposlať na e-mailovú adresu",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Ponechať kópiu",
"forward_limit": "Limit preposlaní na správu na tomto serveri: {count}. Ďalšie preposlania sa preskočia.",
"reject_message": "Správa o odmietnutí",
"reject_placeholder": "Váš e-mail bol odmietnutý",
"label_name": "Názov štítku",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Nový podpriečinok...",
"new_folder": "Nový priečinok...",
"rename": "Premenovať...",
"share": "Zdieľať...",
"import_email": "Importovať .eml alebo .zip...",
"empty_folder": "Vyprázdniť priečinok",
"empty_folder_generic": "Vyprázdniť priečinok",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "Nepodarilo sa premenovať priečinok",
"toast_error_delete": "Nepodarilo sa zmazať priečinok",
"toast_error_delete_has_children": "Priečinok obsahuje podpriečinky. Najprv ich odstráňte.",
"toast_error_delete_has_email": "Priečinok nie je prázdny. Najprv ho vyprázdnite."
"toast_error_delete_has_email": "Priečinok nie je prázdny. Najprv ho vyprázdnite.",
"delete_with_contents_title": "Priečinok nie je prázdny",
"delete_with_contents_message": "Priečinok „{name}“ stále obsahuje {count, plural, one {1 správu} few {# správy} other {# správ}}. Odstrániť priečinok aj všetky jeho správy? Túto akciu nie je možné vrátiť späť.",
"delete_with_contents_confirm": "Odstrániť priečinok a správy"
},
"shortcuts": {
"title": "Klávesové skratky",
@@ -2270,7 +2316,8 @@
"search": "Prejsť na hľadanie",
"help": "Zobraziť skratky",
"refresh": "Obnoviť e-maily",
"select_all": "Vybrať všetko"
"select_all": "Vybrať všetko",
"global_search": "Hľadať všade"
},
"threads": {
"expand_collapse": "Rozbaliť/zbaliť vlákno"
@@ -2722,7 +2769,10 @@
"any_month": "Ktorýkoľvek mesiac",
"has_email": "Má e-mail",
"has_phone": "Má telefón",
"has_photo": "Má fotku"
"has_photo": "Má fotku",
"sort_by": "Zoradiť podľa",
"sort_first_name": "Meno",
"sort_last_name": "Priezvisko"
}
},
"calendar": {
@@ -2758,6 +2808,10 @@
"duplicate": "Duplikovať",
"today_header": "Dnes",
"tomorrow_header": "Zajtra",
"agenda_show_earlier": "Zobraziť skoršie udalosti",
"agenda_loading": "Načítava sa…",
"agenda_range_start": "Udalosti od {date}",
"agenda_range_end": "Udalosti do {date}",
"export_ics": "Exportovať ako .ics",
"copy_title": "Kopírovať názov",
"copy_link": "Kopírovať odkaz na stretnutie",
@@ -2793,6 +2847,14 @@
"color": "Farba"
},
"participants": {
"availability": {
"title": "Dostupnosť",
"free": "Voľný",
"busy": "Zaneprázdnený",
"tentative": "Predbežne",
"unknown": "Nie je používateľom tohto servera",
"checking": "Overuje sa…"
},
"title": "Účastníci",
"add": "Pridať účastníka",
"organizer": "Organizátor",
@@ -2867,7 +2929,10 @@
"all_events": "Všetky udalosti",
"cancel": "Zrušiť",
"save": "Uložiť",
"delete": "Odstrániť"
"delete": "Odstrániť",
"rsvp_title": "Odpovedať na opakujúcu sa udalosť",
"rsvp_description": "Toto je opakujúca sa udalosť. Na ktoré udalosti sa vzťahuje vaša odpoveď?",
"respond": "Odpovedať"
},
"alerts": {
"title": "Pripomenutia",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Zobrazovať časy udalostí v mesačnom zobrazení kalendára",
"show_week_numbers": "Zobraziť čísla týždňov",
"show_week_numbers_desc": "Zobrazovať čísla týždňov v minikalendári",
"calendar_free_scroll": "Voľné posúvanie",
"calendar_free_scroll_desc": "Plynulo prechádzať mesiace, týždne a dni namiesto jedného obdobia naraz",
"enable_tasks": "Povoliť úlohy",
"enable_tasks_desc": "Zobrazovať zobrazenie úloh v kalendári",
"show_tasks_on_calendar": "Zobraziť úlohy v kalendári",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "Oneskorenie 1 sekunda",
"hover_preview_delay_2s": "Oneskorenie 2 sekundy",
"hover_preview_off": "Vypnuté",
"organizer_identity": "Organizovať pozvánky ako",
"organizer_identity_desc": "Adresa kalendára použitá ako organizátor, keď pozývate ľudí na udalosti.",
"organizer_identity_failed": "Nepodarilo sa zmeniť adresu organizátora",
"show_birthday_calendar": "Kalendár narodenín",
"show_birthday_calendar_desc": "Zobrazovať virtuálny kalendár s narodeninami z vašich kontaktov"
},
@@ -2973,6 +3043,9 @@
"alert_now": "Začína teraz",
"alert_in_minutes": "Za {count} min",
"invitation_sent": "Pozvánky boli odoslané",
"invitations_denied_title": "Pozvánky nie je možné odoslať",
"invitations_denied": "Server odmietol odoslať pozvánky: {reason}. Uložiť udalosť bez ich odoslania?",
"save_without_invitations": "Uložiť bez pozvánok",
"rsvp_updated": "Odpoveď bola aktualizovaná",
"rsvp_error": "Aktualizácia odpovede zlyhala",
"event_duplicated": "Udalosť bola duplikovaná",
@@ -3002,7 +3075,7 @@
"supported_formats": "Podporuje súbory iCalendar (.ics)",
"url_description": "Zadajte URL adresu externého zdroja iCalendar (.ics) na import udalostí.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Podporuje URL adresy CalDAV a iCalendar (.ics)",
"url_hint": "Podporuje URL adresy iCalendar (.ics)",
"fetch": "Načítať",
"invalid_url": "Zadajte platnú URL adresu",
"url_fetch_failed": "Načítanie kalendára z URL adresy zlyhalo",
@@ -3149,6 +3222,25 @@
"overdue": "Po termíne"
}
},
"calendar_event_notifications": {
"someone": "Niekto",
"invited": "{name} vás pozval(a) na „{title}“",
"updated": "{name} aktualizoval(a) „{title}“",
"cancelled": "{name} zrušil(a) „{title}“",
"untitled": "Udalosť bez názvu"
},
"share_notifications": {
"someone": "Niekto",
"shared": "{name} s vami zdieľa {kind} „{object}“",
"changed": "{name} zmenil(a) váš prístup k {kind} „{object}“",
"revoked": "{name} odobral(a) váš prístup k {kind} „{object}“",
"object": {
"folder": "priečinok",
"calendar": "kalendár",
"address_book": "adresár",
"files": "priečinok súborov"
}
},
"sharing": {
"title": "Zdieľať \"{name}\"",
"description": "Udeľte prístup ďalším používateľom alebo skupinám na tomto serveri. Zmeny sa prejavia okamžite.",
@@ -3187,6 +3279,8 @@
"has_attachment": "Prílohy",
"date_after": "Po",
"date_before": "Pred",
"size_min": "Väčšie ako (KB)",
"size_max": "Menšie ako (KB)",
"starred": "S hviezdičkou",
"unread": "Neprečítané",
"read": "Prečítané",
@@ -3228,6 +3322,10 @@
"hint": "Kliknite na ľubovoľný e-mail vľavo na začiatok alebo využite sprievodcu nižšie."
},
"files": {
"office_edit": "Upraviť dokument",
"office_loading": "Otvára sa editor dokumentov…",
"office_error": "Editor dokumentov sa nepodarilo otvoriť.",
"office_close": "Zavrieť editor",
"open_in_new_tab": "Otvoriť v novej karte",
"title": "Súbory",
"search_placeholder": "Hľadať súbory...",
@@ -3240,6 +3338,8 @@
"rename": "Premenovať",
"rename_title": "Premenovať",
"new_name": "Nový názov",
"name_forbidden_chars": "Server nepovoľuje v názvoch tieto znaky: {chars}",
"name_reserved": "Tento názov je na serveri vyhradený.",
"delete": "Odstrániť",
"delete_confirm_title": "Odstrániť položku",
"delete_confirm_message": "Naozaj chcete odstrániť \"{name}\"? Túto akciu nie je možné vrátiť.",
@@ -3501,5 +3601,45 @@
"copy_event": "Kopírovať odkaz na udalosť",
"copied": "Odkaz skopírovaný",
"copy_failed": "Odkaz sa nepodarilo skopírovať"
},
"plugins": {
"oauth_callback": {
"completing": "Dokončovanie autorizácie…",
"completing_message": "Počkajte, kým sa vytvorí spojenie.",
"received_title": "Autorizácia prijatá",
"received_message": "Môžete sa vrátiť na kartu, kde ste začali pripájanie.",
"failed_title": "Autorizácia zlyhala",
"denied": "Autorizácia bola zamietnutá.",
"failed_detail": "Autorizácia zlyhala: {error}",
"missing_params": "Chýba autorizačný kód alebo stav.",
"delivery_failed": "Výsledok autorizácie sa nepodarilo odovzdať karte aplikácie. Zavrite toto okno a skúste to znova."
}
},
"global_search": {
"title": "Hľadať všade",
"placeholder": "Hľadať v pošte, kontaktoch, kalendári, súboroch...",
"scope_label": "Hľadať v",
"account_label": "Účet",
"scope_all": "Všetko",
"scope_mail": "Pošta",
"scope_contacts": "Kontakty",
"scope_calendar": "Kalendár",
"scope_files": "Súbory",
"all_accounts": "Všetky účty",
"show_all": "Zobraziť všetky výsledky",
"no_results": "Žiadne výsledky pre „{query}“",
"server_hint": "Celé slová prehľadávajú server; časti slov zodpovedajú už načítaným položkám",
"searching": "Hľadá sa...",
"account_error": "Nepodarilo sa prehľadať {account}",
"recent_searches": "Nedávne hľadania",
"recurring": "Opakovaná",
"no_subject": "(no subject)",
"empty_hint": "Hľadajte poštu, kontakty, udalosti a súbory vo všetkých účtoch. Skúste from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Plocho podľa dátumu",
"grouped": "Zoskupené",
"load_more": "Načítať ďalšie",
"preview_empty": "Vyberte výsledok pre náhľad",
"open_full": "Otvoriť",
"view_label": "Zobrazenie"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "ve {count} diğer",
"draft": "Taslak",
"starred": "Yıldızlı",
"pinned": "Sabitlendi",
"replied": "Yanıtlandı",
"forwarded": "İletildi",
"has_attachment": "Ek içeriyor",
"conversations_count": "{total} konuşmadan {count} tanesi",
"conversations_count_plus": "{count}+ konuşma",
"conversations_count_simple": "{count} konuşma",
@@ -292,6 +296,7 @@
"forward": "İlet",
"delete": "Sil",
"archive": "Arşivle",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Yıldızla",
"unstar": "Yıldızı Kaldır",
"mark_unread": "Okunmadı olarak işaretle",
@@ -303,12 +308,10 @@
"move": "Taşı",
"print": "Yazdır",
"view_source": "Kaynağı görüntüle",
"fullscreen": "Tam ekran",
"exit_fullscreen": "Tam ekrandan çık",
"export_email": ".eml olarak dışa aktar",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Ek olarak ilet",
"import_email": ".eml veya .zip içe aktar",
"keyboard_shortcuts": "Klavye kısayolları (?)",
"email_source": "E-posta Kaynağı",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Okundu bilgisi istendi (devre dışı bırakmak için tıklayın)",
"read_receipt_off": "Okundu bilgisi iste",
"dsn_on": "Teslimat bildirimi istendi (kapatmak için tıklayın)",
"dsn_off": "Teslimat bildirimi iste",
"format_plain_text": "Düz metne geç",
"format_rich_text": "Zengin metne (HTML) geç",
"require_tls_on": "Şifreli teslimat zorunlu (kapatmak için tıklayın)",
"require_tls_off": "Şifreli teslimat zorunlu kıl (TLS)",
"new_message": "Yeni İleti",
"signature_edit_hint": "İmzayı düzenlemek için çift tıklayın",
"reply": "Yanıtla",
@@ -634,11 +643,12 @@
"from_override": {
"toggle_off": "Geçersiz kıl",
"toggle_on": "Geçersiz kılmayı iptal et",
"toggle_tooltip": "Gönderen adını ve adresini serbestçe düzenleyin. Posta hâlâ kimliğiniz üzerinden gönderilir - yalnızca görünür Kimden başlığı değişir.",
"toggle_tooltip": "Gönderen adını ve adresini serbestçe düzenleyin. Posta hâlâ kimliğiniz üzerinden gönderilir.",
"name_label": "Gönderen adı",
"name_placeholder": "Ad",
"email_label": "Gönderen e-posta adresi",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Sunucunuz bu adresi zarf göndereni olarak kabul etmezse yerine {identity} kullanılır ve alıcılar bunu Return-Path başlığında görebilir."
},
"use_template": "Şablon",
"save_as_template": "Şablon Olarak Kaydet",
@@ -651,6 +661,8 @@
"upload_progress": "{uploaded} / {total} yükleniyor",
"upload_cancel": "Yüklemeyi iptal et",
"upload_failed": "{filename} yüklenemedi",
"attachment_too_large": "\"{name}\" sunucunun izin verdiğinden büyük (dosya başına {max})",
"attachments_total_too_large": "Ekler sunucunun ileti başına {max} sınırını aşar",
"drop_files": "Eklemek için dosyaları buraya bırakın",
"show_less": "Daha az göster",
"send_failed": "E-posta gönderilemedi",
@@ -726,7 +738,11 @@
"delete_column": "Sütunu sil",
"toggle_header_row": "Başlık satırını aç/kapat",
"delete_table": "Tabloyu sil",
"pick_size": "Boyut seç"
"pick_size": "Boyut seç",
"font_size": "Yazı boyutu",
"font_size_default": "Varsayılan",
"background_color": "Arka plan rengi",
"remove_background_color": "Arka plan rengini kaldır"
},
"send_filing_warning": "Gönderildi - ancak sonrasındaki temizleme başarısız oldu, eski bir taslak kalabilir."
},
@@ -765,6 +781,10 @@
"email_sent": "E-posta başarıyla gönderildi",
"email_deleted": "E-posta silindi",
"email_archived": "E-posta arşivlendi",
"emails_deleted": "{count, plural, other {# e-posta silindi}}",
"emails_archived": "{count, plural, other {# e-posta arşivlendi}}",
"error_archiving": "E-posta arşivlenemedi",
"error_updating": "E-posta güncellenemedi",
"email_starred": "E-posta yıldızlandı",
"email_unstarred": "E-postanın yıldızı kaldırıldı",
"email_marked_read": "E-posta okundu olarak işaretlendi",
@@ -1143,7 +1163,9 @@
"enabled": "E-posta bildirimleri",
"enabled_desc": "Yeni e-postalar geldiğinde bildirim göster",
"sound": "Bildirim sesi",
"sound_desc": "Yeni e-postalar geldiğinde sesli uyarı çal"
"sound_desc": "Yeni e-postalar geldiğinde sesli uyarı çal",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Takvim Bildirimleri",
@@ -1172,6 +1194,7 @@
"status_busy": "Çalışıyor…",
"status_inactive": "Bu cihazda etkinleştirilmedi",
"status_unsupported": "Bu tarayıcı Web Push'u desteklemiyor",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Arka plan bildirimleri",
"devices_title": "Kayıtlı cihazlar",
"devices_desc": "Bu hesap için arka plan bildirimleri almak üzere kayıtlı tüm cihazlar. Birini iptal ettiğinizde o cihaz bildirimleri hemen almayı bırakır.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Klasör değiştirirken aramayı temizle",
"description": "Bir klasöre tıklamak (Gelen Kutusu, Gönderilenler…) etkin aramayı temizler ve o klasörü gösterir. Kapalıyken arama korunur ve geçtiğiniz klasörde yeniden çalıştırılır."
},
"request_read_receipt": {
"label": "Varsayılan olarak okundu bilgisi iste",
"description": "Yeni bir ileti yazarken okundu bilgisi isteğini önceden etkinleştir."
@@ -1321,6 +1348,12 @@
"label": "Yanıt Adresini Otomatik Seç",
"description": "Yanıtlarken, Kimden adresini iletiyi başlangıçta alan kimliğe otomatik olarak değiştir"
},
"reply_identity_match": {
"label": "Alıcı adresi eşleştirme",
"description": "Hangi alıcı adreslerinin sizin sayılacağı. „Yalnızca tam adres“ sadece yapılandırılmış kimliklerinizden birini seçer; „aynı alan adı“ ayrıca kimlik alan adlarınızdaki diğer tüm adresleri catch-all takma adı olarak değerlendirir ve Kimden başlığını buna göre yeniden yazar. Bu adresler dağıtım listesiyse tam adresi seçin.",
"exact": "Yalnızca tam adres",
"domain": "Alan adlarımdaki herhangi bir adres"
},
"signature_position": {
"label": "İmza konumu",
"description": "Yanıtlarda ve iletmelerde imzanızın nereye ekleneceği. Alıntılanan metnin üzerinde, yanıt için doğal bir kapanış olarak okunur; altında ise orijinal mesajı bir bütün hâlinde tutar.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "Bitiş tarihi başlangıç tarihinden sonra olmalıdır",
"start_in_past": "Başlangıç tarihi geçmişte",
"empty_body": "İleti gövdesi boş - alıcılar boş bir yanıt alacak"
"empty_body": "İleti gövdesi boş - alıcılar boş bir yanıt alacak",
"subject_too_long": "Konu sunucu için çok uzun (en fazla {max} bayt).",
"body_too_long": "İleti sunucu için çok uzun (biçimlendirme dahil en fazla {max} bayt)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "Çöp Kutusu",
"role_junk": "Spam / İstem Dışı",
"role_archive": "Arşiv",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Yok",
"create_folder": "Klasör Oluştur",
"create_subfolder": "Alt Klasör Oluştur",
@@ -1872,6 +1909,8 @@
"title": "Kişiler",
"description": "Kişilerinizi içe ve dışa aktarın",
"group_by_letter_label": "İlk harfe göre grupla",
"sort_by_last_name_label": "Soyada göre sırala",
"sort_by_last_name_description": "Aile üyeleri bir arada görünsün diye kişi listesini soyada göre sıralar",
"group_by_letter_description": "Kişi listesinde alfabetik bölüm başlıkları göster",
"import_label": "Kişileri İçe Aktar",
"import_description": "Kişileri bir vCard (.vcf) dosyasından içe aktarın",
@@ -1963,6 +2002,7 @@
"actions": "İşlemler",
"add_action": "İşlem Ekle",
"stop_processing": "Sonraki kuralları işlemeyi durdur",
"include_spam": "İstenmeyen olarak işaretlenen iletileri de taşı",
"attachment_type_placeholder": "örn. pdf, doc, jpg",
"value_placeholder_multi": "Değer (birden fazla virgülle ayrılır)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Klasör seçin",
"forward_to": "E-posta adresine ilet",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Bir kopyasını sakla",
"forward_limit": "Bu sunucuda ileti başına yönlendirme sınırı: {count}. Fazla yönlendirmeler atlanır.",
"reject_message": "Reddetme iletisi",
"reject_placeholder": "E-postanız reddedildi",
"label_name": "Etiket adı",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Yeni alt klasör...",
"new_folder": "Yeni klasör...",
"rename": "Yeniden adlandır...",
"share": "Paylaş...",
"import_email": ".eml veya .zip içe aktar...",
"empty_folder": "Klasörü boşalt",
"empty_folder_generic": "Klasörü boşalt",
@@ -2236,7 +2279,10 @@
"toast_error_rename": "Klasör yeniden adlandırılamadı",
"toast_error_delete": "Klasör silinemedi",
"toast_error_delete_has_children": "Klasörde alt klasörler var. Önce onları kaldırın.",
"toast_error_delete_has_email": "Klasör boş değil. Önce boşaltın."
"toast_error_delete_has_email": "Klasör boş değil. Önce boşaltın.",
"delete_with_contents_title": "Klasör boş değil",
"delete_with_contents_message": "\"{name}\" klasöründe hâlâ {count} ileti var. Klasör ve tüm iletileri silinsin mi? Bu işlem geri alınamaz.",
"delete_with_contents_confirm": "Klasörü ve iletileri sil"
},
"shortcuts": {
"title": "Klavye Kısayolları",
@@ -2270,7 +2316,8 @@
"search": "Aramaya odaklan",
"help": "Kısayolları göster",
"refresh": "E-postaları yenile",
"select_all": "Tümünü seç"
"select_all": "Tümünü seç",
"global_search": "Her yerde ara"
},
"threads": {
"expand_collapse": "İleti dizisini genişlet/daralt"
@@ -2721,7 +2768,10 @@
"any_month": "Herhangi bir ay",
"has_email": "E-postası var",
"has_phone": "Telefonu var",
"has_photo": "Fotoğrafı var"
"has_photo": "Fotoğrafı var",
"sort_by": "Sıralama",
"sort_first_name": "Ad",
"sort_last_name": "Soyad"
},
"open_categories": "Kategorileri aç"
},
@@ -2758,6 +2808,10 @@
"duplicate": "Çoğalt",
"today_header": "Bugün",
"tomorrow_header": "Yarın",
"agenda_show_earlier": "Önceki etkinlikleri göster",
"agenda_loading": "Yükleniyor…",
"agenda_range_start": "{date} tarihinden itibaren etkinlikler",
"agenda_range_end": "{date} tarihine kadar etkinlikler",
"export_ics": ".ics olarak dışa aktar",
"copy_title": "Başlığı kopyala",
"copy_link": "Toplantı bağlantısını kopyala",
@@ -2793,6 +2847,14 @@
"color": "Renk"
},
"participants": {
"availability": {
"title": "Uygunluk",
"free": "Uygun",
"busy": "Meşgul",
"tentative": "Geçici",
"unknown": "Bu sunucuda kullanıcı değil",
"checking": "Kontrol ediliyor…"
},
"title": "Katılımcılar",
"add": "Katılımcı ekle",
"organizer": "Organizatör",
@@ -2867,7 +2929,10 @@
"all_events": "Tüm etkinlikler",
"cancel": "İptal",
"save": "Kaydet",
"delete": "Sil"
"delete": "Sil",
"rsvp_title": "Yinelenen etkinliğe yanıt ver",
"rsvp_description": "Bu yinelenen bir etkinlik. Yanıtınız hangi etkinlikler için geçerli?",
"respond": "Yanıtla"
},
"alerts": {
"title": "Hatırlatıcı",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Aylık takvim görünümünde etkinlik saatlerini görüntüle",
"show_week_numbers": "Hafta numaralarını göster",
"show_week_numbers_desc": "Mini takvimde hafta numaralarını görüntüle",
"calendar_free_scroll": "Serbest kaydırma",
"calendar_free_scroll_desc": "Tek seferde bir dönem yerine aylar, haftalar ve günler arasında kesintisiz kaydırın",
"enable_tasks": "Görevleri etkinleştir",
"enable_tasks_desc": "Yapılacaklar listesini yönetmek için takvimde görev görünümü göster",
"show_tasks_on_calendar": "Görevleri takvimde göster",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1 saniyelik gecikme",
"hover_preview_delay_2s": "2 saniyelik gecikme",
"hover_preview_off": "Devre dışı",
"organizer_identity": "Davetleri şu kimlikle düzenle",
"organizer_identity_desc": "Kişileri etkinliklere davet ederken düzenleyici olarak kullanılan takvim adresi.",
"organizer_identity_failed": "Düzenleyici adresi değiştirilemedi",
"show_birthday_calendar": "Kişi doğum günü takvimi",
"show_birthday_calendar_desc": "Kişilerinizdeki doğum günlerini gösteren sanal takvim göster"
},
@@ -2973,6 +3043,9 @@
"alert_now": "Şimdi başlıyor",
"alert_in_minutes": "{count} dk içinde",
"invitation_sent": "Davetler gönderildi",
"invitations_denied_title": "Davetler gönderilemiyor",
"invitations_denied": "Sunucu davetleri göndermeyi reddetti: {reason}. Etkinlik davet gönderilmeden kaydedilsin mi?",
"save_without_invitations": "Davetsiz kaydet",
"rsvp_updated": "Yanıt güncellendi",
"rsvp_error": "Yanıt güncellenemedi",
"event_duplicated": "Etkinlik çoğaltıldı",
@@ -3001,7 +3074,7 @@
"supported_formats": "iCalendar (.ics) dosyalarını destekler",
"url_description": "Etkinlikleri içe aktarmak için harici iCalendar (.ics) akışının URL'sini girin.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "CalDAV ve iCalendar (.ics) URL'lerini destekler",
"url_hint": "iCalendar (.ics) URL'lerini destekler",
"fetch": "Getir",
"invalid_url": "Lütfen geçerli bir URL girin",
"url_fetch_failed": "URL'den takvim getirilemedi",
@@ -3149,6 +3222,25 @@
},
"nav_open_menu": "Menüyü aç"
},
"calendar_event_notifications": {
"someone": "Birisi",
"invited": "{name} sizi \"{title}\" etkinliğine davet etti",
"updated": "{name} \"{title}\" etkinliğini güncelledi",
"cancelled": "{name} \"{title}\" etkinliğini iptal etti",
"untitled": "Başlıksız etkinlik"
},
"share_notifications": {
"someone": "Birisi",
"shared": "{name}, \"{object}\" {kind} sizinle paylaştı",
"changed": "{name}, \"{object}\" {kind} erişiminizi değiştirdi",
"revoked": "{name}, \"{object}\" {kind} erişiminizi kaldırdı",
"object": {
"folder": "klasörünü",
"calendar": "takvimini",
"address_book": "adres defterini",
"files": "dosya klasörünü"
}
},
"sharing": {
"title": "\"{name}\" paylaş",
"description": "Bu sunucudaki diğer kullanıcılara veya gruplara erişim verin. Değişiklikler hemen geçerli olur.",
@@ -3187,6 +3279,8 @@
"has_attachment": "Ekler",
"date_after": "Sonra",
"date_before": "Önce",
"size_min": "Şundan büyük (KB)",
"size_max": "Şundan küçük (KB)",
"starred": "Yıldızlı",
"unread": "Okunmamış",
"read": "Okunmuş",
@@ -3228,6 +3322,10 @@
"hint": "Başlamak için soldaki herhangi bir e-postaya tıklayın veya aşağıdaki turu yapın."
},
"files": {
"office_edit": "Belgeyi düzenle",
"office_loading": "Belge düzenleyici açılıyor…",
"office_error": "Belge düzenleyici açılamadı.",
"office_close": "Düzenleyiciyi kapat",
"open_in_new_tab": "Yeni sekmede aç",
"title": "Dosyalar",
"search_placeholder": "Dosyalarda ara...",
@@ -3240,6 +3338,8 @@
"rename": "Yeniden Adlandır",
"rename_title": "Yeniden Adlandır",
"new_name": "Yeni ad",
"name_forbidden_chars": "Sunucu adlarda şu karakterlere izin vermiyor: {chars}",
"name_reserved": "Bu ad sunucuda ayrılmış.",
"delete": "Sil",
"delete_confirm_title": "Kaynağı sil",
"delete_confirm_message": "\"{name}\" silinsin mi? Bu geri alınamaz.",
@@ -3501,5 +3601,45 @@
"copy_event": "Etkinlik bağlantısını kopyala",
"copied": "Bağlantı kopyalandı",
"copy_failed": "Bağlantı kopyalanamadı"
},
"plugins": {
"oauth_callback": {
"completing": "Yetkilendirme tamamlanıyor…",
"completing_message": "Bağlantı kurulurken lütfen bekleyin.",
"received_title": "Yetkilendirme alındı",
"received_message": "Bağlantıyı başlattığınız sekmeye dönebilirsiniz.",
"failed_title": "Yetkilendirme başarısız oldu",
"denied": "Yetkilendirme reddedildi.",
"failed_detail": "Yetkilendirme başarısız oldu: {error}",
"missing_params": "Yetkilendirme kodu veya durum eksik.",
"delivery_failed": "Yetkilendirme sonucu uygulama sekmesine iletilemedi. Lütfen bu pencereyi kapatıp tekrar deneyin."
}
},
"global_search": {
"title": "Her yerde ara",
"placeholder": "Posta, kişiler, takvim ve dosyalarda ara...",
"scope_label": "Şuralarda ara",
"account_label": "Hesap",
"scope_all": "Tümü",
"scope_mail": "Posta",
"scope_contacts": "Kişiler",
"scope_calendar": "Takvim",
"scope_files": "Dosyalar",
"all_accounts": "Tüm hesaplar",
"show_all": "Tüm sonuçları göster",
"no_results": "“{query}” için sonuç yok",
"server_hint": "Tam kelimeler sunucuda arar; kelime parçaları yüklenmiş öğelerle eşleşir",
"searching": "Aranıyor...",
"account_error": "{account} içinde arama yapılamadı",
"recent_searches": "Son aramalar",
"recurring": "Yinelenen",
"no_subject": "(no subject)",
"empty_hint": "Tüm hesaplarda posta, kişi, etkinlik ve dosya arayın. Şunları deneyin: from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Tarihe göre düz",
"grouped": "Gruplandırılmış",
"load_more": "Daha fazla yükle",
"preview_empty": "Önizlemek için bir sonuç seçin",
"open_full": "Aç",
"view_label": "Görünüm"
}
}
+153 -13
View File
@@ -225,6 +225,10 @@
"and_others": "та {count} інші",
"draft": "Чернетка",
"starred": "Позначено зірочкою",
"pinned": "Закріплено",
"replied": "Відповіли",
"forwarded": "Переслано",
"has_attachment": "З вкладенням",
"conversations_count": "{count} з {total} розмов",
"conversations_count_plus": "{count}+ розмов",
"conversations_count_simple": "{count} розмов",
@@ -292,6 +296,7 @@
"forward": "вперед",
"delete": "Видалити",
"archive": "Архів",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "зірка",
"unstar": "Зняти зірочку",
"mark_unread": "Позначити як непрочитане",
@@ -303,12 +308,10 @@
"move": "Перенести",
"print": "Роздрукувати",
"view_source": "Переглянути джерело",
"fullscreen": "На весь екран",
"exit_fullscreen": "Вийти з повноекранного режиму",
"export_email": "Експортувати як .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "Переслати як вкладення",
"import_email": "Імпорт .eml або .zip",
"keyboard_shortcuts": "Комбінації клавіш (?)",
"email_source": "Джерело електронної пошти",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "Запитано сповіщення про прочитання (натисніть, щоб вимкнути)",
"read_receipt_off": "Запитати сповіщення про прочитання",
"dsn_on": "Запитано сповіщення про доставку (натисніть, щоб вимкнути)",
"dsn_off": "Запитати сповіщення про доставку",
"format_plain_text": "Перемкнути на простий текст",
"format_rich_text": "Перемкнути на форматований текст (HTML)",
"require_tls_on": "Потрібна зашифрована доставка (натисніть, щоб вимкнути)",
"require_tls_off": "Вимагати зашифровану доставку (TLS)",
"new_message": "Нове повідомлення",
"signature_edit_hint": "Двічі клацніть, щоб редагувати підпис",
"reply": "Відповісти",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "Замінити",
"toggle_on": "Скасувати заміну",
"toggle_tooltip": "Вільно редагуйте ім'я та адресу відправника. Пошта все ще надсилається через вашу ідентичність - змінюється лише видимий заголовок Від.",
"toggle_tooltip": "Вільно редагуйте ім'я та адресу відправника. Пошта все ще надсилається через вашу ідентичність.",
"name_label": "Ім'я відправника",
"name_placeholder": "Ім'я",
"email_label": "Електронна адреса відправника",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Якщо ваш сервер не приймає цю адресу як відправника конверта, замість неї використовується {identity}, і одержувачі бачитимуть її в заголовку Return-Path."
},
"use_template": "Шаблон",
"save_as_template": "Зберегти як шаблон",
@@ -648,6 +658,8 @@
"upload_progress": "Завантаження {uploaded} / {total}",
"upload_cancel": "Скасувати завантаження",
"upload_failed": "Не вдалося завантажити {filename}",
"attachment_too_large": "«{name}» більший, ніж дозволяє сервер ({max} на файл)",
"attachments_total_too_large": "Вкладення перевищать ліміт сервера {max} на лист",
"drop_files": "Перетягніть файли, щоб прикріпити",
"show_less": "Показувати менше",
"send_failed": "Не вдалося надіслати електронний лист",
@@ -726,7 +738,11 @@
"delete_column": "Видалити стовпець",
"toggle_header_row": "Переключити рядок заголовка",
"delete_table": "Видалити таблицю",
"pick_size": "Вибрати розмір"
"pick_size": "Вибрати розмір",
"font_size": "Розмір шрифту",
"font_size_default": "За замовчуванням",
"background_color": "Колір фону",
"remove_background_color": "Прибрати колір фону"
},
"send_filing_warning": "Надіслано - але подальше очищення не вдалося, може залишитися застаріла чернетка."
},
@@ -765,6 +781,10 @@
"email_sent": "Електронна пошта успішно надіслана",
"email_deleted": "Електронну пошту видалено",
"email_archived": "Електронний лист заархівовано",
"emails_deleted": "{count, plural, one {# лист видалено} few {# листи видалено} many {# листів видалено} other {# листа видалено}}",
"emails_archived": "{count, plural, one {# лист архівовано} few {# листи архівовано} many {# листів архівовано} other {# листа архівовано}}",
"error_archiving": "Не вдалося архівувати лист",
"error_updating": "Не вдалося оновити лист",
"email_starred": "Електронна пошта позначена зірочкою",
"email_unstarred": "З електронної пошти знято зірочку",
"email_marked_read": "Електронна пошта позначена як прочитана",
@@ -1143,7 +1163,9 @@
"enabled": "Сповіщення електронною поштою",
"enabled_desc": "Показувати сповіщення про надходження нових електронних листів",
"sound": "Звук сповіщення",
"sound_desc": "Відтворення звукового сповіщення про надходження нових електронних листів"
"sound_desc": "Відтворення звукового сповіщення про надходження нових електронних листів",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "Сповіщення календаря",
@@ -1172,6 +1194,7 @@
"status_busy": "Виконується…",
"status_inactive": "Не ввімкнено на цьому пристрої",
"status_unsupported": "Цей браузер не підтримує Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "Фонові сповіщення",
"devices_title": "Зареєстровані пристрої",
"devices_desc": "Усі пристрої, зареєстровані для отримання фонових сповіщень цього облікового запису. Після відкликання пристрій одразу перестає їх отримувати.",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "Очищати пошук під час перемикання тек",
"description": "Клацання теки («Вхідні», «Надіслані»…) очищає активний пошук і відкриває цю теку. Якщо вимкнено, пошук лишається й повторюється у вибраній теці."
},
"request_read_receipt": {
"label": "Запитувати сповіщення про прочитання за замовчуванням",
"description": "Заздалегідь вмикати запит сповіщення про прочитання під час створення нового повідомлення."
@@ -1321,6 +1348,12 @@
"label": "Автоматичний вибір адреси для відповіді",
"description": "Під час відповіді автоматично змінюйте адресу відправника на особу, яка спочатку отримала повідомлення"
},
"reply_identity_match": {
"label": "Зіставлення адреси отримання",
"description": "Які адреси отримання вважати вашими. «Лише точна адреса» вибирає виключно одну з налаштованих ідентичностей; «той самий домен» також вважає будь-яку іншу адресу на доменах ваших ідентичностей catch-all-псевдонімом і підставляє її в заголовок «Від». Виберіть точну адресу, якщо такі адреси є списками розсилки.",
"exact": "Лише точна адреса",
"domain": "Будь-яка адреса на моїх доменах"
},
"signature_position": {
"label": "Розташування підпису",
"description": "Куди вставляти підпис у відповідях і пересиланнях. Над цитованим текстом читається природно як завершення відповіді; під ним зберігає цілісність оригінального повідомлення.",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "Дата завершення має бути після дати початку",
"start_in_past": "Дата початку в минулому",
"empty_body": "Тіло повідомлення порожнє - одержувачі отримають порожню відповідь"
"empty_body": "Тіло повідомлення порожнє - одержувачі отримають порожню відповідь",
"subject_too_long": "Тема задовга для сервера (не більше {max} байтів).",
"body_too_long": "Повідомлення задовге для сервера (не більше {max} байтів разом із форматуванням)."
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "сміття",
"role_junk": "Спам/сміття",
"role_archive": "Архів",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "Жодного",
"create_folder": "Створити папку",
"create_subfolder": "Створити вкладену папку",
@@ -1882,7 +1919,9 @@
"categories_description": "Перейменувати категорії контактів",
"no_categories": "Категорії не знайдено",
"group_by_letter_description": "Показувати алфавітні заголовки розділів у списку контактів",
"group_by_letter_label": "Групувати за першою літерою"
"group_by_letter_label": "Групувати за першою літерою",
"sort_by_last_name_label": "Сортувати за прізвищем",
"sort_by_last_name_description": "Упорядкувати список контактів за прізвищем, щоб члени родини були поруч"
},
"downloads": {
"title": "Завантаження",
@@ -1963,6 +2002,7 @@
"actions": "Дії",
"add_action": "Додати дію",
"stop_processing": "Зупинити обробку наступних правил",
"include_spam": "Переміщувати також листи, позначені як спам",
"attachment_type_placeholder": "напр. pdf, doc, jpg",
"value_placeholder_multi": "Значення (декілька через кому)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "Виберіть папку",
"forward_to": "Переслати на електронну адресу",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Зберегти копію",
"forward_limit": "Ліміт пересилань на один лист на цьому сервері: {count}. Зайві пересилання пропускаються.",
"reject_message": "Повідомлення про відмову",
"reject_placeholder": "Ваш лист було відхилено",
"label_name": "Назва мітки",
@@ -2207,6 +2249,7 @@
"new_subfolder": "Нова вкладена папка...",
"new_folder": "Нова папка...",
"rename": "Перейменувати...",
"share": "Поділитися...",
"import_email": "Імпортувати .eml або .zip...",
"empty_folder": "Очистити папку",
"empty_folder_generic": "Очистити папку",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "Не вдалося видалити папку",
"toast_error_delete_has_children": "Папка містить вкладені папки. Спочатку видаліть їх.",
"toast_error_delete_has_email": "Папка не порожня. Спочатку очистіть її.",
"delete_with_contents_title": "Тека не порожня",
"delete_with_contents_message": "Тека «{name}» досі містить {count, plural, one {# повідомлення} few {# повідомлення} other {# повідомлень}}. Видалити теку разом з усіма повідомленнями? Цю дію не можна скасувати.",
"delete_with_contents_confirm": "Видалити теку та повідомлення",
"placeholder_folder_name": "Ім'я папки",
"create": "Створити",
"rename_confirm": "Перейменувати"
@@ -2270,7 +2316,8 @@
"search": "Пошук по фокусу",
"help": "Показати ярлики",
"refresh": "Оновити електронні листи",
"select_all": "Вибрати все"
"select_all": "Вибрати все",
"global_search": "Шукати скрізь"
},
"threads": {
"expand_collapse": "Розгорнути/згорнути ланцюжок"
@@ -2721,7 +2768,10 @@
"any_month": "Будь-який місяць",
"has_email": "З ел. поштою",
"has_phone": "З телефоном",
"has_photo": "З фото"
"has_photo": "З фото",
"sort_by": "Сортувати за",
"sort_first_name": "Ім'я",
"sort_last_name": "Прізвище"
},
"open_categories": "Відкрити категорії"
},
@@ -2758,6 +2808,10 @@
"duplicate": "дублікат",
"today_header": "Сьогодні",
"tomorrow_header": "завтра",
"agenda_show_earlier": "Показати попередні події",
"agenda_loading": "Завантаження…",
"agenda_range_start": "Події з {date}",
"agenda_range_end": "Події до {date}",
"export_ics": "Експортувати як .ics",
"copy_title": "Скопіювати назву",
"copy_link": "Скопіювати посилання зустрічі",
@@ -2793,6 +2847,14 @@
"color": "колір"
},
"participants": {
"availability": {
"title": "Доступність",
"free": "Вільний",
"busy": "Зайнятий",
"tentative": "Попередньо",
"unknown": "Не користувач цього сервера",
"checking": "Перевірка…"
},
"title": "Учасники",
"add": "Додати учасника",
"organizer": "Організатор",
@@ -2867,7 +2929,10 @@
"all_events": "Всі події",
"cancel": "Скасувати",
"save": "зберегти",
"delete": "Видалити"
"delete": "Видалити",
"rsvp_title": "Відповісти на повторювану подію",
"rsvp_description": "Це повторювана подія. До яких подій стосується ваша відповідь?",
"respond": "Відповісти"
},
"alerts": {
"title": "Нагадування",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "Відображення часу подій у місячному календарі",
"show_week_numbers": "Показати номери тижнів",
"show_week_numbers_desc": "Відображення номерів тижнів у міні-календарі",
"calendar_free_scroll": "Вільне прокручування",
"calendar_free_scroll_desc": "Безперервно прокручувати місяці, тижні та дні замість одного періоду за раз",
"enable_tasks": "Увімкнути завдання",
"enable_tasks_desc": "Показати перегляд завдань у календарі для керування справами",
"show_tasks_on_calendar": "Показати завдання в календарі",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1-секундна затримка",
"hover_preview_delay_2s": "2-секундна затримка",
"hover_preview_off": "Вимкнено",
"organizer_identity": "Організовувати запрошення як",
"organizer_identity_desc": "Адреса календаря, що використовується як організатор, коли ви запрошуєте людей на події.",
"organizer_identity_failed": "Не вдалося змінити адресу організатора",
"show_birthday_calendar": "Контактний календар днів народження",
"show_birthday_calendar_desc": "Показати віртуальний календар із днями народження ваших контактів"
},
@@ -2947,6 +3017,9 @@
"alert_now": "Починаючи зараз",
"alert_in_minutes": "Через {count} хв",
"invitation_sent": "Запрошення надіслано",
"invitations_denied_title": "Не вдається надіслати запрошення",
"invitations_denied": "Сервер відмовився надіслати запрошення: {reason}. Зберегти подію без їх надсилання?",
"save_without_invitations": "Зберегти без запрошень",
"rsvp_updated": "Відповідь оновлено",
"rsvp_error": "Не вдалося оновити відповідь",
"event_duplicated": "Подія дублюється",
@@ -2975,7 +3048,7 @@
"supported_formats": "Підтримує файли iCalendar (.ics).",
"url_description": "Введіть URL-адресу зовнішнього каналу iCalendar (.ics), щоб імпортувати події.",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "Підтримує URL-адреси CalDAV та iCalendar (.ics).",
"url_hint": "Підтримує URL-адреси iCalendar (.ics).",
"fetch": "Принести",
"invalid_url": "Введіть дійсну URL-адресу",
"url_fetch_failed": "Не вдалося отримати календар з URL-адреси",
@@ -3164,6 +3237,8 @@
"has_attachment": "Додатки",
"date_after": "Після",
"date_before": "Раніше",
"size_min": "Більше ніж (КБ)",
"size_max": "Менше ніж (КБ)",
"starred": "Позначено зірочкою",
"unread": "Непрочитаний",
"read": "Прочитайте",
@@ -3205,6 +3280,10 @@
"hint": "Натисніть будь-яку електронну пошту ліворуч, щоб розпочати, або перегляньте огляд нижче."
},
"files": {
"office_edit": "Редагувати документ",
"office_loading": "Відкриття редактора документів…",
"office_error": "Не вдалося відкрити редактор документів.",
"office_close": "Закрити редактор",
"open_in_new_tab": "Відкрити в новій вкладці",
"title": "Файли",
"search_placeholder": "Пошук файлів...",
@@ -3217,6 +3296,8 @@
"rename": "Перейменувати",
"rename_title": "Перейменувати",
"new_name": "Нова назва",
"name_forbidden_chars": "Сервер не дозволяє ці символи в назвах: {chars}",
"name_reserved": "Ця назва зарезервована на сервері.",
"delete": "Видалити",
"delete_confirm_title": "Видалити ресурс",
"delete_confirm_message": "Ви впевнені, що хочете видалити \"{name}\"? Це неможливо скасувати.",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "Пошук недоступний в об'єднаному перегляді"
},
"calendar_event_notifications": {
"someone": "Хтось",
"invited": "{name} запросив(ла) вас на «{title}»",
"updated": "{name} оновив(ла) «{title}»",
"cancelled": "{name} скасував(ла) «{title}»",
"untitled": "Подія без назви"
},
"share_notifications": {
"someone": "Хтось",
"shared": "{name} поділився(лася) з вами {kind} «{object}»",
"changed": "{name} змінив(ла) ваш доступ до {kind} «{object}»",
"revoked": "{name} скасував(ла) ваш доступ до {kind} «{object}»",
"object": {
"folder": "текою",
"calendar": "календарем",
"address_book": "адресною книгою",
"files": "текою файлів"
}
},
"sharing": {
"title": "Поділитися «{name}»",
"description": "Надайте доступ іншим користувачам або групам на цьому сервері. Зміни набувають чинності негайно.",
@@ -3501,5 +3601,45 @@
"copy_event": "Скопіювати посилання на подію",
"copied": "Посилання скопійовано",
"copy_failed": "Не вдалося скопіювати посилання"
},
"plugins": {
"oauth_callback": {
"completing": "Завершення авторизації…",
"completing_message": "Зачекайте, поки встановлюється з'єднання.",
"received_title": "Авторизацію отримано",
"received_message": "Ви можете повернутися на вкладку, де розпочали підключення.",
"failed_title": "Не вдалося виконати авторизацію",
"denied": "В авторизації відмовлено.",
"failed_detail": "Не вдалося виконати авторизацію: {error}",
"missing_params": "Відсутній код авторизації або стан.",
"delivery_failed": "Не вдалося передати результат авторизації на вкладку програми. Закрийте це вікно та спробуйте ще раз."
}
},
"global_search": {
"title": "Шукати скрізь",
"placeholder": "Пошук у пошті, контактах, календарі, файлах...",
"scope_label": "Шукати в",
"account_label": "Обліковий запис",
"scope_all": "Усе",
"scope_mail": "Пошта",
"scope_contacts": "Контакти",
"scope_calendar": "Календар",
"scope_files": "Файли",
"all_accounts": "Усі облікові записи",
"show_all": "Показати всі результати",
"no_results": "Нічого не знайдено за запитом «{query}»",
"server_hint": "Цілі слова шукаються на сервері; частини слів — серед уже завантаженого",
"searching": "Пошук...",
"account_error": "Не вдалося виконати пошук у {account}",
"recent_searches": "Останні запити",
"recurring": "Повторювана",
"no_subject": "(no subject)",
"empty_hint": "Шукайте пошту, контакти, події та файли в усіх облікових записах. Спробуйте from:, subject:, has:attachment, after:2026-01-01.",
"flat_by_date": "Списком за датою",
"grouped": "За групами",
"load_more": "Завантажити ще",
"preview_empty": "Виберіть результат для перегляду",
"open_full": "Відкрити",
"view_label": "Вигляд"
}
}
+158 -11
View File
@@ -202,6 +202,9 @@
"delete_confirm": "您確定要刪除「{name}」嗎?",
"no_apps": "尚未新增任何應用程式",
"no_apps_hint": "在側邊欄新增自訂應用程式與連結",
"managed_title": "由您的管理員提供",
"managed_description": "這些應用程式為所有人統一設定,無法變更。",
"managed_badge": "受管理",
"search_icons": "搜尋圖示…",
"show_popular": "熱門",
"show_all": "全部",
@@ -222,6 +225,10 @@
"and_others": "另有 {count} 人",
"draft": "草稿",
"starred": "已加星號",
"pinned": "已釘選",
"replied": "已回覆",
"forwarded": "已轉寄",
"has_attachment": "有附件",
"conversations_count": "顯示 {count} 個對話串,共 {total} 個",
"conversations_count_plus": "{count}+ 個對話串",
"conversations_count_simple": "{count} 個對話串",
@@ -289,6 +296,7 @@
"forward": "轉寄",
"delete": "刪除",
"archive": "封存",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "加上星號",
"unstar": "移除星號",
"mark_unread": "標示為未讀",
@@ -303,7 +311,7 @@
"fullscreen": "全螢幕",
"exit_fullscreen": "離開全螢幕",
"export_email": "匯出為 .eml",
"forward_as_attachment": "以附件轉寄",
"forward_as_attachment": "以附件形式轉寄",
"import_email": "匯入 .eml 或 .zip",
"keyboard_shortcuts": "鍵盤快速鍵 (?)",
"email_source": "郵件原始碼",
@@ -575,6 +583,12 @@
"email_composer": {
"read_receipt_on": "已要求傳送已讀回執(點擊以停用)",
"read_receipt_off": "請求已讀回執",
"dsn_on": "已要求遞送通知(點擊停用)",
"dsn_off": "要求遞送通知",
"format_plain_text": "切換為純文字",
"format_rich_text": "切換為 HTML 格式文字",
"require_tls_on": "已要求加密遞送(點擊停用)",
"require_tls_off": "要求加密遞送 (TLS)",
"new_message": "新郵件",
"signature_edit_hint": "點兩下即可編輯簽名檔",
"reply": "回覆",
@@ -629,11 +643,12 @@
"from_override": {
"toggle_off": "覆寫",
"toggle_on": "取消覆寫",
"toggle_tooltip": "自由編輯寄件者名稱與電子郵件地址。郵件仍會透過您的寄件身分傳送,只有顯示的 From 標頭會變更。",
"toggle_tooltip": "自由編輯寄件者名稱與電子郵件地址。郵件仍會透過您的寄件身分傳送。",
"name_label": "寄件者姓名",
"name_placeholder": "姓名",
"email_label": "寄件者電子郵件地址",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "如果您的伺服器不接受此地址作為信封寄件者,將改用 {identity},收件者可以在 Return-Path 標頭中看到它。"
},
"use_template": "範本",
"save_as_template": "儲存為範本",
@@ -646,6 +661,8 @@
"upload_progress": "正在上傳 {uploaded} / {total}",
"upload_cancel": "取消上傳",
"upload_failed": "無法上傳 {filename}",
"attachment_too_large": "「{name}」超過伺服器允許的大小(每個檔案 {max})",
"attachments_total_too_large": "附件將超過伺服器每封郵件 {max} 的上限",
"drop_files": "拖放要附加的檔案",
"show_less": "顯示較少",
"send_failed": "傳送郵件失敗",
@@ -721,7 +738,11 @@
"delete_column": "刪除欄",
"toggle_header_row": "切換標題列",
"delete_table": "刪除表格",
"pick_size": "選取大小"
"pick_size": "選取大小",
"font_size": "字型大小",
"font_size_default": "預設",
"background_color": "背景顏色",
"remove_background_color": "移除背景顏色"
},
"send_filing_warning": "郵件已傳送,但寄送後清理失敗,可能仍保留舊草稿。"
},
@@ -760,6 +781,10 @@
"email_sent": "郵件傳送成功",
"email_deleted": "郵件已刪除",
"email_archived": "郵件已封存",
"emails_deleted": "{count, plural, other {已刪除 # 封郵件}}",
"emails_archived": "{count, plural, other {已封存 # 封郵件}}",
"error_archiving": "封存郵件失敗",
"error_updating": "更新郵件失敗",
"email_starred": "郵件已加上星號",
"email_unstarred": "郵件已移除星號",
"email_marked_read": "郵件已標示為已讀",
@@ -1139,6 +1164,7 @@
"status_active": "已在此裝置上啟用",
"status_inactive": "在此裝置上未啟用",
"status_unsupported": "此瀏覽器不支援 Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"status_busy": "工作中…",
"enable": "啟用",
"reenable": "重新註冊",
@@ -1176,7 +1202,9 @@
"enabled": "郵件通知",
"enabled_desc": "新郵件到達時顯示通知",
"sound": "通知聲音",
"sound_desc": "新郵件到達時播放音訊提醒"
"sound_desc": "新郵件到達時播放音訊提醒",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "行事曆通知",
@@ -1236,6 +1264,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "切換資料夾時清除搜尋",
"description": "點擊資料夾(收件匣、寄件備份等)會清除目前的搜尋並瀏覽該資料夾。關閉時,搜尋會保留並在切換到的資料夾中重新執行。"
},
"request_read_receipt": {
"label": "預設請求已讀回執",
"description": "撰寫新郵件時預先啟用已讀回執請求。"
@@ -1319,6 +1351,12 @@
"label": "使用收件地址回覆",
"description": "回覆時,使用原始郵件的收件地址寄送。系統會先比對寄件身分;若是網域 catch-all 收件,則透過主要寄件身分傳送,但將 From 標頭改寫為別名地址。"
},
"reply_identity_match": {
"label": "接收地址比對",
"description": "哪些接收地址算作您的地址。「僅精確地址」只從已設定的身分中選擇;「相同網域」還會將身分網域下的其他任何地址視為 catch-all 別名,並將寄件者標頭改寫為該地址。如果這些地址是通訊群組清單,請選擇精確地址。",
"exact": "僅精確地址",
"domain": "我的網域下的任意地址"
},
"signature_position": {
"label": "簽名檔位置",
"description": "在回覆與轉寄中插入簽名檔的位置。放在引用文字上方可自然作為回覆結尾;放在下方則能保持原始郵件連續。",
@@ -1748,7 +1786,9 @@
"warnings": {
"end_before_start": "結束日期必須晚於開始日期",
"start_in_past": "開始日期已在過去",
"empty_body": "郵件內文為空 — 收件者將收到空白回覆"
"empty_body": "郵件內文為空 — 收件者將收到空白回覆",
"subject_too_long": "主旨對伺服器來說太長(最多 {max} 位元組)。",
"body_too_long": "訊息對伺服器來說太長(含格式最多 {max} 位元組)。"
}
},
"folders": {
@@ -1765,6 +1805,8 @@
"role_trash": "垃圾桶",
"role_junk": "垃圾郵件",
"role_archive": "封存",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "無",
"create_folder": "建立信件匣",
"create_subfolder": "建立子信件匣",
@@ -1865,6 +1907,8 @@
"description": "管理側邊欄中的自訂應用程式與連結",
"keep_loaded": "保持應用程式載入",
"keep_loaded_description": "切換內嵌應用程式時,讓其在背景繼續執行,以避免重新載入",
"managed_title": "由您的管理員提供",
"managed_description": "這些應用程式為所有人統一設定,無法變更。",
"manage_title": "自訂應用程式",
"manage_description": "在側邊欄新增、編輯或移除自訂應用程式"
},
@@ -1872,6 +1916,8 @@
"title": "聯絡人",
"description": "匯入與匯出聯絡人",
"group_by_letter_label": "依首字分組",
"sort_by_last_name_label": "依姓氏排序",
"sort_by_last_name_description": "依姓氏排列聯絡人清單,讓家庭成員顯示在一起",
"group_by_letter_description": "在聯絡人清單中顯示依字母排列的分節標題",
"import_label": "匯入聯絡人",
"import_description": "從 vCard(.vcf)檔案匯入聯絡人",
@@ -1963,6 +2009,7 @@
"actions": "動作",
"add_action": "新增動作",
"stop_processing": "停止處理後續規則",
"include_spam": "同時移動被標記為垃圾郵件的郵件",
"attachment_type_placeholder": "例如:pdf、doc、jpg",
"value_placeholder_multi": "值(多個用逗號分隔)",
"condition_fields": {
@@ -2004,6 +2051,8 @@
"copy_to_folder": "選取信件匣",
"forward_to": "轉寄至電子郵件地址",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "保留副本",
"forward_limit": "此伺服器每封郵件的轉寄上限:{count}。多餘的轉寄將被略過。",
"reject_message": "拒絕留言",
"reject_placeholder": "您的郵件已被拒絕",
"label_name": "標籤名稱",
@@ -2200,6 +2249,7 @@
"new_subfolder": "新增子信件匣…",
"new_folder": "新增信件匣…",
"rename": "重新命名…",
"share": "共用...",
"import_email": "匯入 .eml 或 .zip…",
"empty_folder": "清空信件匣",
"empty_folder_generic": "清空信件匣",
@@ -2229,7 +2279,10 @@
"toast_error_rename": "無法重新命名信件匣",
"toast_error_delete": "無法刪除信件匣",
"toast_error_delete_has_children": "信件匣內有子信件匣,請先移除。",
"toast_error_delete_has_email": "信件匣不是空的,請先清空。"
"toast_error_delete_has_email": "信件匣不是空的,請先清空。",
"delete_with_contents_title": "資料夾不是空的",
"delete_with_contents_message": "資料夾「{name}」中仍有 {count} 封郵件。要刪除該資料夾及其所有郵件嗎?此操作無法復原。",
"delete_with_contents_confirm": "刪除資料夾和郵件"
},
"shortcuts": {
"title": "鍵盤快速鍵",
@@ -2263,7 +2316,8 @@
"search": "將焦點移至搜尋框",
"help": "顯示快速鍵",
"refresh": "重新整理郵件",
"select_all": "全部選取"
"select_all": "全部選取",
"global_search": "全域搜尋"
},
"threads": {
"expand_collapse": "展開/收合對話串"
@@ -2715,7 +2769,10 @@
"any_month": "任何月份",
"has_email": "有電子郵件地址",
"has_phone": "有電話",
"has_photo": "有照片"
"has_photo": "有照片",
"sort_by": "排序方式",
"sort_first_name": "名",
"sort_last_name": "姓"
}
},
"calendar": {
@@ -2751,6 +2808,10 @@
"duplicate": "建立副本",
"today_header": "今天",
"tomorrow_header": "明天",
"agenda_show_earlier": "顯示更早的行程",
"agenda_loading": "載入中…",
"agenda_range_start": "顯示 {date} 起的行程",
"agenda_range_end": "顯示至 {date} 的行程",
"export_ics": "匯出為 .ics",
"copy_title": "複製標題",
"copy_link": "複製會議連結",
@@ -2786,6 +2847,14 @@
"color": "顏色"
},
"participants": {
"availability": {
"title": "空閒狀態",
"free": "有空",
"busy": "忙碌",
"tentative": "暫定",
"unknown": "不是此伺服器上的使用者",
"checking": "檢查中…"
},
"title": "參與者",
"add": "新增參與者",
"organizer": "主辦人",
@@ -2860,7 +2929,10 @@
"all_events": "所有活動",
"cancel": "取消",
"save": "儲存",
"delete": "刪除"
"delete": "刪除",
"rsvp_title": "回覆重複活動",
"rsvp_description": "這是重複活動。您的回覆適用於哪些活動?",
"respond": "回覆"
},
"alerts": {
"title": "提醒",
@@ -2898,6 +2970,8 @@
"show_time_in_month_view_desc": "在月檢視中顯示活動時間。小螢幕會顯示完整活動項目,而不只是圓點。",
"show_week_numbers": "顯示週數",
"show_week_numbers_desc": "在迷你行事曆中顯示週數",
"calendar_free_scroll": "自由捲動",
"calendar_free_scroll_desc": "連續捲動瀏覽月、週和日,而不是一次只顯示一個時段",
"enable_tasks": "啟用任務",
"enable_tasks_desc": "在行事曆中顯示任務檢視以管理待辦事項",
"show_tasks_on_calendar": "在行事曆上顯示任務",
@@ -2909,6 +2983,9 @@
"hover_preview_delay_1s": "延遲 1 秒",
"hover_preview_delay_2s": "延遲 2 秒",
"hover_preview_off": "停用",
"organizer_identity": "以此身分發送邀請",
"organizer_identity_desc": "邀請他人參加活動時用作主辦者的行事曆地址。",
"organizer_identity_failed": "無法變更主辦者地址",
"show_birthday_calendar": "聯絡人生日行事曆",
"show_birthday_calendar_desc": "顯示包含聯絡人生日的虛擬行事曆"
},
@@ -2966,6 +3043,9 @@
"alert_now": "現在開始",
"alert_in_minutes": "{count} 分鐘後",
"invitation_sent": "已傳送邀請",
"invitations_denied_title": "無法傳送邀請",
"invitations_denied": "伺服器拒絕傳送邀請:{reason}。是否不傳送邀請直接儲存活動?",
"save_without_invitations": "不傳送邀請並儲存",
"rsvp_updated": "回覆已更新",
"rsvp_error": "無法更新回覆",
"event_duplicated": "已建立活動副本",
@@ -2995,7 +3075,7 @@
"supported_formats": "支援 iCalendar (.ics) 檔案",
"url_description": "輸入外部 iCalendar(.ics)資料來源的 URL,以匯入活動。",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "支援 CalDAV 與 iCalendar(.ics)URL",
"url_hint": "支援 iCalendar(.ics)URL",
"fetch": "取得",
"invalid_url": "請輸入有效的 URL",
"url_fetch_failed": "無法從 URL 取得行事曆",
@@ -3142,6 +3222,25 @@
"overdue": "逾期"
}
},
"calendar_event_notifications": {
"someone": "有人",
"invited": "{name} 邀請您參加「{title}」",
"updated": "{name} 更新了「{title}」",
"cancelled": "{name} 取消了「{title}」",
"untitled": "未命名活動"
},
"share_notifications": {
"someone": "有人",
"shared": "{name} 與您共用了{kind}「{object}」",
"changed": "{name} 變更了您對{kind}「{object}」的存取權限",
"revoked": "{name} 移除了您對{kind}「{object}」的存取權限",
"object": {
"folder": "資料夾",
"calendar": "行事曆",
"address_book": "通訊錄",
"files": "檔案資料夾"
}
},
"sharing": {
"title": "分享「{name}」",
"description": "授予此伺服器上的其他使用者或群組存取權。變更會立即生效。",
@@ -3180,6 +3279,8 @@
"has_attachment": "附件",
"date_after": "後",
"date_before": "前",
"size_min": "大於 (KB)",
"size_max": "小於 (KB)",
"starred": "已加星號",
"unread": "未讀",
"read": "已讀",
@@ -3221,6 +3322,10 @@
"hint": "點擊左側任一郵件即可開始,或進行下方的功能導覽。"
},
"files": {
"office_edit": "編輯文件",
"office_loading": "正在開啟文件編輯器…",
"office_error": "無法開啟文件編輯器。",
"office_close": "關閉編輯器",
"open_in_new_tab": "在新分頁開啟",
"title": "檔案",
"search_placeholder": "搜尋檔案…",
@@ -3233,6 +3338,8 @@
"rename": "重新命名",
"rename_title": "重新命名",
"new_name": "新名稱",
"name_forbidden_chars": "伺服器不允許名稱中包含這些字元:{chars}",
"name_reserved": "此名稱已被伺服器保留。",
"delete": "刪除",
"delete_confirm_title": "刪除項目",
"delete_confirm_message": "您確定要刪除「{name}」嗎?此動作無法復原。",
@@ -3494,5 +3601,45 @@
"copy_event": "複製活動連結",
"copied": "連結已複製",
"copy_failed": "無法複製連結"
},
"plugins": {
"oauth_callback": {
"completing": "正在完成授權…",
"completing_message": "請稍候,正在建立連線。",
"received_title": "已收到授權",
"received_message": "您可以返回開始連線的分頁。",
"failed_title": "授權失敗",
"denied": "授權被拒絕。",
"failed_detail": "授權失敗:{error}",
"missing_params": "缺少授權碼或狀態。",
"delivery_failed": "無法將授權結果傳遞至應用程式分頁。請關閉此視窗並再試一次。"
}
},
"global_search": {
"title": "全域搜尋",
"placeholder": "搜尋郵件、聯絡人、行事曆、檔案...",
"scope_label": "搜尋範圍",
"account_label": "帳戶",
"scope_all": "全部",
"scope_mail": "郵件",
"scope_contacts": "聯絡人",
"scope_calendar": "行事曆",
"scope_files": "檔案",
"all_accounts": "所有帳戶",
"show_all": "顯示所有結果",
"no_results": "找不到「{query}」的結果",
"server_hint": "完整單字會搜尋伺服器;部分單字則比對已載入的項目",
"searching": "搜尋中...",
"account_error": "無法搜尋 {account}",
"recent_searches": "最近搜尋",
"recurring": "重複",
"no_subject": "(no subject)",
"empty_hint": "在所有帳戶中搜尋郵件、聯絡人、行事和檔案。可試試 from:、subject:、has:attachment、after:2026-01-01。",
"flat_by_date": "依日期平列",
"grouped": "分組",
"load_more": "載入更多",
"preview_empty": "選擇結果以預覽",
"open_full": "開啟",
"view_label": "檢視"
}
}
+154 -14
View File
@@ -225,6 +225,10 @@
"and_others": "和 {count} 其他人",
"draft": "草稿",
"starred": "加星标",
"pinned": "已置顶",
"replied": "已回复",
"forwarded": "已转发",
"has_attachment": "有附件",
"conversations_count": "共 {total} 个会话,当前显示 {count} 个",
"conversations_count_plus": "{count}+ 个会话",
"conversations_count_simple": "{count} 个会话",
@@ -234,7 +238,7 @@
"batch_actions": {
"select": "选择邮件",
"select_all": "全选",
"selected_messages": "已選取 {count, plural, one {1} other {#}} 封電子郵件",
"selected_messages": "已选取 {count, plural, one {1} other {#}} 封电子邮件",
"mark_read": "标记为已读",
"mark_unread": "标记为未读",
"delete": "删除",
@@ -292,6 +296,7 @@
"forward": "转发",
"delete": "删除",
"archive": "归档",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "加星标",
"unstar": "取消星标",
"mark_unread": "标记为未读",
@@ -303,12 +308,10 @@
"move": "移动",
"print": "打印",
"view_source": "查看源码",
"fullscreen": "全屏",
"exit_fullscreen": "退出全屏",
"export_email": "导出为 .eml",
"forward_as_attachment": "Forward as attachment",
"forward_as_attachment": "作为附件转发",
"import_email": "导入 .eml 或 .zip",
"keyboard_shortcuts": "键盘快捷键(?)",
"email_source": "邮件源码",
@@ -580,6 +583,12 @@
"email_composer": {
"read_receipt_on": "已请求已读回执(点击以关闭)",
"read_receipt_off": "请求已读回执",
"dsn_on": "已请求投递通知(点击禁用)",
"dsn_off": "请求投递通知",
"format_plain_text": "切换为纯文本",
"format_rich_text": "切换为富文本 (HTML)",
"require_tls_on": "已要求加密投递(点击禁用)",
"require_tls_off": "要求加密投递 (TLS)",
"new_message": "新邮件",
"signature_edit_hint": "双击以编辑签名",
"reply": "回复",
@@ -631,11 +640,12 @@
"from_override": {
"toggle_off": "覆盖",
"toggle_on": "取消覆盖",
"toggle_tooltip": "自由编辑发件人姓名和地址。邮件仍通过您的身份发送 - 仅可见的发件人标题发生变化。",
"toggle_tooltip": "自由编辑发件人姓名和地址。邮件仍通过您的身份发送。",
"name_label": "发件人姓名",
"name_placeholder": "姓名",
"email_label": "发件人电子邮件地址",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "如果您的服务器不接受此地址作为信封发件人,将改用 {identity},收件人可以在 Return-Path 标头中看到它。"
},
"use_template": "模板",
"save_as_template": "保存为模板",
@@ -648,6 +658,8 @@
"upload_progress": "正在上传 {uploaded} / {total}",
"upload_cancel": "取消上传",
"upload_failed": "无法上传 {filename}",
"attachment_too_large": "“{name}”超过了服务器允许的大小(每个文件 {max})",
"attachments_total_too_large": "附件将超过服务器每封邮件 {max} 的上限",
"drop_files": "拖放要附加的文件",
"show_less": "收起",
"send_failed": "发送邮件失败",
@@ -726,7 +738,11 @@
"delete_column": "删除列",
"toggle_header_row": "切换标题行",
"delete_table": "删除表格",
"pick_size": "选择大小"
"pick_size": "选择大小",
"font_size": "字号",
"font_size_default": "默认",
"background_color": "背景颜色",
"remove_background_color": "移除背景颜色"
},
"send_filing_warning": "已发送,但发送后的清理失败,可能会残留旧草稿。"
},
@@ -765,6 +781,10 @@
"email_sent": "邮件发送成功",
"email_deleted": "邮件已删除",
"email_archived": "邮件已归档",
"emails_deleted": "{count, plural, other {已删除 # 封邮件}}",
"emails_archived": "{count, plural, other {已归档 # 封邮件}}",
"error_archiving": "归档邮件失败",
"error_updating": "更新邮件失败",
"email_starred": "邮件已加星标",
"email_unstarred": "邮件已取消星标",
"email_marked_read": "邮件已标记为已读",
@@ -1143,7 +1163,9 @@
"enabled": "邮件通知",
"enabled_desc": "新邮件到达时显示通知",
"sound": "通知声音",
"sound_desc": "新邮件到达时播放音频提醒"
"sound_desc": "新邮件到达时播放音频提醒",
"inbox_only": "Inbox only",
"inbox_only_desc": "Only notify for mail that lands in the Inbox; skip mail a filter files into another folder"
},
"calendar": {
"title": "日历通知",
@@ -1172,6 +1194,7 @@
"status_busy": "工作中…",
"status_inactive": "在此设备上未启用",
"status_unsupported": "此浏览器不支持 Web Push",
"status_lite": "Web Push is not available in Bulwark Lite; it needs the Bulwark server.",
"title": "后台通知",
"devices_title": "已注册的设备",
"devices_desc": "已注册接收此账户后台通知的所有设备。撤销后,该设备会立即停止接收。",
@@ -1238,6 +1261,10 @@
}
},
"email_behavior": {
"clear_search_on_folder_change": {
"label": "切换文件夹时清除搜索",
"description": "点击文件夹(收件箱、已发送等)会清除当前搜索并浏览该文件夹。关闭时,搜索会保留并在切换到的文件夹中重新运行。"
},
"request_read_receipt": {
"label": "默认请求已读回执",
"description": "撰写新邮件时预先启用已读回执请求。"
@@ -1321,6 +1348,12 @@
"label": "自动选择回复地址",
"description": "回复时自动将发件人地址切换为最初收到该邮件的身份"
},
"reply_identity_match": {
"label": "接收地址匹配",
"description": "哪些接收地址算作您的地址。“仅精确地址”只从已配置的身份中选择;“相同域名”还会将身份域名下的其他任何地址视为 catch-all 别名,并将发件人标头改写为该地址。如果这些地址是分发列表,请选择精确地址。",
"exact": "仅精确地址",
"domain": "我的域名下的任意地址"
},
"signature_position": {
"label": "签名位置",
"description": "在回复和转发中插入签名的位置。位于引用文本上方时,可作为回复的自然结尾;位于下方时,保持原始邮件连贯。",
@@ -1746,7 +1779,9 @@
"warnings": {
"end_before_start": "结束日期必须晚于开始日期",
"start_in_past": "开始日期已在过去",
"empty_body": "邮件正文为空--收件人将收到空白回复"
"empty_body": "邮件正文为空--收件人将收到空白回复",
"subject_too_long": "主题对服务器来说太长(最多 {max} 字节)。",
"body_too_long": "消息对服务器来说太长(含格式最多 {max} 字节)。"
}
},
"folders": {
@@ -1763,6 +1798,8 @@
"role_trash": "已删除",
"role_junk": "垃圾邮件",
"role_archive": "归档",
"role_scheduled": "Scheduled",
"role_snoozed": "Snoozed",
"role_none": "无",
"create_folder": "创建文件夹",
"create_subfolder": "新建子文件夹",
@@ -1882,7 +1919,9 @@
"categories_description": "重命名联系人类别",
"no_categories": "未找到类别",
"group_by_letter_description": "在联系人列表中显示按字母顺序排列的分节标题",
"group_by_letter_label": "按首字母分组"
"group_by_letter_label": "按首字母分组",
"sort_by_last_name_label": "按姓氏排序",
"sort_by_last_name_description": "按姓氏排列联系人列表,使家庭成员显示在一起"
},
"downloads": {
"title": "下载",
@@ -1963,6 +2002,7 @@
"actions": "操作",
"add_action": "添加操作",
"stop_processing": "停止处理后续规则",
"include_spam": "同时移动被标记为垃圾邮件的邮件",
"attachment_type_placeholder": "例如:pdf、doc、jpg",
"value_placeholder_multi": "值(多个用逗号分隔)",
"condition_fields": {
@@ -2004,6 +2044,8 @@
"copy_to_folder": "选择文件夹",
"forward_to": "转发至邮箱地址",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "保留副本",
"forward_limit": "此服务器每封邮件的转发上限:{count}。多余的转发将被跳过。",
"reject_message": "拒绝留言",
"reject_placeholder": "您的邮件已被拒绝",
"label_name": "标签名称",
@@ -2207,6 +2249,7 @@
"new_subfolder": "新建子文件夹...",
"new_folder": "新建文件夹...",
"rename": "重命名...",
"share": "共享...",
"import_email": "导入 .eml 或 .zip...",
"empty_folder": "清空文件夹",
"empty_folder_generic": "清空文件夹",
@@ -2234,6 +2277,9 @@
"toast_error_delete": "删除文件夹失败",
"toast_error_delete_has_children": "文件夹包含子文件夹,请先将其删除。",
"toast_error_delete_has_email": "文件夹不为空,请先清空它。",
"delete_with_contents_title": "文件夹不为空",
"delete_with_contents_message": "文件夹“{name}”中仍有 {count} 封邮件。删除该文件夹及其所有邮件?此操作无法撤销。",
"delete_with_contents_confirm": "删除文件夹和邮件",
"placeholder_folder_name": "文件夹名称",
"create": "创建",
"rename_confirm": "重命名"
@@ -2270,7 +2316,8 @@
"search": "聚焦搜索框",
"help": "显示快捷方式",
"refresh": "刷新邮件",
"select_all": "全选"
"select_all": "全选",
"global_search": "全局搜索"
},
"threads": {
"expand_collapse": "展开/折叠会话"
@@ -2721,7 +2768,10 @@
"any_month": "任意月份",
"has_email": "有邮箱",
"has_phone": "有电话",
"has_photo": "有照片"
"has_photo": "有照片",
"sort_by": "排序方式",
"sort_first_name": "名",
"sort_last_name": "姓"
},
"open_categories": "打开分类"
},
@@ -2758,6 +2808,10 @@
"duplicate": "复制",
"today_header": "今天",
"tomorrow_header": "明天",
"agenda_show_earlier": "显示更早的日程",
"agenda_loading": "加载中…",
"agenda_range_start": "显示 {date} 起的日程",
"agenda_range_end": "显示至 {date} 的日程",
"export_ics": "导出为 .ics",
"copy_title": "复制标题",
"copy_link": "复制会议链接",
@@ -2793,6 +2847,14 @@
"color": "颜色"
},
"participants": {
"availability": {
"title": "空闲状态",
"free": "空闲",
"busy": "忙碌",
"tentative": "暂定",
"unknown": "不是此服务器上的用户",
"checking": "正在检查…"
},
"title": "参加者",
"add": "添加参与者",
"organizer": "组织者",
@@ -2867,7 +2929,10 @@
"all_events": "所有活动",
"cancel": "取消",
"save": "保存",
"delete": "删除"
"delete": "删除",
"rsvp_title": "回复重复事件",
"rsvp_description": "这是一个重复事件。您的回复适用于哪些事件?",
"respond": "回复"
},
"alerts": {
"title": "提醒",
@@ -2905,6 +2970,8 @@
"show_time_in_month_view_desc": "在月历视图中显示事件时间",
"show_week_numbers": "显示周数",
"show_week_numbers_desc": "在迷你日历中显示周数",
"calendar_free_scroll": "自由滚动",
"calendar_free_scroll_desc": "连续滚动浏览月、周和日,而不是一次只显示一个时段",
"enable_tasks": "启用任务",
"enable_tasks_desc": "在日历中显示任务视图以管理待办事项",
"show_tasks_on_calendar": "在日历上显示任务",
@@ -2916,6 +2983,9 @@
"hover_preview_delay_1s": "1秒延迟",
"hover_preview_delay_2s": "2秒延迟",
"hover_preview_off": "关闭",
"organizer_identity": "以此身份组织邀请",
"organizer_identity_desc": "邀请他人参加活动时用作组织者的日历地址。",
"organizer_identity_failed": "无法更改组织者地址",
"show_birthday_calendar": "联系人生日日历",
"show_birthday_calendar_desc": "显示包含联系人生日的虚拟日历"
},
@@ -2947,6 +3017,9 @@
"alert_now": "现在开始",
"alert_in_minutes": "{count} 分钟后",
"invitation_sent": "已发送邀请",
"invitations_denied_title": "无法发送邀请",
"invitations_denied": "服务器拒绝发送邀请:{reason}。是否不发送邀请直接保存活动?",
"save_without_invitations": "不发送邀请并保存",
"rsvp_updated": "回复已更新",
"rsvp_error": "无法更新回复",
"event_duplicated": "活动已复制",
@@ -2975,7 +3048,7 @@
"supported_formats": "支持 iCalendar (.ics) 文件",
"url_description": "输入外部 iCalendar (.ics) 源的 URL 以导入事件。",
"url_placeholder": "https://example.com/calendar.ics",
"url_hint": "支持 CalDAV 和 iCalendar (.ics) URL",
"url_hint": "支持 iCalendar (.ics) URL",
"fetch": "获取",
"invalid_url": "请输入有效的 URL",
"url_fetch_failed": "无法从 URL 获取日历",
@@ -3164,6 +3237,8 @@
"has_attachment": "附件",
"date_after": "后",
"date_before": "前",
"size_min": "大于 (KB)",
"size_max": "小于 (KB)",
"starred": "加星标",
"unread": "未读",
"read": "已读",
@@ -3205,6 +3280,10 @@
"hint": "点击左侧的任意邮件即可开始,或参加下方的导览。"
},
"files": {
"office_edit": "编辑文档",
"office_loading": "正在打开文档编辑器…",
"office_error": "无法打开文档编辑器。",
"office_close": "关闭编辑器",
"open_in_new_tab": "在新标签页中打开",
"title": "文件",
"search_placeholder": "搜索文件...",
@@ -3217,6 +3296,8 @@
"rename": "重命名",
"rename_title": "重命名",
"new_name": "新名称",
"name_forbidden_chars": "服务器不允许名称中包含这些字符:{chars}",
"name_reserved": "此名称已被服务器保留。",
"delete": "删除",
"delete_confirm_title": "删除资源",
"delete_confirm_message": "您确定要删除\"{name}\"吗?此操作无法撤消。",
@@ -3451,6 +3532,25 @@
"unified_mailbox": {
"search_unavailable": "统一视图中无法使用搜索"
},
"calendar_event_notifications": {
"someone": "有人",
"invited": "{name} 邀请您参加“{title}”",
"updated": "{name} 更新了“{title}”",
"cancelled": "{name} 取消了“{title}”",
"untitled": "无标题活动"
},
"share_notifications": {
"someone": "有人",
"shared": "{name} 与您共享了{kind}“{object}”",
"changed": "{name} 更改了您对{kind}“{object}”的访问权限",
"revoked": "{name} 移除了您对{kind}“{object}”的访问权限",
"object": {
"folder": "文件夹",
"calendar": "日历",
"address_book": "通讯录",
"files": "文件目录"
}
},
"sharing": {
"title": "共享「{name}」",
"description": "向此服务器上的其他用户或群组授予访问权限。更改会立即生效。",
@@ -3501,5 +3601,45 @@
"copy_event": "复制日程链接",
"copied": "链接已复制",
"copy_failed": "无法复制链接"
},
"plugins": {
"oauth_callback": {
"completing": "正在完成授权…",
"completing_message": "正在建立连接,请稍候。",
"received_title": "已收到授权",
"received_message": "您可以返回发起连接的标签页。",
"failed_title": "授权失败",
"denied": "授权被拒绝。",
"failed_detail": "授权失败:{error}",
"missing_params": "缺少授权码或状态。",
"delivery_failed": "无法将授权结果传递到应用标签页。请关闭此窗口并重试。"
}
},
"global_search": {
"title": "全局搜索",
"placeholder": "搜索邮件、联系人、日历、文件...",
"scope_label": "搜索范围",
"account_label": "账户",
"scope_all": "全部",
"scope_mail": "邮件",
"scope_contacts": "联系人",
"scope_calendar": "日历",
"scope_files": "文件",
"all_accounts": "所有账户",
"show_all": "显示全部结果",
"no_results": "未找到“{query}”的结果",
"server_hint": "完整单词在服务器上搜索;部分单词匹配已加载的内容",
"searching": "搜索中...",
"account_error": "无法搜索 {account}",
"recent_searches": "最近搜索",
"recurring": "重复",
"no_subject": "(no subject)",
"empty_hint": "在所有账户中搜索邮件、联系人、日程和文件。可尝试 from:、subject:、has:attachment、after:2026-01-01。",
"flat_by_date": "按日期平铺",
"grouped": "分组",
"load_more": "加载更多",
"preview_empty": "选择一个结果以预览",
"open_full": "打开",
"view_label": "视图"
}
}
+20
View File
@@ -0,0 +1,20 @@
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
// babel.config.js rewrites lucide-react-native imports to per-icon modules
// under dist/ (scripts/babel-plugin-lucide-imports.js). The package's
// `exports` map only lists its barrel, so resolve those paths as plain files
// instead of letting Metro warn about every one of them.
const LUCIDE_DEEP_IMPORT = 'lucide-react-native/dist/';
const upstreamResolveRequest = config.resolver.resolveRequest;
config.resolver.resolveRequest = (context, moduleName, platform) => {
const resolve = upstreamResolveRequest ?? context.resolveRequest;
if (moduleName.startsWith(LUCIDE_DEEP_IMPORT)) {
return resolve({ ...context, unstable_enablePackageExports: false }, moduleName, platform);
}
return resolve(context, moduleName, platform);
};
module.exports = config;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bulwark-mobile",
"version": "0.1.59",
"version": "0.1.63",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bulwark-mobile",
"version": "0.1.59",
"version": "0.1.63",
"license": "AGPL-3.0-only",
"dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bulwark-mobile",
"version": "0.1.59",
"version": "0.1.63",
"license": "AGPL-3.0-only",
"main": "index.ts",
"scripts": {
+91
View File
@@ -0,0 +1,91 @@
// Babel plugin: rewrite named imports from the `lucide-react-native` barrel to
// the per-icon modules they re-export, so the bundle only contains (and the
// app only evaluates) the icons it uses. The barrel re-exports all ~1,700
// icons, and Metro has no tree shaking, so one `import { Mail }` used to pull
// in and run every icon at startup.
//
// import { Mail, Trash2 as Bin } from 'lucide-react-native';
// becomes
// import Mail from 'lucide-react-native/dist/esm/icons/mail.js';
// import Bin from 'lucide-react-native/dist/esm/icons/trash-2.js';
//
// The name -> file table is read from the package's own ESM barrel, so icon
// aliases (`MailIcon`, `LucideMail`, renamed icons) resolve exactly as they do
// through the barrel. metro.config.js resolves these deep paths, which the
// package's `exports` map does not list.
const fs = require('node:fs');
const path = require('node:path');
const PACKAGE = 'lucide-react-native';
const ESM_DIR = 'dist/esm';
/** Map every name the barrel exports to `{ source, imported }`. */
function loadExportMap() {
// `main` is dist/cjs/lucide-react-native.js; the ESM tree sits next to it.
const pkgDir = path.resolve(path.dirname(require.resolve(PACKAGE)), '..', '..');
const barrel = fs.readFileSync(path.join(pkgDir, ESM_DIR, `${PACKAGE}.js`), 'utf8');
const map = new Map();
for (const m of barrel.matchAll(/export\s*\{([^}]*)\}\s*from\s*'\.\/([^']+)'/g)) {
const source = `${PACKAGE}/${ESM_DIR}/${m[2]}`;
for (const spec of m[1].split(',')) {
const [imported, exported = imported] = spec.trim().split(/\s+as\s+/);
if (imported) map.set(exported, { source, imported });
}
}
if (map.size === 0) {
throw new Error(`${PACKAGE}: could not read the export list from its ESM barrel`);
}
return map;
}
let exportMap = null;
function getExportMap() {
if (!exportMap) exportMap = loadExportMap();
return exportMap;
}
// A value-less binding (only used as a type) is dropped by the TypeScript
// transform anyway; treat it the same way instead of failing the build.
function isOnlyUsedAsType(importPath, localName) {
const binding = importPath.scope.getBinding(localName);
if (!binding) return true;
return binding.referencePaths.every((ref) => Boolean(ref.findParent((p) => p.isTSType())));
}
function lucideImportsPlugin({ types: t }) {
return {
name: 'lucide-direct-imports',
visitor: {
ImportDeclaration(importPath) {
const { node } = importPath;
if (node.source.value !== PACKAGE || node.importKind === 'type') return;
// Side-effect, default and namespace imports keep using the barrel.
if (node.specifiers.length === 0) return;
if (!node.specifiers.every((s) => t.isImportSpecifier(s))) return;
const map = getExportMap();
const replacements = [];
for (const spec of node.specifiers) {
if (spec.importKind === 'type') continue;
const name = t.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
const target = map.get(name);
if (!target) {
if (isOnlyUsedAsType(importPath, spec.local.name)) continue;
throw importPath.buildCodeFrameError(`${PACKAGE} has no export named "${name}"`);
}
const local = t.identifier(spec.local.name);
const specifier = target.imported === 'default'
? t.importDefaultSpecifier(local)
: t.importSpecifier(local, t.identifier(target.imported));
replacements.push(t.importDeclaration([specifier], t.stringLiteral(target.source)));
}
if (replacements.length === 0) importPath.remove();
else importPath.replaceWithMultiple(replacements);
},
},
};
}
module.exports = lucideImportsPlugin;
module.exports.loadExportMap = loadExportMap;
module.exports.PACKAGE = PACKAGE;
+32 -5
View File
@@ -14,6 +14,18 @@ const git = (...args) =>
const gitInherit = (...args) =>
execFileSync('git', args, { cwd: root, stdio: 'inherit' });
// Pre-flight: the release is created with the GitHub CLI as the last step.
// Check it is installed and logged in BEFORE anything is committed, tagged or
// pushed - otherwise a missing login leaves a pushed tag with no release (and
// no release workflow run), which is exactly what a re-run cannot repair.
try {
execFileSync('gh', ['auth', 'status'], { cwd: root, stdio: 'ignore' });
} catch {
console.error('GitHub CLI is not available or not logged in.');
console.error('Run `gh auth login` (or set GH_TOKEN), then re-run `npm run bump`.');
process.exit(1);
}
const dirty = git('status', '--porcelain');
if (dirty) {
console.error('Working tree is not clean. Commit or stash changes before bumping.');
@@ -30,6 +42,15 @@ if (parts.length !== 3 || parts.some(Number.isNaN)) {
parts[2] += 1;
const next = parts.join('.');
// android/app/build.gradle derives versionCode as
// major * 10000000 + minor * 10000 + patch. Past these limits it would stop
// increasing, and Android refuses an update with a lower versionCode.
if (parts[1] > 999 || parts[2] > 9999) {
console.error(`${next} would break the Android versionCode: minor must stay below 1000 and patch below 10000.`);
console.error('Raise the minor or major version in VERSION by hand and commit it, then bump again.');
process.exit(1);
}
const existingTags = new Set(git('tag', '--list').split(/\r?\n/).filter(Boolean));
if (existingTags.has(next)) {
console.error(`Tag ${next} already exists.`);
@@ -54,10 +75,16 @@ gitInherit('commit', '-m', `chore: update version number to ${next}`);
gitInherit('tag', '-a', next, '-m', next);
gitInherit('push', '--follow-tags');
execFileSync(
'gh',
['release', 'create', next, '--title', next, '--generate-notes'],
{ cwd: root, stdio: 'inherit' },
);
try {
execFileSync(
'gh',
['release', 'create', next, '--title', next, '--generate-notes'],
{ cwd: root, stdio: 'inherit' },
);
} catch {
console.error(`The tag ${next} is pushed but the GitHub release could not be created.`);
console.error(`Fix the GitHub CLI login and run: gh release create ${next} --title ${next} --generate-notes`);
process.exit(1);
}
console.log(`Released ${next}`);
+6 -4
View File
@@ -61,13 +61,15 @@ const overlayFlat = flatten(overlay);
// t('key', 'fallback') / t('key', "fallback") / t('key', `fallback`) - the
// fallback may span lines; keep it simple: single-quoted or double-quoted,
// non-nested strings.
const CALL_RE = /\bt\(\s*'([^']+)'\s*,\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")/g;
const KEY_ONLY_RE = /\bt\(\s*'([^']+)'/g;
// non-nested strings. `tr` is the same function where `t` is taken (a
// template or timer variable).
const CALL_RE = /\btr?\(\s*'([^']+)'\s*,\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")/g;
const KEY_ONLY_RE = /\btr?\(\s*'([^']+)'/g;
const added = [];
const missingNoFallback = [];
for (const file of walk(SRC)) {
// App.tsx (the tab bar and loading screen) sits next to src/.
for (const file of [...walk(SRC), join(ROOT, 'App.tsx')]) {
const text = readFileSync(file, 'utf8');
const withFallback = new Map();
for (const m of text.matchAll(CALL_RE)) {
+68
View File
@@ -0,0 +1,68 @@
// List the RN-only overlay keys (locales/rn/en.json) a language has not
// translated in locales/rn/<lang>.json yet. Run from the RN repo root:
//
// node scripts/rn-missing-translations.mjs count per language
// node scripts/rn-missing-translations.mjs de the missing keys with their
// English text, as nested JSON
// node scripts/rn-missing-translations.mjs --prune
// drop translated keys English
// no longer has
//
// Keys the vendored webmail catalog of that language already carries are not
// listed: they are translated there.
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const RN = join(ROOT, 'locales', 'rn');
function flatten(obj, prefix = '', out = new Map()) {
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === 'object' && !Array.isArray(v)) flatten(v, key, out);
else out.set(key, v);
}
return out;
}
function setDeep(obj, key, value) {
const parts = key.split('.');
let cur = obj;
for (const part of parts.slice(0, -1)) cur = cur[part] ??= {};
cur[parts.at(-1)] = value;
}
const readJson = (path) => JSON.parse(readFileSync(path, 'utf8'));
function missingFor(lang, en) {
const done = flatten(readJson(join(RN, `${lang}.json`)));
const webmail = flatten(readJson(join(ROOT, 'locales', lang, 'common.json')));
return [...en].filter(([key]) => !done.has(key) && !webmail.has(key));
}
const en = flatten(readJson(join(RN, 'en.json')));
const lang = process.argv[2];
const langs = readdirSync(RN).filter((f) => f.endsWith('.json') && f !== 'en.json').map((f) => f.slice(0, -5));
if (lang === '--prune') {
for (const l of langs.sort()) {
const path = join(RN, `${l}.json`);
const kept = {};
let dropped = 0;
for (const [key, value] of flatten(readJson(path))) {
if (en.has(key)) setDeep(kept, key, value);
else dropped++;
}
if (dropped) {
writeFileSync(path, `${JSON.stringify(kept, null, 2)}\n`);
console.log(`${l.padEnd(6)} dropped ${dropped}`);
}
}
} else if (lang) {
const out = {};
for (const [key, value] of missingFor(lang, en)) setDeep(out, key, value);
console.log(JSON.stringify(out, null, 2));
} else {
for (const l of langs.sort()) console.log(`${l.padEnd(6)} ${missingFor(l, en).length} of ${en.size} missing`);
}
+60 -1
View File
@@ -14,8 +14,13 @@ const uploadTask = {
cancelAsync: vi.fn(async () => undefined),
};
const createUploadTask = vi.fn((..._args: unknown[]) => uploadTask);
const copyAsync = vi.fn(async (_options: { from: string; to: string }) => undefined);
const deleteAsync = vi.fn(async (_uri: string, _options?: unknown) => undefined);
vi.mock('expo-file-system/legacy', () => ({
cacheDirectory: 'file:///cache/',
createUploadTask: (...args: unknown[]) => createUploadTask(...args),
copyAsync: (options: { from: string; to: string }) => copyAsync(options),
deleteAsync: (uri: string, options?: unknown) => deleteAsync(uri, options),
FileSystemUploadType: { BINARY_CONTENT: 0, MULTIPART: 1 },
}));
@@ -35,7 +40,7 @@ vi.mock('../jmap-client', () => ({
},
}));
import { getDownloadUrl, uploadBlob } from '../blob';
import { getDownloadUrl, isStaleUploadCopy, uploadBlob } from '../blob';
beforeEach(() => {
vi.clearAllMocks();
@@ -125,4 +130,58 @@ describe('uploadBlob', () => {
).rejects.toMatchObject({ name: 'AbortError' });
expect(createUploadTask).not.toHaveBeenCalled();
});
it('uploads files on disk directly without copying them', async () => {
uploadTask.uploadAsync.mockResolvedValueOnce({ status: 200, body: JSON.stringify({ blobId: 'b' }) });
await uploadBlob('file:///tmp/a.pdf', 'application/pdf');
expect(copyAsync).not.toHaveBeenCalled();
expect(deleteAsync).not.toHaveBeenCalled();
});
it('copies a shared content:// URI into the cache, uploads the copy and deletes it', async () => {
uploadTask.uploadAsync.mockResolvedValueOnce({ status: 200, body: JSON.stringify({ blobId: 'blob-3' }) });
const shared = 'content://media/external/images/media/1000000027';
const result = await uploadBlob(shared, 'image/png');
expect(result.blobId).toBe('blob-3');
const { from, to } = copyAsync.mock.calls[0][0];
expect(from).toBe(shared);
expect(to).toMatch(/^file:\/\/\/cache\/upload-/);
expect(createUploadTask.mock.calls[0][1]).toBe(to);
expect(deleteAsync).toHaveBeenCalledWith(to, { idempotent: true });
});
it('deletes the cache copy when the upload fails', async () => {
uploadTask.uploadAsync.mockResolvedValueOnce({ status: 500, body: 'boom' });
await expect(uploadBlob('content://com.example.provider/doc/7', 'application/pdf')).rejects.toThrow(/500/);
const { to } = copyAsync.mock.calls[0][0];
expect(deleteAsync).toHaveBeenCalledWith(to, { idempotent: true });
});
it('does not upload when the shared file cannot be copied', async () => {
copyAsync.mockRejectedValueOnce(new Error('Permission Denial'));
await expect(uploadBlob('content://com.example.provider/doc/8', 'image/jpeg')).rejects.toThrow(/Permission Denial/);
expect(createUploadTask).not.toHaveBeenCalled();
});
});
describe('isStaleUploadCopy', () => {
it('flags upload copies an earlier run of the app left behind', () => {
expect(isStaleUploadCopy('upload-1700000000000-3')).toBe(true);
});
it("leaves this run's copies alone, since they may still be uploading", async () => {
uploadTask.uploadAsync.mockResolvedValueOnce({ status: 200, body: JSON.stringify({ blobId: 'b' }) });
await uploadBlob('content://com.example.provider/doc/9', 'image/png');
const name = copyAsync.mock.calls[0][0].to.replace('file:///cache/', '');
expect(isStaleUploadCopy(name)).toBe(false);
});
it('ignores other cache files', () => {
expect(isStaleUploadCopy('upload-notes.txt')).toBe(false);
expect(isStaleUploadCopy('bulwark-exports')).toBe(false);
expect(isStaleUploadCopy('x-upload-1700000000000-3')).toBe(false);
});
});

Some files were not shown because too many files have changed in this diff Show More