Language localization plugin


1Presentation

This plugin allows two things:

  • Manage URLs that are prefixed by language. The plugin will therefore automatically redirect the user to the requested page with /en/ (for example) at the beginning of the URL.
  • Automatically load the translations corresponding to the language used, to make the translated strings available to the templates.

2Configuration

In the etc/temma.php file, add the pre-plugin and configure it:

<?php
return [
    // loading the plugin
    'plugins' => [
        '_pre' => [
            '\Temma\Plugin\\Language'
        ]
    ],
    // plugin configuration
    'x-language' => [
        // list of languages supported by the site
        'supported' => [ 'fr', 'en', 'de' ],
        // default language, if the browser is not compatible with
        // any of the languages listed above
        'default' => 'fr',
        // (optional) you can indicate not to use
        // template prefixes (see below)
        'templatePrefix' => false,
        // (optionnel) you can indicate the URLs for which
        // language management is not activated
        'protectedUrls' => [
            '/robots.txt',
            '/sitemap.xml',
        ]
    ],
    // configuration of the Smarty view
    'x-smarty-view' => [
        'pluginsDir' => '/path/to/project/lib/smarty-plugins'
    ]
];
  • Lines 4 to 8: Plugins configuration.
    • Line 6: Activation of the pre-plugin that supports language management.
  • Lines 10 to 25: Plugin configuration.
    • Line 12: List of languages supported on the site.
    • Line 15: Definition of the default language (used if the browser is not compatible with any of the languages listed.
    • Lines 21 to 24: Definition of the list of URLs for which language management is disabled.
  • Lines 27 to 29: Configuration of the Smarty view.
    • Line 28: Addition of the directory containing the Smarty plugins provided by Temma, so that the Smarty interpreter is able to find the extension (see below).

3How it works

In the example of the configuration above, we indicate that the site has translations in French, English and German.

When a browser requests a page, two cases can occur:

  • The requested URL does not start with /fr/, /en/ nor /de/. In this case, the plugin will redirect the user to the requested URL, adding the language prefix. The language chosen will be the first one supported by the browser which is also supported by the site (if none is suitable, the default language of the site will be used).
  • The requested URL begins with a language prefix. In this case, the plugin will check that the requested language is well supported by the site (if this is not the case, we are back in the previous case). If the language is well supported, the plugin will extract the language, and modify all the information relating to the controller, the action and the parameters, to pretend that this prefix did not exist in the URL. Beside that, the plugin loads the corresponding translation file, and adds a prefix to the path leading to the template files.

For example, if the user requests page /page/list/date/5, with a browser that supports French, the plugin will redirect her to /fr/page/list/date/5.

When arriving on /fr/page/list/date/5, the plugin will extract the language (fr), will load the translation file in French. Then it will modify several things:

  • The requested controller becomes page (and no longer fr).
  • The requested action becomes list (and no longer page).
  • The list of parameters becomes ['date', '5'] (and no longer ['list', 'date', '5']).

The template variables $CONTROLLER, $ACTION and $URL are also modified accordingly.


4Management of query methods

If the requested URL begins with language information (and this language is authorized by the configuration), the processing explained above will always be carried out.

On the other hand, if the language information is absent, the redirection (to the same URL to which the language prefix is added) is not carried out if the request had a POST or PUT method. In this case, the plugin does nothing and the processing continues normally.


5Protected URLs

It is possible to define a list of URLs which will not be managed by the language plugin. This is useful if, for example, you have controllers that automatically generate the content of robots.txt or sitemap.xml files.

Please note that URLs must begin with the '/' character.


6Prefix to templates path

Unless the templatePrefix configuration variable has been set to false, the plugin will modify the path leading to the template files, prepending a directory corresponding to the requested language.

Thus, if the URL is /fr/article/list, the template used will be (by default) the file templates/fr/article/list.tpl

If the controller uses another file, the prefix will be added anyway.
For example, if the controller contains the following line:

$this->_template('cms/content/article_list.tpl');

Temma will use the file templates/fr/cms/content/article_list.tpl


7Translation files

7.1Basic format

The plugin loads files containing the translations. These files must be placed in the project's etc/lang/ directory. As with the configuration file, these files can be in PHP, JSON, INI, YAML or NEON format.

The PHP format is recommended, as it benefits from OPCode caching (the file is not reread each time it is accessed). But you can choose a different format, such as INI, for simplicity's sake.

File names depend on the language of the translations they contain: fr.php, en.ini, es.json, etc.

Here's an example of an fr.php file:

<?php

return [
	'default' => [
		'Controllers' => 'Contrôleurs',
		'Model'       => 'Modèle',
		'Views'       => 'Vues',
	],
	'header' => [
		'title'     => 'Temma : framework PHP simple et performant',
		'Home'      => 'Accueil',
		'Download'  => 'Télécharger',
		'Community' => 'Communauté',
		'Examples'  => 'Exemples',
	],
	'footer' => [
		'Designed by' = > 'Conception par',
	],
];

The same thing in INI format (fr.ini file):

[default]
Controllers="Contrôleurs"
Model="Modèle"
Views="Vues"

[header]
title="Temma : framework PHP simple et performant"
Home="Accueil"
Download="Télécharger"
Community="Communauté"
Examples="Exemples"

[footer]
Designed by="Conception par"

In JSON format (fr.json file):

{
    "default": {
        "Controllers": "Contrôleurs",
        "Model":       "Modèle",
        "Views":       "Vues"
    },
    "header": {
        "title":     "Temma : framework PHP simple et performant",
        "Home":      "Accueil",
        "Download":  "Télécharger",
        "Community": "Communauté",
        "Examples":  "Exemples"
    },
    "footer": {
        "Designed by": "Conception par"
    }
}

In YAML (file fr.yaml) or NEON (file fr.neon) format:

default:
    Controllers: Contrôleurs
    Model:       Modèle
    Views:       Vues

header:
    title:     "Temma : framework PHP simple et performant"
    Home:      Accueil
    Download:  Télécharger
    Community: Communauté
    Examples:  Exemples

footer:
    Designed by: Conception par

As you can see, the translated strings are sorted by sections (here default, header and footer). These sections correspond to translation domains.

Here again, the basic strings are in English, and each one provides their translation into French.

Important: If you try to translate a string, but it cannot be found in the translation file, it will be used as is.
In the example above, we can see that the base strings are in English, and that a French translation has been provided. It will therefore not be necessary to provide an English translation file.


7.2Context management

It's possible to create several versions of a character string. Each of these versions depends on a customizable context. These contexts are used, for example, to manage masculine/feminine versions of a text.

The default context is the one defined first.

To define contexts, simply associate an associative array with the translation key.

Example file fr.php:

<?php

return [
    'default' => [
        'Actor' => [
            'male'    => 'Acteur',
            'female'  => 'Actrice',
        ],
        'Waiter' => [
            'non-binary' => 'Serveur⋅euse',
            'male'       => 'Serveur',
            'female'     => 'Serveuse',
        ],
    ],
];

And the corresponding en.php file:

<?php

return [
    'job' => [
        'Actor' => [
            'male'    => 'Actor',
            'female'  => 'Actress',
        ],
        'Waiter' => [
            'non-binary' => 'Waitstaff',
            'male'       => 'Waiter',
            'female'     => 'Waitress',
        ],
    ],
];

7.3Count management (singular/plural)

Translation files can also include different variations of the same character string, depending on the number of elements. This goes further than the simple singular/plural: it can be used when there are no elements, or a variable number of elements.

As with contexts, you need to define an associative array, whose keys are the number of elements corresponding to the declension. It is possible to have keys starting with < or <= to define intervals.

The default value is defined with the * key. If not supplied, the first value in the list will be used.

Example translation file fr.php:

<?php

return [
    'default' => [
        'there are flowers' => [
            '0'   => "il n'y a pas de fleurs",
            '1'   => 'il y a une fleur',
            '<=3' => 'il y a quelques fleurs',
            '<10' => 'il y a des fleurs',
            '*'   => 'il y a plein de fleurs',
        ],
    ],
];

And its en.php version:

<?php

return [
    'default' => [
        'there are flowers' => [
            '0'   => 'there are no flowers',
            '1'   => 'there is one flower',
            '<=3' => 'there are a few flowers',
            '<10' => 'there are some flowers',
            '*'   => 'there are lots of flowers',
        ],
    ],
];

7.4Context + count

Contexts and counts can be used together. To do so, each context must contain a countdown.

Here is an example:

<?php

return [
    'default' => [
        'there are actors' => [
            'male' => [
                '0' => "il n'y a pas d'acteurs",
                '1' => 'il y a un acteur',
                '*' => 'il y a des acteurs',
            ],
            'female' => [
                '0' => "il n'y a pas d'actrices",
                '1' => 'il y a une actrice',
                '*' => 'il y a des actrices',
            ],
        ],
    ],
];

8Use in templates

In Smarty templates, two additional variables are created by the plugin:

  • $lang: Contains the current language (fr, en, etc.).
  • $l10n: Contains the table of translations. Must not be used.

You can use the $lang variable directly in your templates:

{if $lang == 'fr'}
    Bonjour tout le monde
{else}
    Hello everyone
{/if}

Displayed in French, this will look like this:

    Bonjour tout le monde

In English:

    Hello everyone

You can also use the |l10n modifier and the {l10n}...{/l10n} block tag.


9Templates: modifier

To translate strings, you can use the l10n modifier. It looks for the translation of the string it receives as input, and returns it after escaping any special characters.

<h1>{"Model"|l10n}</h1>
<h1>{"Views"|l10n}</h1>

{"zkwx<hyq"|l10n}

Displayed in French:

<h1>Modèle</h1>
<h2>Vues</h2>

zkwx&lt;hyq

In English:

<h1>Model</h1>
<h2>Views</h2>

zkwx&lt;hyq
  • Lines 1 and 2: The strings listed in the default section of the translation file can be used directly.
  • Line 4: If the string is not found in the translation file, it is used as is. The special < character is escaped as &lt;.

9.1Modifier: domain

To specify a domain, add it before the translation string, using the hash character (#) to separate the domain from the string.

{"header#Home"|l10n}
{"header#Download"|l10n}

Displayed in French, this would give:

Accueil
Télécharger

In english:

Home
Download

9.2Modifier: context and count

The context and count can be supplied after the domain, separated by commas.

If the context is not supplied, the first one listed in the translation file is used. If the count is not supplied, the default count (*) will be used; if it doesn't exist, the first count defined will be used.

Example template:

{"default,female,1#there are actors"|l10n}
{",,3#there are actors"|l10n}
{"default,female#there are actors"|l10n}

The result in French:

il y a une actrice
il y a des acteurs
il y a des actrices
  • Line 1: The default domain is used, the female context, and the count is set to 1.
  • Line 2: The domain is not specified, so the default context is used. The context is not specified, so the first one defined is used. The count is 3.
  • Line 3: Default domain and female context are used. The count is not specified, so the default count (*) is used.

9.3Modifier: parameters

Parameters can be supplied to the modifier, which will be used to replace portions of text. Each parameter will take the place of a marker of the type %1%, %2%, %3% and so on.

If the translation file contains the following definitions:

<?php
return [
    'default' => [
        'Hello %1%'        => 'Bonjour %1%',
        '%1% has %2% kids' => "%1% a %2% enfants"
    ]
];

Your template may contain:

{"Hello %1%"|l10n:'James'}
{"%1% has %2% kids"|l10n:'Alice':3}

Displayed in French:

Bonjour James
Alice a 3 enfants

Parameters can of course be used at the same time as domain, context and count.

The domain is available with the %domain% tag.
Context is available with the %ctx% tag.
The count is available with the %count% tag.

For example, with the following translation file:

<?php
return [
    'default' => [
        '%1% has %count% kids' => [
            'any' => [
                '0' => "%1% n'a pas d'enfant",
                '1' => "%1% a un enfant",
                '*' => "%1% a %count% enfants"
            ],
            'girls' => [
                '0' => "%1% n'a pas de filles",
                '1' => "%1% a une fille",
                '*' => "%1% a %count% filles"
            ],
            'boys' => [
                '0' => "%1% n'a pas de garçons",
                '1' => "%1% a un garçon",
                '*' => "%1% a %count% garçons"
            ]
        ]
    ]
];

If your template contains this:

{",,3#%1% has %count% kids"|l10n:'Alice'}
{"default,boys,0#%1% has %count% kids"|l10n:'Bob'}
{",girls,1#%1% has %count% kids"|l10n:'Camille'}

The result will be:

Alice a 3 enfants
Bob n'a pas de garçons
Camille a une fille
  • Line 1: The domain is not specified, so the default domain will be used. The context is not specified, so the first one defined (any) will be used. And we take the value 3 for the countdown.
  • Line 2: The default domain is specified. The boys context is specified. The count is set to 0.
  • Line 3: Domain not specified. The girls context is specified. The count is 1.

10Templates: block tag

You can also use the Smarty {l10n} block tag.

{l10n}Model{/l10n}

Displayed in French:

Modèle

Unlike modifiers, translation blocks do not escape special characters.


10.1Block tag: domain

The default domain is always default. Another domain can be specified using the _ attribute (the underscore character) or the domain attribute on the opening tag.

{l10n _='header'}
    Home
{/l10n}
{l10n domain='header'}
    Download
{/l10n}

Displayed in French:

Accueil
Télécharger

10.2Block tag: context and count

It is possible to specify context and count in two different ways.

The first is to add the ctx and count attributes to the opening tag.
The second is to use the _ attribute (underscore character), adding the context and count after the domain, separated by commas.

If the context is not supplied, the first one listed in the translation file will be used. If the count is not supplied, the default count (*) will be used; if it doesn't exist, the first count defined will be used.

Template example:

{l10n _='default,female,1'}
    there are actors
{/l10n}
{l10n _=',,3'}
    there are actors
{/l10n}
{l10n _='default,female'}
    there are actors
{/l10n}

Displayed in French:

il y a une actrice
il y a des acteurs
il y a des actrices
  • Line 1: The default domain, the female context, and the count set to 1 are used.
  • Line 2: The domain is not specified, so the default context is used. The context is not specified, so the first one defined is used. The count is 3.
  • Line 3: Default domain and female context are used. The count is not specified, so the default count (*) is used.

The count attribute can be either a number or an array. If it's an array, its number of elements will be used for the count.

Template example:

{$actors = ['Alice', 'Bernadette', 'Camille']}
{l10n ctx='female' count=$actors}
    there are actors
{/l10n}

Displayed in French:

il y a des actrices

10.3Block tag: parameters

As with modifiers, translation blocks can receive parameters. These parameters are passed as attributes to the opening {l10n} tag, and used in strings in the form %name_parameter%.

Thus, it's possible to use parameters like name="Luke" in the tag (with the %name% marker in the template). But to be compatible with the parameters used with the modifier (see above), it is recommended to name the parameters using numbers starting at 1. For example, parameter 1="Luke" in the tag, and marker %1% in the template.

If the translation file contains the following definitions:

<?php
return [
    'default' => [
        'Hello %1%'        => "Bonjour %1%",
        '%1% has %2% kids' => "%1% a %2% enfants"
    ]
];

Your template may contain:

{l10n 1='Marie'}
    Hello %1%
{/l10n}
{l10n 1='Alice' 2='3'}
    %1% has %2% kids
{/l10n}

Display in French:

Bonjour Marie
Alice a 3 enfants

Parameters can of course be used at the same time as domain, context and count.

The domain is available with the %domain% tag.
Context is available with the %ctx% tag.
The count is available with the %count% marker.

For example, with the following translation file:

<?php
return [
    'default' => [
        '%1% has %count% kids' => [
            'any' => [
                '0' => "%1% n'a pas d'enfant",
                '1' => "%1% a un enfant",
                '*' => "%1% a %count% enfants"
            ],
            'girls' => [
                '0' => "%1% n'a pas de filles",
                '1' => "%1% a une fille",
                '*' => "%1% a %count% filles"
            ],
            'boys' => [
                '0' => "%1% n'a pas de garçons",
                '1' => "%1% a un garçon",
                '*' => "%1% a %count% garçons"
            ]
        ]
    ]
];

If your template contains:

{l10n _=',,1' 1='Alice'}
    %1% has %count% kids
{/l10n}
{l10n _='default,girls,0' 1='Bob'}
    %1% has %count% kids
{/l10n}
{l10n _=',boys,3' 1='Camille'}
    %1% has %count% kids
{/l10n}

It would be exactly the same as this:

{l10n count=1 1='Alice'}
    %1% has %count% kids
{/l10n}
{l10n domain='default' ctx='girls' count=0 1='Bob'}
    %1% has %count% kids
{/l10n}
{l10n ctx='boys' count=['Huey', 'Dewey', 'Louie'] 1='Camille'}
    %1% has %count% kids
{/l10n}

The result will be:

Alice a un enfant
Bob n'a pas de filles
Camille a 3 garçons
  • Lines 1 to 3: The domain is not specified, so the default domain is used. The context is not specified, so the first one defined (any) will be used. And we take the value 1 for the count.
  • Lines 4 to 6: The default domain is specified. The girlss context is specified. The count is set to 0.
  • Lines 7 to 9: The domain is not specified. The boys context is specified. An array is provided for the count, containing 3 elements.

9Prefix to error files

When this plugin is used, it is necessary to translate the error files defined in the errorPages section of the etc/temma.php file (see the documentation).

The paths defined in the configuration then take a prefix consisting of an error-pages directory, followed by a directory corresponding to the language used.

For example, for the following configuration file:

[
    'errorPages' => [
        '404' => 'error404.html'
    ]
]

If we ask for a page that does not exist in French (for example the URL /fr/sdsjnzeoizoueh), Temma will look for the file www/error-pages/fr/error404.html
If we ask for the same page in English (for example the URL /en/sdsjnzeoizoueh), Temma will look for the file www/error-pages/en/error404.html