Giorno 19: Internazionalizzazione e Localizzazione
Ieri abbiamo finito il motore di ricerca, rendendolo più divertente con l'aggiunta di alcune buone cose in AJAX.
Oggi parleremo di internazionalizzazione (o i18n) e di localizzazione|Localizzazione (o l10n|L10n) per Jobeet.
Da Wikipedia:
L'internazionalizzazione è un processo per disegnare applicazioni in modo che possano essere adattate a varie lingue|Lingue e regioni, senza modifiche ai programmi.
La localizzazione è il processo di adattamento di un programma per una specifica regione o lingua, con l'aggiunta di componenti e testi locali.
Come sempre, il framework symfony non ha reinventato la ruota e il suo supporto a i18n e l10n è basato sullo standard ICU.
Utente
Nessuna internazionalizzazione è possibile senza utente. Quando un sito è disponibile in diverse lingue o per diversi posti del mondo, l'utente è responsabile della scelta di quello che risulti migliore.
note
Abbiamo già parlato della classe User di symfony nel giorno 13.
La cultura dell'utente
Le caratteristiche di symfony su i18n e l10n sono basate sulla cultura|Cultura
dell'utente. La cultura è la combinazione della lingua e del paese
dell'utente. Per esempio, la cultura per utente che parla Italiano è
it
e la cultura di utente che viene dall'Italia è it_IT
.
Si può gestire la cultura dell'utente richiamando i metodi setCulture()
e getCulture()
dell'oggetto User:
// in un'azione $this->getUser()->setCulture('fr_BE'); echo $this->getUser()->getCulture();
tip
La lingua|Lingua è codificata in due lettere minuscole, secondo lo standard ISO 639-1, mentre il paese è codificato in due lettere maiuscole, secondo lo standard ISO 3166-1.
La cultura preferita
Per default, la cultura dell'utente è impostata nel file di configurazione
settings.yml
:
# apps/frontend/config/settings.yml all: .settings: default_culture: it_IT
tip
Siccome la cultura è gestita dall'oggetto User, è memorizzata nella sessione. Durante lo sviluppo, se si cambia la cultura di default, si dovrà cancellare il cookie|Cookie di sessione per fare in modo che la nuova impostazione abbia effetto nel browser.
Quando un utente inizia una sessione sul sito Jobeet, possiamo anche
determinare la cultura migliore, basandoci sulle informazioni fornite
dall'header HTTP|Header HTTP Accept-Language
.
Il metodo getLanguages()
dell'oggetto richiesta restituisce un array
di lingue per l'utente corrente, ordinate per preferenza:
// in un'azione $languages = $request->getLanguages();
Ma la maggior parte delle volte il sito non sarà disponibile in una delle
principali 136 lingue del mondo. Il metodo getPreferredCulture()
restituisce
la lingua migliore, confrontando le lingue preferite dall'utente con le
lingue supportate dal sito:
// in un'azione $language = $request->getPreferredCulture(array('en', 'fr'));
Nella chiamata precedente, la lingua restituita sarà Inglese o Francese, secondo le preferenze dell'utente, oppure Inglese (la prima lingua nell'array) se nessuna corrisponde.
Cultura nell'URL
Il sito Jobeet sarà disponibile in Inglese e Francese. Siccome un URL può
rappresentare solo una singola risorsa, la cultura deve essere inserita
nell'URL. Per poterlo fare, apriamo il file routing.yml
e aggiungiamo
la variabile speciale :sf_culture
per tutte le rotte, tranne che per
api_jobs
e homepage
. Per le rotte semplici, aggiungiamo /:sf_culture
all'inizio di url
. Per le rotte di insieme, aggiungiamo un'opzione
prefix_path
|Prefisso che inizia con /:sf_culture
.
# apps/frontend/config/routing.yml affiliate: class: sfPropelRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get } prefix_path: /:sf_culture/affiliate category: url: /:sf_culture/category/:slug.:sf_format class: sfPropelRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object } requirements: sf_format: (?:html|atom) job_search: url: /:sf_culture/search param: { module: job, action: search } job: class: sfPropelRouteCollection options: model: JobeetJob column: token object_actions: { publish: put, extend: put } prefix_path: /:sf_culture/job requirements: token: \w+ job_show_user: url: /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: model: JobeetJob type: object method_for_criteria: doSelectActive param: { module: job, action: show } requirements: id: \d+ sf_method: get
Quando la variabile sf_culture
è usata in una rotta, symfony userà
automaticamente il suo valore per cambiare la cultura dell'utente.
Poiché abbiamo bisogno di tante homepage quante sono le lingue che
supportiamo, (/en/
, /fr/
, ...), l'homepage di default (/
) deve
rinviare su quella localizzata appropriata, secondo la cultura
dell'utente. Ma se l'utente non ha ancora una cultura, perché arriva
su Jobeet per la prima volta, la cultura preferita sarà scelta
per lui.
Innanzitutto, aggiungiamo il metodo isFirstRequest()
a myUser
.
Restituisce true
solo se è la prima richiesta di una sessione
utente:
// apps/frontend/lib/myUser.class.php public function isFirstRequest($boolean = null) { if (is_null($boolean)) { return $this->getAttribute('first_request', true); } else { $this->setAttribute('first_request', $boolean); } }
Aggiungiamo una rotta localized_homepage
:
# apps/frontend/config/routing.yml localized_homepage: url: /:sf_culture/ param: { module: job, action: index } requirements: sf_culture: (?:fr|en)
Cambiamo l'azione index
del modulo job
per implementare la logica
che rinvia l'utente alla homepage "migliore" alla prima
richiesta di una sessione:
// apps/frontend/modules/job/actions/actions.class.php public function executeIndex(sfWebRequest $request) { if (!$request->getParameter('sf_culture')) { if ($this->getUser()->isFirstRequest()) { $culture = $request->getPreferredCulture(array('en', 'fr')); $this->getUser()->setCulture($culture); $this->getUser()->isFirstRequest(false); } else { $culture = $this->getUser()->getCulture(); } $this->redirect('@localized_homepage'); } $this->categories = JobeetCategoryPeer::getWithJobs(); }
Se la variabile sf_culture
non è presente nella richiesta, vuol dire
che l'utente è arrivato all'URL /
. Se questo è il caso e la
sessione è nuova, la cultura preferita viene usata come cultura
dell'utente. Altrimenti, viene usata la cultura corrente dell'utente.
L'ultimo passo è rinviare l'utente all'URL localized_homepage
.
Si noti che la variabile sf_culture
non è stata passata nel
rinvio, perché symfony la aggiunge automaticamente.
Ora, se si prova ad andare all'URL /it/
, symfony restituirà un errore 404|Errore 404,
perché abbiamo ristretto la variabile sf_culture
a en
o fr
.
Aggiungiamo questo requisito a tutte le rotte che includono la cultura:
requirements: sf_culture: (?:fr|en)
Test della cultura|Cultura
È tempo di testare la nostra implementazione. Ma prima di aggiungere altri
test, dobbiamo aggiustare quelli esistenti. Siccome tutti gli URL sono
cambiati, modifichiamo tutti i file dei test funzionali in
test/functional/frontend/
aggiungendo /en
all'inizio di ogni URL.
Non dimenticare di cambiare anche gli URL nel file
lib/test/JobeetTestFunctional.class.php
. Lanciamo tutti i test per
assicurarci che abbiamo aggiustato tutto:
$ php symfony test:functional frontend
Il tester dell'utente fornisce un metodo isCulture()
, che testa la
cultura corrente dell'utente. Apriamo il file jobActionsTest
e
aggiungiamo i seguenti test:
// test/functional/frontend/jobActionsTest.php $browser->setHttpHeader('ACCEPT_LANGUAGE', 'fr_FR,fr,en;q=0.7'); $browser-> info('6 - User culture')-> restart()-> info(' 6.1 - For the first request, symfony guesses the best culture')-> get('/')-> with('response')->isRedirected()-> followRedirect()-> with('user')->isCulture('fr')-> info(' 6.2 - Available cultures are en and fr')-> get('/it/')-> with('response')->isStatusCode(404) ; $browser->setHttpHeader('ACCEPT_LANGUAGE', 'en,fr;q=0.7'); $browser-> info(' 6.3 - The culture guessing is only for the first request')-> get('/')-> with('response')->isRedirected()-> followRedirect()-> with('user')->isCulture('fr') ;
Cambio di lingua
Per consentire all'utente di cambiare cultura, un form|Form di lingua deve
essere aggiunto nel layout. Il framework dei form non fornisce un
form del genere già pronto, ma siccome l'esigenza è abbastanza
comune per i siti internazionali, la squadra di sviluppo di symfony
mantiene il plugin
sfFormExtraPlugin
,
che contiene validatori|Validatori, widget|Widget e form che non possono essere inclusi nel
pacchetto principale di symfony perché sono troppo specifici o hanno
dipendenze esterne, ma sono comunque molto utili.
Installiamo il plugin con il task plugin:install
:
$ php symfony plugin:install sfFormExtraPlugin
note
sfFormExtraPlugin
contiene widget che richiedono dipendenze esterne,
come librerie JavaScript. C'è un widget per un selezionatore di
date in formato grafico, uno per un editor visuale e molto altro.
Consultare la documentazione, perché vi si possono trovare molte cose utili.
Il plugin sfFormExtraPlugin
fornisce un form sfFormLanguage
per
gestire la scelta della lingua. Si può aggiungere il form delle lingue
nel layout in questo modo:
note
Il codice seguente non è pensato per essere implementato. È qui solo per mostrare come si potrebbe essere tentati di implementare qualcosa nel modo sbagliato. Andremo avanti per mostrare come implementarlo nel modo giusto usando symfony.
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <!-- footer content --> <?php $form = new sfFormLanguage( $sf_user, array('languages' => array('en', 'fr')) ) ?> <form action="<?php echo url_for('@change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /> </form> </div> </div>
Riuscite a intravedere il problema? Giusto, la creazione dell'oggetto form non appartiene al livello della Vista. Deve essere creato da un'azione|Azione. Ma siccome il codice è nel layout, il form andrebbe creato in ogni azione, che non è affatto pratico. In questi casi, si dovrebbe usare un component. Un component|Component è come un partial, ma con un po' di codice allegato. Può essere considerato come un'azione leggera.
Si può includere un component in un template usando l'helper
include_component()
:
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <!-- footer content --> <?php include_component('language', 'language') ?> </div> </div>
L'helper accetta il modulo e l'azione come parametri. Il terzo parametro può essere usato per passare parametri al component.
Creiamo un modulo language
per ospitare il component e l'azione che
cambierà realmente la lingua dell'utente:
$ php symfony generate:module frontend language
I component vanno definiti nel file actions/components.class.php
.
Creiamo ora questo file:
// apps/frontend/modules/language/actions/components.class.php class languageComponents extends sfComponents { public function executeLanguage(sfWebRequest $request) { $this->form = new sfFormLanguage( $this->getUser(), array('languages' => array('en', 'fr')) ); } }
Come si può vedere, una classe components è molto simile a una classe actions.
Il template per un component usa la stessa convenzione dei nomi di
un partial: un trattino basso (_
) seguito dal nome del component:
// apps/frontend/modules/language/templates/_language.php <form action="<?php echo url_for('@change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /> </form>
Poiché il plugin non fornisce l'azione che cambia veramente la
cultura dell'utente, modifichiamo il file routing.yml
per creare
la rotta change_language
:
# apps/frontend/config/routing.yml change_language: url: /change_language param: { module: language, action: changeLanguage }
E creiamo l'azione corrispondente:
// apps/frontend/modules/language/actions/actions.class.php class languageActions extends sfActions { public function executeChangeLanguage(sfWebRequest $request) { $form = new sfFormLanguage( $this->getUser(), array('languages' => array('en', 'fr')) ); $form->process($request); return $this->redirect('@localized_homepage'); } }
Il metodo process()
di sfFormLanguage
si prende cura di cambiare
la cultura dell'utente, basandosi sui valori del form inviati dall'utente.
Internazionalizzazione
Lingue, set di caratteri|Set di caratteri e codifica|Codifica
Lingue diverse hanno diversi set di caratteri. La lingua Inglese è la più semplice, in quanto usa solo i caratteri ASCII, la lingua Francese è un po' più complessa, con caratteri accentati come "é", e lingue come il Russo, il Cinese o l'Arabo sono ancora più complesse, poiché tutti i loro caratteri sono fuori dall'elenco ASCII. Tali lingue sono definite con set di caratteri totalmente diversi.
Quando si ha a che fare con dati internazionalizzati, è meglio usare la norma unicode. L'idea dietro unicode|Unicode è quella di stabilire un set universale di caratteri, che contenga tutti i caratteri di tutte le lingue. Il problema con unicode è che un singolo carattere può essere rappresentato con fino a 21 bit. Quindi, per il web, usiamo UTF-8, che mappa Unicode su delle sequenze di ottetti a lunghezza variabile. In UTF-8, le lingue più usate hanno i loro caratteri codificati con meno di 3 bit.
UTF-8 è la codifica di default usata da symfony ed è definita nel file
di configurazione settings.yml
:
# apps/frontend/config/settings.yml all: .settings: charset: utf-8
Inoltre, per abilitare il livello di internazionalizzazione di symfony,
si deve impostare i18n
a true
in settings.yml
:
# apps/frontend/config/settings.yml all: .settings: i18n: true
Template
Un sito internazionalizzato ha un'interfaccia utente tradotta in diverse lingue.
In un template, tutte le stringhe che dipendono dalla lingua devono
essere racchiuse nell'helper __()
(notare che ci sono due trattini bassi).
L'helper __()
è parte del gruppo di helper I18N
, che contiene helper che
facilitano la gestione di i18n nei template. Siccome questo gruppo di
helper non è caricato di default, occorre aggiungerlo manualmente in ogni
template con use_helper('I18N')
, come abbiamo già fatto per il gruppo
di helper Text
, oppure caricarlo globalmente aggiungendolo a
standard_helpers
:
# apps/frontend/config/settings.yml all: .settings: standard_helpers: [Partial, Cache, I18N]
Ecco come usare l'helper __()
per il footer di Jobeet:
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <span class="symfony"> <img src="/legacy/images/jobeet-mini.png" /> powered by <a href="/"> <img src="/legacy/images/symfony.gif" alt="symfony framework" /></a> </span> <ul> <li> <a href=""><?php echo __('About Jobeet') ?></a> </li> <li class="feed"> <?php echo link_to(__('Full feed'), '@job?sf_format=atom') ?> </li> <li> <a href=""><?php echo __('Jobeet API') ?></a> </li> <li class="last"> <?php echo link_to(__('Become an affiliate'), '@affiliate_new') ?> </li> </ul> <?php include_component('language', 'language') ?> </div> </div>
note
L'helper __()
può prendere la stringa della lingua di default, oppure
si può usare un identificatore univoco per ogni stringa. È solo
una questione di gusti. Per Jobeet, useremo la prima strategia,
perché in questo modo i template sono più leggibili.
Quando symfony interpreta un template, ogni volta che l'helper __()
viene richiamato, symfony cerca una traduzione per la cultura corrente
dell'utente. Se trova una traduzione, la usa, altrimenti usa la
stringa non tradotta.
Tutte le traduzioni sono memorizzate in un catalogo|Catalogo di traduzione. Il framework i18n fornisce molte diverse strategie per memorizzare le traduzioni. Noi useremo il formato "XLIFF", che è uno standard ed è il più flessibile. È anche quello usato dall'admin generator e dalla maggior parte dei plugin di symfony.
note
Altri metodi di catalogazione sono gettext
, MySQL
e SQLite
.
Come sempre, uno sguardo alle API i18n
è utile per avere più dettagli.
i18n:extract
Invece di creare a mano il file del catalogo, meglio usare il task
i18n:extract
|Task di estrazione I18n:
$ php symfony i18n:extract frontend fr --auto-save
Il task i18n:extract
trova tutte le stringhe che necessitano di essere
tradotte in fr
nell'applicazione frontend
e crea o aggiorna il
catalogo corrispondente. L'opzione --auto-save
salva le nuove stringhe
nel catalogo.
Si può usare anche l'opzione --auto-delete
per rimuovere automaticamente
le stringhe che non esistono più.
Nel nostro caso, il task popola il file che abbiamo creato:
<!-- apps/frontend/i18n/fr/messages.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd"> <xliff version="1.0"> <file source-language="EN" target-language="fr" datatype="plaintext" original="messages" date="2008-12-14T12:11:22Z" product-name="messages"> <header/> <body> <trans-unit id="1"> <source>About Jobeet</source> <target/> </trans-unit> <trans-unit id="2"> <source>Feed</source> <target/> </trans-unit> <trans-unit id="3"> <source>Jobeet API</source> <target/> </trans-unit> <trans-unit id="4"> <source>Become an affiliate</source> <target/> </trans-unit> </body> </file> </xliff>
Ogni traduzione è gestita da un tag trans-unit
, che ha un attributo id
univoco. Si può ora modificare il file e aggiungere le traduzioni
per la lingua Francese:
<!-- apps/frontend/i18n/fr/messages.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd"> <xliff version="1.0"> <file source-language="EN" target-language="fr" datatype="plaintext" original="messages" date="2008-12-14T12:11:22Z" product-name="messages"> <header/> <body> <trans-unit id="1"> <source>About Jobeet</source> <target>A propos de Jobeet</target> </trans-unit> <trans-unit id="2"> <source>Feed</source> <target>Fil RSS</target> </trans-unit> <trans-unit id="3"> <source>Jobeet API</source> <target>API Jobeet</target> </trans-unit> <trans-unit id="4"> <source>Become an affiliate</source> <target>Devenir un affilié</target> </trans-unit> </body> </file> </xliff>
tip
Essendo XLIFF un formato standard, esistono diversi strumenti che facilitano il processo di traduzione. Open Language Tools è un progetto Java Open Source con un editor XLIFF integrato.
tip
Essendo XLIFF un formato basato su file, le stesse regole di precedenza e mescolanza che esistono per gli altri file di configurazione sono applicabili. I file i18n possono esistere in un progetto, in un'applicazione, o in un modulo, e il file più specifico sovrascrive le traduzioni trovate in quelli più globali.
Traduzioni con parametri
Il principio chiave dietro all'internazionalizzazione è la traduzione di intere frasi. Ma alcune frasi includono valori dinamici. In Jobeet, questo è il caso della homepage con il link "more...":
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <div class="more_jobs"> and <?php echo link_to($count, 'category', $category) ?> more... </div>
Il numero di lavori è una variabile che deve essere sostituita da un segnaposto per la traduzione:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <div class="more_jobs"> <?php echo __('and %count% more...', array('%count%' => link_to($count, 'category', $category))) ?> </div>
La stringa da tradurre è ora "and %count% more...", e il segnaposto %count%
sarà sostituito dal vero valore in tempo reale, grazie al valore fornito
dal secondo parametro dell'helper __()
.
Aggiungiamo la nuova stringa inserendo il tag trans-unit
nel file
messages.xml
, oppure usiamo il task i18n:extract
per aggiornare il
file automaticamente:
$ php symfony i18n:extract frontend fr --auto-save
Dopo aver fatto girare il task, apriamo il file XLIFF per aggiungere la traduzione Francese:
<trans-unit id="5"> <source>and %count% more...</source> <target>et %count% autres...</target> </trans-unit>
L'unico requisito della stringa tradotta è l'utilizzo del segnaposto
%count%
da qualche parte.
Alcune altre stringhe sono anche più complesse, perché coinvolgono dei plurali|Plurali (I18n). A seconda di alcuni numeri, le frasi cambiano, ma non necessariamente nello stesso modo per tutte le lingue. Alcune lingue, come il Polacco o il Russo, hanno delle regole di grammatica molto complesse per i plurali.
Nella pagina della categoria, è mostrato il numero di lavori nella categoria corrente:
<!-- apps/frontend/modules/category/templates/showSuccess.php --> <strong><?php echo count($pager) ?></strong> jobs in this category
Quando una frase ha diverse traduzioni a seconda di un numero, si dovrebbe
usare l'helper format_number_choice()
:
<?php echo format_number_choice( '[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category', array('%count%' => '<strong>'.count($pager).'</strong>'), count($pager) ) ?>
L'helper format_number_choice()
accetta tre parametri:
- La stringa da usare a seconda del numero
- Un array di sostituzioni per i segnaposto
- Il numero da usare per determinare quale testo usare
La stringa che descrive le diverse traduzioni da usare a seconda del numero è formattata nel modo seguente:
- Ogni possibilità è separata da una barra verticale (
|
) - Ogni stringa è composta da una serie seguita dalla traduzione
La serie|Serie (I18n) può descrivere qualsiasi serie di numeri:
[1,2]
: Accetta valori tra 1 e 2, estremi inclusi(1,2)
: Accetta valori tra 1 e 2, estremi esclusi{1,2,3,4}
: Accetta solo i valori elencati[-Inf,0)
: Accetta valori maggiori o uguali di meno infinito e strettamente minori di 0{n: n % 10 > 1 && n % 10 < 5}
: Accetta valori come 2, 3, 4, 22, 23, 24
La traduzione della stringa è simile a quella di altre stringhe:
<trans-unit id="6"> <source>[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category</source> <target>[0]Aucune annonce dans cette catégorie|[1]Une annonce dans cette catégorie|(1,+Inf]%count% annonces dans cette catégorie</target> </trans-unit>
Ora che si è in grado di internazionalizzare ogni tipo di stringa, aggiungere
le chiamate a __()
a tutti i template dell'applicazione di frontend. Non
internazionalizzeremo l'applicazione di backend.
Form|Form (I18n)
Le classi dei form contengono molte stringhe che hanno bisogno di essere tradotte, come le label, i messaggi di errore e i messaggi di aiuto. Tutte queste stringhe sono internazionalizzate automaticamente da symfony, quindi è sufficiente fornire le traduzioni nei file XLIFF.
note
Sfortunatamente, il task i18n:extract
non considera ancora le classi
dei form per cercare le stringhe non tradotte.
Oggetti Propel
Per il sito Jobeet, non internazionalizzeremo tutte le tabelle|Internazionalizzazione del Modello, perché non ha senso chiedere a chi inserisce un lavoro di tradurre|I18n (Modello) la propria inserzione in tutte le lingue disponibili. Ma la tabella delle categorie ha sicuramente bisogno di essere tradotta.
Il plugin Propel supporta nativamente le tabelle i18n. Per ogni tabella che contiene dati localizzati, occorre creare due tabelle: una per le colonne che sono indipendenti da i18n, l'altra per le colonne che devono essere internazionalizzate. Le due tabelle sono collegate da una relazione uno-a-molti.
Aggiorniamo di conseguenza il file schema.yml
|schema.yml
(I18n):
# config/schema.yml jobeet_category: _attributes: { isI18N: true, i18nTable: jobeet_category_i18n } id: ~ jobeet_category_i18n: id: { type: integer, required: true, primaryKey: true, foreignTable: jobeet_category, foreignReference: id } culture: { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true } name: { type: varchar(255), required: true } slug: { type: varchar(255), required: true }
La voce _attributes
definisce le opzioni per la tabella.
E aggiorniamo le fixture|Fixture (I18n) per le categorie:
# data/fixtures/010_categories.yml JobeetCategory: design: { } programming: { } manager: { } administrator: { } JobeetCategoryI18n: design_en: { id: design, culture: en, name: Design } programming_en: { id: programming, culture: en, name: Programming } manager_en: { id: manager, culture: en, name: Manager } administrator_en: { id: administrator, culture: en, name: Administrator } design_fr: { id: design, culture: fr, name: Design } programming_fr: { id: programming, culture: fr, name: Programmation } manager_fr: { id: manager, culture: fr, name: Manager } administrator_fr: { id: administrator, culture: fr, name: Administrateur }
Ricostruiamo il modello per creare gli stub delle classi i18n
:
$ php symfony propel:build --all --and-load --no-confirmation $ php symfony cc
Siccome le colonne name
e slug
sono state spostate nella tabella i18n,
spostiamo il metodo setName()
da JobeetCategory
a JobeetCategoryI18n
:
// lib/model/JobeetCategoryI18n.php public function setName($name) { parent::setName($name); $this->setSlug(Jobeet::slugify($name)); }
Dobbiamo anche aggiustare il metodo getForSlug()
in JobeetCategoryPeer
:
// lib/model/JobeetCategoryPeer.php static public function getForSlug($slug) { $criteria = new Criteria(); $criteria->addJoin(JobeetCategoryI18nPeer::ID, self::ID); $criteria->add(JobeetCategoryI18nPeer::CULTURE, 'en'); $criteria->add(JobeetCategoryI18nPeer::SLUG, $slug); return self::doSelectOne($criteria); }
tip
Siccome propel:build --all
rimuove tutte le tabelle e i dati dal
database,
non dimenticare di ricreare un utente per accedere al backend con il
task guard:create-user
. In alternativa, si può aggiungere un file
fixture in modo che sia aggiunto automaticamente.
Quando si costruisce il modello, symfony crea dei metodi proxy nell'oggetto
JobeetCategory
principale, per accedere comodamente alle colonne
i18n definite in JobeetCategoryI18n
:
$category = new JobeetCategory(); $category->setName('foo'); // imposta il nome per la cultura corrente $category->setName('foo', 'fr'); // imposta il nome per il Francese echo $category->getName(); // prende il nome per la cultura corrente echo $category->getName('fr'); // prende il nome per il Francese
tip
Per ridurre il numero di richieste al database|Perfomance, usare il metodo
doSelectWithI18n()
al posto del solito doSelect()
. Recupererà
l'oggetto principale e l'oggetto i18n in una sola richiesta.
$categories = JobeetCategoryPeer::doSelectWithI18n($c, $culture);
Siccome la rotta category
è legata alla classe del modello
JobeetCategory
e
poiché slug
è ora parte di JobeetCategoryI18n
,
la rotta non è in grado di recuperare automaticamente l'oggetto
Category
. Per aiutare il sistema delle rotte, creiamo un metodo
che si occuperà di recuperare l'oggetto:
// lib/model/JobeetCategoryPeer.php class JobeetCategoryPeer extends BaseJobeetCategoryPeer { static public function doSelectForSlug($parameters) { $criteria = new Criteria(); $criteria->addJoin(JobeetCategoryI18nPeer::ID, JobeetCategoryPeer::ID); $criteria->add(JobeetCategoryI18nPeer::CULTURE, $parameters['sf_culture']); $criteria->add(JobeetCategoryI18nPeer::SLUG, $parameters['slug']); return self::doSelectOne($criteria); } // ... }
Quindi, usiamo l'opzione method
|Opzione method
(Routing) per dire alla
rotta category
di usare il metodo doSelectForSlug()
per recuperare l'oggetto:
# apps/frontend/config/routing.yml category: url: /:sf_culture/category/:slug.:sf_format class: sfPropelRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object, method: doSelectForSlug } requirements: sf_format: (?:html|atom)
Occorre ricaricare le fixture per rigenerare gli slug corretti per le categorie:
$ php symfony propel:data-load
Ora la rotta category
è internazionalizzata e l'URL per una categoria
include lo slug tradotto della categoria:
/frontend_dev.php/fr/category/programmation /frontend_dev.php/en/category/programming
Admin Generator
Per il backend, vogliamo che le traduzioni Francese e Inglese siano modificate nello stesso form:
Si può inserire un form i18n|Form (Traduzione) usando il metodo embedI18N()
:
// lib/form/JobeetCategoryForm.class.php class JobeetCategoryForm extends BaseJobeetCategoryForm { public function configure() { unset($this['jobeet_category_affiliate_list']); $this->embedI18n(array('en', 'fr')); $this->widgetSchema->setLabel('en', 'English'); $this->widgetSchema->setLabel('fr', 'French'); } }
L'interfaccia dell'admin generator supporta nativamente l'internazionalizzazione.
È disponibile con traduzioni in oltre 20 lingue ed è piuttosto facile
aggiungerne di nuove o personalizzare quelle esistenti. Basta copiare il
file della lingua che si vuole personalizzare da symfony (le
traduzioni si possono trovare in
lib/vendor/symfony/lib/plugins/sfPropelPlugin/i18n/
)
nella cartella i18n
dell'applicazione. Siccome il file nella propria
applicazione sarà mescolato con quello di symfony, basta tenere
le stringhe modificate nel file dell'applicazione.
Si noterà che i file di traduzione dell'admin generator hanno nomi
come sf_admin.fr.xml
, invece di fr/messages.xml
. Di fatto,
messages
è il nome del catalogo predefinito usato da symfony e può
essere cambiato per consentire una migliore separazione tra parti
diverse della propria applicazione. Usando un catalogo diverso da
quello predefinito richiede di specificarlo nell'uso dell'helper __()
:
<?php echo __('About Jobeet', array(), 'jobeet') ?>
Nella chiamata a __()
qui sopra, symfony cercherà la stringa
"About Jobeet" nel catalogo jobeet
.
Test
Aggiustare i test|I18n (Test) è parte integrante della migrazione all'internazionalizzazione.
Innanzitutto, aggiorniamo le fixture dei test per le categorie, copiando
le fixture che abbiamo definito sopra in
test/fixtures/010_categories.yml
.
Ricostruiamo il modello per l'ambiente test
:
$ php symfony propel:build --all --and-load --no-confirmation --env=test
Ora possiamo lanciare tutti i test per verificare che girino bene:
$ php symfony test:all
note
Quando abbiamo sviluppato l'interfaccia di backend per Jobeet, non abbiamo scritto test funzionali. Ma se si crea un modulo con un comando di symfony, symfony genera anche dei test di base. Questi test possono essere tranquillamente rimossi.
Localizzazione
Template
Supportare diverse culture vuol dire anche supportare diversi modi di formattare date e numeri. In un template, diversi helper sono a disposizione per tenere in considerazione tutte queste differenze, a seconda della cultura dell'utente:
Nel gruppo di helper Date
:
Helper | Descrizione |
---|---|
format_date() |
Formatta una data |
format_datetime() |
Formatta una data con un orario (ore, minuti, secondi) |
time_ago_in_words_() |
Mostra il tempo trascorso tra una data e ora, a parole |
distance_of_time_in_words() |
Mostra il tempo trascorso tra due date, a parole |
format_daterange() |
Formatta un intervallo di date |
Nel gruppo di helper Number
:
Helper | Descrizione |
---|---|
format_number() |
Formatta un numero |
format_currency() |
Formatta una valuta |
Nel gruppo di helper I18N
:
Helper | Descrizione |
---|---|
format_country() |
Mostra il nome di un paese |
format_language() |
Mostra il nome di una lingua |
Form|Form (I18n)
Il framework dei form fornisce diversi widget|Widget (I18n) e validatori|Validatori (I18n)~ per dati localizzati:
sfWidgetFormI18nDate
sfWidgetFormI18nDateTime
sfWidgetFormI18nChoiceCurrency
sfWidgetFormI18nChoiceLanguage
sfValidatorI18nChoiceLanguage
sfValidatorI18nChoiceTimezone
A domani
L'internazionalizzazione e la localizzazione sono cittadini di prima classe in symfony. Fornire un sito localizzato ai propri utenti è molto facile, in quanto symfony fornisce tutti gli strumenti di base e inoltre dei task a linea di comando per accelerare il tutto.
Preparatevi per un tutorial veramente speciale domani, perché sposteremo molti file ed esploreremo un approccio diverso all'organizzazione di un progetto symfony.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.