Laravel Localization Nested JSON Language File - php

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>
Any help would be nice, thank you very much.

Try:
<label for="email">
{{ __('login')['email'] }}
</label>

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

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;
}

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

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>

Related

PHP/Laravel Check a condition in a function only once

I have written a custom translation function for Laravel, which first checks a configuration variable to see whether to use Laravel's default __() function or my custom trans() function.
Here's my function:
function t($key, $replace = [], $locale = null)
{
$source = Config::get('translate.source');
if ($source == 'database') {
return trans($key, $replace, $locale);
} else {
return __($key, $replace, $locale);
}
}
However, for speed purposes, I don't want the if condition to run reach time I call the t() function, but only on the first call.
Any ideas?
You want to set a variable in the class and refer to that. So, create a variable databaseSource:
private $databaseSource = false;
You then want a method that will change the boolean:
function updateSource()
{
$source = Config::get('translate.source');
if ($source == 'database') {
$this->databaseSource = true;
}
$this->databaseSource = false;
}
You can then use this piece of functionality over and over via accessing the variable instead of getting the source every time you need it.
E.g. if ($databaseSource) { ... }

parse string as object having keys [duplicate]

There are many questions similar to this, however this is slightly different since it's about deep object property access, not just one level of depth.
Let's say I have a variable containing the string foo.bar.
$user = new User();
$user->foo = new Foo();
$user->foo->bar = "Hello World";
$variable = "foo.bar"
I would like to echo $user->foo->bar by making use of $variable:
echo $user->foo->bar
This is what I have tried so far but with no success (it says NULL):
$value = str_replace(".", "->", $value);
echo $user->{$value};
It is very easy to reduce the object path using variable property notation ($o->$p):
$path = 'foo.bar';
echo array_reduce(explode('.', $path), function ($o, $p) { return $o->$p; }, $user);
This could easily be turned into a small helper function.
A little improvement added to #deceze post.
This allow handling cases where you need to go through arrays also.
$path = 'foo.bar.songs.0.title';
echo array_reduce(explode('.', $path), function ($o, $p) { return is_numeric($p) ? $o[$p] : $o->$p; }, $user);
Edit:
And if you have PHP 7+, then the following will safely return null if a property's name is mistyped or if it doesn't exist.
$path = 'foo.bar.songs.0FOOBAR.title';
echo array_reduce(explode('.', $path), function ($o, $p) { return is_numeric($p) ? ($o[$p] ?? null) : ($o->$p ?? null); }, $user);
There is no easy way to do it.
Fortunately though, lots of people want to do this, so there's libraries that support it, like Symfony's PropertyAccessor:
http://symfony.com/doc/current/components/property_access.html
I am posting this as a compliment to an answer (How to write getter/setter to access multi-level array by key names?) that does the same for arrays.
Create the $path array via explode() (or add to the function), then use references.
$path = explode('.', $variable);
Getter
function get($path, $object) {
$temp = &$object;
foreach($path as $var) {
$temp =& $temp->$var;
}
return $temp;
}
$value = get($path, $user);
And of course the evil eval(), not recommended:
$value = str_replace('.', '->', $variable);
eval("echo \$user->$value;");
I have written a recursive algorithm for finding all the values ​​of properties.
public function findAttributeFromJson($json,$attributeFind,$assignAttribute)
{
if(!is_array($json)) return $assignAttribute;
$properties = array_keys($json);
foreach ($properties as $value) {
if($value === $attributeFind)
{
$assignAttribute[count($assignAttribute)] = $json[$value];
}
$assignAttribute = $this->findAttributeFromJson($json[$value],$attributeFind,$assignAttribute);
}
return $assignAttribute;
}
And use it
$arrResult = array();
$arrResult = $this->findAttributeFromJson($arrFind,$properties,$arrResult );

Access objects by placing arrow operator in a string [duplicate]

There are many questions similar to this, however this is slightly different since it's about deep object property access, not just one level of depth.
Let's say I have a variable containing the string foo.bar.
$user = new User();
$user->foo = new Foo();
$user->foo->bar = "Hello World";
$variable = "foo.bar"
I would like to echo $user->foo->bar by making use of $variable:
echo $user->foo->bar
This is what I have tried so far but with no success (it says NULL):
$value = str_replace(".", "->", $value);
echo $user->{$value};
It is very easy to reduce the object path using variable property notation ($o->$p):
$path = 'foo.bar';
echo array_reduce(explode('.', $path), function ($o, $p) { return $o->$p; }, $user);
This could easily be turned into a small helper function.
A little improvement added to #deceze post.
This allow handling cases where you need to go through arrays also.
$path = 'foo.bar.songs.0.title';
echo array_reduce(explode('.', $path), function ($o, $p) { return is_numeric($p) ? $o[$p] : $o->$p; }, $user);
Edit:
And if you have PHP 7+, then the following will safely return null if a property's name is mistyped or if it doesn't exist.
$path = 'foo.bar.songs.0FOOBAR.title';
echo array_reduce(explode('.', $path), function ($o, $p) { return is_numeric($p) ? ($o[$p] ?? null) : ($o->$p ?? null); }, $user);
There is no easy way to do it.
Fortunately though, lots of people want to do this, so there's libraries that support it, like Symfony's PropertyAccessor:
http://symfony.com/doc/current/components/property_access.html
I am posting this as a compliment to an answer (How to write getter/setter to access multi-level array by key names?) that does the same for arrays.
Create the $path array via explode() (or add to the function), then use references.
$path = explode('.', $variable);
Getter
function get($path, $object) {
$temp = &$object;
foreach($path as $var) {
$temp =& $temp->$var;
}
return $temp;
}
$value = get($path, $user);
And of course the evil eval(), not recommended:
$value = str_replace('.', '->', $variable);
eval("echo \$user->$value;");
I have written a recursive algorithm for finding all the values ​​of properties.
public function findAttributeFromJson($json,$attributeFind,$assignAttribute)
{
if(!is_array($json)) return $assignAttribute;
$properties = array_keys($json);
foreach ($properties as $value) {
if($value === $attributeFind)
{
$assignAttribute[count($assignAttribute)] = $json[$value];
}
$assignAttribute = $this->findAttributeFromJson($json[$value],$attributeFind,$assignAttribute);
}
return $assignAttribute;
}
And use it
$arrResult = array();
$arrResult = $this->findAttributeFromJson($arrFind,$properties,$arrResult );

Laravel 5: Why can't I use integers and floating point numbers in the language files?

Suppose I have this language file resources/lang/en/settings.php in a Laravel 5 project I'm working on. And this file looks like this:
<?php
return [
"key_1" => 50,
"key_2" => "50",
];
Now if I wanted to get the value of key_1 like this:
return trans("settings.key_1"); // returns "settings.key_1"
This will return settings.key_1 which is not 50, the value I expect. On the other hand, if I tried to get the value of key_2 which is also 50 but this time as a string, it will return 50 as expected.
return trans("settings.key_2"); // returns 50
So, Why can't I use numbers in the language files, Why the values must be strings?
From the sourcecode:
Lets start at the trans function that you are calling.
/**
* Get the translation for a given key.
*/
public function trans($id, array $parameters = [], $domain = 'messages', $locale = null)
{
return $this->get($id, $parameters, $locale);
}
The get function called by $this->get()
/**
* Get the translation for the given key.
*/
public function get($key, array $replace = [], $locale = null, $fallback = true)
{
list($namespace, $group, $item) = $this->parseKey($key);
// Here we will get the locale that should be used for the language line. If one
// was not passed, we will use the default locales which was given to us when
// the translator was instantiated. Then, we can load the lines and return.
$locales = $fallback ? $this->parseLocale($locale) : [$locale ?: $this->locale];
foreach ($locales as $locale) {
$this->load($namespace, $group, $locale);
$line = $this->getLine(
$namespace, $group, $locale, $item, $replace
);
if (! is_null($line)) {
break;
}
}
// If the line doesn't exist, we will return back the key which was requested as
// that will be quick to spot in the UI if language keys are wrong or missing
// from the application's language files. Otherwise we can return the line.
if (! isset($line)) {
return $key;
}
return $line;
}
As you can see here:
// If the line doesn't exist, we will return back the key which was requested as
// that will be quick to spot in the UI if language keys are wrong or missing
// from the application's language files. Otherwise we can return the line.
if (! isset($line)) {
return $key;
}
The value has not a valid value so isset is not passed therefore it will return the $key value which is the key you requested.
To go even further we can look at the following function which was called in the get function.
/**
* Retrieve a language line out the loaded array.
*/
protected function getLine($namespace, $group, $locale, $item, array $replace)
{
$line = Arr::get($this->loaded[$namespace][$group][$locale], $item);
if (is_string($line)) {
return $this->makeReplacements($line, $replace);
} elseif (is_array($line) && count($line) > 0) {
return $line;
}
}
Here we se the following:
if (is_string($line)) {
This is where the framework actualy checks if the value is a string.

php str_replace template with placeholders

I have one array for data
$data = array(title=>'some title', date=>1350498600, story=>'Some story');
I have a template
$template = "#title#, <br>#date(d)#<br> #date(m)#<br>#date(Y)#<br> #story#";
All i want is to fit data into template and i know that can be done by str_replace but my problem is the date format. date format is coming from the template not from the data, in data date is stored as php date.
yesterday i tried to ask the same question but i think my question wasn't clear.
Anybody please help me.
i think it won't work with str_replace easily so i'm going to use preg_replace
$data = array('title'=>'some title', 'date'=>1350498600, 'story'=>'Some story');
$template = "#title#, <br>#date(d)#<br> #date(m)#<br>#date(Y)#<br> #story#";
$result = preg_replace_callback('/#(\w+)(?:\\((.*?)\\))?#/', function ($match) use($data) {
$value = isset($data[$match[1]]) ? $data[$match[1]] : null;
if (!$value) {
// undefined variable in template throw exception or something ...
}
if (! empty($match[2]) && $match[1] == "date") {
$value = date($match[2], $value);
}
return $value;
}, $template);
Instead of using date(m) or date(Y) you could also do things like
date(d-m-Y) using this snippet
This has the disadvantage that you can format only the date variable using this mechanism. But with a few tweaks you can extend this functionality.
Note: If you use a PHP version below 5.3 you can't use closures but you can do the following:
function replace_callback_variables($match) {
global $data; // this is ugly
// same code as above:
$value = isset($data[$match[1]]) ? $data[$match[1]] : null;
if (!$value) {
// undefined variable in template throw exception or something ...
}
if (! empty($match[2]) && $match[1] == "date") {
$value = date($match[2], $value);
}
return $value;
}
$data = array('title'=>'some title', 'date'=>1350498600, 'story'=>'Some story');
$template = "#title#, <br>#date(d)#<br> #date(m)#<br>#date(Y)#<br> #story#";
// pass the function name as string to preg_replace_callback
$result = preg_replace_callback('/#(\w+)(?:\\((.*?)\\))?#/', 'replace_callback_variables', $template);
You can find more information about callbacks in PHP here
I'd suggest using a templating engine like so:
https://github.com/cybershade/CSCMS/blob/master/core/classes/class.template.php
And then your templates turn out like this:
https://github.com/cybershade/CSCMS/blob/master/themes/cybershade/site_header.tpl
and
https://github.com/cybershade/CSCMS/blob/master/modules/forum/views/viewIndex/default.tpl
Download this file: http://www.imleeds.com/template.class.txt
Rename the extension to .PHP from .TXT
This is something I created years ago, I keep my HTML away from my PHP, always. So see an example below.
<?php
include("template.class.php");
//Initialise the template class.
$tmpl = new template;
$name = "Richard";
$person = array("Name" => "Richard", "Domain" => "imleeds.com");
/*
On index.html, you can now use: %var.name|Default if not found% and also, extend further, %var.person.Name|Default%
*/
//Output the HTML.
echo $tmpl->run(file_get_contents("html/index.html"));
?>

Categories