Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lexi Translate Laravel Package

omaralalwi/lexi-translate

A Laravel package for managing multilingual translations: store and edit language keys in the database, switch locales at runtime, and translate model attributes with a simple trait, so you no longer need to hand-edit lang files.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish

composer require omaralalwi/lexi-translate
php artisan vendor:publish --provider="Omaralalwi\LexiTranslate\LexiTranslateServiceProvider"
php artisan migrate

The publish step gives you a config file (default/fallback locale, cache store + TTL, optional custom Translation model) and a migration for a polymorphic translations table (translatable_type, translatable_id, locale, key, value). Check the migration before migrating — if you already have a translations table from another package, rename or merge.

2. Where to look first

  • config/lexi-translate.php — locales, fallback, cache settings. Everything you'll want to tweak lives here.
  • The published migration — confirm column names and whether indexes/foreign keys are included (you will likely add a composite index yourself).
  • The HasTranslations trait source in vendor/omaralalwi/lexi-translate/src/ — it's a small package, and the trait is the whole public API. Read it once; it saves you guessing at method names.

3. First use case

Add the trait and declare which attributes are translatable:

use Omaralalwi\LexiTranslate\Traits\HasTranslations;

class Post extends Model
{
    use HasTranslations;

    protected $translatable = ['title', 'body'];
}

Then write and read translations for a locale:

$post = Post::create(['slug' => 'hello-world']);

$post->setTranslation('title', 'en', 'Hello World');
$post->setTranslation('title', 'ar', 'مرحبا بالعالم');

$post->getTranslation('title', 'ar');   // 'مرحبا بالعالم'
$post->getTranslation('title', 'fr');   // falls back per config

That's the 80% case: a model with a handful of short-to-medium text fields, an admin form per locale, and reads that respect app()->setLocale(). Confirm the exact method names against the README of the version you install — the package has iterated on its API and the trait is the source of truth.

Implementation Patterns

Locale-aware reads with an explicit fallback chain. Don't rely on implicit fallback. Wrap reads in a helper on the model so the behaviour is greppable and testable:

public function t(string $key, ?string $locale = null): ?string
{
    return $this->getTranslation($key, $locale ?? app()->getLocale())
        ?? $this->getTranslation($key, config('app.fallback_locale'));
}

Save translations inside a transaction. Admin forms typically submit ['en' => [...], 'ar' => [...]]. Loop and persist, and flush the model's translation cache once at the end rather than per attribute:

DB::transaction(function () use ($post, $data) {
    $post->fill($data['base'])->save();

    foreach ($data['translations'] as $locale => $fields) {
        foreach ($fields as $key => $value) {
            $post->setTranslation($key, $locale, $value);
        }
    }
});

Always eager-load the relation. The translations live in a side table, so any listing page is an N+1 waiting to happen:

$posts = Post::with('translations')->paginate(20);

If the package supports a scoped eager load (e.g. translations constrained to the current locale), use it for index pages and reserve the full load for edit screens.

Build API payloads as a locale map. Consumers usually want everything in one response:

return [
    'id' => $post->id,
    'slug' => $post->slug,
    'translations' => collect(config('lexi-translate.locales'))
        ->mapWithKeys(fn ($l) => [$l => [
            'title' => $post->getTranslation('title', $l),
            'body'  => $post->getTranslation('body', $l),
        ]]),
];

Locale switching middleware. Resolve locale from Accept-Language, a ?lang= query, or a session key, then app()->setLocale($locale) — the trait reads from the container. Guard against arbitrary values by validating against config('lexi-translate.locales').

Validation shape. Validate nested locale payloads explicitly so a missing Arabic title surfaces as a form error, not a silent null:

'translations.en.title' => ['required', 'string', 'max:255'],
'translations.ar.title' => ['nullable', 'string', 'max:255'],
'translations.*.body'   => ['nullable', 'string'],

Seeding and imports. A seeder that loops setTranslation() is fine for hundreds of rows but slow for tens of thousands — batch-insert directly into the translations table and clear the cache afterwards.

Pair it with a translation service. The same author maintains a Gemini-based translation package; the natural workflow is: editor saves the source locale, a queued job fills missing locales via the API, results land through setTranslation(), and a draft flag on the parent keeps unreviewed machine output out of the public site.

Testing. Add a factory state or helper that seeds a model with en + one other locale. Assert both the happy path and the fallback path, and assert that a cache flush actually happens after an update — that's the bug you'll otherwise ship.

Gotchas and Tips

  • Cache invalidation is the main failure mode. Writes made through the package should bust the cache, but anything that bypasses it — DB::table('translations')->update(...), raw SQL imports, ->update() on the Translation model directly, or another process writing concurrently — will leave stale reads. Expose an artisan command or admin button that flushes translation caches, and call it at the end of every import.
  • $model->title is ambiguous. The trait may or may not hook attribute access, and its behaviour (current locale vs. fallback vs. raw column) is easy to misread. Prefer getTranslation() in application code so intent is explicit, and reserve the magic accessor for Blade templates where terseness matters.
  • Translations are not fillable on the parent. Post::create(['title' => ...]) won't populate the side table. Persist the parent first, then attach translations. This trips people up when converting an existing app from JSON-column translation packages.
  • Morph map collisions. If you call Relation::enforceMorphMap(), the translatable_type values change. Rows written before the morph map will silently stop resolving — write a data migration that rewrites existing type strings.
  • Add your own indexes. The base migration typically indexes the polymorphic pair; add (translatable_type, translatable_id, locale, key) unique to prevent duplicate rows, and a locale index if you query "all posts translated to Arabic" for a completeness dashboard.
  • Cascade deletes. Verify the migration's onDelete('cascade') or add a deleting model event that removes translations. Orphaned rows accumulate quietly and inflate every join.
  • Locale code consistency. Pick ISO 639-1 (en, ar) and enforce it. Mixing en with en_US produces "missing translation" bugs that look like cache problems but aren't.
  • Searching across locales is a join, not a LIKE. where('title', 'like', ...) on the parent table won't work. Query the relation (whereHas('translations', ...)) and benchmark it — for anything beyond a few thousand rows you'll want a full-text index on value or an external search index.
  • Cache store in tests. If the config points at Redis and your test env doesn't have it, you'll get confusing failures. Set the package's cache store to array in phpunit.xml.
  • Long text belongs in text columns. Confirm the migration uses text/longText for value; a varchar(255) will truncate article bodies without warning on some MySQL configs.
  • Ecosystem is small (72 stars, one primary maintainer). That's not disqualifying — the code is short and readable, and MIT means you can fork — but pin the version in composer.json, read the trait before upgrading, and don't expect Stack Overflow answers. Budget an hour to read the source; it will pay for itself the first time caching misbehaves.
  • Extension points worth knowing: custom Translation model (add status, source, reviewed_by columns there rather than on the parent), configurable cache TTL/store, and the fallback locale chain. If you need per-field fallback (title falls back to en, body doesn't), intercept at your own model helper rather than fighting the package config.
  • When not to use it: if you only ever translate two short fields and need sorting/filtering by translated title, a JSON column with generated columns may be simpler. This package earns its keep when you have many translatable models, many locales, and want queries like "which posts are missing Arabic?" to be cheap.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core