diff options
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 124 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 92 | ||||
| -rw-r--r-- | src/messageview.cpp | 62 | ||||
| -rw-r--r-- | src/messageview.h | 23 | ||||
| -rw-r--r-- | tests/test_messageview.cpp | 93 | ||||
| -rw-r--r-- | translations/qtmaildir_it_IT.ts | 16 |
7 files changed, 324 insertions, 94 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b90c8..d17b46c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ point at which they are stable. ## [Unreleased] +### Added + +- The message pane's right-click menu offers **Select all**. Chromium's own menu + for this pane has never carried it. +- Copying from the message pane now says what was copied. Copy, Copy link + address, Copy image and Copy image address each report in the status bar, + where they expire like every other transient message. + ## [0.26.0] - 2026-08-19 Delete now moves mail into the account's trash folder instead of only tagging diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index ef51b5c..783e4ed 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -6699,3 +6699,127 @@ database directly. **Verified:** clean build with no warnings from any changed file, 24 of 24 suites over three consecutive runs, 211 tests in `test_mainwindow`. Every one of the spec's six testing bullets has a test, each mutation-checked. + +## 115. A copy from the message pane gives no confirmation + +**Observed (user, 2026-08-17):** Copy link address, Copy image address and Copy +image all work, and none of them says so. The user asked for "a small +'link copied' transient that appears and disappears after a few seconds". + +**Cause:** not a defect, unbuilt. These are Chromium's own menu entries and it +does not report success; nothing in this application is listening to them. + +**Copy image was thought to be broken and is not**; see item 116 in the +closed-items file, which is worth reading for how a stale clipboard reading +produced a confident wrong cause. + +All three work. Copy image was briefly filed as broken (item 116) and is not: +the pixels reach the clipboard correctly, and the reading that said otherwise +was taken off a stale clipboard. So this item covers the confirmation for Copy +link address, Copy image address and Copy image alike. + +**Approach.** `MessageView::statusMessage` already exists and the status bar +already expires its messages (item 33), so this is one connection per action and +no new widget, no new timer. `QWebEnginePage::action()` gives each entry's +QAction; connect `triggered` and emit the appropriate string. + +**Constraints.** + +- The message must name what was copied. "Copied" alone is worse than nothing + when three entries sit next to each other in the same menu. +- Do not build a floating overlay for this. The status bar is where this + application reports transient results, and a second mechanism for the same + job is the kind of thing item 45 recorded when two Sync buttons disagreed. +- Depends on nothing; can be built alongside 114 since both touch the same menu. + +**Size: XS.** + +**Built 2026-08-19.** Four entries report through the pane's existing +`statusMessage`: Copy, Copy link address, Copy image and Copy image address, +each naming what it copied rather than saying "Copied". Connected to the PAGE's +own QActions in the constructor, which are the same instances the standard +context menu holds, so the report follows the entry wherever it is triggered +from and no menu of ours is involved. + +That is also what makes this fully testable, unlike item 117 beside it: +triggering the action runs the production path with no context-menu event +needed. The test enables each action first, since Chromium disables a copy entry +when there is nothing of that kind under the cursor and `trigger()` on a +disabled QAction emits nothing at all, which would have left the loop asserting +nothing while looking thorough. Two mutations fail it: a duplicated message, and +one entry left unwired. + +The strings are `QT_TR_NOOP` inside an array, which CLAUDE.md warns extracts +NOTHING at file scope. Verified rather than assumed: `lupdate` found all four +under the `MessageView` context with no warning, because the array sits inside +a member function where the class context exists. + +## 117. The message pane offers no Select all + +**Observed (user, 2026-08-17):** right-clicking a body selection offers Copy and +the search entries, and no Select all. Noticed while hand-testing item 100. + +**NOT caused by item 100, and this was verified rather than argued.** The user +ran a build with `src/messageview.cpp` and `src/messageview.h` reverted to HEAD, +so `removeBrowserActions()` did not exist, and reported the same menu: Copy and +the search entries only. Chromium's standard menu for this pane has never +carried Select all. + +**Three wrong theories preceded that measurement**, which is the part worth +recording, because each was plausible and each cost a round trip: + +1. *The separator sweep removed it.* Disproved with a standalone program + reproducing the sweep against a realistic menu: it drops the leading + separator and keeps every action. +2. *Chromium omits it when there is no selection.* Killed by the user, who had a + selection at the time. +3. *A probe will show what the real menu holds.* `createStandardContextMenu()` + returns NULL outside an actual context-menu event, on the offscreen platform + and on a real display alike, so two probe attempts measured nothing. + +The lesson is the one item 100 had already written down and the agent did not +follow: **a menu built by hand proves nothing about the menu Chromium builds.** +`theBodyMenuDropsTheBrowsersOwnActions` constructs its own QMenu, which is right +for testing the filter and useless for testing what is offered. That test now +says so, and deliberately does NOT assert on SelectAll, since a passing +assertion there would read as a guarantee the code does not make. + +**Approach.** Add it explicitly rather than hoping Chromium supplies it. The +action already exists as `QWebEnginePage::SelectAll` and works; only the menu +entry is missing. + +- `menu->addAction(page->action(QWebEnginePage::SelectAll))` in + `showBodyContextMenu`, placed beside Copy rather than appended after the + search entries. +- Worth considering a `Ctrl+A` binding for the pane at the same time, though + note the pane is not the only focusable widget and the query bar has its own + claim on that key. Check `KeyMap::defaultBindings()` before adding one. + +**Constraints.** + +- **A test for this cannot use a hand-built menu.** That is the trap above. The + honest options are asserting the action is in the menu the production code + returns, which needs a real context-menu event, or leaving it to a hand test + and saying so. Do not write a test that constructs a QMenu and calls it + covered. +- Select all selects the rendered body, not the header label, which is a + separate widget with its own selection. That is probably the desired + behaviour but should be looked at rather than assumed. + +**Size: XS.** + +**Built 2026-08-19** as `MessageView::addPaneActions()`, static and taking the +menu, mirroring `removeBrowserActions()` beside it. Two stale comments went with +it: both `showBodyContextMenu()` and the header of `removeBrowserActions()` +claimed Select all was among the entries the standard menu supplied and that the +filter preserved. Neither was true, and either would have sent the next reader +back down the same three wrong theories. + +**The call site is not covered, and the test says so rather than pretending.** +`addPaneActions()` has a test and a mutation dropping the entry fails it. A +mutation deleting the CALL from `showBodyContextMenu()` leaves the whole suite +green, measured, because `createStandardContextMenu()` returns nothing outside a +real context-menu event and the offscreen platform cannot deliver one. The +honest options the entry named were an assertion on the production menu or a +hand test; the first is impossible here, so it is the second, recorded in the +test so nobody adds an assertion that appears to cover it. 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 335b3d3..be52da9 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 @@ -179,9 +179,9 @@ taking that too literally. | 112 | Toggle unread on a whole thread cannot reach "all unread" on a partly-read thread | defect | S | open, found 2026-08-17. A toggle over a UNION has no direction on a mixed thread | | 113 | No way to see a message's HTML source | information | S | open, 2026-08-17. Chromium's own View source cannot work here; needs our own plain-text dialog. Item 100 removed the dead entry, which was an overreach: the user had not asked for it | | 114 | Save image is offered on every image and does nothing | defect | S | open, found 2026-08-17 by right-clicking a real image. No `downloadRequested` handler exists anywhere, so the request is emitted and never answered | -| 115 | A copy from the message pane gives no confirmation | presentation | XS | open, 2026-08-17. Copy link address, Copy image address and Copy image all work, silently. `statusMessage` already exists and already expires | +| 115 | A copy from the message pane gives no confirmation | presentation | XS | **done** 2026-08-19, unreleased. Four entries report, each naming what it copied; connected to the page's own QActions, so the entry is covered wherever it is triggered from | | 116 | Copy image copies markup instead of the image | defect | XS | **dropped** 2026-08-17, same day. NOT A DEFECT: `wl-paste --list-types` run immediately after a copy reports `image/png`, `application/x-qt-image` and 30 more image flavours. The clipboard is correct and Chromium is behaving. The earlier "text only" reading was taken minutes late off a clipboard that had been overwritten, and a whole cause was theorised on it | -| 117 | The message pane offers no Select all | workflow | XS | open, found 2026-08-17. NOT caused by item 100: verified by hand against a build with that filter reverted, and the menu holds Copy and the search entries either way | +| 117 | The message pane offers no Select all | workflow | XS | **done** 2026-08-19, unreleased. `addPaneActions()` supplies it. The call site is NOT covered by a test and cannot be: the production menu needs a real context-menu event. Stated in the test rather than faked | | 118 | No way to empty the trash from inside the app | workflow | S | open, 2026-08-17. **Blocked on 103**, which creates the trash in the first place. Deliberately left out of 103's spec at the user's request rather than squeezed in | | 119 | The unsynced-changes count cannot be opened to see what it counts | information | S | open, 2026-08-19, from the notes. One of the four things it sums carries no message ids at all, so a list cannot be complete without a change to how the count is kept | @@ -717,94 +717,6 @@ for the network to satisfy it. **Size: S.** -## 115. A copy from the message pane gives no confirmation - -**Observed (user, 2026-08-17):** Copy link address, Copy image address and Copy -image all work, and none of them says so. The user asked for "a small -'link copied' transient that appears and disappears after a few seconds". - -**Cause:** not a defect, unbuilt. These are Chromium's own menu entries and it -does not report success; nothing in this application is listening to them. - -**Copy image was thought to be broken and is not**; see item 116 in the -closed-items file, which is worth reading for how a stale clipboard reading -produced a confident wrong cause. - -All three work. Copy image was briefly filed as broken (item 116) and is not: -the pixels reach the clipboard correctly, and the reading that said otherwise -was taken off a stale clipboard. So this item covers the confirmation for Copy -link address, Copy image address and Copy image alike. - -**Approach.** `MessageView::statusMessage` already exists and the status bar -already expires its messages (item 33), so this is one connection per action and -no new widget, no new timer. `QWebEnginePage::action()` gives each entry's -QAction; connect `triggered` and emit the appropriate string. - -**Constraints.** - -- The message must name what was copied. "Copied" alone is worse than nothing - when three entries sit next to each other in the same menu. -- Do not build a floating overlay for this. The status bar is where this - application reports transient results, and a second mechanism for the same - job is the kind of thing item 45 recorded when two Sync buttons disagreed. -- Depends on nothing; can be built alongside 114 since both touch the same menu. - -**Size: XS.** - -## 117. The message pane offers no Select all - -**Observed (user, 2026-08-17):** right-clicking a body selection offers Copy and -the search entries, and no Select all. Noticed while hand-testing item 100. - -**NOT caused by item 100, and this was verified rather than argued.** The user -ran a build with `src/messageview.cpp` and `src/messageview.h` reverted to HEAD, -so `removeBrowserActions()` did not exist, and reported the same menu: Copy and -the search entries only. Chromium's standard menu for this pane has never -carried Select all. - -**Three wrong theories preceded that measurement**, which is the part worth -recording, because each was plausible and each cost a round trip: - -1. *The separator sweep removed it.* Disproved with a standalone program - reproducing the sweep against a realistic menu: it drops the leading - separator and keeps every action. -2. *Chromium omits it when there is no selection.* Killed by the user, who had a - selection at the time. -3. *A probe will show what the real menu holds.* `createStandardContextMenu()` - returns NULL outside an actual context-menu event, on the offscreen platform - and on a real display alike, so two probe attempts measured nothing. - -The lesson is the one item 100 had already written down and the agent did not -follow: **a menu built by hand proves nothing about the menu Chromium builds.** -`theBodyMenuDropsTheBrowsersOwnActions` constructs its own QMenu, which is right -for testing the filter and useless for testing what is offered. That test now -says so, and deliberately does NOT assert on SelectAll, since a passing -assertion there would read as a guarantee the code does not make. - -**Approach.** Add it explicitly rather than hoping Chromium supplies it. The -action already exists as `QWebEnginePage::SelectAll` and works; only the menu -entry is missing. - -- `menu->addAction(page->action(QWebEnginePage::SelectAll))` in - `showBodyContextMenu`, placed beside Copy rather than appended after the - search entries. -- Worth considering a `Ctrl+A` binding for the pane at the same time, though - note the pane is not the only focusable widget and the query bar has its own - claim on that key. Check `KeyMap::defaultBindings()` before adding one. - -**Constraints.** - -- **A test for this cannot use a hand-built menu.** That is the trap above. The - honest options are asserting the action is in the menu the production code - returns, which needs a real context-menu event, or leaving it to a hand test - and saying so. Do not write a test that constructs a QMenu and calls it - covered. -- Select all selects the rendered body, not the header label, which is a - separate widget with its own selection. That is probably the desired - behaviour but should be looked at rather than assumed. - -**Size: XS.** - ## 118. No way to empty the trash from inside the app **Observed (user, 2026-08-17):** raised while reviewing item 103's spec, as diff --git a/src/messageview.cpp b/src/messageview.cpp index 68821d8..2dba6d1 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -158,6 +158,38 @@ MessageView::MessageView(QWidget *parent) connect(m_view, &QWidget::customContextMenuRequested, this, &MessageView::showBodyContextMenu); + // Item 115. Chromium's copy entries all work and none of them says so, so + // the pane reports for them. Connected to the page's own QActions, which + // are the same instances the standard context menu holds, so this covers + // the entry wherever it is triggered from and needs no menu of our own. + // + // Each message names WHAT was copied. "Copied" alone is worse than nothing + // when three of these sit together in one menu. + // + // The status bar rather than a floating overlay, at the item's insistence: + // this is where the application already reports transient results and + // where they already expire (item 33). A second mechanism for one job is + // what item 45 recorded when two Sync buttons disagreed. + static const struct { + QWebEnginePage::WebAction action; + const char *message; + } kCopyReports[] = { + { QWebEnginePage::Copy, QT_TR_NOOP("Copied the selected text") }, + { QWebEnginePage::CopyLinkToClipboard, QT_TR_NOOP("Copied the link address") }, + { QWebEnginePage::CopyImageToClipboard, QT_TR_NOOP("Copied the image") }, + { QWebEnginePage::CopyImageUrlToClipboard, QT_TR_NOOP("Copied the image address") }, + }; + + for (const auto &report : kCopyReports) { + QAction *action = m_view->page()->action(report.action); + if (!action) + continue; + const QString message = tr(report.message); + connect(action, &QAction::triggered, this, [this, message]() { + emit statusMessage(message); + }); + } + // Ctrl+wheel zoom. The filter goes on the application rather than on // m_view: the wheel event is delivered to an internal QQuickWidget the // view creates lazily, so there is no child to filter at this point and a @@ -660,10 +692,31 @@ void MessageView::removeBrowserActions(QMenu *menu, QWebEnginePage *page) menu->removeAction(menu->actions().constLast()); } +void MessageView::addPaneActions(QMenu *menu, QWebEnginePage *page) +{ + if (!menu || !page) + return; + + // Item 117. Added explicitly rather than relied upon: Chromium's standard + // menu for this pane does not offer Select all and never did, measured by + // hand with a selection active and against a build with + // removeBrowserActions() reverted. The filter is not what removed it, so + // relaxing the filter would not bring it back. + // + // The action itself already exists and already works; only the entry was + // missing. + if (QAction *selectAll = page->action(QWebEnginePage::SelectAll)) + menu->addAction(selectAll); +} + void MessageView::showBodyContextMenu(const QPoint &pos) { - // The page's own menu first: copy, select all and the rest stay exactly as - // they were. This adds to that menu rather than replacing it. + // The page's own menu first: Copy and the rest stay exactly as they were. + // This adds to that menu rather than replacing it. + // + // "and select all" used to be in that sentence and was wrong: Chromium's + // menu here has never offered it. Item 117 measured that and addPaneActions() + // supplies it below. QMenu *menu = m_view->createStandardContextMenu(); if (!menu) menu = new QMenu(this); @@ -673,6 +726,11 @@ void MessageView::showBodyContextMenu(const QPoint &pos) // apply here. Item 100. removeBrowserActions(menu, m_view->page()); + // ...plus the ones it needs and Chromium does not supply. Item 117. + // Before the search entries, so it sits with Copy rather than after a + // separator at the bottom. + addPaneActions(menu, m_view->page()); + // selectedText() reads the selection out of the render process with no // script injection. JavaScript is disabled in this profile and stays so. const SearchOffer offer = selectionSearchOffer(m_view->page()->selectedText()); diff --git a/src/messageview.h b/src/messageview.h index df20e40..0b18769 100644 --- a/src/messageview.h +++ b/src/messageview.h @@ -155,8 +155,10 @@ public: /// Item 100. The pane is not a browser: every document arrives through /// setHtml() with a fixed base URL, so Back, Forward, Reload and Save page /// have nothing to act on and the interceptor blocks everything by default - /// anyway. Copy and Select all are the reason the standard menu is used at - /// all, so the menu is filtered, not rebuilt. + /// anyway. Copy and View source are the reason the standard menu is used at + /// all, so the menu is filtered, not rebuilt. Select all is NOT among them: + /// Chromium's menu here has never offered it, which item 117 measured and + /// addPaneActions() supplies. /// /// View source is NOT filtered, though it was at first. It has a real /// document and a real use; item 113 implements it as our own dialog, @@ -170,6 +172,23 @@ public: /// without a rendered document or a shown popup. static void removeBrowserActions(QMenu *menu, QWebEnginePage *page); + /// Adds the entries this pane needs and Chromium's standard menu does not + /// supply: Select all, for now. + /// + /// Item 117. Chromium's menu for this pane has NEVER carried Select all, + /// measured by hand with a selection active and against a build with + /// removeBrowserActions() reverted. Do not assume the standard menu + /// provides it and do not "restore" it by relaxing the filter above, which + /// never removed it. + /// + /// Static and taking the menu for the same reason as removeBrowserActions(): + /// createStandardContextMenu() returns nothing outside a real context-menu + /// event, so the production menu cannot be built in a test at all. A test + /// that hand-builds a QMenu proves what THIS function does and nothing + /// about what Chromium offers, which is the distinction item 117 records + /// after three wrong theories. Keep the two questions separate. + static void addPaneActions(QMenu *menu, QWebEnginePage *page); + /// Tells the pane whether the query bar currently holds anything. /// /// The menus need it to grey out "Exclude from search": excluding from an diff --git a/tests/test_messageview.cpp b/tests/test_messageview.cpp index 87234aa..df41dd1 100644 --- a/tests/test_messageview.cpp +++ b/tests/test_messageview.cpp @@ -19,6 +19,7 @@ #include <QLabel> #include <QMenu> #include <QPushButton> +#include <QSet> #include <QSignalSpy> #include <QWebEnginePage> #include <QWebEngineUrlScheme> @@ -57,6 +58,8 @@ private slots: void headerOffersNothingForAnAbsentField(); void bodySelectionBecomesAQuotedSearch(); void theBodyMenuDropsTheBrowsersOwnActions(); + void theBodyMenuOffersSelectAll(); + void aCopyFromThePaneReportsWhatWasCopied(); void aSearchFromTheDetailsDialogClosesIt(); private: @@ -779,6 +782,96 @@ void TestMessageView::theBodyMenuDropsTheBrowsersOwnActions() QVERIFY(!left.constLast()->isSeparator()); } +void TestMessageView::theBodyMenuOffersSelectAll() +{ + // Item 117. Chromium's standard menu for this pane has NEVER carried + // Select all: measured by hand with a selection active, and against a + // build with removeBrowserActions() reverted, so the filter is not what + // removed it. The pane adds it. + // + // What this test can and cannot prove is the whole point of the item, and + // three wrong theories were bought before it was measured. The production + // menu comes from createStandardContextMenu(), which returns nothing + // outside a real context-menu event, so no test can build it. This + // therefore asserts what addPaneActions() does to a menu handed to it, and + // says NOTHING about what Chromium offers. Those are separate questions; + // conflating them is what item 117 records. + // + // The limit is worth stating precisely, because it is the second half of + // the same trap: this test does NOT cover showBodyContextMenu() CALLING + // addPaneActions(). Measured, a mutation deleting that call leaves the + // whole suite green. Covering it needs a real context-menu event, which the + // offscreen platform cannot deliver, so the call site is a hand test. Do + // not add an assertion here that appears to cover it. + MessageView view; + auto *page = view.findChild<QWebEnginePage *>(); + QVERIFY2(page, "no page, so this test would assert nothing"); + + QMenu menu; + auto *selectAll = page->action(QWebEnginePage::SelectAll); + QVERIFY2(selectAll, "the page offers no SelectAll action at all"); + + // The guard: absent before, so a pass cannot come from the menu already + // holding it or from the action being added twice by something else. + QVERIFY(!menu.actions().contains(selectAll)); + + MessageView::addPaneActions(&menu, page); + + QVERIFY2(menu.actions().contains(selectAll), + "the pane's menu does not offer Select all"); +} + +void TestMessageView::aCopyFromThePaneReportsWhatWasCopied() +{ + // Item 115. Copy link address, Copy image address and Copy image all work + // and none of them said so. Chromium does not report success, so the pane + // listens to its actions and emits the pane's own status message. + // + // Unlike item 117's entry, this IS fully testable: the connections are made + // to the page's own QActions in the constructor, so triggering one runs the + // production path. No context-menu event is involved. + MessageView view; + auto *page = view.findChild<QWebEnginePage *>(); + QVERIFY2(page, "no page, so this test would assert nothing"); + + QSignalSpy spy(&view, &MessageView::statusMessage); + QVERIFY(spy.isValid()); + + // Each entry names WHAT was copied. "Copied" alone is worse than nothing + // when three entries sit together in one menu, which the item states as a + // constraint, so the messages are asserted to differ from each other. + const QList<QWebEnginePage::WebAction> copies = { + QWebEnginePage::Copy, + QWebEnginePage::CopyLinkToClipboard, + QWebEnginePage::CopyImageToClipboard, + QWebEnginePage::CopyImageUrlToClipboard, + }; + + QStringList seen; + for (const QWebEnginePage::WebAction which : copies) { + QAction *action = page->action(which); + QVERIFY2(action, "the page offers no action for a copy entry"); + + // Enabled explicitly. Chromium disables a copy action when there is + // nothing of that kind under the cursor, and trigger() on a disabled + // QAction emits nothing at all, so without this the loop would assert + // nothing while looking thorough. + action->setEnabled(true); + + spy.clear(); + action->trigger(); + + QTRY_VERIFY_WITH_TIMEOUT(spy.count() == 1, 5000); + const QString message = spy.takeFirst().at(0).toString(); + QVERIFY2(!message.isEmpty(), "a copy reported an empty status message"); + seen.append(message); + } + + // Four distinct messages, so no two entries report the same thing. + QCOMPARE(seen.size(), copies.size()); + QCOMPARE(QSet<QString>(seen.cbegin(), seen.cend()).size(), copies.size()); +} + void TestMessageView::aSearchFromTheDetailsDialogClosesIt() { // The dialog is modal. Without closing it, the query runs and the thread diff --git a/translations/qtmaildir_it_IT.ts b/translations/qtmaildir_it_IT.ts index b9515b4..e7a4b49 100644 --- a/translations/qtmaildir_it_IT.ts +++ b/translations/qtmaildir_it_IT.ts @@ -969,6 +969,22 @@ <context> <name>MessageView</name> <message> + <source>Copied the selected text</source> + <translation>Testo selezionato copiato</translation> + </message> + <message> + <source>Copied the link address</source> + <translation>Indirizzo del collegamento copiato</translation> + </message> + <message> + <source>Copied the image</source> + <translation>Immagine copiata</translation> + </message> + <message> + <source>Copied the image address</source> + <translation>Indirizzo dell'immagine copiato</translation> + </message> + <message> <source>Details...</source> <translation>Dettagli...</translation> </message> |
