Best Practices for Internationalized Tools in Sakai
This document describes how to write Sakai tools that are localized and internationalized.
Contents
Best Practices
- Follow the standards described in this guide
- Always use properties files for user interface text; never include displayable text in java, jsp, or templates (see Localizing Tools below)
- Use properties files only for user interface text and config files for configuration settings (see #Config Properties below).
- Use a proper naming schema for keys in your resource bundles. The name of the keys should provide some information about the context of the displayed text. This helps the translators during the translation process. A naming schema could be
<view>.<type>.<name>
. Examples:edit.button.save
,edit.label.title
orlist.action.addnew
- Group keys by view, ie.
edit.title
,edit.instructions
,list.title
,list.instructions
,create.title
, etc - Never use displayable text when executing comparisons within the logic of the tool (separate codified values from displayable text)
- Always use the ResourceLoader (not ResourceBundle) class for retrieving properties values, and invoke ResourceLoader methods dynamically, to accommodate dynamic user preferences (see Resource Loader below).
- All dynamically constructed phrases must be sensitive to locale specific subject/verb/object ordering by using the Sakai ResourceLoader class: org.sakaiproject.util.ResourceLoader.getFormattedMessage() (see Structure messages appropriately below)
- All numbers and dates should be formatted specific to the user's locale (e.g. java.text.NumberFormat and java.text.DateFormat)
- Test tools in more than one language
Localizing Tools
All language based text should be localized into a properties file (e.g. Messages.properties), specific to each tool. This includes static text rendered from JSF/RSF/Velocity/etc. templates, and dynamic text inserted from the tool classes.
For example:
page.message.key = This is a message which the user will see
Standard Java formatting classes, such as MessageFormat, ChoiceFormat, DateFormat, NumberFormat and DecimalFormat are preferred over tool-specific formatting, which may not be sensitive to international formats. The org.sakaiproject.util.ResourceLoader class provides getFormattedMessage (wrapper for MessageFormat) and getLocale methods to assist in formatting.
There is an excellent tutorial on internationalization at http://java.sun.com/docs/books/tutorial/i18n/index.html as well as a checklist at http://java.sun.com/docs/books/tutorial/i18n/intro/checklist.html.
Beginning in Sakai 2.9, the citations helper will use a new way of configuring search sources in its config.xml file (which is loaded dynamically from a resources folder in the citations-admin site). This is done so changes can be made at run-time rather than at compile-time or startup-time. The config.xml file includes strings that should be internationalized. The format of that file is described in the I18N for Citations Helper in Sakai 2.9.
Structure messages appropriately
Don't split a simple statement across multiple messages in order to place variables in the message. Different languages use different subject/verb/object sentence constructs. The org.sakaiproject.util.ResourceLoader.getFormattedMessage() method will format variable text based on a user's preferred locale (using the same arguments defined by the java.util.MessageFormat class):
Example Final Statement:
Welcome to the awesome view, Mr. User.
Wrong Way:
page.statement.1 = Welcome to the page.statement.2 = view,
Right Way:
# Sample: Welcome to the (page title) view, (user display name). page.statement = Welcome to the {0} view, {1}
Localize Date & Time
Here's an example of creating a DateFormat object to format a date according to local conventions:
DateFormat df = DateFormat.getDateInstance( DateFormat.SHORT, (new ResourceLoader()).getLocale() );
Here's an example of creating a DateFormat object to format a date according to local conventions, and extracting the pattern string for later use:
DateFormat df = DateFormat.getDateInstance( DateFormat.SHORT, (new ResourceLoader()).getLocale() ); date_entry_format_description = ((SimpleDateFormat)df).toPattern();
Config Properties
Properties files (<filename>.properties) should only be used for user interface text that should be translated. All other configuration information (e.g. configuration constants, class names, filenames, etc.) should be in a separate directory tree. Alternately, config files can use the <filename>.config extension and be referenced as follows:
import java.util.Properties; ... Properties p = new Properties(); p.load(this.getClass().getResourceAsStream("filename.config"));
ResourceLoader Class
The org.sakaiproject.util.java.ResourceLoader class is a wrapper class for the ResourceBundle class. It provides dynamic language/locale support for individual users, and is recommended for all Sakai tools.
The sakai-util.jar file (or sakai-util-java.jar file prior to Sakai 2.2) needs to be included with every tool during the maven build process, by including the following dependency in the tool's project.xml file:
<dependency> <groupId>org.sakaiproject</groupId> <artifactId>sakai-util</artifactId> <version>${sakai.version}</version> <properties> <war.bundle>true</war.bundle> </properties> </dependency>
Since Sakai 2.6 (Kernel 1.0+), this is now sakai-kernel-util:
<dependency> <groupId>org.sakaiproject.kernel</groupId> <artifactId>sakai-kernel-util</artifactId> </dependency>
Strings in the tool's properties file can be retrieved in the Java code using the ResourceLoader class:
ResourceLoader rb = new ResourceLoader("_org.sakaiproject.tool.foobar.bundle.Messages_"); String foo = rb.getString("foo");
JSF based tools
Each tool's properties file should be loaded as a managed-bean in it's faces-config.xml file:
<managed-bean> <description> Dynamic Resource Bundle Loader </description> <managed-bean-name>msgs</managed-bean-name> <managed-bean-class>org.sakaiproject.util.java.ResourceLoader</managed-bean-class> <managed-bean-scope>session</managed-bean-scope> <managed-property> <description>Bundle baseName</description> <property-name>baseName</property-name> <value>_org.sakaiproject.tool.foobar.bundle.Messages_</value> </managed-property> </managed-bean>
An alternative to modifying the faces-config.xml file is to insert the following into the jsf file:
<jsp:useBean id="msgs" class="org.sakaiproject.util.ResourceLoader" scope="session"> <jsp:setProperty name="msgs" property="baseName" value="_org.sakaiproject.tool.foobar.bundle.Messages_"/> </jsp:useBean>
Any reference to <f:loadBundle> should be removed from all JSF template files.
Localized strings in properties files are referenced using standard JSF variable syntax:
<h:outputText value="#{msgs.foo}"/>
If you have parameters, you should use the practice of putting these in the messages like the other methods and using outputFormat for format them. The first parameter will go in {0} and the second in {1}, etc.
<h:outputFormat value="#{authorMessages.cert_rem_assmt}" escape="false"> <f:param value="#{publishedassessment.title}"/> </h:outputFormat>
JSP based tools
The ResourceLoader can be initialized by inserting the following into the jsp file:
<jsp:useBean id="msgs" class="org.sakaiproject.util.ResourceLoader" scope="request"> <jsp:setProperty name="msgs" property="baseName" value="_org.sakaiproject.tool.foobar.bundle.Messages_"/> </jsp:useBean>
If the bean is defined as above, localized strings in properties files can be referenced using standard JSP variable syntax:
<c:out value="${msgs.foo}"/>
JSTL <fmt:setLocale> and <fmt:setBundle> should not be necessary for <fmt:message> to work, but with Spring MVC, a localeResolver bean must be registered to handle proper system/site/user settings. An example of this is implemented for metaobj and OSP (SakaiLocaleResolver). Note that it could be moved to somewhere like sakai-kernel-util if others need it; send a message to sakai-dev if this is the case. The localeResolver bean can be set in web-config.xml like this:
<bean id="localeResolver" class="org.sakaiproject.spring.util.SakaiLocaleResolver"/>
If using the SpringMVC approach (with messageSource defined), the JSTL tags will work as expected:
<fmt:message key="foo"/>
A major dvantage to the JSTL tags is parameter support:
<fmt:message key="foo"/> <fmt:param value="${bar}"/> </fmt:message>
NOTE: It should also be possible to inject the locale into the model from ResourceLoader.getLocale() and use <fmt:setLocale> if needed
Velocity based tools
Reference to the ResourceLoader object class can be passed to a Velocity template in it's context:
ResourceLoader rb = new ResourceLoader("_org.sakaiproject.tool.foobar.bundle.Messages_"); context.put("tlang", rb );
Then any strings can be referenced as standard velocity variables:
$tlang.getString("foo");
For Sakai 2.8 and newer you can also supply parameters to getFormattedMessage such as:
$tlang.getFormattedMessage("foo", $value)
Javascript based tools
Javascript requires special handling to identify the preferred Sakai language locale and pull the translation from the correct properties file.
There doesn't appear to be a super way to get this. One way would be to put the users locale in your template as a javascript variable using the ResourceLoader
import org.sakaiproject.util.ResourceLoader;
. . .
ResourceLoader rl = new ResourceLoader(); return rl.getLocale();
Then use this javascript library to read the locale properties files directly: https://code.google.com/p/jquery-i18n-properties/
However some tools don't have their properties file in flat files so this wouldn't work unless they were repackaged to deploy them differently.
This method suggests creating a custom entity which will return the correct resource bundle's json: http://www.slideshare.net/lovenalube/thats-a-nice-ui-but-have-you-internationalized-your-javascript
Ideally, I think the best/easiest way would be if there was a new direct interface to the resource loader with methods to get a message key from a specified bundle the message in the users locale. (This doesn't exist right now)
getString(bundleName,key) {
return ResourceBundle.getBundle(bundleName, getUserPreferredLocale()).getString(key);
}
Or possibly more efficiently, just return the entire bundle in the UsersPreferredLocale as json.
getBundle(bundleName) {
return ResourceBundle.getBundle(bundleName, getUserPreferredLocale();
}
I have no example yet, please attach example(s).
RSF based tools
RSF has a few ways to handle internationalized messages. The simplest way is to place the message key right in the RSF template. The default location for the Messages.properties file in RSF tools is tool/src/webapp/WEB-INF/messages but this is configurable.
In order to use Sakai's ResourceLoader, the setting of messageSource should be done this way:
<bean id="messageSource" class="org.sakaiproject.util.ResourceLoaderMessageSource"> <property name="basename" value="classpath:org/sakaiproject/site/tool/participant/bundle/sitesetupgeneric"/> <property name="cacheSeconds" value="10" /> </bean>
However note that ResourceLoaderMessageSource is only present starting with 2.8. For code that needs to work in earlier releases, one possible approach is to copy that class into your project's source.
Here is an example of placing the message key in the template:
<span rsf:id="msg=page.user.message.key">This is an internationalized message.</span>
Here is an example of using the UIMessage class:
(From the RSF template)
<span rsf:id="my-rsf-id">This will be an internationalized message.</span>
(From the RSF producer)
UIMessage.make(tofill, "my-rsf-id", "page.user.message.key");
There is more information at the RSF Wiki I18n page
GWT (Google Web Toolkit) based tools
Integrating GWT tools with Sakai Internationalization is still a work in progress. Google does provide a Developers Guide to Internationalization that 's a good starting point.
Wicket based tools
Wicket has a few ways of rendering internationalised messages. For static messages, you can put them right into the HTML template:
<span><wicket:message key="some.message.key" /></span>
You can also set internationalised HTML attributes right into the HTML templates:
<table wicket:message="summary:my.great.table.message.key">
For the case where you need a handle on the component, ie for Wicket AJAX updates, add a Label component with a ResourceModel:
Label myLabel = new Label("myLabel", new ResourceModel("some.message.key")); someComponent.add(myLabel);
And for parameterised substitution, use a StringResourceModel instead, which allows an array of replacements:
Label myLabel = new Label("myLabel", new StringResourceModel("some.message.key.with.params", null, new Object[]{ value1, value2 } ));
If your properties files are not in the default location, you can implement your own ResourceLoader, perhaps taking advantage of the Sakai ResourceLoader. Drop this into your main WebApplication class.
//Custom resource loader private static class MyStringResourceLoader implements IStringResourceLoader { private ResourceLoader messages = new ResourceLoader("MyApplication"); public String loadStringResource(Component component, String key) { return messages.getString(key, key); } public String loadStringResource(Class clazz, String key, Locale locale, String style) { messages.setContextLocale(locale); return messages.getString(key, key); } }
And to configure your app to use it, set it up in your WebApplication init method:
// Custom resource loader since our properties are not in the default location getResourceSettings().addStringResourceLoader(new MyStringResourceLoader());
For more information about i18n in Wicket, see https://cwiki.apache.org/WICKET/general-i18n-in-wicket.html
Trimpath based tools
To internationalise applications written with Trimpath you have to use a jQuery plugin. You can find this plugin in this URL
For static messages, you can put them right into the HTML template:
<h2>${some_app_label}</h2>
You need to have one translation file per each language. Every key should be surronded with " " and finished by ";"
some_app_label="Example text"; another_string_key="Another";
Suggested reading
Introducing inheritance to PropertyResourceBundles
"Creating a fully internationalized Java application using PropertyResourceBundles can present some interesting design and implementation problems, including concern over how to modularize the bundles to be used in different areas of the application. In this article, we will explore a solution based on PropertyResourceBundles, which should simplify the design and implementation problems, while promoting reuse of existing bundles."
http://www-128.ibm.com/developerworks/java/library/j-bundles/