diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-15 12:37:41 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-15 12:37:41 +0200 |
| commit | 5a3f827a01d1902a0dadc5debb4200138d4af885 (patch) | |
| tree | 5b00207aefccf625a54ac4b928a05561d1a0fff1 | |
| parent | 8e7208dc771bc9344f583442ad58cdd5b4178233 (diff) | |
| download | qtmaildir-5a3f827a01d1902a0dadc5debb4200138d4af885.tar.gz qtmaildir-5a3f827a01d1902a0dadc5debb4200138d4af885.zip | |
feat(i18n): wire translations and ship an Italian one (item 22)
Nothing loaded a translation before this: no QTranslator, no .ts file and
no build rule, so every string was English whatever the locale said. The
language now comes from the environment, LANG=it_IT.UTF-8, and any other
locale runs in English as before.
The audit found that the tr() discipline was largely holding, and found
eight strings that could never be translated into any language. kFields[]
in tagrulesdialog.cpp declared the rule-builder field labels with
QT_TR_NOOP inside an anonymous namespace, where lupdate reports "tr()
cannot be called without context" and extracts nothing, while the use site
calls TagRulesDialog::tr() on them at runtime. From, To, Cc, Subject, Tag,
Folder, Attachment and Date: the whole vocabulary of the rule builder,
absent from every translation file that could ever exist. The source
compiles and reads correctly; only lupdate reveals it.
Q_DECLARE_TR_FUNCTIONS is not the fix for that case, though it is the fix
for a free function calling tr(). Measured against lupdate: a class
carrying the macro beside the array still extracts 0 strings, because the
context must be attached to the literal itself. QT_TRANSLATE_NOOP names it
explicitly and matches the tr() that already reads them, so the use site
needed no change.
Twenty configuration and keybinding warnings were not translatable either.
They are user-facing, reaching the status label and the "Configuration
problems" dialog. Config already had the tr() macro; KeyMap needed it.
Translating the filter labels then broke startup_query, found in hand
testing: a filter's name is a translated label, so `startup_query = Inbox`
matched nothing where the filter shows as "In arrivo". The application
opened a different view and reported the user's own working config as
invalid. Resolution matches the generator as well now, which is stored in
queries.json and identical in every locale; the translated name still
works. The regression test installs a real QTranslator rather than a stub,
since the bug lives in the gap between the stored string and the displayed
one, and it writes a queries.json because the warning it asserts on is
guarded by a non-empty saved-query list: without one the branch never runs
and the test passes against a broken check.
main.cpp's --help and --version stay bare printf, as they run before
QApplication exists and no translator could serve them.
Verified per the backlog's own standard, that lupdate output is the
evidence rather than reading: 355 strings extracted with zero context
warnings, where before there were 327 with eight; lrelease reporting 355
finished and 0 unfinished; the built .qm loaded in a standalone probe
printing "From -> Da" and both Italian plural forms; and the install rule
placing it where main.cpp looks. test_translations guards it and was
mutation checked, failing on an emptied translation and naming the defect
when QT_TRANSLATE_NOOP is reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | CHANGELOG.md | 30 | ||||
| -rw-r--r-- | CLAUDE.md | 49 | ||||
| -rw-r--r-- | CMakeLists.txt | 5 | ||||
| -rw-r--r-- | README.md | 32 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 94 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 44 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-08-15-i18n-design.md | 161 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 25 | ||||
| -rw-r--r-- | src/config.cpp | 72 | ||||
| -rw-r--r-- | src/keymap.cpp | 6 | ||||
| -rw-r--r-- | src/keymap.h | 6 | ||||
| -rw-r--r-- | src/main.cpp | 31 | ||||
| -rw-r--r-- | src/tagrulesdialog.cpp | 25 | ||||
| -rw-r--r-- | tests/CMakeLists.txt | 16 | ||||
| -rw-r--r-- | tests/test_config.cpp | 87 | ||||
| -rw-r--r-- | tests/test_translations.cpp | 226 | ||||
| -rw-r--r-- | translations/qtmaildir_it_IT.ts | 1543 |
18 files changed, 2377 insertions, 78 deletions
@@ -1,3 +1,6 @@ .superpowers/ build/ HANDOFF.md + +# The .ts is tracked; the .qm is generated from it by lrelease. +*.qm diff --git a/CHANGELOG.md b/CHANGELOG.md index c176c6a..07f4a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,36 @@ point at which they are stable. ## [Unreleased] +### Added + +- **An Italian translation, and the machinery to load one.** Nothing read a + translation before this: there was no `QTranslator`, no `.ts` file and no + build rule, so every string was English whatever the locale said. The + language comes from the environment, `LANG=it_IT.UTF-8`, and any other + locale runs in English as before. All 355 strings are translated. +- `ctest -R translations` guards the translation against the two ways it rots: + a new string added without one, and a string `lupdate` cannot see at all. + +### Fixed + +- **Eight labels in the tagging-rules dialog could never be translated**, in + any language. From, To, Cc, Subject, Tag, Folder, Attachment and Date were + declared with `QT_TR_NOOP` inside an anonymous namespace, where `lupdate` + extracts nothing while the dialog's own `tr()` reads them at runtime, so no + translation file ever contained them. The source looked correct and only + `lupdate` revealed it. +- Twenty configuration and keybinding warnings were not translatable. They are + user-facing: they appear in the status bar and the "Configuration problems" + dialog. +- **`startup_query` naming a built-in filter stopped working under a translated + interface**, found in hand testing the Italian build. A filter's name is a + translated label, so `startup_query = Inbox` matched nothing where the filter + is shown as "In arrivo": the application opened a different view and reported + the user's own working configuration as invalid. It now resolves on the + generator as well, which is the same in every language, so a config written + in English keeps working whatever `LANG` says. The translated name is still + accepted. + ## [0.22.0] - 2026-08-15 Follows 0.21.0's built-in filters with the parts that shipped wrong or missing. @@ -241,7 +241,54 @@ and quit on a sync that never happened. that are only ever shown in passing: status bar messages, tooltips, dialog prose, completion descriptions. Query syntax itself is not user-facing text — notmuch keywords like `tag:` and `date:` are wire format and must never be translated, only the prose -describing them. Pre-existing code has not been audited against this rule. +describing them. The tree was audited against this rule by item 22 on +2026-08-15, and an Italian translation ships, so a new string that misses +`tr()` is now a regression rather than pre-existing debt. + +**`tr()` alone does not make a string translatable, and the source cannot tell +you which.** A literal in an ARRAY or any other place with no enclosing class +needs the context named on the literal itself: +`QT_TRANSLATE_NOOP("TheClass", "Text")`. `QT_TR_NOOP` there compiles, reads +correctly, and extracts NOTHING — `lupdate` prints "tr() cannot be called +without context" and skips it, while the use site's `tr()` looks it up at +runtime under a context no `.ts` file contains. That shipped for the eight +rule-builder field labels in `tagrulesdialog.cpp` and made every one of them +permanently untranslatable in any language. + +`Q_DECLARE_TR_FUNCTIONS` is NOT the fix for that case, though it is the fix for +a free FUNCTION calling `tr()` (which is what `querycompleter.cpp` uses it +for). Measured: a class carrying the macro beside the array still extracts 0. +The context must be on the literal. + +**`lupdate` output is the evidence, never reading**, and `ctest -R +translations` encodes it: it fails on a string with no translation and on one +`lupdate` cannot see. Refresh with `lupdate-qt6 src/ -ts +translations/qtmaildir_it_IT.ts -no-obsolete -locations none` after changing +any user-facing string; a clean run reports zero context warnings, and +`lrelease` must report 0 unfinished, since it silently DROPS an unfinished +string and ships it as English inside an otherwise Italian UI. + +A `QTranslator` must live on `main`'s stack: one scoped to a helper function +unloads on return and every string reverts to English with nothing to see. + +**Translating a string that something MATCHES on breaks config in a language +the author never runs.** The built-in filters' names are labels and are +translated; `startup_query` resolved by comparing the config's text against +those names, so `startup_query = Inbox` matched nothing under `LANG=it_IT` +where the filter is called "In arrivo". The application opened the wrong view +AND warned that the user's own working config was invalid. It resolves on the +GENERATOR as well now, which is stored in queries.json and identical in every +locale. Before wrapping a string in `tr()`, ask whether anything compares +against it; if so, match on the wire-format identifier and treat the +translated name as an additional convenience, never as the identity. The +regression test installs a real `QTranslator` rather than a stub, because the +bug lives in the gap between the stored string and the displayed one and only +a real translation opens it. + +A second trap sits under that test and cost a wrong green: the warning it +asserts on is guarded by `!m_savedQueries.isEmpty()`, so a test with no +`queries.json` never reaches the branch and passes against a broken check. It +writes one, and asserts the file loaded before asserting on what it produced. **This application has a sibling, and one file couples them.** `mailctl` (`../mailctl`) is a narrow, agent-safe CLI over the same notmuch index. The two diff --git a/CMakeLists.txt b/CMakeLists.txt index f021b0e..5639322 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,11 @@ if(QTMAILDIR_BUILD_TESTS) endif() find_package(Qt6 6.5 REQUIRED COMPONENTS ${QTMAILDIR_QT_COMPONENTS}) +# Optional, and host tooling rather than a link dependency: lrelease compiles +# the tracked .ts into a .qm. A build without it is English-only rather than +# broken, which is the same outcome as a missing .qm at runtime. +find_package(Qt6 6.5 QUIET COMPONENTS LinguistTools) + # notmuch ships no pkg-config file; locate it by hand. find_path(NOTMUCH_INCLUDE_DIR notmuch.h) find_library(NOTMUCH_LIBRARY NAMES notmuch) @@ -67,6 +67,33 @@ to produce nothing that ships: cmake -S . -B build -DQTMAILDIR_BUILD_TESTS=OFF ``` +### Translations + +qtmaildir ships an Italian translation. The language is taken from the +environment, so there is no configuration key of its own: + +```bash +LANG=it_IT.UTF-8 qtmaildir +``` + +Any other locale runs the application in English, which is also what happens +when the compiled translation is missing. + +`translations/qtmaildir_it_IT.ts` is tracked in git; the `.qm` it compiles to +is generated at build time and is not. Building needs Qt6's `LinguistTools`, +and a build without them is English-only rather than broken. + +Adding a language means copying the `.ts`, translating it, and adding it to +`src/CMakeLists.txt` beside the Italian one. After changing any user-facing +string, refresh the `.ts` so the new string is there to translate: + +```bash +lupdate-qt6 src/ -ts translations/qtmaildir_it_IT.ts -no-obsolete -locations none +``` + +`ctest -R translations` fails on a string that is missing a translation, and on +one that `lupdate` cannot extract at all. + ### Slackware package `assets/slackbuild/` holds a SlackBuild. It follows SBo conventions, with two @@ -277,6 +304,11 @@ would have meant hand-rolling a parser. They now follow the file. filters, so it can name either; yours wins if both carry the same name. A name matching neither falls back to the **Unread** filter. +A built-in filter can be named either by its English name (`Inbox`) or by the +name shown in your own language (`In arrivo`). The English one is the safer +choice: it is the filter's identity rather than its label, so a config written +that way keeps working whatever `LANG` is set to. + ### Sent mail A **Sent** button appears beside the saved queries once at least one account 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 e6c7840..1704a28 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 @@ -5196,3 +5196,97 @@ The test asserts the Run item exists, that it is first, that triggering it reaches the query, and that the edit actions survived beside it. Restoring the old wiring fails it on the first of those and names the Qt behaviour rather than reporting a wrong query string. + +## 22. Translatability audit and i18n wiring + +**Observed (user, 2026-08-04):** "a full check of the codebase and wiring up of +the i18n system." + +**This is a debt `CLAUDE.md` already records.** The rule that every user-facing +string must be wrapped in `tr()` was added while building query completion, and +that file states plainly that "pre-existing code has not been audited against +this rule". This item is that audit, plus the loading machinery which does not +exist at all. + +**Two halves, and they are different sizes.** + +- *The audit.* Every user-visible string in `src/` checked for `tr()`, with the + translation context correct: a string in a free function needs + `Q_DECLARE_TR_FUNCTIONS`, since calling `QObject::tr()` compiles but files it + under the wrong context. `lupdate` output is the evidence here, not reading. +- *The wiring.* Nothing loads a `.qm` file today: there is no `QTranslator` in + `main.cpp`, no `.ts` files in the tree, and no CMake rule to build or install + them. Until that exists, a translated string has nowhere to come from. + +**Constraint:** query syntax is not user-facing text. notmuch keywords such as +`tag:` and `date:` are wire format and must never be translated, only the prose +describing them. The completion vocabulary is exactly this trap: the values are +literal, the descriptions are prose. + +**Verification:** run `lupdate` and read the generated `.ts`. A string that +does not appear there is not translatable, whatever the source looks like. + +**No longer on demand.** The entry said to do this when a defect needs it, not +before (user, 2026-08-04). Item 66 needs it now, so the deliverable is a RED +reproduction of that defect, not fixture wiring on its own. Fixing 66 is +deliberately excluded: it has never been isolated, and designing a fix beside a +hypothesis is how a wrong one gets locked in. + +**Smaller than this entry has read since 2026-08-04.** No hook has to be added +to `MainWindow`. `wireWorker()` (`src/mainwindow.cpp:1440`) already builds the +worker from `m_config.notmuchConfig()`, an ordinary config key, so a test that +writes a `qtmaildir.conf` pointing at the fixture gets a real worker through the +shipping path with nothing in `src/` changed. Mind the `[general]` prefix trap +when writing that file. + +**Done 2026-08-15, unreleased.** Both halves shipped: the audit and the wiring, +plus an Italian translation of all 355 strings. See +`specs/2026-08-15-i18n-design.md`. + +**What the audit found, and it was not what this entry predicted.** The `tr()` +discipline was largely holding: `lupdate` extracted 327 strings across 13 +contexts before any change. The real find was eight strings that could never be +translated in any language. `kFields[]` in `src/tagrulesdialog.cpp` declared the +rule-builder field labels with `QT_TR_NOOP` inside an anonymous namespace, where +`lupdate` reports "tr() cannot be called without context" and extracts NOTHING, +while the use site calls `TagRulesDialog::tr()` on them at runtime. From, To, +Cc, Subject, Tag, Folder, Attachment and Date: the entire vocabulary of the rule +builder, invisible to every translation file that could ever exist. The source +compiles, reads correctly, and only `lupdate` reveals it, exactly as this +entry's verification note said. + +**The fix this entry recommended is the wrong one, measured.** The entry says a +string in a free function needs `Q_DECLARE_TR_FUNCTIONS`, which is true for the +case `querycompleter.cpp` solves and false here: a class carrying that macro +beside the array still extracts 0 strings, because `lupdate` needs the context +attached to the LITERAL, not to a neighbouring class. `QT_TRANSLATE_NOOP("TagRulesDialog", ...)` +is what works, verified extracting 8 of 8, and it names the context the reading +`tr()` already resolves against so the use site needed no change. + +**Twenty warnings were not translatable either**, seventeen in `config.cpp` and +three in `keymap.cpp`, all user-facing through the status label and the +"Configuration problems" modal. `Config` already had the macro; `KeyMap` needed +it. + +**`main.cpp`'s `--help` and `--version` stay bare `printf` deliberately.** They +run before `QApplication` exists, so no translator could serve them. + +**Wiring.** A `QTranslator` on `main`'s stack, which is load-bearing: one scoped +to a helper unloads on return and silently reverts every string. Installed +before `Config` loads, since config warnings are generated at load time and are +among the translated strings. `QLocale()` reads the environment, so +`LANG=it_IT.UTF-8` selects the file with no config key of this project's own, +and a missing `.qm` returns false and runs English. `LinguistTools` is optional +in CMake: a build without it is English-only rather than broken. + +**Verified**, per this entry's own standard that `lupdate` output is the +evidence: 355 strings extracted with zero context warnings (was 327 with eight), +`lrelease` reporting 355 finished and 0 unfinished, and the built `.qm` loaded +in a standalone probe printing `From -> Da`, `&Archive -> &Archivia`, and both +Italian plural forms. `tests/test_translations.cpp` guards it and was mutation +checked twice: it fails on an emptied translation, and it fails naming the +defect when `QT_TRANSLATE_NOOP` is reverted to `QT_TR_NOOP`. + +**Not done:** no language-selection UI, and no second language. `QLocale()` +reading the environment is how a Linux desktop already chooses; add a selector +when there is something to choose between. 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 c02de31..e921e89 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 @@ -86,7 +86,7 @@ taking that too literally. | 19 | No prompt to sync on exit when edits are pending | behavior | S | **done** | | 20 | Thread view does not match the user's mental model | presentation | L | **done** 2026-08-10, as the card list; see 53 | | 21 | Default shortcuts are not sensible enough | discoverability | S | open | -| 22 | Translatability audit and i18n wiring | correctness | M | open | +| 22 | Translatability audit and i18n wiring | correctness | M | **done** 2026-08-15, unreleased; see `specs/2026-08-15-i18n-design.md`. Found eight rule-builder labels that could never be translated in any language, and twenty untranslatable warnings. Ships an Italian translation of all 355 strings | | 23 | No way to save a search query from the UI | workflow | M | **done** 2026-08-13, shipped in 0.18.0; see `specs/2026-08-13-saved-queries-design.md` | | 24 | No right-click actions on the thread list | discoverability | S | **done** | | 25 | No select-all, and bulk actions are undiscoverable | workflow | S | **done** | @@ -194,48 +194,6 @@ that appeared to be bound. See `KeyMap::defaultBindings()` and the query bar claims it back while focused, so a proposal that moves it must not resurrect that bug. -## 22. Translatability audit and i18n wiring - -**Observed (user, 2026-08-04):** "a full check of the codebase and wiring up of -the i18n system." - -**This is a debt `CLAUDE.md` already records.** The rule that every user-facing -string must be wrapped in `tr()` was added while building query completion, and -that file states plainly that "pre-existing code has not been audited against -this rule". This item is that audit, plus the loading machinery which does not -exist at all. - -**Two halves, and they are different sizes.** - -- *The audit.* Every user-visible string in `src/` checked for `tr()`, with the - translation context correct: a string in a free function needs - `Q_DECLARE_TR_FUNCTIONS`, since calling `QObject::tr()` compiles but files it - under the wrong context. `lupdate` output is the evidence here, not reading. -- *The wiring.* Nothing loads a `.qm` file today: there is no `QTranslator` in - `main.cpp`, no `.ts` files in the tree, and no CMake rule to build or install - them. Until that exists, a translated string has nowhere to come from. - -**Constraint:** query syntax is not user-facing text. notmuch keywords such as -`tag:` and `date:` are wire format and must never be translated, only the prose -describing them. The completion vocabulary is exactly this trap: the values are -literal, the descriptions are prose. - -**Verification:** run `lupdate` and read the generated `.ts`. A string that -does not appear there is not translatable, whatever the source looks like. - -**No longer on demand.** The entry said to do this when a defect needs it, not -before (user, 2026-08-04). Item 66 needs it now, so the deliverable is a RED -reproduction of that defect, not fixture wiring on its own. Fixing 66 is -deliberately excluded: it has never been isolated, and designing a fix beside a -hypothesis is how a wrong one gets locked in. - -**Smaller than this entry has read since 2026-08-04.** No hook has to be added -to `MainWindow`. `wireWorker()` (`src/mainwindow.cpp:1440`) already builds the -worker from `m_config.notmuchConfig()`, an ordinary config key, so a test that -writes a `qtmaildir.conf` pointing at the fixture gets a real worker through the -shipping path with nothing in `src/` changed. Mind the `[general]` prefix trap -when writing that file. - ## 40. No live filter over the current view **Observed (user, 2026-08-05):** "search in current view", spelled out as two diff --git a/docs/superpowers/specs/2026-08-15-i18n-design.md b/docs/superpowers/specs/2026-08-15-i18n-design.md new file mode 100644 index 0000000..105dca8 --- /dev/null +++ b/docs/superpowers/specs/2026-08-15-i18n-design.md @@ -0,0 +1,161 @@ +# Translatability audit and i18n wiring (item 22) + +Date: 2026-08-15 +Item: 22, backlog `2026-08-03-post-0.1.0-usability.md` + +## What this is + +Item 22 is two halves of different sizes: an **audit** of every user-facing +string for `tr()` with the correct context, and the **wiring** that does not +exist at all. Nothing loads a `.qm` file today: no `QTranslator` in `main.cpp`, +no `.ts` in the tree, no CMake rule to build or install one. Until that exists +a translated string has nowhere to come from. + +The user's decision, 2026-08-15: **Italian only, fully translated.** Not +machinery-with-empty-strings, and not a representative slice. One language, all +of it, so the machinery is proven by a translation that actually renders. + +## Audit findings + +Measured with `lupdate-qt6 src/ -ts <probe> -no-obsolete`, not by reading. + +**327 source strings across 13 contexts.** The `tr()` discipline `CLAUDE.md` +records is largely holding; this is an audit with three specific defects, not a +rewrite. + +### Defect 1: eight labels are unreachable (the real find) + +`src/tagrulesdialog.cpp:66-73` declares the rule-builder field labels in an +anonymous namespace: + +```cpp +struct FieldEntry { RuleTerm::Field field; const char *label; }; +const FieldEntry kFields[] = { + {RuleTerm::From, QT_TR_NOOP("From")}, + ... +}; +``` + +lupdate reports `tr() cannot be called without context` eight times and **files +nothing**. The use site at `:940` is `row.field->addItem(tr(entry.label), ...)`, +inside `TagRulesDialog`, so at runtime Qt looks up context `TagRulesDialog` for +a string no `.ts` file ever contained. The eight labels are From, To, Cc, +Subject, Tag, Folder, Attachment and Date: the entire vocabulary of the rule +builder. + +This is exactly the trap the backlog entry predicted, and it is invisible +without running `lupdate`. The source looks correct. + +**The fix is NOT `Q_DECLARE_TR_FUNCTIONS`**, which is what `querycompleter.cpp` +uses for its own namespace problem and what this design assumed first. Verified +against `lupdate-qt6` in a standalone file: a class carrying +`Q_DECLARE_TR_FUNCTIONS` beside the array still extracts **0 strings**, because +lupdate needs the context attached to the literal itself, not to a nearby class. + +The mechanism that works, verified extracting 2 of 2 under context +`TagRulesDialog`: + +```cpp +{RuleTerm::From, QT_TRANSLATE_NOOP("TagRulesDialog", "From")}, +``` + +It names the context explicitly and **matches the existing `tr(entry.label)` +call at `:940` without changing it**, since `TagRulesDialog::tr` resolves in +that same context. + +### Defect 2: twenty untranslated warnings + +Seventeen in `src/config.cpp` (`addWarning`/`addProblem` with a bare +`QStringLiteral`) and three in `src/keymap.cpp:268,274,282`. All are +user-facing: `MainWindow` at `:1730` and `:1746` joins +`m_config.warnings() + m_keyMap.warnings()` into the status label and the +"Configuration problems" modal that `main.cpp:106` raises. + +`Config` already has `Q_DECLARE_TR_FUNCTIONS(Config)` at `config.h:162`, so +those seventeen become `tr()` with no other change. `KeyMap` needs the macro +adding. + +### Defect 3: nothing, deliberately + +`main.cpp`'s `--help` and `--version` text stays bare `printf`. It runs before +`QApplication` is constructed, by design, so no translator could be installed +to serve it. Translating it would be a lie about what can happen. + +### Confirmed clean, and must stay untranslated + +Wire format, per the constraint in `CLAUDE.md` and the backlog entry: + +- `keymap.cpp`'s 85 key-sequence literals (`Ctrl+G`, `Alt+Up`). +- `htmlbuilder.cpp`'s markup and `@PLACEHOLDER@` tokens. +- Config keys (`auto_sync_delay_ms`), notmuch query syntax (`tag:`, `from:`). +- `querycompleter.cpp`'s completion **values** are literal; only the + descriptions beside them are prose, and those already use + `VocabularyStrings::tr`. + +## The wiring + +### Load path, verified + +Confirmed in a standalone program against Qt 6.11, printing `load=1` and +`From -> Da`: + +```cpp +QTranslator translator; +if (translator.load(QLocale(), QStringLiteral("qtmaildir"), + QStringLiteral("_"), translationsDir)) + app.installTranslator(&translator); +``` + +Placed in `main.cpp` **after** `QApplication` is constructed and **before** +`Config` is loaded, since config warnings are generated at load time and are +among the strings being translated. + +`QLocale()` default-constructs to the system locale, so `LANG=it_IT.UTF-8` +selects the file with no config key. A missing `.qm` returns false and the app +runs in English, which is the correct failure. + +**The translator must outlive `app.exec()`.** A stack `QTranslator` in `main` +does; one in a helper function does not, and the strings silently revert. + +### Where the .qm lives + +Installed to `share/qtmaildir/translations/`. `main.cpp` looks there via +`QStandardPaths::AppDataLocation`, falling back to a path beside the binary so +a build tree works without installing. + +### CMake + +`find_package(Qt6 REQUIRED COMPONENTS LinguistTools)` — present on this machine +as `/usr/lib64/cmake/Qt6LinguistTools`. `qt_add_translations` handles both +`lupdate` and `lrelease`; the `.ts` file is tracked in git under +`translations/`, the `.qm` is generated and is not. + +The install rule is part of this, not a follow-up. A `.qm` that is built and +never installed reproduces exactly the state item 22 describes. + +## Verification + +Per the backlog entry: **`lupdate` output is the evidence, not reading.** + +1. `lupdate` runs clean. The eight `tr() cannot be called without context` + warnings are gone, and the probe count rises from 327 by the twenty newly + wrapped warnings. +2. The `.ts` contains context `TagRulesDialog` with the eight field labels. This + is the assertion that would have failed before the fix, and the one a reading + of the source cannot make. +3. `lrelease` reports 0 unfinished for `it_IT`. +4. A test asserts the shipped `.ts` has no empty `<translation>` and no + `type="unfinished"`, so a later string added without a translation is caught + by the suite rather than by seeing English in an Italian UI. +5. Hand test, and it belongs to the user: `LANG=it_IT.UTF-8 qtmaildir`, with + the rule-builder field dropdown open, since those eight labels are the + strings this item exists to fix. + +## What this does not do + +- No language-selection UI or config key. `QLocale()` reads the environment, + which is how a Linux desktop already chooses. Add one when a second language + exists and there is something to choose between. +- No second language. The `.ts` machinery makes one a copy of a file and a + translation pass, with no code change. +- No audit of `tests/`. Test strings are not user-facing. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee1f621..197366d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -46,6 +46,31 @@ target_link_libraries(qtmaildir PRIVATE qtmaildir_lib) install(TARGETS qtmaildir RUNTIME DESTINATION bin) +# The .ts is tracked; the .qm is generated and is not. Built beside the binary +# so a build tree runs translated without installing, and installed to the +# AppDataLocation main.cpp searches. Building the .qm without installing it +# reproduces exactly the state item 22 exists to fix, so the install rule is +# part of this and not a follow-up. +if(Qt6LinguistTools_FOUND) + set(QTMAILDIR_TS ${CMAKE_SOURCE_DIR}/translations/qtmaildir_it_IT.ts) + set(QTMAILDIR_QM ${CMAKE_CURRENT_BINARY_DIR}/translations/qtmaildir_it_IT.qm) + add_custom_command( + OUTPUT ${QTMAILDIR_QM} + COMMAND ${CMAKE_COMMAND} -E make_directory + ${CMAKE_CURRENT_BINARY_DIR}/translations + COMMAND Qt6::lrelease ${QTMAILDIR_TS} -qm ${QTMAILDIR_QM} + DEPENDS ${QTMAILDIR_TS} + COMMENT "Compiling Italian translation" + VERBATIM) + add_custom_target(qtmaildir_translations ALL DEPENDS ${QTMAILDIR_QM}) + add_dependencies(qtmaildir qtmaildir_translations) + install(FILES ${QTMAILDIR_QM} + DESTINATION share/qtmaildir/translations) +else() + message(STATUS + "Qt6 LinguistTools not found; building without translations") +endif() + # The icon goes into the hicolor theme under its scalable directory, which is # where a desktop environment looks for the Icon= name in the .desktop entry. install(FILES ${CMAKE_SOURCE_DIR}/assets/icons/qtmaildir.svg diff --git a/src/config.cpp b/src/config.cpp index 9bed74a..266ed39 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -208,14 +208,14 @@ void Config::load(const QString &path) // first. What was missing is the report. The value parses, so // nothing ever said the 500 in the file is not what is on screen. if (value < MessageView::kMinZoom || value > MessageView::kMaxZoom) { - addProblem(QStringLiteral("Message zoom %1 is outside %2 to %3; " + addProblem(tr("Message zoom %1 is outside %2 to %3; " "using the nearest allowed value.") .arg(value) .arg(MessageView::kMinZoom) .arg(MessageView::kMaxZoom)); } } else { - addProblem(QStringLiteral("Message zoom '%1' is not a number; " + addProblem(tr("Message zoom '%1' is not a number; " "using the default.") .arg(zoom.toString())); } @@ -235,14 +235,14 @@ void Config::load(const QString &path) bool ok = false; const int value = iconSize.toString().trimmed().toInt(&ok); if (!ok) { - addProblem(QStringLiteral("Toolbar icon size '%1' is not a number; " + addProblem(tr("Toolbar icon size '%1' is not a number; " "using %2.") .arg(iconSize.toString()) .arg(m_toolbarIconSize)); } else if (value < kMinToolbarIconSize || value > kMaxToolbarIconSize) { m_toolbarIconSize = qBound(kMinToolbarIconSize, value, kMaxToolbarIconSize); - addProblem(QStringLiteral("Toolbar icon size %1 is outside %2 to " + addProblem(tr("Toolbar icon size %1 is outside %2 to " "%3; using %4.") .arg(value) .arg(kMinToolbarIconSize) @@ -268,7 +268,7 @@ void Config::load(const QString &path) const QDateTime b(QDate(2019, 1, 3), QTime(4, 5)); const QLocale locale = QLocale::system(); if (locale.toString(a, dateFormat) == locale.toString(b, dateFormat)) { - addProblem(QStringLiteral("Date format '%1' contains no date or " + addProblem(tr("Date format '%1' contains no date or " "time field; using the system format.") .arg(dateFormat)); } else { @@ -297,7 +297,7 @@ void Config::load(const QString &path) } else { // Naming the accepted values, since a typo here silently changes what // happens to unsynced work at exit. - addProblem(QStringLiteral("Unknown sync_on_exit '%1'; expected ask, " + addProblem(tr("Unknown sync_on_exit '%1'; expected ask, " "always or never. Using ask.") .arg(syncExit)); } @@ -309,7 +309,7 @@ void Config::load(const QString &path) if (ok) { m_markReadDelayMs = value; } else { - addProblem(QStringLiteral("Mark-read delay '%1' is not a number; " + addProblem(tr("Mark-read delay '%1' is not a number; " "using the default.") .arg(markRead.toString())); } @@ -326,7 +326,7 @@ void Config::load(const QString &path) if (ok) { m_autoSyncDelayMs = value; } else { - addProblem(QStringLiteral("Auto-sync delay '%1' is not a number; " + addProblem(tr("Auto-sync delay '%1' is not a number; " "using the default.") .arg(autoSync.toString())); } @@ -352,7 +352,7 @@ void Config::load(const QString &path) // Skip only the bad entry: one typo must not cost the user the rest // of the list, and the built-ins are appended to regardless. if (value.isEmpty()) { - addProblem(QStringLiteral("[completion] extra_mimetypes: entry '%1' " + addProblem(tr("[completion] extra_mimetypes: entry '%1' " "has no mimetype; ignoring it.") .arg(entry)); continue; @@ -453,18 +453,29 @@ void Config::load(const QString &path) // entry for an account that does not exist and would sit on "All accounts" // without saying why. if (!m_startupAccount.isEmpty() && !account(m_startupAccount).isValid()) { - addProblem(QStringLiteral("Startup account '%1' is not a configured " + addProblem(tr("Startup account '%1' is not a configured " "account; starting on all accounts.") .arg(m_startupAccount)); m_startupAccount.clear(); } - if (m_startupQueryWasSet && !m_savedQueries.isEmpty() - && startupSavedQuery().name.compare(m_startupQuery, - Qt::CaseInsensitive) != 0) { - addProblem(QStringLiteral("Startup query '%1' is not a saved query; " - "opening '%2' instead.") - .arg(m_startupQuery, startupSavedQuery().name)); + // Asks whether the resolved query matched on EITHER a name or a generator, + // rather than comparing the name alone. Comparing names warned about a + // config that was working: `startup_query = Inbox` resolves through the + // generator under a translated UI, where the filter's name is "In arrivo", + // so the app opened the right view and reported it as wrong. + if (m_startupQueryWasSet && !m_savedQueries.isEmpty()) { + const SavedQuery resolved = startupSavedQuery(); + const bool matched = + resolved.name.compare(m_startupQuery, Qt::CaseInsensitive) == 0 + || (!resolved.generated.isEmpty() + && resolved.generated.compare(m_startupQuery, + Qt::CaseInsensitive) == 0); + if (!matched) { + addProblem(tr("Startup query '%1' is not a saved query; " + "opening '%2' instead.") + .arg(m_startupQuery, resolved.name)); + } } } @@ -513,14 +524,14 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) // Order is alphabetical here because childKeys() is genuinely all the // INI knows. The user reorders once and it sticks from then on. if (!m_savedQueries.isEmpty() && !saveSavedQueries()) { - addProblem(QStringLiteral("Could not write saved queries to %1.") + addProblem(tr("Could not write saved queries to %1.") .arg(m_queriesPath)); } return; } if (!file.open(QIODevice::ReadOnly)) { - addProblem(QStringLiteral("Could not read %1: %2.") + addProblem(tr("Could not read %1: %2.") .arg(m_queriesPath, file.errorString())); m_queriesRefused = true; return; @@ -532,7 +543,7 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) file.close(); if (error.error != QJsonParseError::NoError || !document.isObject()) { - addProblem(QStringLiteral("%1 is not valid JSON: %2.") + addProblem(tr("%1 is not valid JSON: %2.") .arg(m_queriesPath, error.errorString())); m_queriesRefused = true; return; @@ -545,7 +556,7 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) // Refused rather than guessed at, and the refusal blocks the save: // rewriting a newer document with this build's reading of it would // destroy whatever the newer build stored. - addProblem(QStringLiteral("%1 has format version %2; this build " + addProblem(tr("%1 has format version %2; this build " "understands %3. Saved queries were not " "loaded.") .arg(m_queriesPath) @@ -585,13 +596,13 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) // dropping the row here would delete it from the file on the next // save, which is the same data loss the unknown-field handling // exists to prevent. - addProblem(QStringLiteral("Saved query '%1' uses an unknown " + addProblem(tr("Saved query '%1' uses an unknown " "generator '%2' and will find nothing.") .arg(query.name, query.generated)); } if (query.name.isEmpty()) { - addProblem(QStringLiteral("A saved query in %1 has no name and was " + addProblem(tr("A saved query in %1 has no name and was " "skipped.").arg(m_queriesPath)); continue; } @@ -813,10 +824,25 @@ SavedQuery Config::startupSavedQuery() const // matched nothing once item 93 shipped Inbox as a filter and the duplicated // saved query was removed, and the app started on whatever query happened // to be first in the file. - for (const SavedQuery &filter : builtinFilters()) { + // + // Matched on the GENERATOR as well as the name, and the generator is what + // makes this survive translation. A filter's name is a translated label, so + // under LANG=it_IT the Inbox filter is called "In arrivo" and a config + // reading `startup_query = Inbox` matched nothing, warned, and fell back to + // another filter: the user's startup view changed because the UI language + // did. The generator is stored in queries.json and matched against a closed + // set, so it is wire format and identical in every locale. Names are still + // tried first, so a translated name a user copied out of their own UI keeps + // working. + const QList<SavedQuery> filters = builtinFilters(); + for (const SavedQuery &filter : filters) { if (filter.name.compare(m_startupQuery, Qt::CaseInsensitive) == 0) return filter; } + for (const SavedQuery &filter : filters) { + if (filter.generated.compare(m_startupQuery, Qt::CaseInsensitive) == 0) + return filter; + } // Named nothing that exists. Falling back to m_savedQueries.first() is what // this used to do and it is worse than it looks: after the duplicated diff --git a/src/keymap.cpp b/src/keymap.cpp index 52b015f..f6ef6a4 100644 --- a/src/keymap.cpp +++ b/src/keymap.cpp @@ -266,13 +266,13 @@ void KeyMap::loadOverrides(QSettings &settings) const QKeySequence sequence = normalizeSequence(key); if (sequence.isEmpty()) { m_warnings.append( - QStringLiteral("Unparseable key sequence '%1' in [keys]").arg(key)); + tr("Unparseable key sequence '%1' in [keys]").arg(key)); continue; } if (!known.contains(action)) { m_warnings.append( - QStringLiteral("Unknown action '%1' bound to '%2' in [keys]") + tr("Unknown action '%1' bound to '%2' in [keys]") .arg(action, key)); continue; } @@ -280,7 +280,7 @@ void KeyMap::loadOverrides(QSettings &settings) const auto previous = seenThisPass.constFind(sequence); if (previous != seenThisPass.constEnd()) { m_warnings.append( - QStringLiteral("Key sequence '%1' bound to both '%2' and '%3' " + tr("Key sequence '%1' bound to both '%2' and '%3' " "in [keys]; keeping '%2'") .arg(key, previous.value(), action)); continue; diff --git a/src/keymap.h b/src/keymap.h index f0addf5..81e1813 100644 --- a/src/keymap.h +++ b/src/keymap.h @@ -18,6 +18,7 @@ #pragma once +#include <QCoreApplication> #include <QHash> #include <QKeySequence> #include <QList> @@ -30,6 +31,11 @@ class QSettings; /// class has no dependency on the widgets that implement the actions. class KeyMap { + // Not a QObject, so tr() comes from here. Its warnings are user-facing: + // MainWindow joins them with Config's into the status label and the + // "Configuration problems" modal. + Q_DECLARE_TR_FUNCTIONS(KeyMap) + public: /// Every action name the application understands. loadOverrides() rejects /// anything not in this set, so a typo in the config cannot bind silently. diff --git a/src/main.cpp b/src/main.cpp index 2804196..59f39be 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,8 +17,13 @@ */ #include <QApplication> +#include <QCoreApplication> +#include <QDir> #include <QIcon> +#include <QLocale> #include <QMessageBox> +#include <QStandardPaths> +#include <QTranslator> #include <QWebEngineUrlScheme> #include <notmuch.h> @@ -84,6 +89,32 @@ int main(int argc, char *argv[]) app.setWindowIcon(QIcon(QStringLiteral(":/icons/qtmaildir.svg"))); app.setDesktopFileName(QStringLiteral("qtmaildir")); + // On main's stack deliberately: a QTranslator must outlive exec(), and one + // scoped to a helper function unloads on return, silently reverting every + // string to English. Installed before Config is loaded, because config + // warnings are generated at load time and are themselves translated. + // + // QLocale() reads the system locale, so LANG=it_IT.UTF-8 selects the file + // with no config key of our own. A missing .qm returns false and the app + // runs in English, which is the correct failure rather than a fatal one. + QTranslator translator; + QStringList translationDirs; + // Beside the binary first, so a build tree works without installing. + translationDirs << QCoreApplication::applicationDirPath() + + QStringLiteral("/translations"); + const QStringList dataDirs = + QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + for (const QString &dir : dataDirs) + translationDirs << dir + QStringLiteral("/translations"); + + for (const QString &dir : std::as_const(translationDirs)) { + if (translator.load(QLocale(), QStringLiteral("qtmaildir"), + QStringLiteral("_"), dir)) { + app.installTranslator(&translator); + break; + } + } + // Fail loudly on an ABI mismatch rather than crashing later. if (LIBNOTMUCH_MAJOR_VERSION < 5) { QMessageBox::critical(nullptr, QObject::tr("qtmaildir"), diff --git a/src/tagrulesdialog.cpp b/src/tagrulesdialog.cpp index ffcdf29..f208527 100644 --- a/src/tagrulesdialog.cpp +++ b/src/tagrulesdialog.cpp @@ -62,15 +62,24 @@ QStringList splitTags(const QString &text) struct FieldEntry { RuleTerm::Field field; const char *label; }; +// QT_TRANSLATE_NOOP, naming the context explicitly, rather than QT_TR_NOOP. +// These literals sit in an anonymous namespace, where lupdate reports "tr() +// cannot be called without context" and extracts NOTHING, while the use site +// below calls TagRulesDialog::tr() on them. The result compiles, reads +// correctly, and leaves all eight labels untranslatable: no .ts file ever +// contained them. Q_DECLARE_TR_FUNCTIONS on a neighbouring class does not fix +// it either (measured: still 0 extracted) because lupdate needs the context on +// the literal itself. The context named here must stay TagRulesDialog to match +// the tr() that reads it. const FieldEntry kFields[] = { - {RuleTerm::From, QT_TR_NOOP("From")}, - {RuleTerm::To, QT_TR_NOOP("To")}, - {RuleTerm::Cc, QT_TR_NOOP("Cc")}, - {RuleTerm::Subject, QT_TR_NOOP("Subject")}, - {RuleTerm::Tag, QT_TR_NOOP("Tag")}, - {RuleTerm::Folder, QT_TR_NOOP("Folder")}, - {RuleTerm::Attachment, QT_TR_NOOP("Attachment")}, - {RuleTerm::Date, QT_TR_NOOP("Date")}, + {RuleTerm::From, QT_TRANSLATE_NOOP("TagRulesDialog", "From")}, + {RuleTerm::To, QT_TRANSLATE_NOOP("TagRulesDialog", "To")}, + {RuleTerm::Cc, QT_TRANSLATE_NOOP("TagRulesDialog", "Cc")}, + {RuleTerm::Subject, QT_TRANSLATE_NOOP("TagRulesDialog", "Subject")}, + {RuleTerm::Tag, QT_TRANSLATE_NOOP("TagRulesDialog", "Tag")}, + {RuleTerm::Folder, QT_TRANSLATE_NOOP("TagRulesDialog", "Folder")}, + {RuleTerm::Attachment, QT_TRANSLATE_NOOP("TagRulesDialog", "Attachment")}, + {RuleTerm::Date, QT_TRANSLATE_NOOP("TagRulesDialog", "Date")}, }; } // namespace diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5f7bd48..f9bc7d1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -34,6 +34,16 @@ endfunction() add_qtmaildir_test(keymap) add_qtmaildir_test(config) +# One test installs the real Italian translation, to prove startup_query still +# resolves when a filter's displayed name is not its English one. It needs the +# compiled .qm, so it depends on the target that builds it. +if(Qt6LinguistTools_FOUND) + target_compile_definitions(test_config PRIVATE + TRANSLATIONS_QM="${CMAKE_BINARY_DIR}/src/translations/qtmaildir_it_IT.qm") + add_dependencies(test_config qtmaildir_translations) +else() + target_compile_definitions(test_config PRIVATE TRANSLATIONS_QM="") +endif() add_qtmaildir_test(mimeparser) target_compile_definitions(test_mimeparser PRIVATE FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures") @@ -57,3 +67,9 @@ add_qtmaildir_test(rulequery) add_qtmaildir_test(searchterm) add_qtmaildir_test(tagstrip) add_qtmaildir_test(messagedetailsdialog) +add_qtmaildir_test(translations) +# Asserts on the tracked .ts rather than the generated .qm: an untranslated +# string is dropped by lrelease, so it is invisible in the .qm and shows up +# only as English in a running Italian UI. +target_compile_definitions(test_translations PRIVATE + TRANSLATIONS_DIR="${CMAKE_SOURCE_DIR}/translations") diff --git a/tests/test_config.cpp b/tests/test_config.cpp index e2dffb0..00e8d89 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -18,6 +18,7 @@ #include <QtTest> #include <QTemporaryDir> +#include <QTranslator> #include <QSettings> #include <QJsonArray> #include <QJsonDocument> @@ -99,6 +100,7 @@ private slots: void theStartupAccountIsReadAndValidated(); void theStartupAccountTakesTheKeyNotTheSyncChannel(); void theStartupQueryCanNameABuiltinFilter(); + void theStartupQuerySurvivesATranslatedFilterName(); void theStartupQueryPrefersASavedQueryOverAFilterOfTheSameName(); void anUnmatchedStartupQueryFallsBackToAFilterNotAStrayQuery(); void theFlaggedFilterIsCalledImportant(); @@ -1175,6 +1177,91 @@ void TestConfig::theStartupQueryCanNameABuiltinFilter() QStringLiteral("tag:inbox")); } +void TestConfig::theStartupQuerySurvivesATranslatedFilterName() +{ + // Reported by the user running the 0.23.0 Italian translation: the app + // started on the wrong view and said + // + // La ricerca iniziale 'Inbox' non è una ricerca salvata; verrà aperta + // 'Non letti'. + // + // A filter's NAME is a translated label, so `startup_query = Inbox` matched + // nothing once the Inbox filter was called "In arrivo": a config file that + // had always worked broke because the UI language changed, and the warning + // named the user's own correct config as the fault. + // + // The fix matches the GENERATOR too, which is stored in queries.json and + // identical in every locale. Uses a real QTranslator rather than a stub, + // because the bug lives in the gap between the stored string and the + // displayed one, and only an actual translation opens that gap. + QTranslator translator; + const QString qm = QStringLiteral(TRANSLATIONS_QM); + QVERIFY2(QFile::exists(qm), + qPrintable(QStringLiteral("no compiled translation at %1").arg(qm))); + QVERIFY2(translator.load(qm), "the Italian translation failed to load"); + QVERIFY(qApp->installTranslator(&translator)); + + // Proves the translator is actually in effect. Without this the test passes + // when the translation silently fails to load, asserting nothing: the names + // stay English and every comparison below succeeds for the wrong reason. + const SavedQuery inbox = Config::builtinFilter(QStringLiteral("inbox")); + QCOMPARE(inbox.name, QStringLiteral("In arrivo")); + + QTemporaryDir dir; + // A saved query has to exist for the warning to be reachable at all: the + // check is guarded by !m_savedQueries.isEmpty(). Without this file the + // branch never runs, and an assertion that no problem was reported passes + // against a broken check by never reaching it. Measured: with no + // queries.json, reverting the fix left this test green. + { + QFile queries(dir.filePath(QStringLiteral("queries.json"))); + QVERIFY(queries.open(QIODevice::WriteOnly)); + queries.write(QStringLiteral(R"({ + "version": 1, + "queries": [ { "name": "Mine", "query": "tag:mine" } ] + })").toUtf8()); + } + + Config config; + config.load(writeIni(dir, QStringLiteral( + "[general]\n" + "startup_query=Inbox\n" + "\n" + "[account.work]\n" + "maildir=work\n" + "sent=Sent\n"))); + QVERIFY2(!config.savedQueries().isEmpty(), + "queries.json did not load, so the warning path is unreachable"); + + const SavedQuery startup = config.startupSavedQuery(); + QCOMPARE(startup.generated, QStringLiteral("inbox")); + QCOMPARE(config.resolvedQuery(startup, QString()), + QStringLiteral("tag:inbox")); + + // And it must not warn about a config that is working. The user saw the + // warning as well as the wrong view, and a warning they cannot act on is + // its own defect. + QVERIFY2(config.problems().isEmpty(), + qPrintable(QStringLiteral("unexpected problem: %1") + .arg(config.problems().join(QLatin1Char(' '))))); + + // The translated name still works, since that is what a user reading their + // own Italian UI would naturally write. + Config byLabel; + byLabel.load(writeIni(dir, QStringLiteral( + "[general]\n" + "startup_query=In arrivo\n" + "\n" + "[account.work]\n" + "maildir=work\n" + "sent=Sent\n"))); + QVERIFY(!byLabel.savedQueries().isEmpty()); + QCOMPARE(byLabel.startupSavedQuery().generated, QStringLiteral("inbox")); + QVERIFY(byLabel.problems().isEmpty()); + + qApp->removeTranslator(&translator); +} + void TestConfig::theFlaggedFilterIsCalledImportant() { // Item 57 decided this and item 93 contradicted it. The `flag` ACTION has diff --git a/tests/test_translations.cpp b/tests/test_translations.cpp new file mode 100644 index 0000000..86ff517 --- /dev/null +++ b/tests/test_translations.cpp @@ -0,0 +1,226 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +// Guards the shipped translation against the two ways it rots silently: a new +// user-facing string added without a translation, and the extraction defect +// item 22 exists to fix, where a literal is invisible to lupdate and therefore +// untranslatable while the source looks correct. +// +// This asserts on the .ts file rather than on a running UI deliberately. The +// backlog entry is explicit that lupdate output is the evidence here, not +// reading: the eight rule-builder labels below were wrapped in QT_TR_NOOP, +// compiled, ran, and were still unreachable by any translation. + +#include <QtTest> + +#include <QFile> +#include <QSet> +#include <QString> +#include <QXmlStreamReader> + +class TestTranslations : public QObject +{ + Q_OBJECT + +private slots: + void everyStringIsTranslated(); + void everyStringIsTranslated_data(); + + void theRuleBuilderFieldLabelsAreExtracted(); + void theFileIsWellFormedAndItalian(); + +private: + static QString tsPath() + { + return QStringLiteral(TRANSLATIONS_DIR "/qtmaildir_it_IT.ts"); + } +}; + +// Reads every <message> as (context, source, translation, unfinished). +struct Entry { + QString context; + QString source; + QString translation; + bool unfinished = false; +}; + +static QList<Entry> readEntries(const QString &path, QString *error) +{ + QList<Entry> entries; + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + *error = QStringLiteral("cannot open %1: %2").arg(path, file.errorString()); + return entries; + } + + QXmlStreamReader xml(&file); + QString context; + while (!xml.atEnd()) { + xml.readNext(); + if (!xml.isStartElement()) + continue; + + // <name> appears inside both <context> and <message>; only the one + // directly under <context> names the class. + if (xml.name() == QLatin1String("name")) { + context = xml.readElementText(); + continue; + } + if (xml.name() != QLatin1String("message")) + continue; + + Entry entry; + entry.context = context; + const bool numerus = + xml.attributes().value(QLatin1String("numerus")) == QLatin1String("yes"); + + while (!(xml.isEndElement() && xml.name() == QLatin1String("message")) + && !xml.atEnd()) { + xml.readNext(); + if (!xml.isStartElement()) + continue; + if (xml.name() == QLatin1String("source")) { + entry.source = xml.readElementText(); + } else if (xml.name() == QLatin1String("translation")) { + entry.unfinished = + xml.attributes().value(QLatin1String("type")) + == QLatin1String("unfinished"); + if (!numerus) { + entry.translation = xml.readElementText(); + } else { + // A numerus message carries one <numerusform> per plural + // form. Italian has two, and an empty one is as untranslated + // as an empty <translation>. + QStringList forms; + while (!(xml.isEndElement() + && xml.name() == QLatin1String("translation")) + && !xml.atEnd()) { + xml.readNext(); + if (xml.isStartElement() + && xml.name() == QLatin1String("numerusform")) + forms << xml.readElementText(); + } + entry.translation = forms.join(QLatin1Char('\x1f')); + if (forms.size() != 2 || forms.contains(QString())) + entry.translation.clear(); + } + } + } + entries.append(entry); + } + + if (xml.hasError()) + *error = xml.errorString(); + return entries; +} + +void TestTranslations::everyStringIsTranslated_data() +{ + QTest::addColumn<QString>("context"); + QTest::addColumn<QString>("source"); + QTest::addColumn<QString>("translation"); + QTest::addColumn<bool>("unfinished"); + + QString error; + const QList<Entry> entries = readEntries(tsPath(), &error); + QVERIFY2(error.isEmpty(), qPrintable(error)); + + // A file that parsed to nothing would let every row-driven check below pass + // by never running, which is the rendering-probe trap in a different shape. + QVERIFY2(entries.size() > 300, + qPrintable(QStringLiteral("only %1 entries; the .ts looks truncated") + .arg(entries.size()))); + + for (const Entry &entry : entries) { + const QByteArray tag = + (entry.context + QLatin1String(" :: ") + entry.source).toUtf8(); + QTest::newRow(tag.constData()) + << entry.context << entry.source << entry.translation + << entry.unfinished; + } +} + +void TestTranslations::everyStringIsTranslated() +{ + QFETCH(QString, translation); + QFETCH(bool, unfinished); + + // lrelease drops anything still flagged unfinished, so such a string ships + // as English inside an otherwise Italian UI rather than failing the build. + QVERIFY2(!unfinished, "still marked type=\"unfinished\""); + QVERIFY2(!translation.trimmed().isEmpty(), "no translation"); +} + +void TestTranslations::theRuleBuilderFieldLabelsAreExtracted() +{ + QString error; + const QList<Entry> entries = readEntries(tsPath(), &error); + QVERIFY2(error.isEmpty(), qPrintable(error)); + + QSet<QString> found; + for (const Entry &entry : entries) { + if (entry.context == QLatin1String("TagRulesDialog")) + found.insert(entry.source); + } + + // These eight sat in an anonymous namespace under QT_TR_NOOP, where lupdate + // reports "tr() cannot be called without context" and extracts nothing, + // while TagRulesDialog::tr() read them at runtime. Every one was + // untranslatable and the source looked right. QT_TRANSLATE_NOOP, naming the + // context explicitly, is what fixed it; Q_DECLARE_TR_FUNCTIONS on a + // neighbouring class does NOT, measured at 0 extracted. + // + // The context asserted here must stay TagRulesDialog: it is what the + // reading tr() resolves against, so a mismatch is untranslated at runtime + // with a perfectly populated .ts. + for (const QString &label : { QStringLiteral("From"), QStringLiteral("To"), + QStringLiteral("Cc"), QStringLiteral("Subject"), + QStringLiteral("Tag"), QStringLiteral("Folder"), + QStringLiteral("Attachment"), + QStringLiteral("Date") }) { + QVERIFY2(found.contains(label), + qPrintable(QStringLiteral( + "TagRulesDialog/%1 is missing from the .ts: lupdate cannot " + "see it, so it can never be translated").arg(label))); + } +} + +void TestTranslations::theFileIsWellFormedAndItalian() +{ + QFile file(tsPath()); + QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text), + qPrintable(file.errorString())); + + QXmlStreamReader xml(&file); + QString language; + while (!xml.atEnd()) { + xml.readNext(); + if (xml.isStartElement() && xml.name() == QLatin1String("TS")) { + language = xml.attributes().value(QLatin1String("language")).toString(); + break; + } + } + QVERIFY2(!xml.hasError(), qPrintable(xml.errorString())); + + // QTranslator::load() derives the file from the locale, so the language + // attribute is what pairs this file with LANG=it_IT. + QCOMPARE(language, QStringLiteral("it_IT")); +} + +QTEST_MAIN(TestTranslations) +#include "test_translations.moc" diff --git a/translations/qtmaildir_it_IT.ts b/translations/qtmaildir_it_IT.ts new file mode 100644 index 0000000..1bf4f31 --- /dev/null +++ b/translations/qtmaildir_it_IT.ts @@ -0,0 +1,1543 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="it_IT"> +<context> + <name>Config</name> + <message> + <source>Message zoom %1 is outside %2 to %3; using the nearest allowed value.</source> + <translation>Lo zoom del messaggio %1 non rientra tra %2 e %3; verrà usato il valore consentito più vicino.</translation> + </message> + <message> + <source>Message zoom '%1' is not a number; using the default.</source> + <translation>Lo zoom del messaggio '%1' non è un numero; verrà usato il valore predefinito.</translation> + </message> + <message> + <source>Toolbar icon size '%1' is not a number; using %2.</source> + <translation>La dimensione delle icone '%1' non è un numero; verrà usata %2.</translation> + </message> + <message> + <source>Toolbar icon size %1 is outside %2 to %3; using %4.</source> + <translation>La dimensione delle icone %1 non rientra tra %2 e %3; verrà usata %4.</translation> + </message> + <message> + <source>Date format '%1' contains no date or time field; using the system format.</source> + <translation>Il formato data '%1' non contiene campi di data o ora; verrà usato il formato di sistema.</translation> + </message> + <message> + <source>Unknown sync_on_exit '%1'; expected ask, always or never. Using ask.</source> + <translation>Valore sconosciuto per sync_on_exit '%1'; sono ammessi ask, always o never. Verrà usato ask.</translation> + </message> + <message> + <source>Mark-read delay '%1' is not a number; using the default.</source> + <translation>Il ritardo di marcatura come letto '%1' non è un numero; verrà usato il valore predefinito.</translation> + </message> + <message> + <source>Auto-sync delay '%1' is not a number; using the default.</source> + <translation>Il ritardo di sincronizzazione automatica '%1' non è un numero; verrà usato il valore predefinito.</translation> + </message> + <message> + <source>[completion] extra_mimetypes: entry '%1' has no mimetype; ignoring it.</source> + <translation>[completion] extra_mimetypes: la voce '%1' non ha un mimetype; verrà ignorata.</translation> + </message> + <message> + <source>Startup account '%1' is not a configured account; starting on all accounts.</source> + <translation>L'account iniziale '%1' non è un account configurato; si parte da tutti gli account.</translation> + </message> + <message> + <source>Startup query '%1' is not a saved query; opening '%2' instead.</source> + <translation>La ricerca iniziale '%1' non è una ricerca salvata; verrà aperta '%2'.</translation> + </message> + <message> + <source>Could not write saved queries to %1.</source> + <translation>Impossibile scrivere le ricerche salvate in %1.</translation> + </message> + <message> + <source>Could not read %1: %2.</source> + <translation>Impossibile leggere %1: %2.</translation> + </message> + <message> + <source>%1 is not valid JSON: %2.</source> + <translation>%1 non è JSON valido: %2.</translation> + </message> + <message> + <source>%1 has format version %2; this build understands %3. Saved queries were not loaded.</source> + <translation>%1 ha la versione di formato %2; questa build ne riconosce %3. Le ricerche salvate non sono state caricate.</translation> + </message> + <message> + <source>Saved query '%1' uses an unknown generator '%2' and will find nothing.</source> + <translation>La ricerca salvata '%1' usa un generatore sconosciuto '%2' e non troverà nulla.</translation> + </message> + <message> + <source>A saved query in %1 has no name and was skipped.</source> + <translation>Una ricerca salvata in %1 non ha nome ed è stata saltata.</translation> + </message> + <message> + <source>Unread</source> + <translation>Non letti</translation> + </message> + <message> + <source>Inbox</source> + <translation>In arrivo</translation> + </message> + <message> + <source>Important</source> + <translation>Importanti</translation> + </message> + <message> + <source>Sent</source> + <translation>Inviati</translation> + </message> +</context> +<context> + <name>HtmlBuilder</name> + <message> + <source>local mail, tagged and searched</source> + <translation>posta locale, etichettata e ricercabile</translation> + </message> + <message> + <source>Copyright &copy; 2026 Danilo M. &middot; version %1</source> + <translation>Copyright &copy; 2026 Danilo M. &middot; versione %1</translation> + </message> +</context> +<context> + <name>KeyMap</name> + <message> + <source>Unparseable key sequence '%1' in [keys]</source> + <translation>Sequenza di tasti '%1' non interpretabile in [keys]</translation> + </message> + <message> + <source>Unknown action '%1' bound to '%2' in [keys]</source> + <translation>Azione sconosciuta '%1' associata a '%2' in [keys]</translation> + </message> + <message> + <source>Key sequence '%1' bound to both '%2' and '%3' in [keys]; keeping '%2'</source> + <translation>La sequenza '%1' è associata sia a '%2' sia a '%3' in [keys]; viene mantenuta '%2'</translation> + </message> +</context> +<context> + <name>MainWindow</name> + <message> + <source>Unsynced changes</source> + <translation>Modifiche non sincronizzate</translation> + </message> + <message numerus="yes"> + <source>%n tag change(s) have not been synced, and no sync command is configured. Quit anyway?</source> + <translation> + <numerusform>%n modifica alle etichette non è stata sincronizzata e non è configurato alcun comando di sincronizzazione. Uscire comunque?</numerusform> + <numerusform>%n modifiche alle etichette non sono state sincronizzate e non è configurato alcun comando di sincronizzazione. Uscire comunque?</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n tag change(s) have not been synced.</source> + <translation> + <numerusform>%n modifica alle etichette non è stata sincronizzata.</numerusform> + <numerusform>%n modifiche alle etichette non sono state sincronizzate.</numerusform> + </translation> + </message> + <message> + <source>Sync before quitting?</source> + <translation>Sincronizzare prima di uscire?</translation> + </message> + <message> + <source>Sync and quit</source> + <translation>Sincronizza ed esci</translation> + </message> + <message> + <source>Quit anyway</source> + <translation>Esci comunque</translation> + </message> + <message> + <source>Sync and quit (default)</source> + <translation>Sincronizza ed esci (predefinito)</translation> + </message> + <message> + <source>Syncing before quitting...</source> + <translation>Sincronizzazione prima di uscire...</translation> + </message> + <message> + <source>Sync failed</source> + <translation>Sincronizzazione non riuscita</translation> + </message> + <message> + <source>The sync could not be started, so your changes are still unsynced.</source> + <translation>Non è stato possibile avviare la sincronizzazione, quindi le modifiche non sono ancora sincronizzate.</translation> + </message> + <message> + <source>Save</source> + <translation>Salva</translation> + </message> + <message> + <source>All accounts</source> + <translation>Tutti gli account</translation> + </message> + <message> + <source>Newest first</source> + <translation>Prima i più recenti</translation> + </message> + <message> + <source>Oldest first</source> + <translation>Prima i più vecchi</translation> + </message> + <message> + <source>The order threads are listed in</source> + <translation>L'ordine in cui sono elencate le conversazioni</translation> + </message> + <message> + <source>notmuch query, e.g. tag:inbox</source> + <translation>ricerca notmuch, ad es. tag:inbox</translation> + </message> + <message> + <source>Sync output</source> + <translation>Output della sincronizzazione</translation> + </message> + <message> + <source>Close</source> + <translation>Chiudi</translation> + </message> + <message> + <source>Hide the sync output until the next failure</source> + <translation>Nascondi l'output della sincronizzazione fino al prossimo errore</translation> + </message> + <message> + <source>&Find</source> + <translation>&Cerca</translation> + </message> + <message> + <source>Focus and select the query bar</source> + <translation>Attiva e seleziona la barra di ricerca</translation> + </message> + <message> + <source>&Next thread</source> + <translation>Conversazione s&uccessiva</translation> + </message> + <message> + <source>Select the next thread</source> + <translation>Seleziona la conversazione successiva</translation> + </message> + <message> + <source>&Previous thread</source> + <translation>Conversazione &precedente</translation> + </message> + <message> + <source>Select the previous thread</source> + <translation>Seleziona la conversazione precedente</translation> + </message> + <message> + <source>&Open thread</source> + <translation>&Apri conversazione</translation> + </message> + <message> + <source>Focus the thread list</source> + <translation>Attiva l'elenco delle conversazioni</translation> + </message> + <message> + <source>&Archive</source> + <translation>&Archivia</translation> + </message> + <message> + <source>Remove inbox from every selected thread</source> + <translation>Rimuove inbox da ogni conversazione selezionata</translation> + </message> + <message> + <source>Archive</source> + <translation>Archivia</translation> + </message> + <message> + <source>&Delete</source> + <translation>&Elimina</translation> + </message> + <message> + <source>Add or remove the deleted tag</source> + <translation>Aggiunge o rimuove l'etichetta deleted</translation> + </message> + <message> + <source>Undelete</source> + <translation>Ripristina</translation> + </message> + <message> + <source>Delete</source> + <translation>Elimina</translation> + </message> + <message> + <source>Mark &spam</source> + <translation>Segna come &spam</translation> + </message> + <message> + <source>Add spam and remove inbox</source> + <translation>Aggiunge spam e rimuove inbox</translation> + </message> + <message> + <source>Mark spam</source> + <translation>Segna come spam</translation> + </message> + <message> + <source>&Important</source> + <translation>&Importante</translation> + </message> + <message> + <source>Mark the selected threads as important</source> + <translation>Segna come importanti le conversazioni selezionate</translation> + </message> + <message> + <source>Mark important</source> + <translation>Segna come importante</translation> + </message> + <message> + <source>Toggle &unread</source> + <translation>Inverti &non letto</translation> + </message> + <message> + <source>Toggle the unread tag</source> + <translation>Inverte l'etichetta unread</translation> + </message> + <message> + <source>Mark read</source> + <translation>Segna come letto</translation> + </message> + <message> + <source>Mark unread</source> + <translation>Segna come non letto</translation> + </message> + <message> + <source>Mark all &read</source> + <translation>Segna tutti come &letti</translation> + </message> + <message> + <source>Remove the unread tag from every thread in this view</source> + <translation>Rimuove l'etichetta unread da ogni conversazione in questa vista</translation> + </message> + <message> + <source>Edit &tags...</source> + <translation>Modifica e&tichette...</translation> + </message> + <message> + <source>Add or remove any tag on the selected threads</source> + <translation>Aggiunge o rimuove qualsiasi etichetta sulle conversazioni selezionate</translation> + </message> + <message> + <source>Tagging &rules...</source> + <translation>&Regole di etichettatura...</translation> + </message> + <message> + <source>Edit the rules that tag mail as it arrives</source> + <translation>Modifica le regole che etichettano la posta in arrivo</translation> + </message> + <message> + <source>&Save query...</source> + <translation>&Salva ricerca...</translation> + </message> + <message> + <source>Keep the current query as a saved query</source> + <translation>Conserva la ricerca attuale come ricerca salvata</translation> + </message> + <message> + <source>Toggle &HTML</source> + <translation>Attiva/disattiva &HTML</translation> + </message> + <message> + <source>Switch the thread between HTML and plain text</source> + <translation>Alterna la conversazione tra HTML e testo semplice</translation> + </message> + <message> + <source>Load &remote content</source> + <translation>Carica contenuti &remoti</translation> + </message> + <message> + <source>Load remote images for the current thread</source> + <translation>Carica le immagini remote della conversazione attuale</translation> + </message> + <message> + <source>Message &details</source> + <translation>&Dettagli del messaggio</translation> + </message> + <message> + <source>Show the full headers of every message in the thread</source> + <translation>Mostra le intestazioni complete di ogni messaggio della conversazione</translation> + </message> + <message> + <source>Zoom &in</source> + <translation>&Ingrandisci</translation> + </message> + <message> + <source>Enlarge the message text</source> + <translation>Ingrandisce il testo del messaggio</translation> + </message> + <message> + <source>Zoom &out</source> + <translation>Ri&duci</translation> + </message> + <message> + <source>Shrink the message text</source> + <translation>Riduce il testo del messaggio</translation> + </message> + <message> + <source>&Actual size</source> + <translation>Dimensione &reale</translation> + </message> + <message> + <source>Return the message text to its default size</source> + <translation>Riporta il testo del messaggio alla dimensione predefinita</translation> + </message> + <message> + <source>&Undo</source> + <translation>&Annulla</translation> + </message> + <message> + <source>Undo the last tag change</source> + <translation>Annulla l'ultima modifica alle etichette</translation> + </message> + <message> + <source>Nothing to undo</source> + <translation>Niente da annullare</translation> + </message> + <message> + <source>&Sync</source> + <translation>&Sincronizza</translation> + </message> + <message> + <source>Run the configured sync command</source> + <translation>Esegue il comando di sincronizzazione configurato</translation> + </message> + <message> + <source>&Complete query</source> + <translation>&Completa ricerca</translation> + </message> + <message> + <source>Offer completions for the query bar</source> + <translation>Propone completamenti per la barra di ricerca</translation> + </message> + <message> + <source>Clear &message pane</source> + <translation>Svuota il pannello &messaggio</translation> + </message> + <message> + <source>Blank the message pane without changing the selection</source> + <translation>Svuota il pannello del messaggio senza cambiare la selezione</translation> + </message> + <message> + <source>Clear &selection</source> + <translation>Annulla &selezione</translation> + </message> + <message> + <source>Blank the message pane and deselect every thread</source> + <translation>Svuota il pannello del messaggio e deseleziona ogni conversazione</translation> + </message> + <message> + <source>Select &all threads</source> + <translation>Seleziona &tutte le conversazioni</translation> + </message> + <message> + <source>Select every thread in the current result list</source> + <translation>Seleziona ogni conversazione nell'elenco dei risultati</translation> + </message> + <message> + <source>&Quit</source> + <translation>&Esci</translation> + </message> + <message> + <source>Quit qtmaildir</source> + <translation>Esce da qtmaildir</translation> + </message> + <message> + <source>&File</source> + <translation>&File</translation> + </message> + <message> + <source>&Edit</source> + <translation>&Modifica</translation> + </message> + <message> + <source>&Message</source> + <translation>&Messaggio</translation> + </message> + <message> + <source>&View</source> + <translation>&Visualizza</translation> + </message> + <message> + <source>&Help</source> + <translation>&Aiuto</translation> + </message> + <message> + <source>&Keyboard shortcuts</source> + <translation>&Scorciatoie da tastiera</translation> + </message> + <message> + <source>&Maildir overview</source> + <translation>&Panoramica della Maildir</translation> + </message> + <message> + <source>&About</source> + <translation>&Informazioni</translation> + </message> + <message> + <source>Main</source> + <translation>Principale</translation> + </message> + <message> + <source>No sync command configured ([sync] command in qtmaildir.conf)</source> + <translation>Nessun comando di sincronizzazione configurato (comando [sync] in qtmaildir.conf)</translation> + </message> + <message> + <source>(unbound)</source> + <translation>(non associato)</translation> + </message> + <message> + <source><tr><th align='left'>Key</th><th align='left'>Does</th><th align='left'>Action name</th></tr></source> + <translation><tr><th align='left'>Tasto</th><th align='left'>Azione</th><th align='left'>Nome azione</th></tr></translation> + </message> + <message> + <source>Keyboard shortcuts</source> + <translation>Scorciatoie da tastiera</translation> + </message> + <message> + <source><table cellspacing='0'><tr><td valign='top'><table cellpadding='3'>%1</table></td><td width='32'></td><td valign='top'><table cellpadding='3'>%2</table></td></tr></table></source> + <translation><table cellspacing='0'><tr><td valign='top'><table cellpadding='3'>%1</table></td><td width='32'></td><td valign='top'><table cellpadding='3'>%2</table></td></tr></table></translation> + </message> + <message> + <source><b>Thread list:</b> <tt>Ctrl</tt>+click adds or removes a single row, <tt>Shift</tt>+click extends the selection to a range. Tag, archive and delete all apply to every selected thread.</source> + <translation><b>Elenco conversazioni:</b> <tt>Ctrl</tt>+clic aggiunge o rimuove una singola riga, <tt>Shift</tt>+clic estende la selezione a un intervallo. Etichetta, archivia ed elimina si applicano a ogni conversazione selezionata.</translation> + </message> + <message> + <source>Rebind any of these in the <tt>[keys]</tt> section of <tt>qtmaildir.conf</tt>, using the action name.</source> + <translation>Puoi riassegnare queste combinazioni nella sezione <tt>[keys]</tt> di <tt>qtmaildir.conf</tt>, usando il nome dell'azione.</translation> + </message> + <message> + <source>Maildir overview</source> + <translation>Panoramica della Maildir</translation> + </message> + <message> + <source><b>Counting...</b></source> + <translation><b>Conteggio in corso...</b></translation> + </message> + <message numerus="yes"> + <source><b>%n account(s)</b></source> + <translation> + <numerusform><b>%n account</b></numerusform> + <numerusform><b>%n account</b></numerusform> + </translation> + </message> + <message> + <source>unknown</source> + <translation>sconosciuto</translation> + </message> + <message> + <source><b>%1</b> messages in <b>%2</b> threads<br><b>%3</b> tags</source> + <translation><b>%1</b> messaggi in <b>%2</b> conversazioni<br><b>%3</b> etichette</translation> + </message> + <message> + <source>About qtmaildir</source> + <translation>Informazioni su qtmaildir</translation> + </message> + <message> + <source><h3>qtmaildir %1</h3><p>A Qt6 mail client for notmuch-indexed Maildirs.</p><p>Reads and organizes local mail. Fetching and sending are handled by external scripts.</p><p>Copyright &copy; 2026 Danilo M. &lt;danix@danix.xyz&gt;<br>Licensed under the GNU General Public License version 2.</p><p>Developed with AI assistance. All code is reviewed, tested and curated by the maintainer.</p></source> + <translation><h3>qtmaildir %1</h3><p>Un client di posta Qt6 per Maildir indicizzate con notmuch.</p><p>Legge e organizza la posta locale. Scaricamento e invio sono affidati a script esterni.</p><p>Copyright &copy; 2026 Danilo M. &lt;danix@danix.xyz&gt;<br>Distribuito secondo la GNU General Public License versione 2.</p><p>Sviluppato con l'assistenza dell'IA. Tutto il codice è riveduto, testato e curato dal manutentore.</p></translation> + </message> + <message numerus="yes"> + <source>%n unread</source> + <translation> + <numerusform>%n non letto</numerusform> + <numerusform>%n non letti</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n flagged</source> + <translation> + <numerusform>%n importante</numerusform> + <numerusform>%n importanti</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n in inbox</source> + <translation> + <numerusform>%n in arrivo</numerusform> + <numerusform>%n in arrivo</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n sent</source> + <translation> + <numerusform>%n inviato</numerusform> + <numerusform>%n inviati</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n draft(s)</source> + <translation> + <numerusform>%n bozza</numerusform> + <numerusform>%n bozze</numerusform> + </translation> + </message> + <message> + <source>last sync failed</source> + <translation>ultima sincronizzazione non riuscita</translation> + </message> + <message numerus="yes"> + <source>%n change(s) waiting to sync</source> + <translation> + <numerusform>%n modifica in attesa di sincronizzazione</numerusform> + <numerusform>%n modifiche in attesa di sincronizzazione</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n configuration warning(s)</source> + <translation> + <numerusform>%n avviso di configurazione</numerusform> + <numerusform>%n avvisi di configurazione</numerusform> + </translation> + </message> + <message> + <source>More queries</source> + <translation>Altre ricerche</translation> + </message> + <message> + <source>Run</source> + <translation>Esegui</translation> + </message> + <message> + <source>Edit...</source> + <translation>Modifica...</translation> + </message> + <message> + <source>Move to menu</source> + <translation>Sposta nel menu</translation> + </message> + <message> + <source>Show as a button</source> + <translation>Mostra come pulsante</translation> + </message> + <message> + <source>Create tagging rule...</source> + <translation>Crea regola di etichettatura...</translation> + </message> + <message> + <source>Delete saved query</source> + <translation>Elimina ricerca salvata</translation> + </message> + <message> + <source>Delete the saved query '%1'?</source> + <translation>Eliminare la ricerca salvata '%1'?</translation> + </message> + <message> + <source>Saved queries</source> + <translation>Ricerche salvate</translation> + </message> + <message> + <source>Could not write the saved queries file.</source> + <translation>Impossibile scrivere il file delle ricerche salvate.</translation> + </message> + <message> + <source>Deleted saved query '%1'.</source> + <translation>Ricerca salvata '%1' eliminata.</translation> + </message> + <message> + <source>Updated saved query '%1'.</source> + <translation>Ricerca salvata '%1' aggiornata.</translation> + </message> + <message> + <source>Save query</source> + <translation>Salva ricerca</translation> + </message> + <message> + <source>Saved query '%1'.</source> + <translation>Ricerca '%1' salvata.</translation> + </message> + <message> + <source>Searching...</source> + <translation>Ricerca in corso...</translation> + </message> + <message numerus="yes"> + <source>Searching... %n thread(s)</source> + <translation> + <numerusform>Ricerca in corso... %n conversazione</numerusform> + <numerusform>Ricerca in corso... %n conversazioni</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n thread(s)</source> + <translation> + <numerusform>%n conversazione</numerusform> + <numerusform>%n conversazioni</numerusform> + </translation> + </message> + <message> + <source>Nothing unread in this view</source> + <translation>Nulla di non letto in questa vista</translation> + </message> + <message> + <source>Mark all read</source> + <translation>Segna tutti come letti</translation> + </message> + <message numerus="yes"> + <source>%1: %n thread(s)</source> + <translation> + <numerusform>%1: %n conversazione</numerusform> + <numerusform>%1: %n conversazioni</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>1 thread selected (%n message(s))</source> + <translation> + <numerusform>1 conversazione selezionata (%n messaggio)</numerusform> + <numerusform>1 conversazione selezionata (%n messaggi)</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n thread(s) selected (%1 messages)</source> + <translation> + <numerusform>%n conversazione selezionata (%1 messaggi)</numerusform> + <numerusform>%n conversazioni selezionate (%1 messaggi)</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n message(s) selected</source> + <translation> + <numerusform>%n messaggio selezionato</numerusform> + <numerusform>%n messaggi selezionati</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%n thread(s) and %1 message(s) selected</source> + <translation> + <numerusform>%n conversazione e %1 messaggi selezionati</numerusform> + <numerusform>%n conversazioni e %1 messaggi selezionati</numerusform> + </translation> + </message> + <message> + <source>(unreadable message)</source> + <translation>(messaggio illeggibile)</translation> + </message> + <message> + <source>This message could not be parsed. +%1</source> + <translation>Non è stato possibile analizzare questo messaggio. +%1</translation> + </message> + <message numerus="yes"> + <source>%n held change(s) sent now that the sync has finished</source> + <translation> + <numerusform>%n modifica in sospeso inviata ora che la sincronizzazione è terminata</numerusform> + <numerusform>%n modifiche in sospeso inviate ora che la sincronizzazione è terminata</numerusform> + </translation> + </message> + <message> + <source>Sync complete</source> + <translation>Sincronizzazione completata</translation> + </message> + <message> + <source>Changes still to sync</source> + <translation>Modifiche ancora da sincronizzare</translation> + </message> + <message> + <source>Changes you made while the sync was running have only now been applied, so that sync did not carry them. Sync once more before quitting.</source> + <translation>Le modifiche fatte mentre la sincronizzazione era in corso sono state applicate solo ora, quindi quella sincronizzazione non le ha trasferite. Sincronizza di nuovo prima di uscire.</translation> + </message> + <message> + <source>A sync is already running (started elsewhere); this one was skipped</source> + <translation>Una sincronizzazione è già in corso (avviata altrove); questa è stata saltata</translation> + </message> + <message> + <source>Sync already running</source> + <translation>Sincronizzazione già in corso</translation> + </message> + <message> + <source>Another sync was already in progress, so this one was skipped. Your changes are most likely being carried over by that run, but this window cannot see it finish, so it has been left open.</source> + <translation>Un'altra sincronizzazione era già in corso, quindi questa è stata saltata. Con ogni probabilità le tue modifiche vengono trasferite da quella esecuzione, ma questa finestra non può vederla terminare, perciò è stata lasciata aperta.</translation> + </message> + <message> + <source>Sync failed (exit %1)</source> + <translation>Sincronizzazione non riuscita (uscita %1)</translation> + </message> + <message> + <source>The sync failed (exit %1), so your changes are still unsynced. The window has been left open.</source> + <translation>La sincronizzazione non è riuscita (uscita %1), quindi le tue modifiche non sono ancora sincronizzate. La finestra è stata lasciata aperta.</translation> + </message> + <message> + <source>Background sync running...</source> + <translation>Sincronizzazione in corso in background...</translation> + </message> + <message> + <source>Syncing...</source> + <translation>Sincronizzazione in corso...</translation> + </message> + <message numerus="yes"> + <source>%n unsynced change(s)</source> + <translation> + <numerusform>%n modifica non sincronizzata</numerusform> + <numerusform>%n modifiche non sincronizzate</numerusform> + </translation> + </message> + <message> + <source>Tag changes made here that a sync has not yet carried to the mail store. An external notmuch run can clear them without this count noticing.</source> + <translation>Modifiche alle etichette fatte qui che nessuna sincronizzazione ha ancora trasferito all'archivio di posta. Un'esecuzione esterna di notmuch può azzerarle senza che questo conteggio se ne accorga.</translation> + </message> + <message> + <source>Select a thread first</source> + <translation>Seleziona prima una conversazione</translation> + </message> + <message> + <source>Edit tags</source> + <translation>Modifica etichette</translation> + </message> + <message numerus="yes"> + <source>%1: %n message(s) (whole thread)</source> + <translation> + <numerusform>%1: %n messaggio (intera conversazione)</numerusform> + <numerusform>%1: %n messaggi (intera conversazione)</numerusform> + </translation> + </message> + <message numerus="yes"> + <source>%1: %n message(s)</source> + <translation> + <numerusform>%1: %n messaggio</numerusform> + <numerusform>%1: %n messaggi</numerusform> + </translation> + </message> + <message> + <source>A sync is running; your change will be applied when it finishes.</source> + <translation>Una sincronizzazione è in corso; la modifica verrà applicata al termine.</translation> + </message> +</context> +<context> + <name>MessageDetailsDialog</name> + <message> + <source>Message details</source> + <translation>Dettagli del messaggio</translation> + </message> + <message> + <source>Message %1 of %2</source> + <translation>Messaggio %1 di %2</translation> + </message> + <message> + <source>Search for this</source> + <translation>Cerca questo</translation> + </message> + <message> + <source>Add to search</source> + <translation>Aggiungi alla ricerca</translation> + </message> + <message> + <source>Exclude from search</source> + <translation>Escludi dalla ricerca</translation> + </message> + <message> + <source>Subject:</source> + <translation>Oggetto:</translation> + </message> + <message> + <source>From:</source> + <translation>Da:</translation> + </message> + <message> + <source>To:</source> + <translation>A:</translation> + </message> + <message> + <source>Cc:</source> + <translation>Cc:</translation> + </message> + <message> + <source>Date:</source> + <translation>Data:</translation> + </message> + <message> + <source>Message-Id:</source> + <translation>Message-Id:</translation> + </message> +</context> +<context> + <name>MessageView</name> + <message> + <source>Details...</source> + <translation>Dettagli...</translation> + </message> + <message> + <source>Show the full headers of every message</source> + <translation>Mostra le intestazioni complete di ogni messaggio</translation> + </message> + <message> + <source>Remote content blocked</source> + <translation>Contenuti remoti bloccati</translation> + </message> + <message> + <source>Load remote content</source> + <translation>Carica contenuti remoti</translation> + </message> + <message> + <source>This thread no longer matches the current query.</source> + <translation>Questa conversazione non corrisponde più alla ricerca attuale.</translation> + </message> + <message> + <source>Show it anyway</source> + <translation>Mostrala comunque</translation> + </message> + <message> + <source>tag %1</source> + <translation>etichetta %1</translation> + </message> + <message> + <source><b>Cannot display message</b></source> + <translation><b>Impossibile mostrare il messaggio</b></translation> + </message> + <message> + <source>subject "%1"</source> + <translation>oggetto "%1"</translation> + </message> + <message> + <source>mail from %1</source> + <translation>posta da %1</translation> + </message> + <message> + <source>From:</source> + <translation>Da:</translation> + </message> + <message> + <source>To:</source> + <translation>A:</translation> + </message> + <message> + <source>Cc:</source> + <translation>Cc:</translation> + </message> + <message> + <source>sender %1</source> + <translation>mittente %1</translation> + </message> + <message> + <source>recipient %1</source> + <translation>destinatario %1</translation> + </message> + <message> + <source>copied to %1</source> + <translation>in copia a %1</translation> + </message> + <message numerus="yes"> + <source>%n message(s) in thread</source> + <translation> + <numerusform>%n messaggio nella conversazione</numerusform> + <numerusform>%n messaggi nella conversazione</numerusform> + </translation> + </message> + <message> + <source>Search for %1</source> + <translation>Cerca %1</translation> + </message> + <message> + <source>Search for this</source> + <translation>Cerca questo</translation> + </message> + <message> + <source>Add to search</source> + <translation>Aggiungi alla ricerca</translation> + </message> + <message> + <source>Exclude from search</source> + <translation>Escludi dalla ricerca</translation> + </message> + <message> + <source>Attachments (%1)...</source> + <translation>Allegati (%1)...</translation> + </message> + <message> + <source>List the attachments in this thread</source> + <translation>Elenca gli allegati di questa conversazione</translation> + </message> + <message> + <source>Attachments</source> + <translation>Allegati</translation> + </message> + <message> + <source>Message</source> + <translation>Messaggio</translation> + </message> + <message> + <source>File</source> + <translation>File</translation> + </message> + <message> + <source>Size</source> + <translation>Dimensione</translation> + </message> + <message> + <source>Save...</source> + <translation>Salva...</translation> + </message> + <message> + <source>Save all...</source> + <translation>Salva tutto...</translation> + </message> + <message> + <source>Choose a folder. A subfolder "%1" will be created inside it.</source> + <translation>Scegli una cartella. Al suo interno verrà creata una sottocartella "%1".</translation> + </message> + <message> + <source>%1 (%2)</source> + <translation>%1 (%2)</translation> + </message> + <message> + <source>Could not create %1</source> + <translation>Impossibile creare %1</translation> + </message> + <message> + <source>Saved %1 attachment(s) to %2</source> + <translation>Salvati %1 allegati in %2</translation> + </message> + <message> + <source>Saved %1 of %2 to %3; failed: %4</source> + <translation>Salvati %1 di %2 in %3; non riusciti: %4</translation> + </message> + <message> + <source>Save attachment to</source> + <translation>Salva allegato in</translation> + </message> + <message> + <source>Could not save attachment: %1</source> + <translation>Impossibile salvare l'allegato: %1</translation> + </message> + <message> + <source>Saved %1</source> + <translation>Salvato %1</translation> + </message> + <message> + <source>No message in this thread has an HTML part</source> + <translation>Nessun messaggio di questa conversazione ha una parte HTML</translation> + </message> +</context> +<context> + <name>QObject</name> + <message> + <source>qtmaildir</source> + <translation>qtmaildir</translation> + </message> + <message> + <source>libnotmuch 5 or newer is required.</source> + <translation>È richiesta libnotmuch 5 o successiva.</translation> + </message> + <message> + <source>Configuration problems</source> + <translation>Problemi di configurazione</translation> + </message> + <message> + <source>(unnamed)</source> + <translation>(senza nome)</translation> + </message> + <message> + <source>'%1': the name must be lowercase letters, digits and dashes</source> + <translation>'%1': il nome deve contenere solo lettere minuscole, cifre e trattini</translation> + </message> + <message> + <source>'%1': another rule already has this name</source> + <translation>'%1': un'altra regola ha già questo nome</translation> + </message> + <message> + <source>'%1': no query</source> + <translation>'%1': nessuna ricerca</translation> + </message> + <message> + <source>'%1': adds and removes nothing</source> + <translation>'%1': non aggiunge né rimuove nulla</translation> + </message> + <message> + <source>Cannot read %1: %2</source> + <translation>Impossibile leggere %1: %2</translation> + </message> + <message> + <source>%1 uses format version %2, newer than this version understands (%3); refusing to guess</source> + <translation>%1 usa la versione di formato %2, più recente di quella riconosciuta da questa versione (%3); non verranno fatte supposizioni</translation> + </message> + <message> + <source>rule #%1</source> + <translation>regola n. %1</translation> + </message> + <message> + <source>%1: name '%2' is not lowercase letters, digits and dashes; loaded as '%3'. Save to keep it.</source> + <translation>%1: il nome '%2' non è composto da lettere minuscole, cifre e trattini; caricato come '%3'. Salva per mantenerlo.</translation> + </message> + <message> + <source>Rule '%1': duplicate id; keeping the first</source> + <translation>Regola '%1': id duplicato; viene mantenuta la prima</translation> + </message> + <message> + <source>Rule '%1': no query; dropped</source> + <translation>Regola '%1': nessuna ricerca; scartata</translation> + </message> + <message> + <source>Rule '%1': adds and removes nothing; dropped</source> + <translation>Regola '%1': non aggiunge né rimuove nulla; scartata</translation> + </message> +</context> +<context> + <name>QueryCompleter</name> + <message> + <source>also accepts free-form dates, e.g. 2026-01-15 or 15/01/2026..today</source> + <translation>accetta anche date libere, ad es. 2026-01-15 o 15/01/2026..today</translation> + </message> +</context> +<context> + <name>SaveQueryDialog</name> + <message> + <source>Save query</source> + <translation>Salva ricerca</translation> + </message> + <message> + <source>Edit saved query</source> + <translation>Modifica ricerca salvata</translation> + </message> + <message> + <source>A name for this query</source> + <translation>Un nome per questa ricerca</translation> + </message> + <message> + <source>Name</source> + <translation>Nome</translation> + </message> + <message> + <source>Built from your accounts and not editable. It follows the sent folder each account configures.</source> + <translation>Costruita dai tuoi account e non modificabile. Segue la cartella della posta inviata configurata da ciascun account.</translation> + </message> + <message> + <source>Query</source> + <translation>Ricerca</translation> + </message> + <message> + <source>All accounts</source> + <translation>Tutti gli account</translation> + </message> + <message> + <source>Account</source> + <translation>Account</translation> + </message> + <message> + <source>Show as a button</source> + <translation>Mostra come pulsante</translation> + </message> + <message> + <source>A saved query named '%1' already exists and will be replaced.</source> + <translation>Esiste già una ricerca salvata di nome '%1' e verrà sostituita.</translation> + </message> +</context> +<context> + <name>SyncPhaseTracker</name> + <message> + <source>Reindexing (notmuch)...</source> + <translation>Reindicizzazione (notmuch)...</translation> + </message> + <message> + <source>Syncing %1...</source> + <translation>Sincronizzazione di %1...</translation> + </message> + <message> + <source>Syncing mail: %1 channels, %2 boxes</source> + <translation>Sincronizzazione posta: %1 canali, %2 caselle</translation> + </message> + <message> + <source>Syncing mail (mbsync)...</source> + <translation>Sincronizzazione posta (mbsync)...</translation> + </message> +</context> +<context> + <name>TagDialog</name> + <message> + <source>A tag name cannot be empty.</source> + <translation>Il nome di un'etichetta non può essere vuoto.</translation> + </message> + <message> + <source>'%1' cannot start with '-': notmuch reads a leading dash as an instruction to remove a tag.</source> + <translation>'%1' non può iniziare con '-': notmuch interpreta un trattino iniziale come istruzione per rimuovere un'etichetta.</translation> + </message> + <message> + <source>'%1' cannot contain spaces.</source> + <translation>'%1' non può contenere spazi.</translation> + </message> + <message> + <source>'%1' contains a character that cannot be typed back.</source> + <translation>'%1' contiene un carattere che non può essere ridigitato.</translation> + </message> + <message> + <source>Edit tags</source> + <translation>Modifica etichette</translation> + </message> + <message numerus="yes"> + <source>%n selected thread(s)</source> + <translation> + <numerusform>%n conversazione selezionata</numerusform> + <numerusform>%n conversazioni selezionate</numerusform> + </translation> + </message> + <message> + <source>tag, or several separated by commas</source> + <translation>etichetta, o più etichette separate da virgole</translation> + </message> + <message> + <source>Add:</source> + <translation>Aggiungi:</translation> + </message> + <message> + <source>Remove:</source> + <translation>Rimuovi:</translation> + </message> + <message> + <source>%1 (on %2 of %3)</source> + <translation>%1 (su %2 di %3)</translation> + </message> + <message> + <source>Tags on the selection:</source> + <translation>Etichette sulla selezione:</translation> + </message> + <message> + <source>Apply</source> + <translation>Applica</translation> + </message> + <message> + <source>Invalid tag</source> + <translation>Etichetta non valida</translation> + </message> +</context> +<context> + <name>TagRulesDialog</name> + <message> + <source>From</source> + <translation>Da</translation> + </message> + <message> + <source>To</source> + <translation>A</translation> + </message> + <message> + <source>Cc</source> + <translation>Cc</translation> + </message> + <message> + <source>Subject</source> + <translation>Oggetto</translation> + </message> + <message> + <source>Tag</source> + <translation>Etichetta</translation> + </message> + <message> + <source>Folder</source> + <translation>Cartella</translation> + </message> + <message> + <source>Attachment</source> + <translation>Allegato</translation> + </message> + <message> + <source>Date</source> + <translation>Data</translation> + </message> + <message> + <source>Tagging rules</source> + <translation>Regole di etichettatura</translation> + </message> + <message> + <source>Rules tag mail as it arrives, applied by the notmuch post-new hook. Editing them here changes what the next sync does; it does not retag mail you already have.</source> + <translation>Le regole etichettano la posta in arrivo e sono applicate dall'hook post-new di notmuch. Modificarle qui cambia ciò che farà la prossima sincronizzazione; non rietichetta la posta che hai già.</translation> + </message> + <message> + <source>On</source> + <translation>Attiva</translation> + </message> + <message> + <source>Stage</source> + <translation>Fase</translation> + </message> + <message> + <source>Rule</source> + <translation>Regola</translation> + </message> + <message> + <source>Tags</source> + <translation>Etichette</translation> + </message> + <message> + <source>Matches</source> + <translation>Corrispondenze</translation> + </message> + <message> + <source>Applied on every sync</source> + <translation>Applicata a ogni sincronizzazione</translation> + </message> + <message> + <source>comma separated</source> + <translation>separate da virgole</translation> + </message> + <message> + <source>Name</source> + <translation>Nome</translation> + </message> + <message> + <source>Add tags</source> + <translation>Aggiungi etichette</translation> + </message> + <message> + <source>Remove tags</source> + <translation>Rimuovi etichette</translation> + </message> + <message> + <source>Match &all</source> + <translation>Corrispondono &tutte</translation> + </message> + <message> + <source>Match a&ny</source> + <translation>Corrisponde una &qualsiasi</translation> + </message> + <message> + <source>But not</source> + <translation>Ma non</translation> + </message> + <message> + <source>Add e&xclusion</source> + <translation>Aggiungi e&sclusione</translation> + </message> + <message> + <source>Match</source> + <translation>Corrispondenza</translation> + </message> + <message> + <source>Edit as &text</source> + <translation>Modifica come &testo</translation> + </message> + <message> + <source>Edit the notmuch query directly. A rule too complex to show as rows opens this way.</source> + <translation>Modifica direttamente la ricerca notmuch. Una regola troppo complessa da mostrare come righe si apre in questo modo.</translation> + </message> + <message> + <source>Query</source> + <translation>Ricerca</translation> + </message> + <message> + <source>Note</source> + <translation>Nota</translation> + </message> + <message> + <source>&New</source> + <translation>&Nuova</translation> + </message> + <message> + <source>&Copy</source> + <translation>&Copia</translation> + </message> + <message> + <source>&Delete</source> + <translation>&Elimina</translation> + </message> + <message> + <source>Count &matches</source> + <translation>Conta corrispon&denze</translation> + </message> + <message> + <source>Count the messages each rule's query matches across all mail. This does not tag anything.</source> + <translation>Conta i messaggi che la ricerca di ogni regola trova in tutta la posta. Non etichetta nulla.</translation> + </message> + <message> + <source>&Preview in list</source> + <translation>Ante&prima nell'elenco</translation> + </message> + <message> + <source>Run this rule's query in the main window, to see which mail it collects. This does not tag anything.</source> + <translation>Esegue la ricerca di questa regola nella finestra principale, per vedere quale posta raccoglie. Non etichetta nulla.</translation> + </message> + <message> + <source>Dismiss</source> + <translation>Ignora</translation> + </message> + <message numerus="yes"> + <source>%n rule(s) in the file need attention: %1. Save to write them back.</source> + <translation> + <numerusform>%n regola nel file richiede attenzione: %1. Salva per riscriverla.</numerusform> + <numerusform>%n regole nel file richiedono attenzione: %1. Salva per riscriverle.</numerusform> + </translation> + </message> + <message> + <source>invalid</source> + <translation>non valida</translation> + </message> + <message numerus="yes"> + <source>%n rule(s) cannot be saved as they are: %1</source> + <translation> + <numerusform>%n regola non può essere salvata così com'è: %1</numerusform> + <numerusform>%n regole non possono essere salvate così come sono: %1</numerusform> + </translation> + </message> + <message> + <source>Could not write %1.</source> + <translation>Impossibile scrivere %1.</translation> + </message> + <message> + <source>is</source> + <translation>è</translation> + </message> + <message> + <source>is not</source> + <translation>non è</translation> + </message> + <message> + <source>has</source> + <translation>ha</translation> + </message> + <message> + <source>has not</source> + <translation>non ha</translation> + </message> + <message> + <source>before</source> + <translation>prima di</translation> + </message> + <message> + <source>after</source> + <translation>dopo</translation> + </message> + <message> + <source>contains</source> + <translation>contiene</translation> + </message> + <message> + <source>contains not</source> + <translation>non contiene</translation> + </message> + <message> + <source>This query is more than the builder can show, so it stays as text. It is still saved and applied normally.</source> + <translation>Questa ricerca è più complessa di quanto il costruttore possa mostrare, quindi resta come testo. Viene comunque salvata e applicata normalmente.</translation> + </message> +</context> +<context> + <name>ThreadListModel</name> + <message> + <source>Important</source> + <translation>Importante</translation> + </message> + <message> + <source>Has an attachment</source> + <translation>Ha un allegato</translation> + </message> +</context> +<context> + <name>VocabularyStrings</name> + <message> + <source>messages with a tag</source> + <translation>messaggi con un'etichetta</translation> + </message> + <message> + <source>same as tag:</source> + <translation>come tag:</translation> + </message> + <message> + <source>sender address or name</source> + <translation>indirizzo o nome del mittente</translation> + </message> + <message> + <source>recipient, including Cc</source> + <translation>destinatario, Cc compreso</translation> + </message> + <message> + <source>words in the subject</source> + <translation>parole nell'oggetto</translation> + </message> + <message> + <source>a date or a range</source> + <translation>una data o un intervallo</translation> + </message> + <message> + <source>attachment filename</source> + <translation>nome file dell'allegato</translation> + </message> + <message> + <source>attachment content type</source> + <translation>tipo di contenuto dell'allegato</translation> + </message> + <message> + <source>Maildir folder name</source> + <translation>nome della cartella Maildir</translation> + </message> + <message> + <source>directory below the Maildir root</source> + <translation>directory sotto la radice della Maildir</translation> + </message> + <message> + <source>a thread id</source> + <translation>un id di conversazione</translation> + </message> + <message> + <source>a single message id</source> + <translation>un singolo id di messaggio</translation> + </message> + <message> + <source>both conditions</source> + <translation>entrambe le condizioni</translation> + </message> + <message> + <source>either condition</source> + <translation>una delle due condizioni</translation> + </message> + <message> + <source>exclude what follows</source> + <translation>esclude ciò che segue</translation> + </message> + <message> + <source>since midnight</source> + <translation>da mezzanotte</translation> + </message> + <message> + <source>the previous day</source> + <translation>il giorno precedente</translation> + </message> + <message> + <source>the current week</source> + <translation>la settimana corrente</translation> + </message> + <message> + <source>the week before this one</source> + <translation>la settimana precedente</translation> + </message> + <message> + <source>the current month</source> + <translation>il mese corrente</translation> + </message> + <message> + <source>the month before this one</source> + <translation>il mese precedente</translation> + </message> + <message> + <source>the current year</source> + <translation>l'anno corrente</translation> + </message> + <message> + <source>the last seven days</source> + <translation>gli ultimi sette giorni</translation> + </message> + <message> + <source>the last month</source> + <translation>l'ultimo mese</translation> + </message> + <message> + <source>PDF document</source> + <translation>documento PDF</translation> + </message> + <message> + <source>JPEG image</source> + <translation>immagine JPEG</translation> + </message> + <message> + <source>PNG image</source> + <translation>immagine PNG</translation> + </message> + <message> + <source>HTML document</source> + <translation>documento HTML</translation> + </message> + <message> + <source>ZIP archive</source> + <translation>archivio ZIP</translation> + </message> + <message> + <source>account directory</source> + <translation>directory dell'account</translation> + </message> + <message> + <source>and everything below it</source> + <translation>e tutto ciò che contiene</translation> + </message> +</context> +</TS> |
