Search code examples
symfonytranslationsymfony-3.4variable-length-array

Does the symfony translation system support an arbitrary-length list of elements to be concatenated?


I need to translate variable-length sentences in Symfony 3.4.

Sample sentence: "Included: 4 breakfasts, 1 brunch and 3 dinners."

For the exemple let's say the following constraints:

  1. I have to express the number of breakfasts, brunches, lunches and dinners that a travel may have in a full itinerary
  2. The list of "things" to enumerate is known and finite. There is singular and plural translation for each of the key-words.
  3. Not all of them have to appear. If there are 0 units, the full block will be omitted.
  4. The list is comma-separated.
  5. Except the last block which is added with an "and" connector.
  6. If there's only 1 block there's no comma and no "and" connector.
  7. Each can be singular/plural.
  8. There are multiple languages, for this example I'll use English and Spanish, but in the reality there is also catalan.

The keywords are the following in english-singular / english-plural / spanish-singular / spanish-plural:

breakfast / breakfasts / desayuno / desayunos
brunch / brunches / alumerzo-desayuno / almuerzo-desayunos
lunch / lunches / almuerzo / almuerzos
dinner / dinners / cena / cenas

So... I can imagine the following tests cases:

[
    'language' => 'en-US',
    'parameters' => [
        'breakfast' => 4,
        'dinner' => 1,
    ],
    'expectedOutput' => 'Included: 4 breakfasts and 1 dinner.'
],
[
    'language' => 'es-ES',
    'parameters' => [
        'breakfast' => 2,
        'brunch' => 1,
        'lunch' => 5,
        'dinner' => 2,
    ],
    'expectedOutput' => 'Incluido: 2 desayunos, 1 desayuno-almuerzo, 5 almuerzos y 2 cenas.'
],
[
    'language' => 'en-US',
    'parameters' => [
        'breakfast' => 1,
    ],
    'expectedOutput' => 'Included: 1 breakfast.'
],
[
    'language' => 'es-ES',
    'parameters' => [
        'dinner' => 4,
        'lunch' => 3,
    ],
    'expectedOutput' => 'Incluido: 3 almuerzos y 4 cenas.'
],

Questions

  • Is this supported by the native translation engine in Symfony 3.4?
  • If not, is this supported by the native translation engines of Symfony 4 or 5?
  • Otherwise, is there any known pattern or strategy that fits this case?

Solution

  • What the translation engine can do to some extent is use a number to switch between different cases (for singular/plural). Have a look at transChoice for symfony 3.4 (til 4.1.x apparently). Since symfony 4.2, the ICU message format (follow links too) may be used to handle pluralization.

    However, (conditional) string building is not really a task for the translation engine at all (even though for simple cases transChoice or the ICU message format might work out fine). So you'll probably end up building the strings from bits and pieces.

    Building the string in php

    // fetch translations for the itemss
    $items = [];
    foreach($parameters as $key => $count) {
        if($count <= 0) {
            continue;
        } 
    
        $items[] = $translator->transChoice($key, $count);
    }
    
    // handle 2+ items differently from 1 item
    if(count($items) > 1) {
        // get last item
        $last = array_pop($items);
        // comma-separate all other items, add last item with "and"
        $itemstring = implode(', ', $items)
            . $translator->trans('included_and') 
            . $last; 
    } else {
        $itemstring = $items[0] ?? ''; // if no items or something like 
                                       // $translator->trans('included_none')
    }
    
    return $translator->trans('included', ['%items%' => $itemstring]);
    

    and in the translation file (english):

    'included' => 'Included: %items%.',
    'included_and' => ' and ',
    'breakfast' => '%count% breakfast|%count% breakfasts', // etc. for all other stuff
    

    Please note, that transChoice should automatically set the %count% placeholder to the count provided as the second parameter to transChoice.

    alternate approaches

    You could easily split the translatable strings into breakfast_singular and breakfast_plural if you wanted to avoid the use of transChoice, you would have to replace

    transChoice($key, $count)
    

    with

    trans($key.($count > 1 ? '_plural' : '_singular'), ['%count%' => $count])
    

    You could also expand the included string into included_single and included_multiple where the latter is being translated as 'Included: %items% and %last%', with appropriate parameters and conditionals.

    Ultimately though, string generation or more specifically language generation isn't really a job for a simple translation engine.