techouse/intl-date-time
Locale-aware date and time formatting for Laravel powered by PHP's Intl extension. Provides Carbon macros, Blade directives, validation rules, and easy localization of dates across your app, with dozens of locales supported out of the box. Simple to configure and use.
Two preconditions gate everything: a Laravel Nova license (this is a Nova field, not a general Laravel date helper) and the PHP Intl extension (ext-intl), since all locale rendering runs through ICU. If either is missing, stop — there is no salvageable headless surface here.
composer require techouse/intl-date-time
Then read these three things before writing code, in this order:
vendor/techouse/intl-date-time/composer.json — the nova, php, and laravel/framework constraints. The package is archived, so those constraints are frozen; nobody upstream will widen them for your Nova major.src/ — its make() signature, which IntlDateFormatter style/pattern options it exposes, and whether resolve()/fill() normalize the value before it reaches your model. Also check whether the Carbon macros and Blade directives advertised in the README actually ship, or whether the Nova field is the only real surface.dist/js/field.js — glance at the bundle header. Mix/Vue 2 vs. Vite/Vue 3 is the single detail that predicts whether the field loads on your Nova version.First use case: one Nova resource backed by an existing datetime column, used by an editor whose locale is not en-US. Do not start with a greenfield column.
use Techouse\IntlDateTime\IntlDateTime;
public function fields(Request $request)
{
return [
IntlDateTime::make('Published At')
->rules('required', 'date')
->sortable(),
];
}
Then run the only test that matters before committing: create a record, save, reload the edit form, and confirm the value round-trips without drift. If that fails on your Nova major, stop and build the thin owned field instead — Flatpickr + Intl.DateTimeFormat, ~150–300 lines, comparable effort and far less lifecycle risk.
Adopt per-field, never globally. The field is declarative and stateless — no migration, no config publish, no queue, no service provider work beyond Nova's asset registration. Roll it onto one resource, watch it for a sprint, then expand. Keeping it off your highest-traffic admin screens limits blast radius when a Nova upgrade breaks it.
Separate display locale from storage format. The picker renders in the user's locale; the column persists ISO-8601/UTC. Verify by inspecting the raw request payload after a save — do not assume.
// Model: keep the cast, keep UTC in the column
protected $casts = ['published_at' => 'datetime'];
// Locale and timezone come from the user, not from config
'locale' => fn () => auth()->user()?->locale ?? config('app.locale'),
'timezone' => fn () => auth()->user()?->timezone ?? config('app.timezone'),
// FormRequest: validate the server, never the rendered string
'published_at' => ['required', 'date_format:Y-m-d\TH:i:s\Z'],
Wrap it in your own field class. Highest-leverage pattern for an archived dependency — it gives you a one-line swap point later.
namespace App\Nova\Fields;
use Techouse\IntlDateTime\IntlDateTime;
class LocalizedDateTime extends IntlDateTime
{
public function component()
{
return 'localized-date-time'; // your published/owned Vue component
}
}
Every resource then references LocalizedDateTime, and replacing the implementation touches one file.
Fork-and-own once the spike passes. Consume via a VCS repository entry rather than Packagist, tag it, rebuild assets under your own toolchain, and add a smoke test asserting a save/load round-trip.
"repositories": [{"type": "vcs", "url": "[email protected]:acme/nova-intl-datetime.git"}],
"require": {"acme/nova-intl-datetime": "^1.0"}
Pin expectations in browser tests, not in your head. Locale rendering depends on the client's ICU data, so a Dusk/Playwright assertion on the rendered input string is worth more than a PHP unit test. Pair it with a server-side assertion on what actually lands in the column.
Use it as a reference implementation even if you don't ship it. MIT-licensed, one field class plus a Vue component — reading the source is often faster than evaluating alternatives and will sharpen your estimate for the owned build.
date/date_format rules plus a datetime cast are non-negotiable; without them you will persist 03/04 verbatim.APP_TIMEZONE.grep the field class and the compiled bundle.resolve(), fill(), or component() in your subclass — that is the whole surface area.Intl call. Do not profile; spend that time on the ownership plan.How can I help you explore Laravel packages today?