jetstreamlabs/zora
Zora is a Laravel package from Jetstream Labs that simplifies adding AI-powered features to your application. It offers a clean, developer-friendly API and sensible defaults so you can integrate intelligent workflows quickly while keeping your code expressive.
Zora bridges Laravel's lang/ directory and your JavaScript bundle. Instead of duplicating strings in Vue/React and PHP, you keep one source of truth in Laravel and let Zora emit it as a JS-importable asset.
Minimal setup:
Install the package:
composer require jetstreamlabs/zora
Publish its config (tag name may vary — check php artisan vendor:publish output):
php artisan vendor:publish --provider="JetstreamLabs\Zora\ZoraServiceProvider"
Generate the translation asset:
php artisan zora:generate
This typically writes JSON (or a JS module) into resources/js/ or public/ — confirm the target path in config/zora.php.
Import it in your frontend:
import translations from '@/zora/en.json';
// or, if Zora exposes a helper:
import { trans } from '@/zora';
Where to look first: the published config/zora.php (source paths, output path, locales, format) and the generated file itself — its shape tells you exactly how to consume keys. Then check the package's service provider for registered commands and any Blade directive or Vite hook.
First use case: take a hardcoded string in a Vue component ("Save changes"), move it to lang/en/messages.php under 'save' => 'Save changes', regenerate, and swap the literal for trans('messages.save'). Once that works, the rest is scaling.
Generate at build time, not runtime. Wire zora:generate into your asset build so translations can't drift:
// package.json
"scripts": {
"dev": "php artisan zora:generate && vite",
"build": "php artisan zora:generate && vite build"
}
Or add it to composer.json post-autoload-dump / a deploy script. Avoid generating on every request — treat the output as a compiled artifact.
Per-locale files with dynamic import. If Zora emits one file per locale, lazy-load the active one:
const locale = document.documentElement.lang || 'en';
const messages = await import(`@/zora/${locale}.json`);
This keeps bundles small and supports locale switching without a full reload.
Bridge Laravel's locale into JS. Share the current locale from Blade or Inertia:
<script>window.__LOCALE__ = @json(app()->getLocale());</script>
// Inertia middleware
'locale' => app()->getLocale(),
Then hydrate your i18n library (vue-i18n, react-i18next) from Zora's output and set the active locale from that value.
Nested keys and dot notation. Laravel's lang/en/messages.php with ['auth' => ['failed' => '...']] should map to messages.auth.failed. Decide early whether your JS trans() helper resolves dot paths itself or whether you flatten keys at generation time — pick one convention and enforce it across the team.
Reuse existing i18n libraries. Zora's job is producing the data; let vue-i18n or i18next handle interpolation, pluralization, and fallbacks. Feed Zora's output in as the message catalog rather than building a bespoke trans() from scratch.
Watch mode in development. If Zora ships a --watch flag, use it alongside Vite. If not, add a small filesystem watcher on lang/ in your dev script so edits to PHP translation files trigger regeneration and HMR.
CI/deploy checklist. Run zora:generate in the pipeline after composer install and before npm run build. If you commit generated files, add a CI step that regenerates and fails on a dirty diff — that catches developers who edited lang/ but forgot to regenerate.
Stale output is the #1 failure mode. The generated asset is a snapshot. Changing lang/en/foo.php and reloading won't reflect anything until you regenerate. If a string "won't update," check the generated file before debugging the frontend.
Escaping and HTML. Laravel's __() doesn't escape by default, but JSON serialization will faithfully carry quotes, apostrophes, and < characters. If you render translations with v-html or dangerouslySetInnerHTML, sanitize on the PHP side or avoid the pattern entirely.
Pluralization and placeholders. PHP's trans_choice and :name placeholders are Laravel-specific syntax. Confirm whether Zora transforms these into ICU/vue-i18n format or passes them through raw — if raw, your JS library won't understand :count or | plural separators. Handle this in a formatter layer or at the translation-file level.
Locale fallback. Laravel falls back to fallback_locale when a key is missing; your JS side won't unless you configure it. Either merge fallback-locale messages into each locale's output at generation time, or configure your i18n library's fallbackLng to match config('app.fallback_locale').
Config path quirks. Packages like this often default to lang/ (Laravel 8 style) while newer apps use lang/ at the project root vs. resources/lang/. Verify config/zora.php points at the right directory for your Laravel version; a silently empty generated file usually means a wrong source path.
Caching. If Zora caches compiled translations, php artisan cache:clear (and possibly config:clear) resolves "I regenerated but nothing changed." Also clear any Vite/Webpack persistent cache when the output path changes.
Don't ship every locale to every user. Large apps with many locales generate big files. Emit per-locale assets and lazy-load, or split by namespace if the package supports it. A single 500 KB translations.js in the main bundle is a common accidental regression.
Extension points to look for: custom output formatters (JS module vs. JSON vs. TS types), configurable key-flattening, namespace grouping, and a Blade @zora directive or Vite plugin. If the defaults don't fit, subclassing the generator or wrapping the command in your own artisan command is usually cleaner than post-processing files.
Low-activity package caution. With modest adoption and a single recent release, pin the version ("jetstreamlabs/zora": "^1.0") and read the generator source before relying on it. The surface area is small enough that forking or wrapping it is viable if maintenance lags — the core logic (walk the lang/ tree, emit JSON) is straightforward and worth understanding regardless.
How can I help you explore Laravel packages today?