Internationalisation in Ext-JS using Mixins

We have been researching Ext-JS library. One area of concern was localisation. The preferred technique is to asynchronously load a Javascript file which after-the-fact modifies class prototypes to change text. Eeek! We found that there were timing problems, so how to solve it?

The Ext-JS class system provides a mechanism to

  • Compose classes using Mix-ins.
  • Ensure that the required Javascript files are loaded in the correct order as required.

This seems ideal as long as we can control the process that serves the language mix-in so that it serves in the right language.

My test class was generated using Ext-JS’s designer and is as follows:

Ext.define('workflow.view.MyForm', {
    extend: 'workflow.view.ui.MyForm',
    mixins : ["workflow.view.MyForm_I18N"],

    initComponent: function() {
        var me = this;
        me.callParent(arguments);
    	alert("View Mixin Test: " + this.text.hello);
    }
});

An example mixin that we use, when reformatted, is

Ext.define('workflow.view.MyForm_I18N',
{
	text:{
		hello:'Hello from the Java Resource System'
	}
});

As can be seen from my test text, my server platform is Java and I wish to use the Java resource system. This is great for our developers and our localisation team. The above was generated using a properties file.

hello = Hello from the Java Resource System

If you’re working with another server platform then you can use its technique. For example Apache has a means to serve different files depending on locale.

When Ext-JS requests a mix-in class it builds a file name of the form “{product}/app/{class.Name}.js“. Sadly the product name is converted to lower case so we lose this information in the request. I have made my product names lower case to work around this, but if this is a problem then the server will have to be made more intelligent.

A Servlet Filter detects any request for a file within an app directory ending _I18N.js. This is then forwarded to a Servlet which generates the mix-in code.

@WebFilter("*")
public class LocaleMixinFilter implements Filter
{
	private static final Pattern INTERESTING_FILES =
			Pattern.compile(".*?([^/]++)/app/(.*_I18N.js)");

// ...

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException
	{
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		String uri = httpRequest.getRequestURI();

		Matcher matcher = INTERESTING_FILES.matcher(uri);
		if(matcher.matches())
		{
			String product = matcher.group(1);
			String name = matcher.group(2);
			LOG.fine(getClass(), "Forwarding localisation request for {0}/{1}", product, name);
			request.getRequestDispatcher(LocaleMixinServlet.ROOT + product + "/" + name).forward(request, response);
		}
		else
		{
			chain.doFilter(request, response);
		}
	}
}

I’ve gone a little to town on the Servlet. I’ve not implemented cache control yet. That can happen later. I have allowed properties to be structured rather than just flat name/value pairs. For example the properties file

hello = Hello from the Java Resource System
panelLabels.top = The Top
panelLabels.bottom = The Bottom

results in

Ext.define('workflow.view.MyForm_I18N',
{
	text:
	{
		hello:'Hello from the Java Resource System',
		panelLabels:
		{
			bottom:'The Bottom',
			top:'The Top'
		}
	}
});

The intersting parts of the Servlet code are below. The BrowserResource class improves on ServletRequest.getResources() by taking into account our product’s supported locales.

@WebServlet("/LocaleMixin/*")
public class LocaleMixinServlet extends HttpServlet
{
	private static final int HTTP_NOT_FOUND = 404;

	/** Package prefix for all bundles. */
	private static final String BUNDLE_PREFIX = "com.mycompany.web.ui.localemixin.";

	/** Pattern that a valid bundle name must match. */
	private static final Pattern VALID_PATH = Pattern.compile("/([a-zA-Z_][a-zA-Z0-9_./]*)\\.js");

	/** Location of the servlet. */
	public static final String ROOT = "/LocaleMixin/";

// ....

	@Override
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException
	{
		String path = request.getPathInfo();
		if(path != null && path.length() > 0)
		{
			Matcher matcher = VALID_PATH.matcher(path);
			if(matcher.matches())
			{
				Locale locale = BrowserLocale.getBestResourceLocale(request);
				String extClass = matcher.group(1).replace('/', '.');
				String bundle = BUNDLE_PREFIX + extClass;

				LOG.fine(getClass(), "Request for {0} in {1}", bundle, locale);

				try
				{
					ResourceBundle resources = ResourceBundle.getBundle(bundle, locale);
					makeMixin(extClass, response, resources);
					return;
				}
				catch(MissingResourceException e)
				{
					response.sendError(HTTP_NOT_FOUND, "Could not find resources.");
				}
			}
		}
		response.sendError(HTTP_NOT_FOUND, "Could not parse request path or request path missing");
	}

	private void makeMixin(String extClass, HttpServletResponse response, ResourceBundle resources) throws IOException, ServletException
	{
		response.setContentType("text/javascript");
		Writer out = response.getWriter();
		out.write("Ext.define('");
		out.write(extClass);
		out.write("',{");

		Node root = new Node("text");
		Enumeration<String> keyEnum = resources.getKeys();
		while(keyEnum.hasMoreElements())
		{
			String key = keyEnum.nextElement();
			String value = resources.getString(key);
			root.addProperty(key, value);
		}
		root.toJavascriptObject(out);
		out.write("});");
	}
}

Node.java is an example of a State Pattern, which may be the subject of another post. A simple solution would be to output a flat name/value map. That was the initial version. It would also be interesting to try an off-the-shelf JSON encoder with the ResourceBundle.

1 thought on “Internationalisation in Ext-JS using Mixins

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.