1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
423 if (hasPluralHandling) {
424
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
442
443 return (String)localValue;
444 }
445 }
446 catch (Exception e) {}
447 }
448 else {
449
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 }