Plugins pages: every shipped plugin's description is shown in English, whatever language the reader uses #633

Closed
opened 2026-10-01 10:48:32 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, checking which manifest fields of the built-in plugins are still lazy once the classes exist.

Checked again on 2026-10-01 against main at 0a5cad587: still present. plugins/base.py:319 and :287-289 are where they were; the row built in plugins/policy.py is now at :294, and the two in core/server_views.py at :1072 and :1094.

shipped() turns a built-in plugin's description into a plain string when the plugin's module is imported, which is at start-up, in LANGUAGE_CODE (en-gb). The translations of these paragraphs, present in every plugin's catalogue, can never be shown.

  • Where:
    • plugins/base.py:319 (shipped): description=str(description), while label=label is passed as given.
    • plugins/base.py:287-289 (description_of), plugins/policy.py:303, core/server_views.py:1071 and :1093, core/gdpr.py:390: each hands the already-resolved string on.
    • templates/settings/plugins.html:95-96, templates/server/plugins.html:61, templates/server/person_plugins.html:54-55, templates/server/email.html:41: printed as {{ row.description }} or {{ manifest.description }}, with no second translation.
    • Every caller passes a gettext_lazy string, for example plugins/gdpr/__init__.py:45-52 and plugins/postal_rules/__init__.py:85-91. The modules are imported from the apps' ready() (core/apps.py:13-19, plugins/apps.py:17-34).
  • What happens: a person reading Postulo in Portuguese opens Settings → Plugins, or an administrator opens Server settings → Plugins or one person's plugins. Each row has a translated name (where the name is a translatable one) and an English paragraph under it. This holds for all nineteen plugins built with shipped(), in every language. Run against the snapshot: for every built-in, isinstance(manifest.description, Promise) is false, while the feature labels are lazy. With a pt-pt catalogue entry added in memory for both strings of the gdpr plugin and translation.override("pt-pt"), label_of returned the Portuguese entry and description_of returned "The data the instance keeps about other people: …". The Portuguese text for that paragraph is in plugins/gdpr/locale/pt_PT/LC_MESSAGES/django.po.
  • Why it matters: the page where a person decides what to switch on explains each choice in a language they may not read, and the translators' work on these strings is never used. A third-party plugin that builds Manifest(description=_(…)) itself is not affected.
  • Fix: pass the description through unchanged in shipped() (drop the str()), so the manifest keeps the lazy string and description_of() resolves it per request, as it already does with str(manifest_of(plugin).description). The templates that print manifest.description then resolve it at render time. Where a manifest is serialised or compared, call str() at that point.

Done when

  • A test in tests/test_plugin_manifests.py asserts that, under translation.override with a catalogue entry for the description, description_of(<a shipped plugin>) returns the translation; it fails today.
  • The same file asserts that no built-in's manifest.description is a plain str resolved at import.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, checking which manifest fields of the built-in plugins are still lazy once the classes exist. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. `plugins/base.py:319` and `:287-289` are where they were; the row built in `plugins/policy.py` is now at `:294`, and the two in `core/server_views.py` at `:1072` and `:1094`. `shipped()` turns a built-in plugin's description into a plain string when the plugin's module is imported, which is at start-up, in `LANGUAGE_CODE` (`en-gb`). The translations of these paragraphs, present in every plugin's catalogue, can never be shown. - **Where:** - `plugins/base.py:319` (`shipped`): `description=str(description),` while `label=label` is passed as given. - `plugins/base.py:287-289` (`description_of`), `plugins/policy.py:303`, `core/server_views.py:1071` and `:1093`, `core/gdpr.py:390`: each hands the already-resolved string on. - `templates/settings/plugins.html:95-96`, `templates/server/plugins.html:61`, `templates/server/person_plugins.html:54-55`, `templates/server/email.html:41`: printed as `{{ row.description }}` or `{{ manifest.description }}`, with no second translation. - Every caller passes a `gettext_lazy` string, for example `plugins/gdpr/__init__.py:45-52` and `plugins/postal_rules/__init__.py:85-91`. The modules are imported from the apps' `ready()` (`core/apps.py:13-19`, `plugins/apps.py:17-34`). - **What happens:** a person reading Postulo in Portuguese opens *Settings → Plugins*, or an administrator opens *Server settings → Plugins* or one person's plugins. Each row has a translated name (where the name is a translatable one) and an English paragraph under it. This holds for all nineteen plugins built with `shipped()`, in every language. Run against the snapshot: for every built-in, `isinstance(manifest.description, Promise)` is false, while the feature labels are lazy. With a `pt-pt` catalogue entry added in memory for both strings of the `gdpr` plugin and `translation.override("pt-pt")`, `label_of` returned the Portuguese entry and `description_of` returned "The data the instance keeps about other people: …". The Portuguese text for that paragraph is in `plugins/gdpr/locale/pt_PT/LC_MESSAGES/django.po`. - **Why it matters:** the page where a person decides what to switch on explains each choice in a language they may not read, and the translators' work on these strings is never used. A third-party plugin that builds `Manifest(description=_(…))` itself is not affected. - **Fix:** pass the description through unchanged in `shipped()` (drop the `str()`), so the manifest keeps the lazy string and `description_of()` resolves it per request, as it already does with `str(manifest_of(plugin).description)`. The templates that print `manifest.description` then resolve it at render time. Where a manifest is serialised or compared, call `str()` at that point. ## Done when - A test in `tests/test_plugin_manifests.py` asserts that, under `translation.override` with a catalogue entry for the description, `description_of(<a shipped plugin>)` returns the translation; it fails today. - The same file asserts that no built-in's `manifest.description` is a plain `str` resolved at import.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:48:32 +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#633
No description provided.