View Javadoc

1   /*
2    *  Gettext Commons
3    *
4    *  Copyright (C) 2005  Felix Berger
5    *  Copyright (C) 2005  Steffen Pingel
6    *
7    *  This library is free software; you can redistribute it and/or
8    *  modify it under the terms of the GNU Lesser General Public
9    *  License as published by the Free Software Foundation; either
10   *  version 2.1 of the License, or (at your option) any later version.
11   *
12   *  This library is distributed in the hope that it will be useful,
13   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15   *  Lesser General Public License for more details.
16   *
17   *  You should have received a copy of the GNU Lesser General Public
18   *  License along with this library; if not, write to the Free Software
19   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20   */
21  package org.xnap.commons.i18n;
22  
23  import java.lang.reflect.Method;
24  import java.lang.reflect.Modifier;
25  import java.text.MessageFormat;
26  import java.util.Locale;
27  import java.util.MissingResourceException;
28  import java.util.ResourceBundle;
29  
30  /**
31   * Provides methods for internationalization.
32   * <p>
33   * To learn how message strings wrapped in one of the <code>tr*()</code>
34   * methods can be extracted and localized, see <a
35   * href="http://code.google.com/p/gettext-commons/wiki/Tutorial">this tutorial</a>.
36   * 
37   * @author Steffen Pingel
38   * @author Felix Berger
39   * @author Tammo van Lessen
40   * @since 0.9
41   */
42  public class I18n {
43  
44  	/**
45  	 * Reference to the current localization bundles.
46  	 */
47  	private ResourceBundle bundle;
48  
49  	/**
50  	 * The locale of the strings used in the source code.
51  	 * 
52  	 * @see #trc(String, String)
53  	 */
54  	private Locale sourceCodeLocale = Locale.ENGLISH;
55  
56  	private String baseName;
57  
58  	private ClassLoader loader;
59  
60  	private Locale locale;
61  
62  	/**
63  	 * Constructs an I18n object for a resource bundle.
64  	 * 
65  	 * @param bundle
66  	 *            must not be <code>null</code>
67  	 * @throws NullPointerException
68  	 *             if <code>bundle</code> is null
69  	 * @since 0.9
70  	 */
71  	public I18n(ResourceBundle bundle)
72  	{
73  		setResources(bundle);
74  	}
75  
76  	/**
77  	 * Constructs an I18n object by calling {@link #setResources(String, Locale,
78  	 * ClassLoader)}.
79  	 * 
80  	 * @throws MissingResourceException
81  	 *             if the resource bundle could not be loaded
82  	 * @throws NullPointerException
83  	 *             if one of the arguments is <code>null</code>
84  	 * @since 0.9
85  	 */
86  	public I18n(String baseName, Locale locale, ClassLoader loader)
87  	{
88  		setResources(baseName, locale, loader);
89  	}
90  
91  	/**
92  	 * Returns the current resource bundle.
93  	 * 
94  	 * @since 0.9
95  	 */
96  	public ResourceBundle getResources()
97  	{
98  		return bundle;
99  	}
100 
101 	/**
102 	 * Returns the locale this instance was created with. This can be different
103 	 * from the locale of the resource bundle returned by
104 	 * {@link #getResources()}.
105 	 * 
106 	 * @return the locale or null, if this instance was directly created from a
107 	 *         resource bundle
108 	 * @since 0.9
109 	 */
110 	public Locale getLocale()
111 	{
112 		return locale;
113 	}
114 
115 	/**
116 	 * Sets a resource bundle to be used for message translations.
117 	 * <p>
118 	 * If this is called, the possibly previously specified class loader and
119 	 * baseName are invalidated, since the bundle might be from a different
120 	 * context. Subsequent calls to {@link #setLocale(Locale)} won't have any
121 	 * effect.
122 	 * 
123 	 * @since 0.9
124 	 */
125 	public void setResources(ResourceBundle bundle)
126 	{
127 		if (bundle == null) {
128 			throw new NullPointerException();
129 		}
130 		this.bundle = bundle;
131 		this.baseName = null;
132 		this.locale = bundle.getLocale();
133 		this.loader = null;
134 	}
135 
136 	/**
137 	 * Tries to load a resource bundle using {@link
138 	 * ResourceBundle#getBundle(java.lang.String, java.util.Locale,
139 	 * java.lang.ClassLoader)}.
140 	 * 
141 	 * @throws MissingResourceException
142 	 *             if the bundle could not be loaded
143 	 * @throws NullPointerException
144 	 *             if one of the arguments is <code>null</code>
145 	 * @since 0.9
146 	 */
147 	public void setResources(String baseName, Locale locale, ClassLoader loader)
148 	{
149 		this.bundle = ResourceBundle.getBundle(baseName, locale, loader);
150 		this.baseName = baseName;
151 		this.locale = locale;
152 		this.loader = loader;
153 	}
154 
155 	/**
156 	 * Marks <code>text</code> to be translated, but doesn't return the
157 	 * translation but <code>text</code> itself.
158 	 * 
159 	 * @since 0.9
160 	 */
161 	public static final String marktr(String text)
162 	{
163 		return text;
164 	}
165 
166 	/**
167 	 * Tries to load a resource bundle for the locale.
168 	 * <p>
169 	 * The resource bundle is then used for message translations. Note, you have
170 	 * to retrieve all messages anew after a locale change in order for them to
171 	 * be translated to the language specified by the new locale.
172 	 * <p>
173 	 * 
174 	 * @return false if there is not enough information for loading a new
175 	 *         resource bundle, see {@link #setResources(ResourceBundle)}.
176 	 * @throws MissingResourceException
177 	 *             if the resource bundle for <code>locale</code> could not be
178 	 *             found
179 	 * @throws NullPointerException
180 	 *             if <code>locale</code> is null
181 	 * @since 0.9
182 	 */
183 	public boolean setLocale(Locale locale)
184 	{
185 		if (baseName != null && loader != null) {
186 			setResources(baseName, locale, loader);
187 			return true;
188 		}
189 		else {
190 			this.locale = locale;
191 		}
192 		return false;
193 	}
194 
195 	/**
196 	 * Sets the locale of the text in the source code.
197 	 * <p>
198 	 * Only languages that have one singular and one plural form can be used as
199 	 * source code locales, since {@link #trn(String, String, long)} takes
200 	 * exactly these two forms as parameters.
201 	 * 
202 	 * @param locale
203 	 *            the locale
204 	 * @throws NullPointerException
205 	 *             if <code>locale</code> is <code>null</code>
206 	 * @see #trc(String, String)
207 	 * @since 0.9
208 	 */
209 	public void setSourceCodeLocale(Locale locale)
210 	{
211 		if (locale == null) {
212 			throw new NullPointerException("locale must not be null");
213 		}
214 		sourceCodeLocale = locale;
215 	}
216 
217 	/**
218 	 * Returns <code>text</code> translated into the currently selected
219 	 * language. Every user-visible string in the program must be wrapped into
220 	 * this function.
221 	 * 
222 	 * @param text
223 	 *            text to translate
224 	 * @return the translation
225 	 * @since 0.9
226 	 */
227 	public final String tr(String text)
228 	{
229 		try {
230 			return bundle.getString(text);
231 		}
232 		catch (MissingResourceException e) {
233 			return text;
234 		}
235 	}
236 
237 	/**
238 	 * Returns <code>text</code> translated into the currently selected
239 	 * language.
240 	 * <p>
241 	 * Occurrences of {number} placeholders in text are replaced by
242 	 * <code>objects</code>.
243 	 * <p>
244 	 * Invokes
245 	 * {@link MessageFormat#format(java.lang.String, java.lang.Object[])}.
246 	 * 
247 	 * @param text
248 	 *            text to translate
249 	 * @param objects
250 	 *            arguments to <code>MessageFormat.format()</code>
251 	 * @return the translated text
252 	 * @since 0.9
253 	 */
254 	public final String tr(String text, Object[] objects)
255 	{
256 		return MessageFormat.format(tr(text), objects);
257 	}
258 
259 	/**
260 	 * Convenience method that invokes {@link #tr(String, Object[])}.
261 	 * 
262 	 * @since 0.9
263 	 */
264 	public final String tr(String text, Object o1)
265 	{
266 		return tr(text, new Object[]{ o1 });
267 	}
268 
269 	/**
270 	 * Convenience method that invokes {@link #tr(String, Object[])}.
271 	 * 
272 	 * @since 0.9
273 	 */
274 	public final String tr(String text, Object o1, Object o2)
275 	{
276 		return tr(text, new Object[]{ o1, o2 });
277 	}
278 
279 	/**
280 	 * Convenience method that invokes {@link #tr(String, Object[])}.
281 	 * 
282 	 * @since 0.9
283 	 */
284 	public final String tr(String text, Object o1, Object o2, Object o3)
285 	{
286 		return tr(text, new Object[]{ o1, o2, o3 });
287 	}
288 
289 	/**
290 	 * Convenience method that invokes {@link #tr(String, Object[])}.
291 	 * 
292 	 * @since 0.9
293 	 */
294 	public final String tr(String text, Object o1, Object o2, Object o3, Object o4)
295 	{
296 		return tr(text, new Object[]{ o1, o2, o3, o4 });
297 	}
298 
299 	/**
300 	 * Returns the plural form for <code>n</code> of the translation of
301 	 * <code>text</code>.
302 	 * 
303 	 * @param text
304 	 *            the key string to be translated.
305 	 * @param pluralText
306 	 *            the plural form of <code>text</code>.
307 	 * @param n
308 	 *            value that determines the plural form
309 	 * @return the translated text
310 	 * @since 0.9
311 	 */
312 	public final String trn(String text, String pluralText, long n)
313 	{
314 		try {
315 			return trnInternal(bundle, text, pluralText, n);
316 		}
317 		catch (MissingResourceException e) {
318 			return (n == 1) ? text : pluralText;
319 		}
320 	}
321 
322 	/**
323 	 * Returns the plural form for <code>n</code> of the translation of
324 	 * <code>text</code>.
325 	 * 
326 	 * @param text
327 	 *            the key string to be translated.
328 	 * @param pluralText
329 	 *            the plural form of <code>text</code>.
330 	 * @param n
331 	 *            value that determines the plural form
332 	 * @param objects
333 	 *            object args to be formatted and substituted.
334 	 * @return the translated text
335 	 * @since 0.9
336 	 */
337 	public final String trn(String text, String pluralText, long n, Object[] objects)
338 	{
339 		return MessageFormat.format(trn(text, pluralText, n), objects);
340 	}
341 
342 	/**
343 	 * Overloaded method that invokes
344 	 * {@link #trn(String, String, long, Object[])} passing <code>Object</code>
345 	 * arguments as an array.
346 	 * 
347 	 * @since 0.9
348 	 */
349 	public final String trn(String text, String pluralText, long n, Object o1)
350 	{
351 		return trn(text, pluralText, n, new Object[]{ o1 });
352 	}
353 
354 	/**
355 	 * Overloaded method that invokes
356 	 * {@link #trn(String, String, long, Object[])} passing <code>Object</code>
357 	 * arguments as an array.
358 	 * 
359 	 * @since 0.9
360 	 */
361 	public final String trn(String text, String pluralText, long n, Object o1, Object o2)
362 	{
363 		return trn(text, pluralText, n, new Object[]{ o1, o2 });
364 	}
365 
366 	/**
367 	 * Overloaded method that invokes
368 	 * {@link #trn(String, String, long, Object[])} passing <code>Object</code>
369 	 * arguments as an array.
370 	 * 
371 	 * @since 0.9
372 	 */
373 	public final String trn(String text, String pluralText, long n, Object o1, Object o2, Object o3)
374 	{
375 		return trn(text, pluralText, n, new Object[]{ o1, o2, o3 });
376 	}
377 
378 	/**
379 	 * Overloaded method that invokes
380 	 * {@link #trn(String, String, long, Object[])} passing <code>Object</code>
381 	 * arguments as an array.
382 	 * 
383 	 * @since 0.9
384 	 */
385 	public final String trn(String text, String pluralText, long n, Object o1, Object o2, Object o3, Object o4)
386 	{
387 		return trn(text, pluralText, n, new Object[]{ o1, o2, o3, o4 });
388 	}
389 
390 	/**
391 	 * Returns the plural form for <code>n<code> of the translation of ???
392 	 *      
393 	 * Based on GettextResource.java that is part of GNU gettext for Java
394 	 * Copyright (C) 2001 Free Software Foundation, Inc.
395 	 * 
396 	 * @param bundle a ResourceBundle
397 	 * @param text the key string to be translated, an ASCII string
398 	 * @param pluralText its English plural form
399 	 * @return the translation of <code>text</code> depending on <code>n</code>,
400 	 *         or <code>text</code> or <code>pluralText</code> if none is found
401 	 */
402 	private static String trnInternal(ResourceBundle orgBundle, String text, String pluralText, long n)
403 	{
404 		ResourceBundle bundle = orgBundle;
405 		do {
406 			boolean isGetTextBundle = false;
407 			boolean hasPluralHandling = false;
408 			Method handleGetObjectMethod = null;
409 			Method getParentMethod = null;
410 			Method lookupMethod = null;
411 			Method pluralEvalMethod = null;
412 			try {
413 				handleGetObjectMethod = bundle.getClass().getMethod("handleGetObject", new Class[]{ String.class });
414 				getParentMethod = bundle.getClass().getMethod("getParent", new Class[0]);
415 				isGetTextBundle = Modifier.isPublic(handleGetObjectMethod.getModifiers());
416 				lookupMethod = bundle.getClass().getMethod("lookup", new Class[]{ String.class });
417 				pluralEvalMethod = bundle.getClass().getMethod("pluralEval", new Class[]{ Long.TYPE });
418 				hasPluralHandling = true;
419 			}
420 			catch (Exception e) {}
421 			if (isGetTextBundle) {
422 				// GNU gettext generated bundle
423 				if (hasPluralHandling) {
424 					// GNU gettext generated bundle w/ plural handling
425 					try {
426 						Object localValue = lookupMethod.invoke(bundle, new Object[]{ text });
427 						if (localValue.getClass().isArray()) {
428 							String[] pluralforms = (String[])localValue;
429 							long index = 0;
430 							try {
431 								index = ((Long)pluralEvalMethod.invoke(bundle, new Object[]{ new Long(n) }))
432 										.longValue();
433 								if (!(index >= 0 && index < pluralforms.length)) {
434 									index = 0;
435 								}
436 							}
437 							catch (IllegalAccessException e) {}
438 							return pluralforms[(int)index];
439 						}
440 						else {
441 							// Found the value. It doesn't depend on n in this
442 							// case.
443 							return (String)localValue;
444 						}
445 					}
446 					catch (Exception e) {}
447 				}
448 				else {
449 					// GNU gettext generated bundle w/o plural handling
450 					try {
451 						Object localValue = handleGetObjectMethod.invoke(bundle, new Object[]{ text });
452 						if (localValue != null) {
453 							return (String)localValue;
454 						}
455 					}
456 					catch (Exception e) {}
457 				}
458 				bundle = null;
459 				try {
460 					bundle = (ResourceBundle)getParentMethod.invoke(bundle, new Object[0]);
461 				}
462 				catch (Exception e) {}
463 			}
464 			else {
465 				return bundle.getString(text);
466 			}
467 		}
468 		while (bundle != null);
469 		throw new MissingResourceException("Can not find resource for key " + text + " in bundle "
470 				+ orgBundle.getClass().getName(), orgBundle.getClass().getName(), text);
471 	}
472 
473 	/**
474 	 * Disambiguates translation keys.
475 	 * 
476 	 * @param comment
477 	 *            the text translated + a disambiguation hint in brackets.
478 	 * @param text
479 	 *            the ambiguous key string
480 	 * @return <code>text</code> if the locale of the underlying resource
481 	 *         bundle equals the source code locale, the translation of
482 	 *         <code>comment</code> otherwise.
483 	 * @see #setSourceCodeLocale(Locale)
484 	 * @since 0.9
485 	 */
486 	public final String trc(String comment, String text)
487 	{
488 		return sourceCodeLocale.equals(getResources().getLocale()) ? text : tr(comment);
489 	}
490 
491 }