On my current GWT-GXT project, I wanted to enable the application for multiple locales. GWT offers many ways to internationalize your application. To give you an architectural context, services are front-ending client requests and when exceptional situations arise, these services throw exceptions containing Message Key and not messages, along with message parameters as additional information. Using message keys indirection it allows us to translate that message key in to locale-specific message, a standard technique.
It is the responsibility of the page renderer to then pick up the correct locale-specific resource bundle and render the locale-specific message based on the message keys present in the locale-specific properties file. With GWT, this gets very interesting. The way GWT folks have thought about this is really cool. They offer various internationalization techniques Static string internationalization, Dynamic string internationalization and Extending or implementing Localizable. For us, Static String internationalization suited the best.
GWT provides a Messages interface that one needs to extend for their use. So, say for example, I have a GatewayService that throws following message keys wrapped in its custom exception
public final class GatewayMessageKeys {
private GatewayMessageKeys() {
}
public static final String INCORRECT_CREDENTIALS = "incorrectCredentials";
public static final String ACCOUNT_DOES_NOT_EXIST = "accountDoesNotExist";
public static final String SERVICE_UNAVAILABLE = "serviceUnavailable";
public static final String NOT_LOGGED_IN = "notLoggedIn";
public static final String LOGOUT_SUCCESSFUL = "logoutSuccessful";
public static final String LOGOUT_ERROR = "logoutError";
public static final String SESSION_TIMEOUT = "sessionTimeout";
}
For these message keys, i'll extend GWT provided Messages interface as...
import com.google.gwt.i18n.client.Messages;
public interface GatewayMessages extends Messages {
String incorrectCredentials();
String accountDoesNotExist();
String serviceNotAvailable();
String notLoggedIn();
String sessionTimeout();
String logoutSuccessful();
String logoutError();
}
Once I look at this interface...the aha moment dawns on me! The concept is so clear....any message-key that you throw from services, manifests as method in the GatewayMessages interface. The beauty is that this method always returns a String based message and takes in any type parameters that you would want to substitute in the final localized message. So, the rendering of a localized message happens using a function-based approach and bunch of these functions are grouped together in an interface...so cohesively packed!
To me this is an innovative concept for implementing localization. GWT calls these methods as Message Accessors. These methods have bindings to properties files for localized messages. At runtime, GWT automatically provides an instance of an generated subclass that is implemented using values from a property file selected based on locale...I'm impressed! So to start with, we need make the Application.gwt.xml inherit the internationalization module.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.7.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/1.7.0/distro-source/core/src/gwt-module.dtd">
<module rename-to='applicationName'>
<!-- Inherit the core Web Toolkit stuff. -->
<inherits name='com.google.gwt.user.User'/>
<!-- Inherit the GXT stuff. -->
<inherits name='com.extjs.gxt.ui.GXT'/>
<!-- Inherit Internationalization module -->
<inherits name="com.google.gwt.i18n.I18N"/>
<!-- English language, independent of country -->
<extend-property name="locale" values="en"/>
<!-- Specify the app entry point class. -->
<entry-point class='com.company.product.client.Application'/>
</module>
Having done that, you will want to put this to use and the place where it would generally end-up is in the response handler's onFailure() method. Below is one such sample call.
AsyncCallback<User> loginActionResponseHandler = new AsyncCallback<User>() {
public void onFailure(Throwable caught) {
String messageKey = caught.getMessage();
String message = null;
if(GatewayMessageKeys.INCORRECT_CREDENTIALS.equals(messageKey)) {
gatewayMessages = (GatewayMessages) GWT.create(GatewayMessages.class);
message = gatewayMessages.incorrectCredentials();
}
show(message);
}
public void onSuccess(User authenticated) {
// do something
}
};
Now, the
GatewayMessages interface, has several methods and based on the type of the return Key and I already can see bunch of ugly if-branches when it comes to selecting the correct method to call based on the incoming message key. This job of translating message key to message demands a role of its own....As I BDD, the design this role of a
MessageKeyTranslator comes into being...below are the behavior specifications
@RunWith(MockitoJUnit44Runner.class)
public class GatewayMessageKeyTranslatorSpecs {
@Mock
private GatewayMessages mockGatewayMessages;
private GatewayMessageKeyTranslator messageKeyTranslator;
@Before
public void givenThatIHave() {
messageKeyTranslator = new GatewayMessageKeyTranslator(mockGatewayMessages);
}
@Test
public void itTranslatesIncorrectCredentialsKey() {
String messageToReturn = "[Incorrect Credentials]";
when(mockGatewayMessages.incorrectCredentials()).thenReturn(messageToReturn);
assertThat(messageKeyTranslator.translate(GatewayMessageKeys.INCORRECT_CREDENTIALS), is(messageToReturn));
verify(mockGatewayMessages).incorrectCredentials();
}
@Test
public void itTranslatesAccountDoesNotExistKey() {
String messageToReturn = "[Account Does Not Exist]";
when(mockGatewayMessages.accountDoesNotExist()).thenReturn(messageToReturn);
assertThat(messageKeyTranslator.translate(GatewayMessageKeys.ACCOUNT_DOES_NOT_EXIST), is(messageToReturn));
verify(mockGatewayMessages).accountDoesNotExist();
}
@Test
public void itTranslatesServiceUnavailableKey() {
String messageToReturn = "[Service Not Available]";
when(mockGatewayMessages.serviceNotAvailable()).thenReturn(messageToReturn);
assertThat(messageKeyTranslator.translate(GatewayMessageKeys.SERVICE_UNAVAILABLE), is(messageToReturn));
verify(mockGatewayMessages).serviceNotAvailable();
}
@Test
public void itTranslatesNotLoggedInKey() {
String messageToReturn = "[Not Logged In]";
when(mockGatewayMessages.notLoggedIn()).thenReturn(messageToReturn);
assertThat(messageKeyTranslator.translate(GatewayMessageKeys.NOT_LOGGED_IN), is(messageToReturn));
verify(mockGatewayMessages).notLoggedIn();
}
@Test
public void itTranslatesSessionTimeoutKey() {
String messageToReturn = "[Session Timeout]";
when(mockGatewayMessages.sessionTimeout()).thenReturn(messageToReturn);
assertThat(messageKeyTranslator.translate(GatewayMessageKeys.SESSION_TIMEOUT), is(messageToReturn));
verify(mockGatewayMessages).sessionTimeout();
}
@Test
public void itTranslatesLogoutSuccessfulKey() {
String messageToReturn = "[Logout Successful]";
when(mockGatewayMessages.logoutSuccessful()).thenReturn(messageToReturn);
assertThat(messageKeyTranslator.translate(GatewayMessageKeys.LOGOUT_SUCCESSFUL), is(messageToReturn));
verify(mockGatewayMessages).logoutSuccessful();
}
@Test
public void itTranslatesLogoutErrorKey() {
String messageToReturn = "[Logout Error]";
when(mockGatewayMessages.logoutError()).thenReturn(messageToReturn);
assertThat(messageKeyTranslator.translate(GatewayMessageKeys.LOGOUT_ERROR), is(messageToReturn));
verify(mockGatewayMessages).logoutError();
}
@Test
public void itTranslatesEmptyKey() {
String messageToReturn = "";
assertThat(messageKeyTranslator.translate(""), is(messageToReturn));
verify(mockGatewayMessages, never()).incorrectCredentials();
verify(mockGatewayMessages, never()).accountDoesNotExist();
verify(mockGatewayMessages, never()).serviceNotAvailable();
verify(mockGatewayMessages, never()).notLoggedIn();
verify(mockGatewayMessages, never()).sessionTimeout();
verify(mockGatewayMessages, never()).logoutError();
verify(mockGatewayMessages, never()).logoutSuccessful();
}
@Test
public void itTranslatesNullKey() {
String messageToReturn = "";
assertThat(messageKeyTranslator.translate(null), is(messageToReturn));
verify(mockGatewayMessages, never()).incorrectCredentials();
verify(mockGatewayMessages, never()).accountDoesNotExist();
verify(mockGatewayMessages, never()).serviceNotAvailable();
verify(mockGatewayMessages, never()).notLoggedIn();
verify(mockGatewayMessages, never()).sessionTimeout();
verify(mockGatewayMessages, never()).logoutError();
verify(mockGatewayMessages, never()).logoutSuccessful();
}
}
Here is the GatewayMessageKeyTranslator that got flushed out from the above specs.
public class GatewayMessageKeyTranslator implements MessageKeyTranslator {
private GatewayMessages gatewayMessages;
public GatewayMessageKeyTranslator() {
gatewayMessages = (GatewayMessages) GWT.create(GatewayMessages.class);
}
public GatewayMessageKeyTranslator(GatewayMessages gatewayMessages) {
this.gatewayMessages = gatewayMessages;
}
public String translate(String messageKey, Object... args) {
if (GatewayMessageKeys.INCORRECT_CREDENTIALS.equals(messageKey)){
return gatewayMessages.incorrectCredentials();
}
else if (GatewayMessageKeys.ACCOUNT_DOES_NOT_EXIST.equals(messageKey)){
return gatewayMessages.accountDoesNotExist();
}
else if (GatewayMessageKeys.SERVICE_UNAVAILABLE.equals(messageKey)){
return gatewayMessages.serviceNotAvailable();
}
else if (GatewayMessageKeys.NOT_LOGGED_IN.equals(messageKey)){
return gatewayMessages.notLoggedIn();
}
else if (GatewayMessageKeys.SESSION_TIMEOUT.equals(messageKey)){
return gatewayMessages.sessionTimeout();
}
else if (GatewayMessageKeys.LOGOUT_SUCCESSFUL.equals(messageKey)){
return gatewayMessages.logoutSuccessful();
}
else if (GatewayMessageKeys.LOGOUT_ERROR.equals(messageKey)){
return gatewayMessages.logoutError();
}
else {
return "";
}
}
}
The implementation is really ugly with so-many if branches...but my point here is to get the role with responsibility of message key translation flushed out first...basically get it working and then make it clean!
Further...there are many services in the system and a similar approach is required at other places...so, I need to come up with an abstraction using which all the translators in the system can work with...so I extract MessageKeyTranslator interface . Such an interface can reside in a package of its own, and doing so will result in a package with Abstractness value to 1, Instability value close to 0...which is good.
public interface MessageKeyTranslator {
public String translate(String messageKey, Object ...args);
}
So, the client code, now becomes really clean...
AsyncCallback<User> loginActionResponseHandler = new AsyncCallback<User>() {
public void onFailure(Throwable caught) {
String messageKey = caught.getMessage();
MessageKeyTranslator translator = new GatewayMessageKeyTranslator();
String message = translator.translate(messageKey);
show(message);
}
public void onSuccess(User authenticated) {
// do something
}
};
Now that we have got things right, lets tackle the cleaning bit...Though this is plain Java code, I cannot use Java Reflection to select the method based upon the incoming key, because, GWT java code compiles to JavaScript eventually and hence java reflection will not work. There are other open-source GWT Reflection Apis, but I have not tried'em. By using reflection, I can then come-up with a generic implementation DefaultMessageKeyTranslator which would then be closed for modification. I would have then avoided the concrete translators proliferation. If you have other ideas/comments/suggestions, do send them in....
If you feel this article has helped you, leave a comment below and if others can benefit from this article, share it: