Translations: a working round trip between this repository and its Weblate project on translate.tiagoagueda.com #349

Closed
opened 2026-10-01 10:14:17 +00:00 by tiagoagueda · 4 comments
Owner

Requested by the maintainer on 2026-10-01, for every repository: integration and a fully working workflow of string translation with the corresponding translation projects hosted on translate.tiagoagueda.com (Weblate). This is the core's issue and the one the others point at.

Where it stands

As set up on 2026-09-16 (to be checked against the server before starting):

  • One Weblate project, postulo, with 21 components: postulo (src/postulo/locale/*/LC_MESSAGES/django.po), one plugin-<name> for each of the 15 plugin catalogue sets that were in the tree that day, and one for each of the five plugin repositories. All gettext PO, new_lang: none, POSIX language codes.
  • It is read-only, by decision at the time: no push address, so Weblate pulls from Forgejo and nothing it holds reaches the repository. Translations made there have to be exported and committed by hand, which nobody has a procedure for.
  • The tree has 17 plugin catalogue sets today (src/postulo/plugins/*/locale), so at least two have no component.
  • The browser extensions, postulo-thunderbird and postulo-templates have no component at all.

So the first half of a round trip exists for most catalogues, and the second half does not exist for any.

What "fully working" means

The same four things in every repository:

  1. Out: a string added or changed here appears in Weblate without anybody doing anything (Weblate is told of each push, or pulls on a schedule).
  2. In: a translation saved in Weblate arrives here without anybody exporting a file: Weblate commits and proposes the change, and this repository's CI runs on it before it reaches main.
  3. Accepted: what Weblate writes passes this repository's own catalogue checks unchanged, and what this repository's tooling writes does not show up in Weblate as a change.
  4. Explained: the README (and the core's docs/TRANSLATING.md) tell a speaker where to go and what happens to what they type.

What has to be settled or built in the core

  • How a translation comes back. Weblate can push to a branch and open a pull request on Forgejo, so that CI judges the catalogues before main moves. main is what CI, the dev image and releases run from, which is why a direct push was declined; say which it is, create the token it needs, and record where the token lives.
  • How Weblate hears of a push. A Forgejo webhook to Weblate's hook, per repository, or a schedule.
  • The draft flag. A string a machine wrote and no speaker has read carries draft in the catalogue (docs/TRANSLATING.md), and the interface's translation bars count it (#312). Weblate knows fuzzy and its own review states, not draft. Decide the mapping in both directions: a draft should arrive in Weblate as needing a reader, and a speaker approving it there should clear the flag here.
  • One format, two writers. scripts/messages.py extract --check fails on a catalogue that differs from what the tool writes (header comment, #: references, wrapping, the order of entries). Either Weblate is configured to write exactly that, or the tool normalises what arrives; tests/test_translations.py::test_the_catalogues_are_what_a_fresh_extraction_writes is the test that will say.
  • Every catalogue set has a component, kept true by a check rather than by memory: a new bundled plugin with a locale/ and no component is a failure somewhere visible.
  • Every scaffolded language is there (#347): with new_lang: none, a language exists in Weblate when its catalogue exists here, so scaffolding every language is what makes each one translatable by a speaker.
  • The gates still hold (#347): a commit from Weblate must not be able to break the development gate (fr-FR and pt-PT complete), and a release reads the languages it selects, however far Weblate has got with the others.
  • Codes are written as #337 decides; the component's language-code style follows from it.
  • Who may translate, and on what terms. Sign-in is through the instance's identity provider today. Say how a speaker who is not the maintainer gets an account, and under what licence their translation comes in (#346 asks the same question for code).
  • docs/TRANSLATING.md and the wiki describe the path from a speaker's point of view. This is the part of #72 that reads "a translator does not need a developer".

Done when

  • A string added in a commit is visible in Weblate, in every scaffolded language, without anybody touching Weblate.
  • A translation saved in Weblate arrives as a pull request (or the decided equivalent), passes CI, and after merging is in the catalogue with the right flags; no file was exported by hand.
  • scripts/messages.py extract --check passes on a catalogue Weblate wrote, and Weblate shows no change after extract runs on an unchanged tree.
  • Every catalogue set in the tree has its component, and a check fails when one does not.
  • A speaker can follow docs/TRANSLATING.md from "I speak this language" to "my translation is in the next release" without asking anybody.

The other repositories

Each has the same issue, for its own catalogues: postulo/postulo-apprise#8, postulo/postulo-dav#17, postulo/postulo-imap#6, postulo/postulo-paperless#8, postulo/postulo-helloworld#7, postulo/postulo-chromium#13, postulo/postulo-thunderbird#10, postulo/postulo-firefox#6, postulo/postulo-templates#10, postulo/postulo-mcp#5.

Requested by the maintainer on 2026-10-01, for every repository: **integration and a fully working workflow of string translation with the corresponding translation projects hosted on translate.tiagoagueda.com (Weblate).** This is the core's issue and the one the others point at. ## Where it stands As set up on 2026-09-16 (to be checked against the server before starting): - One Weblate project, `postulo`, with 21 components: `postulo` (`src/postulo/locale/*/LC_MESSAGES/django.po`), one `plugin-<name>` for each of the 15 plugin catalogue sets that were in the tree that day, and one for each of the five plugin repositories. All gettext PO, `new_lang: none`, POSIX language codes. - **It is read-only, by decision at the time:** no push address, so Weblate pulls from Forgejo and nothing it holds reaches the repository. Translations made there have to be exported and committed by hand, which nobody has a procedure for. - The tree has 17 plugin catalogue sets today (`src/postulo/plugins/*/locale`), so at least two have no component. - The browser extensions, postulo-thunderbird and postulo-templates have no component at all. So the first half of a round trip exists for most catalogues, and the second half does not exist for any. ## What "fully working" means The same four things in every repository: 1. **Out:** a string added or changed here appears in Weblate without anybody doing anything (Weblate is told of each push, or pulls on a schedule). 2. **In:** a translation saved in Weblate arrives here without anybody exporting a file: Weblate commits and proposes the change, and this repository's CI runs on it before it reaches `main`. 3. **Accepted:** what Weblate writes passes this repository's own catalogue checks unchanged, and what this repository's tooling writes does not show up in Weblate as a change. 4. **Explained:** the README (and the core's `docs/TRANSLATING.md`) tell a speaker where to go and what happens to what they type. ## What has to be settled or built in the core - **How a translation comes back.** Weblate can push to a branch and open a pull request on Forgejo, so that CI judges the catalogues before `main` moves. `main` is what CI, the dev image and releases run from, which is why a direct push was declined; say which it is, create the token it needs, and record where the token lives. - **How Weblate hears of a push.** A Forgejo webhook to Weblate's hook, per repository, or a schedule. - **The `draft` flag.** A string a machine wrote and no speaker has read carries `draft` in the catalogue (`docs/TRANSLATING.md`), and the interface's translation bars count it (#312). Weblate knows `fuzzy` and its own review states, not `draft`. Decide the mapping in both directions: a draft should arrive in Weblate as needing a reader, and a speaker approving it there should clear the flag here. - **One format, two writers.** `scripts/messages.py extract --check` fails on a catalogue that differs from what the tool writes (header comment, `#:` references, wrapping, the order of entries). Either Weblate is configured to write exactly that, or the tool normalises what arrives; `tests/test_translations.py::test_the_catalogues_are_what_a_fresh_extraction_writes` is the test that will say. - **Every catalogue set has a component**, kept true by a check rather than by memory: a new bundled plugin with a `locale/` and no component is a failure somewhere visible. - **Every scaffolded language is there** (#347): with `new_lang: none`, a language exists in Weblate when its catalogue exists here, so scaffolding every language is what makes each one translatable by a speaker. - **The gates still hold** (#347): a commit from Weblate must not be able to break the development gate (fr-FR and pt-PT complete), and a release reads the languages it selects, however far Weblate has got with the others. - **Codes** are written as #337 decides; the component's language-code style follows from it. - **Who may translate, and on what terms.** Sign-in is through the instance's identity provider today. Say how a speaker who is not the maintainer gets an account, and under what licence their translation comes in (#346 asks the same question for code). - **`docs/TRANSLATING.md` and the wiki** describe the path from a speaker's point of view. This is the part of #72 that reads "a translator does not need a developer". ## Done when - A string added in a commit is visible in Weblate, in every scaffolded language, without anybody touching Weblate. - A translation saved in Weblate arrives as a pull request (or the decided equivalent), passes CI, and after merging is in the catalogue with the right flags; no file was exported by hand. - `scripts/messages.py extract --check` passes on a catalogue Weblate wrote, and Weblate shows no change after `extract` runs on an unchanged tree. - Every catalogue set in the tree has its component, and a check fails when one does not. - A speaker can follow `docs/TRANSLATING.md` from "I speak this language" to "my translation is in the next release" without asking anybody. ## The other repositories Each has the same issue, for its own catalogues: postulo/postulo-apprise#8, postulo/postulo-dav#17, postulo/postulo-imap#6, postulo/postulo-paperless#8, postulo/postulo-helloworld#7, postulo/postulo-chromium#13, postulo/postulo-thunderbird#10, postulo/postulo-firefox#6, postulo/postulo-templates#10, postulo/postulo-mcp#5.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:14:17 +00:00
Author
Owner

What Weblate holds today, read on the server on 2026-10-01 after postulo/postulo#337 renamed Serbian's catalogues to sr_Cyrl. Nothing was changed there.

Weblate has merged nothing since the day it was set up. AUTO_UPDATE is off and no repository has a webhook, so the nightly task fetches and never merges. The postulo checkout is at its 2026-09-16 state, 132 commits behind what it has fetched and 148 behind a6363a397; each plugin component is a commit or two behind. Every component still shows the September catalogue: 2,117 strings in postulo, where the tree has over 3,000.

The postulo component cannot merge at all. It carries a commit of its own, 75ad12b6b, from the days after it was set up: one translation made in Weblate (pt-PT, API token: "Token de API" became "Token da API"), with the header rewritten the way Weblate writes one (Last-Translator, Language-Team, X-Generator). Those header lines sit beside POT-Creation-Date, which every extract changes, so merging main conflicts in pt_PT/LC_MESSAGES/django.po. Reproduced in a scratch clone: one conflict, in the header; the translation itself merges. This is "one format, two writers" from the body, met for real with a single translation. plugin-paperless carries a header-only commit made with it (fa54e1e) and merges cleanly.

That translation never reached the repository. main still says "Token de API" in pt-PT and pt-BR; postulo-paperless says "Token da API".

What the Serbian rename will do when a merge does happen. Weblate reads a directory named sr_Cyrl as its language sr_Cyrl, Serbian (Cyrillic script), which is a different language there from sr, Serbian. In each of the 21 components the Serbian translation goes, since its file is gone, and Serbian (Cyrillic script) is made from the file. Nothing is lost by that: Serbian has no suggestion, no comment, no pending change and no edit by a person in Weblate. Its address changes from /sr/ to /sr_Cyrl/. A project language alias (sr_Cyrl:sr) would keep the old name; the new one says what the catalogue is. No other directory changed name, and fr_FR is read as fr as before.

To bring Weblate level, in this order:

  1. Take the one translation into the repository ("Token da API" for API token in pt-PT, and pt-BR if it should follow), so that nothing exists only in Weblate.
  2. Reset the postulo and plugin-paperless components to upstream (Repository maintenance → Reset), which discards those two commits.
  3. Update the six components that clone; the fifteen linked ones follow postulo. That is when Serbian changes over.

After that, whatever this issue decides about the header and about how translations come back is what stops it happening again at the next translation.

What Weblate holds today, read on the server on 2026-10-01 after postulo/postulo#337 renamed Serbian's catalogues to `sr_Cyrl`. Nothing was changed there. **Weblate has merged nothing since the day it was set up.** `AUTO_UPDATE` is off and no repository has a webhook, so the nightly task fetches and never merges. The `postulo` checkout is at its 2026-09-16 state, 132 commits behind what it has fetched and 148 behind `a6363a397`; each plugin component is a commit or two behind. Every component still shows the September catalogue: 2,117 strings in `postulo`, where the tree has over 3,000. **The `postulo` component cannot merge at all.** It carries a commit of its own, `75ad12b6b`, from the days after it was set up: one translation made in Weblate (pt-PT, *API token*: "Token de API" became "Token da API"), with the header rewritten the way Weblate writes one (`Last-Translator`, `Language-Team`, `X-Generator`). Those header lines sit beside `POT-Creation-Date`, which every `extract` changes, so merging `main` conflicts in `pt_PT/LC_MESSAGES/django.po`. Reproduced in a scratch clone: one conflict, in the header; the translation itself merges. This is "one format, two writers" from the body, met for real with a single translation. `plugin-paperless` carries a header-only commit made with it (`fa54e1e`) and merges cleanly. **That translation never reached the repository.** `main` still says "Token de API" in pt-PT and pt-BR; postulo-paperless says "Token da API". **What the Serbian rename will do when a merge does happen.** Weblate reads a directory named `sr_Cyrl` as its language `sr_Cyrl`, *Serbian (Cyrillic script)*, which is a different language there from `sr`, *Serbian*. In each of the 21 components the *Serbian* translation goes, since its file is gone, and *Serbian (Cyrillic script)* is made from the file. Nothing is lost by that: Serbian has no suggestion, no comment, no pending change and no edit by a person in Weblate. Its address changes from `/sr/` to `/sr_Cyrl/`. A project language alias (`sr_Cyrl:sr`) would keep the old name; the new one says what the catalogue is. No other directory changed name, and `fr_FR` is read as `fr` as before. **To bring Weblate level, in this order:** 1. Take the one translation into the repository ("Token da API" for *API token* in pt-PT, and pt-BR if it should follow), so that nothing exists only in Weblate. 2. Reset the `postulo` and `plugin-paperless` components to upstream (*Repository maintenance → Reset*), which discards those two commits. 3. Update the six components that clone; the fifteen linked ones follow `postulo`. That is when Serbian changes over. After that, whatever this issue decides about the header and about how translations come back is what stops it happening again at the next translation.
Author
Owner

Done on 2026-10-01, at the maintainer's word: Weblate is level with every repository. The three steps of the comment above, and one more.

  1. The translation that existed only in Weblate is in the repository: e7539cce0 here ("Token da API" for API token in pt-PT, with the three header lines Weblate wrote and the catalogue tool keeps), and postulo-paperless 866be38 for its header-only commit. pt-BR is as it was; the commit in Weblate did not touch it.
  2. postulo and plugin-paperless were reset to upstream, through the API, which discarded those two commits now that main holds what they said.
  3. The other four cloning components were updated, and Weblate rescanned everything.
  4. Two components were added, plugin-gdpr (Plugin: Data protection) and plugin-webhook, linked to postulo and configured as the other fifteen are. The tree has seventeen plugin sets and Weblate had fifteen; that is one line of this issue's body, done.

What Weblate holds now, read back through the API: 23 components, each at its repository's head with nothing to merge, push or commit and no merge failure; 69 languages in each; 3,065 strings in postulo. Serbian is Serbian (Cyrillic script), sr_Cyrl, in all 23, and plain Serbian is gone from them. fr_FR is still read as French and pt_PT, pt_BR as before: a directory name is gettext's and Weblate reads it its own way, so nothing about postulo/postulo#337 needed setting there.

What has not changed, and will put it behind again:

  • AUTO_UPDATE is off and no repository has a webhook, so Weblate fetches every night and merges nothing. It is level as of e7539cce0 and each plugin's head, and drifts from the next commit on.
  • The next translation made in Weblate rewrites a header again, and the next merge conflicts again. Until this issue settles one format for two writers, a translation made there has to be taken into the repository by hand, and the component reset.

Found beside it, not repaired: the project's glossary component has no git repository under it (/app/data/vcs/postulo/glossary holds the .tbx files and no .git). It has been locked with a merge failure since 2026-09-17, a task fails on it after every update, and it holds no term in any language. It still lists plain Serbian. Weblate's own LocalRepository.create_blank_repository on that directory, then adding the files, would give it one.

Done on 2026-10-01, at the maintainer's word: Weblate is level with every repository. The three steps of the comment above, and one more. 1. **The translation that existed only in Weblate is in the repository**: `e7539cce0` here ("Token da API" for *API token* in pt-PT, with the three header lines Weblate wrote and the catalogue tool keeps), and postulo-paperless `866be38` for its header-only commit. pt-BR is as it was; the commit in Weblate did not touch it. 2. **`postulo` and `plugin-paperless` were reset to upstream**, through the API, which discarded those two commits now that `main` holds what they said. 3. **The other four cloning components were updated**, and Weblate rescanned everything. 4. **Two components were added**, `plugin-gdpr` (*Plugin: Data protection*) and `plugin-webhook`, linked to `postulo` and configured as the other fifteen are. The tree has seventeen plugin sets and Weblate had fifteen; that is one line of this issue's body, done. **What Weblate holds now**, read back through the API: 23 components, each at its repository's head with nothing to merge, push or commit and no merge failure; 69 languages in each; 3,065 strings in `postulo`. Serbian is *Serbian (Cyrillic script)*, `sr_Cyrl`, in all 23, and plain *Serbian* is gone from them. `fr_FR` is still read as *French* and `pt_PT`, `pt_BR` as before: a directory name is gettext's and Weblate reads it its own way, so nothing about postulo/postulo#337 needed setting there. **What has not changed, and will put it behind again:** - `AUTO_UPDATE` is off and no repository has a webhook, so Weblate fetches every night and merges nothing. It is level as of `e7539cce0` and each plugin's head, and drifts from the next commit on. - The next translation made in Weblate rewrites a header again, and the next merge conflicts again. Until this issue settles one format for two writers, a translation made there has to be taken into the repository by hand, and the component reset. **Found beside it, not repaired:** the project's `glossary` component has no git repository under it (`/app/data/vcs/postulo/glossary` holds the `.tbx` files and no `.git`). It has been locked with a merge failure since 2026-09-17, a task fails on it after every update, and it holds no term in any language. It still lists plain *Serbian*. Weblate's own `LocalRepository.create_blank_repository` on that directory, then adding the files, would give it one.
Author
Owner

The maintainer's list of 2026-10-02 states a rule that depends on this round trip: all further translations must be made using Weblate.

#706 writes the rule into docs/TRANSLATING.md, CONTRIBUTING.md and CLAUDE.md. docs/TRANSLATING.md:181-190 still tells a speaker to edit the .po file and open a pull request, and that path cannot be dropped until a translation made in Weblate arrives as a pull request that passes CI, with who may sign in and under what licence settled. #706 is blocked on this issue for those parts.

#705 (“Kind” or “Type”) renames message ids. extract drops what the source lost (tests/test_messages_tool.py:100), so a translation held only in Weblate at that moment would be lost.

From the list of improvements of 2026-10-02.

The maintainer's list of 2026-10-02 states a rule that depends on this round trip: all further translations must be made using Weblate. #706 writes the rule into `docs/TRANSLATING.md`, `CONTRIBUTING.md` and `CLAUDE.md`. `docs/TRANSLATING.md:181-190` still tells a speaker to edit the `.po` file and open a pull request, and that path cannot be dropped until a translation made in Weblate arrives as a pull request that passes CI, with who may sign in and under what licence settled. #706 is blocked on this issue for those parts. #705 (“Kind” or “Type”) renames message ids. `extract` drops what the source lost (`tests/test_messages_tool.py:100`), so a translation held only in Weblate at that moment would be lost. From the list of improvements of 2026-10-02.
Author
Owner

Done. Weblate follows main through a Forgejo webhook, drafts new strings with LibreTranslate (needs editing = fuzzy, compiled unless it would fail to format), and sends what it holds back as a pull request from the weblate branch, opened by the Forgejo account weblate. The catalogues are in Weblate's own layout, so its pull requests pass extract --check unchanged (ae0c6998a, tests/test_po_roundtrip.py); CI runs the guard and holds Weblate's pull request to fr-FR and pt-PT complete (e014152fc); every catalogue set has a component (scripts/weblate.py components). Proven end to end by #829, merged as 8c3d03570 with every CI job green. Plugins and extensions: their own issues.

Done. Weblate follows main through a Forgejo webhook, drafts new strings with LibreTranslate (needs editing = `fuzzy`, compiled unless it would fail to format), and sends what it holds back as a pull request from the `weblate` branch, opened by the Forgejo account weblate. The catalogues are in Weblate's own layout, so its pull requests pass `extract --check` unchanged (ae0c6998a, tests/test_po_roundtrip.py); CI runs the guard and holds Weblate's pull request to fr-FR and pt-PT complete (e014152fc); every catalogue set has a component (`scripts/weblate.py components`). Proven end to end by #829, merged as 8c3d03570 with every CI job green. Plugins and extensions: their own issues.
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#349
No description provided.