Traduzioni

Il termine “internazionalizzazione” si riferisce al processo di astrazione delle stringhe e altri pezzi specifici dell’applicazione che variano in base al locale, in uno strato dove possono essere tradotti e convertiti in base alle impostazioni internazionali dell’utente (ad esempio lingua e paese). Per il testo, questo significa che ognuno viene avvolto con una funzione capace di tradurre il testo (o “messaggio”) nella lingua dell’utente:

// il testo verrà *sempre* stampato in inglese
echo 'Hello World';

// il testo può essere tradotto nella lingua dell'utente finale o
// restare in inglese
echo $translator->trans('Hello World');

Note

Il termine locale si riferisce all’incirca al linguaggio dell’utente e al paese. Può essere qualsiasi stringa che l’applicazione utilizza poi per gestire le traduzioni e altre differenze di formati (ad esempio il formato di valuta). Si consiglia di utilizzare il codice di lingua ISO 639-1, un carattere di sottolineatura (_), poi il codice di paese ISO 3166-1 alpha-2 (per esempio fr_FR per francese/Francia).

In questo capitolo si imparerà a usare il componenten Translation nel framework Symfony2. Si può leggere la documentazione del componente Translation per saperne di più. Nel complesso, il processo ha diverse fasi:

  1. Abilitare e configurare il servizio translation di Symfony;
  2. Astrarre le stringhe (i. “messaggi”) avvolgendoli nelle chiamate al Translator (“Traduzione di base”);
  3. Creare risorse di traduzione per ogni lingua supportata che traducano tutti i messaggio dell’applicazione;
  4. Determinare, impostare e gestire le impostazioni locali dell’utente per la richiesta e, facoltativamente, sull’intera sessione.

Configurazione

Le traduzioni sono gestire da un servizio translator, che utilizza i locale dell’utente per cercare e restituire i messaggi tradotti. Prima di utilizzarlo, abilitare translator nella configurazione:

Vedere Fallback e locale predefinito per dettagli sulla voce fallback e su cosa faccia Symfony quando non trova una traduzione.

Il locale usato nelle traduzioni è quello memorizzato nella richiesta. Tipicamente, è impostato tramite un attributo _locale in una rotta (vedere Il locale e gli URL).

Traduzione di base

La traduzione del testo è fatta attraverso il servizio translator (Symfony\Component\Translation\Translator). Per tradurre un blocco di testo (chiamato messaggio), usare il metodo :method:`Symfony\\Component\\Translation\\Translator::trans`. Supponiamo, ad esempio, che stiamo traducendo un semplice messaggio all’interno del controllore:

// ...
use Symfony\Component\HttpFoundation\Response;

public function indexAction()
{
    $translated = $this->get('translator')->trans('Symfony2 is great');

    return new Response($translated);
}

Quando questo codice viene eseguito, Symfony2 tenterà di tradurre il messaggio “Symfony2 is great” basandosi sul locale dell’utente. Perché questo funzioni, bisogna dire a Symfony2 come tradurre il messaggio tramite una “risorsa di traduzione”, che è una raccolta di traduzioni dei messaggi per un dato locale. Questo “dizionario” delle traduzioni può essere creato in diversi formati, ma XLIFF è il formato raccomandato:

Per informazioni sulla posizione di questi file, vedere Sedi per le traduzioni e convenzioni sui nomi.

Ora, se la lingua del locale dell’utente è il francese (per esempio fr_FR o fr_BE), il messaggio sarà tradotto in J'aime Symfony2. Si può anche tradurre il messaggio da un template.

Il processo di traduzione

Per tradurre il messaggio, Symfony2 utilizza un semplice processo:

  • Viene determinato il locale dell’utente corrente, che è memorizzato nella richiesta;
  • Un catalogo di messaggi tradotti viene caricato dalle risorse di traduzione definite per il locale (ad es. fr_FR). Vengono anche caricati i messaggi dal locale predefinito e aggiunti al catalogo, se non esistono già. Il risultato finale è un grande “dizionario” di traduzioni;
  • Se il messaggio si trova nel catalogo, viene restituita la traduzione. Se no, il traduttore restituisce il messaggio originale.

Quando si usa il metodo trans(), Symfony2 cerca la stringa esatta all’interno del catalogo dei messaggi e la restituisce (se esiste).

Segnaposto per i messaggi

A volte, un messaggio da tradurre contiene una variabile:

use Symfony\Component\HttpFoundation\Response;

public function indexAction($name)
{
    $translated = $this->get('translator')->trans('Hello '.$name);

    return new Response($translated);
}

Tuttavia, la creazione di una traduzione per questa stringa è impossibile, poiché il traduttore proverà a cercare il messaggio esatto, includendo le parti con le variabili (per esempio “Hello Ryan” o “Hello Fabien”).

Per dettagli su come gestire questa situazione, vedere component-translation-placeholders nella documentazione del componente. Per i template, vedere Template Twig.

Pluralizzazione

Un’ulteriore complicazione si presenta con traduzioni che possono essere plurali o meno, in base a una qualche variabile:

There is one apple.
There are 5 apples.

Per poterlo gestire, usare il metodo :method:`Symfony\\Component\\Translation\\Translator::transChoice` del tag o del filtro transchoice nel template.

Per ulteriori e approfondite informazioni, vedere component-translation-pluralization nella documentazione del componente Translation.

Traduzioni nei template

Le traduzioni avvengono quasi sempre all’interno di template. Symfony2 fornisce un supporto nativo sia per i template Twig che per quelli PHP.

Template Twig

Symfony2 fornisce tag specifici per Twig (trans e transchoice), che aiutano nella traduzioni di messaggi di blocchi statici di testo:

{% trans %}Hello %name%{% endtrans %}

{% transchoice count %}
    {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}

Il tag transchoice prende in automatico la variabile %count% dal contesto e la passa al traduttore. Questo meccanismo funziona solo usando un segnaposto che segue lo schema %variabile%.

Caution

La notazione %variabile% dei segnaposti è obbligatoria quando si traduce in un template Twig usando il tag.

Tip

Se si deve usare un simbolo di percentuale (%) in una stringa, occorre raddoppiarlo: {% trans %}Percent: %percent%%%{% endtrans %}

Si può anche specificare il dominio del messaggio e passare variabili aggiuntive:

{% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %}

{% trans with {'%name%': 'Fabien'} from "app" into "fr" %}Hello %name%{% endtrans %}

{% transchoice count with {'%name%': 'Fabien'} from "app" %}
    {0} %name%, there are no apples|{1} %name%, there is one apple|]1,Inf] %name%, there are %count% apples
{% endtranschoice %}

I filtri trans e transchoice possono essere usati per tradurre testi variabili ed espressioni complesse:

{{ message|trans }}

{{ message|transchoice(5) }}

{{ message|trans({'%name%': 'Fabien'}, "app") }}

{{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }}

Tip

L’uso dei tag o dei filtri di traduzione ha il medesimo effetto, ma con una sottile differenza: l’escape automatico si applica solo alla traduzione che usa un filtro. In altre parole, se ci si deve assicurare che il testo tradotto non abbia escape, occorre applicare il filtro raw dopo il filtro di traduzione:

{# il testo tra tag non subisce escape #}
{% trans %}
    <h3>foo</h3>
{% endtrans %}

{% set message = '<h3>foo</h3>' %}

{# stringhe e variabili tradotte con filtro subiscono escape #}
{{ message|trans|raw }}
{{ '<h3>bar</h3>'|trans|raw }}

Tip

Si può impostare il dominio di un intero template Twig con un semplice tag:

{% trans_default_domain "app" %}

Notare che questo influenza solo in template attuale, non i template “inclusi” (per evitare effetti collaterali).

Template PHP

Il servizio di traduzione è accessibile nei template PHP attraverso l’aiutante translator:

<?php echo $view['translator']->trans('Symfony2 is great') ?>

<?php echo $view['translator']->transChoice(
    '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
    10,
    array('%count%' => 10)
) ?>

Sedi per le traduzioni e convenzioni sui nomi

Symfony2 cerca i file dei messaggi (ad esempio le traduzioni) in due sedi:

  • la cartella app/Resources/translations;
  • la cartella app/Resources/<bundle>/translations;
  • la cartella Resources/translations/ del bundle.

I posti sono elencati in ordine di priorità. Quindi, si possono sovrascrivere i messaggi di traduzione di un bundle in una qualsiasi delle due cartelle superiori.

Il meccanismo di priorità si basa sulle chiavi: occorre dichiarare solamente le chiavi da sovrascrivere in un file di messaggi a priorità superiore. Se una chiave non viene trovata in un file di messaggi, il traduttore si appoggerà automaticamente ai file di messaggi a priorità inferiore.

È importante anche il nome del file con le traduzioni: ogni file con i messaggi deve essere nominato secondo il seguente schema: dominio.locale.caricatore:

  • dominio: Un modo opzionale per organizzare i messaggi in gruppi (ad esempio admin, navigation o il predefinito messages, vedere “using-message-domains”);
  • locale: Il locale per cui sono state scritte le traduzioni (ad esempio en_GB, en, ecc.);
  • caricatore: Come Symfony2 dovrebbe caricare e analizzare il file (ad esempio xliff, php o yml).

Il caricatore può essere il nome di un qualunque caricatore registrato. Per impostazione predefinita, Symfony fornisce i seguenti caricatori:

  • xliff: file XLIFF;
  • php: file PHP;
  • yml: file YAML.

La scelta di quali caricatori utilizzare è interamente a carico dello sviluppatore ed è una questione di gusti.

Note

È anche possibile memorizzare le traduzioni in una base dati o in qualsiasi altro mezzo, fornendo una classe personalizzata che implementa l’interfaccia Symfony\Component\Translation\Loader\LoaderInterface. Vedere dic-tags-translation-loader per maggiori informazioni.

Caution

Ogni volta che si crea una nuova risorsa di traduzione (o si installa un bundle che include risorse di traduzioni), assicurarsi di pulire la cache, in modo che Symfony possa rilevare le nuove risorse:

$ php app/console cache:clear

Fallback e locale predefinito

Ipotizzando che il locale dell’utente sia fr_FR e che si stia traducendo la chiave Symfony2 is great. Per trovare la traduzione francese, Symfony verifica le risorse di traduzione di vari locale:

  1. Prima, Symfony cerca la traduzione in una risorsa di traduzione fr_FR (p.e. messages.fr_FR.xfliff);
  2. Se non la trova, Symfony cerca una traduzione per una risorsa di traduzione fr (p.e. messages.fr.xliff);
  3. Se non trova nemeno questa, Symfony usa il parametro di configurazione fallback, che ha come valore predefinito en (vedere Configurazione).

Gestire il locale dell’utente

Il locale dell’utente attuale è memorizzato nella richiesta e accessibile tramite l’oggetto request:

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $locale = $request->getLocale();

    $request->setLocale('en_US');
}

Tip

Leggere /cookbook/session/locale_sticky_session per imparare come memorizzare il locale in sessione.

Vedere la sezione seguente, Il locale e gli URL, per impostare il locale tramite rotte.

Il locale e gli URL

Dal momento che si può memorizzare il locale dell’utente nella sessione, si può essere tentati di utilizzare lo stesso URL per visualizzare una risorsa in più lingue in base al locale dell’utente. Per esempio, http://www.example.com/contact può mostrare contenuti in inglese per un utente e in francese per un altro. Purtroppo questo viola una fondamentale regola del web: un particolare URL deve restituire la stessa risorsa indipendentemente dall’utente. Inoltre, quale versione del contenuto dovrebbe essere indicizzata dai motori di ricerca?

Una politica migliore è quella di includere il locale nell’URL. Questo è completamente supportato dal sistema delle rotte utilizzando il parametro speciale _locale:

Quando si utilizza il parametro speciale _locale in una rotta, il locale corrispondente verrà automaticamente impostato sulla richiesta e potrà essere recuperate tramite il metodo :method:`Symfony\\Component\\HttpFoundation\\Request::getLocale`. In altre parole, se un utente visita l’URI /fr/contact, il locale fr viene impostato automaticamente come locale per la richiesta corrente.

È ora possibile utilizzare il locale dell’utente per creare rotte ad altre pagine tradotte nell’applicazione.

Impostare un locale predefinito

Che fare se non si è in grado di determinare il locale dell’utente? Si può garantire che un locale sia impostato a ogni richiesta, definendo un default_locale per il framework:

Tradurre i messaggi dei vincoli

Se si usano i vincoli di validazione dei form, la traduzione dei messaggi di errore è facile: basta creare una risorsa di traduzione per il dominio validators.

Per iniziare, supponiamo di aver creato un oggetto PHP, necessario da qualche parte in un’applicazione:

// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;

class Author
{
    public $name;
}

Aggiungere i vincoli tramite uno dei metodi supportati. Impostare l’opzione del messaggio al testo sorgente della traduzione. Per esempio, per assicurarsi che la proprietà $name non sia vuota, aggiungere il seguente:

Creare un file di traduzione sotto il catalogo validators per i messaggi dei vincoli, tipicamente nella cartella Resources/translations/ del bundle.

Tradurre contenuti della base dati

La traduzione di contenuti della base dati andrebbe affidata a Doctrine, tramite l’estensione Translatable o il behavior Translatable (per PHP 5.4+). Per maggiori informazioni, vedere la documentazione di queste librerie.

Debug delle traduzioni

New in version 2.5: Il comando translation:debug è stato introdotto in Symfony 2.5.

Durante la manutenzione di un bundle, si potrebbe usare o disabilitare un messaggio di traduzioni, senza aggiornare tutti i cataloghi dei messaggi. Il comando translation:debug aiuta a trovare questi messaggi di traduzioni mancanti o inutilizzati, per un locale dato. Mostra una tabella con i risultati della traduzione del messaggio nel locale fornito e il risultato quando viene usato il fallback. Inoltre, mostra quando le traduzioni sono uguali all traduzione fallback (potrebbe indicare che il messaggio non sia stato tradotto correttamente).

Grazie agli estrattori di messaggi, il comando troverà il tag di traduzione o l’uso di filtri nei template Twig:

{% trans %}Symfony2 is great{% endtrans %}

{{ 'Symfony2 is great'|trans }}

{{ 'Symfony2 is great'|transchoice(1) }}

{% transchoice 1 %}Symfony2 is great{% endtranschoice %}

Individuerà anche i seguenti utilizzi di traduzione in template PHP:

$view['translator']->trans("Symfony2 is great");

$view['translator']->trans('Symfony2 is great');

Caution

Gli estrattori non sono in grado di ispezionare i messaggi tradoti fuori dai template, il che vuol dire che gli utilizzi di traduzioni in label di form o dentro a controllori non saranno individuati. Traduzioni dinamiche, che coinvolgano variabili o espressioni, non sono individuate nei template, il che vuol dire che questo esempio non sarà analizzato:

{% set message = 'Symfony2 is great' %}
{{ message|trans }}

Si supponga che il locale predefinito sia fr e di aver configurato en come locale di fallback (vedere Configurazione e Fallback e locale predefinito su come configurarli). Si supponga inoltre di aver già preparato alcune traduzioni per il locale fr in un AcmeDemoBundle:

e per il locale en:

Per individuare tutti i messaggi nel locale fr per AcmeDemoBundle, eseguire:

$ php app/console translation:debug fr AcmeDemoBundle

Si otterrà questo output:

images/book/translation/debug_1.png

Indica che il messaggio Symfony2 is great è inutilizzato, perché è stato tradotto, ma viene usato.

Ora, se si traduce il messaggio in uno dei template, si otterrà questo output:

images/book/translation/debug_2.png

Lo stato è vuoto, che vuol dire che il messaggio è stato tradotto nel locale fr e usato in un template.

Se si cancella il messaggio Symfony2 is great dal file di traduzione per il locale fr e si esegue il comando, si otterrà:

images/book/translation/debug_3.png

Lo stato indica che il messaggio è mancante, perché non è tradotto nel locale fr, ma è usato in un template. Inoltre, il messaggio nel locale fr è uguale al messaggio nel locale en. Questo è caso particolare, perché il messaggio non tradotto ha lo stesso id della sua traduzione nel locale en.

Se si copia il contenuto del file di traduzione del locale en nel file di traduzione del locale fr e si esegue il comando, si otterrà:

images/book/translation/debug_4.png

Si può vedere che le traduzioni del messaggio sono identiche nei locale fr ed en, che vuol dire che questo messaggio è stato probabilmente copiato da francese a inglese e forse ci si è dimenticati di tradurlo.

L’ispezione predefinita avviene su tutti i domini, ma si può specificare un singolo dominio:

$ php app/console translation:debug en AcmeDemoBundle --domain=messages

Quando i bundle hanno molti messaggi, è utile mostrare solo quelli non usati oppure solo quelli mancanti, usando le opzioni --only-unused o --only-missing:

$ php app/console translation:debug en AcmeDemoBundle --only-unused
$ php app/console translation:debug en AcmeDemoBundle --only-missing