concept24/nova-astrotranslatable-ai

Concept24 Translatable AI Field for Laravel Nova. Based on Astrotomic Laravel Translatable.

Maintainers

Package info

bitbucket.org/concept24/nova-astrotranslatable-ai

pkg:composer/concept24/nova-astrotranslatable-ai

Transparency log

Statistics

Installs: 186

Dependents: 0

Suggesters: 0

v3.3.2 2026-07-07 09:27 UTC

README

A Laravel Nova package that makes any input field astrotomic/laravel-translatable compatible, with built-in AI-powered text generation, translation, image generation, and visual image analysis via OpenRouter.

Requirements

  • PHP: >= 8.0
  • laravel/nova: ^4.12 | ^5.0
  • astrotomic/laravel-translatable: ^11.10

Features

  • Supports almost all fields (including third-party ones)
  • Supports default validation automatically
  • Simple to implement with minimal code changes
  • Locale tabs to switch between different locale values of the same field
  • Double click on a tab to switch all fields to that locale
  • AI Text Generation — generate content using OpenAI, Anthropic, Google, xAI, or Perplexity models via OpenRouter
  • Resource-agnostic "Descriere" template — generate descriptions for any resource (products, categories, collections, …) using its title, existing short/long description, and main image; context toggles appear only when the data exists
  • AI Translation — auto-translate field values to all configured locales
  • AI Image Generation — generate images using Google Gemini or OpenAI GPT image models
  • AI Visual Image Analysis — describe any image using vision-capable models (Google Gemini, OpenAI, xAI Grok, Anthropic Claude)
  • FAQ (GEO) template — generate structured Q&A pairs (Plain Text / HTML / Markdown / JSON) with optional schema.org FAQPage JSON-LD for AI-search visibility; supports external URL extraction via Perplexity Sonar
  • System prompt inspector — view and optionally override the system message per generation, without DB persistence
  • Multilanguage UI — all interface strings are translatable; language files are publishable
  • Supports nova-settings package

Known non-working fields

Limitations

  • The following methods can not be used, as this package uses them internally:
    • resolveUsing
    • fillUsing

Installation

Firstly, set up astrotomic/laravel-translatable.

Install the package via Composer:

composer require concept24/nova-astrotranslatable-ai

Publish the configuration files:

php artisan vendor:publish --tag="nova-translatable-config"

This publishes two config files:

  • config/nova-translatable.php — locales and display options
  • config/openrouter.php — AI/OpenRouter settings

Optionally publish the language files to customize UI translations:

php artisan vendor:publish --tag="nova-translatable-lang"

This copies ro.json and en.json to resources/lang/vendor/nova-astrotranslatable/. Published files take priority over the package defaults.

Configuration

config/nova-translatable.php

return [
    // Define the locales available for translation
    'locales' => ['en' => 'English', 'ro' => 'Romanian'],

    // Display Nova's current locale first in the tabs
    'prioritize_nova_locale' => true,

    // Tab layout: 'row', 'column', or 'none'
    'display_type' => 'row',

    // Locale select position: 'left-absolute', 'left-static', 'right-absolute', 'right-static'
    'locale_select.display_type' => 'right-absolute',

    // Auto-fill other locales when saving from this locale (e.g. 'en'), or null to disable
    'fill_other_locales_from' => null,
];

config/openrouter.php

All AI behaviour is controlled via this file — no code changes needed to adjust providers, families, or defaults.

return [
    // API credentials & timeouts
    'openrouter_api_key'   => env('OPENROUTER_API_KEY'),
    'field_with_ai_active' => env('FIELD_WITH_AI_ACTIVE', false),
    'timeout_text'         => env('OPENROUTER_TIMEOUT_TEXT', 30),
    'timeout_image'        => env('OPENROUTER_TIMEOUT_IMAGE', 60),

    // Text generation: providers (display order) and model families per provider
    'text_models' => [
        'providers' => ['openai', 'anthropic', 'google', 'x-ai', 'perplexity'],
        'families'  => [
            'openai'     => ['mini', 'standard'],
            'google'     => ['flash-lite-preview', 'flash-preview', 'pro-preview'],
            'anthropic'  => ['sonnet', 'opus', 'haiku'],
            'perplexity' => ['sonar-base', 'sonar-pro', 'sonar-deep-research', 'sonar-pro-search'],
            'x-ai'       => ['grok-fast'],
        ],
    ],

    // Vision analysis: providers, families, default model and max output tokens
    'vision_models' => [
        'providers' => ['google', 'openai', 'x-ai', 'anthropic'],
        'families'  => [
            'google'    => ['gemini-flash-preview'],
            'openai'    => ['gpt-full'],
            'x-ai'      => ['grok-fast'],
            'anthropic' => ['claude-sonnet', 'claude-opus'],
        ],
        'default_model'         => env('OPENROUTER_DEFAULT_VISION_MODEL', 'google/gemini-3-flash-preview'),
        'max_completion_tokens' => env('OPENROUTER_VISION_MAX_TOKENS', 1000),
    ],

    // Image generation: providers, aspect ratios and sizes (served to frontend at runtime)
    'image_generation' => [
        'providers'          => ['google', 'openai'],
        'openai_id_contains' => 'gpt',
        'provider_priority'  => ['google' => 0, 'openai' => 1],
        'aspect_ratios'      => [
            '1:1', '2:3', '3:2', '3:4', '4:2', '4:3', '4:5', '5:4',
            '9:16', '16:9', '21:9', '1:4', '4:1', '1:8', '8:1',
        ],
        'sizes' => ['0.5K', '1K', '2K', '4K'],
    ],
];

Add the following to your .env file:

OPENROUTER_API_KEY=your-openrouter-api-key
FIELD_WITH_AI_ACTIVE=true
OPENROUTER_TIMEOUT_TEXT=30
OPENROUTER_TIMEOUT_IMAGE=60
OPENROUTER_DEFAULT_VISION_MODEL=google/gemini-3-flash-preview
OPENROUTER_VISION_MAX_TOKENS=1000

Usage

Call ->translatable() on any field:

// Any Nova field
Text::make('Name')
    ->rules('required', 'min:2')
    ->translatable(),

// Any third-party input field
Multiselect::make('Football teams')
    ->rules('required')
    ->translatable(),

// Optionally pass custom locales on a per-field basis
Number::make('Population')
    ->translatable([
        'en' => 'English',
        'ro' => 'Romanian',
    ]),

Source Model Requirements (context checkboxes)

Every context checkbox ("bifă") in the AI drawer is duck-typed: the package probes the source model with method_exists() / attribute reads, and the checkbox only appears when the corresponding data exists. Nothing breaks when a method is missing — the option is simply hidden. To unlock all checkboxes, the source model must implement the methods below.

Baseline (required for the field itself)

RequirementProvided byUsed for
translate(string $locale)Astrotomic\Translatable\Translatable traitreading per-locale context values
getTranslationsArray()same traitresolving field values per locale
translateOrNew(string $locale)same traitsaving translated values

Resource type detection

TypeDetected when
productmodel has getCharacteristicsAttribute()
categorymodel is instanceof App\Modules\Catalog\Models\Category
genericanything else — generic toggles still work

⚠️ App coupling: App\Modules\Catalog\Models\Category and App\Modules\Catalog\Models\Benefit are referenced directly. Category-specific toggles and the benefits fallback only work in apps providing these classes.

"Descriere" template — checkbox requirements

CheckboxModel must implement
Folosește titlul (produsului)translatable title field, or a plain title attribute/column (first non-empty across locales, ro first)
Folosește descrierea scurtă existentătranslatable desc_short or body_short (first non-empty wins)
Folosește descrierea lungă existentătranslatable desc_long, body, description, or content (checked in this order)
Folosește caracteristicilegetCharacteristicsAttribute(): array — key/value pairs (['Culoare' => 'alb', ...]); its presence also marks the resource as product
— brandulbrand relation whose related model exposes name
— categoriacategory relation whose related model has a translatable title
— beneficiilebenefits(): BelongsToMany to Benefit, filtered by is_for_product=1 + is_active=1, ordered by order, with translatable name / body; falls back to Benefit::where('is_general', 1)
Folosește imaginea principalămodel implements Spatie\MediaLibrary\HasMedia with a registered gallery collection (gallery_desktop is preferred when both exist); first image is read from disk, base64-encoded, and passed through vision analysis

FAQ (GEO) template — checkbox requirements

CheckboxModel must implement
Folosește numele produsuluitranslatable title (via translate('ro'), then current locale, then plain attribute)
Folosește caracteristicilegetCharacteristicsAttribute(): array
Folosește numele branduluibrand relation → name
Folosește categoria principalăcategory relation → translatable title
Folosește titlul categoriei(Category resource) translatable title
Folosește descrierea(Category resource) translatable description, body, content, or text (checked in this order)
Folosește beneficiilesame benefits() requirements as above; Category uses only general benefits
Folosește imaginea principalăHasMedia + gallery / gallery_desktop collection
Folosește link-ulgetUrlAttribute() returning the resource's public URL (relative URLs are passed through url()); powers the quick-fill chip

Image tab — checkbox requirements

Checkbox / featureModel must implement
Include regulile AI ale categorieicategory() relation whose related model carries a non-empty string ai_rules attribute (column added app-side)
Preia pozele produselor (master + per-product)products(): BelongsToMany; each related product should provide translatable title, optionally getCharacteristicsAttribute() (attributes + dimensions text), and getFirstMedia() on its gallery collection — products without an image are listed disabled
Salvează pozele în galerieaddMediaFromBase64() / addMediaFromUrl() (Spatie InteractsWithMedia trait); new media is prepended to the gallery / gallery_desktop collection via Media::setNewOrder()

Vision tab ("From model gallery")

FeatureModel must implement
Gallery thumbnail gridgetMedia() (HasMedia); reads the gallery collection, falls back to getMedia('*') filtered to images, max 20

Reference model skeleton

A source model that unlocks every checkbox:

use Astrotomic\Translatable\Translatable;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;

class Product extends Model implements HasMedia
{
    use Translatable, InteractsWithMedia;

    // title + short/long copy as translated attributes
    public array $translatedAttributes = ['title', 'desc_short', 'desc_long'];

    // marks the resource as "product" and feeds the characteristics toggles
    public function getCharacteristicsAttribute(): array
    {
        return ['Culoare' => 'alb', 'Lățime (cm)' => '120'];
    }

    public function brand(): BelongsTo { /* related model exposes ->name */ }

    public function category(): BelongsTo { /* translatable title; optional ai_rules column */ }

    public function benefits(): BelongsToMany { /* Benefit: is_for_product, is_active, order */ }

    // only for bundle/collection-type models — enables "Preia pozele produselor"
    public function products(): BelongsToMany { /* ... */ }

    // enables "Folosește link-ul" (FAQ)
    public function getUrlAttribute(): string { /* public URL */ }

    public function registerMediaCollections(): void
    {
        $this->addMediaCollection('gallery'); // or gallery_desktop
    }
}

AI Features

When FIELD_WITH_AI_ACTIVE=true, an AI button appears on translatable fields. The drawer has three tabs:

Tab 1 — Text Generation

Generate content using pre-defined templates:

TemplateDescription
generalGeneral purpose text
product_titleE-commerce product title (max 70 chars)
product_descriptionDescriere — works on any resource: optional title, existing short/long description, main image analysis; products additionally get attributes, benefits, brand, category
seo_meta_titleSEO meta title (max 60 chars, with optional site name suffix)
seo_meta_descriptionSEO meta description (max 155 chars)
social_media_postSocial media post with hashtags
testimonial_requestCustomer testimonial request email
button_nameCTA button label (max 4 words)
article_titleArticle/blog title (max 80 chars)
article_contentFull article with structured paragraphs
faqFAQ (GEO) — configurable Q&A pairs with optional JSON-LD for AI-search visibility

Available models (text): OpenAI · Anthropic · Google · xAI · Perplexity — fetched dynamically from OpenRouter, cached 1 hour.

Auto-translation — after generation, translate the result to all other configured locales directly from the results section.

Description context — the product_description ("Descriere") template is resource-agnostic. Each context checkbox appears only when the corresponding data exists on the model, so it works on products, categories, collections, or any other resource:

ToggleAppears whenSource
Folosește titlul / titlul produsuluimodel has a titletitle (translation or column)
Folosește descrierea scurtă existentămodel has short copydesc_shortbody_short
Folosește descrierea lungă existentămodel has long copydesc_longbodydescriptioncontent
Folosește caracteristicile / brandul / categoria / beneficiileproduct onlyproduct attributes, brand, category, benefits
Folosește imaginea principalămodel has main_pic_urlgallery media collection → vision analysis
  • The label adapts to the resource (e.g. "Folosește titlul produsului" on products vs. "Folosește titlul" elsewhere).
  • "Toate" toggle checkbox to select/deselect all visible context options at once (indeterminate state when partially selected).
  • Main image: read from disk, base64-encoded, and analyzed via a vision model before the description is generated (any HasMedia model with a gallery collection).
  • On edit, available context options are pre-checked and the free-text prompt becomes optional.
  • What each toggle requires on the model: see Source Model Requirements.

System prompt transparency & override — the drawer shows a collapsible "System prompt pentru acest template" box with the exact system message the LLM will receive for the selected template. A per-generation override checkbox turns it into an editable textarea (default prefilled, max 2000 chars, strip_tags-sanitized). Override applies only to the current request — no DB persistence, resets on template switch or modal close.

FAQ (GEO) template

A dedicated Q&A generator optimized for Generative Engine Optimization — helping Products and Categories appear as cited sources in ChatGPT, Claude, Perplexity, Google AI Overview, and similar AI-powered search.

Count — 3 to 20 pairs (default 5), user-configurable via el-input-number.

Output formats (selectable):

  • Plain TextQ: ... A: ... (default, easiest to copy into blockText blocks)
  • HTML<h3> question / <p> answer (WYSIWYG-ready)
  • Markdown### Question + paragraph
  • JSON — array of { "question": "...", "answer": "..." } objects, ready for programmatic consumption (default)

Schema.org FAQPage JSON-LD — an additive checkbox appends a ---JSONLD--- separator plus a valid FAQPage structured-data object. Backend splits the two sections and returns the JSON-LD separately in a read-only textarea with a copy button, ready to paste into a page's <head> or a dedicated raw-HTML block.

Resource-aware context toggles — the drawer auto-detects the resource and shows only relevant checkboxes:

ResourceToggleSource
ProductFolosește numele produsului$product->translate('ro')->title
ProductFolosește caracteristicilegetCharacteristicsAttribute()
ProductFolosește numele brandului$product->brand->name
ProductFolosește categoria principală$product->category->translate('ro')->title
CategoryFolosește titlul categoriei$category->translate('ro')->title
CategoryFolosește descrierea$category->translate('ro')->description
BothFolosește beneficiileProduct-specific + fallback to general; Category uses general only
BothFolosește imaginea principalăgallery media collection → vision analysis
AnyFolosește link-ulAny public URL (see below)

Link extraction (any public URL) — when "Folosește link-ul" is checked, the backend forces the model to perplexity/sonar-pro-search (or the next available Perplexity Sonar variant) for native web browsing. Supports the resource's own URL (pre-filled from getUrlAttribute() via a quick-fill chip), the manufacturer site, a brand page, or any public page. SSRF-guarded: rejects localhost, RFC1918 IPv4 private ranges, IPv6 loopback (::1), unique-local (fc00::/7), link-local (fe80::/10), and non-http(s) schemes.

Prompt optional — when any context toggle (including use_link) is active, the free-text prompt is optional; the backend still validates prompt presence otherwise. When a link is the only source (no internal context), the prompt instructs the model to generate FAQs exclusively from page content, with hedging for unclear details ("conform fișei producătorului..."). When combined with internal context, the prompt tells the model to combine both, prioritizing page facts over generic info.

Tones available (FAQ-specific): neutru, informativ (default) · conversațional, prietenos · tehnic, precis · profesional, formal. Factual registers only; use the system-prompt override to force unusual tones.

Tab 2 — Image Generation

Generate images using Google Gemini or OpenAI GPT image models.

  • Custom aspect ratios: 1:1, 2:3, 3:2, 4:3, 16:9, 9:16, and more (extended set for Gemini 3.1)
  • Image sizes: 0.5K, 1K, 2K, 4K
  • Multi-image source upload — up to 3 source images for image-to-image generation (multimodal models); grid preview with per-image remove, drag & drop support
  • Image generation gallery — history preserved between generations
  • Download generated images directly from the interface

Tab 3 — Visual Image Analysis (Analiză vizuală imagine)

Describe an image in detail using a vision-capable LLM.

Image source (two sub-tabs):

  • Upload image — drag & drop or click to upload any image from your computer
  • From model gallery — select one image from the current resource's media library (gallery collection), displayed as a thumbnail grid (up to 20 images); click again to deselect

Vision models available:

  • Googlegemini-3-flash-preview (default)
  • OpenAI — latest GPT-4o (non-mini, non-pro, non-audio)
  • xAI — latest Grok fast variant
  • Anthropic — Claude Sonnet / Opus

Models are fetched from OpenRouter using the input_modalities=image&output_modalities=text filter, cached 1 hour.

Controls:

  • Model selector — grouped by provider
  • Max tokens — configurable output length (default: 1000, range: 100–4000); help text: 1 token ≈ 3–4 characters
  • Prompt — pre-filled with a default Romanian prompt instructing the model to describe the image in detail without HTML/Markdown formatting; fully editable

Default prompt:

Descrie imaginea in detaliu. Concentreaza-te pe detaliile tehnice, forma, culoare, formă, materiale vizibile, caracteristici estetice și orice detalii relevante pentru o descriere de produs. Nu formata descrierea in HTML sau Markdown.

The default prompt is translatable via language files (see Multilanguage section).

Result: plain text description in a textarea with a copy button.

Credits & Cost Tracking

All operations display real-time cost in the drawer header:

  • Credite — remaining OpenRouter balance (refreshed after each operation, cached 60s)
  • Last operation cost — cost of the most recent call
  • Session total — cumulative cost for the current drawer session

Multilanguage UI

All static strings in the interface are translatable. The package ships with Romanian (ro.json) and English (en.json) language files.

To customize translations, publish the lang files:

php artisan vendor:publish --tag="nova-translatable-lang"

Published files are located at resources/lang/vendor/nova-astrotranslatable/ and take priority over package defaults. The correct locale file is loaded automatically based on app()->getLocale().

To add a new language, create a new JSON file (e.g. fr.json) in the published directory following the same key structure as en.json.

Validation

Define locale-specific validation rules with ->rulesFor() and the HandlesTranslatable trait:

use Kiritokatklian\NovaAstrotranslatable\HandlesTranslatable;

class Product extends Resource
{
    use HandlesTranslatable;

    public function fields(Request $request)
    {
        return [
            Text::make(__('Name'), 'name')
                ->sortable()
                ->translatable()
                ->rules(['max:255'])
                ->rulesFor('en', [
                    'required',
                ])
                ->rulesFor(['en', 'ro'], function ($locale) {
                    return ["unique:products,name->$locale{{resourceId}}"];
                }),
        ];
    }
}

Edge Cases

BelongsToMany allowDuplicateRelations corner-case

When using this field inside a BelongsToMany as a pivot field with ->allowDuplicateRelations() and you want to filter out exact matches using the NotExactlyAttached rule, use the BelongsToManyTranslatable field instead of the regular BelongsToMany.

Versioning

VersionLaravel NovaLaravelPHPNotes
^1.0^4.12^10|^11>=8.0
^2.0^4.12|^5.0^11|^12>=8.0AI text & image generation
^2.1^4.12|^5.0^11|^12>=8.0Product image analysis, select-all toggle
^2.2^4.12|^5.0^11|^12>=8.0Visual image analysis tab, multilanguage UI (ro/en/fr/de/es), publishable lang files, config-driven providers/families/models/aspect-ratios/sizes
^2.3^4.12|^5.0^11|^12>=8.0Image generation gallery with history, Nova domain route constraint
^2.4^4.12|^5.0^11|^12>=8.0Multi-image source upload (up to 3 images) for image-to-image generation
^2.5^4.12|^5.0^11|^12>=8.0FAQ (GEO) template with schema.org FAQPage JSON-LD, Perplexity Sonar link extraction (any public URL, SSRF-guarded), Category resource support, system prompt inspector with per-generation override, reordered drawer (template/model before prompt)
^2.8^4.12|^5.0^11|^12>=8.0FAQ JSON output format (default) — each pair as { "question": "...", "answer": "..." }; Markdown preview hidden for non-Markdown formats
2.8.2^4.12|^5.0^11|^12>=8.0Respect FIELD_WITH_AI_ACTIVE=false on the frontend — skip models, image-models, vision-models, credits requests in mounted() when AI is disabled
^2.9^4.12|^5.0^11|^12>=8.0Descriere template is now resource-agnostic — title and main-image context decoupled from product resources; new "use existing short/long description" toggles (desc_short/body_short, desc_long/body/description/content); template renamed "Descriere produs" → "Descriere"; non-product resources (Category, Collection, …) pre-check available context on edit
^3.0^4.12|^5.0^11|^12>=8.0Image tab overhaul: unified product source checklist (master "Preia pozele produselor" toggle gating per-product checkboxes, first 10 pre-checked, scrollable), saved images become the product main image (main + close-ups prepended to the gallery collection), single "Salvează pozele în galerie" request, original product image used as source. Text tab: character budget instead of max-tokens (per-template defaults + legend), live character counter on the result, "Descriere" pulls the latest saved gallery image via ajax; generated text is never truncated — length-budget directive + complete-sentence trim on finish_reason: length + reasoning-token reserve & reasoning.effort=low so reasoning models don't return empty text. Generic products() support — any BelongsToMany.
3.0.1^4.12|^5.0^11|^12>=8.0Gallery collection is resolved per model — prefers gallery, falls back to gallery_desktop (some projects renamed it) for save, description main-image, preview and product sources. Source images on the app's own host are base64-inlined at generation time so the image model can use them even when the URL isn't publicly reachable (e.g. local *.test).
3.0.2^4.12|^5.0^11|^12>=8.0Gallery resolution order inverted — gallery_desktop is now preferred over gallery when a model registers both (still falls back to gallery, default unchanged).
3.0.3^4.12|^5.0^11|^12>=8.0Reload the page after saving generated images (and close-ups) to the gallery, so the Nova media field picks up the newly-attached media — fixes the resource Save deleting them because the field's stale list didn't include them.
3.0.4^4.12|^5.0^11|^12>=8.0"Generează close-up-uri" now regenerates close-ups that already have an image, overwriting the previous one — the button no longer skips close-ups whose image was already generated.
^3.1^4.12|^5.0^11|^12>=8.0Per-close-up product source picker (below each description) — the chosen product photos are sent alongside the scene as fidelity references, with a prompt that treats the scene as ambient/lighting only and the product photos as the authoritative product. Own-host source images are now inlined by reading the file from disk instead of an HTTP round-trip to our own server, which self-starved under concurrent requests (file session lock + limited workers) and timed out — fixing close-ups silently failing when several were generated at once.
^3.2^4.12|^5.0^11|^12>=8.0Optional "include category AI rules" toggle on the image tab (checked by default), shown only when the resource's model has a category() relation and the related category carries a non-null ai_rules attribute — generically detected, no-op when the column is absent. When on, the category rules are appended to the scene and close-up image prompts. The ai_rules column + Nova field are added app-side.
^3.3^4.12|^5.0^11|^12>=8.0The "include category AI rules" toggle moved above the "use product photos" checkbox and is now expandable — a "vezi / editează" toggle reveals the rules in an inline textarea, editable for the current session only (edits are used in this session's image prompts, don't touch the category, and reset on reload).
3.3.1^4.12|^5.0^11|^12>=8.0Docs: new "Source Model Requirements" README section — documents the model-level methods/attributes each context checkbox requires (Descriere, FAQ, image tab, vision tab), with a reference model skeleton and cross-links from the feature tables.
3.3.2^4.12|^5.0^11|^12>=8.0Category ai_rules are no longer truncated — the full text reaches the image model: the 2000-char cut at field-meta level was removed, and the generateImage prompt ceiling raised from 1000 to 20000 chars (abuse guard only) so scene and close-up prompts keep the complete rules.

Credits

License

This project is open-sourced software licensed under the MIT license.