digitalcloud/multilingual-nova
Add multilingual support to Laravel Nova resources with this package, letting you manage translations for multiple languages directly in Nova forms and fields alongside your existing resource setup.
Before you install anything, run the compatibility check. This package's last release was December 2021, which predates Nova 4. Open its composer.json and read the laravel/nova constraint before touching your lockfile — v3.0.0 targets the Nova 3 field API. If you are on Nova 4/5, expect to fork and port the field class rather than composer require it.
Three preconditions must already be true:
digitalcloud/multilingual is installed and the model already stores translations as locale-keyed JSON ({"en": "...", "fr": "..."}).$model->title returns the active locale's string, not the raw array.If any of those is missing, stop — the effort to retrofit them exceeds writing a plain JSON field yourself.
Install and publish:
composer require digitalcloud/multilingual-nova:3.0.0 # pin exactly
php artisan vendor:publish --provider="DigitalCloud\MultilingualNova\FieldServiceProvider"
php artisan view:clear && php artisan config:clear
Then republish the compiled JS/CSS into public/vendor/... using the tag the package documents. Do not try to rebuild the frontend from source on a modern Node — the lockfile pins node-sass@4, which fails to compile on Node 14+. Treat the shipped assets as vendored binaries.
First use case. Take one resource with one short translatable field (Post.title) and two locales. Add the field to fields():
use DigitalCloud\MultilingualNova\Multilingual;
public function fields(NovaRequest $request)
{
return [
Multilingual::make('Title', 'title'),
];
}
Then load the create form, save a value in each locale, and inspect the raw DB row with php artisan tinker to confirm the JSON round-trip before you add a second field or a third locale. Confirm the class name and any make() argument order against the installed version's source — the v3.0.0 field service provider rework (PR #56) changed registration, and field aliases may differ from v2.x.
Where to look first: the published config (locale list and fallback), the field class itself in vendor/digitalcloud/multilingual-nova/src, and the compiled assets directory. Those three determine 90% of your day-to-day behaviour.
One field per translatable attribute, grouped in the form. The package renders each field as collapsible locale panels, so keep translated fields adjacent rather than scattered through the resource. Editors scan vertically.
public function fields(NovaRequest $request)
{
return [
ID::make()->sortable(),
Multilingual::make('Title', 'title'),
Multilingual::make('Excerpt', 'excerpt'),
Multilingual::make('Body', 'body'),
Slug::make('Slug', 'slug')->onlyOnForms(),
];
}
Keep the field off index/detail unless you control the rendering. A multilingual field on an index column typically dumps the active-locale string or the whole blob. The usual pattern is ->onlyOnForms() on the multiline fields, and a separate read-only text field for the list view that resolves through the parent package's accessor:
Text::make('Title', 'title')->onlyOnIndex()->sortable(),
Validation is per-locale but declared once. Rules apply to each locale panel, so ->rules('max:255') means 255 characters per locale, not for the whole payload. Where the requirement is per-locale-specific (e.g. en required, fr optional), expect to either configure required-locale behaviour in the published config or attach a custom rule to the field. Verify the actual enforcement by submitting an empty required locale in staging — do not assume.
Fallback display is the parent package's job, not this one's. When an editor leaves fr blank, the front end reads en because digitalcloud/multilingual resolves fallbacks. That means the admin can show an empty panel while the site shows English content. Decide whether editors should see the fallback value pre-filled or see a hint, and check the config for that behaviour.
Workflow for a rollout:
3.0.0 in composer.json, commit the lockfile.Post::factory()->create([
'title' => ['en' => 'Hello', 'fr' => 'Bonjour'],
]);
Branding/hook usage. v3.0.0 adds a custom CSS class hook and translatable tool titles. Use the CSS hook to align locale panels with your admin theme rather than overriding vendor CSS with !important. Translated tool titles are set through Nova's translation files or the config — useful for multi-brand admin where the nav label differs per tenant.
composer.json before installing. On Nova 4+, plan a fork-and-port budget, not an install.npm install in this package. node-sass@4 and [email protected] will fail on modern Node. If you must change CSS, do it in your app's stylesheet targeting the exposed hook class and leave the vendored bundle untouched.tinker after the first save.fillable/guarded must allow the attribute; Nova's save path goes through the field's fill, so a guarded column fails quietly.searchable() on a JSON column matches the raw JSON blob across all locales and produces false positives. For real per-locale search, add generated columns or a whereJsonContains index outside the package and keep the field onlyOnForms().dependsOn and conditional fields break down. Conditional visibility is evaluated once per field, not per locale panel, so "show Body only when Title is filled in fr" is not expressible. Model it as always-visible, or use a separate per-locale field for the rare case.Image::make('Hero (EN)', 'hero.en')) or a custom field. Don't assume the Multilingual wrapper works with them.->rules('unique:posts,title') will compare against the JSON blob, not the per-locale value. Use a custom rule that decodes the column, or move uniqueness enforcement to the parent package's slug logic.vendor:publish --force, view:clear, config:clear, then hard-refresh the browser. Stale compiled JS against a new field service provider produces a blank panel or a silent no-op save.Textarea per locale is a day's work if the package dies. That portability is your insurance, so keep the storage shape clean and don't let package-specific formatting leak into it.How can I help you explore Laravel packages today?