Skip to content

Internationalization Helpers

David Steele edited this page Jun 13, 2013 · 3 revisions

ironsites ironsites

AEM better

Internationalization (i18n) Helpers

ironsites provides sane patterns for content-managed i18n copy with reasonable Sling supported fallbacks as well as low-effort access to the CQ out-of-box i18n translation helper.

Hard-coding copy in your JSPs is an anti-pattern; even if you need a placeholder default to maintain formatting. CQ5.5+ provides a translation UI that allows an experienced user to avoid that painful deploy due to a mispelling that was forgotten about in a dialog if you prepare in advance for it and make use of sling internationlization support. ironsites prescribes a development pattern and taglib to give developers a pattern to repeat, because the following common case is still practically hard-coding.

<%=properties.get("greeting", "Hello World")%>

i18n Helpers : Apache Sling Native i18n Support

The preferred practice is to let sling do it's job with sling:MessageEntry nodes. The below may appear to be slightly more development effort, but it is worthwhile. Without reasonable defaults, many components can look awkward or broken, especially since comprehensive layouts are frequently shipped with at least placeholder text varying from lorem ipsum to fit-for-use defaults. As a result developers will bake-in defaults or rely on the author to save some other dialog property and bootstrap against a cq:Widget defaultValue. Moreover, it is not uncommon for functional specifications to neglect some copy as authorable/configurable. Even if this were not the case, component dialogs will bloat as all component copy fragments become configurable properties. The reality is that some copy is infrequently changed outside of initial translation and should not burden the authoring community.

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="sling:Folder"
    jcr:language="en"
    jcr:title="English"
    jcr:mixinTypes="[mix:language]"
    sling:basename="[fooapp/components/barcomponent]">
    <greeting jcr:primaryType="sling:MessageEntry"
		sling:key="greeting"
		sling:message="Hello {name}, how is the weather in {city}?"/>
</jcr:root>

In production, hard-coded defaults could mean some untranslatable text which necessitates a patch deployment. As an alternative to this scenario, developers can create sling:MessageEntry nodes for all placeholder copy and perhaps fallback to last-resort signals to indicate absense of a sling:MessageEntry (such as [greeting] in the above scriptlet). These nodes can be reliably updated on a running instance with the CQ5 Translator UI (note: change host/port) and corrected in the next regular deployment. Interestingly, these nodes can be also be imported/exported to XLIFF, a common format suitable for consumption and translation by external services.

i18n Helpers : CQ Native sling:Message Support

i18n translation helper is an out-of-box capability which is provides syntax around sling:MessageEntry nodes as well. ironsites populates an instance of I18n with a custom ResourceBundle which provides the fallback behavior described above based on sling:basename. i18nHelper tag also creates a variable and PageContext object called "i18n" which is an instance of this class.

A developer may wonder when it would be more idiomatic to use this instance in place of the above "messages" construct. In my opinion, the ValueMap is more convenient. But I18n does reduce boilerplate when interpolating Strings via [MessageFormat](http://dev.day.com/docs/en/cq/current/javadoc/com/day/cq/i18n/I18n.html#get(java.lang.String, java.lang.String, java.lang.Object...)). MessageFormat tokens are going to be common in a multi-lingual site where sentence structure could vary and ordering is important.

Click on {0} chat with {1}

{1}와 함께 {0}채팅을 클릭합니다

In the above {0} might represent a call-to-action and {1} would represent a name. But the language structure requires a different order for the value in these tokens which is important to programmatically populate correctly. Out-of-box CQ code that has similar needs to internationalize copy in ExtJS and granite UI elements also rely on interpolating tokens in this manner. I18n does provide a convenient way to pass in values to fill-in tokens into varargs parameters of it's overloaded accessor.

i18nHelper tag creates an instance of I18n (JSP variable & EL accessible PageContext object) which contains the ResourceBundle with local node properties and sling:basename/resourceType fallbacks. Although, for most cases, the JSP variable will be used via scriptlet. This goes without saying, but the index order of the String values passed should correspond to the numbering of the {idx} tokens found in the message. In the below example, it is worth mentioning that markup in a sling:message should be properly escaped in its docview XML representation of a sling:MessageEntry node.

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="sling:MessageEntry"
    sling:key="header"
    sling:message="Welcome to &lt;a href=&quot;{0}&quot;&gt;{1}&lt;a&gt;"/>
<h2><%=i18n.get("header", null, WCMUtil.getSafePath(root.getPath()), WCMUtil.getPageTitle(root))%></h2>

i18n Helpers : Magic messages

An ironsites tag, <isi:i18nHelper/>, makes available the a "messages" variable which performs the work of finding and presenting a local property on a component instance resource/node, or otherwise falling back to a component-wide default fallback based on a sling:basename matching the component sling:resourceType. This behaves much like the out-of-box "properties" ValueMap granted by <cq:setContentBundle/> but also provides the ability to fallback to sling:MessageEntry nodes assuming the simple sling:basename => sling:resourceType convention is followed by the developer.

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="sling:Folder"
    jcr:language="en"
    jcr:title="English"
    jcr:mixinTypes="[mix:language]"
    sling:basename="[fooapp/components/barcomponent]">
    <greeting jcr:primaryType="sling:MessageEntry"
		sling:key="greeting"
		sling:message="Hello {name}, how is the weather in {city}?"/>
</jcr:root>
<%=messages.get("greeting", "[greeting]")%>

Hello {name}, how is the weather in {city}?

i18n Helpers : Formatting/Interpolation

Some copy must support interpolation with dynamic values (e.g. "Hello {name}, how is the weather in {city}"), but this becomes difficult when translating because sentence structures across languages vary wildly. Also, some dynamic/computed/personalized values must be localized (e.g. currency, date/time, etc). java.text.MessageFormat makes this situation slightly better if your authors are willing to put up with: "Hello {0}, how is the weather in {1}". This is what the out-of-box i18n translation helper class can help with. ironsites makes an instance of I18n accessible with a single tag. In the below example, instead of just accesing a message by its "greeting" key, we first replace our friendly variables with MessageFormat indices.

<%=i18n.get(StringUtils.replaceEach(
		messages.get("greeting",""),
		new String[]{"{name}", "{city}"}, new String[]{"{0}", "{1}"}),
			null,
			"David", "Detroit, MI") %>

Hello David, how is the weather in Detroit, MI?

The above barely hides the underlying MessageFormat#format which powers that method of the i18n translation helper and is encumbered with additional boilerplate for replacing our friendly variables. We can AEM better with ironsites helper methods for this common task.

<%=I18nUtil.interpolate(messages.get("greeting",""), 
	"{name}={0};{city}={1}", "David", "Detroit, MI")%>

That {name}={0};{city}={1} String is a simple formatting pattern to replace friendly variables with MessageFormat indices. Under the hood, this is just a a few splits and StringUtils#replaceEach, but think about how far this is from creating an instance of I18n based own the sling:resourceType rolled up on your own. This is a great help when you are performing server-side interpolation with dynamic values of a known order, but if you can/must defer performing the interpolation to the browser using a Javascript library we can carve this down even further. A developer can easily replace tokens with alternative variable formatting for browser-side templating using an ironsites JSTL function.

${isi:swap(messages.greeting, '{name}={{name}};{city}={{city}}')}

Hello {{name}}, how is the weather in {{city}}?

To recap, isi is the taglib namespace declared in /apps/ironsites/common/global.jsp. messages is a magic ValueMap established by <isi:i18nHelper/> (which is why we can use the dot syntax for the getter). And finally, {name}={{name}};{city}={{city}} will cause the placeholder token of the left of the equals (=) to be replaced by the token on the right. In the above example, instead of using MessageFormat indices, we opt for the popular handlebars templating. Lastly, if using JSTL functions is designed for server-side interpolation, this can be done but due to the absense of varargs support with JSTL functions, an Object Array (Object[]) must be passed instead.

<% pageContext.setAttribute("greetingValues", new Object[] { "David", "Detroit, MI" }); %>
...
${isi:interpolate(messages.greeting, '{name}={0};{city}={1}', greetingValues)}

Hello David, how is the weather in Detroit, MI?

i18n Helpers : I18nResourceJsonServlet

Unless disabled in Felix Configuration Manager, the I18nResourceJsonServlet is listening for the ".i18n.json" selector/extension against your sling resources. For those components that want easy access to sling:MessageEntry values, this servlet will expose them (again based on sling:basename=sling:resourceType) as JSON. This is helpful if you want to provide copy, templated or otherwise, for rich client Javascript frameworks. This servlet will not expose your component node/resource properties, just the sling:MessageEntry nodes.