Localizzazione in Java tramite lo standard TMX
Nicola Asuni, 2004-10-14 / 2006-06-04Introduzione
Uno degli aspetti fondamentali dell'internazionalizzazione consiste nel separare dal nucleo del codice sorgente i testi, le etichette, i messaggi ed altre informazioni sensibili alla localizzazione. Questo consente di mantenere la stessa base di codice sorgente per tutte le versioni del prodotto nelle diverse lingue e facilita il processo di traduzione perchè tutte le risorse sensibili al contesto locale sono ben identificate ed isolate.
Fin dalla JDK 1.1 Java offre un vasto supporto all'internazionalizzazione (i18n) attraverso una molteplicità di strumenti che consentono diversi approcci. In quest'ottica rivestono una particolare importanza il supporto all'Unicode 2.0, gli ambienti multilingua e la localizzazione degli oggetti, giusto per citarne alcuni.
Tuttavia questi strumenti possono risultare insufficienti quando il nostro mercato di riferimento è quello globale in cui i costi di traduzione e manutenzione dei testi (che comprendono le etichette, i messaggi, gli elementi di menù, ...) possono assumere dimensioni consistenti.
In quest'ottica, l'adozione dello standard
TMX consente di introdurre nella gestione dei testi e delle traduzioni i concetti di riuso, aumento della consistenza, riduzione della durata del ciclo produttivo e riduzione dei costi.
TMX - Translation Memory eXchange
http://www.lisa.org/tmx/TMX è un open standard che utilizza l'XML per l'archiviazione e l'interscambio di
memorie di traduzione (
TM - Translation Memory) create attraverso l'ausilio di strumenti software di traduzione e localizzazione (CAT - Computer Aided Translation).
TMX è il risultato di un'iniziativa presa da uno degli Special Interest Group di
LISA conosciuto come OSCAR (Open Standards for Container/Content Allowing Re-use).
Lo scopo del TMX è quello di offrire un sistema neutrale per lo scambio di dati tra diversi sistemi di traduzione che minimizzi o elimini la perdita di dati critici.
Il formato TMX è supportato dalla maggior parte dei software di traduzione.
Le specifiche dello standard TMX sono liberamente disponibili nel sito
http://www.lisa.org/tmx/ dove potrete trovare numerosi riferimenti, documenti, articoli e strumenti software.
Esempio di codice TMX
sample_tmx.xml
<?xml version="1.0" ?>
<tmx version="1.4">
<header
creationtool="XYZTool"
creationtoolversion="1.01-023"
datatype="PlainText"
segtype="sentence"
adminlang="en-us"
srclang="EN"
o-tmf="ABCTransMem">
</header>
<body>
<tu tuid="hello" datatype="plaintext">
<tuv xml:lang="en">
<seg>hello</seg>
</tuv>
<tuv xml:lang="it">
<seg>ciao</seg>
</tuv>
</tu>
<tu tuid="world" datatype="plaintext">
<tuv xml:lang="en">
<seg>world</seg>
</tuv>
<tuv xml:lang="it">
<seg>mondo</seg>
</tuv>
</tu>
</body>
</tmx>
dove:
- tu: translation unit, unità padre di ogni elemento da tradurre, può contenere un identificatore univoco (tuid).
- tuv: translation unit variant, unità contenente il codice di lingua della traduzione (xml:lang).
- seg: segment, contiene la traduzione.
TM - Translation Memory
http://www.opentag.com/tm.htmTranslation Memories (TM), conosciute anche come
Translation Databases, sono essenzialmente costituite da un database (archivio) dove le frasi del testo di riferimento sono associate alle corrispondenti traduzioni in una o più lingue.
L'insieme della frase di riferimento con le sue traduzioni è detto
translation memory unit.
Le applicazioni che fanno uso delle TM, sono strumenti di ausilio alla traduzione linguistica, progettate per migliorare la qualità e l'efficenza del processo di traduzione umana e non per sostituirlo.
Ogni nuova frase inserita attraverso l'applicazione TM verrà cercata tra le frasi di riferimento nel database e ne verrà calcolato un valore di corrispondenza.
Quando il valore di corrispondenza sarà del 100% (exact match) la traduzione presente nel database verrà direttamente inserita nel testo da tradurre. Quando il valore di corrispondenza sarà inferiore al 100% ma superiore ad una certa soglia (fuzzy match), la frase tradotta verrà inserita come traduzione proposta che dovrà essere verificata ed eventualmente corretta da traduttore umano. Le frasi con un margine di corrispondenza sotto il margine di soglia andranno tradotte manualmente per intero. Le nuove frasi inserite verranno registrate nel database per esser utilizzate per future traduzioni.
Diverse aziende offrono complessi prodotti commerciali in questo settore.
LISA - Localization Industry Standards Association
http://www.lisa.orgFondata nel 1990, LISA è la principale organizzazione mondiale no-profit per il GILT (Globalizzazione, Internazionalizzazione, Localizzazione e Traduzione). LISA si rivolge ad individui, aziende, associazioni ed organizzazioni per gli standard, che si occupano di lingue e tecnologie per le lingue.
Oltre 400 aziende leader nel settore IT, service providers e professionisti che rappresentano le grandi aziende con mercati internazionali, hanno contribuito a stabilire le linee guida di LISA basate sul
best practice e gli standard tecnologici per le lingue a supporto della globalizzazione.
LISA mette in contatto fra loro numerosi soggetti che contribuiscono al processo di internazionalizzazione delle aziende e che comprendono: consumatori, governi, organizzazioni per gli standard, istituti di ricerca e consulenza, fornitori di servizi e di tecnologie per la taduzione e localizzazione.
LISA offre servizi sotto forma di iniziative per la standardizzazione, Special Interest Groups, conferenze e programmi di apprendimento di supporto GILT alle imprese.
I soci ed i gruppi affiliati a LISA comprendono l'International Organization for Standardization (ISO Liaison Category A Members of TC 37 and TC 46), World Bank, OASIS, IDEAlliance, AIIM, Advisory Council (TAC), Fort-Ross, €TTEC, Japan Technical Communicators Association, Society of Automotive Engineers (SAE), European Union, Canadian Translation Bureau, TermNet, American Translators Association (ATA), IWIPS, Fédération Internationale des Traducteurs (FIT), Termium, JETRO, Institute of Translating and Interpreting (ITI), Unicode Consortium, OpenI18N, professionisti ed organizzazioni per il commercio.
Tra i membri ed i co-fondatori di LISA spiccano alcune tra le aziende più grandi e conosciute a livello mondiale: Adobe, Avaya, Cisco Systems, CLS Communication, EMC, Hewlett Packard, IBM, Innodata Isogen, Fuji Xerox, Microsoft, Oracle, Nokia, Logitech, SAP, Siebel Systems, Standard Chartered Bank, FileNet, LionBridge Technologies, Lucent, Sun Microsystems, WH&P, PeopleSoft, Philips Medical Systems, Rockwell Automation, The RWS Group, Xerox Corporation and Canon Research.
Bridge Java per TMX
Attraverso la
java.util.ResourceBundle class, Java dispone di una soluzione pratica per la localizzazione. È infatti possibile estrarre gli elementi testuali dal codice sorgente originale ed isolarli in un componente ResourceBundle quale ad esempio la classe ListResourceBundle o un file di proprietà.
Questa soluzione offre dei sicuri vantaggi al programmatore ma può risultare piuttosto complessa dal punto di vista del traduttore, sopratutto in termini di riusabilità della traduzione.
Un eccellente alternativa consiste nell'archiviazione delle risorse testuali nel formato di interscambio
TMX (file XML) che consente ai traduttori di importare ed esportare le traduzioni da o verso i loro strumenti di traduzione (ne esistono diversi in commercio compatibili con TMX) in maniera totalmente indipendente dal linguaggio di programmazione utilizzato.
Così come suggerito dall'articolo
Use XML as a Java Localization Solution di Masaki Itagaki, la soluzione ottimale per implementare lo standard TMX nelle applicazioni Java, consiste nell'estendere la classe ResourceBundle in modo che possa leggere direttamente i dati da file XML conformi allo standard TMX (
classe Java ==> file TMX <== programma di traduzione).
Cio consente di sfruttare tutti i vantaggi offerti dalla classe ResourceBundle e di rendere più semplice il porting verso TMX delle applicazioni esistenti.
Gli svantaggi offerti da questa tecnica sono principalmente dovuti al tempo ed alla memoria necessaria per il caricamento dell'intero file TMX.
Al fine di semplificare l'esposizione, prenderemo in considerazione solo quegli elementi TMX necessari a tradurre un testo semplice (vedi
sample_tmx.xml):
- tu: translation unit, unità padre di ogni elemento da tradurre, può contenere un identificatore univoco (tuid).
- tuv: translation unit variant, unità contenente il codice di lingua della traduzione (xml:lang).
- seg: segment, contiene la traduzione.
per esempio:
<tu tuid="hello" datatype="plaintext">
<tuv xml:lang="en">
<seg>hello</seg>
</tuv>
<tuv xml:lang="it">
<seg>ciao</seg>
</tuv>
</tu>
La classe TMXResourceBundle.java
Istanziare la classe
TMXResourceBundle è come istanziare la classe
PropertyResourceBundle.
Attraverso il costruttore specifichiamo il nome ed il percorso del file in formato TMX dove sono contenute le traduzioni ed il codice ISO della lingua di cui vogliamo estrarre le traduzioni.
Quando la classe è istanziata, il metodo
parseXmlFile che fa uso del parser XML di Sun (
javax.xml.parsers.DocumentBuilder.parse), provvede a caricare i dati TMX in un oggetto di tipo
org.w3c.dom.Document (DOM - Document Object Model).
A questo punto i nodi del documento vengono analizzati e le coppie chiave-valore vengono aggiunte ad
hashcontents (un oggetto di tipo
java.util.Hashtable). Le coppie chiave-valore sono costituite rispettivamente dall'attributo
tuid dell'elemento
tu e dal valore del nodo
seg contenuto all'interno del nodo
tuv in cui il valore dell'attributo
xml:lang è identico a quello della lingua specificata.
L'estensione della classe ResourceBundle richiede la ridefinizione (overriding) dei metodi astratti
handleGetObject e
getKeys così da poter ricavare l'elemento associato ad una determinata chiave attraverso i metodi ereditati da
ResourceBundle:
getObject(String key),
getString(String key),
getStringArray(String key).
L'overload
getString(String key, String def) del metodo
getString(String key) di
ResourceBundle consente di ottenere la stringa associata ad una determinata chiave o un valore di default in caso di errore.
Codice Sorgente
(
download the full project from Sourceforge).
package com.tecnick.tmxjavabridge;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Vector;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* <p>
* Reads resource text data directly from a TMX (XML) file.
* </p>
* <p>
* First, the TMXResourceBundle class instantiates itself with two parameters: a
* TMX file name and a target language name. Then, using a DOM parser, it reads
* all of a translation unit's properties for the key information and specified
* language data and populates a hashtable with them.
* </p>
* <p>
* <b>TMX info: </b> http://www.lisa.org/tmx/
* </p>
*
* <h4>Implementation notes</h4>
* <p>
* You instantiate the TMXResourceBundle class in a program to read data from a
* TMX file. Once the class is instantiated, it reads all the data in a TMX file
* and loads into a DOM tree. Then it populates a hashtable so the
* handleGetObject() method can be called to find text information based on a
* key just as a standard ResourceBundle class does. <br>
* Instantiating the TMXResouceBundle class is the same as instantiating the
* PropertyResourceBundle class. First you obtain a system language code (e.g.:
* from a locale's information). In TMX the value of the attribute must be one
* of the ISO language identifiers (a two- or three-letter code) or one of the
* standard locale identifiers (a two- or three-letter language code, a dash,
* and a two-letter region code).
* </p>
*
* Copyright (c) 2004-2006 Tecnick.com S.r.l (www.tecnick.com) Via Ugo Foscolo
* n.19 - 09045 Quartu Sant'Elena (CA) - ITALY www.tecnick.com -
* info@tecnick.com <br/> Project homepage: <a
* href="http://tmxjavabridge.sourceforge.net"
* target="_blank">http://tmxjavabridge.sourceforge.net</a><br/> License:
* http://www.gnu.org/copyleft/lesser.html LGPL
*
* @author Nicola Asuni [www.tecnick.com].
* @version 1.1.008
*/
public class TMXResourceBundle extends ResourceBundle implements Serializable {
/**
* Serial Version UID
*/
private static final long serialVersionUID = -2098421084432070017L;
/**
* The hastable that will contain data loaded from XML
*/
protected Hashtable hashcontents = null;
/**
* Number of translation units (tu) items
*/
protected int numberOfItems = 0;
/**
* Vector to store tu items keys
*/
protected Vector vectOfItems;
/**
* TMX to Hashtable conversion. Reads XML and store data in HashTable.
*
* @param xmlfile
* the TMX (XML) file to read, supports also URI resources or JAR
* resources
* @param language
* ISO language identifier (a two- or three-letter code)
*/
public TMXResourceBundle(String xmlfile, String language) {
this(xmlfile, language, "");
}
/**
* Copy object data to this object.
* @param obj object to copy.
*/
private void copyTMXResourceBundle(TMXResourceBundle obj) {
this.hashcontents = obj.hashcontents;
this.numberOfItems = obj.numberOfItems;
this.vectOfItems = obj.vectOfItems;
}
/**
* TMX to Hashtable conversion. Reads XML and store data in HashTable. NOTE:
* you must manually delete the cachefile to refresh its content.
*
* @param xmlfile
* the TMX (XML) file to read, supports also URI resources or JAR
* resources
* @param language
* ISO language identifier (a two- or three-letter code)
* @param cachefile
* name of the file used to store cache data for the specified
* language
*/
public TMXResourceBundle(String xmlfile, String language, String cachefile) {
// try to get data from cachefile (if any)
if (cachefile.length() > 0) {
try {
FileInputStream fis = new FileInputStream(cachefile);
ObjectInputStream in = new ObjectInputStream(fis);
copyTMXResourceBundle((TMXResourceBundle)in.readObject());
in.close();
return;
} catch (Exception e) {
System.err.println("Exception:" + e);
}
}
String temp_key = null; // store hashtable key names
String temp_value = null; // store hashtable values
NamedNodeMap temp_list = null; // list of <tu> attributes
Attr temp_attr = null; // <tu> attribute
NodeList listOfTUVs = null; // list of <tuv> elements
NodeList listOfSEG = null; // list of <seg> elements
Element SEGElements = null; // <seg> element
int numberOfTUVs = 0; // number of <tuv> elements
// Create Document with parser
Document document = parseXmlFile(xmlfile, false);
// handle document error
if (document == null) {
hashcontents = new Hashtable(); // initialize a void hashtable
return;
}
// Make a list of Term Units and count the number of items
NodeList listOfTermUnits = document.getElementsByTagName("tu");
numberOfItems = listOfTermUnits.getLength();
// set tu keys vector size
vectOfItems = new Vector(numberOfItems);
// set hash size
hashcontents = new Hashtable(numberOfItems);
for (int i = 0; i < numberOfItems; i++) {
temp_value = null;
// set a key
temp_list = listOfTermUnits.item(i).getAttributes();
temp_attr = (Attr) temp_list.getNamedItem("tuid");
temp_key = temp_attr.getValue();
vectOfItems.add(temp_key); // store key on vector
// get a value
// Make a TUV list => "listOfTUVs"
Node TUVs = listOfTermUnits.item(i);
if (TUVs.getNodeType() == Node.ELEMENT_NODE) {
Element TUVElements = (Element) TUVs;
listOfTUVs = TUVElements.getElementsByTagName("tuv");
numberOfTUVs = listOfTUVs.getLength();
}
// Check each TUV. If it's a specified lang, then get a SEG value
for (int j = 0; j < numberOfTUVs; j++) {
temp_list = listOfTUVs.item(j).getAttributes();
temp_attr = (Attr) temp_list.getNamedItem("xml:lang");
if (temp_attr.getValue().equalsIgnoreCase(language)) {
// -- Get a SEG value
SEGElements = (Element) listOfTUVs.item(j);
listOfSEG = SEGElements.getElementsByTagName("seg");
try {
temp_value = listOfSEG.item(0).getFirstChild()
.getNodeValue();
} catch (Exception e) {
// in case of error print error message and set value to
// void string
System.err.println(this.getClass().getName() + "(\""
+ xmlfile + "\", \"" + language + "\") :: "
+ "Void <seg> value on <tu tuid=\"" + temp_key
+ "\"> key");
temp_value = "";
}
}
}
// Populate hashtable
if ((temp_key != null) && (temp_value != null)) {
hashcontents.put(temp_key, temp_value);
}
} // for loop
// try to save this object on cache file
if (cachefile.length() > 0) {
try {
FileOutputStream fos = new FileOutputStream(cachefile);
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(this);
out.close();
} catch (Exception e) {
System.err.println("Exception:" + e);
}
}
}
/**
* Parses an XML file and returns a DOM document.
*
* @param filename
* the name of XML file
* @param validating
* If true, the contents is validated against the DTD specified
* in the file.
* @return the parsed document
*/
public Document parseXmlFile(String filename, boolean validating) {
Document doc = null;
DocumentBuilderFactory factory = null;
// Create a builder factory
try {
factory = DocumentBuilderFactory.newInstance();
} catch (FactoryConfigurationError e) {
System.err.println(e);
return null;
}
factory.setValidating(validating);
// Create the builder and parse the file
try {
try {
// try to get the file from jar
InputStream instream = getClass().getResourceAsStream(filename);
doc = factory.newDocumentBuilder().parse(instream);
} catch (Exception ejar) {
try {
// try to get the file as external URI
doc = factory.newDocumentBuilder().parse(filename);
} catch (IOException euri) {
try {
// try to get the file as local filename
doc = factory.newDocumentBuilder().parse(new File(filename));
} catch (IOException efile) {
try {
// try to resolve the path as relative to local
// class folder
String[] classPath = System.getProperties().getProperty("java.class.path", ".").split(";");
String newpath = classPath[0] + "/" + filename;
doc = factory.newDocumentBuilder().parse(
new File(newpath));
} catch (IOException epath) {
// unable to get the input file
System.err.println("IOException:" + epath);
}
}
}
}
} catch (ParserConfigurationException e) {
System.err.println("[" + filename + "] ParserConfigurationException:" + e);
} catch (SAXException e) {
System.err.println("[" + filename + "] SAXException:" + e);
}
return doc;
}
/**
* Get key value, return default if void.
*
* @param key
* name of key
* @param def
* default value
* @return parameter value or default
*/
public String getString(String key, String def) {
String param_value = "";
try {
param_value = this.getString(key);
if ((param_value != null) && (param_value.length() > 0)) {
return param_value;
}
} catch (Exception e) {
// for any exception return the default value
return def;
}
return def;
}
/**
* handleGetObject implementation
*
* @param key
* the resource key
* @return the content associated to the specified key
* @throws MissingResourceException
*/
public final Object handleGetObject(String key) throws MissingResourceException {
return hashcontents.get(key);
}
/**
* Returns the number of translation units
*
* @return number of Items
*/
public int getNumberOfItems() {
return numberOfItems;
}
/**
* Define getKeys method
*
* @return item elements
*/
public Enumeration getKeys() {
return vectOfItems.elements();
}
}Classe di esempio
Questa classe mostra come istanziare la classe
TMXResourceBundle con il file di esempio
sample_tmx.xml.
Nell'esempio il codice della lingua (en = inglese) viene specificato in maniera esplicita ma può essere anche ricavato dall'ambiente locale.
Codice Sorgente
package com.tecnick.tmxjavabridge.sample;
import com.tecnick.tmxjavabridge.TMXResourceBundle;
/**
* Sample class for TMXResourceBundle class.
* <br/><br/>
* Copyright (c) 2004-2006
* Tecnick.com S.r.l (www.tecnick.com)
* Via Ugo Foscolo n.19 - 09045 Quartu Sant'Elena (CA) - ITALY
* www.tecnick.com - info@tecnick.com<br/>
* License: http://www.gnu.org/copyleft/lesser.html LGPL
*
* @author Nicola Asuni [www.tecnick.com].
* @version 1.1.008
*/
public class TMXJBSample {
/**
* loads TMX data
*/
final static TMXResourceBundle res_en = new TMXResourceBundle("tmx/sample_tmx.xml", "en");
// test cache system
final static TMXResourceBundle res_it = new TMXResourceBundle("tmx/sample_tmx.xml", "it", "src/com/tecnick/tmxjavabridge/test/test_tmx_it.obj");
/**
* Prints 2 strings on System.out
* @param args String[]
*/
public static void main(String[] args) {
System.out.println(res_en.getString("hello", ""));
System.out.println(res_en.getString("world", ""));
System.out.println(res_it.getString("hello", ""));
System.out.println(res_it.getString("world", ""));
}
}
Riferimenti
- ASUNI N, "PHP Localization with TMX standard", PHP Solutions Nr 3/2006, pl/en/it/fr/es/de
- Asuni N, "TMXResourceBundle - TMX PHP Bridge" [online] 2005-01-08, http://tmxphpbridge.sourceforge.net.
- Asuni N, "TMXResourceBundle - TMX Java Bridge" [online] 2005-01-08, http://tmxjavabridge.sourceforge.net.
- Itagaki M, "Use XML as a Java Localization Solution" [online] 2000-11-10, http://www.ftponline.com/javapro/archives/mi0011/default.asp.
- O'Conner J, "Java Internationalization: Localization with ResourceBundles" [online] 1998-10-01, http://java.sun.com/developer/technicalArticles/Intl/ResourceBundles/.
- OSCAR - LISA, "TMX - Translation Memory eXchange" [online] 2004-10-01, http://www.lisa.org/standards/tmx.
- OSCAR - LISA, "TMX 1.4b Specification" [online] 2005-03-26, http://www.lisa.org/standards/tmx/tmx.html.
- W3C, "Extensible Markup Language (XML)" [online] 2005-08-02, http://www.w3.org/XML/.