Repository: OpenRefine/OpenRefine (java)

Issue:
Locale-dependency of Maven tests
Running `mvn test` with the `fr_FR` system locale gives the following test failures:

```
[ERROR] Failures: 
[ERROR]   ImporterUtilitiesTests.testGetOrAllocateColumn:176 expected [Column 1] but found [Colonne 1]
[ERROR]   SeparatorBasedImporterTests.readSimpleData_0Header_1Row:167 expected [Column 1] but found [Colonne 1]
[ERROR]   SeparatorBasedImporterTests.readSimpleData_0Header_1Row:167 expected [Column 1] but found [Colonne 1]
[ERROR]   SeparatorBasedImporterTests.readSimpleData_0Header_1Row:167 expected [Column 1] but found [Colonne 1]
[ERROR]   SeparatorBasedImporterTests.readSimpleData_RowLongerThanHeader:340 expected [Column 4] but found [Colonne 4]
[ERROR]   SeparatorBasedImporterTests.readSimpleData_RowLongerThanHeader:340 expected [Column 4] but found [Colonne 4]
[ERROR]   SeparatorBasedImporterTests.readSimpleData_RowLongerThanHeader:340 expected [Column 4] but found [Colonne 4]
[ERROR]   ReconOperationTests.serializeReconProcess:121 Objects above are not equal as JSON strings.
```

Tests should pass regardless of the system locale.

Pull request:
Fixed Maven tests to work in different locales
Fixes #6171 

Changes proposed in this pull request:
- Made it so the following Maven tests do not depend on locale:
  - `readSimpleData_0Header_1Row()`
  - `readSimpleData_RowLongerThanHeader()`
  - `serializeReconProcess()`
  - `readXlsxAsText()`


Patch:
diff --git a/extensions/gdata/pom.xml b/extensions/gdata/pom.xml
index 59375016a..2ed919c1b 100644
--- a/extensions/gdata/pom.xml
+++ b/extensions/gdata/pom.xml
@@ -84,7 +84,7 @@
     <dependency>
       <groupId>com.google.apis</groupId>
       <artifactId>google-api-services-drive</artifactId>
-      <version>v3-rev20231120-2.0.0</version>
+      <version>v3-rev20231128-2.0.0</version>
     </dependency>
     <dependency>
       <groupId>com.google.apis</groupId>
diff --git a/extensions/wikibase/module/styles/dialogs/wikibase-dialog.css b/extensions/wikibase/module/styles/dialogs/wikibase-dialog.css
index f7006289c..3ce5cd44d 100644
--- a/extensions/wikibase/module/styles/dialogs/wikibase-dialog.css
+++ b/extensions/wikibase/module/styles/dialogs/wikibase-dialog.css
@@ -59,7 +59,7 @@
 
 .wikibase-list .wikibase-url {
   padding-left: 13px;
-  color: gray;
+  color: var(--light-grey);
 }
 
 .wikibase-list li a, .wikibase-list li span {
diff --git a/extensions/wikibase/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizer.java b/extensions/wikibase/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizer.java
index 353d3a3f5..a54c8eaf8 100644
--- a/extensions/wikibase/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizer.java
+++ b/extensions/wikibase/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizer.java
@@ -22,7 +22,7 @@ public class FileNameScrutinizer extends EditScrutinizer {
     // see https://commons.wikimedia.org/wiki/Commons:File_naming
     public static final int maxFileNameBytes = 240;
     public static final Pattern forbiddenFileNameChars = Pattern.compile(
-            ".*([^ %!\"$&'()*,\\-./\\d:;=?@\\p{L}\\p{Mc}\\\\^_`~\\x80-\\xFF+]|%[0-9A-Fa-f]{2}|&[A-Za-z0-9\\x80-\\xff]+;|&#[0-9]+;|&#x[0-9A-Fa-f]+;).*");
+            ".*([^ %!\"$&'()*,\\-./\\d:;=?@\\p{L}\\p{M}\\p{N}\\\\^_`~\\x80-\\xFF+]|%[0-9A-Fa-f]{2}|&[A-Za-z0-9\\x80-\\xff]+;|&#[0-9]+;|&#x[0-9A-Fa-f]+;).*");
 
     public static final String duplicateFileNamesInBatchType = "duplicate-file-names-in-batch";
     public static final String fileNamesAlreadyExistOnWikiType = "file-names-already-exist-on-wiki";
@@ -98,7 +98,7 @@ public class FileNameScrutinizer extends EditScrutinizer {
             // Invalid characters
             Matcher matcher = forbiddenFileNameChars.matcher(fileName);
             if (matcher.matches()) {
-                QAWarning issue = new QAWarning(invalidCharactersInFileNameType, null, QAWarning.Severity.CRITICAL,
+                QAWarning issue = new QAWarning(invalidCharactersInFileNameType, null, QAWarning.Severity.IMPORTANT,
                         1);
                 issue.setProperty("example_filename", fileName);
                 issue.setProperty("invalid_character", matcher.group(1));
diff --git a/main/webapp/modules/core/MOD-INF/controller.js b/main/webapp/modules/core/MOD-INF/controller.js
index 2fa089a27..ce9328949 100644
--- a/main/webapp/modules/core/MOD-INF/controller.js
+++ b/main/webapp/modules/core/MOD-INF/controller.js
@@ -483,6 +483,7 @@ function init() {
 
       "scripts/reconciliation/recon-manager.js",
       "scripts/reconciliation/recon-dialog.js",
+      "scripts/reconciliation/recon-dialog-2.js",
       "scripts/reconciliation/standard-service-panel.js",
 
       "scripts/dialogs/expression-preview-dialog.js",
diff --git a/main/webapp/modules/core/langs/translation-en.json b/main/webapp/modules/core/langs/translation-en.json
index d6b4687ea..2eec9bb9e 100644
--- a/main/webapp/modules/core/langs/translation-en.json
+++ b/main/webapp/modules/core/langs/translation-en.json
@@ -440,8 +440,9 @@
     "core-recon/contact-service": "Contacting reconciliation service",
     "core-recon/error-contact": "Error contacting recon service",
     "core-recon/wd-recon-lang": "en",
-    "core-recon/recon-col": "Reconcile column",
-    "core-recon/pick-service": "Pick a service or extension on left",
+    "core-recon/recon-col": "Reconcile column $1",
+    "core-recon/pick-service": "Select a service to reconcile against:",
+    "core-recon/disable-next-button": "Pick a service to continue",
     "core-recon/enter-url": "Enter the service's URL",
     "core-recon/cell-contains": "Each cell contains:",
     "core-recon/max-candidates": "Maximum number of candidates to return",
diff --git a/main/webapp/modules/core/project.vt b/main/webapp/modules/core/project.vt
index bc7bf7eea..7c21ea12a 100644
--- a/main/webapp/modules/core/project.vt
+++ b/main/webapp/modules/core/project.vt
@@ -38,7 +38,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     <title>OpenRefine</title>
     <link rel="icon" type="image/png" href="images/favicon.png">
     $styleInjection
-    <script type="text/javascript">var theProject = { id : $projectID };</script>
+    <script type="text/javascript">
+      const urlParams = new URLSearchParams(window.location.search);
+      var theProject = { id : urlParams.get("project") };
+    </script>
     <script type="text/javascript" src="wirings.js"></script>
     $scriptInjection
     <script>Refine.encodings = $encodingJson; Refine.defaultEncoding = $defaultEncoding;</script>
diff --git a/main/webapp/modules/core/scripts/reconciliation/recon-dialog-2.html b/main/webapp/modules/core/scripts/reconciliation/recon-dialog-2.html
new file mode 100644
index 000000000..bacc61723
--- /dev/null
+++ b/main/webapp/modules/core/scripts/reconciliation/recon-dialog-2.html
@@ -0,0 +1,21 @@
+<div class="dialog-frame" style="width: 900px;">
+	<div class="dialog-header" bind="dialogHeader"></div>
+	<div class="dialog-body" bind="dialogBody">
+		<div class="grid-layout layout-normal layout-full grid-layout-for-ui">
+			<div class="recon-dialog-service">
+				<div class="recon-dialog-service-panel-container"
+					bind="servicePanelContainer">
+                </div>
+			</div>
+		</div>
+	</div>
+	<div class="dialog-footer" bind="dialogFooter" style="justify-content: space-between">
+		<div class="dialog-footer-bg">
+            <button class="button" bind="backButton"></button>
+		</div>
+		<div class="dialog-footer-bg">
+            <button class="button button-primary" bind="reconcileButton"></button>
+			<button class="button" bind="cancelButton"></button>
+		</div>
+	</div>
+</div>
diff --git a/main/webapp/modules/core/scripts/reconciliation/recon-dialog-2.js b/main/webapp/modules/core/scripts/reconciliation/recon-dialog-2.js
new file mode 100644
index 000000000..a8d4e3c42
--- /dev/null
+++ b/main/webapp/modules/core/scripts/reconciliation/recon-dialog-2.js
@@ -0,0 +1,128 @@
+/*
+
+Copyright 2010, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,           
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY           
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ */
+
+function ReconDialog2(column, selectedServiceRecordindex, serviceRecords, record, previousService) {
+  this._column = column;
+  this._serviceRecords = serviceRecords;
+  this._selectedServiceRecordIndex = selectedServiceRecordindex;
+  this._previousService = previousService;
+  this._record = record;
+
+  this._createDialog();
+}
+
+ReconDialog2.prototype._createDialog = function() {
+  var self = this;
+  var dialog = $(DOM.loadHTML("core", "scripts/reconciliation/recon-dialog-2.html"));
+
+  this._elmts = DOM.bind(dialog);
+  this._elmts.dialogHeader.text($.i18n('core-recon/recon-col',this._column.name ));
+  this._elmts.reconcileButton.html($.i18n('core-buttons/start-recon'));
+  this._elmts.cancelButton.html($.i18n('core-buttons/cancel'));
+  this._elmts.backButton.html($.i18n('core-buttons/previous'));
+
+  this._elmts.reconcileButton.on('click',function() { self._onOK(); });
+  this._elmts.cancelButton.on('click',function() { self._dismiss(); });
+  this._elmts.backButton.on('click',function() { self._previousDialog(self._column, self._previousService); });
+
+
+  this._level = DialogSystem.showDialog(dialog);
+  this._selectService(self._record);
+};
+
+
+ReconDialog2.prototype._previousDialog = function(column, previousService) {
+  this._dismiss();
+  new ReconDialog(column, previousService);
+}
+
+
+ReconDialog2.prototype._onOK = function() {
+
+  if (this._selectedServiceRecordIndex >= 0) {
+    var record = this._serviceRecords[this._selectedServiceRecordIndex];
+    if (record.handler) {
+      record.handler.start();
+    }
+  }
+  this._dismiss();
+};
+
+
+ReconDialog2.prototype._dismiss = function() {
+  for (var i = 0; i < this._serviceRecords.length; i++) {
+    var record = this._serviceRecords[i];
+    if (record.handler) {
+      record.handler.dispose();
+    }
+  }
+  this._serviceRecords = null;
+
+  DialogSystem.dismissUntil(this._level - 1);
+};
+
+
+ReconDialog2.prototype._cleanDialog = function() {
+  for (var i = 0; i < this._serviceRecords.length; i++) {
+    var record = this._serviceRecords[i];
+    if (record.handler) {
+      record.handler.deactivate();
+    }
+    record.selector.remove();
+  }
+  this._serviceRecords = [];
+  this._selectedServiceRecordIndex = -1;
+};
+
+
+ReconDialog2.prototype._selectService = function(record) {
+  for (var i = 0; i < this._serviceRecords.length; i++) {
+    if (record === this._serviceRecords[i]) {
+       {  this._selectedServiceRecordIndex = i;
+          if (record.handler) {
+            record.handler.activate();
+          } else {
+          var handlerConstructor = eval(record.service.ui.handler);
+          record.handler = new handlerConstructor(
+              this._column, record.service, this._elmts.servicePanelContainer);
+        }  
+        return;
+    }
+  };   
+};
+}
+
+ReconDialog2.prototype._refresh = function(newSelectIndex) {
+  this._cleanDialog();
+  this._populateDialog();
+};
diff --git a/main/webapp/modules/core/scripts/reconciliation/recon-dialog.html b/main/webapp/modules/core/scripts/reconciliation/recon-dialog.html
index cfb8336b2..1903b709a 100644
--- a/main/webapp/modules/core/scripts/reconciliation/recon-dialog.html
+++ b/main/webapp/modules/core/scripts/reconciliation/recon-dialog.html
@@ -3,17 +3,13 @@
 	<div class="dialog-body" bind="dialogBody">
 		<div class="grid-layout layout-normal layout-full grid-layout-for-ui">
 			<div class="recon-dialog-service">
-				<div class="recon-dialog-service-list-container">
-					<div class="recon-dialog-service-opener">
-						<div class="recon-dialog-service-opener-title" bind="serviceListTitle"></div>
-						<img class="recon-dialog-service-opener-img" src="images/menu-opener.png" />
-					</div>
-					<div class="recon-dialog-service-list" bind="serviceList"></div>
-				</div>
-				<div class="recon-dialog-service-panel-container"
+                <div class="recon-dialog-service-panel-container"
 					bind="servicePanelContainer">
 					<div class="recon-dialog-service-panel-message"
 						bind="servicePanelMessage"></div>
+					</div>
+                <div class="recon-dialog-service-list-container">
+					<div class="recon-dialog-service-list" input type="radio" name="service-choice" bind="serviceList"></div>
 				</div>
 			</div>
 		</div>
@@ -24,7 +20,8 @@
 			<a class="button" bind="discoverServicesButton" href="https://reconciliation-api.github.io/testbench" target="_blank" ></a>
 		</div>
 		<div class="dialog-footer-bg">
-			<button class="button" bind="reconcileButton"></button>
+			<div class="pop-up-message" id="popup-message" bind="popUpMessage"></div>
+			<button class="button button-primary" bind="nextButton"></button>
 			<button class="button" bind="cancelButton"></button>
 		</div>
 	</div>
diff --git a/main/webapp/modules/core/scripts/reconciliation/recon-dialog.js b/main/webapp/modules/core/scripts/reconciliation/recon-dialog.js
index e75947549..65cbb6800 100644
--- a/main/webapp/modules/core/scripts/reconciliation/recon-dialog.js
+++ b/main/webapp/modules/core/scripts/reconciliation/recon-dialog.js
@@ -31,10 +31,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
  */
 
-function ReconDialog(column, types) {
+function ReconDialog(column, previousServiceURL) {
   this._column = column;
   this._serviceRecords = [];
   this._selectedServiceRecordIndex = -1;
+  this._record = null;
+  this.previousRecordURL = previousServiceURL;
 
   this._createDialog();
 }
@@ -44,44 +46,43 @@ ReconDialog.prototype._createDialog = function() {
   var dialog = $(DOM.loadHTML("core", "scripts/reconciliation/recon-dialog.html"));
 
   this._elmts = DOM.bind(dialog);
-  this._elmts.dialogHeader.text($.i18n('core-recon/recon-col')+' "' + this._column.name + '"');
-  
+  this._elmts.dialogHeader.text($.i18n('core-recon/recon-col',this._column.name));
+
   this._elmts.servicePanelMessage.html($.i18n('core-recon/pick-service'));
-  this._elmts.serviceListTitle.html($.i18n('core-recon/service-title'));
   this._elmts.addStandardServiceButton.html($.i18n('core-buttons/add-std-svc')+"...");
-  this._elmts.reconcileButton.html($.i18n('core-buttons/start-recon'));
   this._elmts.cancelButton.html($.i18n('core-buttons/cancel'));
   this._elmts.discoverServicesButton.html($.i18n('core-buttons/discover-services'));
+  this._elmts.nextButton.html($.i18n('core-buttons/next'));
+  this._elmts.popUpMessage.html($.i18n('core-recon/disable-next-button'));
 
   this._elmts.addStandardServiceButton.on('click',function() { self._onAddStandardService(); });
 
-  this._elmts.reconcileButton.on('click',function() { self._onOK(); });
   this._elmts.cancelButton.on('click',function() { self._dismiss(); });
+  this._elmts.nextButton.on('click',function() { 
+    if(self._record){
+    self._nextDialog(self._column, self._selectedServiceRecordIndex, self._serviceRecords, self._record, self.previousRecord);
+    }
+    else{
+      var message = document.getElementById('popup-message');
+        message.classList.add('show');
+        setTimeout(function() {
+            message.classList.remove('show');
+        }, 1000); 
+
+    }
+   });
 
   this._level = DialogSystem.showDialog(dialog);
-  this._populateDialog();
-  this._registerDialogServiceOpener();
+  this._populateDialog(self.previousRecordURL);
+
 };
 
-ReconDialog.prototype._registerDialogServiceOpener = function() {
-  var self = this;
 
-  $('.recon-dialog-service-opener').on('click',function() {
-    self._toggleServices();
-  });
+ReconDialog.prototype._nextDialog = function(column, selectedServiceRecordIndex, serviceRecords, record, previousService) {
+  this._dismiss();
+  new ReconDialog2(column, selectedServiceRecordIndex, serviceRecords, record, previousService);
 }
 
-ReconDialog.prototype._onOK = function() {
-  if (this._selectedServiceRecordIndex >= 0) {
-    var record = this._serviceRecords[this._selectedServiceRecordIndex];
-    if (record.handler) {
-      if (record.handler.start()) {
-        this._dismiss();
-      }
-    }
-  }
-};
-
 ReconDialog.prototype._dismiss = function() {
   for (var i = 0; i < this._serviceRecords.length; i++) {
     var record = this._serviceRecords[i];
@@ -106,102 +107,80 @@ ReconDialog.prototype._cleanDialog = function() {
   this._selectedServiceRecordIndex = -1;
 };
 
-ReconDialog.prototype._populateDialog = function() {
+ReconDialog.prototype._populateDialog = function(selectedURL) {
   var self = this;
+  self._elmts.serviceList.empty();
 
   var services = ReconciliationManager.getAllServices();
   if (services.length > 0) {
+    
     var renderService = function(service) {
       var record = {
           service: service,
           handler: null
       };
 
-      record.selector = $('<a>')
-      .attr("href", "javascript:{}")
-      .addClass("recon-dialog-service-selector")
-      .text(service.name)
-      .appendTo(self._elmts.serviceList)
-      .on('click',function() {
-    	self._toggleServices();
-        self._selectService(record);
-      });
+      var label = $('<label>')
+        .addClass('recon-dialog-service-list-element')
+        .appendTo(self._elmts.serviceList);
+
+        
+      record.selector = $('<input type="radio" name="service-choice">')
+        .addClass("recon-dialog-service-selector")
+        .val(service.name) 
+        .appendTo(label);
+
+      if(selectedURL === service.url) {
+        record.selector.prop('checked',true);
+        self._record=record;
+      }
+
+      var mainSpan=$('<span>')
+        .addClass("recon-service-entry")
+        .appendTo(label);
+
+      var divElement=$('<div>')
+        .text(service.name)
+        .appendTo(mainSpan);
+
+      var divElementURL = $('<div>')
+        .addClass("recon-dialog-url-element")
+        .text(service.url)
+        .appendTo(mainSpan);
+      
+       label.on('click', function() {
+       self._record = record;
+      
+});
 
       $('<a>')
       .html("&nbsp;")
       .addClass("recon-dialog-service-selector-remove")
-      .prependTo(record.selector)
+      .prependTo(label)
       .on('click',function(event) {
+        $(this).parents('label').remove();
         ReconciliationManager.unregisterService(service, function() {
-          self._refresh(-1);
+          self._record = null;
         });
+
         event.stopImmediatePropagation();
       });
 
       self._serviceRecords.push(record);
+      
     };
 
     for (var i = 0; i < services.length; i++) {
       renderService(services[i]);
     }
+    
   }
 };
 
-ReconDialog.prototype._toggleServices = function() {
-  var self = this;
-  self._toggleServiceTitle(500);
-  self._toggleServiceList(500);
-};
-
-ReconDialog.prototype._toggleServiceTitle = function(duration) {
-  var title = $('.recon-dialog-service-opener-title');
-  title.animate({
-	width : 'toggle'
-	}, duration, 'swing', function() {
-  });
-};
-
-ReconDialog.prototype._toggleServiceList = function(duration) {
-  $(".recon-dialog-service-list").toggle("slide", duration);
-};
 
-ReconDialog.prototype._selectService = function(record) {
-  for (var i = 0; i < this._serviceRecords.length; i++) {
-    if (record === this._serviceRecords[i]) {
-      if (i !== this._selectedServiceRecordIndex) {
-        if (this._selectedServiceRecordIndex >= 0) {
-          var oldRecord = this._serviceRecords[this._selectedServiceRecordIndex];
-          if (oldRecord.handler) {
-            oldRecord.selector.removeClass("selected");
-            oldRecord.handler.deactivate();
-          }
-        }
-
-        this._elmts.servicePanelMessage.hide();
-
-        record.selector.addClass("selected");
-        if (record.handler) {
-          record.handler.activate();
-        } else {
-          var handlerConstructor = eval(record.service.ui.handler);
-
-          record.handler = new handlerConstructor(
-              this._column, record.service, this._elmts.servicePanelContainer);
-        }
-
-        this._selectedServiceRecordIndex = i;
-        return;
-      }
-    }
-  }
-};
-
-ReconDialog.prototype._refresh = function(newSelectIndex) {
+ReconDialog.prototype._refresh = function(selectedURL) {
   this._cleanDialog();
-  this._populateDialog();
-  if (newSelectIndex >= 0) {
-    this._selectService(this._serviceRecords[newSelectIndex]);
-  }
+  this._populateDialog(selectedURL);
 };
 
 ReconDialog.prototype._onAddStandardService = function() {
@@ -224,7 +203,7 @@ ReconDialog.prototype._onAddStandardService = function() {
     var url = jQueryTrim(elmts.input[0].value);
     if (url.length > 0) {
       ReconciliationManager.registerStandardService(url, function(index) {
-        self._refresh(index);
+        self._refresh(url);
       });
     }
     dismiss();
diff --git a/main/webapp/modules/core/scripts/views/data-table/column-header-ui.js b/main/webapp/modules/core/scripts/views/data-table/column-header-ui.js
index 8f7ed51c6..2356e9071 100644
--- a/main/webapp/modules/core/scripts/views/data-table/column-header-ui.js
+++ b/main/webapp/modules/core/scripts/views/data-table/column-header-ui.js
@@ -69,7 +69,7 @@ DataTableColumnHeaderUI.prototype._render = function() {
   elmts.dropdownMenu.on('click',function() {
     self._createMenuForColumnHeader(this);
   });
-
+  
   if ("reconStats" in this._column) {
     var stats = this._column.reconStats;
     if (stats.nonBlanks > 0) {
diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js b/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js
index a34695d15..ab0e794c5 100644
--- a/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js
+++ b/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js
@@ -33,8 +33,16 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
   var columnIndex = Refine.columnNameToColumnIndex(column.name);
+  var serviceUrl = null;
+  var service = null;
+  if (column.reconConfig) {
+    serviceUrl = column.reconConfig.service;
+  }
+  if (serviceUrl) {
+    service = ReconciliationManager.getServiceFromUrl(serviceUrl);
+  }
   var doReconcile = function() {
-    new ReconDialog(column);
+      new ReconDialog(column, serviceUrl);
   };
 
   var doReconDiscardJudgments = function() {
diff --git a/main/webapp/modules/core/styles/reconciliation/recon-dialog.css b/main/webapp/modules/core/styles/reconciliation/recon-dialog.css
index 2a219ccfe..e035a75ea 100644
--- a/main/webapp/modules/core/styles/reconciliation/recon-dialog.css
+++ b/main/webapp/modules/core/styles/reconciliation/recon-dialog.css
@@ -32,96 +32,111 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 
 .recon-dialog-service {
-  padding: 0 0 10px;
+  padding: 0 10px 10px;
 }
 
 .recon-dialog-service-list-container {
-  position: absolute;
+  border: 1px solid #ccc;
+  max-height: 470px;
+  height: 470px !important;
   z-index: 10;
-}
-
-.recon-dialog-service-list {
-  background: none repeat scroll 0 0 #FFFFFF;
-  border: 1px solid #AAAAAA;
-  height: 470px;
-  margin-left: 10px;
   overflow: auto;
-  padding: 1px;
-  width: 200px;
-}
-
-.recon-dialog-service-opener {
-  height: 25px;
-  margin-left: -5px;
-}
-
-.recon-dialog-service-opener:hover {
-  cursor: pointer;
 }
 
-.recon-dialog-service-opener-title {
-  background: none repeat scroll 0 0 #E0EDFE;
-  float: left;
-  font-size: 18px;
-  height: 100%;
-  text-align: center;
-  width: 220px;
-}
+.recon-dialog-service-list {
+padding:1px;
+padding-bottom:10px ;
+height: 10px ;
+max-height: 100px;
+display: block;
 
-.recon-dialog-service-opener-img {
-  background: none repeat scroll 0 0 #E0EDFE;
-  border-bottom-right-radius: 12px;
-  border-top-right-radius: 12px;
-  padding: 4px 2px;
 }
 
-.recon-dialog-service-opener-img:hover {
-  background: none repeat scroll 0 0 #BFDBFF;
-}
 
-.recon-dialog-service-panel-container {
-  float: left;
-  min-height: 500px;
-  margin-left: 20px;
-  width: 850px;
-}
-
-a.recon-dialog-service-selector {
-  display: block;
+a.recon-dialog-service-list-container {
+  border: 1px solid #ccc;
   padding: 10px;
-  text-decoration: none;
-  color: var(--text-primary);
+  max-height: 100px;
+  height: 100px !important;
 }
 
-a.recon-dialog-service-selector:hover {
+a.recon-dialog-service-list-container {
   background: #eee;
 }
 
-a.recon-dialog-service-selector.selected {
+a.recon-dialog-service-list-container.selected {
   background: #eee;
   font-weight: bold;
 }
 
 .recon-dialog-service-panel-message {
-  text-align: center;
-  font-size: 200%;
-  padding: 50px 0;
-  color: #aaa;
+    text-align: left;
+    font-size: 100%;
+    padding: 10px 0;
+    color:black;
 }
+.recon-service-entry{
+  margin-left: 0.2em;
+  margin-right: 0.25em;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  max-width: 15ch;
+  display: inline-block;
+  vertical-align: middle 
 
+}
+
+.recon-dialog-url-element{
+  color:var(--light-grey);
+}
 a.recon-dialog-service-selector-remove {
   float: right;
   position: relative;
-  top: -5px;
+  top: 5px;
   left: 5px;
   width: 12px;
   height: 12px;
+  margin-right: 10px;
   text-decoration: none;
   background-image: url(../../images/close-map.png);
   background-repeat: no-repeat;
   background-position: 0px 0px;
 }
+.dialog-footer {
+  display: flex;
+  justify-content: space-between;
+}
+
+.button-left {
+  display: flex;
+}
+
+.button-right {
+  display: flex;
+  gap: 10px; 
+}
+.recon-service-entry{
+  display:inline-block;
+
+}
+.recon-dialog-service-list-element{
+  display:block;
+  margin-bottom: 0.5em ;
+  padding:5px 10px
+}
+.recon-dialog-service-list-element:hover{
+  background-color:var(--dialog-header);
+}
+
+.pop-up-message {
+  position: absolute;
+  background-color: white;
+  color: black;
+  padding: 5px;
+  display: none; 
+  top:570px;
+}
 
-a.recon-dialog-service-selector-remove:hover {
-  background-position: -12px 0px;
+.pop-up-message.show {
+  display: block; 
 }
diff --git a/refine b/refine
index 58cdda042..08c9208d4 100755
--- a/refine
+++ b/refine
@@ -42,17 +42,18 @@ Options
     --jmx                         Enable JMX monitoring.
 
 Actions
+    run                           Run OpenRefine [default].
     build                         Build OpenRefine.
     clean                         Clean compiled classes.
-    dist <version>                Make all distributions.
+    test                          Run all tests.
     extensions_test               Run the extensions tests.
-    linux_dist <version>          Make Linux binary distribution.
-    mac_dist <version>            Make MacOSX binary distribution.
-    run                           Run OpenRefine [default].
     server_test                   Run the server tests.
-    test                          Run all tests.
-    e2e_tests                      Run the e2e tests.
+    e2e_tests                     Run the e2e tests.
+    lint                          Reformat the source code according to OpenRefine's conventions.
+    mac_dist <version>            Make MacOS binary distribution.
     windows_dist <version>        Make Windows binary distribution.
+    linux_dist <version>          Make Linux binary distribution.
+    dist <version>                Make all distributions.
 EOF
 exit 1
 }
@@ -244,6 +245,13 @@ do_mvn() {
 
 # ----------------------------------------------------------------------------------------------
 
+lint() {
+    mvn_prepare
+    "$MVN" formatter:format impsort:sort
+}
+
+# ----------------------------------------------------------------------------------------------
+
 dist() {
     get_version $1
     build_prepare
@@ -720,12 +728,13 @@ case "$ACTION" in
   clean) do_mvn clean;;
   test) test $1;;
   tests) test $1;;
-  e2e_tests) e2e_tests;;    
+  e2e_tests) e2e_tests;;
   server_test) server_test $1;;  
   server_tests) server_test $1;;  
   extensions_test) extensions_test $1;;
   extensions_tests) extensions_test $1;;
   run) run;;
+  lint) lint;;
   mac_dist) mac_dist $1;;
   windows_dist) windows_dist $1;;
   linux_dist) linux_dist $1;;
diff --git a/refine.bat b/refine.bat
index ef9361f6f..3e6768dfd 100644
--- a/refine.bat
+++ b/refine.bat
@@ -48,6 +48,7 @@ echo     extensions_test         Run the extensions tests.
 echo     run                     Run OpenRefine.
 echo     server_test             Run the server tests.
 echo     test                    Run all the tests.
+echo     lint                    Reformat the source code according to OpenRefine's conventions.
 goto :eof
 
 
@@ -190,6 +191,7 @@ if ""%ACTION%"" == ""build"" goto doMvn
 if ""%ACTION%"" == ""server_test"" goto doMvn
 if ""%ACTION%"" == ""extensions_test"" goto doMvn
 if ""%ACTION%"" == ""test"" goto doMvn
+if ""%ACTION%"" == ""lint"" goto doMvn
 if ""%ACTION%"" == ""clean"" goto doMvn
 if ""%ACTION%"" == ""run"" goto doRun
 if ""%ACTION%"" == """" goto doRun
@@ -270,6 +272,12 @@ set MVN_ACTION=compile test-compile dependency:build-classpath
 if ""%ACTION%"" == ""test"" set MVN_ACTION=test dependency:build-classpath
 if ""%ACTION%"" == ""server_test"" set MVN_ACTION=test -f main
 if ""%ACTION%"" == ""extensions_test"" set MVN_ACTION=test -f extensions
+if ""%ACTION%"" == ""lint"" (
+    set MVN_ACTION=formatter:format impsort:sort
+    rem Skip the call to process-resources as it's not needed for this action
+    goto :mvnCall
+)
 call "%MVN%" process-resources
+:mvnCall
 call "%MVN%" %MVN_ACTION%
 goto :eof


Test patch:
diff --git a/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizerTest.java b/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizerTest.java
index abb1da6c4..5ced94b25 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizerTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizerTest.java
@@ -78,6 +78,16 @@ public class FileNameScrutinizerTest extends ScrutinizerTest {
         assertNoWarningRaised();
     }
 
+    @Test
+    public void testValidCharactersInFilenameNonAscii() {
+        MediaInfoEdit edit = new MediaInfoEditBuilder(TestingData.newMidA)
+                .addFileName("সমাচার দর্পণ - ৮ অক্টোবর ১৮৩৬.pdf")
+                .build();
+
+        scrutinize(edit);
+        assertNoWarningRaised();
+    }
+
     @Test()
     public void testInvalidCharactersInFilenameTab() throws IOException, MediaWikiApiErrorException {
         MediaInfoEdit edit = new MediaInfoEditBuilder(TestingData.newMidA)
diff --git a/main/tests/server/src/com/google/refine/importers/ExcelImporterTests.java b/main/tests/server/src/com/google/refine/importers/ExcelImporterTests.java
index 7edf3c6b7..13693c706 100644
--- a/main/tests/server/src/com/google/refine/importers/ExcelImporterTests.java
+++ b/main/tests/server/src/com/google/refine/importers/ExcelImporterTests.java
@@ -50,6 +50,7 @@ import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.Locale;
 
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
@@ -94,7 +95,6 @@ public class ExcelImporterTests extends ImporterTest {
 
     private static final File xlsFileWithMultiSheets = createSheetsWithDifferentColumns(false);
     private static final File xlsxFileWithMultiSheets = createSheetsWithDifferentColumns(true);
-    private static final NumberFormat NUMBER_FORMAT = DecimalFormat.getInstance();
 
     @Override
     @BeforeTest
@@ -268,8 +268,10 @@ public class ExcelImporterTests extends ImporterTest {
 
         Assert.assertEquals(project.rows.size(), ROWS);
         Assert.assertEquals(project.rows.get(1).cells.size(), COLUMNS);
-        Assert.assertEquals(((String) project.rows.get(1).getCellValue(0)), NUMBER_FORMAT.format(1.1));
-        Assert.assertEquals(((String) project.rows.get(2).getCellValue(0)), NUMBER_FORMAT.format(2.2));
+
+        final NumberFormat numberFormat = DecimalFormat.getInstance(Locale.getDefault());
+        Assert.assertEquals((String) project.rows.get(1).getCellValue(0), numberFormat.format(1.1));
+        Assert.assertEquals((String) project.rows.get(2).getCellValue(0), numberFormat.format(2.2));
 
         assertEquals((String) project.rows.get(1).getCellValue(1), "FALSE");
         assertEquals((String) project.rows.get(2).getCellValue(1), "TRUE");
@@ -286,8 +288,10 @@ public class ExcelImporterTests extends ImporterTest {
         assertEquals(project.rows.get(1).getCellValue(7), "1");
         assertEquals(project.rows.get(2).getCellValue(7), "2");
 
-        assertEquals(project.rows.get(1).getCellValue(8), String.format("%.2f", 100.0) + "%");
-        assertEquals(project.rows.get(2).getCellValue(8), String.format("%.2f", 200.0) + "%");
+        final DecimalFormat decimalFormat = (DecimalFormat) NumberFormat.getNumberInstance(Locale.getDefault());
+        decimalFormat.applyPattern("0.00");
+        assertEquals(project.rows.get(1).getCellValue(8), decimalFormat.format(100.00) + "%");
+        assertEquals(project.rows.get(2).getCellValue(8), decimalFormat.format(200.00) + "%");
 
         assertEquals(project.rows.get(1).getCellValue(9), "0001");
         assertEquals(project.rows.get(2).getCellValue(9), "0002");
@@ -296,7 +300,7 @@ public class ExcelImporterTests extends ImporterTest {
 
         assertEquals(project.rows.get(ROWS - 1).getCellValue(11), NOW_STRING.substring(0, 10)); // date only
 
-        assertEquals(project.rows.get(ROWS - 1).getCellValue(12), "$" + NUMBER_FORMAT.format(1234.56));
+        assertEquals(project.rows.get(ROWS - 1).getCellValue(12), "$" + numberFormat.format(1234.56));
 
         verify(options, times(1)).get("ignoreLines");
         verify(options, times(1)).get("headerLines");
diff --git a/main/tests/server/src/com/google/refine/importers/ImporterUtilitiesTests.java b/main/tests/server/src/com/google/refine/importers/ImporterUtilitiesTests.java
index c61468cf3..1c7c2314d 100644
--- a/main/tests/server/src/com/google/refine/importers/ImporterUtilitiesTests.java
+++ b/main/tests/server/src/com/google/refine/importers/ImporterUtilitiesTests.java
@@ -48,6 +48,7 @@ import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Test;
 
 import com.google.refine.RefineTest;
+import com.google.refine.messages.OpenRefineMessage;
 import com.google.refine.model.Cell;
 import com.google.refine.model.Column;
 import com.google.refine.model.Project;
@@ -173,8 +174,8 @@ public class ImporterUtilitiesTests extends RefineTest {
         List<String> newColumnNames = new ArrayList<String>();
         Column c0 = ImporterUtilities.getOrAllocateColumn(project, newColumnNames, 0, false);
         Column c1 = ImporterUtilities.getOrAllocateColumn(project, newColumnNames, 1, false);
-        Assert.assertEquals(c0.getName(), "Column 1");
-        Assert.assertEquals(c1.getName(), "Column 2");
+        Assert.assertEquals(c0.getName(), OpenRefineMessage.importer_utilities_column() + " 1");
+        Assert.assertEquals(c1.getName(), OpenRefineMessage.importer_utilities_column() + " 2");
         Assert.assertEquals(newColumnNames.size(), 2);
     }
 }
diff --git a/main/tests/server/src/com/google/refine/importers/SeparatorBasedImporterTests.java b/main/tests/server/src/com/google/refine/importers/SeparatorBasedImporterTests.java
index f96121f62..1ae54cd7e 100644
--- a/main/tests/server/src/com/google/refine/importers/SeparatorBasedImporterTests.java
+++ b/main/tests/server/src/com/google/refine/importers/SeparatorBasedImporterTests.java
@@ -53,6 +53,7 @@ import org.testng.annotations.BeforeTest;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import com.google.refine.messages.OpenRefineMessage;
 import com.google.refine.util.ParsingUtilities;
 
 public class SeparatorBasedImporterTests extends ImporterTest {
@@ -164,9 +165,9 @@ public class SeparatorBasedImporterTests extends ImporterTest {
             Assert.fail("Exception during file parse", e);
         }
         Assert.assertEquals(project.columnModel.columns.size(), 3);
-        Assert.assertEquals(project.columnModel.columns.get(0).getName(), "Column 1");
-        Assert.assertEquals(project.columnModel.columns.get(1).getName(), "Column 2");
-        Assert.assertEquals(project.columnModel.columns.get(2).getName(), "Column 3");
+        Assert.assertEquals(project.columnModel.columns.get(0).getName(), OpenRefineMessage.importer_utilities_column() + " 1");
+        Assert.assertEquals(project.columnModel.columns.get(1).getName(), OpenRefineMessage.importer_utilities_column() + " 2");
+        Assert.assertEquals(project.columnModel.columns.get(2).getName(), OpenRefineMessage.importer_utilities_column() + " 3");
         Assert.assertEquals(project.rows.size(), 1);
         Assert.assertEquals(project.rows.get(0).cells.size(), 3);
         Assert.assertEquals(project.rows.get(0).cells.get(0).value, "data1");
@@ -337,9 +338,9 @@ public class SeparatorBasedImporterTests extends ImporterTest {
         Assert.assertEquals(project.columnModel.columns.get(0).getName(), "col1");
         Assert.assertEquals(project.columnModel.columns.get(1).getName(), "col2");
         Assert.assertEquals(project.columnModel.columns.get(2).getName(), "col3");
-        Assert.assertEquals(project.columnModel.columns.get(3).getName(), "Column 4");
-        Assert.assertEquals(project.columnModel.columns.get(4).getName(), "Column 5");
-        Assert.assertEquals(project.columnModel.columns.get(5).getName(), "Column 6");
+        Assert.assertEquals(project.columnModel.columns.get(3).getName(), OpenRefineMessage.importer_utilities_column() + " 4");
+        Assert.assertEquals(project.columnModel.columns.get(4).getName(), OpenRefineMessage.importer_utilities_column() + " 5");
+        Assert.assertEquals(project.columnModel.columns.get(5).getName(), OpenRefineMessage.importer_utilities_column() + " 6");
         Assert.assertEquals(project.rows.size(), 1);
         Assert.assertEquals(project.rows.get(0).cells.size(), 6);
         Assert.assertEquals(project.rows.get(0).cells.get(0).value, "data1");
diff --git a/main/tests/server/src/com/google/refine/operations/recon/ReconOperationTests.java b/main/tests/server/src/com/google/refine/operations/recon/ReconOperationTests.java
index 2d2e62c53..e21ae400c 100644
--- a/main/tests/server/src/com/google/refine/operations/recon/ReconOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/recon/ReconOperationTests.java
@@ -34,6 +34,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Properties;
 
+import org.apache.commons.text.StringEscapeUtils;
 import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.BeforeSuite;
@@ -41,6 +42,7 @@ import org.testng.annotations.Test;
 
 import com.google.refine.RefineTest;
 import com.google.refine.browsing.EngineConfig;
+import com.google.refine.messages.OpenRefineMessage;
 import com.google.refine.model.Column;
 import com.google.refine.model.Project;
 import com.google.refine.model.Recon;
@@ -82,7 +84,8 @@ public class ReconOperationTests extends RefineTest {
             "           \"columnName\" : \"researcher\",\n" +
             "           \"expression\" : \"forNonBlank(cell.recon.judgment, v, v, if(isNonBlank(value), \\\"(unreconciled)\\\", \\\"(blank)\\\"))\",\n"
             +
-            "           \"name\" : \"researcher: judgment\"\n" +
+            "           \"name\" : \"researcher: " +
+            StringEscapeUtils.escapeJson(OpenRefineMessage.recon_operation_judgement_facet_name()) + "\"\n" +
             "         },\n" +
             "         \"facetOptions\" : {\n" +
             "           \"scroll\" : true\n" +
@@ -94,7 +97,8 @@ public class ReconOperationTests extends RefineTest {
             "           \"columnName\" : \"researcher\",\n" +
             "           \"expression\" : \"cell.recon.best.score\",\n" +
             "           \"mode\" : \"range\",\n" +
-            "           \"name\" : \"researcher: best candidate's score\"\n" +
+            "           \"name\" : \"researcher: " +
+            StringEscapeUtils.escapeJson(OpenRefineMessage.recon_operation_score_facet_name()) + "\"\n" +
             "         },\n" +
             "         \"facetType\" : \"range\"\n" +
             "       } ],\n" +