By default, Laravel’s translation system only supports loading translation files from the top-level lang directory. It does not support loading translations from subfolders.
But what happens as your application grows? You'll likely want to organize your translation files into nested folders for better maintainability and structure.
I know Laravel supports namespaced translation loading, but that’s not always the ideal solution — especially when you're not working with packages or want to keep things simple and consistent across your app.
In this guide, I’ll show you how to extend Laravel’s core translation system to support nested folders.
To customize Laravel’s translation behavior, we first need to understand how it works under the hood.
Laravel uses two main classes to load translations:
What we care about here is the FileLoader class. This is responsible for loading translation files from the filesystem.
Fortunately, Laravel is built to be extensible, so we can override and extend this functionality to support nested translation directories.
If you trace Laravel’s codebase, you’ll notice that the FileLoader is registered as a singleton in the container. This means we can override it by binding our own implementation.
Rather than modifying the core TranslationServiceProvider, we’ll create a custom service provider that extends Laravel's default and swaps in our custom loader.
Here’s how it looks:
<?php
namespace App\Support\Libraries\Translations\Providers;
use App\Support\Libraries\Translations\NestedFolderTranslationLoader;
use Illuminate\Translation\TranslationServiceProvider;
/**
* Enables loading translation files from nested folders for better organization.
*/
class FolderTranslationServiceProvider extends TranslationServiceProvider
{
/**
* Register the translation line loader.
*/
protected function registerLoader(): void
{
$this->app->singleton('translation.loader', function ($app) {
return new NestedFolderTranslationLoader($app['files'], [
__DIR__ . '/lang',
$app['path.lang'],
]);
});
}
}
In the code above, we create a custom service provider called FolderTranslationServiceProvider by extending Laravel’s default TranslationServiceProvider.
The key part is the registerLoader() method, where we override the default singleton binding for 'translation.loader' in Laravel’s service container. Instead of using the built-in FileLoader, we bind our own NestedFolderTranslationLoader.
This allows us to control how translation files are loaded — and in our case, support nested folders. The loader is initialized with two paths:
__DIR__ . '/lang': A custom path (optional or for testing).
$app['path.lang']: Laravel’s default lang directory.
By doing this, Laravel will now use our custom logic when loading translation files throughout the app.
Next, register your new provider in bootstrap/providers.php (or in config/app.php if you’re still using the default structure).
This will replace the default FileLoader with your NestedFolderTranslationLoader, enabling support for loading translation files from nested directories.
before we head to step 3, we need to know more about FileLoader class and which part we need to focus on it.
When digging into Laravel’s FileLoader, the method we care most about is loadPaths(). This method is responsible for loading PHP-based translation files — not JSON translations.
here is the original code of this method:
/**
* Load a locale from a given path.
*
* @param array $paths
* @param string $locale
* @param string $group
* @return array
*/
protected function loadPaths(array $paths, $locale, $group)
{
return (new Collection($paths))
->reduce(function ($output, $path) use ($locale, $group) {
if ($this->files->exists($full = "{$path}/{$locale}/{$group}.php")) {
$output = array_replace_recursive($output, $this->files->getRequire($full));
}
return $output;
}, []);
}
What it does in short:
This works great for flat structures, but completely ignores files inside subfolders — and that’s exactly what we aim to support in this post.
To enable nested folder support for PHP-based translation files, we override Laravel’s FileLoader by extending it in a new class: NestedFolderTranslationLoader.
This class replaces the default logic for loading translation groups. Laravel by default looks for files like lang/en/auth.php, but with our change, it can now scan folders like lang/en/auth/ and automatically merge everything inside — including deeply nested files.
first thing we are going to do is to extract the original logic into custom function so we call it later
protected function loadSingleFile($path, &$output,?callable $callback = null): void
{
if ($this->files->exists($path)) {
$result = $this->files->getRequire($path);
if(is_callable($callback)){
$result = $callback($result);
}
$output = array_replace_recursive($output, $result);
}
}
This method converts a translation file’s nested path into a Laravel-style dot notation key. It removes the base folder path, strips the .php extension, and replaces directory separators with dots — so a file like lang/en/dashboard/users/index.php becomes dashboard.users.index. This allows merging nested files into the final translation array with properly namespaced keys.
From the original loadPaths() method, we can see that it uses the $paths array and reduces it into a single merged array of translations. We'll follow the same approach in our custom loader. However, before returning the result, we add a check: if the resolved path points to a directory, we scan that folder for .php files. For each valid PHP file found, we load it as usual — but with its proper nested namespace derived from its location within the folder. This allows us to support organized, nested translation structures seamlessly.
Now in our code, in our custom loadPaths function override we will have this portion of code inside the reduce callback:
if($this->files->exists($path) && $this->files->isDirectory($path)) {
$allFiles = [
...glob($path . '/*.php'),
...glob($path . '/**/*.php'),
];
foreach ($allFiles as $file) {
$this->loadSingleFile($file, $output, function($result) use($path, $file) {
$trans_path = $this->filePathToTranslationPath($file, $path);
return collect($result)->mapWithKeys(function($result, $key) use($trans_path){
return ["$trans_path.$key" => $result];
})->undot()->toArray();
});
}
}
This block handles the core logic that enables loading nested translation files.
First, it checks if the $path points to an existing directory. If it is, we gather all .php files within that folder, including subdirectories, using glob with both single and recursive patterns.
Each file is then loaded using loadSingleFile(), and we pass a callback to it. This callback rewrites the loaded array’s keys based on the file’s relative nested path — effectively namespacing the keys according to the folder structure.
For example, if a file is located at lang/en/auth/passwords/reset.php, its keys will be prefixed as auth.passwords.reset.* rather than just flattening everything.
We use mapWithKeys() in the callback to prefix each key with its computed dot-notation path, then apply undot() to re-expand it into a nested array structure. This keeps the final translation array both namespaced and properly nested, matching Laravel's expected format — while also supporting deep folder structures cleanly.
/**
* Load a locale from a given path.
*
* @param array $paths
* @param string $locale
* @param string $group
* @return array
*/
protected function loadPaths(array $paths, $locale, $group): array
{
return collect($paths)
->reduce(function ($output, $path) use ($locale, $group) {
$path = "$path/$locale/$group";
if($this->files->exists($path) && $this->files->isDirectory($path)) {
$allFiles = [
...glob($path . '/*.php'),
...glob($path . '/**/*.php'),
];
foreach ($allFiles as $file) {
$this->loadSingleFile($file, $output, function($result) use($path, $file) {
$trans_path = $this->filePathToTranslationPath($file, $path);
return collect($result)->mapWithKeys(function($result, $key) use($trans_path){
return ["$trans_path.$key" => $result];
})->undot()->toArray();
});
}
}
$this->loadSingleFile("$path.php", $output);
return $output;
}, []);
}
And that’s it — with this setup, Laravel can now load translation files from nested folders seamlessly. You can verify it’s working by creating a deeply nested structure under your lang directory (e.g., lang/en/dashboard/users.php) and referencing the translations using dot notation like __('dashboard.users.key').
Here is the full code snippet of the class:
<?php
/**
* @noinspection PhpUnhandledExceptionInspection
* @noinspection PhpDocMissingThrowsInspection
*/
namespace App\Support\Libraries\Translations;
use Illuminate\Support\Str;
use Illuminate\Translation\FileLoader;
class NestedFolderTranslationLoader extends FileLoader
{
/**
* Load a locale from a given path.
*
* @param array $paths
* @param string $locale
* @param string $group
* @return array
*/
protected function loadPaths(array $paths, $locale, $group): array
{
return collect($paths)
->reduce(function ($output, $path) use ($locale, $group) {
$path = "$path/$locale/$group";
if($this->files->exists($path) && $this->files->isDirectory($path)) {
$allFiles = [
...glob($path . '/*.php'),
...glob($path . '/**/*.php'),
];
foreach ($allFiles as $file) {
$this->loadSingleFile($file, $output, function($result) use($path, $file) {
$trans_path = $this->filePathToTranslationPath($file, $path);
return collect($result)->mapWithKeys(function($result, $key) use($trans_path){
return ["$trans_path.$key" => $result];
})->undot()->toArray();
});
}
}
$this->loadSingleFile("$path.php", $output);
return $output;
}, []);
}
protected function filePathToTranslationPath($file, $path): string
{
$sub_path = trim(str_replace($path , '', $file), '/');
$ext = pathinfo($sub_path, PATHINFO_EXTENSION);
return Str::of($sub_path)
->replaceLast(".$ext", '')
->replace(['/', '\\'], '.', $sub_path)
->replace('..', '.')
->toString();
}
protected function loadSingleFile($path, &$output,?callable $callback = null): void
{
if ($this->files->exists($path)) {
$result = $this->files->getRequire($path);
if(is_callable($callback)){
$result = $callback($result);
}
$output = array_replace_recursive($output, $result);
}
}
}
With this custom loader in place, Laravel now fully supports nested translation folders. Your language files can be organized more naturally, making your app’s localization easier to manage and scale. This simple extension enhances Laravel’s translation system without complicating your codebase.
Senior Full Stack Web Developer with expertise in building SaaS and modern web applications. Specialized in PHP/Laravel and React/Vue ecosystems