Run the documented translator adapters, and rewrite the prose round them - #36
Merged
Conversation
jakejackson1
force-pushed
the
docs/deslop-i18n
branch
27 times, most recently
from
August 21, 2026 04:22
7de8ed5 to
b342f06
Compare
Three things, all downstream of the same discovery: nothing ran the adapters the README told a caller to write. Wiring the Symfony one up against the real library turned up a bug. Symfony's PoFileLoader reads a `.po` into one catalogue entry per msgid, including entries still carrying an empty `msgstr`, so `trans()` answers `''` for anything not yet translated. Every untranslated message rendered blank rather than falling back to English — on the failure path, in front of whoever submitted the file. Gettext defines an empty `msgstr` as untranslated; Symfony's loader does not apply that rule, so `Translation::translate()` now does, for every loader rather than that one. tools/translator-readme/ is the arrangement tools/psr7-readme/ already uses: its own pinned manifest, so the library still depends on nothing, and a verify.php that reads each snippet out of the documentation at run time rather than copying it. Reword an adapter and the extraction fails by name. The libraries are installed by Composer in CI on both ends of the supported range — illuminate/translation 8.x is the newest that still installs on 7.3, and one pinned set resolves on both. Each adapter is checked for the three things it has to hold: a translated msgid comes back translated, an untranslated one falls back to English rather than to an empty string, and a value still interpolates. The catalogue is built by running the commands the pages document — msginit, msgfmt, msgmerge, msgattrib — rather than approximating them in PHP, so the pipeline a reader follows is the one that gets tested. WordPress cannot be run without WordPress, but its extraction half can: `wp i18n make-pot` runs before WordPress loads. wordpress.sh builds a plugin tree from `git archive`, so what it holds is what a consumer's vendor/ holds. It runs the documented `make-pot --merge`, compiles the result, runs the strings-file generator the wordpress.org section documents, and asserts the negative that section turns on — that a catalogue left inside vendor/ is still invisible to make-pot. The runtime half that actually goes wrong is pinned in PHP instead: marker-import.php imports the marker and calls both `__()` and `\__()`, which is the situation the docs warn about. Each library now has a page under docs/translation/ carrying its adapter, the commands to build its catalogue, and what to watch for. The README links to them rather than repeating the snippets, so each exists once and the check cannot pass against a stale copy. CLAUDE.md gains a section on writing comments and documentation, so the same edits stop having to be asked for. The prose those pages were cut from was written at length rather than for clarity: the same fact stated twice in different words, asides longer than the clause carrying them, and sentences narrating why a design is good rather than what it does. i18n.php carried twenty lines of docblock over a function whose body is `return $text;`. UPGRADE.md described six reworded messages in a paragraph where a before/after table is what somebody upgrading wants. No message id moves and the catalogue regenerates byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jakejackson1
force-pushed
the
docs/deslop-i18n
branch
from
August 21, 2026 04:28
b342f06 to
7ebff5d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #35. Nothing ran the translator adapters the README told a caller to write. Wiring one up against the real library found a bug, so this adds the checks, the per-library pages, and a prose pass over what was left.
A bug, found by running the documented code
Symfony's
PoFileLoaderreads a.pointo one catalogue entry per msgid — including entries still carrying an emptymsgstr.trans()then answers''for anything not yet translated:Gettext defines an empty
msgstras untranslated. Symfony's loader does not apply that rule, soTranslation::translate()now does — for every loader, not just that one.Anyone with a partly-translated catalogue would have shown blank error messages to whoever submitted the file.
The checks
tools/translator-readme/, built the waytools/psr7-readme/already is: its own pinned manifest so the library still depends on nothing, and averify.phpthat reads each snippet out of the documentation at run time rather than copying it. Reword an adapter and the extraction fails by name.The libraries are Composer dependencies installed in CI on PHP 7.3 and 8.5 — one pinned set resolves on both;
illuminate/translation8.x is the newest that still installs on 7.3.Each adapter is checked for three things: a translated msgid comes back translated, an untranslated one falls back to English rather than an empty string, and a value still interpolates.
The catalogue is built by running the documented commands —
msginit,msgfmt --check,msgmerge,msgattrib— not approximated in PHP, so the pipeline a reader follows is the one under test. Symfony and php-gettext consume a real GNU message catalog.WordPress
wp i18n make-potruns before WordPress loads, so the extraction half needs wp-cli but no WordPress and no database.wordpress.shbuilds a plugin tree fromgit archive— what it holds is what a consumer'svendor/holds — runs the documentedmake-pot --merge, checks the library's msgids and the plugin's own survived, then compiles and reads the result back.It also asserts the negative the WordPress page turns on: that a catalogue left inside
vendor/is still invisible tomake-pot. That claim would otherwise rot silently.The runtime half that actually goes wrong is pinned in PHP instead.
marker-import.phpimports this library's__()marker and calls both__()and\__()— the situation the docs warn about, where a missing backslash makes the translator a silent no-op.Mutation-tested, not just green
Symfony: an untranslated msgid rendered as ""The Symfony adapter is no longer in the documentationthe library's msgids are missing from the merged catalogueOne mutation I ran was invalid: emptying
i18n/upload.potin the working tree does not failwordpress.sh, because it builds fromgit archive HEAD. That is the script working as designed, but it means working-tree edits cannot mutation-test it.I also confirmed
msgfmt --checkearns the place the docs give it — a translation dropping a%1$sgivesa format specification for argument 1 doesn't exist in 'msgstr', because xgettext marks those entries#, php-format.Documentation
One page per library under
docs/translation/, each with its adapter, the commands to build its catalogue, and its gotchas — Symfony's empty-msgstrbehaviour, Laravel'sprintfplaceholders not being:name, php-gettext's two-package split, and the whole wordpress.org limitation including why--includeproves nothing.The README links rather than repeats. That matters for the check as much as for length: with a snippet in two places,
verify.phpwould only exercise whichever copy it found first, so a stale one could pass.docsis added topackage.yml's expected top level, so the pages ship.Prose
The docblocks and docs added in #35 were written at length rather than for clarity — the same fact twice in different words, asides longer than their clause, sentences narrating why a design is good rather than what it does.
i18n.phpcarried twenty lines of docblock over a function whose body isreturn $text;.UPGRADE.mddescribed six reworded messages in a paragraph where a before/after table is what somebody upgrading wants.Verification
551 tests, PHPStan level 9, PSR-12, parallel-lint, the PSR-7 bridge, the translator adapters and the WordPress pipeline all pass locally. No message id moves and
i18n/upload.potregenerates byte for byte.The wp-cli download and
apt-get gettextsteps have only run on my machine — this PR's own CI run is the first real test of the new workflow.🤖 Generated with Claude Code