/*
TranslationService.java
Utility class to provide localized string assembly services.
This class is designed to use localized properties files to do
string lookup and synthesis.
Created: 7 May 2004
Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu
-----------------------------------------------------------------------
Ganymede Directory Management System
Copyright (C) 1996-2013
The University of Texas at Austin
Ganymede is a registered trademark of The University of Texas at Austin
Contact information
Author Email: ganymede_author@arlut.utexas.edu
Email mailing list: ganymede@arlut.utexas.edu
US Mail:
Computer Science Division
Applied Research Laboratories
The University of Texas at Austin
PO Box 8029, Austin TX 78713-8029
Telephone: (512) 835-3200
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package arlut.csd.Util;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
/*------------------------------------------------------------------------------
class
TranslationService
------------------------------------------------------------------------------*/
/**
* <p>Utility class to provide localized string assembly services.
* This class is designed to use localized properties files to do
* string lookup and synthesis.</p>
*
* <p>The Ganymede source tree uses TranslationService pervasively to
* handle translation for all text message strings in the server,
* client, and admin console.</p>
*
* <p>This class must be used in conjunction with a particular usage
* convention in order to support automatic validation of message
* files with the 'ant validate' task in the Ganymede build.xml file.</p>
*
* <p>That convention is that every class that needs language
* translation services must declare a static final TranslationService
* object named 'ts' which is initialized with the fully qualified
* name of the class to which it belongs.</p>
*
* <p>Actual translation calls should all be in the form of
* ts.l("messageName"), possibly with extra parameters, such as
* ts.l("messageName", arg1, arg2), and so forth. The 'l' method of
* TranslationService is designed to be as compact as possible so that
* it can be used wherever you would normally use a string. It
* returns a String formatted according to whatever language-sensitive
* property files are defined in Ganymede's src/resources
* directory.</p>
*
* <p>If you follow these conventions, the 'ant validate' task will be
* able to automatically analyze your code and cross reference it
* against the message property files under src/resources. 'ant
* validate' will warn you if you are missing messages, or if the
* messages are malformed, or if the messages specify a different
* number of parameters than the source code which users the messages
* is expecting.</p>
*
* <p>So, for use within Ganymede, please be sure always to follow
* these conventions.</p>
*
* @author Jonathan Abbey
*/
public final class TranslationService {
/**
* <p>This is a factory method for creating TranslationService
* objects. We use a factory method so that we have the option
* later on of tracking the TranslationService objects we create and
* possibly supporting some kind of reload functionality, howsomever
* primitive.</p>
*
* @param resourceName The package-qualified resource class name to
* load. We automatically support the use of properties files here.
*
* @param locale A Locale object controlling the particular region we're
* going to try and get translations for, though we'll fallback to our
* generic translation classes (i.e., the English ones) if we don't have
* any localization classes for the given Locale.
*/
public static TranslationService getTranslationService(String resourceName, Locale locale) throws MissingResourceException
{
return new TranslationService(resourceName, locale);
}
/**
* <p>This is a factory method for creating TranslationService
* objects. We use a factory method so that we have the option
* later on of tracking the TranslationService objects we create and
* possibly supporting some kind of reload functionality, howsomever
* primitive.</p>
*
* @param resourceName The package-qualified resource class name to
* load. We automatically support the use of properties files here.
*/
public static TranslationService getTranslationService(String resourceName) throws MissingResourceException
{
return new TranslationService(resourceName, null);
}
// ---
private ResourceBundle bundle = null;
private Locale ourLocale = null;
private MessageFormat formatter = null;
private String lastPattern = null;
private String resourceName;
private int wordWrapCols=0;
/* -- */
/**
* <p>We've declared the constructor private so as to force use of
* the static factory method.</p>
*
* @param resourceName The package-qualified resource class name to
* load. We automatically support the use of properties files here.
*
* @param locale A Locale object controlling the particular region we're
* going to try and get translations for, though we'll fallback to our
* generic translation classes (i.e., the English ones) if we don't have
* any localization classes for the given Locale.
*/
private TranslationService(String resourceName, Locale locale) throws MissingResourceException
{
if (locale != null)
{
this.ourLocale = locale;
}
else
{
this.ourLocale = Locale.getDefault();
}
this.resourceName = resourceName;
}
/**
* <p>Private helper to handle lazy-loading of the resource bundle.</p>
*/
private synchronized ResourceBundle getBundle()
{
if (this.bundle == null)
{
this.bundle = ResourceBundle.getBundle(resourceName, ourLocale);
}
return this.bundle;
}
/**
* <p>This method sets the desired word wrap columns for this
* TranslationService. Any formatted strings returned by this
* TranslationService will be wrapped to the number of columns
* specified.</p>
*
* <p>If cols is 0, word wrapping will be disabled.</p>
*
* @return The number of columns that this TranslationService was previously
* wrapping at
*/
public int setWordWrap(int cols)
{
int old_value = this.wordWrapCols;
if (cols <= 0)
{
wordWrapCols = 0;
}
else
{
wordWrapCols = cols;
}
return old_value;
}
/**
* <p>This method returns true if this TranslationService has a
* non-empty, non-null resource string corresponding to the key
* parameter.</p>
*/
public boolean hasPattern(String key)
{
String pattern;
/* -- */
try
{
pattern = getBundle().getString(key);
}
catch (MissingResourceException ex)
{
return false;
}
if (pattern.equals(""))
{
return false;
}
return true;
}
/**
* <p>This method takes a localization key and returns the localized
* string that matches it in this TranslationService.</p>
*
* <p>This heavily overloaded method is called 'l' for localize, and
* it has such a short name so that I can use it everywhere in
* Ganymede without concern.</p>
*/
public String l(String key)
{
String pattern = null;
String result;
/* -- */
try
{
pattern = getBundle().getString(key);
}
catch (MissingResourceException ex)
{
return null;
}
// we don't actually need to use format with no template
// substitutions, but if we don't, then we'll have different
// property file rules for how to handle single quotes in the two
// cases. Easier just to always use format and insist that all
// single quotes in the property files are doubled up.
// Thanks Sun, for such a brain-dead quoting system. What was
// wrong with backslash-escape, again?
result = this.format(pattern, null);
return result;
}
/**
* <p>This method takes a localization key and a parameter and
* creates a localized string out of them.</p>
*
* <p>This method is called 'l' for localize, and it has such a
* short name so that I can use it everywhere in Ganymede with
* minimal source code disruption.</p>
*
* <p>This method obviously requires Java 5 due to its use of
* varargs.</p>
*/
public String l(String key, Object... params)
{
String pattern = null;
/* -- */
try
{
pattern = getBundle().getString(key);
}
catch (MissingResourceException ex)
{
return null;
}
return this.format(pattern, params);
}
public String toString()
{
return("TranslationService: " + resourceName + ", locale = " + ourLocale.toString());
}
private String format(String pattern, Object params[])
{
String result = null;
// we have to synchronize to protect the formatter and lastPattern
// objects from concurrent use
synchronized (this)
{
if (formatter == null)
{
formatter = new MessageFormat(pattern);
}
else if (!pattern.equals(this.lastPattern))
{
formatter.applyPattern(pattern);
lastPattern = pattern;
}
result = formatter.format(pattern, params);
}
if (wordWrapCols > 0 && result.length() > wordWrapCols)
{
result = WordWrap.wrap(result, wordWrapCols);
}
return result;
}
}