creativeorange/laravel-translatable-and-injectable
Add translatable attributes to Laravel Eloquent models and inject dynamic values into translation strings at runtime, so locale-aware text can include model data, placeholders, or custom variables without hardcoding.
creativeorange/laravel-translatable-and-injectable combines the familiar "one JSON column, many locales" model (à la spatie/laravel-translatable) with placeholder injection, so a single stored string such as "Hi :name, your order :ref shipped" can be rendered with per-request values.
Minimal steps:
composer require creativeorange/laravel-translatable-and-injectablecomposer.json first — v3.0.1 is a packaging-only release (PR #2 touched composer.json), so the illuminate/Laravel version constraints are the single most important thing to verify before you commit to v3. Confirm your app version is inside those constraints; if not, stay on the 2.x line.php artisan vendor:publish --provider="Creativeorange\...\ServiceProvider"), then check config/app.php for locale and fallback_locale.protected $translatable = ['name', 'description'];. The backing columns must be json (MySQL 5.7+/Postgres) or text.:customer_name or :discount placeholder injected at render time rather than string-concatenated in Blade.Model-level translation with injection. The core workflow is write-once, render-many:
$product->setTranslation('description', 'en', 'Hi :name, the :product is back in stock.');
$product->setTranslation('description', 'nl', 'Hallo :name, de :product is weer op voorraad.');
echo $product->translate('description', ['name' => $user->first_name, 'product' => $product->sku]);
Wrap this in a small presenter/view-model rather than calling translate() from controllers — you will want a single place to default the injection payload (e.g. always pass name and app_url).
Form + validation workflow. Editing translations usually means an array-shaped form. Validate with 'name.*' => ['required', 'string', 'max:255'] and loop config('app.locales') when building the inputs, then setTranslations() per attribute on save. Keep the locale list in a config file, not hard-coded in views.
Blade usage. Expose a computed accessor (getDescriptionAttribute()) that calls translate() with a default payload, so templates stay {{ $product->description }}. For anything user-supplied, escape at the template: {!! nl2br(e($product->translate('description', $payload))) !!}.
Seeding and factories. Seed the source locale only; let the fallback chain handle the rest during development. Define a factory state per locale so tests can assert both the happy path and the fallback path.
Integration tips. If you already use spatie/laravel-translatable, do not put both traits on the same model — pick one per model. When exposing models through an API, be explicit about whether toArray() returns the resolved translation or the raw locale map; wrap in an API Resource to control it.
composer.json. That usually means widened or narrowed Laravel/illuminate constraints. Diff it (https://github.com/creativeorange/laravel-translatable-and-injectable/compare/v3.0.0...v3.0.1) before trusting a green composer update, and pin with ^3.0 rather than *. No migrations or code changes should be required, but if the constraint narrowed, composer update will silently keep you on 3.0.0 — check composer show afterwards.__()/trans(). Don't nest a :attribute placeholder inside a string that also passes through trans() — you'll get double replacement or a leaked placeholder in production.en vs en_US vs en-GB). A mismatched key fails silently into the fallback rather than throwing — dump the raw JSON column ($model->getRawOriginal('name')) when a translation "doesn't appear."nl translation renders English and nobody notices. Add a test or a lint command that asserts every configured locale has a non-empty value for required attributes.whereJsonContains requires MySQL 5.7+/Postgres/SQLite 3.38+; on MariaDB and older MySQL the semantics differ. If you need searchable translations, denormalise the primary locale into a plain column.$casts as array/json alongside the trait — the trait expects to own the serialisation. Also ensure the attribute isn't in $hidden/$appends inconsistently across the two.json? (2) does the raw value contain the locale key? (3) does your injection array key match the placeholder exactly, including case? (4) is the fallback locale returning something you mistook for a hit?How can I help you explore Laravel packages today?