Replaces WordPress's __() with one that serves both the Laravel and the
WordPress translation catalogs, so a Pollora application can write either
style and get the right answer. Theme views, config files and WooCommerce
filters keep calling __(), and a plugin's text domain and a Laravel key with
placeholders both resolve.
Part of Pollora, the Laravel framework for WordPress. In a Pollora project it is already installed and loaded: just call
__().
composer require pollora/helper-overriderYou rarely need this line: pollora/framework requires the package and ships
the WordPress core patch it relies on (see below). Outside the framework,
WordPress's own __() must be renamed first, or PHP stops on a duplicate
function declaration.
The package must load before laravel/framework, whose __() sits behind
the same function_exists() guard: whichever helpers.php Composer includes
first owns the name. That is why it has no Composer dependency at all, as
explained under How the override happens.
Requirements:
- PHP 8.3+
- Laravel 12 or 13, at runtime (not as a Composer dependency — see below)
WordPress declares __() unguarded in wp-includes/l10n.php, so it cannot
simply be redefined. The Pollora framework patches WordPress core to rename it
to __wp() (patches/wordpress-core.patch, applied through
cweagans/composer-patches), which frees the name for this package's
src/helpers.php, loaded via Composer's autoload.files.
This package must stay dependency-free.
laravel/frameworkalso declares__(), behind the samefunction_exists()guard, and Composer emitsautoload.filesin dependency order. The package only wins the race while nothing sorts it afterlaravel/framework. Adding anilluminate/*entry torequirefails nothing loudly —__()silently becomes Laravel's, and every WordPress catalog stops resolving. The Laravel classes are reached through PSR-4 at call time instead, long after the autoloader is up, and guarded byclass_exists().tests/Unit/OverrideOrderTest.phpis the alarm for this.
The two translators do not overlap, and the second argument is what tells them apart.
| Call | Routed to | Why |
|---|---|---|
__('Some string') |
Laravel, then WordPress default |
No intent expressed. The only ambiguous case, and the only one where both are consulted. |
__('Some string', 'my-plugin') |
WordPress, my-plugin domain |
A text domain is an explicit WordPress call. Laravel is never consulted, so an unrelated key of the same name cannot shadow a plugin's catalog. |
__('Shipping :brand', ['brand' => 'X']) |
Laravel, then WordPress default — placeholders filled either way |
Named replacements are a Laravel idiom. WordPress has no named placeholders: a msgid uses %s, substituted by sprintf() at the call site. |
There is no key-prefix escape hatch to force one side over the other. A prefix
like wordpress. would be invisible to gettext extraction tools (wp i18n make-pot, Poedit) — they read the literal __() argument, so a translator
would translate a msgid the runtime never actually looks up once the prefix
is stripped. If you need WordPress specifically, pass its text domain; if you
need Laravel specifically, pass replacements (even an unused empty one won't
do — see the ambiguous row above).
For the third row, if the key is absent from the Laravel catalog the line is
taken from WordPress — which returns the key verbatim when it knows nothing
about it — and the named placeholders are filled in regardless. That is what
keeps __('Shipping :brand', ['brand' => 'Test']) from rendering as
Shipping :brand in a locale you have not translated.
The WordPress locale (fr_FR) is tried first, then its base language (fr).
Laravel's JSON lookup — the one __() uses — matches the locale exactly and has
no language fallback of its own, so without this a project shipping
lang/fr.json would never be consulted on a fr_FR site. An existing
lang/fr_FR.json still wins.
Only __() is overridden. The rest of the WordPress family (_e(), _x(),
_n(), esc_html__() …) does not route through __() in core — those call
translate(), translate_with_gettext_context() and translate_plural()
directly — so each would need its own core patch. Two of them have no faithful
mapping anyway:
_x()/_ex()— Laravel has no notion of a gettext context. Ignoring it would collapse two strings distinguished only by context onto one Laravel key._n()/_nx()— WordPress takes(singular, plural, count)and delegates plural forms to the.moPlural-Formsheader; Laravel encodes pluralization inside the string and resolves it withtrans_choice(). Routing_n()to Laravel would make its$pluralargument dead and break languages with more than two forms.
Per-class handlers registered through Lang::stringable() apply on the Laravel
path but not on the WordPress-fallback path, where such objects fall back to
__toString(). The fallback line comes from a gettext catalog rather than the
Laravel one and must keep working with no application booted, so it substitutes
placeholders through ReplacementApplier rather than through the translator.
ReplacementApplierParityTest diffs that class against a real
Illuminate\Translation\Translator to keep the two in step.
How __() behaves in a Pollora project, and when to choose .po/.mo or
Laravel files: Translations.
composer test # pint + rector + phpstan + pest
composer test:unit # pest only
composer lint # apply Pint
composer refacto # apply RectorContributions are welcome: see the contributing guide. Report security issues privately, as described in the security policy.
Pollora Helper Overrider is open-source software licensed under the MIT license. © RuBee group