Settings → Connections: sync reports, push errors and the private-address refusal are English only, with capture wording #384

Closed
opened 2026-10-01 10:46:18 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, following each message a connection shows back to its owner.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at plugins/http.py:36-46 (check_destination) and :49-54 (_refused, used by _guard at :77), plugins/public_addresses.py:53-67 (the four reasons in public_addresses_for, which #248 moved out of plugins/fetching.py) and plugins/views.py:460 (the Sync now flash); the other locations are unchanged.

Several sentences a person reads on Settings → Connections never pass through gettext, so they are English whatever language the page is in, and one of them is also wrong for the place it appears.

Related to #335, which makes sync_connection run in its owner's language and time zone, so that what a plugin words during a scheduled run (its notes included) is no longer in the instance's language. This issue adds what #335 does not reach: the counters of the report are not translatable in any language, and two more families of messages on the same page are not either.

1. The sync report

SyncReport.summary() words its counters with f-strings and joins the plugin's notes to them. sync_connection stores the result in Connection.last_summary, and the Connections page prints it inside a translated sentence.

  • Where:
    • plugins/base.py:815-828, SyncReport.summary(): f"{self.pushed} pushed", f"{self.pulled} pulled", f"{self.removed} removed", f"{self.skipped} skipped" and "nothing to do".
    • plugins/syncing.py:92, sync_connection: connection.last_summary = report.summary()[:500]; run_syncs (:110-137) calls it with no translation.override, which is #335's subject.
    • templates/connections/list.html:47-49: Last run {{ when }}: {{ report }}.
    • plugins/views.py:459, ConnectionSyncNowView: the flash "Synced: %(summary)s.".
    • tests/test_syncs.py:93-95, :140, :151 and :220-221 pin the English words.
  • What happens: a person reading in German sees "Letzter Lauf …: 3 pushed, 1 pulled · …". None of "pushed", "pulled", "removed", "skipped" or "nothing to do" is in any catalogue, and each counter needs its own plural forms and agreement in most languages, which an f-string cannot give. Because the sentence is stored, even translated counters would be frozen in the language of the run: after Sync now the report follows the request's language, and after the next scheduled run it is rewritten in the instance default.

2. The private-address refusal for connections

check_destination and _refused append the same English sentence, written twice, to whatever UnsafeURL said.

  • Where:
    • plugins/http.py:34-44 (check_destination) and :47-52 (_refused, used by _guard at :75): f"{exc} Connections may only reach private or local addresses when the operator sets POSTULO_CONNECTIONS_ALLOW_PRIVATE=true.", with no gettext.
    • plugins/fetching.py:121-133 (public_addresses_for): the four reasons it is appended to, all worded for capturing a posting.
    • Shown by plugins/webhook/__init__.py:184-186 (form error) and :217-220 (Test result), and wherever a plugin's DestinationRefused becomes a connection's last_error.
  • What happens:
    1. The policy sentence is added whatever the refusal was. A webhook host that does not resolve gives a translated "That hostname could not be resolved." followed by an English sentence about a policy that had nothing to do with it; the same follows "Only http and https addresses can be captured." and "That does not look like a complete web address.".
    2. When the address is private, the reason is the capture one: "That address is on a private or local network, and Postulo will not fetch it. Paste the posting text in by hand instead.", then the English sentence. Advice about pasting a posting appears on a webhook form.

3. Web Push messages

plugins/browser/webpush.py raises plain English strings that the browser notifier shows as they are.

  • Where:

    • plugins/browser/webpush.py:191-205, parse_subscription: "The subscription is not something a browser wrote.", "A push address has to be HTTPS." and "The subscription's keys are not the browser's.", shown as a form error by plugins/browser/__init__.py:130-134 (validate) and as a Test result by :190-194 (test).
    • plugins/browser/webpush.py:251-254, push: f"The push service answered {response.status_code}.", shown as a Test result by plugins/browser/__init__.py:225 and, re-raised from send (:185), kept as the connection's last error. The comment above it says so: "this string is shown on the connection and stored in last_error".
  • What happens: a person reading in Portuguese who tests a browser connection while the push service is failing reads "The push service answered 503." under a Portuguese heading. (The 404/410 message at :242-246 is replaced by a translated sentence in both send and test, so it is not part of this.)

  • Why it matters: Postulo promises that every user-facing string is translatable. These are the lines that tell a person whether their address book and calendar are in step, why a webhook was refused and why a push did not arrive, on a page every connected person sees.

  • Fix:

    • Sync report: keep the numbers rather than a sentence (for example a JSON field with pushed, pulled, removed, skipped and notes, or the counters beside last_summary), and word the counters at display time with ngettext in the reader's language, with a translated "nothing to do". ConnectionSyncNowView words its flash the same way. The notes stay as the plugin wrote them, in the owner's language once #335 lands.
    • Refusal: write the policy sentence once, under gettext. Give UnsafeURL a kind (or raise distinct subclasses from public_addresses_for) so check_destination and _refused add the operator sentence only for a private address, and give connections their own wording for that case instead of the capture one.
    • Web Push: put the webpush.py messages through gettext, in the browser plugin's own catalogue (plugins/browser/locale/), not the core's.

Done when

  • tests/test_syncs.py renders the Connections list under de after a sync that pushed and pulled, and asserts the counters are not the English words; and asserts the Sync now flash is worded in the request's language.
  • tests/security/test_outbound.py asserts that a destination refused for an unresolvable host does not mention POSTULO_CONNECTIONS_ALLOW_PRIVATE, and that one refused for a private address mentions it and does not mention pasting a posting.
  • tests/test_browser_notifier.py asserts, under a language the browser plugin ships, that the error for an http:// push address and the Test result for a push service answering 503 are not the English sentences.
  • tests/test_translations.py sees the new msgids in every catalogue, the browser plugin's included.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, following each message a connection shows back to its owner. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `plugins/http.py:36-46` (`check_destination`) and `:49-54` (`_refused`, used by `_guard` at `:77`), `plugins/public_addresses.py:53-67` (the four reasons in `public_addresses_for`, which #248 moved out of `plugins/fetching.py`) and `plugins/views.py:460` (the *Sync now* flash); the other locations are unchanged. Several sentences a person reads on Settings → Connections never pass through gettext, so they are English whatever language the page is in, and one of them is also wrong for the place it appears. Related to #335, which makes `sync_connection` run in its owner's language and time zone, so that what a plugin words during a scheduled run (its notes included) is no longer in the instance's language. This issue adds what #335 does not reach: the counters of the report are not translatable in any language, and two more families of messages on the same page are not either. ## 1. The sync report `SyncReport.summary()` words its counters with f-strings and joins the plugin's notes to them. `sync_connection` stores the result in `Connection.last_summary`, and the Connections page prints it inside a translated sentence. - **Where:** - `plugins/base.py:815-828`, `SyncReport.summary()`: `f"{self.pushed} pushed"`, `f"{self.pulled} pulled"`, `f"{self.removed} removed"`, `f"{self.skipped} skipped"` and `"nothing to do"`. - `plugins/syncing.py:92`, `sync_connection`: `connection.last_summary = report.summary()[:500]`; `run_syncs` (`:110-137`) calls it with no `translation.override`, which is #335's subject. - `templates/connections/list.html:47-49`: `Last run {{ when }}: {{ report }}`. - `plugins/views.py:459`, `ConnectionSyncNowView`: the flash "Synced: %(summary)s.". - `tests/test_syncs.py:93-95`, `:140`, `:151` and `:220-221` pin the English words. - **What happens:** a person reading in German sees "Letzter Lauf …: 3 pushed, 1 pulled · …". None of "pushed", "pulled", "removed", "skipped" or "nothing to do" is in any catalogue, and each counter needs its own plural forms and agreement in most languages, which an f-string cannot give. Because the sentence is stored, even translated counters would be frozen in the language of the run: after *Sync now* the report follows the request's language, and after the next scheduled run it is rewritten in the instance default. ## 2. The private-address refusal for connections `check_destination` and `_refused` append the same English sentence, written twice, to whatever `UnsafeURL` said. - **Where:** - `plugins/http.py:34-44` (`check_destination`) and `:47-52` (`_refused`, used by `_guard` at `:75`): `f"{exc} Connections may only reach private or local addresses when the operator sets POSTULO_CONNECTIONS_ALLOW_PRIVATE=true."`, with no gettext. - `plugins/fetching.py:121-133` (`public_addresses_for`): the four reasons it is appended to, all worded for capturing a posting. - Shown by `plugins/webhook/__init__.py:184-186` (form error) and `:217-220` (Test result), and wherever a plugin's `DestinationRefused` becomes a connection's `last_error`. - **What happens:** 1. The policy sentence is added whatever the refusal was. A webhook host that does not resolve gives a translated "That hostname could not be resolved." followed by an English sentence about a policy that had nothing to do with it; the same follows "Only http and https addresses can be captured." and "That does not look like a complete web address.". 2. When the address is private, the reason is the capture one: "That address is on a private or local network, and Postulo will not fetch it. Paste the posting text in by hand instead.", then the English sentence. Advice about pasting a posting appears on a webhook form. ## 3. Web Push messages `plugins/browser/webpush.py` raises plain English strings that the browser notifier shows as they are. - **Where:** - `plugins/browser/webpush.py:191-205`, `parse_subscription`: "The subscription is not something a browser wrote.", "A push address has to be HTTPS." and "The subscription's keys are not the browser's.", shown as a form error by `plugins/browser/__init__.py:130-134` (`validate`) and as a Test result by `:190-194` (`test`). - `plugins/browser/webpush.py:251-254`, `push`: `f"The push service answered {response.status_code}."`, shown as a Test result by `plugins/browser/__init__.py:225` and, re-raised from `send` (`:185`), kept as the connection's last error. The comment above it says so: "this string is shown on the connection and stored in `last_error`". - **What happens:** a person reading in Portuguese who tests a browser connection while the push service is failing reads "The push service answered 503." under a Portuguese heading. (The 404/410 message at `:242-246` is replaced by a translated sentence in both `send` and `test`, so it is not part of this.) - **Why it matters:** Postulo promises that every user-facing string is translatable. These are the lines that tell a person whether their address book and calendar are in step, why a webhook was refused and why a push did not arrive, on a page every connected person sees. - **Fix:** - Sync report: keep the numbers rather than a sentence (for example a JSON field with `pushed`, `pulled`, `removed`, `skipped` and `notes`, or the counters beside `last_summary`), and word the counters at display time with `ngettext` in the reader's language, with a translated "nothing to do". `ConnectionSyncNowView` words its flash the same way. The notes stay as the plugin wrote them, in the owner's language once #335 lands. - Refusal: write the policy sentence once, under gettext. Give `UnsafeURL` a kind (or raise distinct subclasses from `public_addresses_for`) so `check_destination` and `_refused` add the operator sentence only for a private address, and give connections their own wording for that case instead of the capture one. - Web Push: put the `webpush.py` messages through gettext, in the browser plugin's own catalogue (`plugins/browser/locale/`), not the core's. ## Done when - `tests/test_syncs.py` renders the Connections list under `de` after a sync that pushed and pulled, and asserts the counters are not the English words; and asserts the *Sync now* flash is worded in the request's language. - `tests/security/test_outbound.py` asserts that a destination refused for an unresolvable host does not mention `POSTULO_CONNECTIONS_ALLOW_PRIVATE`, and that one refused for a private address mentions it and does not mention pasting a posting. - `tests/test_browser_notifier.py` asserts, under a language the browser plugin ships, that the error for an `http://` push address and the Test result for a push service answering 503 are not the English sentences. - `tests/test_translations.py` sees the new msgids in every catalogue, the browser plugin's included.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#384
No description provided.