Lothar Mueller
Lothar Mueller

Reputation: 2528

Xpages: how can we implement localization for client side JS code?

I'm currently developing a multi-lingual Xpages driven application using standard ways to localize static and dynamic strings through internal .property resources and resourceBundles. Within the application, users can choose their preferred language, this decision currently is stored in a user config document; I'm also planning to store those decisions as browser cookies. If there is no user defined preference the browser's default language drives the locale used within the application. This works fine for all server side elements.

Now I have to add some client side scripting, and I there need to use some localized strings as well. I have to admit that I don't have a clue which is the best approach for this.

Main questions are:

  1. can I somehow use existing file resources / resource bundles?
  2. if so: how can I reference those resources from my script code?
  3. if not: what's the best way to organize and reference my own file resources?
  4. finally: if I have to do it all by myself: how do I find out which locale is the user's preferred one? That one is easy if a cookie is set (see above), but what if I need to reference the browser's preference and/or if a user has disallowed usage of cookies?

Edit: I considered going the dojo i18n way, but I don't know how to implement such a custom plugin.

Upvotes: 3

Views: 776

Answers (2)

Mark Leusink
Mark Leusink

Reputation: 3757

An alternative is to read the resource bundles directly in the browser and reference that in JavaScript where you need it. So to answer your main questions:

  1. Yes, but if you load it directly from the NSF it's not really usable yet. I would recommend to create an XPage to output it for you as a JSON object:

    <?xml version="1.0" encoding="UTF-8"?>
    <xp:view xmlns:xp="http://www.ibm.com/xsp/core" rendered="false">
    
    <xp:this.resources>
        <xp:bundle
        src="/labels_en.properties"
        var="translations">
        </xp:bundle>
    </xp:this.resources>
    
    <xp:this.afterRenderResponse><![CDATA[#{javascript:
    try {
    
     var externalContext = facesContext.getExternalContext();
     var writer = facesContext.getResponseWriter();
     var response = externalContext.getResponse();
    
     response.setContentType("application/json"); 
    
     var jsonOutput = {};
     var keys = translations.keySet();
    
     for (var key in keys) {
       jsonOutput[key] = translations[key];
     }
    
     writer.write(  "var translations = " + toJson(jsonOutput)  ) ;
     writer.endDocument();
    
    } catch (e) {
        print(e);
    }
    }]]>
        </xp:this.afterRenderResponse>
    
    </xp:view>
    
  2. You need to include the XPage from 1. as a client side JavaScript library on your page. I created a global JavaScript variable called translations in the XPage from 1. so you can call it as translations.key where key references a variable from the properties file.

  3. No answer needed...
  4. In the XPage from answer 1, you can load the appropriate resource bundle based on the user's locale (browser language setting):

    <xp:this.resources>
        <xp:bundle var="translations">
        <xp:this.src><![CDATA[#{javascript:
    var language = "en";        //default language
    
    switch (context.getLocaleString() ) {
    case "nl":
        language = "nl";
        break;
    }
    
    return "/labels_" + language + ".properties";}]]></xp:this.src>
        </xp:bundle>
    </xp:this.resources>
    

Upvotes: 3

xpages-noob
xpages-noob

Reputation: 1579

I use the following approach: All my client-side JavaScript (CSJS) libraries are stored as file resources in the database design (Resources > Files). In the code of the libraries I use an EL-like notation to mark translateable parts, for example:

var text="#{TRLT.TextToTranslate}";

Whenever I want to use the libraries on a XPage, I do not directly reference them in the XPage's resources, but add a reference to a "Helper-XPage" (js.xsp) instead:

<xp:this.resources>
    <xp:headTag tagName="script">
        <xp:this.attributes>
            <xp:parameter name="src" value="js.xsp?lng=#{sessionScope.language}&amp;v=#{applicationScope.versionApp}"/>
            <xp:parameter name="type" value="text/javascript"/>
        </xp:this.attributes>
    </xp:headTag>
</xp:this.resources>

The js.xsp behaves like a JavaScript resource: I set rendered="false" on the XPage and manually create a response with Content-Type="text/javascript" in the beforeRenderResponse event. When the XPage is called, it reads all CSJS libraries, concatenates them and fills in all the translations based on the dictionaries that I generally use for server-side translations.

Here is the code for the beforeRenderResponse event of js.xsp:

importPackage(java.io);
importPackage(java.lang);
importPackage(java.util.regex);
importPackage(javax.servlet.http);
importPackage(org.apache.commons.io); // AFAIK this package is not installed by default (but generally very helpful)

var i,arr,lng,js,libs,c,s,m,bfr,dct,response,ec,is,os;

//---------------------------- initialize main variables
ec=facesContext.getExternalContext(); // the external context

lng=(param.lng || "en");  // the language can be provided as url parameter, otherwise use a default

dct=(applicationScope["TRLT_"+lng] || {}); // in my case, the dictionaries (HashMaps) containing the translations are stored in the applicationScope, but of course they can be loaded from anywhere (resource bundles etc.)

libs=(param.libs ? fromJson(param.libs) : ["mylib1.js","mylib2.js"]) // the library names can be provided as url parameter, otherwise use a default

//---------------------------- concatenate all libraries
js=new StringBuilder();
for (i=0;i<libs.length;i++) {
    if (s=IOUtils.toString(ec.getResourceAsStream(libs[i]),"UTF-8")) js.append("\n\n"+s);
}
js=js.toString();

//---------------------------- search for and replace translateable parts
m=Pattern.compile("[#$]\\{TRLT\\.([^}]+)\\}").matcher(js);
bfr=new StringBuffer();
c=0;
while (m.find() && c<1e6) {
    c++;
    s=m.group(1);
    m.appendReplacement(bfr,dct[s] || s || "");
}
m.appendTail(bfr);
js=bfr.toString();

//---------------------------- create the response and finalize
response=ec.getResponse();
response.setHeader("Cache-Control","max-age="+(60*60*24*365).toFixed(0)); // its important to set the expiration "a bit" into the future to prevent the browser from reloading the js.xsp everytime you reference it on another XPage; in order to force the browser to update the XPage, use versioning (see url parameter "v" in the headTag definition above)
response.setDateHeader("Expires",new Date().getTime()+(1000*60*60*24*365));
response.setHeader("Content-Type","text/javascript; name=\"libs.js\"");
response.setHeader("Content-Disposition","inline; filename=\"libs.js\"");

is=new ByteArrayInputStream(js.getBytes("UTF-8"));
os=response.getOutputStream();
IOUtils.copy(is,os);
is.close();
os.close();
facesContext.responseComplete();
return;

PS: I had to modify the codes presented here because the original versions have some dependecies to my general framework as well as some additional caching and error handling. Hence I can't guarantee that there are no typos, but in principle it should work.

Upvotes: 2

Related Questions