Plugin de localisation multilingue


1Présentation

Ce plugin permet deux choses :

  • Gérer des URLs qui sont préfixées par la langue. Le plugin va donc rediriger automatiquement l'utilisateur vers la page demandée avec /fr/ (par exemple) au début de l'URL.
  • Charger automatiquement les traductions correspondant à la langue utilisée, pour mettre les chaînes de caractère traduites à disposition des templates.

2Configuration

Dans le fichier etc/temma.php, il faut ajouter le pré-plugin et le configurer :

<?php
return [
    // chargement du plugin
    'plugins' => [
        '_pre' => [
            '\Temma\Plugins\Language'
        ]
    ],
    // configuration du plugin
    'x-language' => [
        // liste des langues supportées par le site
        'supported' => [ 'fr', 'en', 'de' ],
        // langue par défaut, si le navigateur n'est compatible avec
        // aucune des langues listées ci-dessus
        'default' => 'fr',
        // (optionnel) on peut indiquer de ne pas utiliser les
        // préfixes de template (voir plus bas)
        'templatePrefix' => false,
        // (optionnel) on peut indiquer les URLs pour lesquelles
        // la gestion de la langue n'est pas activée
        'protectedUrls' => [
            '/robots.txt',
            '/sitemap.xml',
        ]
    ],
    // configuration de la vue Smarty
    'x-smarty-view' => [
        'pluginsDir' => '/path/to/project/lib/smarty-plugins'
    ]
];
  • Lignes 4 à 8 : Configuration des plugins.
    • Ligne 6 : Activation du pré-plugin qui prend en charge la gestion des langues.
  • Lignes 10 à 25 : Configuration du plugin.
    • Ligne 12 : Liste des langues supportées sur le site.
    • Ligne 15 : Définition de la langue par défaut (utilisée si le navigateur n'est compatible avec aucune des langues listées.
    • Lignes 21 à 24 : Définition de la liste des URLs pour lesquelles la gestion de la langue est désactivée.
  • Lignes 27 à 29 : Configuration de la vue Smarty.
    • Ligne 28 : Ajout du répertoire contenant les plugins Smarty fournis par Temma, pour que l'interpréteur Smarty soit capable de trouver l'extension (voir plus bas).

3Fonctionnement

Dans l'exemple de la configuration ci-dessus, on indique que le site possède des traductions en français, anglais et allemand.

Lorsqu'un navigateur va demander une page, il peut se produire deux cas de figure :

  • Soit l'URL demandée ne commence pas par /fr/, /en/ ni /de/. Dans ce cas, le plugin va rediriger l'utilisateur vers l'URL demandée, en lui ajoutant le préfixe de langue. La langue choisie sera la première supportée par le navigateur qui est aussi supportée par le site (si aucune ne convient, ce sera la langue par défaut du site qui sera utilisée).
  • Soit l'URL demandée commence par un préfixe de langue. Dans ce cas, le plugin va vérifier que la langue demandée est bien supportée par le site (si ce n'est pas le cas, on se retrouve dans le cas précédent). Si la langue est bien supportée, le plugin va extraire la langue, et modifier toutes les informations relatives au contrôleur, à l'action et aux paramètres, pour faire comme si ce préfixe n'avait pas existé dans l'URL. À côté de ça, le plugin charge le fichier de traductions correspondant, et ajoute un préfixe au chemin menant vers les fichiers de template.

Par exemple, si l'utilisateur demande la page /page/liste/date/5, avec un navigateur qui supporte le français, le plugin va le rediriger vers /fr/page/liste/date/5.

En arrivant sur /fr/page/liste/date/5, le plugin va extraire la langue (fr), va charger le fichier de traduction en français. Ensuite, il va modifier plusieurs choses :

  • Le contrôleur demandé devient page (et non plus fr).
  • L'action demandée devient liste (et non plus page).
  • La liste des paramètres devient ['date', '5'] (et non plus ['liste', 'date', '5']).

Les variables de template $CONTROLLER, $ACTION et $URL sont aussi modifiées en conséquence.


4Gestion des méthodes des requêtes

Si l'URL demandée commence par une information de langue (et que cette langue est autorisée par la configuration), le traitement expliqué ci-dessus s'effectuera toujours.

Par contre, si l'information de langue est absente, la redirection (vers la même URL à laquelle est ajouté le préfixe de langue) ne s'effectue pas si la requête avait une méthode POST ou PUT. Dans ce cas, le plugin ne fait rien et les traitements continuent normalement.


5URLs protégées

Il est possible de définir une liste d'URLs qui ne seront pas gérées par le plugin de langue. Cela est utile si vous avez par exemple des contrôleurs qui génèrent automatiquement le contenu des fichiers robots.txt ou sitemap.xml.

Attention, les URLs doivent commencer par le caractère '/'.


6Préfixe au chemin vers les templates

À moins que la variable de configuration templatePrefix ait été configurée à false, le plugin va modifier le chemin menant aux fichiers de template, en y ajoutant au début un répertoire correspondant à la langue demandée.

Ainsi, si l'URL est /fr/article/liste, le template utilisé sera (par défaut) le fichier templates/fr/article/liste.tpl

Si le contrôleur utilise un autre fichier, le préfixe sera ajouté de toute manière.
Par exemple, si le contrôleur contient la ligne suivante :

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

Temma utilisera le fichier templates/fr/cms/content/article_list.tpl


7Fichiers de traduction

7.1Format de base

Le plugin charge des fichiers contenant les traductions. Ces fichiers doivent être placés dans le répertoire etc/lang/ du projet. Comme pour le fichier de configuration, ces fichiers peuvent être au format PHP, JSON, INI, YAML ou NEON.

Le format PHP est conseillé, car il bénéficie de la mise en cache d'OPCode (le fichier n'est pas relu à chaque accès). Mais vous pouvez choisir un format différent, comme le format INI par souci de simplicité.

Les noms des fichiers dépendent de la langue des traductions qu'ils contiennent : fr.php, en.ini, es.json, etc.

Voici un exemple de fichier fr.php :

<?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',
	],
];

La même chose au format INI (fichier fr.ini) :

[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"

Au format JSON (fichier fr.json) :

{
    "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"
    }
}

Au format YAML (fichier fr.yaml) ou NEON (fichier fr.neon) :

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

Comme vous pouvez le voir, les chaînes traduites sont classées par sections (ici default, header et footer). Ces sections correspondent à des domaines de traduction.

Ici encore, les chaînes de base sont en anglais, et on fournit par chacun leur traduction en français.

Important : Si on essyaye de traduire une châine de caractères, mais qu'elle est introuvable dans le fichier de traduction, elle sera utilisée telle quelle.
Dans l'exemple ci-dessus, on peut voir que les chaînes de base sont en anglais, et qu'on a fourni une traduction en français. Il ne sera donc pas nécessaire de fournir un fichier de traduction en anglais.


7.2Gestion du contexte

Il est possible de décliner une chaîne de caractères en plusieurs versions. Chacune de ces version dépend d'un contexte personnalisable. Ces contextes sont par exemple utilisés pour gérer les versions masculin/féminin d'un texte.

Le contexte par défaut est celui défini en premier.

Pour définir des contexte, il suffit d'associer un tableau associatif à la clé de traduction.

Exemple de fichier fr.php :

<?php

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

Et le fichier en.php correspondant :

<?php

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

7.3Gestion du décompte (singulier/pluriel)

Les fichiers de traduction peuvent aussi comporter différentes déclinaisons d'une même chaîne de caractères, en fonction d'un nombre d'éléments. Cela va plus loin que le simple singulier/pluriel, cela permet de gérer lorsqu'il n'y a aucun élément, ou un nombre variable d'éléments.

Comme pour les contextes, il faut définir un tableau associatif, dont les clés sont le nombre d'éléments correspondant à la déclinaison. Il est possible d'avoir des clés qui commencent par < ou <= pour définir des intervalles.

La valeur par défaut est définie avec la clé *. Si elle n'est pas fournie, ce sera la première valeur de la liste qui sera utilisée.

Exemple de fichier de traduction 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',
        ],
    ],
];

Et sa déclinaison en.php :

<?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.4Contexte + décompte

Il est possible d'utiliser les contextes et le décompte de manière conjointe. Pour cela, chaque contexte doit contenir un décompte.

Voici un exemple :

<?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',
            ],
        ],
    ],
];

8Utilisation dans les templates

Dans les templates Smarty, deux variables supplémentaires sont crées par le plugin :

  • $lang : Contient la langue courante (fr, en, etc.).
  • $l10n : Contient le tableau des traductions. Ne doit pas être utilisée.

Vous pouvez utiliser directement la variable $lang dans vos templates :

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

Affiché en française, cela donnera :

    Bonjour tout le monde

En anglais :

    Hello everyone

Il est aussi possible d'utiliser le modificateur |l10n et la balise de bloc {l10n}…{/l10n}.


9Templates : modificateur

Pour traduire des chaînes de caractères, on peut utiliser le modificateur Smarty l10n. Il cherche la traduction de la chaîne qu'il reçoit en entrée, et la retourne après en avoir échappé les caractères spéciaux.

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

{"zkwx<hyq"|l10n}

Affiché en français, cela donnera :

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

zkwx&lt;hyq

En anglais :

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

zkwx&lt;hyq
  • Lignes 1 et 2 : Les chaînes listées dans la section default du fichier de traduction sont utilisables directement.
  • Ligne 4 : Si la chaîne de caractères n'est pas trouvée dans le fichier de traduction, elle est utilisée telle quelle. Le caractère spécial < est échappé en &lt;.

9.1Modificateur : domaine

Pour spécifier un domaine, il faut l'ajouter avant la chaîne de traduction, en utilisant le caractère dièse (#) pour séparer le domaine et la chaîne.

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

Affiché en français, cela donnera :

Accueil
Télécharger

En anglais :

Home
Download

9.2Modificateur : contexte et décompte

Le contexte et le décompte peuvent être fournis à la suite du domaine, séparés par des virgules.

Si le contexte n'est pas fourni, on utilisera le premier listé dans le fichier de traduction. Si le décompte n'est pas fourni, c'est le décompte par défaut (*) qui sera utilisé ; s'il n'existe pas, ce sera le premier décompte défini qui sera utilisé.

Exemple de template :

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

Le résultat en français :

il y a une actrice
il y a des acteurs
il y a des actrices
  • Ligne 1 : On prend le domaine par défaut, le contexte female, et le décompte vaut 1.
  • Ligne 2 : Le domaine n'est pas spécifié, c'est donc le contexte par défaut qui est utilisé. Le contexte n'est pas spécifié, c'est donc le premier défini qui est utilisé. Le décompte vaut 3.
  • Ligne 3 : On prend le domaine par défaut, et le contexte female. Le décompte n'est pas spécifié, c'est donc le décompte par défaut (*) qui est utilisé.

9.3Modificateur : paramètres

Il est possible de fournir des paramètres au modificateur, qui seront utilisés pour remplacer des portions de texte. Chaque paramètre prendra la place d'un marqueur du type %1%, %2%, %3% et ainsi de suite.

Si le fichier de traduction contient les définitions suivantes :

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

Votre template pourra contenir :

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

Affiché en français :

Bonjour James
Alice a 3 enfants

Les paramètres peuvent évidemment être utilisés en même temps que le domaine, le contexte et le décompte.

Le domaine est disponible avec le marqueur %domain%.
Le contexte est disponible avec le marqueur %ctx%.
Le décompte est disponible avec le marqueur %count%.

Par exemple, avec le fichier de traduction suivant :

<?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"
            ]
        ]
    ]
];

Si votre template contient ceci :

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

Cela aura pour résultat :

Alice a 3 enfants
Bob n'a pas de garçons
Camille a une fille
  • Ligne 1 : Le domaine n'est pas spécifié, c'est donc celui par défaut qui sera utilisé. Le contexte n'est pas spécifié, c'est donc le premier défini (any) qui sera utilisé. Et on prend la valeur 3 pour le décompte.
  • Ligne 2 : Le domaine default est spécifié. Le contexte boys est spécifié. Le décompte vaut 0.
  • Ligne 3 : Le domaine n'est pas spécifié. Le contexte girls est spécifié. Le décompte vaut 1.

10Templates : balise de bloc

Il est aussi possible d'utiliser la balise de bloc Smarty {l10n}.

{l10n}Model{/l10n}

Affiché en français :

Modèle

Contrairement aux modificateurs, les blocs de traduction n'échappent pas les caractères spéciaux.


10.1Balise : domaine

Le domaine par défaut est toujours default. Un autre domaine peut être spécifié en utilisant l'attribut _ (le caractère underscore) ou l'attribut domain sur la balise ouvrante.

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

Affiché en français :

Accueil
Télécharger

10.2Balise : contexte et décompte

Il est possible de spécifier le contexte et le décompte de deux manières différentes.

La première consiste à utiliser ajouter des attributs ctx et count à la balise ouvrante.
La seconde consiste à utiliser l'attibut _ (caractère underscore) en y ajoutant le contexte et le décompte après le domaine, séparés par des virgules.

Si le contexte n'est pas fourni, on utilisera le premier listé dans le fichier de traduction. Si le décompte n'est pas fourni, c'est le décompte par défaut (*) qui sera utilisé ; s'il n'existe pas, ce sera le premier décompte défini qui sera utilisé.

Exemple de template :

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

Le résultat en français :

il y a une actrice
il y a des acteurs
il y a des actrices
  • Ligne 1 : On prend le domaine par défaut, le contexte female, et le décompte vaut 1.
  • Ligne 2 : Le domaine n'est pas spécifié, c'est donc le contexte par défaut qui est utilisé. Le contexte n'est pas spécifié, c'est donc le premier défini qui est utilisé. Le décompte vaut 3.
  • Ligne 3 : On prend le domaine par défaut, et le contexte female. Le décompte n'est pas spécifié, c'est donc le décompte par défaut (*) qui est utilisé.

L'attribut count peut recevoir un nombre ou un tableau. Si c'est un tableau, c'est son nombre d'éléments qui servira pour le décompte.

Exemple de template :

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

Le résultat en français :

il y a des actrices

10.3Balise : paramètres

Comme pour les modificateurs, les blocs de traduction peuvent recevoir des paramètres. Ces paramètres sont passés en attributs à la balise {l10n} ouvrante, et utilisés dans les chaînes de caractère sous la forme %nom_paramètre%.

Ainsi, il est possible d'utiliser des paramètres comme name="Luke" dans la balise (avec le marqueur %name% dans le template). Mais pour être compatible avec les paramètres utilisés avec le modificateur (voir plus haut), il est recommandé de nommé les paramètres en utilisant des numéros commençant à 1. Par exemple, le paramètre 1="Luke" dans la balise, et le marqueur %1% dans le template.

Si le fichier de traduction contient les définitions suivantes :

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

Votre template pourra contenir :

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

Affiché en français :

Bonjour Marie
Alice a 3 enfants

Les paramètres peuvent évidemment être utilisés en même temps que le domaine, le contexte et le décompte.

Le domaine est disponible avec le marqueur %domain%.
Le contexte est disponible avec le marqueur %ctx%.
Le décompte est disponible avec le marqueur %count%.

Par exemple, avec le fichier de traduction suivant :

<?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"
            ]
        ]
    ]
];

Si votre template contient ceci :

{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}

Ce serait exactement identique à ceci :

{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=['Riri', 'Fifi', 'Loulou'] 1='Camille'}
    %1% has %count% kids
{/l10n}

Et cela aura pour résultat :

Alice a un enfant
Bob n'a pas de filles
Camille a 3 garçons
  • Lignes 1 à 3 : Le domaine n'est pas spécifié, c'est donc celui par défaut qui sera utilisé. Le contexte n'est pas spécifié, c'est donc le premier défini (any) qui sera utilisé. Et on prend la valeur 1 pour le décompte.
  • Ligne 4 à 6 : Le domaine default est spécifié. Le contexte girls est spécifié. Le décompte vaut 0.
  • Ligne 7 à 9 : Le domaine n'est pas spécifié. Le contexte boys est spécifié. Un tableau est fourni pour le décompte, contenant 3 éléments.

11Préfixe aux fichiers d'erreur

Lorsque ce plugin est utilisé, il faut traduire les fichiers d'erreur définis dans la section errorPages du fichier etc/temma.php (voir la documentation).

Les chemins définis dans la configuration prennent alors un préfixe constitué d'un répertoire error-pages, suivi d'un répertoire correspondant à la langue utilisée.

Par exemple, pour le fichier de configuration suivant :

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

Si on demande une page qui n'existe pas en langue française (par exemple l'URL /fr/sdsjnzeoizoueh), Temma ira chercher le fichier www/error-pages/fr/error404.html
Si on demande la même page en langue anglaise (par exemple l'URL /en/sdsjnzeoizoueh), Temma ira chercher le fichier www/error-pages/en/error404.html