For one of my customers I am developing an easy way for editing the localization/translation of existing XPages applications: Instead of importing and exporting the property files with the Domino Designer (which is not an option for non-developers) it would be more suitable, if the users would have the possibility to edit the files directly in the browser. I am currently working on a XPage solution for this, and this is my current beta version.
This is a proof-of-concept with some known bugs, f.e. HTML special chars are not encoded correctly. But this can be fixied easily. First, it is required that the users can choose a property file to edit. This can be realized with a small Java class I have created (based on a stackoverflow.com answer):
package ch.hasselba.xpages.localization; import java.util.Vector; import javax.faces.context.FacesContext; import lotus.domino.Database; import lotus.domino.Document; import lotus.domino.NoteCollection; import lotus.domino.NotesException; public class DesignElements { private final String EMPTY_STRING = ""; private final String[] FLAG_PROPERTIES = { "gC~4K2P", "gC~4K2"; "gC~4;2" }; private final String FIELD_$FLAGS = "$Flags"; private final String FIELD_TITLE = "$TITLE"; /** * returns Vector containing all property files of a database * * No error handling included! * * @category Domino * @author Sven Hasselbach * @category Tools * @version 0.2 */ public Vector<String> getPropertFileList() { FacesContext fc = FacesContext.getCurrentInstance(); Vector<String> data = new Vector<String>(); try { // get DB Database db = (Database) fc.getApplication().getVariableResolver() .resolveVariable(fc, "database"); // get all design docs NoteCollection nc = db.createNoteCollection(false); nc.selectAllDesignElements(true); nc.buildCollection(); // process all notes String noteId = ""; noteId = nc.getFirstNoteID(); Document doc = null; // while (!(EMPTY_STRING.equals(noteId))) { // get design doc doc = db.getDocumentByID(noteId); // check if its a property file for (int i = 0; i < FLAG_PROPERTIES.length; i++) { if (FLAG_PROPERTIES[i].equals(doc .getItemValueString(FIELD_$FLAGS))) { // add to Vector data.add(doc.getItemValueString(FIELD_TITLE)); } } // next one noteId = nc.getNextNoteID(noteId); // recycle doc recycleObject(doc); } } catch (NotesException e) { e.printStackTrace(); } return data; } /** * recycles a domino document instance * * @param lotus * .domino.Base obj to recycle * @category Domino * @author Sven Hasselbach * @category Tools * @version 1.1 */ public static void recycleObject(lotus.domino.Base obj) { if (obj != null) { try { obj.recycle(); } catch (Exception e) { } } } }
The difference to the answer on stackoverflow.com is the additional property flag I have added to this class (I know it would be better to check the flags instead of comparing the strings, but it seems to work this way).
Next step is to load the selected property file. This can be done with a managed bean and a helper class for the entries itself. Here is the helper class:
package ch.hasselba.xpages.localization; import java.util.UUID; /** * Locale Entry of a locale file * * @author Sven Hasselbach * @category Localization * @version 1.0 * */ public class LocaleEntry { private String id; private String name; private String value; /** * initializes the object and * sets an unique identifier */ public LocaleEntry(){ this.id = UUID.randomUUID().toString(); this.name = ""; this.value = ""; } /** * returns unique identifier of the object * @return String unique id */ public String getId() { return id; } /** * sets the unique identifier of the entry * @param id String unique id */ public void setId(final String id) { this.id = id; } /** * returns the name of the entry * @return String */ public String getName() { return name; } /** * sets the name of the entry * @param String */ public void setName(String name) { this.name = name; } /** * returns the value of the entry * @return String */ public String getValue() { return value; } /** * sets the value of the entry * @param value */ public void setValue(String value) { this.value = value; } }
And here comes the „Loader“ class:
package ch.hasselba.xpages.localization; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Vector; import com.ibm.xsp.context.FacesContextEx; /** * "Loader" class to access the entries * of a .property file * * @author Sven Hasselbach * @version 1.0 */ public class Locale { private Vector<LocaleEntry> locales; /** * @return vector containing the LocalEntry objects */ public Vector<LocaleEntry> getLocales() { return locales; } /** * sets vector containing the LocalEntry objects * @param Vector */ public void setLocales(Vector<LocaleEntry> locales) { this.locales = locales; } /** * wrapper for the static method call * * @param fileName name of the property file to load */ public void loadFile(final String fileName) { Map<String, String> m = getPropertiesFromFile(fileName); this.locales = parseMap(m); } /** * loads a property file and parses it to a key/value map * * @param fileName name of the property file to load * @return Map containing the key/values * * The loading routine is shamelessly copied from Ulrich Krause: * http://openntf.org/XSnippets.nsf/snippet.xsp?id=access-.properties-files */ public static Map<String, String> getPropertiesFromFile(String fileName) { Properties prop = new Properties(); try { prop.load(FacesContextEx.getCurrentInstance().getExternalContext() .getResourceAsStream(fileName)); Map<String, String> map = new HashMap<String, String>((Map) prop); return map; } catch (Exception e) { e.printStackTrace(); } return null; } /** * parses a property map and create a vector * with LocalEntry objects * * @param map to parse * @return Vector containing the LocalEntry objects */ public static Vector<LocaleEntry> parseMap(final Map<String, String> map) { // init new vector Vector<LocaleEntry> localEntries = new Vector<LocaleEntry>(); String key; String value; // get key set for iteration Iterator<?> it = map.keySet().iterator(); while (it.hasNext()) { // extract key & value key = (String) it.next(); value = (String) map.get(key); // create new entry and add to vector LocaleEntry lEntry = new LocaleEntry(); lEntry.setName(key); lEntry.setValue(value); localEntries.add(lEntry); } // return vector return localEntries; } /** * dumps current object data to console * Just for debugging * * @category Debug */ public void dump() { for (int i = 0; i < this.locales.size(); i++) { LocaleEntry e = this.locales.get(i); System.out.println(e.getId() + " - " + e.getName() + " - " + e.getValue()); } } }
Now the new managed bean can be added to the faces-config.xml:
<managed-bean> <managed-bean-name>locale</managed-bean-name> <managed-bean-class>ch.hasselba.xpages.localization.Locale</managed-bean-class> <managed-bean-scope>session</managed-bean-scope> </managed-bean>
The resulting XPage can look like this:
<?xml version="1.0" encoding="UTF-8"?> <xp:view xmlns:xp="http://www.ibm.com/xsp/core"> Property File:  <xp:comboBox id="comboBoxFile" value="#{sessionScope.PropertyFile}"> <xp:selectItems> <xp:this.value> <![CDATA[#{javascript: importPackage(ch.hasselba.xpages.localization); ch.hasselba.xpages.localization.DesignElements().getPropertFileList(); }]]> </xp:this.value> </xp:selectItems> </xp:comboBox>     <xp:button value="Load" id="buttonLoadFile"> <xp:eventHandler event="onclick" submit="true" refreshMode="complete"> <xp:this.action> <![CDATA[#{javascript: importPackage(ch.hasselba.xpages.localization); var bean = facesContext.getApplication().getVariableResolver() .resolveVariable(facesContext, "locale"); bean.loadFile(sessionScope.get("PropertyFile") )}]]> </xp:this.action> </xp:eventHandler> </xp:button> <hr /> <xp:label id="labelSelecteFile"> <xp:this.value> <![CDATA[#{javascript:sessionScope.get("PropertyFile")}]]> </xp:this.value> </xp:label> <xp:br /> <xp:br /> <xp:repeat id="repeat1" rows="9999" var="lMap" value="#{locale.locales}"> <xp:label value="#{lMap.name}" id="labelMapName" /> <xp:inputText id="inputTextValue" value="#{lMap.value}" /> <xp:br /> </xp:repeat> </xp:view>
And this is how it looks in the browser:
The next article describes the procedure to save the properties back the database.
Hello Sven,
I’m a follower of your blog. My name is Antonio Maza and I work with xpages. I have read your article XPages: UI for editing localization files (1) and I found it very interesting. I’m waiting for your next article describing the procedure to save the properties back the database, can you tell me something about it?
Thanks for sharing your work.
Best regards, Antonio