In the application we are going to use TranslatorInterface
and call its translate()
method passing message key (id), parameters to substitute in the final string, category and target locale.
interface TranslatorInterface
{
public function translate(
string $id,
array $parameters = [],
string $category = null,
string $locale = null
): string;
}
It is expected that we get final string that we can output to end user from this call:
public function actionTest(TranslatorInterface $translator)
{
$translated = $translator->translate('salary.paid', ['amount' => 1000], 'ui', 'en_US');
}
For the following translation:
return [
'message.new.articles' => 'Salary paid: {amount}.'
];
It will give you “Salary paid: 1000.”.
Under the hood
What happens when we call TranslatorInterface::translate()
? Translator is expected to obtain
the message given its key (id), category and target locale, and then format
it by replacing placeholders in the message with values from params.
In order to achieve greater flexibility in terms of message storage formats and formatting, we introduce two interfaces:
-
MessageReaderInterface
that helps getting a message given id, category and target locale:interface MessageReaderInterface { public function getMessage(string $id, string $category, string $locale): string; }
-
MessageFormatterInterface
that formats the message given parameters:interface MessageFormatterInterface { public function format(string $message, array $parameters, string $locale): string; }
Translator is configured using combinations of these per category source:
class Translator implements TranslatorInterface
{
public function addCategorySource($category, MessageReaderInterface $reader, MessageFormatterInterface $formatter): self
{
// ...
}
}
Overall it looks like the following:
-
Translator::translate('salary.paid', ['amount' => 1000], 'ui', 'en_US');
MessageReaderInterface::getMessage('salary.paid', 'ui', 'en_US');
MessageFormatterInterface::format('Salary paid: {amount}.', ['amount' => 1000], 'en_US');
-
return 'Salary paid: 1000.';
.
Possible message reader implementations are:
- PHP file returning array of id => message.
- JSON where keys are ids and values are messages.
- Database.
Possible message formatters are:
- Simple formatter that just replaces
{placeholder}
with the corresponding parameter value. - Powerful Intl-based formatter like it was in Yii 2 that supports plurals etc.
Translation extractor
Similar to Yii 2, we are going to have a command line too that will get through source code, extract message keys and
write these into a translation resource merging with existing messages (if any). For these purpose, additional to MessageReaderInterface
we’ve introduced MessageWriterInterface
:
interface MessageWriterInterface
{
public function write(array $messages): void;
}
Recommending usage of ids instead of full messages
We are going to recommend using IDs or message keys instead of full messages like it was in Yii 2:
$translator->translate('salary.paid', ['amount' => 1000], 'ui', 'en_US');
// instead of
$translated = $translator->translate('Salary paid: {amount}.', ['amount' => 1000], 'ui', 'en_US');
That would allow:
- Not to care about source language being English (it’s not recommended to use non-English since it’s harder to find translator who’s able to handle both your less common source language and less common language to translate to).
- To switch to another formatter without touching application code. That would require to adjust translations though.
Note that it would be still possible to use full strings as keys since when no translation string is found, we
pass message key to formatter.
Gettext compatibility problem
GNU gettext is a popular way to handle string translation with good tools such as
Poedit. Ideally, we’d like to allow using it but there is a problem with plurals.
Gettext, unlike intl, handles plurals at the message storage level, not at formatting level. It has separate keys for
a singular form and any of plural forms and this fact is reflected in its usage.
Messages with no plurals are obtained with gettext($id)
while messages with plurals are obtained with
a dedicated method ngettext($idSingular, $idPlural, $n)
i.e. message id is selected based on the value of $n
.
With intl formatter it does not make sense to store messages in such way and to have separate method for getting plurals. It is different:
'There {catsNumber,plural,=0{are no cats} =1{is one cat} other{are # cats}}!'
- Both strings for n=1 and n>1 are in a single message.
-
$n
could be named differently. In our message it iscatsNumber
. - There could be multiple plurals in a single message so multiple
$n
values.
Considering all that there’s a problem. API that makes sense for Gettext doesn’t make sense when formatter is intl-based
i.e. for PHP files + intl formatter, DB + intl formatter etc.
Possible solutions:
- Do not implement gettext.
- Do not use native gettext plurals, use only singular strings and format these using intl.
Both aren’t ideal and we need help about deciding how to handle that.
Packages
-
translator
- interfaces. -
message-*
- message sources. -
formatter-*
- message formatters.
Other considerations
Likely there’s no need for separate MessageReaderInterface
and MessageWriterInterface
. These could be merged into MessageSourceInterface
.