Laravel Localization Nested JSON Language File
Asked Answered
L

6

5

I'm trying to make my project multi-language.

I want to work with JSON files since my project has lots of pages,so a lot of strings to translate.

In order to avoid confusion I want to use nested JSON objects instead of just keys.

For example my en.json file looking like this:

{
    "login": {
        "login": "Login",
        "email": "Email",
        "emailPlaceholder": "Registered email address",
        "password": "Password",
        "passwordPlaceHolder": "Your account password"
    },

   "dashboard": {}
}

So I want to have a key for each page.

But when I use it on the view files, it reads just as a regular string:

<label for="email">
    {{ __('login.email') }}
</label>

enter image description here

Any help would be nice, thank you very much.

Lowndes answered 1/2, 2021 at 8:11 Comment(2)
Based on this comment in the translation method, nested translations in json are not supported, you should use a PHP array if you want to use nested translations.Shelton
Read Here: laravel.com/docs/8.x/localization. Laravel uses arrays instead of JSON. You are free to do nesting also.Cock
O
11

Try:

<label for="email">
    {{ __('login')['email'] }}
</label>
Oblation answered 17/12, 2021 at 17:28 Comment(0)
R
3

You can get this approach by changing the default __ helper by:

use Illuminate\Support\Arr; 

function __($key = null, $replace = [], $locale = null)
{
    // Default behavior
    if (is_null($key)) return $key;        
    if (trans()->has($key)) return trans($key, $replace, $locale);

    // Search in .json file
    $search = Arr::get(trans()->get('*'), $key);
    if ($search !== null) return $search;

    // Return .json fallback
    $fallback = Arr::get(trans()->get('*', [], config('app.fallback_locale')), $key);
    if ($fallback !== null) return $fallback;

    // Return key name if not found
    else return $key;
}

Creating custom bootstrap helpers

if you dont know how to change the default helper, create a file with any name (ex: bootstrap/helpers.php), then in the file public/index.php add this line just before 'Register The Auto Loader'

/*
|--------------------------------------------------------------------------
| Register Custom Helpers
|------------------------------------------------------------------------
*/

require __DIR__.'/../bootstrap/helpers.php';

(Optional) Variables feature

if you also want to use the Variables feature, just like __(welcome.user, ['user'=>'david']) you must create a new helper on that file:

use Illuminate\Support\Str;

function trans_replacements($line, array $replace)
{
    if (empty($replace)) return $line;

    $shouldReplace = [];

    foreach ($replace as $key => $value) {
        $shouldReplace[':'.Str::ucfirst($key)] = Str::ucfirst($value);
        $shouldReplace[':'.Str::upper($key)] = Str::upper($value);
        $shouldReplace[':'.$key] = $value;
    }

    return strtr($line, $shouldReplace);
}

and then replace return $search with trans_replacements($search, $replace) and return $fallback with trans_replacements($fallback, $replace)

(Optional) Countable feature

for Countable feature (ex: 'an apple|many apples'), is the same process, just add this helper:

use Illuminate\Support\Facades\App;

function trans_choice($key, $number, array $replace = [], $locale = null)
{        
    // Get message
    $message = __($key, $replace, $locale);

    // If the given "number" is actually an array or countable we will simply count the
    // number of elements in an instance.
    if (is_array($number) || $number instanceof Countable) 
    $number = count($number);

    $replace['count'] = $number;

    return trans_replacements(
        trans()->getSelector()->choose($message, $number, $locale = App::getLocale()), 
        $replace
    );
}

Bootstrap Helpers File

here is the file, just in case: bendeckdavid/laravel_locale_nested_json

Ratoon answered 31/1, 2022 at 9:45 Comment(0)
G
0

Laravel not supporting this by default is annoying. However, you can use this function I made:

function ___($path) {
        $properties = explode(".", $path);
        $base = __($properties[0]);
        unset($properties[0]);
        foreach($properties as $property) {
            // Allows specification of indexes
            if(is_numeric($property)) {
                $property = intval($property);
            }
            // If the key has not been found, return the initial parameter like __()
            try {
                $base = $base[$property];
            } catch (\Throwable $th) {
                return $path;
            }
        }
        return $base;
}
Gratulant answered 9/1, 2022 at 19:57 Comment(0)
U
0

If you are someone like me stumbling upon here wanting to use a file structure like below without the need to specify keys for each string when using php language files.

-app
-lang
  --fr
    ---Headers.json

A little modification to David G's answer above,

<?php

use Illuminate\Support\Arr; 

function ___($path = null, $replace = [], $locale = null)
{
    // Default behavior
    if (is_null($path)) return $path;
    if(!strrpos($path, '.')) {
        return  $path;
    }

    $properties = explode(".", $path);
    $currentLocale = app()->getLocale();    

    // If the file/folder doesn't exist 
    // return the last part of the dot notation
    $translated_string = substr($path, strrpos($path, '.') + 1);

    if(is_dir(lang_path($currentLocale))) {
        if (is_file(lang_path($currentLocale.'/'.$properties[0].'.json'))) {
            $contents = json_decode(file_get_contents(lang_path($currentLocale.'/'.$properties[0].'.json')), true);
            // If no value exists for the key, the key is itself returned
            $translated_string = Arr::get($contents, $properties[1], $properties[1]);
        }
    }

    return $translated_string;
}

As of now this doesn't work for files under subdirectories under the locale folder. I am sure, the above code can be optimized, but this should give you an idea.

Also, this function uses triple-underscore {{___()}} instead of the regular double-underscore {{__()}} to avoid conflicts.

Works for,

{{___('Messages.Welcome') }} // lang/(locale)/Messages.json
{{___('Welcome')}} // lang/(locale).json
Umlaut answered 20/7, 2022 at 6:1 Comment(0)
E
0

Extend Laravel's core binding: Instead of modifying index.php and artisan to require custom functions, you can extend Laravel's binding of the translator alias.

Create a Translator class under app/Translation/Translator.php:

namespace App\Translation;

use Illuminate\Support\Arr;
use Illuminate\Translation\Translator as BaseTranslator;

class Translator extends BaseTranslator
{
    /**
     * @param $key
     * @param array $replace
     * @param $locale
     * @param $fallback
     *
     * @return array|string|null
     */
    public function get($key, array $replace = [], $locale = null, $fallback = true)
    {
        $results = parent::get($key, $replace, $locale, $fallback);

        // If the key does not contain nested translation
        // or the result did not return the key back, then it's okay
        if (!str_contains($key, '.') || $results !== $key) {
            if (is_array($results)) {
                return $key;
            }

            return $results;
        }

        $locale = $locale ?: $this->locale;
        $line = Arr::get($this->loaded['*']['*'][$locale], $key);

        // Handle fallback to default language
        if (!isset($line) && $fallback && !empty($this->getFallback()) && $locale !== $this->getFallback()) {
            $this->load('*', '*', $this->getFallback());
            $line = Arr::get($this->loaded['*']['*'][$this->getFallback()], $key);
        }

        return $this->makeReplacements($line ?: $key, $replace);
    }
}

Bind your class in app/Providers/AppServiceProvider.php:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Translation\Translator;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->extend('translator', function ($service, $app) {
            $translator = new Translator($service->getLoader(), $service->getLocale());
            $translator->setFallback($service->getFallback());

            return $translator;
        });
    }
}

Now, you can simply use nested paths in translations:

<label for="email">
    {{ __('login.email') }}
</label>
Emerald answered 7/4, 2024 at 11:27 Comment(0)
M
-4

Assuming your json is stored in $json, you want to parse it into array by using json_decode

$data = json_decode($json, TRUE);

You can access it by:

<label for="email">
    {{ $data['login']['emailPlaceholder'] }}
</label>
Marrano answered 1/2, 2021 at 8:19 Comment(1)
Nothing to do with the question asked except it shares the word 'JSON'.Escape

© 2022 - 2025 — McMap. All rights reserved.