4 * Copyright 2010 The Codist Monk.
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 package net
.sourceforge
.aprog
.i18n
;
27 import static net
.sourceforge
.aprog
.tools
.Tools
.*;
29 import java
.io
.UnsupportedEncodingException
;
30 import java
.lang
.reflect
.Method
;
31 import java
.text
.MessageFormat
;
32 import java
.util
.ArrayList
;
33 import java
.util
.Collection
;
34 import java
.util
.HashSet
;
35 import java
.util
.Iterator
;
36 import java
.util
.Locale
;
37 import java
.util
.MissingResourceException
;
38 import java
.util
.ResourceBundle
;
40 import java
.util
.logging
.Level
;
42 import net
.sourceforge
.aprog
.tools
.Tools
;
45 * Instances of this class can translate messages using locales and resource bundles.
46 * <br>The easiest way to add translation to a Swing program with this class is by using the static methods in {@link Translator.Helpers}.
47 * <br>To improve performance, call {@code this.setAutoCollectingLocales(false)} after all available locales have been collected.
48 * <br>You can manually collect locales with {@link #collectAvailableLocales(String)}.
49 * <br>Instances of this class are thread-safe as long as the listeners don't cause synchronization problems.
51 * @author codistmonk (2010-05-11)
54 public class Translator
{
56 private final Collection
<Listener
> listeners
;
58 private final Set
<Autotranslator
> autotranslators
;
60 private final Set
<Locale
> availableLocales
;
62 private Locale locale
;
64 private boolean autoCollectingLocales
;
67 this.listeners
= new ArrayList
<Listener
>();
68 this.autotranslators
= new HashSet
<Autotranslator
>();
69 this.availableLocales
= new HashSet
<Locale
>();
70 this.locale
= Locale
.getDefault();
71 this.autoCollectingLocales
= true;
77 * <br>Should not be null
78 * <br>Shared parameter
80 public final synchronized void addTranslatorListener(final Listener listener
) {
81 this.listeners
.add(listener
);
89 public final synchronized void removeTranslatorListener(final Listener listener
) {
90 this.listeners
.remove(listener
);
95 * @return {@code true} if {@link #collectAvailableLocales(String)} is called automatically each time a translation is performed
97 public final boolean isAutoCollectingLocales() {
98 return this.autoCollectingLocales
;
103 * @param autoCollectingLocales {@code true} if {@link #collectAvailableLocales(String)} should be called automatically each time a translation is performed
105 public final void setAutoCollectingLocales(final boolean autoCollectingLocales
) {
106 this.autoCollectingLocales
= autoCollectingLocales
;
112 * <br>A non-null value
115 public final synchronized Listener
[] getTranslatorListeners() {
116 return this.listeners
.toArray(new Listener
[this.listeners
.size()]);
122 * @param <T> the actual type of {@code object}
124 * <br>Should not be null
125 * <br>Input-output parameter
126 * <br>Shared parameter
127 * @param textPropertyName
128 * <br>Should not be null
129 * <br>Shared parameter
130 * @param translationKey
131 * <br>Should not be null
132 * <br>Shared parameter
133 * @param messagesBase
134 * <br>Should not be null
135 * <br>Shared parameter
137 * <br>Should not be null
138 * @return {@code object}
139 * <br>A non-null value
142 public final synchronized <T
> T
translate(final T object
, final String textPropertyName
, final String translationKey
, final String messagesBase
, final Object
... parameters
) {
143 this.autoCollectLocales(messagesBase
);
145 final Autotranslator autotranslator
= this.new Autotranslator(object
, textPropertyName
, translationKey
, messagesBase
, parameters
);
147 autotranslator
.translate();
149 // If there is already another autotranslator with the same object and textPropertyName
150 // remove it before adding the new autotranslator
151 this.autotranslators
.remove(autotranslator
);
152 this.autotranslators
.add(autotranslator
);
158 * Removes {@code object} from the list of autotranslatables and resets its text property to the translation key.
159 * <br>That means that subsequent calls to {@link #setLocale(Locale)} won't update {@code object} anymore.
162 * <br>Should not be null
163 * <br>Input-output parameter
164 * <br>Shared parameter
165 * @param textPropertyName
166 * <br>Should not be null
167 * <br>Shared parameter
169 public final synchronized void untranslate(final Object object
, final String textPropertyName
) {
170 for (final Iterator
<Autotranslator
> iterator
= this.autotranslators
.iterator(); iterator
.hasNext();) {
171 final Autotranslator autotranslator
= iterator
.next();
173 if (autotranslator
.getObject().equals(object
) && autotranslator
.getTextPropertyName().equals(textPropertyName
)) {
176 autotranslator
.untranslate();
186 * <br>A non-null value
189 public final synchronized Locale
getLocale() {
194 * If {@code this.getLocale()} is not equal to {@code locale},
195 * then the locale is changed, the autotranslators are updated and the listeners are notified.
198 * <br>Should not be null
199 * <br>Shared parameter
201 public final synchronized void setLocale(final Locale locale
) {
202 if (!this.getLocale().equals(locale
)) {
203 final Locale oldLocale
= this.getLocale();
205 this.locale
= locale
;
207 for (final Autotranslator autotranslator
: this.autotranslators
) {
208 autotranslator
.translate();
211 for (final Listener listener
: this.getTranslatorListeners()) {
212 listener
.localeChanged(oldLocale
, this.getLocale());
218 * The set of available locales can be augmented with {@link #getAvailableLocales()}.
219 * <br>{@link #getAvailableLocales()} is called each time a translation is performed.
223 * <br>A non-null value
225 public final synchronized Locale
[] getAvailableLocales() {
226 return this.availableLocales
.toArray(new Locale
[this.availableLocales
.size()]);
232 * @param translationKey
233 * <br>Should not be null
234 * @param messagesBase
235 * <br>Should not be null
237 * <br>Should not be null
239 * <br>A non-null value
241 public final synchronized String
translate(final String translationKey
, final String messagesBase
, final Object
... parameters
) {
242 this.autoCollectLocales(messagesBase
);
244 String translatedMessage
= translationKey
;
247 final ResourceBundle messages
= ResourceBundle
.getBundle(messagesBase
, this.getLocale());
249 translatedMessage
= iso88591ToUTF8(messages
.getString(translationKey
));
250 } catch (final MissingResourceException exception
) {
251 System
.err
.println(Tools
.debug(2, exception
.getMessage()));
252 getLoggerForThisMethod().log(Level
.WARNING
, "Missing translation for locale (" + Translator
.this.getLocale() + ") of " + translationKey
);
255 final Object
[] localizedParameters
= parameters
.clone();
257 for (int i
= 0; i
< localizedParameters
.length
; ++i
) {
258 if (localizedParameters
[i
] instanceof Throwable
) {
259 localizedParameters
[i
] = ((Throwable
) localizedParameters
[i
]).getLocalizedMessage();
263 return MessageFormat
.format(translatedMessage
, localizedParameters
);
267 * Scans {@code messagesBase} using {@link Locale#getAvailableLocales()} and adds the available locales to {@code this}.
268 * <br>A locale is "available" to the translator if an appropriate resource bundle is found.
270 * @param messagesBase
271 * <br>Should not be null
273 public final synchronized void collectAvailableLocales(final String messagesBase
) {
274 // TODO don't rely on Locale.getAvailableLocales(), use only messagesBase if possible
275 for (final Locale predefinedLocale
: Locale
.getAvailableLocales()) {
277 if (predefinedLocale
.equals(ResourceBundle
.getBundle(messagesBase
, predefinedLocale
).getLocale())) {
278 this.availableLocales
.add(predefinedLocale
);
280 } catch (final Exception exception
) {
287 * Calls {@link #collectAvailableLocales(String)} if {@code this.isAutoCollectingLocales()}.
289 * @param messagesBase
290 * <br>Should not be null
292 private final void autoCollectLocales(final String messagesBase
) {
293 if (this.isAutoCollectingLocales()) {
294 this.collectAvailableLocales(messagesBase
);
300 * This class defines a property translation operation.
302 * @author codistmonk (creation 2010-05-11)
305 private final class Autotranslator
{
307 private final Object object
;
309 private final String textPropertyName
;
311 private final String translationKey
;
313 private final String messagesBase
;
315 private final Object
[] parameters
;
317 private final Method setter
;
322 * <br>Should not be null
323 * <br>Shared parameter
324 * @param textPropertyName
325 * <br>Should not be null
326 * <br>Shared parameter
327 * @param translationKey
328 * <br>Should not be null
329 * <br>Shared parameter
330 * @param messagesBase
331 * <br>Should not be null
332 * <br>Shared parameter
334 * <br>Should not be null
335 * <br>Shared parameter
336 * @throws RuntimeException if a setter cannot be retrieved for the property.
338 public Autotranslator(final Object object
, final String textPropertyName
,
339 final String translationKey
, final String messagesBase
, final Object
... parameters
) {
340 this.object
= object
;
341 this.textPropertyName
= textPropertyName
;
342 this.translationKey
= translationKey
;
343 this.messagesBase
= messagesBase
;
344 this.parameters
= parameters
;
345 this.setter
= getSetter(object
, textPropertyName
, String
.class);
351 * <br>A non-null value
354 public final Object
getObject() {
361 * <br>A non-null value
364 public final String
getTextPropertyName() {
365 return this.textPropertyName
;
368 public final void translate() {
369 this.set(Translator
.this.translate(this.translationKey
, this.messagesBase
, this.parameters
));
373 * Sets the property with the translation key.
375 public final void untranslate() {
376 this.set(this.translationKey
);
380 public final boolean equals(final Object object
) {
381 final Autotranslator that
= castToCurrentClass(object
);
383 return this == that
||
385 this.getObject().equals(that
.getObject()) &&
386 this.getTextPropertyName().equals(that
.getTextPropertyName());
390 public final int hashCode() {
391 return this.object
.hashCode() + this.textPropertyName
.hashCode();
395 * Calls {@code this.setter} with parameter {@code text}.
398 * <br>Should not be null
399 * <br>Shared parameter
401 private final void set(final String text
) {
403 this.setter
.invoke(this.getObject(), text
);
404 } catch (final Exception exception
) {
405 getLoggerForThisMethod().log(Level
.WARNING
, "", exception
);
411 private static Translator defaultTranslator
;
414 * This method creates the default translator if necessary, and then always returns the same value.
417 * <br>A non-null value
420 public static final synchronized Translator
getDefaultTranslator() {
421 if (defaultTranslator
== null) {
422 defaultTranslator
= new Translator();
425 return defaultTranslator
;
429 * This method gets or creates a {@link Locale} corresponding to {@code languageCountryVariant}.
430 * <br>{@code languageCountryVariant} is a String made of 1 to 3 elements separated by "_":
431 * <br>language ("" or ISO 639 2-letter code) ["_" country ("" or ISO 3166 2-letter code) ["_" variant (can be "")]]
432 * @param languageCountryVariant
433 * <br>Should not be null
435 * <br>A possibly new value
436 * <br>A non-null value
438 public static final Locale
createLocale(final String languageCountryVariant
) {
439 final String
[] tmp
= languageCountryVariant
.split("_");
440 final String language
= tmp
[0];
441 final String country
= tmp
.length
> 1 ? tmp
[1] : "";
442 final String variant
= tmp
.length
> 2 ? tmp
[2] : "";
444 for (final Locale locale
: Locale
.getAvailableLocales()) {
445 if (locale
.getLanguage().equals(language
) && locale
.getCountry().equals(country
) && locale
.getVariant().equals(variant
)) {
450 return new Locale(language
, country
, variant
);
454 * This method does the opposite of {@link #createLocale(String)}.
457 * <br>Should not be null
460 * <br>A non-null value
462 public static final String
getLanguageCountryVariant(final Locale locale
) {
463 String result
= locale
.getLanguage();
465 if (locale
.getCountry().length() > 0) {
466 result
+= "_" + locale
.getCountry();
469 if (locale
.getVariant().length() > 0) {
470 result
+= "_" + locale
.getVariant();
477 * This method reinterprets strings read from property files using UTF-8.
478 * <br>{@link ResourceBundle} interprets the contents of .properties files as if they used ISO-8859-1 encoding.
479 * <br>If UTF-8 is used to encode these files, the retrieved messages will present bad characters.
480 * <br>For instance, the character 'Ω' is encoded as {@code 0xCEA9} in UTF-8 but cannot be directly encoded in ISO-8859-1.
481 * <br>Instead, the code \u03A9 would have to be used so that {@link ResourceBundle} retrieves the character 'Ω'.
482 * <br>If a file contains 'Ω' in UTF-8, {@link ResourceBundle} will interpret it using ISO-8859-1 as "Ω".
483 * because {@code 0xCE} is 'Î' and {@code 0xA9} is '©' in this encoding.
484 * <br>If {@code s = "Ω"} is the string retrieved from a file containing 'Ω' in UTF-8,
485 * then {@code !s.equals("Ω")} but {@code iso88591ToUTF8(s).equals("Ω")}.
487 * @param translatedMessage
488 * <br>Should not be null
489 * <br>Shared parameter
490 * @return a new string or {@code translatedMessage} if the conversion fails
491 * <br>A non-null value
494 public static final String
iso88591ToUTF8(final String translatedMessage
) {
496 return new String(translatedMessage
.getBytes("ISO-8859-1"), "UTF-8");
497 } catch (final UnsupportedEncodingException exception
) {
498 getLoggerForThisMethod().log(Level
.WARNING
, "", exception
);
500 return translatedMessage
;
506 * Listener interface for translator events.
508 * @author codistmonk (creation 2009-09-28)
511 public static interface Listener
{
514 * Called whenever the translator's locale has been changed, and after the registered
515 * objects have been translated.
518 * <br>Should not be null
520 * <br>Should not be null
521 * <br>Shared parameter
523 public abstract void localeChanged(Locale oldLocale
, Locale newLocale
);