Index: main/trunk/greenstone3/src/java/org/greenstone/gsdl3/util/XSLTUtil.java
===================================================================
--- main/trunk/greenstone3/src/java/org/greenstone/gsdl3/util/XSLTUtil.java	(revision 35745)
+++ main/trunk/greenstone3/src/java/org/greenstone/gsdl3/util/XSLTUtil.java	(revision 35996)
@@ -822,56 +822,43 @@
 	}
 
-	public static String getInterfaceStringsAsJavascript(String interface_name, String lang, String prefix)
+	public static String getInterfaceStringsAsJavascript(String interfaceName, String lang, String prefix)
 	{
 		String prependToPrefix = "gs.text";
-		return XSLTUtil.getInterfaceStringsAsJavascript(interface_name, lang, prefix, prependToPrefix);
+		return XSLTUtil.getInterfaceStringsAsJavascript(interfaceName, lang, prefix, prependToPrefix);
 	}
 
 	/**
-	 * Generates a Javascript object graph to store language strings. Leaf nodes contain the string and the previous node, the key.
-	 * Further preceding nodes denote the prefix of the string (i.e. the gs3 language strings object, which interface they belong to etc.).
-	 * Accessing a language string on a well-formed JS object graph will be similar to this: const myString = gs.text.atea.asr.Title;
-	 * @param interface_name The name of the interface to retrieve language strings for.
+	 * Generates a Javascript object graph to store language strings. Leaf nodes are the key, and their value the string.
+	 * Further preceding nodes denote the prefix of the string within the language strings property file.
+	 * Accessing a language string from the object graph can be done as such: 'const myString = {prependToPrefix}.{prefix}.{key};'
+	 * @param interfaceName The name of the interface to retrieve language strings for.
 	 * @param lang The language to retrieve.
-	 * @param prefix The prefix to place the language strings under (e.g. atea.asr).
-	 * @param prependToPrefix
-	 * @return Javascript code that will generate the language string object graph.
+	 * @param prefix The prefix to to retrieve strings under. E.g. a value of 'atea.macroniser' will only retrieve strings prefixed with that value.
+	 * @param prependToPrefix An accessor string to prepend to the generated JS object graph.
+	 * @return Stringified Javascript code that will generate the language string object graph.
 	 */
-	public static String getInterfaceStringsAsJavascript(String interface_name, String lang, String prefix, String prependToPrefix)
+	public static String getInterfaceStringsAsJavascript(String interfaceName, String lang, String prefix, String prependToPrefix)
 	{
 		// now we allow looking for files in the interface's resources folder
-		CustomClassLoader my_loader = new CustomClassLoader(XSLTUtil.class.getClassLoader(),
-				GSFile.interfaceResourceDir(GlobalProperties.getGSDL3Home(), interface_name));
-		String prefixwithdot = prefix + ".";
+		CustomClassLoader my_loader = new CustomClassLoader(
+			XSLTUtil.class.getClassLoader(),
+			GSFile.interfaceResourceDir(GlobalProperties.getGSDL3Home(), interfaceName)
+		);
 
 		StringBuffer outputStr = new StringBuffer();
-		HashSet<String> initialisedDepths = new HashSet<>();
-
-		// Initialise the root prefix object
-		outputStr.append("if(!" + prependToPrefix + ") { ");
-		outputStr.append(prependToPrefix + " = {}; ");
-		outputStr.append("}\n");
-		initialisedDepths.add(prependToPrefix);
-
-		// Initialise the prefix object structure, handling '.' characters.
-		// JS identifies these as property accessors, hence requiring a new object for each 'level'.
-		String currentDepth = prependToPrefix;
-		String[] prefixComponents = prefix.split("\\.");
-
-		for (String element : prefixComponents) {
-			currentDepth += "." + element;
-
-			outputStr.append("if(!" + currentDepth + ") { ");
-			outputStr.append(currentDepth + " = {}; ");
-			outputStr.append("}\n");
-
-			initialisedDepths.add(currentDepth);
-		}
-
-		for (String dictName : new String[] { "interface_" + interface_name, "interface_default",
-				"interface_default2" }) {
-			// get all the keys from the english dictionary as this is a complete set
+		HashSet<String> initialisedNodes = new HashSet<>();
+
+		// The dictionaries to pull keys from
+		String[] dictionaries = new String[] {
+			"interface_" + interfaceName,
+			"interface_default",
+			"interface_default2"
+		};
+
+		for (String dictName : dictionaries)
+		{
+			// get all the *keys* from the english dictionary as this is a complete set
 			Dictionary dict = new Dictionary(dictName, "en", my_loader);
-			Enumeration keys = dict.getKeys();
+			Enumeration<String> keys = dict.getKeys();
 			if (keys == null) {
 				continue;
@@ -880,34 +867,22 @@
 			// Get all properties in the language-specific dictionary with the given key prefix
 			// Create Javascript strings of the form:
-			// prefixPath.key= "value";\n
-			while (keys.hasMoreElements()) {
-				String key = (String) keys.nextElement();
-				if (key.startsWith(prefixwithdot)) {
-					String[] keyComponents = key.split("\\.");
-					currentDepth = prependToPrefix + "." + prefix;
-
-					for (int i = prefixComponents.length; i < keyComponents.length - 1; i++) {
-						currentDepth += "." + keyComponents[i];
-
-						if (initialisedDepths.contains(currentDepth)) {
-							continue;
-						}
-
-						outputStr.append("if(!" + currentDepth + ") { ");
-						outputStr.append(currentDepth + " = {}; ");
-						outputStr.append("}\n");
-
-						initialisedDepths.add(currentDepth);
-					}
+			// prependToPrefix.key="value";\n
+			while (keys.hasMoreElements())
+			{
+				String key = keys.nextElement();
+				if (key.startsWith(prefix))
+				{
+					// Builds the JS object structure we need to access the key.
+					// Also has the side effect of ensuring that any '.' characters in
+					// the key are valid once parsed by the JS engine.
+					buildJSObjectGraph(
+						outputStr,
+						prependToPrefix + "." + key.substring(0, key.lastIndexOf(".")), // Strip the actual key from the path
+						initialisedNodes
+					);
 
 					// get the language dependent value for the key. This will return the english if no value found for the given lang
-					String value = getInterfaceText(interface_name, dictName, lang, key, null);
-
-					outputStr.append(prependToPrefix);
-					outputStr.append(".");
-					outputStr.append(key);
-					outputStr.append("=\"");
-					outputStr.append(value);
-					outputStr.append("\";\n");
+					String value = getInterfaceText(interfaceName, dictName, lang, key, null);
+					outputStr.append(prependToPrefix + "." + key + "=\"" + value + "\";\n");
 				}
 			}
@@ -915,4 +890,65 @@
 
 		return outputStr.toString();
+	}
+	
+	/**
+	 * Builds a string that will initialize an empty javascript object.
+	 * I.e. 'gs.text??={};'.
+	 * @param buffer The buffer to append the string to.
+	 * @param objectPath The path to the JS object to initialize. E.g. gs.text.atea.
+	 * @param isRootObject A value indicating whether the object is a root object. If not, a logical nullish assignment statement will be built in order to produce cleaner javascript.
+	 */
+	private static void buildJSObjectInitializer(
+		StringBuffer buffer,
+		String objectPath,
+		Boolean isRootObject
+	)
+	{
+		if (!isRootObject) {
+			buffer.append(objectPath + "??={};\n");
+			return;
+		}
+
+		buffer.append("if(typeof " + objectPath + "===\"undefined\"){" + objectPath + "={};}\n");
+	}
+
+	/**
+	 * Builds a string that will initialize a javascript object graph.
+	 * I.e. the structure required to access the final property on the object graph 'gs.text.atea.asr'.
+	 * @param buffer The buffer to append the string to.
+	 * @param objectGraph The object graph to build.
+	 * @param visitedNodes A map of previously built nodes. Updated to add nodes that are produced in this function.
+	 */
+	private static void buildJSObjectGraph(
+		StringBuffer buffer,
+		String objectGraph,
+		HashSet<String> preBuiltNodes
+	)
+	{
+		if (objectGraph == null) {
+			return;
+		}
+
+		String[] nodes = objectGraph.split("\\.");
+
+		if (!preBuiltNodes.contains(nodes[0])) {
+			buildJSObjectInitializer(buffer, nodes[0], true);
+			preBuiltNodes.add(nodes[0]);
+		}
+
+		if (nodes.length == 1) {
+			return;
+		}
+
+		String currentDepth = nodes[0];
+		for (int i = 1; i < nodes.length; i++) {
+			currentDepth += "." + nodes[i];
+			if (preBuiltNodes.contains(currentDepth)) {
+				continue;
+			}
+
+			buildJSObjectInitializer(buffer, currentDepth, false);
+			preBuiltNodes.add(currentDepth);
+		}
 	}
 
