Search by

adedsec / nova-translated-labels

Adedsec

Translate Laravel Nova menu groups, resources, fields, filters, actions and options from one lang file, and show each label's real database name in grey under it.

Package info

github.com/Adedsec/nova-translated-labels

pkg:composer/adedsec/nova-translated-labels

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.3 2026-11-08 15:43 UTC

This package is auto-updated.

Last update: 2026-11-08 15:44:03 UTC


README

Translate your whole Laravel Nova admin panel from one lang file (menu groups, resources, fields, panels, filters, actions and option labels) and show each label's real database name in small grey text under it.

Nova Translated Labels

Admins read the panel in their own language and still know which table and column they are looking at, so they can find it in Metabase, a SQL client or the code.

شماره تلفن          Title          Author
phone_number        title          author_id
  • Your code keeps its English labels. Translations live in lang/{locale}/nova-labels.php.
  • By default the grey name is the column (phone_number), the foreign key of a BelongsTo (author_id), the related table of a has-many field (comments), the table of a resource (posts), the original name of a group (Content), or the stored value of an option (draft).
  • Prefer the English label your code uses, or both? Set it per kind of label (see Configuration).
  • Works for any language. Right-to-left panels (Nova::enableRTL()) align correctly.
  • Labels that are already in a non-Latin script are left alone, so you can translate gradually.
  • An artisan command lists every label that is still untranslated, ready to paste.

Requirements

  • PHP 8.2+
  • Laravel 11, 12 or 13
  • Laravel Nova 5

Installation

composer require adedsec/nova-translated-labels

Add the trait to your base Nova resource (app/Nova/Resource.php). Every resource that extends it is then translated:

use Adedsec\NovaTranslatedLabels\Concerns\HasTranslatedLabels;
use Laravel\Nova\Resource as NovaResource;

abstract class Resource extends NovaResource
{
    use HasTranslatedLabels;
}

To translate the fields inside an action's modal, or a filter's name and options, add a trait to those classes too. Action names are translated by the resource trait already.

use Adedsec\NovaTranslatedLabels\Concerns\TranslatesActionFields;

class PublishPost extends Action
{
    use TranslatesActionFields;
}
use Adedsec\NovaTranslatedLabels\Concerns\TranslatesFilterLabels;

class StatusFilter extends Filter
{
    use TranslatesFilterLabels;
}

Publish the lang file for your app locale, and optionally the config:

php artisan vendor:publish --tag=nova-translated-labels-lang
php artisan vendor:publish --tag=nova-translated-labels-config

Writing translations

lang/fa/nova-labels.php (use your own locale):

return [
    'groups' => [
        'Content' => 'محتوا',
    ],

    'resources' => [
        // keyed by the resource's uriKey
        'posts' => ['label' => 'مقاله‌ها', 'singular' => 'مقاله'],
    ],

    'fields' => [
        // keyed by the name you pass to make()
        'Title' => 'عنوان',
        'Created At' => 'تاریخ ایجاد',
    ],

    'panels' => ['Details' => 'جزئیات'],
    'filters' => ['Status' => 'وضعیت'],
    'actions' => ['Publish Post' => 'انتشار مقاله'],

    'options' => [
        // Select, Badge, BooleanGroup and filter option labels
        'Draft' => 'پیش‌نویس',
    ],

    'overrides' => [
        // when one resource needs a different word, keyed by uriKey
        'orders' => ['fields' => ['Status' => 'وضعیت سفارش']],
    ],
];

Anything without a translation keeps its English text and still gets its grey real name.

Finding what is left

php artisan nova-translated-labels:missing          # table
php artisan nova-translated-labels:missing --json   # keyed like the lang file

--json prints only the JSON, nested like nova-labels.php with an empty value for each label to fill in, so you can redirect it into a file. An empty value still counts as missing, so a label you have not filled in yet stays on the list.

The command walks every resource in app/Nova (change it with --path), including fields, actions, filters and options. It exits with a failure code while anything is missing, or when a resource could not be walked, so you can run it in CI. If some fields look at the current user, pass --user=1 to walk the resources as that user.

Configuration

config/nova-translated-labels.php:

Key Default What it does
file nova-labels Lang file name: lang/{locale}/{file}.php. The lang publish tag uses this name, so set it before publishing.
subtitles.groups original What to show in grey under menu groups: original, none
subtitles.resources / fields / options database What to show in grey: database (table, column, foreign key or stored value), original (the English label from your code), both, or none
translated_pattern any non-Latin letter Labels matching it count as already translated. Set it to null when your admin language uses the Latin script.
max_static_options 40 Option lists longer than this are treated as database rows and left untranslated
script true Load the script that draws the grey line

How it works

The resource trait hooks into Nova's field, action and group resolution and replaces each label with its translation. It then appends the real name between two invisible separator characters (U+2063). A small script that Nova loads on every page turns that marked text into the grey line in the sidebar, the index headers, the detail and form labels and the filter menu. Everywhere else (buttons, toasts, breadcrumbs, input placeholders, tooltips, the browser tab title), the script removes the real name.

A few things to know:

  • Native dropdowns (<option>) cannot hold styled text, so their options show the value inline: پیش‌نویس (draft).
  • Action keys do not change. Nova builds an action's URL key from its name, so the package pins the original key before renaming the action. Existing action URLs and export filenames keep working.
  • The action log stores the translated name. Nova writes an action's name into action_events when it runs, so entries are in the language that was active at the time.
  • CSV exports are untouched. Their headers come from your export code, not from field labels.
  • Validation messages use the translated label. The API response also carries the marked real name, which the script removes on screen.
  • Relation fields are only resolved to a foreign key or table when Nova itself treats them as relations. A plain field that happens to share a name with a model method is never called.

Not translated yet

  • Lenses, including their fields. The missing command does not walk them either.
  • Metrics, cards and dashboards.
  • The names of individual tabs. A Tab::group() heading is translated like a panel, but the tabs inside it keep their names.
  • Section names you write yourself in Nova::mainMenu(). Wrap them in __() instead. Resources listed there with MenuItem::resource() are still translated.

Testing

Nova is not on Packagist, so installing this package's dev dependencies needs a Nova licence:

composer config http-basic.nova.laravel.com your-email@example.com your-license-key
composer install
composer test
composer analyse

The script in dist/ has its own tests, which run in Node 22 with jsdom:

npm install
npm test

Contributing

Issues and pull requests are welcome. Please add a test for any behaviour you change. CI cannot install Nova for pull requests from forks, so only the script tests run there; run the PHP tests locally before you open one.

License

The MIT License (MIT). See LICENSE.