Skip to content
This repository has been archived by the owner on Jan 30, 2020. It is now read-only.

Translation parameters support #104

Open
thexpand opened this issue Sep 21, 2018 · 6 comments
Open

Translation parameters support #104

thexpand opened this issue Sep 21, 2018 · 6 comments

Comments

@thexpand
Copy link
Contributor

thexpand commented Sep 21, 2018

The Idea

Recently we talked on Slack about having parameters support for translated strings.
I made a research of how other frameworks are already doing it.

Other Frameworks

Here is the documentation on how it is implemented in Symfony and in Laravel.

Symfony

I like how Symfony uses PHP's strtr here: https://github.com/symfony/translation/blob/master/Formatter/MessageFormatter.php#L42-L45
However, I don't like the usage with those percent % characters as they make the string less readable, e.g. You scored %scored_points% points and you pass the exam with %score%% success!.

Laravel

I don't like Laravel's replacement implementation as this is a fairly large amount of code for such a simple task. However, I like how they name their parameters with a leading colon :, eg. :scored_points, :score, which makes the string a little bit easier to read. The previous example would become You scored :scored_points points and you pass the exam with :score% success!

My suggested approach

It will be best to use colon-prefixed parameters (readability) and strtr, which takes into account other parameters' names, regarding of the order they are defined (in contrast to Laravel's approach to use str_replace with sorted parameters), meaning that if the parameterized string contains both :score and :scored_points, the value for former would not overwrite the latter, unless the parameter has not been passed.

Implementation

I want to do and will do a PR, but first I want to discuss with you the idea and what are the possible solutions, taking into account the current state of this component. I would like to hear your opinion and suggestions.

The current methods can be changed, thus introducing a BC Break (the feature will be released with the next major version). Currently there are 2 methods in the TranslatorInterface: translate and translatePlural, which may be modified as follows:

/**
 * Translate a message.
 *
 * @param  string $message
 * @param  array  $parameters
 * @param  string $textDomain
 * @param  string $locale
 * @return string
 */
public function translate(
    $message,
    array $parameters = [],
    $textDomain = 'default',
    $locale = null
);

/**
 * Translate a plural message.
 *
 * @param  string      $singular
 * @param  string      $plural
 * @param  int         $number
 * @param  array       $parameters
 * @param  string      $textDomain
 * @param  string|null $locale
 * @return string
 */
public function translatePlural(
    $singular,
    $plural,
    $number,
    array $parameters = [],
    $textDomain = 'default',
    $locale = null
);
@MatthiasKuehneEllerhold
Copy link
Contributor

Placeholders with "%placeholder%" are already in use in the Zend-Form component. They have the huge benefit that you can preg_search your whole HTML body for unreplaced placeholders. If you forget the value for ":scored_points" the output will use the value of ":score" twice and scramble the output.

if (preg_match_all('~%[a-z0-9-_]+%~iu', $response, $matches) > 0) {
    // oh uh!
}

Also the length of the placeholder is clearly marked, so the order of replacement is irrelevant. (Well except in erroneous (?) cases where you replace one placeholder with another, the value of parameter "%scored_points%" is "%score%"...).

":placeholder:" is in use in the zend-router component, but they are for route-parameters. "%placeholder%" in the form component is directly used for placeholder in translations.

@thexpand
Copy link
Contributor Author

@MatthiasKuehneEllerhold Erroneous cases like the one you mentioned by replacing one placeholder with another is prevented with strtr, so that won't be an issue. Consider the following example:

<?php
$message = 'You scored %score% points with a %score_percentage%% success!';

// Outputs "You scored %score_percentage% points with a 99% success!"
echo strtr($message, [
    '%score%' => '%score_percentage%',
    '%score_percentage%' => 99,
]);

@MatthiasKuehneEllerhold
Copy link
Contributor

MatthiasKuehneEllerhold commented Sep 24, 2018

Sorry if I mixed my points.

You said for using str_replace we need to sort all placeholders by length. Which strtr conveniently does for us (probably faster than via userland sort).

I meant that the order of replacement is only relevant if we're using placeholders without end markers (e. g. ":placeholder"). If we're using end markers (e. g. "%placeholder%") the order is irrelevant except in the mentioned erroneous case.

For example using str_replace without sorting:

message = 'You scored :score points with a :score_percentage% success!';
echo str_replace(
  $message, 
 [
    ':score' => 50,
    ':score_percentage' => 99,
  ]
);
// Outputs "You scored 50 points with a 50_percentage% success!"

Using either strtr would've prevented the false replacement or a length-sort on the placeholders beforehand.

But using end-markers this can't happen:

message = 'You scored %score% points with a %score_percentage%% success!';
echo str_replace(
  $message, 
 [
    '%score%' => 50,
    '%score_percentage%' => 99,
  ]
);
// Outputs "You scored 50 points with a 99% success!"

TL;DR: I think the end-markers are direly necessary.

@thexpand
Copy link
Contributor Author

thexpand commented Sep 24, 2018

Yes, I understand you now. So basically we have 2 cases here.
Clearly, if the :score_percentage parameter is omitted, but :score is present, it will do a false replacement that will result in an unexpected behavior. So, it totally makes sense to use end-markers, too. We just have to decide what the markers should look like. I'm okay with %, I just thought initially that they make the string a little bit harder to read.

Having that sorted out, the next thing to decide is whether to use the str_replace or strtr. I did not do a benchmark myself, but from what I've read str_replace is faster for longer replacement strings. However, performance here might not be an issue. The problem I see with str_replace is the one you already pointed out previously - someone might set a parameter name as the value of another parameter. Using strtr sorts out this thing and we won't have to deal with sorting parameters by length.

Next thing on the list is whether the usage will be (1) or (2).
We can clearly see that Symfony uses (2), but I don't think it is good for one particular reason - it is not poka-yoke. Personally I prefer (1), because it is poka-yoke - there is one and only one way to do it. People can't get confused. The way they would define parameters in the message will be %param% and they will pass the name of the param in the array as param. End of story - no confusion.

// 1. No marks in the parameters
$this->translate('You scored %score% points', ['score' => 100]);

// 2. Marks in the parameters
$this->translate('You scored %score% points', ['%score%' => 100]);

@MatthiasKuehneEllerhold
Copy link
Contributor

We're using internally (1) (no marks in the parameters) because this way its easier for the end user to define the parameters. And thats why we're using foreach and str_replace.
Using a bulk replace via strtr or str_replace with (2) would be much faster I reckon.

@weierophinney
Copy link
Member

This repository has been closed and moved to laminas/laminas-i18n; a new issue has been opened at laminas/laminas-i18n#7.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants