Statistics
| Revision:

root / trunk / libraries / libInternationalization / src / org / gvsig / i18n / Messages.java @ 5868

History | View | Annotate | Download (21.9 KB)

1
/* gvSIG. Sistema de Informaci?n Geogr?fica de la Generalitat Valenciana
2
*
3
* Copyright (C) 2006 IVER T.I. and Generalitat Valenciana.
4
*
5
* This program is free software; you can redistribute it and/or
6
* modify it under the terms of the GNU General Public License
7
* as published by the Free Software Foundation; either version 2
8
* of the License, or (at your option) any later version.
9
*
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
* GNU General Public License for more details.
14
*
15
* You should have received a copy of the GNU General Public License
16
* along with this program; if not, write to the Free Software
17
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,USA.
18
*
19
* For more information, contact:
20
*
21
*  Generalitat Valenciana
22
*   Conselleria d'Infraestructures i Transport
23
*   Av. Blasco Ib??ez, 50
24
*   46010 VALENCIA
25
*   SPAIN
26
*
27
*      +34 963862235
28
*   gvsig@gva.es
29
*      www.gvsig.gva.es
30
*
31
*    or
32
*
33
*   IVER T.I. S.A
34
*   Salamanca 50
35
*   46005 Valencia
36
*   Spain
37
*
38
*   +34 963163400
39
*   dac@iver.es
40
*/
41

    
42
package org.gvsig.i18n;
43

    
44
import java.io.File;
45
import java.net.MalformedURLException;
46
import java.net.URL;
47
import java.net.URLClassLoader;
48
import java.util.ArrayList;
49
import java.util.Enumeration;
50
import java.util.HashMap;
51
import java.util.List;
52
import java.util.Locale;
53
import java.util.MissingResourceException;
54
import java.util.ResourceBundle;
55
import java.util.StringTokenizer;
56

    
57
import org.apache.log4j.Logger;
58

    
59
/**
60
 * <p>This class offers some methods to provide internationalization services
61
 * to other projects. All the methods are static.</p>
62
 * 
63
 * <p>The most useful method is {@link #getText(String) getText(key)} (and family),
64
 * which returns the translation associated
65
 * with the provided key. The class must be initialized before getText can be
66
 * used.</p>
67
 * 
68
 * <p>The typical usage sequence would be:</p>
69
 * <ul>
70
 * <li>Add some locale to the prefered locales list: <code>Messages.addLocale(new Locale("es"))</code></li>
71
 * <li>Add some resource file containing translations: <code>Messages.addResourceFamily("text", new File("."))</code></li>
72
 * <li>And finaly getText can be used: <code>Messages.getText("aceptar")</code></li>
73
 * </ul>
74
 * 
75
 * <p>The resource files are Java properties files, which contain <code>key=translation</code>
76
 * pairs, one
77
 * pair per line. These files must be encoded using iso-8859-1 encoding, and unicode escaped
78
 * sequences must be used to include characters outside the former encoding.
79
 * There are several ways to specify the property file to load, see the different
80
 * addResourceFamily methods for details.</p> 
81
 * 
82
 * @author C?sar Mart?nez Izquierdo (cesar.martinez@iver.es)
83
 *
84
 */
85
public class Messages {
86
    private static Logger logger = Logger.getLogger("Messages");
87
    private static String _CLASSNAME = "org.gvsig.i18n.Messages";
88
    private static int _INITIALSIZE = 3700; // I try to set an initial capacity similar to the amount of strings in gvSIG
89
    
90
    /* Each entry will contain a hashmap with translations. Each hasmap
91
     * contains the translations for one language, indexed by the
92
     * translation key. The translations for language (i) in the preferred locales
93
     * list are contained in the position (i) of the localeResources list */
94
    private static ArrayList localeResources = new ArrayList(); 
95
        private static ArrayList preferredLocales = new ArrayList(); // contains the ordered list of prefered languages/locales (class Locale)
96
        
97
        /**
98
         * <p>Gets the localized message associated with the provided key.
99
         * If the key is not in the dictionary, return the key and register
100
         * the failure in the log.</p>
101
         * 
102
         * <p>The <code>callerName</code> parameter is only
103
         * used as a label when logging, so any String can be used. However, a
104
         * meaningful String should be used, such as the name of the class requiring
105
         * the translation services, in order to identify the source of the failure
106
         * in the log.</p>    
107
         * 
108
         * @param key         An String which identifies the translation that we want to get. 
109
         * @param callerName  A symbolic name given to the caller of this method, to
110
         *                    show it in the log if the key was not found
111
         * @return            an String with the message associated with the provided key.
112
         *                    If the key is not in the dictionary, return the key. If the key
113
         *                    is null, return null.
114
         */
115
        public static String getText(String key, String callerName) {
116
                if (key==null) return null;
117
                for (int numLocale=0; numLocale<localeResources.size(); numLocale++) {
118
                        // try to get the translation for any of the languagues in the preferred languages list
119
                        String translation = (String) ((HashMap)localeResources.get(numLocale)).get(key);
120
                        if (translation!=null)
121
                                return translation;
122
                }
123
                logger.warn(callerName+" "+Messages.getText("Messages._no_se_encontro_la_traduccion_para", _CLASSNAME ,false)+" "+key);
124
                return key;
125
        }
126
        
127
        /**
128
         * <p>Gets the localized message associated with the provided key.
129
         * If the key is not in the dictionary, return the key and register
130
         * the failure in the log.</p>
131
         * 
132
         * @param key     An String which identifies the translation that we want to get.
133
         * @return        an String with the message associated with the provided key.
134
         *                If the key is not in the dictionary, return the key. If the key
135
         *                is null, return null.
136
         */
137
        public static String getText(String key) {
138
                return getText(key, _CLASSNAME);
139
        }
140
        
141
        /**
142
         * <p>Gets the localized message associated with the provided key.
143
         * If the key is not in the dictionary, it returns null and the failure
144
         * is only registered in the log if the param log is true.</p>
145
         * 
146
         * @param key    An String which identifies the translation that we want to get.
147
         * @param log    Determines whether log a key failure or not
148
         * @return       an String with the message associated with the provided key,
149
         *               or null if the key is not in the dictionary.
150
         */
151
        public static String getText(String key, boolean log) {
152
                return getText(key, _CLASSNAME, log);
153
        }
154
        
155
        /**
156
         * <p>Gets the localized message associated with the provided key.
157
         * If the key is not in the dictionary, it returns null and the failure
158
         * is only registered in the log if the param log is true.</p>
159
         * 
160
         * @param key         An String which identifies the translation that we want to get.
161
         * @param callerName  A symbolic name given to the caller of this method, to
162
         *                    show it in the log if the key was not found
163
         * @param log         Determines whether log a key failure or not
164
         * @return            an String with the message associated with the provided key,
165
         *                    or null if the key is not in the dictionary.
166
         */
167
        public static String getText(String key, String callerName, boolean log) {
168
                if (key==null) return null;
169
                for (int numLocale=0; numLocale<localeResources.size(); numLocale++) {
170
                        // try to get the translation for any of the languagues in the preferred languages list
171
                        String translation = (String) ((HashMap)localeResources.get(numLocale)).get(key);
172
                        if (translation!=null)
173
                                return translation;
174
                }
175
                if (log) {
176
                        logger.warn(callerName+" "+Messages.getText("Messages._no_se_encontro_la_traduccion_para", _CLASSNAME ,false)+" "+key);
177
                }
178
                return null;
179
        }
180

    
181
        /**
182
         * <p>Adds an additional family of resource files containing some translations.
183
         * A family is a group of files with a common baseName.
184
         * The file must be an iso-8859-1 encoded file, which can contain any unicode
185
         * character using unicode escaped sequences, and following the syntax:
186
         * <code>key1=value1
187
         * key2=value2</code>
188
         * where 'key1' is the key used to identify the string and must not
189
         * contain the '=' symbol, and 'value1' is the associated translation.</p>
190
         * <p<For example:</p>
191
         * <code>cancel=Cancelar
192
         * accept=Aceptar</code>
193
         * <p>Only one pair key-value is allowed per line.</p>
194
         * 
195
         * <p>The actual name of the resource file to load is determined using the rules
196
         * explained in the class java.util.ResourceBundle. Summarizing, for each language
197
         * in the specified preferred locales list it will try to load a file with
198
         *  the following structure: <code>family_locale.properties</code></p>
199
         *
200
         * <p>For example, if the preferred locales list contains {"fr", "es", "en"}, and
201
         * the family name is "text", it will try to load the files "text_fr.properties",
202
         * "text_es.properties" and finally "text_en.properties".</p>
203
         * 
204
         * <p>Locales might be more specific, such us "es_AR"  (meaning Spanish from Argentina)
205
         * or "es_AR_linux" (meaning Linux system preferring Spanish from Argentina). In the
206
         * later case, it will try to load "text_es_AR_linux.properties", then
207
         * "text_es_AR.properties" if the former fails, and finally "text_es.properties".</p>
208
         * 
209
         * <p>The directory used to locate the resource file is determining by using the
210
         * getResource method from the provided ClassLoader.</p>
211
         *  
212
         * @param family    The family name (or base name) which is used to search
213
         *                  actual properties files.
214
         * @param loader    A ClassLoader which is able to find a property file matching
215
         *                                         the specified family name and the preferred locales
216
         * @see             <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/ResourceBundle.html">ResourceBundle</a>
217
         */
218
        public static void addResourceFamily(String family, ClassLoader loader) {
219
                addResourceFamily(family, loader, "");
220
        }
221
        
222
        /**
223
         * <p>Adds an additional family of resource files containing some translations.
224
         * The search path to locate the files is provided by the dirList parameter.</p>
225
         * 
226
         * <p>See {@link addResourceFamily(String, ClassLoader)} for a discussion about the
227
         * format of the property files and the way to determine the candidat files
228
         * to load. Note that those methods are different in the way to locate the 
229
         * candidat files. This method searches in the provided paths (<code>dirList</code>
230
         * parameter), while the referred method searches using the getResource method
231
         * of the provided ClassLoader.</p>
232
         *  
233
         * @param family    The family name (or base name) which is used to search
234
         *                  actual properties files.
235
         * @param dirList   A list of search paths to locate the property files
236
         * @throws MalformedURLException
237
         * @see             <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/ResourceBundle.html">ResourceBundle</a>
238
         */
239
        public static void addResourceFamily(String family, File[] dirList) throws MalformedURLException{
240
                // use our own classloader
241
                URL[] urls = new URL[dirList.length];                
242
        
243
                        int i;
244
                        for (i=0; i<urls.length; i++) {
245
                                urls[i] = dirList[i].toURL();
246
                        }
247
        
248
                ClassLoader loader = new MessagesClassLoader(urls);
249
                addResourceFamily(family, loader, "");
250
        }
251

    
252
        /**
253
         * <p>Adds an additional family of resource files containing some translations.
254
         * The search path to locate the files is provided by the dir parameter.</p>
255
         * 
256
         * <p>See {@link addResourceFamily(String, ClassLoader)} for a discussion about the
257
         * format of the property files and the way to determine the candidat files
258
         * to load. Note that those methods are different in the way to locate the 
259
         * candidat files. This method searches in the provided path (<code>dir</code>
260
         * parameter), while the referred method searches using the getResource method
261
         * of the provided ClassLoader.</p>
262
         *  
263
         * @param family    The family name (or base name) which is used to search
264
         *                  actual properties files.
265
         * @param dir       The search path to locate the property files
266
         * @throws MalformedURLException
267
         * @see             <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/ResourceBundle.html">ResourceBundle</a>
268
         */
269
        public static void addResourceFamily(String family, File dir) throws MalformedURLException{
270
                // use our own classloader
271
                URL[] urls = new URL[1];                
272
                urls[0] = dir.toURL();
273
                ClassLoader loader = new MessagesClassLoader(urls);
274
                addResourceFamily(family, loader, "");
275
        }
276

    
277

    
278
        /**
279
         * <p>Adds an additional family of resource files containing some translations.
280
         * The search path is determined by the getResource method from the
281
         * provided ClassLoader.</p>
282
         * 
283
         * <p>This method is identical to {@link addResourceFamily(String, ClassLoader)},
284
         * except that it adds a <pode>callerName</code> parameter to show in the log.</p>
285
         * 
286
         * <p>See {@link addResourceFamily(String, ClassLoader)} for a discussion about the
287
         * format of the property files andthe way to determine the candidat files
288
         * to load.</p>
289
         *  
290
         * @param family      The family name (or base name) which is used to search
291
         *                    actual properties files.
292
         * @param loader      A ClassLoader which is able to find a property file matching
293
         *                                           the specified family name and the preferred locales
294
         * @param callerName  A symbolic name given to the caller of this method, to
295
         *                    show it in the log if there is an error
296
         * @see               <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/ResourceBundle.html">ResourceBundle</a>
297
         */
298
        public static void addResourceFamily(String family, ClassLoader loader, String callerName) {
299
                String currentKey;
300
                Enumeration properties;
301
                int totalLocales = preferredLocales.size();
302

    
303
                if (totalLocales == 0) {
304
                        // if it's empty, warn about that
305
                        logger.warn(Messages.getText("Messages.no.hay.lista.de.idiomas.preferidos.quiza.olvido.inicializar.clase", _CLASSNAME, false));
306
                }
307
                Locale lang;                
308
                ResourceBundle bundle = null;
309
                
310
                for (int numLocale=0; numLocale<totalLocales; numLocale++) { // for each language
311
                        lang = (Locale) preferredLocales.get(numLocale);
312
                        try {
313
                                /**
314
                                 * Here we are doing a pervert use of getBundle method. Normally, it is call it in in this way:
315
                                 * getBundle("text", "en", loader), in order to get the file 'text_en.properties'. However, in
316
                                 * this way the default file ('text.properties') is also loaded, and we want to avoid this
317
                                 * (because 'text.properties contains the Spanish translation).
318
                                 * So we use the method in this non-standard way:
319
                                 * getBundle("text_en", "en", loader), ensuring that we are just loading the file 'text_en.properties'
320
                                 **/
321
                                if (!lang.getLanguage().equals("es"))
322
                                        bundle = ResourceBundle.getBundle(family+"_"+lang.getLanguage(), lang, loader);
323
                                else // we allow 'text.properties' to be loaded for Spanish
324
                                        bundle = ResourceBundle.getBundle(family, lang, loader);
325
                                
326
//                                if (bundle.getLocale().getLanguage().equals(lang.getLanguage()) || lang.getLanguage().equals("es")) {
327
                                        // we ensure we didn't get a fallback
328
                                        // (except if lang == "es" -- spanish is the default language, so we accept a the main translation file for it)
329
                                properties = bundle.getKeys();
330
                                while (properties.hasMoreElements()) {
331
                                        currentKey = (String) properties.nextElement();
332
                                        if (! ((HashMap)localeResources.get(numLocale)).containsKey(currentKey)) {
333
                                                ((HashMap)localeResources.get(numLocale)).put(currentKey, bundle.getString(currentKey));
334
                                        }
335
                                }
336
//                                }
337
                                /*else {
338
                                        logger.warn(Messages.getText("Messages.las_traducciones_no_pudieron_ser_cargadas", false)+" -- "+callerName+" -- "+lang.getLanguage());
339
                                }*/
340
                        }
341
                        catch (MissingResourceException ex) {
342
                                logger.warn(Messages.getText("Messages.las_traducciones_no_pudieron_ser_cargadas", false)+" -- "+callerName+" -- "+lang.getLanguage());
343
                                //logger.warn(_CLASSNAME+" " + ex.getLocalizedMessage());
344
                        }
345
                }
346
        }
347
        
348
        /**
349
         * <p>Adds an additional family of resource files containing some translations.</p>
350
         * 
351
         * <p>This method is identical to {@link addResourceFamily(String, ClassLoader, String)},
352
         * except that it uses the caller's class loader.</p>
353
         * 
354
         * <p>See {@link addResourceFamily(String, ClassLoader)} for a discussion about the
355
         * format of the property files and the way to determine the candidat files
356
         * to load.</p>
357
         *  
358
         * @param family      The family name (or base name) which is used to search
359
         *                    actual properties files.
360
         * @param callerName  A symbolic name given to the caller of this method, to
361
         *                    show it in the log if there is an error. This is only used
362
         *                    to show
363
         *                    something meaningful in the log, so you can use any string
364
         * @see               <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/ResourceBundle.html">ResourceBundle</a>
365
         */
366
        public static void addResourceFamily(String family, String callerName) {
367
                String currentKey;
368
                Enumeration properties;
369
                int totalLocales = preferredLocales.size();
370

    
371
                if (totalLocales == 0) {
372
                        // if it's empty, warn about that
373
                        logger.warn(Messages.getText("Messages.no.hay.lista.de.idiomas.preferidos.quiza.olvido.inicializar.clase", _CLASSNAME, false));
374
                }
375
                Locale lang;                
376
                ResourceBundle bundle = null;
377
                
378
                for (int numLocale=0; numLocale<totalLocales; numLocale++) { // for each language
379
                        try {
380
                                lang = (Locale) preferredLocales.get(numLocale);
381
                                bundle = ResourceBundle.getBundle(family, lang);
382
                                if (bundle.getLocale().getLanguage().equals(lang.getLanguage()) || lang.getLanguage().equals("es")) {
383
                                        // we ensure we didn't get a fallback
384
                                        // (except if lang == "es" -- spanish is the default language, so we accept a the main translation file for it)
385
        
386
                                        properties = bundle.getKeys();
387
                                        while (properties.hasMoreElements()) {
388
                                                currentKey = (String) properties.nextElement();
389
                                                if (! ((HashMap)localeResources.get(numLocale)).containsKey(currentKey)) {
390
                                                        ((HashMap)localeResources.get(numLocale)).put(currentKey, bundle.getString(currentKey));
391
                                                }
392
                                        }
393
                                }
394
                                else {
395
                                        logger.warn(Messages.getText("Messages.las_traducciones_no_pudieron_ser_cargadas", false)+" -- "+callerName+" -- "+lang.getLanguage());
396
                                }
397
                        }
398
                        catch (MissingResourceException ex) {
399
                                logger.warn(_CLASSNAME+" " + ex.getLocalizedMessage());
400
                        }
401
                }
402
        }
403
        
404
        
405
        /**
406
         * Returns an ArrayList containing the ordered list of prefered Locales
407
         * Each element of the ArrayList is a Locale object.
408
         * 
409
         * @return an ArrayList containing the ordered list of prefered Locales
410
         * Each element of the ArrayList is a Locale object.
411
         */
412
        public static ArrayList getPreferredLocales() {
413
                return preferredLocales;
414
        }
415
        
416
        /**
417
         * <p>Sets the ordered list of preferred locales.
418
         * Each element of the ArrayList is a Locale object.</p>
419
         * 
420
         * <p>Note that calling this method does not load any translation, it just
421
         * adds the language to the preferred locales list, so this method must
422
         * be always called before the translations are loaded using
423
         * the addResourceFamily() methods.</p>
424
         * 
425
         * @param preferredLocales an ArrayList containing Locale objects.
426
         * The ArrayList represents an ordered list of preferred locales
427
         */
428
        public static void setPreferredLocales(ArrayList preferredLocalesList) {
429
                for (int numLocale=0; numLocale < preferredLocalesList.size(); numLocale++) {
430
                        addLocale((Locale) preferredLocalesList.get(numLocale));
431
                }
432
        }
433

    
434
        /**
435
         * Adds a Locale at the end of the ordered list of preferred locales.
436
         * Note that calling this method does not load any translation, it just
437
         * adds the language to the preferred locales list, so this method must
438
         * be always called before the translations are loaded using
439
         * the addResourceFamily() methods.
440
         * 
441
         * @param lang   A Locale object specifying the locale to add
442
         */
443
        public static void addLocale(Locale lang) {
444
                if (!preferredLocales.contains(lang)) { // avoid duplicates
445
                                preferredLocales.add(lang); // add the lang to the ordered list of preferred locales
446
                                localeResources.add(new HashMap(_INITIALSIZE)); // add a hashmap which will contain the translation for this language
447
                }
448
        }
449

    
450
        /**
451
         * Removes the specified Locale from the list of preferred locales and the
452
         * translations associated with this locale.
453
         * 
454
         * @param lang   A Locale object specifying the locale to remove
455
         * @return       True if the locale was in the preferred locales list, false otherwise
456
         */
457
        public static boolean removeLocale(Locale lang) {
458
                int numLocale = preferredLocales.indexOf(lang);
459
                if (numLocale!=-1) { // we found the locale in the list
460
                        try {
461
                                preferredLocales.remove(numLocale);
462
                                localeResources.remove(numLocale);
463
                        }
464
                        catch (IndexOutOfBoundsException ex) {
465
                                logger.warn(_CLASSNAME + "." + "removeLocale: " + ex.getLocalizedMessage());
466
                        }
467
                        return true;
468
                }
469
                return false;
470
        }
471

    
472
        /**
473
         * Cleans the translation tables (removes all the translations from memory).
474
         */
475
        public static void removeResources() {
476
                for (int numLocale=0; numLocale<localeResources.size(); numLocale++) {
477
                        ((HashMap)localeResources.get(numLocale)).clear();
478
                }
479
        }
480

    
481
        /**
482
         * The number of translation keys which have been loaded till now  
483
         * (In other words: the number of available translation strings).
484
         * 
485
         * @param lang The language for which we want to know the number of translation keys
486
         * return The number of translation keys for the provided language.
487
         */
488
        protected static int size(Locale lang) {
489
                int numLocale = preferredLocales.indexOf(lang);
490
                if (numLocale!=-1) {
491
                        return ((HashMap)localeResources.get(numLocale)).size();
492
                };
493
                return 0;
494
        }
495
        /**
496
         * Searches the subdirectories of the provided directory, finding
497
         * all the translation files, and constructing a list of available translations.
498
         * It reports different country codes or variants, if available.
499
         * For example, if there is an en_US translation and an en_GB translation, both
500
         * locales will be present in the Vector.
501
         * 
502
         * @return
503
         */
504
        
505
        /**
506
         * 
507
         * @return A Vector containing the available locales. Each element is a Locale object
508
         */
509
        /*public static Vector getAvailableLocales() {
510
                return _availableLocales;
511
        }*/
512
        
513
        /**
514
         * 
515
         * @return A Vector containing the available languages. Each element is an String object
516
         */
517
        /*public static Vector getAvailableLanguages() {
518
                Vector availableLanguages = new Vector();
519
                Locale lang;
520
                Enumeration locales = _availableLocales.elements();
521
                while (locales.hasMoreElements()) {
522
                        lang = (Locale) locales.nextElement();
523
                        availableLanguages.add(lang.getLanguage());
524
                }
525
                return availableLanguages;
526
        }*/
527
}