Laravel's localization features provide a convenient way to retrieve strings in various languages, allowing to easily support multiple languages within the application.
At this point I'm using the function trans() and related that ships with Laravel, to translate (localize) the user interface (output) of my application, as one would usually do with a default setup. As how this strategy works, we are translating english keys into another language. It is clear that keys need not to be in any particular language, and they don't even need to be readable text, that we just keep them readable for sake of software maintainability and comply a standard, since the programmatic keywords are in english.
My question is if there's a well known way to accurately translate user input in a selected language back to the base language (let's say english).
An example of this would be letting the user to write certain keywords or expressions in their language as input, and get them into a base language.
Currently, I would do something like this:
// Dummy Sample Code I
$foreignInput = 'lunes';
$translatedToBase = trans("input.days.$foreignInput");
dd($translatedToBase); // "monday"
// lang/es/input.php
[
// ...
'days' => [
'lunes' => 'monday',
// ...
],
// ...
]
I would also do something like this:
// Dummy Sample Code II
$foreignInput = 'lun';
$translatedToBase = trans("input.days.$foreignInput");
dd($translatedToBase); // "mon"
// lang/es/input.php
[
// ...
'days' => [
'lun' => 'mon',
// ...
],
// ...
]
This would perfectly work for the purpose, however, it feels hacky (unsafe, little reliable, hard to maintain and counter-intuitive) and I wonder if there's a package of better way to achieve this. I could even require not using trans(), that's fine for me.
I understand there may be lots of options, so suggestions are welcome as long as I can keep some constrains:
Also please note that in this example I used a weekday name, but I'd not limit the solution to a datetime context.
Plus, I'm seeking to rely on the translation to be strict, this means that I'm expecting it to be deterministic, reason I'm putting aside a live translation external service.
Thanks for reading!
First of all, I wouldn't structure the language files the way you suggested. It's a subversion of Laravel's organisation to have shifting keys that have the same value throughout translations, and they would have to be specifically populated for reverse translation because they'd be too hard to use for normal translation. I'd much rather stick to the normal Laravel structure:
// lang/en/input.php
[
// ...
'days' => [
'monday' => 'Monday',
// ...
],
// ...
];
// lang/es/input.php
[
// ...
'days' => [
'monday' => 'lunes',
// ...
],
// ...
]
// lang/de/input.php
[
// ...
'days' => [
'monday' => 'Montag',
// ...
],
// ...
]
Here's an object capable of processing the reverse translation:
<?php
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
class ReverseTranslator
{
protected $languageGroups = [];
protected function loadGroups($locale)
{
// Iterate through all available groups and store
$languagePath = resource_path("lang/{$locale}/");
$languageGroups = array_map(function ($file) use ($languagePath) {
return str_replace([$languagePath, '.php'], '', $file);
}, File::glob($languagePath.'*.php'));
$this->languageGroups[$locale] = $languageGroups;
return $languageGroups;
}
protected function lowercase($value)
{
return is_array($value) ? array_map([$this, 'lowercase'], $value) : Str::lower($value);
}
protected function arraySearchRecursive($search, array $array, $mode = 'value', $return = false) {
$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
foreach ($iterator as $key => $value) {
if ($search == $value) {
$keys = [];
for ($i = $iterator->getDepth()-1; $i >= 0; $i--) {
$keys[] = $iterator->getSubIterator($i)->key();
}
$keys[] = $key;
return implode('.', $keys);
}
}
return false;
}
protected function search($foreign, $group, $locale = 'en')
{
// Load all strings for group in current language
$groupStrings = trans($group);
// Recursive and case-insensitive search
return $this->arraySearchRecursive($this->lowercase($foreign), $this->lowercase($groupStrings));
}
public function get($foreign, $group = null, $locale = 'en')
{
if (!$group) {
if (!isset($this->languageGroups[$locale])) {
$this->loadGroups($locale);
}
foreach ($this->languageGroups[$locale] as $group) {
$key = $this->search($foreign, $group, $locale);
if ($key !== false) {
return trans("{$group}.{$key}", [], null, $locale);
}
}
// Failed to match -- return original string
return $foreign;
}
$key = $this->search($foreign, $group, $locale);
if ($key !== false) {
return trans("{$group}.{$key}", [], null, $locale);
}
// Failed to match -- return original string
return $foreign;
}
}
Put this on your app/Providers/AppServiceProvider.php inside the register method:
$this->app->bind('reverse-translator', function () {
return new ReverseTranslator();
});
Then you could build a Facade to access your object more literally.
class ReverseTranslator extends \Illuminate\Support\Facades\Facade
{
/**
* Get the registered name of the component.
*
* #return string
*/
protected static function getFacadeAccessor()
{
return 'reverse-translator';
}
}
Now you can write your helper like so (using the Facade, not the object itself):
function rtrans($foreign, $group = null, $locale = 'en') {
return ReverseTranslator::get($foreign, $group, $locale);
}
Then usage would simple:
rtrans('lunes'); // "Monday"
rtrans('lunes', 'input'); // "Monday", quicker
rtrans('lunes', 'input.days'); // "Monday", quickest
rtrans('lunes', 'other'); // "lunes", because of wrong group
rtrans('lunes', null, 'de'); // "Montag"
Note:
I've made the search case-insensitive to be a little easier to use, and because case is relevant and should be considered in translation arrays (see "Monday" on lang/en/input.php).
Obviously it's much more efficient to know which file you want to load and search, so if you know your translating weekdays, you'd pass 'days' as your group. But if you don't know, then you'd have to iterate through all available language files, which could become costly depending on how often you do it and how many files you have.
Maybe the ideal solution would be to extend the default Laravel translator to give it "reverse translation" capabilities, instead of using a stand-alone object like above, but for the sake of clarity it has been isolated.
I hope this is a good starting point. Buena suerte!
Related
I have the following class method for creating a Twig environment object.
public function getView($filename,
array $customFunctions = null,
array $customFunctionArgs = null,
$debug = false) {
$loader = new \Twig_Loader_Filesystem('/App/Views/Templates/Main');
$twig = new \Twig_Environment($loader);
if (isset($customFunctions)) {
foreach ($customFunctions as $customFunction) {
$customFunction['name'] = new \Twig_SimpleFunction($customFunction['name'],
function ($customFunctionArgs) {
return $customFunction['method']($customFunctionArgs);
});
$twig->addFunction($customFunction['name']);
}
}
// Check debugging option
if ($debug == true && !$twig->isDebug()) {
$twig->enableDebug();
$twig->addExtension(new \Twig_Extension_Debug());
} elseif (!$debug && $twig->isDebug()) {
$twig->disableDebug();
}
$template = $twig->load($filename);
return $template;
}
Problem is, I don't understand how to pass values in order to make this work dynamically and keep all the objects in context and scope. For instance, here is how I'm trying to use it but can't pass the variables as a reference I guess?
$customFunctions = ['name' => 'customFunctionName',
'method' => $Class->method($arg)];
$customFunctionArgs = [$arg];
$template = $View->getView('template.html.twig', $customFunctions, $customFunctionArgs, true);
My environment is PHP 5.6 & Twig 1.35.0. I suppose this is not a Twig specific question per se, but more of how to use class objects within other classes/methods.
FĂ©lix Gagnon-Grenier's answer helped me find a solution to this problem. However, I feel the need to post an answer with all the missing pieces to the puzzle for anyone that needs a solution for this.
I believe it will make more sense if I start at the end and explain to the beginning. When creating your array, there are several things to consider.
Any class objects that are needed for the function have to be declared inside a use() with the closure.
Any arguments for the custom function must be declared as a function parameter for the closure. This will allow you to declare them later.
I ended up adding a sub-array with the arguments I needed for each custom function, that way I don't need to iterate over them separately.
$customFunctions = [
[
'name' => 'customFunction',
'method' => function($arg1, $arg2) use($Class) {
return $Class->customFunction($arg1, $arg2);
},
'arguments' =>
[
'arg1', 'arg2'
]
]
];
$template = $View->getView(
'template.html.twig',
true,
$customFunctions
);
echo $View->renderView($template);
Based on this code (reflective of question above), I had to make some notable modifications.
if (isset($customFunctions)) {
foreach ($customFunctions as $index => $customFunction) {
if (isset($customFunctions['arguments'])) {
$arguments = $customFunctions['arguments'];
} else {
$arguments = [];
}
$twigFunction = new \Twig_SimpleFunction(
$customFunction['name'],
function (...$arguments) use ($customFunction) {
return $customFunction['method'](...$arguments);
});
$twig->addFunction($twigFunction);
}
}
You can do this whatever way works for you, but there are important things to consider which I struggled with. Once again, your arguments MUST go into the function parameters. function (...$arguments) use ($customFunction). Your custom function will be passed in the use(). In order to actually pass the arguments in the closure, you must use ... to unpack them (as an array). This applies to PHP 5.6+. It allows the arguments to be dynamically expanded to the correct amount, otherwise you will get missing argument errors.
There are slight flaws in how you construct the custom functions data array and the loop that injects them into the template.
The custom functions should be a three dimensional array
$customFunctions = [
[ // notice the extra level, allowing you to access the name
'name' => 'customFunctionName',
'method' => function() { return 'wat'; }
// you need to pass a callable, not the result of a call
]
];
The scope is not inherited like you seem to think it is, you need to use() variables you intend to access. I personnally would not overwrite the 'name' value of the array, but that's uncanny paranoia of internal side effects, it seems to work in practice.
if (isset($customFunctions)) {
foreach ($customFunctions as $customFunction) {
$customFunction['name'] = new \Twig_SimpleFunction(
$customFunction['name'],
function () use ($customFunctionArgs, $customFunction) {
return $customFunction['method']($customFunctionArgs);
});
$twig->addFunction($customFunction['name']);
}
}
You might need to add looping over $args so that the correct args are sent to the correct function (send $args[0] to $customFunctions[0] etc.).
Note that this prevents you from sending a parameter into your custom function unless you add it in the loop:
function ($templateArg) use ($customFunctionArgs, $customFunction) {
return $customFunction['method']($customFunctionArgs, $templateArg);
}
Here is a gist with tests if you're interested.
From very long time i am working on php.
But one question may I have no idea about
like I have one function as bellow:
function hello($param1, $param2="2", $param3="3", $param4="4")
Now whenever I will use this function and if I need 4th params thats the $param4 then still I need to call all as blank like this one:
hello(1, '', '', "param4");
So is there any another way to just pass 1st and 4th param in call rather then long list of blanks ?
Or is there any other standard way for this ?
There was an RFC for this named skipparams but it was declined.
PHP has no syntactic sugar such as hello(1, , , "param4"); nor hello(1, default, default, "param4"); (per the RFC) for skipping optional parameters when calling a function.
If this is your own function then you can choose the common jQuery style of passing options into plug-ins like this:
function hello( $param1, $more_params = [] )
{
static $default_params = [
'param2' => '2',
'param3' => '3',
'param4' => '4'
];
$more_params = array_merge( $default_params, $more_params );
}
Now you can:
hello( 1, [ 'param4'=>'not 4, muahaha!' ] );
If your function requires some advanced stuff such as type hinting then instead of array_merge() you will need to manually loop $more_params and enforce the types.
One potential way you can do this, while a little bit hacky, may work well in some situations.
Instead of passing multiple variables, pass a single array variable, and inside the function check if the specific keys exist.
function hello($param1, $variables = ["param2" => "2", "param3" => "3", "param4" => "4"]) {
if(!array_key_exists("param2", $variables)) $variables['param2'] = "2";
if(!array_key_exists("param3", $variables)) $variables['param3'] = "3";
if(!array_key_exists("param4", $variables)) $variables['param4'] = "4";
echo "<pre>".print_r($variables, true)."</pre>";
}
This will allow you to set "param4" in the above variable, while still remaining default on all of the others.
Calling the function this way:
hello("test", ["param4" => "filling in variable 4"]);
Will result in the output being:
Array
(
[param4] => filling in variable 4
[param2] => 2
[param3] => 3
)
I don't generally recommend this if it can be avoided, but if you absolutely need this functionality, this may work for you.
The key here is that you have a specifically named index inside the array being passed, that you can check against inside the function itself.
The answer, as I see it, is yes and no.
No, because there's no way to do this in a standard fashion.
Yes, because you can hack around it. This is hacky, but it works ;)
Example:
function some_call($parm1, $parm2='', $parm3='', $parm4='') { ... }
and the sauce:
function some_call_4($parm1, $parm4) {
return some_call($parm1, '', '', $parm4);
}
So if you make that call ALOT and are tired of typing it out, you can just hack around it.
Sorry, that's all I've got for you.
It is an overhead, but you can use ReflectionFunction to create a class, instance of which that can be invoked with named parameters:
final class FunctionWithNamedParams
{
private $func;
public function __construct($func)
{
$this->func = $func;
}
public function __invoke($params = [])
{
return ($this->func)(...$this->resolveParams($params));
}
private function resolveParams($params)
{
$rf = new ReflectionFunction($this->func);
return array_reduce(
$rf->getParameters(),
function ($carry, $param) use ($params) {
if (isset($params[$param->getName()])) {
$carry[] = $params[$param->getName()];
} else if ($param->isDefaultValueAvailable()) {
$carry[] = $param->getDefaultValue();
} else {
throw new BadFunctionCallException;
}
return $carry;
},
[]
);
}
}
Then you can use it like this:
function hello($param1, $param2 = "2", $param3 = "3", $param4 = "4")
{
var_dump($param1, $param2, $param3, $param4);
}
$func = new FunctionWithNamedParams('hello');
$func(['param1' => '1', 'param4' => 'foo']);
Here is the demo.
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.
I have lots of PHP classes like this:
class Article
{
public function fetchAll($params) {
if ($params["client"] == "mobile") {
return [
"author" => "",
"body" => ""
];
}
return [
"author" => "",
"body" => "",
"publishedOn => """
];
}
}
As you see fetchAll() method returning different fields according to $params["client"].
I need to check existence of these fields. I have 3 options to achieve my goal.
(Note: All codes simplified, dont worry about syntax errors.)
Method 1 : Different test methods for different clients
class ArticleTest extends ...
{
public function testFetchAll() {
$params = [];
$data = $article->fetchAll($params);
$this->assertEqual(array_keys($data), ["author","body","publishedOn"]);
}
public function testFetchAll2() {
$params = ["client" => "mobile"];
$data = $article->fetchAll($params);
$this->assertEqual(array_keys($data), ["author","body"]);
}
}
Cons: Using same codes again and again, it's a really bad practice. No need to explain. Refactoring is really difficult. Also this method will cause dozens of test methods.
Method 2 : Using for-loop for different clients
class ArticleTest extends ...
{
public function testFetchAll() {
for ($i=0; $i<2; $i++) {
$params = [];
if ($i == 1) $params["client"] = "mobile";
$data = $article->fetchAll($params);
if ($i == 0)
$this->assertEqual(array_keys($data), ["author","body","publishedOn"]);
else
$this->assertEqual(array_keys($data), ["author","body"]);
}
}
}
Cons : I'm not sure if using loops on PHPUnit tests is a good idea. Also code readability decreased.
Method 3 : Different test cases / test files for different clients
// articleTest.php
class ArticleTest extends ...
{
public function testFetchAll() {
$params = [];
$data = $article->fetchAll($params);
$this->assertEqual(array_keys($data), ["author","body","publishedOn"]);
}
}
// articleTest2.php
class ArticleTest2 extends ...
{
public function testFetchAll() {
$params = ["client" => "mobile"];
$data = $article->fetchAll($params);
$this->assertEqual(array_keys($data), ["author","body"]);
}
}
Cons: This method causing dozens of test files, and using same codes again and again. Refactoring is really difficult.
Performance Comparison
Method 1 : Time: 127 ms, Memory: 8.00Mb
Method 2 : Time: 125 ms, Memory: 8.00Mb
Method 3 : Time: 96 ms, Memory: 8.00Mb
I'm trying to understand which method is better with lots of classes like this and waiting for a discussion about optimizing unit tests in this situation.
EDIT: Even if your question focus on optimization, there is actually a broadly accepted best practice for your case so that's what I have shown in my answer.
Just use dataProviders. With those you can execute the same test, passing arguments to the test method. The arguments can be variations on the params or / and the expected result.
So you could do this:
public function fetchAllTestData() {
return [
// case 1
[
[], // params
['author', 'body', 'publishedon'] // expectedKeys
],
// case 2
[
['client' => 'mobile'], // params
['author', 'body'] // expectedKeys
],
];
}
/**
* #dataProvider fetchAllTestData
*/
public function testFetchAll(array $params, array $expectedKeys) {
$data = $article->fetchAll($params);
$this->assertEqual($expectedKeys, array_keys($data));
}
This way the test will be executed independently for each dataProvider row. This means that if the first case fails, the second will be tested so you will know if it also fails or not. If you use loops inside the test case, whenever there is a failure, the rest of the cases won't be tested.
Take a look at dataProviders documentation.
NOTE When writing tests please pay attention to the assert methods parameters ordering. Usually the expected data comes first. If you pass them in reversed order the error messages will be misleading and the arrays and objects comparison will tell that there are missing rows when in fact the rows should not be there.
I'm currently working on an OO PHP application. I have a class called validation which I would like to use to check all of the data submitted is valid, however I obviously need somewhere to define the rules for each property to be checked. At the moment, I'm using arrays during the construction of a new object. eg:
$this->name = array(
'maxlength' => 10,
'minlength' => 2,
'required' => true,
'value' => $namefromparameter
)
One array for each property.
I would then call a static method from the validation class which would carry out various checks depending on the values defined in each array.
Is there a more efficient way of doing this?
Any advice appreciated.
Thanks.
I know the associative array is used commonly to configure things in PHP (it's called magic container pattern and is considered bad practice, btw), but why don't you create multiple validator classes instead, each of which able to handle one rule? Something like this:
interface IValidator {
public function validate($value);
}
$validators[] = new StringLengthValidator(2, 10);
$validators[] = new NotNollValidator();
$validators[] = new UsernameDoesNotExistValidator();
This has multiple advantages over the implementation using arrays:
You can document them (very important), phpdoc cannot parse comments for array keys.
Your code becomes typo-safe (array('reqiured' => true))
It is fully OO and does not introduce new concepts
It is more readable (although much more verbose)
The implementation of each constraint can be found intuitively (it's not in a 400-line function, but in the proper class)
EDIT: Here is a link to an answer I gave to a different question, but that is mostly applicable to this one as well.
Since using OO it would be cleaner if you used classes for validating properties. E.g.
class StringProperty
{
public $maxLength;
public $minlength;
public $required;
public $value;
function __construct($value,$maxLength,$minLength,$required)
{
$this->value = $value;
$this-> maxLength = $maxLength;
$this-> minLength = $minLength;
$this-> required = $required;
}
function isValidat()
{
// Check if it is valid
}
function getValidationErrorMessage()
{
}
}
$this->name = new StringProperty($namefromparameter,10,2,true);
if(!$this->name->isValid())
{
$validationMessage = $this->name-getValidationErrorMessage();
}
Using a class has the advantage of encapsulating logic inside of it that the array (basically a structure) does not have.
Maybe get inspired by Zend-Framework Validation.
So define a master:
class BaseValidator {
protected $msgs = array();
protected $params = array();
abstract function isValid($value);
public function __CONSTRUCT($_params) {
$this->params = $_params;
}
public function getMessages() {
// returns errors-messages
return $this->msgs;
}
}
And then build your custom validators:
class EmailValidator extends BaseValidator {
public function isValid($val=null) {
// if no value set use the params['value']
if ($val==null) {
$val = $this->params['value'];
}
// validate the value
if (strlen($val) < $this->params['maxlength']) {
$this->msgs[] = 'Length too short';
}
return count($this->msgs) > 0 ? false : true;
}
}
Finally your inital array could become something like:
$this->name = new EmailValidator(
array(
'maxlength' => 10,
'minlength' => 2,
'required' => true,
'value' => $namefromparameter,
),
),
);
validation could then be done like this:
if ($this->name->isValid()) {
echo 'everything fine';
} else {
echo 'Error: '.implode('<br/>', $this->name->getMessages());
}