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

Filament Translate Field Laravel Package

codewithdennis/filament-translate-field

A Filament form field for managing translations across multiple locales. Add one field to your form and let editors enter and switch between language versions of content in a clean, integrated UI—ideal for multilingual admin panels built with Filament.

View on GitHub
Deep Wiki
Context7

Getting Started

What you're actually getting. Two related things, and the docs conflate them. First, a locale-tabbed form field: one input, a tab strip for nl / en / de, values persisted as a JSON array keyed by locale. Second, a one-click "translate" action attached to that field that reads the current locale's value, calls a translation provider, and writes the result into the target locale's key. The first is a solid UI pattern. The second is a policy decision wearing a UI costume.

The lifecycle reality, stated up front. The repository is archived, last release 2024-10-10, ~10 stars, zero dependents, no security response path. Treat it as a reference implementation under MIT that you read, run in a scratch panel, and then vendor into your own codebase with attribution. Do not composer require it as a production dependency you can't patch.

Minimal steps to evaluate it (half a day):

  1. Read composer.json before anything else. Confirm the PHP / Laravel / Filament constraints against your pinned versions. If it predates your Filament major, stop — the estimate to fork it is now the whole task.
  2. Clone into a throwaway panel, composer require codewithdennis/filament-translate-field (path repository or VCS is fine for evaluation), and register the plugin/provider if the service provider requires it.
  3. Publish the config (php artisan vendor:publish) and read it end to end. It will tell you the provider, the key mechanism, and the locale list — that config is the real documentation for this package.
  4. Drop the field into one throwaway resource and exercise it with representative content: a short plain string, a long textarea, and a rich-text field with HTML.
  5. Open the field's source file and find the action closure. That closure is the thing you're going to copy. Everything else is scaffolding.

First use case: a ProductResource with name and description stored as JSON columns. Editors write Dutch, click translate, review the English, save. Don't pick your regulated or public-facing-without-review content for the pilot — pick back-office catalog copy where a human reads the output before it ships.

Where to look first, in order: composer.json (compatibility) → config file (provider, locales, key) → the field class (locale tabs, state path handling) → the action closure (the actual translation call, error handling, HTTP client) → tests, if any exist (assume none worth keeping).

Implementation Patterns

JSON column + array cast is the storage contract.

protected $casts = ['name' => 'array', 'description' => 'array'];

// DB: {"nl": "Fietsbel", "en": "Bicycle bell"}

The field expects an array keyed by locale. If you already have flat name / name_en columns, don't fight it inside the field — normalise at the model boundary with accessors/mutators, or do a one-time migration. Mixing a string column with an array-backed field produces the confusing half-working state where the field renders empty but saves fine.

Wrap the field in one reusable place. Every resource needing translations should call a single helper, not repeat field configuration:

// app/Filament/Support/TranslatableField.php
public static function make(string $name): TranslateField
{
    return TranslateField::make($name)
        ->locales(config('translate-field.locales'))
        ->required(fn () => app()->getLocale() === config('app.locale'))
        ->visible(fn () => auth()->user()->can('translate.content'));
}

That gives you a permission gate, a per-locale required rule (default locale required, others optional), and a single file to change when the field breaks on a Filament upgrade.

Put the provider behind your own interface — this is the whole game.

interface TranslationService {
    public function translate(string $text, string $from, string $to): string;
}

Wire it in a service provider, bind one concrete driver (DeepL, Google, LibreTranslate, whatever cleared procurement), and have the field action resolve it from the container rather than reading the config directly. This one decision converts "archived package problem" into a non-issue: when you want glossaries, caching, or a self-hosted provider later, you touch one file. It also makes the field testable with a stub.

Validate per locale, not on the array. Laravel's nested array rules work if you dot the paths:

['name.nl' => 'required|string|max:255', 'name.en' => 'nullable|string|max:255']

Doing 'name' => 'array' alone lets editors save a 40KB English textarea into a column validated for 255 — which is also your invoice guard if you translate on save.

Mark machine output visibly and durably. Add translated_at, translated_by, and translation_provider (or a small translations_audit table) and render a "machine-translated — review required" badge in the UI when translated_at > updated_by_human_at. Without this, unreviewed output silently becomes authoritative. This is cheap to add at implementation time and expensive to retrofit after an incident.

Read fallbacks explicitly on the front end.

$name = $model->name[app()->getLocale()]
    ?? $model->name[config('app.fallback_locale')]
    ?? null;

A tiny helper on a HasTranslations trait beats copy-pasting this into every Blade view.

Test the action with the provider stubbed. A Livewire form test that fills the source field, calls the action via callAction(), asserts the target locale key is populated and the audit fields are set. That's ~20 lines and it's the only thing that will catch a Filament upgrade silently breaking your field.

Gotchas and Tips

Archived means the next Filament major is yours. Pin the version, run composer audit, and assume the first upgrade after adoption requires a fork. Budget for it at adoption time or you'll pay for it as an outage. The Field base class API, afterStateUpdated signatures, and form state path handling are the three places these packages typically break.

Async races overwrite manual edits. The translate action fires an HTTP request; a fast editor can type into the target field while it's in flight and lose their text when the response lands. Either disable the target field during the request, or compare the current state against what was there when the request started and discard/confirm on mismatch. Do not skip this — it's the #1 way editors lose trust in the button.

Rich text will break before plain text does. Tiptap/Quill-backed rich editors hold their own DOM state and do not automatically re-render when Livewire sets the underlying value. You'll see the translation land in the database but not in the visible editor. Call the editor's refresh hook after $set, and strip or normalise HTML tags before sending to the provider — many providers mangle <p>/<br> and return HTML-escaped entities.

Guard cost and quota. Add a character-length cap before the API call (Str::length($value) > 5000 → reject with a notification), a short HTTP timeout (Http::timeout(15)->retry(2, 200)), and a cache keyed on hash('xxh128', $text.$from.$to) so identical strings aren't re-billed. Without these, a 40KB textarea is a surprise invoice line and a provider outage is a 30-second hanging form.

Never render the API key into the form. Config or secrets manager only. Remember php artisan config:cache — a key read via env() outside a config file is null in production and produces a confusing "translation silently does nothing" bug.

JSON columns don't index well. where('name->en', 'like', '%foo%') works on MySQL and Postgres but not SQLite, so your test suite may pass while production search is slow. If translated content needs to be searchable, plan for a dedicated search index or full-text column, not JSON path queries.

Nested fields and repeaters use dot paths. Inside a repeater the state path is data.0.name.en. If your tabs don't switch independently per repeater item, that's the state path, not a rendering bug. Verify with $livewire->getFormStatePath().

Don't translate on afterStateUpdated of the source field. That fires on every keystroke and turns a 500-character bio into fifty API calls. Translate only on explicit click (or on save with a debounce + dirty check).

Docs are thin; the source is ground truth. The package description describes a locale-tabbed multilingual field; the marketplace blurb describes one-click translation. Both may be present, partially present, or aspirational in the archived state. Read the field class and the action closure before you write your estimate — and don't quote the README to stakeholders as evidence of features.

PII and data egress are code-adjacent questions you can't solve in code. Every click ships user-authored text to a third party. If the content class touches customer data, regulated copy, or anything under a DPA question, get a written answer before wiring the button, not after. Your TranslationService interface lets you point at a self-hosted provider — that's the escape hatch, and it's worth building in from day one rather than as a rewrite.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin