pmochine/laravel-tongue

πŸŽ‰ Finally a subdomain localization that works how you want it to work. 🌐
58,398 41
Install
composer require pmochine/laravel-tongue
Latest Version:6.1.0
PHP:^8.2
License:MIT
Last Updated:Oct 7, 2026
Links: GitHub  ·  Packagist
Maintainer: pmochine

Laravel Tongue πŸ‘… - Multilingual subdomain URLs and redirects

tests styleci

Packagist Packagist Packagist

Laravel Tongue

If you are looking for an easy package for subdomain multilingual URLs, this package is for you. 😜

Old Way: https://example.com/de, https://example.com/fr etc. New Way: https://de.example.com, https://fr.example.com etc.

Requirements

Laravel Tongue Laravel PHP
6.x 11, 12, 13 8.2 or higher (Laravel 13 needs 8.3 or higher)
5.x 8.41 to 10 7.4, 8.1 or higher

For older Laravel versions, read Older Laravel versions. If you upgrade from 5.x, read the upgrade guide.

Installation in 4 Steps

1: Add with composer πŸ’»

  composer require pmochine/laravel-tongue

2: Publish Configuration File (you need to change some things to use it 😎)

  php artisan vendor:publish --provider="Pmochine\LaravelTongue\ServiceProvider" --tag="config"

3: The Middleware 🌐

Laravel Tongue comes with a middleware that can be used to enforce the use of a language subdomain. For example the user calls example.com it goes directly to fr.example.com.

The package registers the middleware alias speaks-tongue for you. You do not need to add it yourself.

If your app defines the alias itself, the package keeps your definition. Since Laravel 11 you do this in bootstrap/app.php:

  use Illuminate\Foundation\Configuration\Middleware;

  ->withMiddleware(function (Middleware $middleware) {
      $middleware->alias([
          'speaks-tongue' => \Pmochine\LaravelTongue\Middleware\TongueSpeaksLocale::class,
      ]);
  })

An app that still has app/Http/Kernel.php can add the same line to the $middlewareAliases array.

4: Add in your Env πŸ”‘

  APP_DOMAIN=yourdomain.com #Only important for domains with many dots like: '155ad73e.eu.ngrok.io'
  SESSION_DOMAIN=.yourdomain.com #Read down below why

Important! Note the dot before the domain name. Now the session is available in every subdomain πŸ™ƒ. This is important because you want to save all your cookie πŸͺ data in one place and not in many other.

Laravel finds the service provider of the package with package auto-discovery. You do not need to register it.

Usage - (or to make it runnable πŸƒβ€β™‚οΈ)

Locale detection πŸ”

Open app/Providers/AppServiceProvider.php and add this to the boot() method:

  public function boot(): void
  {
      // This will guess a locale from the current HTTP request
      // and set the application locale.
      tongue()->detect();
  }

Carbon uses the new locale automatically. Its Laravel service provider listens to the LocaleUpdated event.

Laravel loads your routes after the boot() method of the AppServiceProvider. So tongue()->detect() sets the locale before dialect()->interpret() translates your routes.

If your app still has app/Providers/RouteServiceProvider.php, you can call tongue()->detect() at the start of its boot() method instead.

Once you have done this, there is nothing more that you MUST do. Laravel application locale has been set and you can use other locale-dependent Laravel components (e.g. Translation) as you normally do.

Middleware 🌐

If you want to enforce the use of a language subdomain for some routes, you can simply assign the middleware provided, for example as follows in routes/web.php:

  // Without the localize middleware, this route can be reached with or without language subdomain
  Route::get('logout', 'AuthController@logout');
  
  // With the localize middleware, this route cannot be reached without language subdomain
  Route::group([ 'middleware' => [ 'speaks-tongue' ]], function() {
  
      Route::get('welcome', 'WelcomeController@index');
  
  });

For more information about Middleware, please refer to Laravel docs.

Laravel Octane

Octane boots your app one time for many requests. A service provider does not see each request. So do not call tongue()->detect() in a service provider. Use the middleware TongueDetectsLocale of the package instead. It detects the locale for each request. Add it before all other middleware:

  // bootstrap/app.php
  ->withMiddleware(function (Middleware $middleware) {
      $middleware->prepend(\Pmochine\LaravelTongue\Middleware\TongueDetectsLocale::class);
  })

You can also use its alias detects-tongue on a group of routes, for example for API routes without a service provider. In a group with both middleware, put detects-tongue before speaks-tongue.

Octane registers your routes one time, before the first request. So register translated routes with dialect()->localizedRoutes(). Then the routes of all locales exist, and the middleware detects the locale for each request.

Route caching

php artisan route:cache works for all routes. Register translated routes with dialect()->localizedRoutes(). Without it, the cache keeps the translated paths of one locale only.

Frontend 😴

  <!doctype html>
  <html lang="{{tongue()->current()}}" dir="{{tongue()->leftOrRight()}}">

    <head>
      @include('layouts.head')
    </head>

    <body>
    ...

The above <html> tag will always have a supported locale and directionality (β€˜ltr’ or β€˜rtl’). The latter is important for right-to-left languages like Arabic and Hebrew since the whole page layout will change for those.

Alternate links for search engines

Search engines find the other languages of a page with hreflang links. Add them to the <head> of your layout:

  @foreach (dialect()->alternates() as $hreflang => $url)
      <link rel="alternate" hreflang="{{ $hreflang }}" href="{{ $url }}">
  @endforeach

dialect()->alternates() lists each supported locale and x-default for the fallback locale, as Google asks for. Each URL is the address of the locale itself, with the query string of the current page, like ?page=2. So the fallback locale uses the beautiful URL example.com. The language switcher still links to en.example.com, because that link switches the cookie.

If a cookie or the browser language asks for another locale, the middleware still redirects a visitor from example.com. Google's crawler sends no Accept-Language header, so it sees the page of the fallback locale.

The hreflang is the locale key, with _ replaced by -. Google accepts ISO 639-1 language codes. If your locale key is no such code, set the hreflang in config/localization.php:

  'fil' => ['name' => 'Filipino', 'script' => 'Latn', 'native' => 'Filipino', 'regional' => 'fil_PH', 'hreflang' => 'tl'],

Configuration

Once you have imported the config file, you will find it at config/localization.php.

Important: Before you start changing the values, you still need to set the "main language" of your page. If your main language is fr, please add this to your config/app.php file under 'fallback_locale' => 'fr',.

We asume that your fallback language has always translated pages. We get the current locale via four ways:

  1. First we determine the local with the subdomain of the URL the user is coming from

If there is no subdomain added, we get the locale from:

  1. an already set language cookie
  2. or the browsers prefered language
  3. or at the end we fall back to the fallback_locale

Note: The value locale in config/app.php has no impact. tongue()->detect() overwrites it.

Configuration values

  • domain (default: null)

You don't need to worry about this, only when you are using domains with multiple dots, like: 155ad73e.eu.ngrok.io. Without it, we cannot check what your subdomain is.

  • beautify_url (default: true)

Makes the URL BEAUTIFUL πŸ’β€β™€οΈ. ( Use to set fallback language to mydomain.com and not to en.mydomain.com). That is why I even created this package. I just could not find this! 😭

  • subdomains (default: [])

Sometimes you would like to have your admin panel as a subdomain URL. Here you can whitelist those subdomains (only important if those URLs are using the middleware).

  • aliases (default: []) Sometimes you would like to specify aliases to use custom subdomains instead of locale codes. For example:
  gewinnen.domain.com --> "de"
  gagner.domain.com --> "fr",
  • alias_urls (default: false)

By default, the aliases only work for incoming requests. The URLs that Tongue builds still use the locale, like de.domain.com. If you set alias_urls to true, these URLs use the first alias of the locale, like gewinnen.domain.com. The middleware then redirects de.domain.com and the other aliases of the locale to gewinnen.domain.com.

The beautiful URL of the fallback locale (beautify_url) wins over its alias. Tongue ignores an alias that is also a locale code or a whitelisted subdomain.

  • acceptLanguage (default: true)

Use this option to enable or disable the use of the browser πŸ’» settings during locale detection.

  • cookie_localization (default: true)

Use this option to enable or disable the use of cookies πŸͺ during the locale detection.

  • cookie_serialize (default: false)

If you have not changed anything in your middleware "EncryptCookies", you don't need to change anything here as well. More

  • prevent_redirect (default: false)

Important for debugging, when you want to deactivate the middleware speaks-tongue.

  • supportedLocales (default: πŸ‡¬πŸ‡§πŸ‡©πŸ‡ͺπŸ‡ͺπŸ‡ΈπŸ‡«πŸ‡·πŸ‡­πŸ‡Ί)

Don't say anyone that I copied it from mcamara 🀫

Route translation

If you want to use translated routes (en.yourdomain.com/welcome, fr.yourdomain.com/bienvenue), proceed as follows:

First, create language files for the languages that you support. Since Laravel 9 the language files are in the lang folder of your app. If you do not have this folder, run php artisan lang:publish.

lang/en/routes.php:

  return [
    
    // route name => route translation
    'welcome' => 'welcome',
    'user_profile' => 'user/{username}',
  
  ];

lang/fr/routes.php:

  return [
    
    // route name => route translation
    'welcome' => 'bienvenue',
    'user_profile' => 'utilisateur/{username}',
    
  ];

Then, here is how you define translated routes in routes/web.php:

  Route::group([ 'middleware' => [ 'speaks-tongue' ]], function() {
    
      Route::get(dialect()->interpret('routes.welcome'), 'WelcomeController@index');
    
  });

You can, of course, name the language files as you wish, and pass the proper prefix (routes. in the example) to the interpret() method.

Translated routes for route caching and Octane

The routes above get the paths of the locale of the current request. Laravel registers them for each request. With php artisan route:cache or Laravel Octane, Laravel registers the routes only one time. Then put the translated routes into dialect()->localizedRoutes():

  Route::middleware(['detects-tongue', 'speaks-tongue'])->group(function () {
      dialect()->localizedRoutes(function () {
          Route::get(dialect()->interpret('routes.welcome'), [WelcomeController::class, 'index'])->name('welcome');
          Route::get(dialect()->interpret('routes.user_profile'), [UserController::class, 'show'])->name('user_profile');
      });
  });

Tongue runs the callback one time for each supported locale. In each round, dialect()->interpret() gives the path of that locale, also in Route::prefix(). So en.example.com/welcome and fr.example.com/bienvenue are two routes, and the middleware detects-tongue sets the locale for each request.

  • The callback must register the same routes in the same order for each locale.
  • The route of the fallback locale keeps its name, like welcome. Route names must be unique for the route cache, so the routes of the other locales get the locale as suffix, like welcome.fr. To check the current route, use request()->routeIs('welcome', 'welcome.*'). If another route already has a name like welcome.fr, Tongue throws a LogicException.
  • If several locales have the same path, they share one route. Two different routes can not have the same path in different locales, because Laravel finds a route by its path. Then Tongue throws a LogicException.
  • dialect()->translate('welcome', [], 'fr'), dialect()->current(), dialect()->translateUrl() and dialect()->alternates() use the route of the locale. route('welcome') gives the path of the fallback locale.
  • If a request uses the path of another locale, like fr.example.com/welcome, the middleware speaks-tongue redirects to fr.example.com/bienvenue. A translated slug in the URL gets the slug of the locale. A form request keeps its method and data, because the redirect has the status 307.
  • Tongue runs detects-tongue and speaks-tongue before the route model binding of Laravel. So the binding finds a translated slug in the right locale.

Translated slugs

A model can have a different slug in each language, like en.example.com/article/important-change and de.example.com/artikel/wichtige-aenderung. Then implement LocalizedUrlRoutable in the model:

  use Pmochine\LaravelTongue\Contracts\LocalizedUrlRoutable;

  class Article extends Model implements LocalizedUrlRoutable
  {
      // The slug of the article in the given locale
      public function getLocalizedRouteKey(string $locale)
      {
          return $this->getTranslation('slug', $locale); // for example with spatie/laravel-translatable
      }

      // Route model binding finds the article by the slug of the current locale
      public function resolveRouteBinding($value, $field = null)
      {
          return static::where('slug->'.app()->getLocale(), $value)->firstOrFail();
      }
  }

dialect()->current(), dialect()->translateAll() and dialect()->translate() then use the slug of the target locale. If getLocalizedRouteKey() returns null for a locale, the URL keeps the current slug. This also works for routes without translated paths. The parameter must use route model binding, like Route::get('article/{article}', ...) with an Article $article argument in the controller.

Helper Functions - (finally something useful 😎)

This package provides useful helper functions that you can use - for example - in your views:

Translate your current URL into the given language

  <a href="{{ dialect()->current('fr') }}">See the french version</a>

Get all translated URL except the current URL

  @foreach (dialect()->translateAll(true) as $locale => $url)
      <a href="https://github.com/pmochine/Laravel-Tongue/blob/HEAD/{{ $url }}">{{ $locale }}</a>
  @endforeach

You can pass false as parameter so it won't exclude the current URL.

Translate URL to the language you want

  <a href="{{ dialect()->translate('routes.user_profile', [ 'username' => 'JohnDoe' ], 'fr') }}">See JohnDoe's profile</a>
  // Result: https://fr.example.com/utilisateur/JohnDoe 

Remember: Set the translation in the lang folder

Use dialect()->translate($routeName, $routeAttributes = null, $locale = null) to generate an alternate version of the given route. This will return an URL with the proper subdomain and also translate the URI if necessary.

$routeName can be a translation key, like routes.user_profile, or the name of a route, like home:

  Route::get('/', HomeController::class)->name('home');

  dialect()->translate('home', [], 'fr'); // https://fr.example.com

If the route got its path from dialect()->interpret(), you get the translated path of the locale. For a route without translation, Laravel builds the path like route(). Attributes that are not in the path become the query string.

If the route has no translation, dialect()->current() keeps the path of the current page.

You can pass route parameters if necessary. If you don't give a specific locale, it will use the current locale ☺️.

Translate any URL of your app

  dialect()->translateUrl('https://de.example.com/hallo/john?tab=2', 'fr');
  // Result: https://fr.example.com/bonjour/john?tab=2

Use dialect()->translateUrl($url, $locale = null) for a URL instead of a route, for example the URL of the previous page. Tongue finds the route of the URL like the router does, also for a URL in another locale than the current page. A translated route gets the translated path, and a translated slug gets the slug of the locale. The query string stays. If no route matches, only the subdomain changes.

A relative URL is relative to the app, like in url(). In an app under https://example.com/shop, use hallo/john and not /shop/hallo/john.

Redirect URL to the language you want

  <a href="{{ dialect()->redirectUrl(route('home'), 'fr') }}">See Homepage in French</a>
  // Result: https://fr.example.com 

Use dialect()->redirectUrl($url = null, $locale = null); to redirect for example to the same URL but in different locale. Warning: Works only when the paths are not translated. Use dialect()->translate() for that.

Get your config supported locale list

  $collection = tongue()->speaking(); //returns collection

Remember it returns a collection. You can add methods to it (see available methods) Examples:

  $keys = tongue()->speaking()->keys()->all(); //['en','de',..]
  $sorted = tongue()->speaking()->sort()->all(); //['de','en',..]

Additionally, you can even get some addtional information:

  tongue()->speaking('BCP47', 'en'); // en-GB
  tongue()->speaking('subdomains'); // ['admin']
  tongue()->speaking('subdomains', 'admin'); // true
  tongue()->speaking('aliases'); // ['gewinnen' => 'de', 'gagner' => 'fr]
  tongue()->speaking('aliases', 'gewinnen'); //' de'

Get the current language that is set

  $locale = tongue()->current(); //de

Or if you like you can get the full name, the alphabet script, the native name of the language & the regional code.

  $name = tongue()->current('name'); //German
  $script = tongue()->current('script'); //Latn
  $native = tongue()->current('native'); //Deutsch
  $regional = tongue()->current('regional'); //de_DE

How to Switch Up the Language πŸ‡¬πŸ‡§->πŸ‡©πŸ‡ͺ

For example with a selector:

  <ul>
      @foreach(tongue()->speaking()->all() as $localeCode => $properties)
          <li>
              <a rel="alternate" hreflang="{{ $localeCode }}" href="{{ dialect()->current($localeCode) }}">
                  {{ $properties['native'] }}
              </a>
          </li>
      @endforeach
  </ul>

Or in a controller far far away...

  /**
   * Sets the locale in the app
   * @return redirect to previous url
   */
  public function store()
  {
    $locale = request()->validate([
      'locale' => ['required', Rule::in(tongue()->speaking()->keys()->all())],
    ])['locale'];

    return tongue()->speaks($locale)->back();
  } 

back() redirects to the previous page in the new locale. If the previous page belongs to a translated route, it also gets the translated path, for example de.example.com/hallo/john becomes example.com/hello/john.

Upgrade Guide 🎒

Upgrade to 6.x.x from 5.x.x

Version 6 supports Laravel 11, 12 and 13. It needs PHP 8.2 or higher.

  1. Make sure that your app runs on Laravel 11 or higher and PHP 8.2 or higher.

  2. Update the package:

      composer require pmochine/laravel-tongue:^6.0
    
  3. You can delete your own speaks-tongue alias from app/Http/Kernel.php or bootstrap/app.php. The package registers it now. If you keep your alias, the package uses it.

  4. If your app has no app/Providers/RouteServiceProvider.php anymore, move tongue()->detect() to the boot() method of app/Providers/AppServiceProvider.php.

  5. Optional: add the new key 'alias_urls' => false, to your published config/localization.php. If the key is missing, the package uses false.

These changes can affect your app:

  • dialect()->translate() and dialect()->current() now also find a route by its name. Before, a route name that was no translation key kept the path of the current page.
  • tongue(), dialect(), the facades and app('tongue') now return the same instance. Before, each call of a helper built a new instance.
  • dialect()->translate() and dialect()->current() encode route attributes in translated paths, like route() of Laravel. A space becomes %20.
  • Aliases are case-insensitive now, like hosts.
  • The home page URL has no trailing slash: https://fr.example.com instead of https://fr.example.com/.
  • If you extend Tongue or Dialect: the constructors take no arguments, and Dialect has no $app property.
  • tongue()->back() translates the path of the previous page for translated routes. Before, it only changed the subdomain.

Upgrade to 2.x.x from 1.x.x

There are little changes that might be important for you.

  • We added two new config elements in localization. domain and aliases. Add these like here.
  • Add APP_DOMAIN in your .env if you have a complicated domain, like: 155ad73e.eu.ngrok.io
  • Now you are able to use aliases in your subdomain. For example: gewinnen.domain.com --> "de"
  • If a subdomain is invalid, it returns to the latest valid locale subdomain.

Older Laravel versions

Support for Laravel 8.41 up to Laravel 10

If you want to use:

PHP ^7.4 or ^8.1 and Laravel 8.41 up to Laravel 10

you need to download the version 5.0.0. This version gets no more updates.

  composer require pmochine/laravel-tongue:^5.0

Support for Laravel 9.x.x

If you want to use:

PHP <=8.0 and Laravel 9.x.x

you need to download the version 4.0.0.

  composer require pmochine/laravel-tongue:4.0.0

Support for Laravel 7.22.0 up to Laravel 8.41.0

If you want to use:

PHP >=7.3 and at least 7.22.0 <= Laravel <=8.41.0

you need to download the version 3.0.0.

  composer require pmochine/laravel-tongue:3.0.0

Support for Laravel 6.x.x up to Laravel 7.21.0

If you want to use:

PHP >=7.2 and at least 6.x.x <= Laravel <=7.21.0

you need to download the version 2.2.1 or lower.

  composer require pmochine/laravel-tongue:2.2.1

Support for Laravel 5.x.x

If you want to use:

PHP >=7.0 and at least 5.4 <= Laravel <=5.8

you need to download the version 2.0.0 or lower.

  composer require pmochine/laravel-tongue:2.0.0

Security

If you discover any security related issues, please don't email me. I'm afraid 😱. avidofood@protonmail.com

Credits

Now comes the best part! 😍 This package is based on

Oh come on. You read everything?? If you liked it so far, hit the ⭐️ button to give me a 🀩 face.

Related Packages

craft-forge/filament-language-switcher

Zero-config language switcher for Filament admin panels. Automatically scans ava...

105,129 11
longman/laravel-multilang

Package to integrate multi language (multi locale) functionality in Laravel 5.x

14,726 54
douyasi/locator

Browser Preferred Language Detector

6,858 4
mominalzaraa/filament-localization

The first and only automatic Filament localization package with intelligent reso...

5,622 11

Version History

Version Released PHP Laravel License
6.1.0 ^8.2 ^11.0 || ^12.0 || ^13.0 MIT
6.0.0 ^8.2 ^11.0 || ^12.0 || ^13.0 MIT
5.0.0 ^7.4 || ^8.1 ^8.41||^9.0||^10.0 MIT
4.0.0 ^7.4 || ^8.0 ^8.41||^9.0 MIT
3.1.0 ^7.4 || ^8.0 ^8.41 MIT