Skip to content

Move the translation marker onto Translation - #44

Merged
jakejackson1 merged 1 commit into
mainfrom
fix/marker-on-translation
Aug 24, 2026
Merged

Move the translation marker onto Translation#44
jakejackson1 merged 1 commit into
mainfrom
fix/marker-on-translation

Conversation

@jakejackson1

Copy link
Copy Markdown
Member

Fixes #42.

GravityPdf\Upload\__() was a function in src/Upload/i18n.php, loaded by Composer's files autoload. That entry is Composer's own — an autoloader that indexes classes and nothing else, such as a classmap built over php-scoper output, never runs it. Every error path then fatals with Call to undefined function GravityPdf\Upload\__(), on a site where uploads had only ever succeeded.

It is now Translation::__(), a static method, so it travels with the class. composer.json declares PSR-4 and nothing else.

Verification

The issue's own repro, loading every class and no files entry:

before:  FATAL: Error: Call to undefined function GravityPdf\Upload\__()
after:   rejected cleanly: File contents do not match the "ttf" extension. Must be one of: font/sfnt

Extraction is unchanged and needs no configuration, upstream or downstream. xgettext matches a keyword against the trailing identifier and ignores the class prefix, so -k__:1 reads Translation::__(), \GravityPdf\Upload\Translation::__() and an aliased T::__() alike. composer i18n:pot regenerates i18n/upload.pot byte-identical across the move — that is the proof, and the i18n workflow is what keeps it true.

One correction to the issue

composer dump-autoload --classmap-authoritative does not drop autoload.files — tested against a fixture; autoload_files.php is still emitted and autoload_real.php still requires it. The trigger is specifically an autoloader that isn't Composer's, which is what a scoped WordPress plugin tree normally has. The changelog says it that way so nobody on plain --classmap-authoritative goes looking for a bug they don't have.

The namespace fallback goes with it

An unqualified function call resolves against the global namespace when the current one has no match, so a validator forgetting use function GravityPdf\Upload\__; reached WordPress's __() and translated at the throw. A class name has no such fallback: the same mistake now fatals at the call.

That is still only reached on an error path, so I18nTest::testEveryCallerCanReachTheMarker() is retargeted rather than deleted — it reads every file under src/ for the import instead of waiting for a rejected upload. testNoGlobalMarkerCanMaskAMissingImport() is gone; there is no fallback left for a global stub to mask. A new testTheMarkerTravelsWithTheClass() refuses both a bare function and a files entry coming back.

tools/translator-readme/marker-import.php existed to prove the "note the leading backslash" advice in the Laravel and WordPress recipes, which was load-bearing only while a consumer could import our __(). The fixture and the caveat both go; global-underscore.php stays, since the documented adapters still call a global __().

Naming

Kept __ rather than mark(). PHP documents the __ prefix as reserved by convention; __ alone has never been a defined magic method, and CI proves it on 7.3 through 8.5. mark() would cost an extractor flag in our script and in every consumer extracting their own marked strings.

No deprecation shim: 4.0.0 is unreleased (latest tag is 3.1.0), so the function was never published.

Checks

phpunit (698 tests, 1,392 assertions, 1 skipped) · phpstan level 9 · lint · check-syntax · i18n:pot (no diff) · base64-docs · psr7-readme · translator-readme — all pass locally.

🤖 Generated with Claude Code

`GravityPdf\Upload\__()` was a function in `src/Upload/i18n.php`, loaded by
Composer's `files` autoload. That entry is Composer's own: an autoloader that
indexes classes and nothing else — a classmap over php-scoper output, which is
how a WordPress plugin usually consumes this library — never runs it. Every
error path in the library then fatals with `Call to undefined function`, on a
site where uploads had only ever succeeded.

It is now `Translation::__()`, a static method, so it loads with the class.
`composer.json` declares PSR-4 and nothing else, and `I18nTest` refuses both a
bare function and a `files` entry coming back.

Extraction is unchanged and needs no configuration, upstream or downstream:
`xgettext` matches a keyword against the trailing identifier and ignores the
class prefix, so `-k__:1` reads `Translation::__()` as it read `__()`.
`i18n/upload.pot` regenerates byte-identical.

The namespace fallback goes with it. An unqualified function call resolves
against the global namespace when the current one has no match, so a validator
forgetting the import reached WordPress's `__()` and translated at the throw.
A class name has no such fallback — the same mistake now fatals at the call —
and `I18nTest::testEveryCallerCanReachTheMarker()` still reads `src/` for the
import, because that call sits on an error path either way.

`tools/translator-readme/marker-import.php` proved the leading backslash in the
Laravel and WordPress recipes mattered, which it did only while a consumer
could import our `__()`. Both the fixture and the docs' caveat go.

Fixes #42

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jakejackson1
jakejackson1 merged commit 44695b8 into main Aug 24, 2026
39 checks passed
@jakejackson1
jakejackson1 deleted the fix/marker-on-translation branch August 24, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4.0: i18n.php is unreachable under classmap autoloading — fatal on every error path

1 participant