yeswedev/nova-ckeditor4-field-translatable
Install the package
composer require yeswedev/nova-ckeditor4-field-translatable
If the package ships publishable assets/config, run:
php artisan vendor:publish --provider="Yeswedev\NovaCkeditor4FieldTranslatable\FieldServiceProvider"
(Provider/class names in this package are not stable across forks — verify with ls vendor/yeswedev/nova-ckeditor4-field-translatable/src before wiring anything into a resource.)
Where to look first
vendor/yeswedev/nova-ckeditor4-field-translatable/src/ — the field class, its options() / locale API, and the Vue component under resources/js/.composer.json — check whether it depends on spatie/nova-translatable, spatie/laravel-translatable, or implements locale handling itself. That single line decides your storage strategy.dist/js/ (or a CDN <script> in the layout) — confirms whether CKEditor 4 is bundled or expected to be loaded globally.First use case — the one you'll hit immediately: a bilingual CMS page or blog post with a body column that currently uses Textarea. Swap it for the CKEditor field so editors can format rich text, per locale, inside a single Nova form.
use Yeswedev\NovaCkeditor4FieldTranslatable\Ckeditor4FieldTranslatable;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title'),
Ckeditor4FieldTranslatable::make('Body')
->rules('required'),
];
}
Single-locale rich text. Straightforward field replacement; the value stored is raw HTML. Pair the field with a detail-view presenter and a trimmed index preview:
Ckeditor4FieldTranslatable::make('Body')
->hideFromIndex()
->rules('required', 'max:65535'),
// Detail
Text::make('Body')->asHtml()->onlyOnDetail(),
Translatable columns. The package name implies locale-aware rendering; two ways it's typically wired:
Wrapped by spatie/nova-translatable — keep the field dumb and let the container fan it out:
use Spatie\NovaTranslatable\Translatable;
Translatable::make([
Ckeditor4FieldTranslatable::make('Body'),
]),
Field-native locales — the field exposes a locale list (->locales(['en','fr']) or similar) and renders one editor per locale in tabs inside its own Vue component. Check the class for the exact method; this is the fork's main selling point over the upstream waynestate field.
Either way, the model side must support it:
use Spatie\Translatable\HasTranslations;
class Page extends Model
{
use HasTranslations;
public $translatable = ['body'];
protected $casts = ['body' => 'array'];
}
Editor configuration. Almost all CKEditor 4 knobs are passed as an options array and merged into the instance config:
Ckeditor4FieldTranslatable::make('Body')
->options([
'height' => 400,
'toolbar' => [
['Source', '-', 'Bold', 'Italic', 'Link', 'Unlink'],
['NumberedList', 'BulletedList', 'Blockquote'],
['Image', 'Table', 'Maximize'],
],
'allowedContent' => true,
])
->stacked();
Keep ->options() JSON-serializable — closures and PHP objects will silently break the Vue hydration.
File / image uploads. If the fork inherited ->withFiles() from the parent package, enable it and make sure storage:link and the Nova disk config line up; otherwise paste-by-URL and the Image dialog's "Browse Server" button will 404 while the editor itself looks fine.
Sanitization on save. Nova fields never sanitize; treat the editor as untrusted input:
Ckeditor4FieldTranslatable::make('Body')
->fillUsing(function ($request, $model, $attribute) {
$cleaner = new \HTMLPurifier(\HTMLPurifier_Config::createDefault());
$model->setTranslations($attribute, array_map(
fn ($html) => $cleaner->purify($html),
(array) $request->input($attribute, [])
));
});
Index/lens columns. HTML will wreck table layout — always ->hideFromIndex() or ->displayUsing(fn ($v) => Str::limit(strip_tags($v), 60)). Same for CSV exports.
Combining with dependsOn. Works, but pass plain scalar values from the driving field, not the editor's HTML payload.
window.CKEDITOR / window.ClassicEditor and you get a broken toolbar or a silent no-op. Grep your composer.lock for ckeditor before installing.Ckeditor4FieldTranslatable::make() that resolves to a nonexistent class throws a confusing "Class not found" at form render, not at boot. Confirm from src/ and add it to your IDE helper.spatie/nova-translatable, storing into a plain TEXT column will save {"en":"…"} as a literal string and editors will see JSON on the next load. Check $translatable and the column type (json/LONGTEXT) and the HasTranslations trait together — a missing trait is the most common cause.json column limits. MySQL JSON maxes at 1 GB but a single string inside is capped at max_allowed_packet; for long articles, LONGTEXT with json_encode-on-write is often safer.spatie/nova-translatable's tab UI, typing in EN and flipping to FR keeps the value (Vue model is per-field), but full page reloads in a resource with ?viaResource params can drop it. Save before switching when editing long bodies.->options() replaces rather than merges in most forks; if the default toolbar disappears after your first config call, you've overwritten it and need to supply the complete toolbar array.->rules('required') applies to the whole array in translatable setups, so an empty FR body passes when EN is filled. Use ->rulesFor() on the wrapper or a Rule closure that inspects each locale explicitly.cdn.ckeditor.com, or a Vue component registered under the wrong Nova.booting hook all present as an empty <textarea> with no error in the UI. Nova::script() in the provider's boot() is where to look.dist/js, customizing the toolbar rendering means re-publishing and rebuilding, not overriding a class. If you need heavy customization, subclass the PHP field and ->options() your way out of it, or fork the component.How can I help you explore Laravel packages today?