From f94f9ac4162c35c8d67481ca22a7343408f8cd8f Mon Sep 17 00:00:00 2001
From: Chris Hallberg <crhallberg@gmail.com>
Date: Fri, 20 Nov 2015 12:47:28 -0500
Subject: [PATCH] VuFind JS namespace variable. Contains .getPath and .transate
 functions. More to come.

---
 .../View/Helper/Root/JsTranslations.php       | 16 +++-
 themes/bootstrap3/js/cart.js                  | 12 +--
 themes/bootstrap3/js/check_item_statuses.js   |  4 +-
 themes/bootstrap3/js/check_save_statuses.js   | 10 +--
 themes/bootstrap3/js/common.js                | 25 ++++--
 themes/bootstrap3/js/facets.js                | 28 +++---
 themes/bootstrap3/js/hierarchyTree.js         | 10 +--
 themes/bootstrap3/js/hold.js                  |  8 +-
 themes/bootstrap3/js/ill.js                   | 56 ++++++------
 themes/bootstrap3/js/keep_alive.js            |  4 +-
 themes/bootstrap3/js/lightbox.js              |  7 +-
 themes/bootstrap3/js/openurl.js               | 86 +++++++++----------
 themes/bootstrap3/js/pubdate_vis.js           |  3 +-
 themes/bootstrap3/js/record.js                | 31 +++----
 themes/bootstrap3/templates/cart/cart.phtml   |  2 +-
 .../bootstrap3/templates/layout/layout.phtml  |  9 +-
 16 files changed, 170 insertions(+), 141 deletions(-)

diff --git a/module/VuFind/src/VuFind/View/Helper/Root/JsTranslations.php b/module/VuFind/src/VuFind/View/Helper/Root/JsTranslations.php
index f06481a776e..9bf89399fa0 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/JsTranslations.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/JsTranslations.php
@@ -87,11 +87,11 @@ class JsTranslations extends AbstractHelper
     }
 
     /**
-     * Generate Javascript from the internal strings.
+     * Generate JSON from the internal strings.
      *
      * @return string
      */
-    public function getScript()
+    public function getJSON()
     {
         $parts = [];
         foreach ($this->strings as $k => $v) {
@@ -100,6 +100,16 @@ class JsTranslations extends AbstractHelper
                 : $this->transEsc->__invoke($v);
             $parts[] = $k . ': "' . addslashes($translation) . '"';
         }
-        return $this->varName . ' = {' . implode(',', $parts) . '};';
+        return '{' . implode(',', $parts) . '}';
+    }
+
+    /**
+     * Assign JSON to a variable.
+     *
+     * @return string
+     */
+    public function getScript()
+    {
+        return $this->varName . ' = ' . $this->getJSON() . ';';
     }
 }
diff --git a/themes/bootstrap3/js/cart.js b/themes/bootstrap3/js/cart.js
index 59986810b7f..04e3a29bab2 100644
--- a/themes/bootstrap3/js/cart.js
+++ b/themes/bootstrap3/js/cart.js
@@ -1,4 +1,4 @@
-/*global bulkActionSubmit, cartCookieDomain, Cookies, newAccountHandler, path, vufindString, Lightbox, refreshPageForLogin */
+/*global bulkActionSubmit, cartCookieDomain, Cookies, newAccountHandler, Lightbox, refreshPageForLogin, VuFind */
 
 var _CART_COOKIE = 'vufind_cart';
 var _CART_COOKIE_SOURCES = 'vufind_cart_src';
@@ -131,17 +131,17 @@ function registerUpdateCart($form) {
         });
         var updated = getFullCartItems();
         var added = updated.length - orig.length;
-        msg += added + " " + vufindString.itemsAddBag;
+        msg += added + " " + VuFind.translate('itemsAddBag');
         if (inCart > 0 && orig.length > 0) {
-          msg += "<br/>" + inCart + " " + vufindString.itemsInBag;
+          msg += "<br/>" + inCart + " " + VuFind.translate('itemsInBag');
         }
-        if (updated.length >= vufindString.bookbagMax) {
-          msg += "<br/>" + vufindString.bookbagFull;
+        if (updated.length >= VuFind.translate('bookbagMax')) {
+          msg += "<br/>" + VuFind.translate('bookbagFull');
         }
         $('#'+elId).data('bs.popover').options.content = msg;
         $('#cartItems strong').html(updated.length);
       } else {
-        $('#'+elId).data('bs.popover').options.content = vufindString.bulk_noitems_advice;
+        $('#'+elId).data('bs.popover').options.content = VuFind.translate('bulk_noitems_advice');
       }
       $('#'+elId).popover('show');
       if (cartNotificationTimeout !== false) {
diff --git a/themes/bootstrap3/js/check_item_statuses.js b/themes/bootstrap3/js/check_item_statuses.js
index 2c79147047b..62d213f58ea 100644
--- a/themes/bootstrap3/js/check_item_statuses.js
+++ b/themes/bootstrap3/js/check_item_statuses.js
@@ -1,4 +1,4 @@
-/*global path*/
+/*global VuFind */
 
 function checkItemStatuses() {
   var id = $.map($('.ajaxItem'), function(i) {
@@ -11,7 +11,7 @@ function checkItemStatuses() {
   $(".ajax-availability").removeClass('hidden');
   $.ajax({
     dataType: 'json',
-    url: path + '/AJAX/JSON?method=getItemStatuses',
+    url: VuFind.getPath() + '/AJAX/JSON?method=getItemStatuses',
     data: {id:id},
     success: function(response) {
       if(response.status == 'OK') {
diff --git a/themes/bootstrap3/js/check_save_statuses.js b/themes/bootstrap3/js/check_save_statuses.js
index cbe0c0fd89c..12dac954b81 100644
--- a/themes/bootstrap3/js/check_save_statuses.js
+++ b/themes/bootstrap3/js/check_save_statuses.js
@@ -1,4 +1,4 @@
-/*global path*/
+/*global VuFind */
 
 function checkSaveStatuses() {
   var data = $.map($('.result,.record'), function(i) {
@@ -16,7 +16,7 @@ function checkSaveStatuses() {
     }
     $.ajax({
       dataType: 'json',
-      url: path + '/AJAX/JSON?method=getSaveStatuses',
+      url: VuFind.getPath() + '/AJAX/JSON?method=getSaveStatuses',
       data: {id:ids, 'source':srcs},
       success: function(response) {
         if(response.status == 'OK') {
@@ -31,7 +31,7 @@ function checkSaveStatuses() {
               $container.append('<ul></ul>');
               $ul = $container.children('ul:first');
             }
-            var html = '<li><a href="' + path + '/MyResearch/MyList/' + result.list_id + '">'
+            var html = '<li><a href="' + VuFind.getPath() + '/MyResearch/MyList/' + result.list_id + '">'
                      + result.list_title + '</a></li>';
             $ul.append(html);
             $container.removeClass('hidden');
@@ -42,6 +42,4 @@ function checkSaveStatuses() {
   }
 }
 
-$(document).ready(function() {
-  checkSaveStatuses();
-});
+$(document).ready(checkSaveStatuses);
diff --git a/themes/bootstrap3/js/common.js b/themes/bootstrap3/js/common.js
index 2abb181be31..9be1c423b94 100644
--- a/themes/bootstrap3/js/common.js
+++ b/themes/bootstrap3/js/common.js
@@ -1,4 +1,17 @@
-/*global ajaxLoadTab, btoa, checkSaveStatuses, console, extractSource, hexEncode, isPhoneNumberValid, Lightbox, path, rc4Encrypt, refreshCommentList, refreshTagList, unescape, vufindString */
+/*global ajaxLoadTab, btoa, checkSaveStatuses, console, extractSource, hexEncode, isPhoneNumberValid, Lightbox, rc4Encrypt, refreshCommentList, refreshTagList, unescape, VuFind */
+
+function VuFindNamespace(p, s) {
+  var path = p;
+  var strings = s;
+
+  var getPath = function() { return path; }
+  var translate = function(op) { return strings[op]; }
+
+  return {
+    getPath: getPath,
+    translate: translate
+  };
+};
 
 /* --- GLOBAL FUNCTIONS --- */
 function htmlEncode(value){
@@ -97,7 +110,7 @@ function bulkActionSubmit($form) {
   }
   if (submit == 'print') {
     //redirect page
-    var url = path+'/Records/Home?print=true';
+    var url = VuFind.getPath() + '/Records/Home?print=true';
     for(var i=0;i<checks.length;i++) {
       url += '&id[]='+checks[i].value;
     }
@@ -177,7 +190,7 @@ function newAccountHandler(html) {
 // This is a full handler for the login form
 function ajaxLogin(form) {
   Lightbox.ajax({
-    url: path + '/AJAX/JSON?method=getSalt',
+    url: VuFind.getPath() + '/AJAX/JSON?method=getSalt',
     dataType: 'json',
     success: function(response) {
       if (response.status == 'OK') {
@@ -203,7 +216,7 @@ function ajaxLogin(form) {
         // login via ajax
         Lightbox.ajax({
           type: 'POST',
-          url: path + '/AJAX/JSON?method=login',
+          url: VuFind.getPath() + '/AJAX/JSON?method=login',
           dataType: 'json',
           data: params,
           success: function(response) {
@@ -295,7 +308,7 @@ function setupAutocomplete() {
         source: function(query, cb) {
           var searcher = extractClassParams(element);
           $.ajax({
-            url: path + '/AJAX/JSON',
+            url: VuFind.getPath() + '/AJAX/JSON',
             data: {
               q:query,
               method:'getACSuggestions',
@@ -372,7 +385,7 @@ $(document).ready(function() {
       window.print();
     });
     // Make an ajax call to ensure that ajaxStop is triggered
-    $.getJSON(path + '/AJAX/JSON', {method: 'keepAlive'});
+    $.getJSON(VuFind.getPath() + '/AJAX/JSON', {method: 'keepAlive'});
   }
 
   // Advanced facets
diff --git a/themes/bootstrap3/js/facets.js b/themes/bootstrap3/js/facets.js
index 852b5cb0a3e..0343875c59c 100644
--- a/themes/bootstrap3/js/facets.js
+++ b/themes/bootstrap3/js/facets.js
@@ -1,21 +1,21 @@
-/*global htmlEncode, path, vufindString */
+/*global htmlEncode, VuFind */
 function buildFacetNodes(data, currentPath, allowExclude, excludeTitle, counts)
 {
   var json = [];
-  
+
   $(data).each(function() {
     var html = '';
     if (!this.isApplied && counts) {
-      html = '<span class="badge" style="float: right">' + this.count.toString().replace(/\B(?=(\d{3})+\b)/g, vufindString.number_thousands_separator);
+      html = '<span class="badge" style="float: right">' + this.count.toString().replace(/\B(?=(\d{3})+\b)/g, VuFind.getPath('number_thousands_separator'));
       if (allowExclude) {
         var excludeURL = currentPath + this.exclude;
         excludeURL.replace("'", "\\'");
         // Just to be safe
         html += ' <a href="' + excludeURL + '" onclick="document.location.href=\'' + excludeURL + '\'; return false;" title="' + htmlEncode(excludeTitle) + '"><i class="fa fa-times"></i></a>';
       }
-      html += '</span>'; 
+      html += '</span>';
     }
-    
+
     var url = currentPath + this.href;
     // Just to be safe
     url.replace("'", "\\'");
@@ -23,12 +23,12 @@ function buildFacetNodes(data, currentPath, allowExclude, excludeTitle, counts)
       + ' onclick="document.location.href=\'' + url + '\'; return false;">';
     if (this.operator == 'OR') {
       if (this.isApplied) {
-        html += '<i class="fa fa-check-square-o"></i>';  
+        html += '<i class="fa fa-check-square-o"></i>';
       } else {
-        html += '<i class="fa fa-square-o"></i>';  
+        html += '<i class="fa fa-square-o"></i>';
       }
     } else if (this.isApplied) {
-      html += '<i class="fa fa-check pull-right"></i>';  
+      html += '<i class="fa fa-check pull-right"></i>';
     }
     html += ' ' + this.displayText;
     html += '</span>';
@@ -47,7 +47,7 @@ function buildFacetNodes(data, currentPath, allowExclude, excludeTitle, counts)
       'li_attr': this.isApplied ? { 'class': 'active' } : {}
     });
   });
-  
+
   return json;
 }
 
@@ -58,7 +58,7 @@ function initFacetTree(treeNode, inSidebar)
     return;
   }
   treeNode.data('loaded', true);
-  
+
   var facet = treeNode.data('facet');
   var operator = treeNode.data('operator');
   var currentPath = treeNode.data('path');
@@ -70,14 +70,14 @@ function initFacetTree(treeNode, inSidebar)
   if (inSidebar) {
     treeNode.prepend('<li class="list-group-item"><i class="fa fa-spinner fa-spin"></i></li>');
   } else {
-    treeNode.prepend('<div><i class="fa fa-spinner fa-spin"></i><div>');  
+    treeNode.prepend('<div><i class="fa fa-spinner fa-spin"></i><div>');
   }
-  $.getJSON(path + '/AJAX/JSON?' + query,
+  $.getJSON(VuFind.getPath() + '/AJAX/JSON?' + query,
     {
       method: "getFacetData",
       facetName: facet,
       facetSort: sort,
-      facetOperator: operator 
+      facetOperator: operator
     },
     function(response, textStatus) {
       if (response.status == "OK") {
@@ -93,7 +93,7 @@ function initFacetTree(treeNode, inSidebar)
             'data': results
           }
         });
-      } 
+      }
     }
   );
 }
diff --git a/themes/bootstrap3/js/hierarchyTree.js b/themes/bootstrap3/js/hierarchyTree.js
index 4cf029a4380..b1c5df9a048 100644
--- a/themes/bootstrap3/js/hierarchyTree.js
+++ b/themes/bootstrap3/js/hierarchyTree.js
@@ -1,4 +1,4 @@
-/*global hierarchySettings, path, vufindString*/
+/*global hierarchySettings, VuFind */
 
 
 var hierarchyID, recordID, htmlID, hierarchyContext;
@@ -27,7 +27,7 @@ function html_entity_decode(string, quote_style) {
 
 function getRecord(recordID) {
   $.ajax({
-    url: path + '/Hierarchy/GetRecord?' + $.param({id: recordID}),
+    url: VuFind.getPath() + '/Hierarchy/GetRecord?' + $.param({id: recordID}),
     dataType: 'html',
     success: function(response) {
       if (response) {
@@ -75,7 +75,7 @@ function doTreeSearch() {
       searchAjax.abort();
     }
     searchAjax = $.ajax({
-      "url" : path + '/Hierarchy/SearchTree?' + $.param({
+      "url" : VuFind.getPath() + '/Hierarchy/SearchTree?' + $.param({
         'lookfor': keyword,
         'hierarchyID': hierarchyID,
         'type': $("#treeSearchType").val()
@@ -127,7 +127,7 @@ function buildJSONNodes(xml) {
   return jsonNode;
 }
 function buildTreeWithXml(cb) {
-  $.ajax({'url': path + '/Hierarchy/GetTree',
+  $.ajax({'url': VuFind.getPath() + '/Hierarchy/GetTree',
     'data': {
       'hierarchyID': hierarchyID,
       'id': recordID,
@@ -189,7 +189,7 @@ $(document).ready(function() {
       'core' : {
         'data' : function (obj, cb) {
           $.ajax({
-            'url': path + '/Hierarchy/GetTreeJSON',
+            'url': VuFind.getPath() + '/Hierarchy/GetTreeJSON',
             'data': {
               'hierarchyID': hierarchyID,
               'id': recordID
diff --git a/themes/bootstrap3/js/hold.js b/themes/bootstrap3/js/hold.js
index 06555cc43ea..4895a5ab344 100644
--- a/themes/bootstrap3/js/hold.js
+++ b/themes/bootstrap3/js/hold.js
@@ -1,4 +1,4 @@
-/*global path */
+/*global VuFind */
 function setUpHoldRequestForm(recordId) {
   $('#requestGroupId').change(function() {
     var $emptyOption = $("#pickUpLocation option[value='']");
@@ -11,13 +11,13 @@ function setUpHoldRequestForm(recordId) {
     var params = {
       method: 'getRequestGroupPickupLocations',
       id: recordId,
-      requestGroupId: $('#requestGroupId').val()              
+      requestGroupId: $('#requestGroupId').val()
     };
     $.ajax({
       data: params,
       dataType: 'json',
       cache: false,
-      url: path + '/AJAX/JSON',
+      url: VuFind.getPath() + '/AJAX/JSON',
       success: function(response) {
         if (response.status == 'OK') {
           var defaultValue = $('#pickUpLocation').data('default');
@@ -36,7 +36,7 @@ function setUpHoldRequestForm(recordId) {
         $('#pickUpLocationLabel i').removeClass("fa fa-spinner icon-spin");
         $('#pickUpLocation').removeAttr('disabled');
       }
-    });   
+    });
   });
   $('#requestGroupId').change();
 }
diff --git a/themes/bootstrap3/js/ill.js b/themes/bootstrap3/js/ill.js
index 3ae164aea78..bf0a73d9fce 100644
--- a/themes/bootstrap3/js/ill.js
+++ b/themes/bootstrap3/js/ill.js
@@ -1,30 +1,34 @@
-/*global path */
+/*global VuFind */
 function setUpILLRequestForm(recordId) {
-    $("#ILLRequestForm #pickupLibrary").change(function() {
-        $("#ILLRequestForm #pickupLibraryLocation option").remove();
-        $("#ILLRequestForm #pickupLibraryLocationLabel i").addClass("fa fa-spinner icon-spin");
-        var url = path + '/AJAX/JSON?' + $.param({method:'getLibraryPickupLocations', id: recordId, pickupLib: $("#ILLRequestForm #pickupLibrary").val() });
-        $.ajax({
-            dataType: 'json',
-            cache: false,
-            url: url,
-            success: function(response) {
-                if (response.status == 'OK') {
-                    $.each(response.data.locations, function() {
-                        var option = $("<option></option>").attr("value", this.id).text(this.name);
-                        if (this.isDefault) {
-                            option.attr("selected", "selected");
-                        }
-                        $("#ILLRequestForm #pickupLibraryLocation").append(option);
-                    });
-                }
-                $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("fa fa-spinner icon-spin");
-            },
-            fail: function() {
-                $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("fa fa-spinner icon-spin");
+  $("#ILLRequestForm #pickupLibrary").change(function() {
+    $("#ILLRequestForm #pickupLibraryLocation option").remove();
+    $("#ILLRequestForm #pickupLibraryLocationLabel i").addClass("fa fa-spinner icon-spin");
+    var url = VuFind.getPath() + '/AJAX/JSON?' + $.param({
+      id: recordId,
+      method:'getLibraryPickupLocations',
+      pickupLib: $("#ILLRequestForm #pickupLibrary").val()
+    });
+    $.ajax({
+      dataType: 'json',
+      cache: false,
+      url: url,
+      success: function(response) {
+        if (response.status == 'OK') {
+          $.each(response.data.locations, function() {
+            var option = $("<option></option>").attr("value", this.id).text(this.name);
+            if (this.isDefault) {
+              option.attr("selected", "selected");
             }
-        });   
-        
+            $("#ILLRequestForm #pickupLibraryLocation").append(option);
+          });
+        }
+        $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("fa fa-spinner icon-spin");
+      },
+      fail: function() {
+        $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("fa fa-spinner icon-spin");
+      }
     });
-    $("#ILLRequestForm #pickupLibrary").change();
+
+  });
+  $("#ILLRequestForm #pickupLibrary").change();
 }
diff --git a/themes/bootstrap3/js/keep_alive.js b/themes/bootstrap3/js/keep_alive.js
index 5556008d676..d0e45d79b4d 100644
--- a/themes/bootstrap3/js/keep_alive.js
+++ b/themes/bootstrap3/js/keep_alive.js
@@ -1,7 +1,7 @@
-/*global path, keepAliveInterval */
+/*global keepAliveInterval, VuFind */
 
 $(document).ready(function() {
   window.setInterval(function() {
-    $.getJSON(path + '/AJAX/JSON', {method: 'keepAlive'});
+    $.getJSON(VuFind.getPath() + '/AJAX/JSON', {method: 'keepAlive'});
   }, keepAliveInterval * 1000);
 });
diff --git a/themes/bootstrap3/js/lightbox.js b/themes/bootstrap3/js/lightbox.js
index 70ad0212813..b4a0980fc1b 100644
--- a/themes/bootstrap3/js/lightbox.js
+++ b/themes/bootstrap3/js/lightbox.js
@@ -1,4 +1,4 @@
-/*global checkSaveStatuses, console, deparam, path, Recaptcha, vufindString */
+/*global checkSaveStatuses, console, deparam, Recaptcha, VuFind */
 
 var Lightbox = {
   /**
@@ -262,7 +262,7 @@ var Lightbox = {
    */
   get: function(controller, action, get, post, callback) {
     // Build URL
-    var url = path+'/AJAX/JSON?method=getLightbox&submodule='+controller+'&subaction='+action;
+    var url = VuFind.getPath()+'/AJAX/JSON?method=getLightbox&submodule='+controller+'&subaction='+action;
     if(typeof get !== "undefined" && get !== {}) {
       url += '&'+$.param(get);
     }
@@ -385,6 +385,7 @@ var Lightbox = {
     var POST = $form.attr('method') && $form.attr('method').toUpperCase() == 'POST';
     if($form.attr('action')) {
       // Parse action location
+      var path = VuFind.getPath();
       var action = $form.attr('action').substring($form.attr('action').indexOf(path)+path.length+1);
       var params = action.split('?');
       action = action.split('/');
@@ -458,7 +459,7 @@ $(document).ready(function() {
 
   Lightbox.addFormHandler('exportForm', function(evt) {
     $.ajax({
-      url: path + '/AJAX/JSON?' + $.param({method:'exportFavorites'}),
+      url: VuFind.getPath() + '/AJAX/JSON?' + $.param({method:'exportFavorites'}),
       type:'POST',
       dataType:'json',
       data:Lightbox.getFormData($(evt.target)),
diff --git a/themes/bootstrap3/js/openurl.js b/themes/bootstrap3/js/openurl.js
index 142c325a111..7d9f4432e2c 100644
--- a/themes/bootstrap3/js/openurl.js
+++ b/themes/bootstrap3/js/openurl.js
@@ -1,55 +1,55 @@
-/*global extractClassParams, path*/
+/*global extractClassParams, VuFind */
 
 function loadResolverLinks($target, openUrl) {
-    $target.addClass('ajax_availability');
-    var url = path + '/AJAX/JSON?' + $.param({method:'getResolverLinks',openurl:openUrl});
-    $.ajax({
-        dataType: 'json',
-        url: url,
-        success: function(response) {
-            if (response.status == 'OK') {
-                $target.removeClass('ajax_availability')
-                    .empty().append(response.data);
-            } else {
-                $target.removeClass('ajax_availability').addClass('error')
-                    .empty().append(response.data);
-            }
-        }
-    });
+  $target.addClass('ajax_availability');
+  var url = VuFind.getPath() + '/AJAX/JSON?' + $.param({method:'getResolverLinks',openurl:openUrl});
+  $.ajax({
+    dataType: 'json',
+    url: url,
+    success: function(response) {
+      if (response.status == 'OK') {
+        $target.removeClass('ajax_availability')
+          .empty().append(response.data);
+      } else {
+        $target.removeClass('ajax_availability').addClass('error')
+          .empty().append(response.data);
+      }
+    }
+  });
 }
 
 function embedOpenUrlLinks(element) {
-    // Extract the OpenURL associated with the clicked element:
-    var openUrl = element.children('span.openUrl:first').attr('title');
+  // Extract the OpenURL associated with the clicked element:
+  var openUrl = element.children('span.openUrl:first').attr('title');
 
-    // Hide the controls now that something has been clicked:
-    var controls = element.parents('.openUrlControls');
-    controls.removeClass('openUrlEmbed').addClass('hidden');
+  // Hide the controls now that something has been clicked:
+  var controls = element.parents('.openUrlControls');
+  controls.removeClass('openUrlEmbed').addClass('hidden');
 
-    // Locate the target area for displaying the results:
-    var target = controls.next('div.resolver');
+  // Locate the target area for displaying the results:
+  var target = controls.next('div.resolver');
 
-    // If the target is already visible, a previous click has populated it;
-    // don't waste time doing redundant work.
-    if (target.hasClass('hidden')) {
-        loadResolverLinks(target.removeClass('hidden'), openUrl);
-    }
+  // If the target is already visible, a previous click has populated it;
+  // don't waste time doing redundant work.
+  if (target.hasClass('hidden')) {
+    loadResolverLinks(target.removeClass('hidden'), openUrl);
+  }
 }
 
 $(document).ready(function() {
-    // assign action to the openUrlWindow link class
-    $('a.openUrlWindow').click(function(){
-        var params = extractClassParams(this);
-        var settings = params.window_settings;
-        window.open($(this).attr('href'), 'openurl', settings);
-        return false;
-    });
-
-    // assign action to the openUrlEmbed link class
-    $('.openUrlEmbed a').click(function() {
-        embedOpenUrlLinks($(this));
-        return false;
-    });
-
-    $('.openUrlEmbed.openUrlEmbedAutoLoad a').trigger("click");
+  // assign action to the openUrlWindow link class
+  $('a.openUrlWindow').click(function(){
+    var params = extractClassParams(this);
+    var settings = params.window_settings;
+    window.open($(this).attr('href'), 'openurl', settings);
+    return false;
+  });
+
+  // assign action to the openUrlEmbed link class
+  $('.openUrlEmbed a').click(function() {
+    embedOpenUrlLinks($(this));
+    return false;
+  });
+
+  $('.openUrlEmbed.openUrlEmbedAutoLoad a').trigger("click");
 });
diff --git a/themes/bootstrap3/js/pubdate_vis.js b/themes/bootstrap3/js/pubdate_vis.js
index 370aa39e312..2d0915b062b 100644
--- a/themes/bootstrap3/js/pubdate_vis.js
+++ b/themes/bootstrap3/js/pubdate_vis.js
@@ -1,7 +1,6 @@
 /*global htmlEncode*/
 
-function PadDigits(n, totalDigits)
-{
+function PadDigits(n, totalDigits) {
   if (n <= 0){
     n= 1;
   }
diff --git a/themes/bootstrap3/js/record.js b/themes/bootstrap3/js/record.js
index b67e1f085ab..3436b14961b 100644
--- a/themes/bootstrap3/js/record.js
+++ b/themes/bootstrap3/js/record.js
@@ -1,4 +1,4 @@
-/*global checkSaveStatuses, deparam, extractClassParams, htmlEncode, Lightbox, path, syn_get_widget, userIsLoggedIn, vufindString */
+/*global checkSaveStatuses, deparam, extractClassParams, htmlEncode, Lightbox, syn_get_widget, userIsLoggedIn, VuFind */
 
 /**
  * Functions and event handlers specific to record pages.
@@ -8,7 +8,7 @@ function checkRequestIsValid(element, requestType, blockedClass) {
   var vars = deparam(element.href);
   vars['id'] = recordId;
 
-  var url = path + '/AJAX/JSON?' + $.param({method:'checkRequestIsValid', id: recordId, requestType: requestType, data: vars});
+  var url = VuFind.getPath() + '/AJAX/JSON?' + $.param({method:'checkRequestIsValid', id: recordId, requestType: requestType, data: vars});
   $.ajax({
     dataType: 'json',
     cache: false,
@@ -42,7 +42,7 @@ function setUpCheckRequest() {
 }
 
 function deleteRecordComment(element, recordId, recordSource, commentId) {
-  var url = path + '/AJAX/JSON?' + $.param({method:'deleteRecordComment',id:commentId});
+  var url = VuFind.getPath() + '/AJAX/JSON?' + $.param({method:'deleteRecordComment',id:commentId});
   $.ajax({
     dataType: 'json',
     url: url,
@@ -55,7 +55,7 @@ function deleteRecordComment(element, recordId, recordSource, commentId) {
 }
 
 function refreshCommentList($target, recordId, recordSource) {
-  var url = path + '/AJAX/JSON?' + $.param({method:'getRecordCommentsAsHTML',id:recordId,'source':recordSource});
+  var url = VuFind.getPath() + '/AJAX/JSON?' + $.param({method:'getRecordCommentsAsHTML',id:recordId,'source':recordSource});
   $.ajax({
     dataType: 'json',
     url: url,
@@ -82,7 +82,7 @@ function registerAjaxCommentRecord() {
     var form = this;
     var id = form.id.value;
     var recordSource = form.source.value;
-    var url = path + '/AJAX/JSON?' + $.param({method:'commentRecord'});
+    var url = VuFind.getPath() + '/AJAX/JSON?' + $.param({method:'commentRecord'});
     var data = {
       comment:form.comment.value,
       id:id,
@@ -139,7 +139,8 @@ function ajaxLoadTab($newTab, tabid, setHash) {
   // Parse out the base URL for the current record:
   var urlParts = document.URL.split(/[?#]/);
   var urlWithoutFragment = urlParts[0];
-  if (path == '') {
+  var path = VuFind.getPath();
+  if (path === '') {
     // special case -- VuFind installed at site root:
     var chunks = urlWithoutFragment.split('/');
     var urlroot = '/' + chunks[3] + '/' + chunks[4];
@@ -180,7 +181,7 @@ function refreshTagList(target, loggedin) {
   var $tagList = $(target).find('.tagList');
   if ($tagList.length > 0) {
     $tagList.empty();
-    var url = path + '/AJAX/JSON?' + $.param({method:'getRecordTags',id:recordId,'source':recordSource});
+    var url = VuFind.getPath() + '/AJAX/JSON?' + $.param({method:'getRecordTags',id:recordId,'source':recordSource});
     $.ajax({
       dataType: 'json',
       url: url,
@@ -209,7 +210,7 @@ function ajaxTagUpdate(link, tag, remove) {
   var recordId = $target.find('.hiddenId').val();
   var recordSource = $target.find('.hiddenSource').val();
   $.ajax({
-    url:path+'/AJAX/JSON?method=tagRecord',
+    url:VuFind.getPath() + '/AJAX/JSON?method=tagRecord',
     method:'POST',
     data:{
       tag:'"'+tag.replace(/\+/g, ' ')+'"',
@@ -292,7 +293,7 @@ function recordDocReady() {
       if ($(this.parentNode).hasClass('noajax')) {
         return true;
       }
-      var newTab = $('<div class="tab-pane active '+tabid+'-tab"><i class="fa fa-spinner fa-spin"></i> '+vufindString['loading']+'...</div>');
+      var newTab = $('<div class="tab-pane active '+tabid+'-tab"><i class="fa fa-spinner fa-spin"></i> '+VuFind.translate('loading')+'...</div>');
       $top.find('.tab-content').append(newTab);
       return ajaxLoadTab(newTab, tabid);
     }
@@ -303,7 +304,7 @@ function recordDocReady() {
   setupRecordToolbar();
   // Form handlers
   Lightbox.addFormCallback('emailRecord', function(){
-    Lightbox.confirm(vufindString['bulk_email_success']);
+    Lightbox.confirm(VuFind.translate('bulk_email_success'));
   });
   Lightbox.addFormCallback('placeHold', function(html) {
     Lightbox.checkForError(html, function(html) {
@@ -314,22 +315,22 @@ function recordDocReady() {
     });
   });
   Lightbox.addFormCallback('placeILLRequest', function() {
-    document.location.href = path+'/MyResearch/ILLRequests';
+    document.location.href = VuFind.getPath() + '/MyResearch/ILLRequests';
   });
   Lightbox.addFormCallback('placeStorageRetrievalRequest', function() {
-    document.location.href = path+'/MyResearch/StorageRetrievalRequests';
+    document.location.href = VuFind.getPath() + '/MyResearch/StorageRetrievalRequests';
   });
   Lightbox.addFormCallback('saveRecord', function() {
     checkSaveStatuses();
     refreshTagList();
-    Lightbox.confirm(vufindString['bulk_save_success']);
+    Lightbox.confirm(VuFind.translate('bulk_save_success'));
   });
   Lightbox.addFormCallback('smsRecord', function() {
-    Lightbox.confirm(vufindString['sms_success']);
+    Lightbox.confirm(VuFind.translate('sms_success'));
   });
   // Tag lightbox
   Lightbox.addFormCallback('tagRecord', function(html) {
     refreshTagList(true);
-    Lightbox.confirm(vufindString['add_tag_success']);
+    Lightbox.confirm(VuFind.translate('add_tag_success'));
   });
 }
diff --git a/themes/bootstrap3/templates/cart/cart.phtml b/themes/bootstrap3/templates/cart/cart.phtml
index 271a063cab1..e3a893c40e7 100644
--- a/themes/bootstrap3/templates/cart/cart.phtml
+++ b/themes/bootstrap3/templates/cart/cart.phtml
@@ -72,7 +72,7 @@
     }
   }
   function submitCartForm(elem, data) {
-    var url = path+'/AJAX/JSON?method=getLightbox&submodule=Cart&subaction=Home';
+    var url = VuFind.getPath() + '/AJAX/JSON?method=getLightbox&submodule=Cart&subaction=Home';
     $.post(url, data, determineCallback(elem));
   }
   function submitFormWithIds(elem, data) {
diff --git a/themes/bootstrap3/templates/layout/layout.phtml b/themes/bootstrap3/templates/layout/layout.phtml
index 03f2d14dfdc..e11401bedd8 100644
--- a/themes/bootstrap3/templates/layout/layout.phtml
+++ b/themes/bootstrap3/templates/layout/layout.phtml
@@ -25,9 +25,6 @@
     <?=$this->headLink()?>
     <?=$this->headStyle()?>
     <?
-      // Set global path for Javascript code:
-      $this->headScript()->prependScript("path = '" . rtrim($this->url('home'), '/') . "';");
-
       // Deal with cart stuff:
       if (!isset($this->renderingError)) {
         // Add translation strings
@@ -99,6 +96,12 @@
       }
     ?>
     <?=$this->headScript()?>
+    <script>
+      var VuFind = new VuFindNamespace(
+          '<?=rtrim($this->url('home'), '/') ?>',
+          <?=$this->jsTranslations()->getJSON() ?>
+      );
+    </script>
   </head>
   <body class="<?=$this->layoutClass('offcanvas-row')?><? if ($this->layout()->rtl): ?> rtl<? endif; ?>">
     <? // Set up the search box -- there are three possible cases:
-- 
GitLab