A Laravel package for generating TypeScript/JavaScript enums and frontend translation files from your Laravel app.
- Automatically discover and generate frontend enums from PHP backed enums
- Opt-in enum generation using the
#[GenerateEnum]attribute - Flexible discovery via configurable paths (supports Modules or custom structures)
- Generate TypeScript or JavaScript output
- Support for translation file generation (TS, JS, or JSON)
- Configurable backed-enum discovery toggle and excludes
- PHP 8.2 or higher
- Laravel 11.x orhigher
Install the package via Composer:
composer require gaiatools/laravel-type-bridgePublish the configuration file:
Smart config publishing
php artisan type-bridge:publishOr Laravel config publishing
php artisan vendor:publish --tag=type-bridge-configSee config/type-bridge.php for all configuration options and inline documentation.
The translator utilities generated by this package can work with multiple i18n libraries through a tiny "engine" interface (an object exposing t(key: string): string). Out of the box we support:
- i18next
- react-i18next (uses the same i18next engine)
- vue-i18n
Tip: In config/type-bridge.php you can set i18n_library to control how translation keys are generated/organized for your target library. The default i18next also covers react-i18next.
Create a backed enum in your Laravel application:
<?php
namespace App\Enums;
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
}Generate the frontend enum (TypeScript by default):
php artisan type-bridge:enumsThis will create a TypeScript file at resources/js/enums/generated/Status.ts:
// !!!!
// This is a generated file.
// Do not manually change this file
// !!!!
export const Status = {
Active: 'active',
Inactive: 'inactive',
Pending: 'pending',
} as const;
export type Status = typeof Status[keyof typeof Status];To generate JavaScript instead of TypeScript:
php artisan type-bridge:enums --format=jsValidate that your previously generated frontend enum files are still in sync with the current PHP enums without writing any files. This is ideal for CI to detect drift.
Run:
php artisan type-bridge:enums --check [--format=ts|js]Behavior:
- Discovers current PHP enums and computes the expected frontend entries.
- Loads the previously generated frontend files from your configured output path.
- Compares both keys and values.
- New case → reported as added (green in decorated terminals)
- Removed case → reported as removed (red in decorated terminals)
- Changed value → reported as both remove (old) and add (new) on the same row
- Exit codes:
- 0 when everything is in sync
- 1 when any difference is detected (suitable for failing CI)
Notes:
--formatcontrols which frontend files to compare against by extension (tsorjs). If omitted, the command uses your configuredtype-bridge.output_format.- Each drifted enum is shown as its own table with the fully-qualified class name as the header, to avoid ambiguity when multiple enums share a short name.
- Cases with the same key are matched onto the same row so value changes are immediately readable side-by-side. Pure additions or removals appear on their own row with
-in the empty column. - When an enum has both unmatched removals and unmatched additions, a warning is shown — this may indicate a rename where frontend references need to be updated manually.
Examples
In sync:
Checking enums against previously generated frontend files...
✅ Enums are in sync with generated frontend files.
Differences found (new case added to the PHP enum):
❌ Enums differ from generated frontend files:
App\Enums\Status
+----------+---------------------+
| Removed | Added |
+----------+---------------------+
| - | ARCHIVED: 'archive' |
+----------+---------------------+
Run `php artisan type-bridge:enums --dirty --format=ts` to regenerate.
Value change:
❌ Enums differ from generated frontend files:
App\Enums\Status
+---------------------+-------------------+
| Removed | Added |
+---------------------+-------------------+
| PENDING: 'awaiting' | PENDING: 'pending'|
+---------------------+-------------------+
Run `php artisan type-bridge:enums --dirty --format=ts` to regenerate.
Possible rename (unmatched removal and addition):
❌ Enums differ from generated frontend files:
App\Enums\Status
+--------------------+---------------------+
| Removed | Added |
+--------------------+---------------------+
| DRAFT: 'draft' | - |
| - | ARCHIVED: 'archive' |
+--------------------+---------------------+
⚠ Unmatched removals and additions detected — if any are renames, update references manually before regenerating.
Run `php artisan type-bridge:enums --dirty --format=ts` to regenerate.
Output (resources/js/enums/generated/Status.js):
// !!!!
// This is a generated file.
// Do not manually change this file
// !!!!
export const Status = {
Active: 'active',
Inactive: 'inactive',
Pending: 'pending',
};Generate only enums that are missing or out of sync with the frontend output, using the same drift criteria as --check. This is useful for incremental builds or large projects.
Run:
php artisan type-bridge:enums --dirty [--format=ts|js]Behavior:
- Computes diffs exactly like
--check(missing files, added/removed cases, or changed values). - Writes only the enums that are dirty.
- Prints
No dirty enums found.when everything is already in sync.
Use the #[GenerateEnum] attribute to explicitly mark enums for generation:
<?php
namespace App\Enums;
use GaiaTools\TypeBridge\Attributes\GenerateEnum;
#[GenerateEnum]
enum ThemeVisibility: string
{
case Private = 'private';
case Unlisted = 'unlisted';
case Public = 'public';
}Enum groups let you export curated subsets or mappings alongside the base enum. Groups are defined by public static methods and are only included when you opt-in via #[GenerateEnum(includeMethods: [...])].
Rules:
- Each listed method must be
public staticwith zero parameters. - The method must return an array.
- A sequential array becomes a group array unless it contains only enum cases; arrays of enum cases become a group record keyed by case name.
- An associative array becomes a group record.
- Values may be enum cases, backed values that match a case, or scalar/null literals.
- The group name is the method name converted to StudlyCase and must not collide with the enum name or other groups.
Example:
<?php
namespace App\Enums;
use GaiaTools\TypeBridge\Attributes\GenerateEnum;
#[GenerateEnum(includeMethods: ['staffRoles', 'memberRoles'])]
enum UserRole: string
{
case Admin = 'admin';
case Manager = 'manager';
case Support = 'support';
case Member = 'member';
case Guest = 'guest';
/** @return array<int, self> */
public static function staffRoles(): array
{
return [self::Admin, self::Manager, self::Support];
}
/** @return array<int, string> */
public static function memberRoles(): array
{
return [self::Member->value, self::Guest->value];
}
}Generated output (TypeScript):
export const UserRole = {
Admin: 'admin',
Manager: 'manager',
Support: 'support',
Member: 'member',
Guest: 'guest',
} as const;
export type UserRole = typeof UserRole[keyof typeof UserRole];
export const StaffRoles = {
Admin: UserRole.Admin,
Manager: UserRole.Manager,
Support: UserRole.Support,
} as const;
export type StaffRoles = typeof StaffRoles[keyof typeof StaffRoles];
export const MemberRoles = [
UserRole.Member,
UserRole.Guest,
] as const;
export type MemberRoles = typeof MemberRoles[number];# Publish config with smart detection
# Automatically detects TypeScript/JavaScript and your i18n library!
php artisan type-bridge:publish# Generate all enums, translations, and enum translators
php artisan type-bridge:generate
# Generate everything for a specific locale
php artisan type-bridge:generate en
# Limit generation to specific enums (short name or FQCN)
php artisan type-bridge:generate --enums=Status --enums=App\\Enums\\Role
# Use a separate format for translations (ts|js|json)
php artisan type-bridge:generate --translations-format=json# Generate enums using the default format from config (ts or js)
php artisan type-bridge:enums
# Generate enums explicitly as JavaScript
php artisan type-bridge:enums --format=js
# Generate enums explicitly as TypeScript
php artisan type-bridge:enums --format=ts
# Generate only new/changed enums (based on --check drift rules)
php artisan type-bridge:enums --dirty# Generate translations for a locale (outputs to resources/js/lang/generated)
php artisan type-bridge:translations en
# Generate translations as JSON instead of TS (json|js|ts)
php artisan type-bridge:translations en --format=json
# Generate "flat" translation keys instead of nested
php artisan type-bridge:translations en --flatTranslation output examples (for locale en):
- TypeScript (
resources/js/lang/generated/en.ts)
// !!!!
// This is a generated file.
// Do not manually change this file
// !!!!
export const en = {
common: {
ok: "OK",
cancel: "Cancel",
}
} as const;
export type en = typeof en;These reusable helpers are required by the generated enum translators. Run once to publish them into your frontend source tree.
# Publishes:
# - composables/useTranslator.(ts|js)
# - lib/createEnumTranslationMap.(ts|js)
# - lib/translators.(ts|js)
php artisan type-bridge:publish-translator-utils [--force]Defaults (can be changed in config/type-bridge.php → enum_translators.*):
- utils_composables_output_path: resources/js/composables
- utils_lib_output_path: resources/js/lib
The file extensions follow your type-bridge.output_format (ts by default).
Generate per-enum translator composables/functions that map enum values to translated labels using your configured i18n library.
# Uses the global output format (ts|js)
php artisan type-bridge:enum-translators
# Force a specific format
php artisan type-bridge:enum-translators --format=ts
php artisan type-bridge:enum-translators --format=js
# Dry-run: discover candidates and show eligibility without writing files
php artisan type-bridge:enum-translators --dryBy default files are written to:
- resources/js/composables/generated (config: enum_translators.translator_output_path)
Dry-run output columns:
- Enum: FQCN of the PHP enum
- Prefix: Translation key prefix that will be used
- In FE generation set: Whether this enum is part of your frontend enums set
- Has translations: Whether any translations exist for that prefix
Only enums that are both in the FE generation set and have translations are eligible for generation.
The generated translators call a global engine. You must configure it once during your app bootstrapping by calling configureTranslationEngine.
// i18next (also works for react-i18next)
import i18n from '@/i18n';
import { configureTranslationEngine } from '@/composables/useTranslator';
configureTranslationEngine({
t: (key: string) => i18n.t(key),
});
// React + react-i18next note:
// Prefer using the shared i18next instance you initialize for your app (as above).
// If you export that instance from your i18n setup file, this works in both React and non-React code.
// vue-i18n
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import App from './App.vue';
import { configureTranslationEngine } from '@/composables/useTranslator';
const i18nVue = createI18n({ /* ... */ });
const app = createApp(App).use(i18nVue);
// After vue-i18n is registered
configureTranslationEngine({
t: (key: string) => i18nVue.global.t(key) as string,
});
app.mount('#app');// i18next (also works for react-i18next)
import i18n from '@/i18n';
import { configureTranslationEngine } from '@/composables/useTranslator';
configureTranslationEngine({
t: (key) => i18n.t(key),
});
// vue-i18n
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import App from './App.vue';
import { configureTranslationEngine } from '@/composables/useTranslator';
const i18nVue = createI18n({ /* ... */ });
const app = createApp(App).use(i18nVue);
configureTranslationEngine({
t: (key) => i18nVue.global.t(key),
});
app.mount('#app');Once configured, any generated translator like useStatusTranslator() will use the global engine automatically:
import { useTranslator } from '@/composables/useTranslator';
import { createEnumTranslationMap } from '@/lib/createEnumTranslationMap';
import { Status } from '@/enums/generated/Status';
const translations = createEnumTranslationMap(Status, 'Status');
const tStatus = useTranslator(translations); // uses the globally configured engineAfter publishing the utilities and generating translators, you can translate enum values in the frontend.
Global engine configuration (once at app bootstrap) is required. See “Global translation engine setup” above. You can also use the lightweight wrappers from lib/translators:
// Vue i18n helper
import { createI18n } from 'vue-i18n';
import { createVueI18nTranslator } from '@/lib/translators';
import { configureTranslationEngine } from '@/composables/useTranslator';
const i18n = createI18n({ /* ... */ });
configureTranslationEngine(createVueI18nTranslator(i18n));Manual configuration using i18next works similarly:
import i18n from '@/i18n';
import { configureTranslationEngine } from '@/composables/useTranslator';
configureTranslationEngine({ t: (key) => i18n.t(key) });If you generated the Status enum and have translations under the Status.* namespace, you can build a translator on the fly:
import { Status } from '@/enums/generated/Status';
import { createEnumTranslationMap } from '@/lib/createEnumTranslationMap';
import { useTranslator } from '@/composables/useTranslator';
const statusMap = createEnumTranslationMap(Status, 'Status');
const tStatus = useTranslator(statusMap);
tStatus(Status.Active); // → "Active"
// Build select options
const options = tStatus.options();
// → [{ value: 'active', label: 'Active' }, ...]
// Check if a value has a translation mapping
const hasPending = tStatus.has(Status.Pending); // true/falseYou can also override the engine for a specific translator call:
const custom = useTranslator(statusMap, { t: (k) => myCustomFn(k) });-
Configure your target i18n library via
type-bridge.i18n_library(supportsi18nextandvue-i18n). -
Control where generated translator files and utilities are written via
enum_translators.*keys inconfig/type-bridge.php:- translator_output_path
- utils_composables_output_path / utils_composables_import_path
- utils_lib_output_path / utils_lib_import_path
-
The generators respect
type-bridge.output_formatfor TS/JS. -
JavaScript (
resources/js/lang/generated/en.js)
// !!!!
// This is a generated file.
// Do not manually change this file
// !!!!
export const en = {
common: {
ok: "OK",
cancel: "Cancel",
}
};- JSON (
resources/js/lang/generated/en.json)
{
"common": {
"ok": "OK",
"cancel": "Cancel"
}
}The #[GenerateEnum] attribute accepts the following options:
requiresComments(bool): Include PHPDoc comments in generated output
MIT