Nel mio post precedente dedicato a MCAD abbiamo visto come tradurre la nostra applicazione in più lingue. Giocando con le property Localizable e Language delle WF, possiamo tradurre direttamente a design-time Text dei Button, dei GroupBox, degli Headers, delle WF stesse e così via. Tutto il (succulento) codice è incluso nella InitializeComponent() della WF, che invece di creare i controls impostandone le proprietà, in pratica le va a leggere direttamente dai files di risorsa generati automaticamente dall'IDE di VS.
Adesso quello che rimane da fare è localizzare tutta la parte che invece è inclusa nel codice, ovvero tutto quello che viene comunicato allo user attraverso MessageBox, per esempio, oppure cambiando ancora una volta le property dei controls che sono state inizializzate all'apertura dello stesso. Come facciamo? Vediamo subito, punto per punto!
La Study Guide di Lorenzo ci dà un aiuto sostanziale. In breve, questa volta bisogna creare manualmente i files di risorsa che ci servono, uno per ciascuna lingua. Quindi, se abbiamo aperto l'IDE di VS, andiamo nel Solution Explorer, clicchiamo con il pulsante dx, aggiungiamo un nuovo elemento al progetto di tipo "Assembly Resource File". Ci viene chiesto il nome: io nel mio caso l'ho chiamato semplicemente "Vocabulary", ricordandomi di aggiungere il suffisso che identifica la Culture che sto codificando. Quindi, supponendo di voler tradurre in italiano (IT) e in inglese (US), avrò due files:
- Vocabularyit-IT.resx
- Vocabularyen-US.resx
Facendo doppio-click su ciascuno dei files, l'IDE permette di inserire, modificare e cancellare stringhe all'interno dei nostri files di risorsa. Per ogni risorsa, i dati essenziali (correggetemi se sbaglio) sono:
- name (ne parleremo adesso)
- value (c'è bisogno di spiegarlo? )
Prendiamo in considerazione il file di risorsa "it-IT". Nel campo name inserite "EMPTY_STRING", nel campo value inserite "Il nome non può essere vuoto! Ricordati di inserire il nome di questa persona!".
Adesso passiamo all'altro file di risorsa "en-US". Nel campo name inserite "EMPTY_STRING", nel campo value inserite "Name cannot be empty! Please insert name of this person!".
Se avete codificato altre lingue, aprite il file di risorsa e traducete il campo value. Il campo name deve essere sempre "EMPTY_STRING".
Fate un bel click sul pulsante Salva dell'IDE.
A questo punto passiamo a ritoccare il nostro codice. Lo Study Guide comprende un paragrafo intitolato "Create resource-only assemblies": si svela i segreti per utilizzare le classe ResourceManager. A me non è bastato, da quelle righe non sono riuscito a capire come funziona. Alla fine ho risolto, e vi sto per dire come. La sintassi riportata dalla Study Guide è la seguente:
ResourceManager resMan = new ResourceManager(“Namespace.Resource”, Assembly.GetExecutingAssembly());
La classe ResourceManager legge il contenuto dei files di risorsa. Utilizza due metodi in particolare, il GetObject() e il GetString(): il primo per contenuti binari, il secondo per banali stringhe come nel nostro caso. Il costruttore della ResourceManager ha diversi overloading: quello che abbiamo usato noi richiede una string (baseName) e un assembly (assembly). Usando l'espressione Assembly.GetExecutingAssembly() facciamo riferimento all'assembly in esecuzione, in pratica il file eseguibile della nostra applicazione. Il vero mistero che mi ha tormentato è il valore da passare alla string baseName: stanco di provare a caso, ho usato Reflection per trovarlo. Ecco il codice del costruttore del mio frmMain:
public frmMain()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
System.Reflection.Assembly myAssembly = System.Reflection.Assembly.GetExecutingAssembly();
string cultureName = Thread.CurrentThread.CurrentUICulture.Name;
string[] names = myAssembly.GetManifestResourceNames();
foreach (string name in names)
Console.WriteLine(name);
resMan = new ResourceManager("MCAD_04.Vocabulary" + cultureName , myAssembly);
}
Qualche spiegazione è doverosa. Tramite Reflection ottengo un'istanza del mio Assembly corrente (myAssembly). Nella string cultureName salvo la Culture del thread corrente ("it-IT" o "en-US"). Nell'array names[] salvo l'elenco delle resources definite nell'assembly corrente (metodo GetManifestResourceNames della classe Assembly). Il ciclo for...each mi è servito per visualizzare tutti i files di risorsa contenuti nell'assembly: "MCAD_05.frmMain.resources", "MCAD_05.Vocabularyit-IT.resources" e "MCAD_05.Vocabularyen-US.resources". Ok, a questo punto capiamo quale string bisogna passare al costruttore di ResourceManager: mi aspettavo di poter scrivere:
resMan =
new ResourceManager(names[1] , myAssembly);
ma questa riga non viene compilata (nelle Additional Information dell'errore leggo: ResourceManager base name should not end in .resources). Alla fine, ho semplicemente scritto la riga riportata nel riquadro sopra, ovvero all'apertura dell'applicazione, istanzo il ResourceManager resMan usando come file di risorsa quello relativo al CurrentUICulture del thread corrente, che è stato impostato nel main(). Se nelle Regional Options ho settato Italian, uso il file "MCAD_05.Vocabularyit-IT"; se ho impostato English (United States), uso il file "MCAD_05.Vocabularyen-US". Niente di più facile (adesso che ho capito)!
A questo punto in tutti i punti del codice dove utilizzo una stringa, correggo il codice: la stringa non deve essere più scritta nel source-code, ma letta in tempo reale dal file di risorsa. Faccio un piccolo esempio: nei files di risorsa, abbiamo inserito una sola stringa, il cui name era "EMPTY_STRING". Come si deduce facilmente, il campo value contiene il messaggio di errore nel caso in cui lo user non abbia inserito, appunto, il nome della persona. Dove compare questa stringa nel codice? Diamo un'occhiata alla function validate(), ecco qui un piccolo snippet di codice:
// check for birthdate
msg = string.Empty;
if (this.txtBirthDate.Text.Length != 10)
// versione vecchia
//msg = "This is not a valid date! Please correct!";
// versione nuova
msg = resMan.GetString("NAME_EMPTY");
Ho riportato la versione vecchia (commentata) e la versione riveduta e corretta per la localizzazione. Come si può vedere, utilizza resMan (ResourceManager) per ottenere la stringa identificata da "EMPTY_STRING". Il gioco è fatto! Se la stringa non dovesse andare bene, è sufficiente modificare il file di risorsa, ricompilare e nulla di più. Una precisazione: quando eseguiamo il progetto, le risorse NON vengono prese dai files .resx (che, sorpresa sorpresa, sono XML), ma dai files di risorse che vengono inclusi nel file eseguibile finale. Quindi, per esempio, se vogliamo correggere una stringa, dobbiamo redistribuire l'eseguibile. Scomodo, vero? La soluzione secondo me è quella di usare assembly esterni (satellite assembly, ricordiamocelo!), come quelli che vengono usati nella localizzazione che abbiamo visto nel mio post precedente: se qualcosa non va bene, se la traduzione va raffinata, è sufficiente distribuire semplicemente la DLL relativa alla Culture che bisogna sostituire (in breve, myAssembly deve essere istanziato non con GetExecutingAssembly(), ma caricando un assembly esterno).
Ultima annotazione. In questo caso abbiamo usato una stringa semplice, una stringa da visualizzare in caso di errore. Supponiamo adesso di voler implementare il nostro meccanismo di localizzazione nella property Phrase della nostra classe Age. Questo è il codice attuale:
public string Phrase
{
get
{
string msg = "{0} is {1} years old!";
msg = String.Format(msg, this.name, this.GetAge());
return(msg);
}
}
Inizialmente, creiamo una string msg con due placeholder (termine che uso io, non è ufficiale ). Ricordano la mitica printf (e derivati) del mitico ANSI-C delle mie superiori. Il metodo statico Format della classe String sostituisce ogni placeholder con il suo corrispondente valore, prelevandolo dai parametri della chiamata. Il placeholder {0} viene sostituito con this.name, {1} con this.GetAget(). Non c'entra molto con la localizzazione, perchè l'ho trovato un metodo molto molto elegante per risolvere il problema delle frasi molto diverse da una lingua all'altra.
Torniamo a noi. Apriamo nuovamente i files di risorsa.
In quello italiano, creiamo una nuova risorsa chiamata "PROPERTY_PHRASE", il cui value è
"Quest'anno {0} compirà {1} anni!"
In quello inglese, creiamo una nuova risorsa chiamata "PROPERTY_PHRASE", il cui value è
"{0} is {1} years old!"
La property Phrase della classe diventa così:
public string Phrase
{
get
{
string msg = resMan.GetString("PROPERTY_PHRASE");
msg = String.Format(msg, this.name, this.GetAge());
return(msg);
}
}
Come prima, la stringa viene prelevata dal file di risorsa. Il resto è rimasto invariato. Compiliamo il codice e proviamo ad eseguirlo. Se avete l'OS in lingua italiana, l'applicazione parte in lingua italiana: se inserite un nome, la sua data di nascita e cliccate sul button Calcola età, nella ListBox comparirà:
"Quest'anno Mario Rossi compirà 29 anni!"
Cambiate nelle Regional Options la lingua, nel nostro caso English (United States) e fate la stessa prova. Questa volta la stringa che comparirà nella ListBox sarà:
"Mario Rossi is 29 years old!"
Come si vede, il nome della persona e la sua età sono in posti diversi all'interno della frase, ma con i placeholder abbiamo risolto brillantemente la questione. Grazie a Corrado per avermi illuminato sui forum di UGIdotNET!
Direi che è proprio una bella cosa, vero? Non mi è capitato spesso di avere a che fare con la localizzazione: quando ne ho avuto bisogno ho usato un database (perchè ne usavo già uno per l'applicazione stessa). Non so dire che sia meglio usare assembly o un database, però questo è il metodo descritto dalla Study Guide di Lorenzo, e a me basta ed avanza. Qualche suggerimento? A voi...
Nel frattempo, alla prossima...