last update: 29/ july / 2004
[Questo articolo analizza il procedimento di internazionalizzazione e quello di localizzazione di un programma dal punto di vista del programmatore ed è dedicato a programmatori C con almeno le conoscenze di base del linguaggio.]
Avevo già anticipato in un precedente articolo,
dedicato alla localizzazione dei programmi, i concetti di base
relativi a internazionalizzazione (i18n) e localizzazione (l10n).
Ritengo opportuno evitare di ripetere qui tali concetti di base e
passare direttamente ad analizzare il punto di vista del
programmatore.
Va premesso che il presente non vuole essere un trattato
esaustivo sull'i18n (non sarei in grado di arrivare a tanto) ma
una semplice introduzione per accompagnare un programmatore, a
digiuno di tali concetti, un po' più addentro
nell'argomento.
Ritengo importante ribadire l'utilità del processo di
globalizzazione dei programmi quale strumento per permettere agli
stessi di essere fruibili in un numero maggiore di diversi paesi
e quindi al maggior numero possibile di persone. Questa
possibilità è ancora più importante nel
mondo dell'open source dove raggiungere un ampio numero di
sviluppatori o di testers può rappresentare per un
programma la possibilità di sopravvivere.
A ciò si aggiunge la considerazione che molti meccanismi
vengono implementati automaticamente dalle librerie standard o
dai tools di programmazione e quindi, spesso, lo sforzo richiesto
al programmatore è solo quello di predisporre il programma
ad essere localizzato.
Sono comunque ben lontano dall'affermare che l'i18n sia semplice,
non a caso il capitolo 22 del manuale della libc, dedicato
all'argomento, si apre con una frase che, tradotta, suona
più o meno così:
"Una domanda molto comune nei newsgroup e mailing list
è 'Come posso fare <operazione qualsiasi di modifica a una stringa?'
I programmatori più coscienziosi fanno seguito alla domanda con '...e
come faccio a rendere tale codice portatile?'
Povero innocente programmatore non hai idea della
profondità dei guai nei quali ti stai gettando. Sarebbe
meglio per la tua salute mentale infilare il problema in un
cestino per la carta e dimenticartene..."
Ben lontano dal volervi spaventare ribadisco che cercherò di introdurvi all'argomento a piccole dosi per cui farete sempre in tempo ad abbandonare il tutto...andiamo quindi ad iniziare.
Le regole guida dell' i18n richiedono che i programmatori scrivano un programma in modo da evitare di codificare al suo interno informazioni o strutture che non siano poi in grado di passare un processo di l10n. Per facilitare tale operazione si ha a disposizione per ciascuna lingua una tabella di regole e costrutti detta locale atta a descrivere delle specifiche convenzioni culturali. Alcuni aspetti di tali convenzioni possono essere ad esempio:
Purtroppo nel tempo la definizione di locale è stata
oggetto di numerosi tentativi di standardizzazione che hanno
portato a vari metodi di codifica e che prevedono differenti
sistemi di locale come ad es. il POSIX locale per le convenzioni
culturali di base, l' ISO/IEC 14652 che è una estensione
del POSIX, l'X locale per le applicazioni Xwindow etc...
Le uniche locale che sono garantite essere sempre disponibili in
ogni sistema sono la "C" e la "POSIX" che garantiscono un
comportamento delle varie funzioni conforme alle specifiche
standard del linguaggio C.
LC_ALL | sovrascrive tutte le categorie locali | |
LC_COLLATE | per i confronti dei caratteri | |
LC_TYPE | classificazione dei caratteri | |
LC_MONETARY | informazioni legate alla valuta | |
LC_NUMERIC | formattazione dei numeri | |
LC_TIME | formattazione data e ora | |
LC_MESSAGES | messaggi |
LC_PAPER | per la gestione delle dimensioni della carta | |
LC_NAME | per la gestione dei nomi delle persone | |
LC_ADDRESS | per la formattazione degli indirizzi e dei codici postali | |
LC_TELEPHONE | per il formato dei numeri telefonici | |
LC_MEASUREMENT | per le unità di misura | |
LC_VERSIONS | per la gestione delle versioni della
locale (le informazioni sono disponibili tramite la funzione nl_langinfo() che vedremo poi). |
Il bello di tutto ciò è che lo standard C
implementa alcuni meccanismi di gestione dell'i18n in modo
automatico. Quindi prima di spaventarvi con altre nozioni
passerei ad un esempio pratico:
Come sopra enunciato i programmi ereditano le impostazioni locali
dalle variabili di ambiente, ma tali variabili non impongono
automaticamente la locale usata dalle funzioni di libreria. Ogni
programma si avvia con presettata la locale 'C'. Per utilizzare
la locale specificata dalle variabili d'ambiente si deve
utilizzare la funzione setlocale() come
vedremo...
Utilizzando un editor a vostra scelta create quindi il file esempio1.C e copiateci il codice seguente:
/*esempio1.c - out sample*/
#include <stdio.h>
#include <locale.h>
int main (int argc, char *argv[]) {
float price=12.5;
/*selezione della lingua dall'ambiente */
/*utilizzare il comando export LANG=xx_XX */
setlocale(LC_ALL,"");
printf ("formato numerico locale: %f \n",price);
return 0;
}
compiliamo
$ gcc esempio1.c -o esempio1
settiamo la variabile d'ambiente relativa al linguaggio
scegliendo l'inglese USA
$ export LANG="en_US"
ed eseguiamo il programma
$ esempio1 formato numerico locale: 12.500000
L'output prodotto è il numero 12,5 in formato numerico
anglosassone con il . (punto) come separatore decimale.
Ora selezionamo il formato italiano:
$ export LANG="it_IT" $ esempio1 formato numerico locale: 12,500000
L'output prodotto è (ovviamente) il numero 12,5 in
formato italiano con la , (virgola) come separatore decimale.
Faccio notare l'uso di LC_ALL all'interno della funzione
setlocale() per settare la locale per tutte le categorie e le
funzioni associate; specificando un parametro differente, al
posto di LC_ALL, l'impostazione viene eseguita solo per la
categoria di funzioni relative. Nell'esempio avremmo potuto
utilizzare la seguente forma
setlocale(LC_NUMERIC,"")
ottenendo lo stesso risultato influenzando solo il comportamento
delle funzioni legate al trattamento numerico.
Chiaramente non è necessario selezionare interattivamente dalla shell la lingua dell'output desiderato ma è possibile selezionarla da programma, indipendentemente dalle impostazioni delle variabili d'ambiente, come nell'esempio che segue:
/*esempio2.c - locale sample*/
#include <stdio.h>
#include <locale.h>
int main (int argc, char *argv[]) {
float price=12.5;
//selezione del locale USA da programma
setlocale(LC_ALL,"us_US");
printf("formato numerico US: %f\n",price);
//selezione del locale Italia da programma
setlocale(LC_ALL,"it_IT");
printf("Formato numerico It: %f\n",price);
return 0;
}
passiamo a compilazione ed esecuzione:
$ gcc esempio2.c -o esempio2
$ esempio2 formato numerico US: 12.500000 Formato numerico It: 12,500000Cambiando la locale da codice è comunque opportuno salvare la locale originale del sistema in una variabile in modo da poterla ripristinare. Ecco uno spezzone di codice che ottiene lo scopo:
..... char *p, *save_locale; /*leggo la variabile d'ambiente*/ p=setlocale(LC_ALL,""); /*la salvo*/ save_locale = (char *)malloc(strlen(p) +1); strcpy(save_locale, p); ...... /*eventuali modifiche*/ ......... /*ripristino*/ p = setlocale(LC_ALL, save_locale); free(save_locale);in questo modo abbiamo riportato il sistema alle condizioni iniziali.
A questo punto si potrebbe pensare che se tutto funziona automaticamente non c'è in realtà molto lavoro da fare. Per non deludere nessuno complichiamo un po' le cose con un programmino che mostra il valore dell'esempio precedente in formato valutario.
/* esempio3.c */
#include <stdio.h>
#include <libintl.h>
#include <locale.h>
int main (int argc, char *argv[]) {
float price=12.5;
/*setto la lingua inglese*/
setlocale(LC_ALL,"en_US");
/*estraggo il simbolo della valuta*/
const char *sig=localeconv()->currency_symbol;
printf("formato monetario US: %s %f\n",sig,price);
/* come sopra ma per formato italiano*/
setlocale(LC_ALL,"it_IT");
const char *sig2= localeconv()->currency_symbol;
printf("formato monetario IT: %s %f\n",sig2,price);
return 0;
}
passiamo ancora alla compilazione e all'output
$ gcc esempio3.c -o esempio3 $ esempio3 formato monetario USA: $ 12.500000 formato monetario Italia: L. 12,500000
Come si vede la funzione localeconv() ha prelevato i simboli corretti dalla locale prescelta e nel caso della lira ha anche inserito il punto del simbolo. Nel caso qualcuno si stia chiedendo perchè il formato italiano mostra la Lira come simbolo valutario al posto dell' Euro il motivo è che il programma è stato eseguito in una vecchia distribuzione di GNU/Linux nella quale le tabelle dei simboli non sono state aggiornate.
L'esempio appena mostrato però non tiene conto del fatto che non in tutti i paesi il simbolo valutario viene mostrato prima del valore. Un esempio più completo dovrebbe infatti tenere conto di questo come segue:
/* esempio4.c */
#include <libintl.h>
#include <locale.h>
int main (int argc, char *argv[]) {
float price=12.5;
/*setto la locale per la lingua tedesca*/
setlocale(LC_ALL,"de_DE");
printf("formato monetario Germania: ");
/*verifico se il simbolo valutario precede il valore ed eventualmente lo stampo*/
printf("%s ", (((int) localeconv()->p_cs_precedes)? localeconv()->currency_symbol:""));
/*stampo il valore */
printf("%f",price);
/*verifico se il simbolo valutario segue il valore ed eventualmente lo stampo*/
printf("%s ", (!((int) localeconv()->p_cs_precedes)? localeconv()->currency_symbol:""));
printf("\n");
/*come sopra ma per la lingua italiana*/
setlocale(LC_ALL,"it_IT");
printf("formato monetario Italia: " );
printf("%s ", (((int) localeconv()->p_cs_precedes)? localeconv()->currency_symbol:""));
printf("%f",price);
printf("%s ", (!((int) localeconv()->p_cs_precedes)? localeconv()->currency_symbol:""));
printf("\n");
return 0;
}
ovvero abbiamo verificato se il simbolo precede o segue il
valore ed agito di conseguenza.
Possiamo verificare il tutto nell'output prodotto:
$ esempio4 formato monetario Germania: 12,500000DM formato monetario Italia: L.12,500000
Non abbiamo tenuto ancora conto degli spazi che precedono o
seguono il simbolo ed altro ma penso che comunque i concetti
siano chiari.
Come si è visto la funzione localeconv() può
ritornare un buon numero di informazioni tramite la struttura
lconv da essa restituita. Il modo migliore per
verificare quali esse possano essere è leggere
direttamente il file include locale.h relativo alla libreria
locale. Questo file è oltretutto commentato in maniera
molto chiara e ritengo inutile farlo a mia volta.
Se siete confusi provvedo subito a complicare le cose: vista
la complessità nell'utilizzo delle funzioni standard del C
gli autori degli standard X/Open hanno deciso di venire in nostro
aiuto con una funzione atta a maneggiare direttamente i formati
monetari (giusto per rimanere in tema) ma non solo. Tale funzione
infatti può accedere a tutti i singoli elementi delle tabelle
locale.
La funzione in questione è
ssize_t strfmon(char *s, size_t max, const char
*format,...); ed è contenuta in monetary.h
un esempio di codice congruente con quelli già presentati
potrebbe essere il presente:
/* esempio6.c */
#include <stdio.h>
#include <libintl.h>
#include <locale.h>
#include <monetary.h>
int main (int argc, char *argv[]) {
double price=12.5;
char buf[BUFSIZ];
setlocale(LC_ALL,"de_DE");
printf("formato monetario Germania: ");
if (strfmon(buf,sizeof(buf),"%i",price) > 0 ) printf("%s \n ",buf);
return 0;
}
Eseguendo il programma si ottiene:
$./esempio6 formato monetario Germania: 12,50 EUR
Come si vede le cose sono enormemente semplificate in quanto la funzione produce già un output correttamente formattato. Confrontando i due metodi la funzione strfmon() sembra essere nettamente vincente per semplicità d'uso ma bisogna tenere presente che la localeconv() ha una maggiore portabilità facendo parte delle librerie standard del C.
In maniera analoga, a quanto visto per produrre un corretto
output numerico, possiamo verificare quanto sia importante usare
le corrette funzioni per la validazione dell'input quando esso
comprenda delle lettere come nell'esempio che segue:
/* esempio7.c */
#include <libintl.h>
#include <locale.h>
#include <ctype.h>
int main (int argc, char *argv[]) {
/*vengono settate tre variabili diverse, solo per comodità*/
/*di lettura del programma, contenenti le lettere: a,b,à */
char first[]="a";
char second[]="b";
char third[]="à";
/*creo un buffer per l'output*/
char *a=0;
setlocale(LC_ALL,"en_EN");
printf("In Inghilterra: \n" );
a= isalpha((int)first[0])?" è una lettera":" non è una lettera";
printf("[%s]%s \n",first,a);
a= isalpha((int)second[0])?" è una lettera":" non è una lettera";
printf("[%s]%s \n",second,a);
a= isalpha((int)third[0])?" è una lettera":" non è una lettera";
printf("[%s]%s \n",third,a);
setlocale(LC_ALL,"it_IT");
printf("In Italia: \n" );
a= isalpha((int)first[0])?" è una lettera":" non è una lettera";
printf("[%s]%s \n",first,a);
a= isalpha((int)second[0])?" è una lettera":" non è una lettera";
printf("[%s]%s \n",second,a);
a= isalpha((int)third[0])?" è una lettera":" non è una lettera";
printf("[%s]%s \n",third,a);
return 0;
}
il programma produce il seguente output:
In Inghilterra: [a] è una lettera [b] è una lettera [à] non è una lettera In Italia: [a] è una lettera [b] è una lettera [à] è una letteraRagioniamo un attimo sull'outpt: la locale "C" che viene usata per default dalle funzioni avrebbe dato lo stesso risultato del primo caso ovvero non avrebbe riconosciuto la à accentata come una lettera. Se l'esempio fosse stato una routine per la validazione dell'input che doveva accettare i soli caratteri alfabetici, in assenza dell'utilizzo della tabella locale, il programma avrebbe lavorato correttamente in Inghilterra ma non in Italia andando a scartare un input valido. Analoghe attenzioni si dovranno avere nell'ordinamento dei caratteri con la funzione strcoll() in quanto ordinare le stringhe in base al valore ASCII dei caratteri che la compongono porterà problemi con lettere accentate e similari che verranno ordinate in modo improprio.
La traduzione dei messaggi di testo è la parte, del
processo di globalizzazione, che viene percepita per prima
dall'utente finale ed è anche quella più complessa
da gestire.
Fondamentalmente non esiste alcuno standard POSIX per la
traduzione dei messaggi ma ci sono due standards de-facto
uno da X/Open Group e l'altro proposto inizialmente da Sun
ed ora utilizzato anche nei sistemi GNU. Entrambi si basano
sullo stesso principio ovvero sulla costruzione di una tabella
di riferimento tramite la quale effettuare la traduzione
dinamicamente agganciando a run-time la voce tradotta.
Lo standard X/Open si basa sulla funzione catgets() che funziona
sul principio di associare ad ogni frase da tradurre un
identificativo numerico in modo da potere attraverso esso recuperare
l'equivalente frase tradotta dal file della traduzione.
Per indicare alla funzione catgets() dove è situato il
file contenente i messaggi tradotti viene utilizzata funzione
catopen() la quale, a sua volta, si basa sulla variabile
d'ambiente NLSPATH.
Non vorrei dilungarmi eccessivamente su questa funzione in
quanto in ambiente GNU/Linux sembra essere meno usata, rispetto alla
funzione gettext(), proprio per la difficoltà di manutenzione dei files
di traduzione e delle tabelle di appoggio.
La Sun Microsystems, a suo tempo, attraverso l'Uniforum group, ha
cercato di proporre un approccio differente per la traduzione dei
messaggi proponendo la funzione gettext().
La funzione gettext() pur non essendo mai stata ratificata come
standard lo è divenuta di fatto in quanto a parere di
molti risulta più semplice da usare. A differenza della
funzione catgets essa non necessita di un identificatore per la
frase da tradurre ma utilizza la frase stessa come identificatore
per la ricerca. La funzione viene definita nell'header
libintl.h e la sua definizione esatta è:
char * gettext (const char *msgid)
char * textdomain (const char *domainname)questa funzione definisce il dominio di default per tutte le chiamate successive di gettext()
char * bindtextdomain (const char *domainname, const char *dirname)permette di specficare la directory ce contiene il file di messaggi tradotti per il dominio indicato (in realtà si tratta dell'indirizzo della gerarchia di directories nelle quali vanno salvati i files tradotti). Bisogna tenere conto che il percorso indicato può anche essere relativo e cautelarsi avendo cura che il programma non utilizzi la funzione
chdir
per cambiare la directory di lavoro corrente, nel quale caso è
opportuno che venga utilizzato per bindtxtdomain() un percorso assoluto. Vediamo un esempio classico...
/*ciao.c*/
#include <locale.h>
#define PACKAGE "ciao"
#define LOCALEDIR "."
int main ()
{
setlocale (LC_ALL, "");
/*definisco il dominio (percorsi e nome file) */
/*per la funzione gettext() */
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);
/*testo l'output in inglese ed in italiano*/
/*scegliendo esplicitamente la lingua */
setlocale (LC_ALL, "en_US");
printf (gettext ("Hello world\n"));
setlocale (LC_ALL, "it_IT");
printf (gettext ("Hello world\n"));
}
Per la creazione del file delle traduzioni vi rimando al precedente articolo,
ricordandovi che il file oggetto va posto in una struttura di directory riferita
a quella definita dal programma nella macro LOCALEDIR.
la struttura sarà codice lingua/LC_MESSAGES.$./ciao Hello world Ciao mondoTenete comunque presente che in un normale programma non viene scelta da programma la lingua di output ma viene prodotto l'output in base alla lingua corrente di sistema.
Vi sono delle raccomandazioni essenziali contenute nel manuale di gettext()
ove si consiglia di:
... strcpy (s, gettext("The red ")); strcat (s, gettext(" car")); strcat (s, gettext(" is on ")); strcat (s, gettext(" black")); strcat (s, gettext(" road.")); ...Una volta stampata la stringa "s", in inglese, verrebbe visualizzato: "The red car is on black road.",
char * ngettext (const char *msgid1, const char *msgid2,
unsigned long int n)
RFC 1766 - Tags for the identification of Languages
Secure Programming for Linux and Unix HOWTO - capitolo
5.8
Programming for Internationalization FAQ
Thai
Locale
Riferimenti
alla funzione catgets
Manuale di gettext
Home page di
Kbabel
The C++ Programming Language (3rd Edition) - Appendix D by Bjarne Stroustrup