pikbdesigns/laravel-full-translation

Laravel Full Translation — a Laravel 13 package for URL-based multilingual routing (/en/, /es/) with automatic locale detection, translated route slugs, a built-in string scanner, and Artisan commands to export and audit your app's translation completeness. No manual route duplication required.

Maintainers

Package info

github.com/pikbdesigns/laravel-full-translation

pkg:composer/pikbdesigns/laravel-full-translation

Transparency log

Statistics

Installs: 14

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.4 2026-08-02 17:45 UTC

This package is auto-updated.

Last update: 2026-08-02 19:30:20 UTC


README

URL-based multilingual support for Laravel without rewriting every route.

Add locale prefixes (/en/about, /es/about) to your Laravel app with minimal setup. The package handles locale detection (URL, session, cookie, browser Accept-Language), route registration, URL generation, and translation file management.

Table of Contents

Requirements

  • PHP 8.2+
  • Laravel 11+

Installation

composer require pikbdesigns/laravel-full-translation

The service provider and facade are auto-discovered. Publish the config:

php artisan vendor:publish --tag=translations-config

Optionally publish the views (for the language switcher component):

php artisan vendor:publish --tag=translations-views

Configuration

The published config lives at config/full-translation.php. All keys have sensible defaults.

Locale Settings

Key Default Description
default_locale 'en' Fallback locale when none is detected
supported_locales ['en', 'es', 'fr'] Locales your app supports. Can be a flat array or associative array with rich metadata
locale_mapping [] Maps URL slugs to internal locale codes (e.g., 'pt-br' => 'pt_BR')
hide_default_locale false When true, the default locale has no URL prefix (/about vs /en/about)
locales_order [] Custom order for locales in the language switcher (e.g., ['es', 'fr', 'en'])

Rich Locale Metadata

supported_locales can be a simple array of codes or an associative array with metadata:

// Simple (default)
'supported_locales' => ['en', 'es', 'fr'],

// Rich metadata
'supported_locales' => [
    'en' => ['name' => 'English', 'script' => 'Latn', 'native' => 'English', 'regional' => 'en_GB'],
    'es' => ['name' => 'Spanish', 'script' => 'Latn', 'native' => 'español', 'regional' => 'es_ES'],
    'fr' => ['name' => 'French', 'script' => 'Latn', 'native' => 'Français', 'regional' => 'fr_FR'],
],

The native name is used in the language switcher when available. The regional key maps to PHP locale codes for LC_TIME and LC_MONETARY.

Detection & Persistence

Key Default Description
use_session true Store detected locale in the session
use_cookie true Store detected locale in a cookie
cookie_name 'locale' Cookie name for locale persistence
cookie_lifetime 525600 Cookie lifetime in minutes (default: 1 year)
use_accept_language true Use browser Accept-Language header for detection

URL Behavior

Key Default Description
route_prefix '{locale}' Placeholder used in route prefixes
localized_urls true When true, Route::localized() prefixes routes with the locale. When false, routes are registered without prefixes (see Non-prefixed mode)
route_name_strategy 'localized' Route naming: 'localized' (localized.en.about) or 'original' (keep your names, see Route name strategy)
urls_ignored [] URL patterns to skip locale processing (e.g., ['/nova', '/nova/*'])
http_methods_ignored ['POST', 'PUT', 'PATCH', 'DELETE'] HTTP methods that skip locale processing

Scanner Settings

Key Default Description
scan_helpers [...] Function/directive names the scanner extracts strings from
scan_paths ['app', 'resources/views', 'routes'] Directories to scan for translatable strings
excluded_directories [] Directories to exclude from scanning (relative to scan_paths)
file_patterns ['*.php', '*.blade.php'] File patterns to scan (supports * and ? wildcards)
allow_newlines false Whether strings containing newlines are included in scanning

Export Settings

Key Default Description
sort_keys true Sort translation keys alphabetically in exported files
translated_sort_order 'alpha' Sort order when sort_keys is false: 'alpha', 'top', or 'bottom'
add_manual_strings true Automatically add strings from manual-strings.json to translation files on export
exclude_translation_keys true Exclude Laravel PHP translation keys from JSON export if they have translations

Middleware

Register these in your app/Http/Kernel.php (or bootstrap/app.php in Laravel 11+):

Middleware Purpose
SetLocale Detects locale from URL, session, cookie, or browser and sets app()->setLocale()
LocaleSessionRedirect Redirects GET requests to the session-stored locale's URL
LocaleCookieRedirect Redirects GET requests to the cookie-stored locale's URL
HideDefaultLocaleInUrl 302 redirects /en/about to /about when en is the default

Global middleware (bootstrap/app.php)

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'locale.set' => \Pikbdesigns\FullTranslation\Http\Middleware\SetLocale::class,
        'locale.session' => \Pikbdesigns\FullTranslation\Http\Middleware\LocaleSessionRedirect::class,
        'locale.cookie' => \Pikbdesigns\FullTranslation\Http\Middleware\LocaleCookieRedirect::class,
        'locale.hide' => \Pikbdesigns\FullTranslation\Http\Middleware\HideDefaultLocaleInUrl::class,
    ]);
})

Recommended order

Apply SetLocale early (typically in the web middleware group), then the redirect middlewares. Use HideDefaultLocaleInUrl when hide_default_locale is true.

Ignoring URLs and methods

The SetLocale middleware respects urls_ignored and http_methods_ignored:

// Skip locale processing for Nova and API routes
'urls_ignored' => ['/nova', '/nova/*', '/api/*'],

// Skip locale processing for form submissions
'http_methods_ignored' => ['POST', 'PUT', 'PATCH', 'DELETE'],

Route Registration

Route::localized() macro

The package registers a Route::localized() macro that creates route groups for every supported locale:

Route::localized(function () {
    Route::get('/about', [PageController::class, 'about'])->name('about');
    Route::get('/contact', [PageController::class, 'contact'])->name('contact');
});

This generates named routes like localized.en.about, localized.es.about, etc. The SetLocale middleware is automatically applied to each group.

Non-prefixed mode

Set localized_urls to false when your application does not want locale prefixes in URLs. This is common for admin panels, dashboards, or apps that serve one locale per user (e.g., an authenticated user's preferred language).

'localized_urls' => false,

With this mode:

Route::localized(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
});
  • Routes are registered at /dashboard (no locale prefix)
  • Only the SetLocale middleware is applied; the locale is resolved from the session, cookie, or Accept-Language header
  • Routes are named localized.dashboard
  • The redirect middlewares (LocaleSessionRedirect, LocaleCookieRedirect, UnlocalizedRedirect, RootRedirect) are not registered, since there is no localized URL to redirect to

Use the locale switching endpoint to let users change their locale in this mode.

Route name strategy

By default, Route::localized() names routes localized.{locale}.{name} (e.g., localized.en.about). Set route_name_strategy to 'original' to keep the names you give routes in the callback:

'route_name_strategy' => 'original',
Route::localized(function () {
    Route::get('/about', [PageController::class, 'about'])->name('about');
});

This registers the routes under the plain name about (plus localized.en.about, localized.es.about, etc. under the 'localized' strategy).

Caveat: a Laravel route name maps to exactly one route. When you register the same name across several locale groups, route('about') resolves to the last-registered locale's URL. Prefer the default 'localized' strategy for new apps, and use 'original' only when migrating an existing app and you need route('name') calls to keep working.

hide_default_locale behavior

When hide_default_locale is false (default):

  • All routes are registered with locale prefixes: /en/about, /es/about, /fr/about
  • Unlocalized URLs (e.g., /about) redirect to the localized version (e.g., /en/about)
  • The redirect respects session, cookie, and Accept-Language header for locale selection
  • Root / redirects to /{locale}/

When hide_default_locale is true:

  • The default locale is served without a prefix: /about
  • Other locales still use prefixes: /es/about, /fr/about
  • Visiting /en/about redirects to /about (via HideDefaultLocaleInUrl middleware)

Manual approach

If you prefer explicit control:

Route::prefix('{locale}')
    ->middleware(['locale.set'])
    ->group(function () {
        Route::get('/about', [PageController::class, 'about'])->name('about');
    });

Config Presets

Common configurations are just a few config keys. Pick the preset that matches your app.

SEO / public site (locale-prefixed URLs)

'localized_urls' => true,
'route_name_strategy' => 'localized',
'hide_default_locale' => false,
'use_session' => true,
'use_cookie' => true,
'use_accept_language' => true,

Admin panel / per-user locale (no URL prefixes)

'localized_urls' => false,
'route_name_strategy' => 'original',
'hide_default_locale' => false,
'use_session' => true,
'use_cookie' => true,
'use_accept_language' => false,

Hybrid (public web localized, API untouched)

Keep localized_urls => true and exclude API paths from locale processing:

'localized_urls' => true,
'urls_ignored' => ['/api', '/api/*', '/nova', '/nova/*'],
'http_methods_ignored' => ['POST', 'PUT', 'PATCH', 'DELETE'],

See Mixed Stacks (Web + API) for the full pattern.

Helper Functions

All helpers are globally available (loaded via autoload files):

// Generate a localized URL for the current or given locale
localizeUrl('es');          // https://example.com/es/about
localizeUrl(null, '/faq');  // https://example.com/en/faq

// Get the current application locale
getCurrentLocale(); // 'en'

// Get all supported locales (codes only)
getSupportedLocales(); // ['en', 'es', 'fr']

// Check if a locale is the default
isDefaultLocale('en'); // true
isDefaultLocale();     // checks current locale

// Strip locale prefix from a URL
getNonLocalizedUrl('/es/about'); // '/about'

// Render a locale-specific view with fallback
localizedView('pages.home');        // tries pages.home.en, falls back to pages.home
localizedView('pages.home', [], 'es'); // tries pages.home.es

Facade

The facade is registered as FullLocalization:

use Pikbdesigns\FullTranslation\Facades\FullLocalization;

// Basic
FullLocalization::getLocale();                          // 'en'
FullLocalization::setLocale('es');
FullLocalization::getDefaultLocale();                   // 'en'
FullLocalization::isDefaultLocale('en');                // true

// Locales
FullLocalization::getSupportedLocales();                // ['en', 'es', 'fr']
FullLocalization::getSupportedLocalesWithMetadata();    // ['en' => ['name' => 'English', ...], ...]
FullLocalization::getAvailableLocales();                // [['name' => 'English', 'code' => 'en', 'native' => 'English'], ...]
FullLocalization::getLocalesOrder();                    // []

// URLs
FullLocalization::getLocalizedUrl('es');                // https://example.com/es/about
FullLocalization::getLocalizedUrl('es', '/faq', false); // '/es/faq'
FullLocalization::getNonLocalizedUrl('/es/about');       // '/about'

// Route translations
FullLocalization::getRouteTranslations('es');            // require lang/es/routes.php
FullLocalization::getTranslatedRoute('about', 'es');    // 'acerca-de'
FullLocalization::mapLocale('pt-br');                   // 'pt_BR' (if mapped)

// Locale checks
FullLocalization::checkLocaleInSupportedLocales('es');  // true
FullLocalization::isHiddenDefault('en');                // false

// Ignoring
FullLocalization::getUrlsIgnored();                     // ['/nova', '/nova/*']
FullLocalization::getHttpMethodsIgnored();              // ['POST', 'PUT', 'PATCH', 'DELETE']
FullLocalization::isUrlIgnored('/nova/dashboard');      // true
FullLocalization::isHttpMethodIgnored('POST');          // true

Language Switcher

A Blade component is included at resources/views/language-switcher.blade.php:

@include('full-translation::language-switcher')

It renders links for each supported locale (except the current one, shown as active text). When using rich locale metadata, it displays native language names (e.g., "Français" instead of "FR").

You can publish and customize it:

php artisan vendor:publish --tag=translations-views

For a fully custom switcher, render the available locales yourself:

@php
    $locales = \Pikbdesigns\FullTranslation\Facades\FullLocalization::getAvailableLocales();
    $current = \Pikbdesigns\FullTranslation\Facades\FullLocalization::getLocale();
    $requestUrl = request()->getRequestUri();
@endphp

<div class="language-switcher">
    @foreach ($locales as $locale)
        @if ($locale['code'] !== $current)
            <a href="{{ \Pikbdesigns\FullTranslation\Facades\FullLocalization::getLocalizedUrl($locale['code'], $requestUrl, true) }}"
               hreflang="{{ $locale['code'] }}"
               title="{{ $locale['native'] ?? $locale['name'] }}">
                {{ $locale['native'] ?? strtoupper($locale['code']) }}
            </a>
        @else
            <span class="active" aria-current="page">
                {{ $locale['native'] ?? strtoupper($locale['code']) }}
            </span>
        @endif
    @endforeach
</div>

Each link points to the current page translated into that locale (via getLocalizedUrl()), with the active locale rendered as a <span> instead of a link. In non-prefixed mode, swap the href for the locale switching endpoint instead:

<a href="{{ route('locale.switch', $locale['code']) }}">{{ $locale['native'] ?? strtoupper($locale['code']) }}</a>

Locale switching endpoint

The package ships an invokable LocaleController that validates the requested locale, sets it as active, persists it to the session and cookie (respecting use_session / use_cookie), and redirects back.

Register the route (for example in routes/web.php):

use Pikbdesigns\FullTranslation\Http\Controllers\LocaleController;

Route::get('/locale/{locale}', LocaleController::class)->name('locale.switch');

The controller returns a 404 for locales not listed in supported_locales. Wire it into the language switcher:

@foreach (\Pikbdesigns\FullTranslation\Facades\FullLocalization::getAvailableLocales() as $locale)
    <a href="{{ route('locale.switch', $locale['code']) }}">{{ $locale['native'] ?? strtoupper($locale['code']) }}</a>
@endforeach

This endpoint is especially useful in non-prefixed mode, where there are no localized URLs to link to.

Custom ordering

Use locales_order to control the display order:

'locales_order' => ['es', 'fr', 'en'],

Mixed Stacks (Web + API)

Many apps combine localized web routes with API routes that must not be localized. The pattern is simple: keep localized_urls => true for the web, and exclude the API paths from locale processing via urls_ignored.

Config

'localized_urls' => true,
'urls_ignored' => ['/api', '/api/*', '/nova', '/nova/*'],
'http_methods_ignored' => ['POST', 'PUT', 'PATCH', 'DELETE'],

Routes

Register API routes outside Route::localized() (typically in routes/api.php):

// routes/api.php - not localized
Route::prefix('v1')->group(function () {
    Route::get('/users', [UserController::class, 'index']);
});
// routes/web.php - localized
Route::localized(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
});

Middleware ordering (Laravel 11+)

Laravel 11+ registers middleware in bootstrap/app.php. Apply SetLocale in the global web group so locale detection runs before your route middleware. If you registered the redirect middlewares, keep them after SetLocale:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'locale.set' => \Pikbdesigns\FullTranslation\Http\Middleware\SetLocale::class,
        'locale.session' => \Pikbdesigns\FullTranslation\Http\Middleware\LocaleSessionRedirect::class,
        'locale.cookie' => \Pikbdesigns\FullTranslation\Http\Middleware\LocaleCookieRedirect::class,
        'locale.hide' => \Pikbdesigns\FullTranslation\Http\Middleware\HideDefaultLocaleInUrl::class,
    ]);

    $middleware->web(append: [
        'locale.set',
        'locale.session',
        'locale.cookie',
    ]);
})

The urls_ignored patterns make SetLocale skip the API paths automatically, so API responses are never locale-redirected and always resolve their own locale (e.g., from an Accept-Language header or token).

Translation Export & Inspect Commands

Export translations

Scans your codebase for translatable strings and writes them to JSON files:

# Export for all supported locales
php artisan export:translations

# Export for specific locale(s)
php artisan export:translations en
php artisan export:translations en,fr,es

Features:

  • Reads scan_helpers and scan_paths from config
  • Respects file_patterns and excluded_directories for fine-grained scanning control
  • Creates/updates {locale}.json files in lang/
  • Default locale values are set to the key itself; other locales receive the default locale's value as a placeholder (so there's always something to display)
  • Respects sort_keys for alphabetical ordering
  • When sort_keys is false, uses translated_sort_order ('alpha', 'top', or 'bottom')
  • Merges strings from manual-strings.json when add_manual_strings is true
  • Skips strings containing newlines when allow_newlines is false

Inspect translations

Shows a summary table of all keys and their completion status. A key is marked as pending (?) if its value in a non-default locale is identical to the default locale's value, meaning it hasn't been translated yet. Missing keys are also treated as pending.

php artisan inspect:translations

Output:

Translation Keys Summary
--------------------------------------------------------------------------------
Key               EN              ES              FR              Status
--------------------------------------------------------------------------------
Welcome           Welcome         Bienvenido      Bienvenue       ?
About             About                                       ?

Translation File Structure

The package uses Laravel's JSON translation files:

lang/
+-- en.json          # Default locale
+-- es.json          # Spanish
+-- fr.json          # French
+-- {locale}/
    +-- routes.php   # Route string translations

JSON files

Standard Laravel JSON translations. Keys are the original strings, values are translations:

{
    "Welcome": "Bienvenido",
    "About": "Acerca de"
}

Route translations

The {locale}/routes.php file maps route strings for translated URLs:

// lang/es/routes.php
return [
    'about' => 'acerca-de',
    'contact' => 'contacto',
];

Used by RouteStringTranslator and accessible via FullLocalization::getTranslatedRoute().

Manual strings

For dynamic phrases that can't be scanned (e.g., constructed at runtime), add them to lang/manual-strings.json:

[
    "Hello :name",
    "You have :count items"
]

These are automatically merged into translation files when add_manual_strings is true.

Testing

composer test

Changelog

Please see CHANGELOG.md for more information on recent changes.

License

MIT