diff options
| -rw-r--r-- | AGENTS.md | 76 | ||||
| -rw-r--r-- | CHANGELOG.md | 25 | ||||
| -rw-r--r-- | README.md | 53 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 55 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-09-24-calendar-design.md | 20 | ||||
| -rw-r--r-- | src/repeatrule.cpp | 7 | ||||
| -rw-r--r-- | src/repeatrule.h | 8 | ||||
| -rw-r--r-- | tests/test_calendarstore.cpp | 20 | ||||
| -rw-r--r-- | translations/qtmaildir_it_IT.ts | 524 |
9 files changed, 765 insertions, 23 deletions
@@ -79,6 +79,13 @@ ComposeWindow (its own top-level window, one per message being written) ├ MessageSender (QProcess, the per-account send_command on stdin) └ SendDialog (the undo countdown) DraftStore (autosave to the drafts folder) +CalendarWindow (its own top-level window, opened from MainWindow) + ├ toolbar: collection combo · ‹ Today › · month combo · year spin · Month|Agenda · + New + ├ QStackedWidget: MonthView | AgendaView ← read QList<Occurrence> only + ├ EventPane: read-only details ⇄ edit form + └ CalendarStore (the only libical user) CalendarWriter (atomic write) + MonthLayout (pure geometry) CalendarSync (QProcess) RepeatRule + ComposeContext (a struct: what a Reply or Forward inherits) ComposeContextBuilder (namespace: fills one, and picks the account) CardLayout (pure geometry, no painting) @@ -95,20 +102,24 @@ those types have ever existed, and looking for them wastes a search. The widget classes that do exist are `MessageView`, `ThreadListView`, `TagStrip`, `TagDialog`, `MessageDetailsDialog`, `PendingChangesDialog`, `RowStyleDelegate`, `CardDelegate`, `ComposeWindow`, `SendDialog`, -`ThreadDashboard` and `BusyIndicator`; `TagChip` is a namespace of -painting helpers, not a widget, `SearchTerm` is a namespace of query builders, -and `ThreadCidMap`, `CardLayout`, `SearchOffer`, `HeaderRow`, `ThreadDigest` -and `PendingChangeRow` are structs. +`ThreadDashboard`, `BusyIndicator`, `CalendarWindow` and `EventPane`; `TagChip` +is a namespace of painting helpers, not a widget, `SearchTerm` is a namespace of +query builders, and `ThreadCidMap`, `CardLayout`, `SearchOffer`, `HeaderRow`, +`ThreadDigest` and `PendingChangeRow` are structs. `SubjectDelegate` existed until item 53 and is gone. **The compose units are mostly NAMESPACES, and the same warning applies to them.** `MarkdownRenderer`, `MarkdownFormat`, `MessageBuilder`, -`ComposeContextBuilder`, `DraftStore` and `MaildirName` are namespaces of free -functions over values, deliberately, so the markdown, the MIME assembly and -the account-picking are all testable without a widget. `MessageSender` IS a -QObject, because it owns a `QProcess`. There is no `FormatToolbar` class: the -composer's formatting row is built inline in `ComposeWindow` and asks -`MarkdownFormat` what each button does to the selection. +`ComposeContextBuilder`, `DraftStore`, `MaildirName`, `CalendarStore` and +`CalendarWriter` are namespaces of free functions over values, deliberately, so +the markdown, the MIME assembly, the account-picking, the iCalendar work and +the atomic write are all testable without a widget. `MessageSender` IS a +QObject, because it owns a `QProcess`, and so is `CalendarSync`. There is no +`FormatToolbar` class: the composer's formatting row is built inline in +`ComposeWindow` and asks `MarkdownFormat` what each button does to the +selection. `MonthLayout` is the calendar's counterpart to `CardLayout` and, like +it, a class that computes every rect with no painter and no widget, so the 6×7 +grid is tested as a function call. **`MessageDetailsDialog` was a `QPlainTextEdit` inside `MessageView` until item 85.** It is rows now so each value can carry its own context menu, and its @@ -1012,6 +1023,51 @@ Nothing here licenses dropping a test that CAN fail. The data-safety properties are exactly the measurable kind, and items 87, 105 and 176 are each a case where the missing test was the whole problem. +## Calendar + +The calendar is a second application inside this one, and its traps are its own. +Each was learned while building item 206; the spec is +`docs/superpowers/specs/2026-09-24-calendar-design.md`. + +**libical is confined to `calendarstore.cpp` behind `icalraii.h`, the nmraii +rule.** C handles are owned by RAII aliases and no `icalcomponent*` crosses out +of that file; everything the rest of the tree sees is the plain value structs in +`src/caltypes.h`. `calendarstore.h` includes no libical header, so a file that +merely calls the store cannot leak a handle. + +**Expansion runs in the event's own zone, not in UTC.** A weekly 10:00 +`Europe/Rome` meeting is 10:00 on both sides of the DST change, so its UTC time +moves: 08:00Z before the last Sunday of October, 09:00Z after. Expanding in UTC +would keep 08:00Z and draw the second occurrence at 09:00 local, an hour wrong +for half the year. `Expansion runs in the event's own zone, and converts to +local after` is the rule; the test asserts the hour in Rome, not the UTC time. + +**An all-day `DTEND;VALUE=DATE` is exclusive.** A one-day event on the 24th ends +at the start of the 25th, so a window beginning on the 25th must not show it. +The side pane shows the inclusive last day and converts back on save; do not +grow an off-by-one by treating the stored end as the day to display. + +**120 of the user's files name a `TZID` without embedding the `VTIMEZONE` the +RFC requires.** The `TZID` resolves through `QTimeZone` as an IANA name, and an +edit must write the `TZID` PARAMETER back rather than converting to UTC or +floating: `DTSTART;TZID=Europe/Rome:...` stays what it was. Converting would +silently move an event for every client that later reads it. + +**The calendar's `Delete`, `Ctrl+Z`, `PgUp`/`PgDn` and `Home` shortcuts are +scoped to the views with `Qt::WidgetWithChildrenShortcut`.** They are window +shortcuts otherwise, and a bare `Delete` or `Ctrl+Z` then fires while the user +is typing in the edit form's Title field, deleting the event or undoing the +calendar instead of editing text. `EventPane` and the stacked views are the +scopes; Escape is scoped to the pane. + +**`calendar_sync_command` is read through a `QStringList` join.** With no quote +around the value, QSettings splits a comma-holding string into a `QStringList` +whose `toString()` is EMPTY, so `vdirsyncer sync calendars,other` would reach +the runner as nothing at all. The loader joins the list back with commas; +`calendar_sync_delay_ms` is read as a string for the same reason, since a +`toString().toInt()` on a list value is also empty and silently keeps the +default. + ## Web view security The most security-sensitive area: a browser engine pointed at input from strangers. Do not diff --git a/CHANGELOG.md b/CHANGELOG.md index cb58e2c..a1b6b92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,37 @@ point at which they are stable. ### Added +- **A calendar window.** **View > Calendar** opens a month grid, with an Agenda + view one toggle away and a side pane that shows the selected event and turns + into the edit form. Events can be created, edited and deleted, each undoable + with `Ctrl+Z`, and the pane says when an event was organised by someone else, + which is read-only. It reads and writes the vdirsyncer vdir directly, so khal + is not needed; names and colours come from the collections' own `displayname` + and `color` files. The feature is off until `calendars_dir` names the vdir. - **Contact completion.** Point `contacts_dir` under `[general]` at a directory of vCards, vdirsyncer's `~/.local/share/vdirsyncer/contacts/` being the usual one, and the composer's To, Cc and Bcc fields and the query bar's `from:` and `to:` complete against it. The store is read-only and loaded once at startup; omit the key and the feature is off. +### Upgrading + +**The calendar is off until `calendars_dir` is set.** No existing config key +changes meaning and nothing breaks by doing nothing: without the key the +calendar action is simply absent and no vdir is read. To turn it on, point it +at your vdir root and, if new events should land in one collection rather than +the first, name that collection's directory in `default_calendar`. + +**A vdirsyncer cron line should take the same lock the calendar uses.** The +default `calendar_sync_command` is prefixed with +`flock -w 60 /tmp/vdirsyncer.lock`, so a save in the calendar and a cron run do +not sync at once. Add the same prefix to your cron line and append +`&& vdirsyncer metasync` to keep the collections' names and colours current: + +```cron +*/30 * * * * flock -w 60 /tmp/vdirsyncer.lock vdirsyncer sync calendars && vdirsyncer metasync +``` + ## [0.29.0] - 2026-09-14 Spam is now a folder you can act on: Mark spam moves the file into a @@ -487,6 +487,59 @@ Omit the key and the feature is off, exactly as if it did not exist. A path that is set and missing is reported at startup, since there you asked for something and are not getting it. +## Calendar + +**View > Calendar**, or the calendar button on the toolbar, opens the calendar +in its own window. It is a month grid by default, with an **Agenda** view one +toggle away, and a side pane that shows the selected event and becomes the edit +form when you press **Edit** or **+ New**. Creating, editing and deleting are +all undoable with `Ctrl+Z`, so a mistake costs a keystroke rather than a +confirmation dialog. The grid and the agenda are read-only views over one list +of events; every change is written from the side pane. + +The window reads and writes a **vdirsyncer vdir**, a directory of `.ics` files. +It needs no khal: the same files khal reads are read here directly, so khal can +be removed with nothing missing. vdirsyncer syncs that vdir against your CalDAV +server; qtmaildir itself speaks no network protocol here any more than it does +for mail, and it runs `calendar_sync_command` after a write the way it runs +your mail sync command after a tag change. + +Names and colours are not configured here. Each collection's name comes from its +own `displayname` file and its colour from its `color` file, falling back to the +directory name and a colour derived from it when those are absent. Those files +are what `vdirsyncer metasync` fills in when the calendar pair carries a +`metadata` key: + +```ini +[pair calendars] +metadata = ["color", "displayname"] +``` + +**Events organised by someone else are read-only.** An invitation puts another +person's address in `ORGANIZER`; Edit and Delete are absent for it and the pane +says why. Events with no organiser, or organised by one of your own account +addresses, are yours to change. Accepting and refusing invitations is not part +of this yet. + +| Key | Default | Meaning | +|---|---|---| +| `calendars_dir` | unset (off) | The vdir root, e.g. `~/.local/share/calendars/`. | +| `default_calendar` | first collection | Directory name new events go to. | +| `calendar_sync_command` | `flock -w 60 /tmp/vdirsyncer.lock vdirsyncer sync calendars` | Run after writes; empty disables. | +| `calendar_sync_delay_ms` | `2000` | Delay before it runs. | + +With `calendars_dir` unset the feature is off: the View menu entry is not there +and nothing is read. Point it at the vdir and the action appears. + +`calendar_sync_command` takes the same `flock` vdirsyncer uses, so a save here +and a cron run cannot sync at once. If vdirsyncer already runs from cron, +prefix that line with the same lock and append `&& vdirsyncer metasync`, or the +two jobs race and the names and colours go stale: + +```cron +*/30 * * * * flock -w 60 /tmp/vdirsyncer.lock vdirsyncer sync calendars && vdirsyncer metasync +``` + ## The query bar The bar at the top takes a notmuch query and shows the matching threads. diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 8334ddf..84df4e6 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -142,10 +142,12 @@ taking that too literally. | 71 | A toolbar action does not sync, so the edit sits until the next cron run | workflow | S | **done** 2026-08-11; 2s default, `auto_sync_delay_ms` | | 72 | No khard/khal integration | workflow | ? | **split 2026-09-18** into 204 (recipient completion), 205 (contact editing), 206 (calendar window) and 207 (invitations), after the user named what they want. Nothing is planned under this number any more; it stays as the index entry the notes' single line maps to. The edge-tts reminder half is out of scope: its own project | | 204 | No recipient completion, in the composer or the query bar | workflow | S | **done** 2026-09-24, shipped, built via subagent-driven-development, `3303c78..7454096`. `ContactStore` over the vdirsyncer vCard 3.0 directory feeds one shared `QCompleter` for the composer's To/Cc/Bcc and the query bar's `from:`/`to:`. Hand-tested and confirmed. One review fix (control-character/RFC 5322 sanitisation of `FN`) landed and was re-reviewed clean. Section in the closed file | -| 205 | Contacts can be read but not edited from the app | workflow | M | open, 2026-09-18, from 72's split, **asked for by the user** while settling 204. Writing a vdir, so it shares the write-during-sync question with 206 and the fold/escape rules with 204's parser. Needs a brainstorm of its own | -| 206 | No calendar: events cannot be viewed, added or edited | workflow | L | open, 2026-09-18, from 72's split, **asked for by the user** in place of khal. Its own TOP-LEVEL WINDOW and **libical**, both settled by the user. Spec-and-branch scale, not a Tuesday pickup. Blocks 207 | +| 205 | Contacts can be read but not edited from the app | workflow | M | open, 2026-09-18, from 72's split, **asked for by the user** while settling 204. Writing a vdir, so it shares the write-during-sync question with 206 and the fold/escape rules with 204's parser. Its write-during-sync question is answered by 206's spec, § Writing: the same writer and stale check serve a vCard. Needs a brainstorm of its own | +| 206 | No calendar: events cannot be viewed, added or edited | workflow | L | open, **built on branch calendar** 2026-09-24, awaiting hand test; spec specs/2026-09-24-calendar-design.md, plan plans/2026-09-24-calendar.md | | 207 | An invitation cannot be accepted, refused or sent | workflow | M | open, 2026-09-18, from 72's split, **asked for by the user**: "as it is today I don't have a way of accepting an invitation." `text/calendar` in the pane, an iMIP reply through the existing `send_command`. **Blocked on 206**, which owns the calendar it writes to | | 208 | No way to add a new correspondent to the contacts | workflow | ? | open, 2026-09-18, **asked for by the user** while settling 204: an "add to contacts" button on mail from someone not in the store. Writes a vcard, so it lands on 205's writer rather than 204's reader. **Needs a brainstorm when picked up** | +| 209 | The calendar has no Week view | presentation | M | open, from 206's brainstorm. The occurrence list is the seam: a new painter over CalendarItem, no store change | +| 210 | A repeating event cannot be edited "this and following" | workflow | S-M | open, from 206's brainstorm. Splits a series into two files, divides EXDATEs and overrides, shifts moved RECURRENCE-IDs, converts COUNT; see the spec's Out of scope | | 73 | This backlog is past four thousand lines | maintenance | S | **done** 2026-08-13; 5056 lines to 578, closed sections moved to `2026-08-03-post-0.1.0-usability-closed.md` | | 74 | "Searching..." keeps claiming a query is running while rows are already arriving | feedback | XS | **done** 2026-08-15, unreleased. The status-bar half only: the bar now counts threads per batch. The cold-cache delay itself was measured in 2026-08-11 and is not fixable here | | 75 | The tagging rules window forgets its size and its column widths | persistence | S | **done** 2026-08-13, shipped in 0.17.0. The window-kind question is left open, see the closed-items file | @@ -1729,3 +1731,52 @@ senders or a `Reply-To` that disagrees with `From`. anything, and it is being written to a file that syncs to a DAV server and to every other device. Escaping is the vCard writer's job (205's), but the DECISION to write unvalidated header text into a synced store is this item's. + +## 209. The calendar has no Week view + +**Observed.** Not a user symptom: a decision taken while settling 206. The spec +(`specs/2026-09-24-calendar-design.md`, Decisions 2 and Out of scope) ships +**Month** (the default) and **Agenda** and defers **Week** rather than dropping +it. The user asked for a calendar and named the views in that order. + +**Cause.** Not a defect. A view is real UI with no counterpart elsewhere in this +tree, so the two that were built were the two that were specified; Week was +deferred to keep the branch to one spec. + +**Approach.** The seam is already in place. `CalendarStore::occurrences()` +returns one `QList<Occurrence>` and every view paints `CalendarItem` over it, so +a Week view is a new painter and a seven-column geometry unit beside +`MonthLayout`, with no change to the store, the parser or the writer. The +existing MonthView/AgendaView toggle becomes a three-way one, and the same +`‹ Today ›` and month/year controls drive it. + +**Constraints.** It must read the same occurrence list, so the DST rule holds +for free only if it, too, expands in the event's own zone (AGENTS.md's Calendar +section). Nothing about a Week view justifies a second expansion path or a +store change; if it seems to need one, the approximation is wrong, not the +seam. + +## 210. A repeating event cannot be edited "this and following" + +**Observed.** Not a user symptom: a decision taken while settling 206. The +spec's Decisions 5 and Out of scope record that an edit offers **This +occurrence** and **All** only, and that "this and following" is deferred. It is +the scope every other calendar client offers, so a user will reach for it. + +**Cause.** Not a defect. The control and `CalendarStore::applyEdit` implement +two scopes; the third is a different and larger edit than either. + +**Approach.** Splitting a series into two files, as the spec's Out of scope +describes: divide the `EXDATE`s and the `RECURRENCE-ID` overrides between the +earlier and later halves, shift any moved `RECURRENCE-ID` when the time changes, +and convert a `COUNT` into an `UNTIL` on the first half and a fresh `DTSTART` on +the second. `applyEdit`'s discipline still applies: the properties the form +does not own survive on both halves. + +**Constraints.** The undo stack holds one command per file, so an edit that +produces two files is two commands or one command holding two paths; either way +both halves must move together on undo or a half-split series is left behind. +The split runs through `CalendarWriter`, so it inherits the stale check: a +server-normalised source must refuse rather than divide a file it does not +recognise. See the spec's Out of scope for the full list of what the split has +to divide. diff --git a/docs/superpowers/specs/2026-09-24-calendar-design.md b/docs/superpowers/specs/2026-09-24-calendar-design.md index e33767e..72358b2 100644 --- a/docs/superpowers/specs/2026-09-24-calendar-design.md +++ b/docs/superpowers/specs/2026-09-24-calendar-design.md @@ -1,7 +1,7 @@ # A calendar window: view, add, edit and delete events Date: 2026-09-24 -Status: approved, not yet implemented +Status: approved, implemented on branch calendar Backlog: item 206 (blocks 207; answers 205's write-during-sync question) ## Problem @@ -201,9 +201,9 @@ fourth Thursday"), as Thunderbird does. event is reloaded, the form stays open holding the user's values, and the status bar says the event changed on disk and needs checking before saving again. Nothing is overwritten blind. -2. **Atomic write.** Write `.<name>.tmp` in the same directory and `rename()` - it into place. vdirsyncer lists only `*.ics`, so it never sees the - temporary. +2. **Atomic write.** `QSaveFile` writes a temporary in the same directory and + `rename()`s it into place on `commit()`, so vdirsyncer, which lists only + `*.ics`, never sees a partial file. 3. Every edit sets `DTSTAMP` and `LAST-MODIFIED` and increments `SEQUENCE`. A **new** event is `<uuid>.ics` in `default_calendar` or the collection chosen @@ -307,6 +307,14 @@ Edit and View holding every action; `Ctrl+N` New, `Ctrl+E` Edit, `Delete`, `Ctrl+Z` / `Ctrl+Shift+Z`, `PgUp` / `PgDn` for the month, `Home` for today, `Esc` to cancel an edit. Window shortcuts, not `KeyMap` entries. +**Those that are not safe as window shortcuts are scoped to the views with +`Qt::WidgetWithChildrenShortcut`.** A bare `Delete`, `Ctrl+Z`, `PgUp`/`PgDn` or +`Home` would otherwise fire while the cursor is in the edit form's Title field, +deleting the event or moving the month instead of editing text. `Delete`, +`PgUp`/`PgDn` and `Home` sit on the stacked views; Undo and Redo sit there too; +Escape is scoped to `EventPane`. `Ctrl+N`, `Ctrl+E` and `Ctrl+W` stay window +shortcuts, since no text field wants them. + **i18n.** Every string in `tr()`; the Italian `.ts` refreshed, `lrelease` reporting 0 unfinished. @@ -316,7 +324,7 @@ All in `[general]`, read without the `general/` prefix per AGENTS.md: | Key | Default | Meaning | |---|---|---| -| `calendars_dir` | `~/.local/share/calendars/` | The vdir root, tilde-expanded like `contacts_dir`. Empty turns the feature off: no action, no error. | +| `calendars_dir` | unset (off) | The vdir root, tilde-expanded like `contacts_dir`. Empty turns the feature off: no action, no error. | | `default_calendar` | first collection by name | Directory name of the collection new events go to. | | `calendar_sync_command` | `flock -w 60 /tmp/vdirsyncer.lock vdirsyncer sync calendars` | Run after writes. Empty disables. | | `calendar_sync_delay_ms` | `2000` | Debounce before the sync runs. | @@ -336,7 +344,7 @@ addresses, never copied from the live vdir. yields only the window; `COUNT` and `UNTIL`; monthly `BYDAY=-1FR`; yearly `BYMONTH`+`BYDAY`. `RepeatRule` round-trips every row of the table, detects a custom rule, keeps `WKST`. `applyEdit` keeps `VALARM`, `ATTENDEE` and - `X-*` byte for byte, increments `SEQUENCE`, creates an override, adds an + `X-*` value for value, increments `SEQUENCE`, creates an override, adds an `EXDATE`. `newEvent` embeds its `VTIMEZONE`. `isEditable` for an organiser who is the user, someone else, nobody, and for a read-only collection. - **`test_calendarwriter`**, in a temporary directory: the atomic write; the diff --git a/src/repeatrule.cpp b/src/repeatrule.cpp index b9b9539..95a0ec3 100644 --- a/src/repeatrule.cpp +++ b/src/repeatrule.cpp @@ -18,7 +18,6 @@ #include "repeatrule.h" -#include <QCoreApplication> #include <QLocale> #include <QMap> @@ -58,13 +57,11 @@ const char *const kOrdinals[] = { QT_TRANSLATE_NOOP("RepeatRule", "fourth"), }; -QString tr(const char *text) { return QCoreApplication::translate("RepeatRule", text); } - QString ordinalWord(int ordinal) { if (ordinal == -1) - return tr("last"); - return (ordinal >= 1 && ordinal <= 4) ? tr(kOrdinals[ordinal - 1]) : QString(); + return RepeatRule::tr("last"); + return (ordinal >= 1 && ordinal <= 4) ? RepeatRule::tr(kOrdinals[ordinal - 1]) : QString(); } } // namespace diff --git a/src/repeatrule.h b/src/repeatrule.h index 59dfceb..f686197 100644 --- a/src/repeatrule.h +++ b/src/repeatrule.h @@ -18,6 +18,7 @@ #pragma once +#include <QCoreApplication> #include <QDate> #include <QList> #include <QString> @@ -29,6 +30,13 @@ /// is what lets every row of the spec's repeat table be tested as a string. struct RepeatRule { + // describe() formats the rule in words, and lupdate needs the class's own + // context to extract them (AGENTS.md: a class needs this macro, an array + // needs QT_TRANSLATE_NOOP on the literal). The macro ends in a private: + // section, so the explicit public: restores the fields below. + Q_DECLARE_TR_FUNCTIONS(RepeatRule) +public: + enum class Freq { None, Daily, Weekly, Monthly, Yearly }; /// Monthly and Yearly only: on a day of the month, or on the Nth weekday. enum class By { MonthDay, Weekday }; diff --git a/tests/test_calendarstore.cpp b/tests/test_calendarstore.cpp index e492bfb..524304e 100644 --- a/tests/test_calendarstore.cpp +++ b/tests/test_calendarstore.cpp @@ -158,6 +158,8 @@ private slots: void editingTheSameOccurrenceAgainReplacesItsOverride(); void deletingOneOccurrenceAddsAnExdateAndDropsItsOverride(); void deletingOneOccurrenceKeepsExistingExdates(); + + void theLiveVdirLoadsCleanly(); }; void TestCalendarStore::parsesAUtcEvent() @@ -680,5 +682,23 @@ void TestCalendarStore::deletingOneOccurrenceKeepsExistingExdates() QCOMPARE(CalendarStore::occurrences({ e }, rome(21, 0), rome(26, 0)).size(), 3); } +void TestCalendarStore::theLiveVdirLoadsCleanly() +{ + // Opt-in, READ-ONLY: never writes. Run by hand before handing the build + // over: QTMAILDIR_LIVE_CALENDARS=~/.local/share/calendars ./test_calendarstore + const QString dir = qEnvironmentVariable("QTMAILDIR_LIVE_CALENDARS"); + if (dir.isEmpty()) + QSKIP("QTMAILDIR_LIVE_CALENDARS not set"); + const LoadResult r = CalendarStore::load(dir); + qInfo("collections=%lld events=%lld unparsable=%d unknownZones=%d", + qlonglong(r.collections.size()), qlonglong(r.events.size()), + r.unparsable, r.unknownZones); + QCOMPARE(r.unparsable, 0); + const QList<Occurrence> year = CalendarStore::occurrences( + r.events, QDateTime(QDate(2026, 1, 1), QTime(0, 0)), + QDateTime(QDate(2027, 1, 1), QTime(0, 0))); + qInfo("occurrences in 2026=%lld", qlonglong(year.size())); +} + QTEST_MAIN(TestCalendarStore) #include "test_calendarstore.moc" diff --git a/translations/qtmaildir_it_IT.ts b/translations/qtmaildir_it_IT.ts index 371be09..6012a90 100644 --- a/translations/qtmaildir_it_IT.ts +++ b/translations/qtmaildir_it_IT.ts @@ -2,6 +2,218 @@ <!DOCTYPE TS> <TS version="2.1" language="it_IT"> <context> + <name>AgendaView</name> + <message> + <source>all day</source> + <translation>tutto il giorno</translation> + </message> + <message> + <source>%1 - %2</source> + <translation>%1 - %2</translation> + </message> +</context> +<context> + <name>CalendarWindow</name> + <message> + <source>Calendar</source> + <translation>Calendario</translation> + </message> + <message> + <source>Syncing calendars...</source> + <translation>Sincronizzazione calendari...</translation> + </message> + <message> + <source>Calendar sync failed</source> + <translation>Sincronizzazione calendari fallita</translation> + </message> + <message> + <source>Calendar sync failed.</source> + <translation>Sincronizzazione dei calendari fallita.</translation> + </message> + <message> + <source>Previous month</source> + <translation>Mese precedente</translation> + </message> + <message> + <source>Today</source> + <translation>Oggi</translation> + </message> + <message> + <source>Go to today</source> + <translation>Vai a oggi</translation> + </message> + <message> + <source>Next month</source> + <translation>Mese successivo</translation> + </message> + <message> + <source>Month</source> + <translation>Mese</translation> + </message> + <message> + <source>Agenda</source> + <translation>Agenda</translation> + </message> + <message> + <source>&New event</source> + <translation>&Nuovo evento</translation> + </message> + <message> + <source>&Close</source> + <translation>&Chiudi</translation> + </message> + <message> + <source>&Edit event</source> + <translation>&Modifica evento</translation> + </message> + <message> + <source>&Delete event</source> + <translation>&Elimina evento</translation> + </message> + <message> + <source>Cancel &editing</source> + <translation>Annulla &modifica</translation> + </message> + <message> + <source>&Undo</source> + <translation>&Annulla</translation> + </message> + <message> + <source>&Redo</source> + <translation>&Ripeti</translation> + </message> + <message> + <source>&Previous month</source> + <translation>Mese &precedente</translation> + </message> + <message> + <source>&Next month</source> + <translation>Mese &successivo</translation> + </message> + <message> + <source>&Today</source> + <translation>&Oggi</translation> + </message> + <message> + <source>&Month</source> + <translation>&Mese</translation> + </message> + <message> + <source>&Agenda</source> + <translation>&Agenda</translation> + </message> + <message> + <source>&File</source> + <translation>&File</translation> + </message> + <message> + <source>&Edit</source> + <translation>&Modifica</translation> + </message> + <message> + <source>&View</source> + <translation>&Visualizza</translation> + </message> + <message> + <source>All calendars</source> + <translation>Tutti i calendari</translation> + </message> + <message numerus="yes"> + <source>%n file(s) could not be read</source> + <translation> + <numerusform>%n file non è stato letto</numerusform> + <numerusform>%n file non sono stati letti</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n event(s) in an unknown time zone, shown in local time</source> + <translation> + <numerusform>%n evento in un fuso orario sconosciuto, mostrato in ora locale</numerusform> + <numerusform>%n eventi in un fuso orario sconosciuto, mostrati in ora locale</numerusform> + </translation> + </message> + <message> + <source>(no title)</source> + <translation>(nessun titolo)</translation> + </message> + <message> + <source>Save or cancel the event being edited first.</source> + <translation>Salva o annulla prima l'evento in modifica.</translation> + </message> + <message> + <source>Edit a repeating event</source> + <translation>Modifica un evento ricorrente</translation> + </message> + <message> + <source>Edit only this occurrence, or every occurrence?</source> + <translation>Modificare solo questa occorrenza o tutte le occorrenze?</translation> + </message> + <message> + <source>This occurrence</source> + <translation>Questa occorrenza</translation> + </message> + <message> + <source>All occurrences</source> + <translation>Tutte le occorrenze</translation> + </message> + <message> + <source>Kept as set elsewhere</source> + <translation>Mantenuto come impostato altrove</translation> + </message> + <message> + <source>None</source> + <translation>Nessuno</translation> + </message> + <message> + <source>New event</source> + <translation>Nuovo evento</translation> + </message> + <message> + <source>Edit event</source> + <translation>Modifica evento</translation> + </message> + <message> + <source>Delete a repeating event</source> + <translation>Elimina un evento ricorrente</translation> + </message> + <message> + <source>Delete only this occurrence, or every occurrence?</source> + <translation>Eliminare solo questa occorrenza o tutte le occorrenze?</translation> + </message> + <message> + <source>Could not remove this occurrence.</source> + <translation>Impossibile rimuovere questa occorrenza.</translation> + </message> + <message> + <source>Delete event</source> + <translation>Elimina evento</translation> + </message> + <message> + <source>The event changed on disk, probably from a sync. Check it and try again.</source> + <translation>L'evento è cambiato su disco, probabilmente per una sincronizzazione. Controllalo e riprova.</translation> + </message> + <message> + <source>Could not write the calendar: %1</source> + <translation>Impossibile scrivere nel calendario: %1</translation> + </message> + <message> + <source>Calendars synced.</source> + <translation>Calendari sincronizzati.</translation> + </message> + <message> + <source>The server kept a different version of: %1</source> + <translation>Il server ha mantenuto una versione diversa di: %1</translation> + </message> + <message> + <source>Unsaved event</source> + <translation>Evento non salvato</translation> + </message> + <message> + <source>Save the changes to this event?</source> + <translation>Salvare le modifiche a questo evento?</translation> + </message> +</context> +<context> <name>CardLayout</name> <message> <source>message</source> @@ -249,6 +461,14 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> <translation>La directory dei contatti '%1' non esiste; il completamento dei contatti non troverà alcun contatto.</translation> </message> <message> + <source>Calendars directory '%1' does not exist; the calendar will be empty.</source> + <translation>La cartella dei calendari '%1' non esiste; il calendario sarà vuoto.</translation> + </message> + <message> + <source>Calendar sync delay '%1' is not a number; using the default.</source> + <translation>Il ritardo di sincronizzazione del calendario '%1' non è un numero; uso il valore predefinito.</translation> + </message> + <message> <source>Language '%1' is not a locale name; using the system language. Expected something like 'it' or 'it_IT'.</source> <translation>'%1' non è un nome di locale; verrà usata la lingua di sistema. Atteso qualcosa come 'it' o 'it_IT'.</translation> </message> @@ -398,6 +618,201 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> </message> </context> <context> + <name>EventPane</name> + <message> + <source>When</source> + <translation>Quando</translation> + </message> + <message> + <source>Repeats</source> + <translation>Ripete</translation> + </message> + <message> + <source>Calendar</source> + <translation>Calendario</translation> + </message> + <message> + <source>Location</source> + <translation>Luogo</translation> + </message> + <message> + <source>Notes</source> + <translation>Note</translation> + </message> + <message> + <source>Reminder</source> + <translation>Promemoria</translation> + </message> + <message> + <source>People</source> + <translation>Persone</translation> + </message> + <message> + <source>&Edit</source> + <translation>&Modifica</translation> + </message> + <message> + <source>&Delete</source> + <translation>&Elimina</translation> + </message> + <message> + <source>All day</source> + <translation>Tutto il giorno</translation> + </message> + <message> + <source>Title</source> + <translation>Titolo</translation> + </message> + <message> + <source>Starts</source> + <translation>Inizia</translation> + </message> + <message> + <source>Ends</source> + <translation>Finisce</translation> + </message> + <message> + <source>Repeat</source> + <translation>Ripetizione</translation> + </message> + <message> + <source>&Save</source> + <translation>&Salva</translation> + </message> + <message> + <source>Cancel</source> + <translation>Annulla</translation> + </message> + <message> + <source>Does not repeat</source> + <translation>Non si ripete</translation> + </message> + <message> + <source>Daily</source> + <translation>Giornaliero</translation> + </message> + <message> + <source>Weekly</source> + <translation>Settimanale</translation> + </message> + <message> + <source>Monthly</source> + <translation>Mensile</translation> + </message> + <message> + <source>Yearly</source> + <translation>Annuale</translation> + </message> + <message> + <source>every </source> + <translation>ogni </translation> + </message> + <message> + <source>on day</source> + <translation>il giorno</translation> + </message> + <message> + <source>on the</source> + <translation>il</translation> + </message> + <message> + <source>first</source> + <translation>primo</translation> + </message> + <message> + <source>second</source> + <translation>secondo</translation> + </message> + <message> + <source>third</source> + <translation>terzo</translation> + </message> + <message> + <source>fourth</source> + <translation>quarto</translation> + </message> + <message> + <source>last</source> + <translation>ultimo</translation> + </message> + <message> + <source>of %1</source> + <translation>di %1</translation> + </message> + <message> + <source>forever</source> + <translation>per sempre</translation> + </message> + <message> + <source>until</source> + <translation>fino al</translation> + </message> + <message> + <source>for</source> + <translation>per</translation> + </message> + <message> + <source> times</source> + <translation> volte</translation> + </message> + <message> + <source>Custom rule, kept as is</source> + <translation>Regola personalizzata, mantenuta com'è</translation> + </message> + <message> + <source>Custom rule</source> + <translation>Regola personalizzata</translation> + </message> + <message> + <source>(no title)</source> + <translation>(nessun titolo)</translation> + </message> + <message> + <source>%1 to %2, all day</source> + <translation>%1 - %2, tutto il giorno</translation> + </message> + <message> + <source>%1, all day</source> + <translation>%1, tutto il giorno</translation> + </message> + <message> + <source>%1, %2 - %3</source> + <translation>%1, %2 - %3</translation> + </message> + <message> + <source>Yes, kept as set elsewhere</source> + <translation>Sì, mantenuto come impostato altrove</translation> + </message> + <message> + <source>None</source> + <translation>Nessuno</translation> + </message> + <message> + <source>Organiser: %1</source> + <translation>Organizzatore: %1</translation> + </message> + <message> + <source>%1 (%2)</source> + <translation>%1 (%2)</translation> + </message> + <message> + <source>no reply</source> + <translation>nessuna risposta</translation> + </message> + <message> + <source>Read-only calendar.</source> + <translation>Calendario in sola lettura.</translation> + </message> + <message> + <source>Organised by %1: read-only.</source> + <translation>Organizzato da %1: sola lettura.</translation> + </message> + <message> + <source>Attendees have not been notified of changes.</source> + <translation>I partecipanti non sono stati avvisati delle modifiche.</translation> + </message> +</context> +<context> <name>HtmlBuilder</name> <message> <source>local mail, tagged and searched</source> @@ -904,6 +1319,10 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> <translation>&Regole di etichettatura...</translation> </message> <message> + <source>No calendar is configured: set calendars_dir under [general].</source> + <translation>Nessun calendario configurato: imposta calendars_dir in [general].</translation> + </message> + <message> <source>&Check for stranded spam</source> <translation>Cerca spam &fuori posto</translation> </message> @@ -940,6 +1359,14 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> <translation>Modifica le regole che etichettano la posta in arrivo</translation> </message> <message> + <source>&Calendar</source> + <translation>&Calendario</translation> + </message> + <message> + <source>Open the calendar</source> + <translation>Apri il calendario</translation> + </message> + <message> <source>&Save query...</source> <translation>&Salva ricerca...</translation> </message> @@ -1747,6 +2174,16 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> </message> </context> <context> + <name>MonthView</name> + <message numerus="yes"> + <source>+%n more</source> + <translation> + <numerusform>+%n altro</numerusform> + <numerusform>+%n altri</numerusform> + </translation> + </message> +</context> +<context> <name>PendingChangesDialog</name> <message> <source>Unsynced changes</source> @@ -1887,6 +2324,93 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> </message> </context> <context> + <name>RepeatRule</name> + <message> + <source>first</source> + <translation>il primo</translation> + </message> + <message> + <source>second</source> + <translation>il secondo</translation> + </message> + <message> + <source>third</source> + <translation>il terzo</translation> + </message> + <message> + <source>fourth</source> + <translation>il quarto</translation> + </message> + <message> + <source>last</source> + <translation>l'ultimo</translation> + </message> + <message> + <source>Custom rule</source> + <translation>Regola personalizzata</translation> + </message> + <message> + <source>Does not repeat</source> + <translation>Non si ripete</translation> + </message> + <message> + <source>Daily</source> + <translation>Giornaliero</translation> + </message> + <message> + <source>Every %1 days</source> + <translation>Ogni %1 giorni</translation> + </message> + <message> + <source>Weekly</source> + <translation>Settimanale</translation> + </message> + <message> + <source>Every %1 weeks</source> + <translation>Ogni %1 settimane</translation> + </message> + <message> + <source>, on %1</source> + <translation>, il %1</translation> + </message> + <message> + <source>Monthly</source> + <translation>Mensile</translation> + </message> + <message> + <source>Every %1 months</source> + <translation>Ogni %1 mesi</translation> + </message> + <message> + <source>, on the %1 %2</source> + <translation>, %1 %2</translation> + </message> + <message> + <source>, on day %1</source> + <translation>, il giorno %1</translation> + </message> + <message> + <source>Yearly</source> + <translation>Annuale</translation> + </message> + <message> + <source>Every %1 years</source> + <translation>Ogni %1 anni</translation> + </message> + <message> + <source>, on the %1 %2 of %3</source> + <translation>, %1 %2 di %3</translation> + </message> + <message> + <source>, %1 times</source> + <translation>, %1 volte</translation> + </message> + <message> + <source>, until %1</source> + <translation>, fino al %1</translation> + </message> +</context> +<context> <name>SaveQueryDialog</name> <message> <source>Save query</source> |
