vanilo/translation
vanilo/translation adds multilingual content support to Vanilo e-commerce applications, providing translatable model attributes, locale-aware handling, and translation management for products, categories, and other store entities in Laravel.
composer require vanilo/translation
php artisan vendor:publish --tag=vanilo.translation # migrations + config
php artisan migrate
The publish tag/provider name varies between Vanilo releases. If the tag fails, run bare php artisan vendor:publish, pick the Vanilo\Translation\... entries from the list, then check config/ for the resulting file (usually config/vanilo/translation.php or config/vanilo.translation.php). Confirm what actually loaded with:
php artisan tinker --execute="dump(config('vanilo.translation'));"
Where to look first, in order:
src/Contracts/ — the Translatable contract and translation interface. This is the real API; everything else is plumbing.src/Providers/ — what gets bound into the container and under which keys (driver binding, morph map registration).src/Traits/ — the trait you add to your own models. Read it before anything else; method names differ across versions (setTranslation(), translate(), getTranslation()).tests/ — a package with 1 star and 0 dependents has tests as its only reliable documentation. The test suite is usually more current than the README.database/migrations/ — the schema tells you what's queryable and what indexes exist.First use case: translating the fields shoppers see — Product name/description and Property values — while keeping one canonical row per product.
use Vanilo\Translation\Traits\Translatable;
class Product extends Model
{
use Translatable;
protected $translatable = ['name', 'description', 'slug'];
}
app()->setLocale('de');
$product->name; // German value
$product->name = 'Neues Produkt'; // writes the de translation
$product->name; // if your version uses explicit setters:
// $product->setTranslation('name', 'de', 'Neues Produkt');
Then wire locale into the request (Accept-Language middleware or a ?locale= query param) before you render, and make sure app()->setLocale() survives to the point where the model is serialized.
Trait on your own models, driver on the package. Keep $translatable as an explicit allowlist. Never derive it from $fillable — you'll translate foreign keys, prices, and status columns and break joins.
Locale resolution is middleware, not a model concern. Resolve the locale once, early, and let the trait read app()->getLocale(). Doing locale switching inside controllers or Blade leads to models cached under the wrong locale.
// app/Http/Middleware/SetLocale.php
public function handle($request, Closure $next)
{
$locale = $request->header('Accept-Language', config('app.locale'));
app()->setLocale(substr($locale, 0, 2));
return $next($request);
}
Admin CRUD as locale tabs. Load one form per locale, each submitting to the same resource endpoint with a hidden locale field. Wrap the write in a transaction and save the translation last so a validation failure doesn't leave partial translations. Validate per-locale with required_without rules rather than requiring every locale for every field.
Eager-load translations whenever you list. The single biggest performance regression with any translation package:
Product::with('translations')->where('status', 'active')->paginate(24);
If the driver is a JSON-column driver, this is a no-op and you should instead select only the columns you need.
Seed multilingual fixtures deterministically. Give factories a locale loop so tests don't depend on app()->locale leaking between test cases:
foreach (['en', 'de', 'fr'] as $locale) {
$product->setTranslation('name', $locale, "Name {$locale}");
}
Cache translated output, invalidate on the model, not the translation. Cache keys must include the locale ("product.{$id}.{$locale}"). Hook saved/deleted on the parent model — translation rows are often written through a relation and won't fire the parent's events reliably.
Route model binding + locale-aware slugs. If slugs are translatable, resolve by joining translations for the active locale, and keep a locale-scoped unique index on the slug. Fall back to the default-locale slug with a 301 rather than a 404 when a locale variant is missing.
API consumers: accept a locale query param that overrides the header, and include a translations block only when ?include=translations is requested. Don't leak every locale by default — payloads grow multiplicatively with locale count.
translationMissing()/hasTranslation() check and log misses in non-production.en, en_US, en-US, and en-us are four different keys. Pick one convention (BCP-47 short codes are the safe default), normalize at the middleware boundary, and add a test asserting en_US maps to en.locale column is almost always string(5) and indexed. Bumping to region-qualified locales (pt-BR) fits, but zh-Hant-TW does not — check the migration before you commit to a locale strategy, and widen it in your own migration if needed.(translatable_type, translatable_id, locale, key) can exceed the index byte limit under utf8mb4 on older MySQL/MariaDB. If migrations fail, shorten translatable_type or use a morph alias map rather than get_class().driver and run php artisan config:clear. Laravel's cached config will otherwise serve the old driver to some workers.ORDER BY on a translated column either sorts by the base column (consistent, wrong) or needs a join with a locale predicate (correct, slower, prone to duplicate rows if the join isn't unique — always constrain on locale in the join condition, not the WHERE).Translatable trait on models so you can swap back.composer.json, and if you need a fix, prefer a fork or a path repository over hoping for a release. Read src/ before trusting the README — for a package this size the whole implementation is an afternoon of reading, and that's the cheapest insurance you'll get.How can I help you explore Laravel packages today?