diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index badbb374104..7fc51ce2184 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -971,6 +971,7 @@ function createHoverText(hoverData, opts, gd) {
         }
 
         // hovertemplate
+        var d3locale = gd._fullLayout._d3locale;
         var hovertemplate = d.hovertemplate || false;
         var hovertemplateLabels = d.hovertemplateLabels || d;
         var eventData = d.eventData[0] || {};
@@ -978,6 +979,7 @@ function createHoverText(hoverData, opts, gd) {
             text = Lib.hovertemplateString(
                 hovertemplate,
                 hovertemplateLabels,
+                d3locale,
                 eventData,
                 {meta: fullLayout.meta}
             );
diff --git a/src/lib/index.js b/src/lib/index.js
index 1c0c0eb7900..be6d6707344 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -1031,17 +1031,18 @@ var maximumNumberOfHoverTemplateWarnings = 10;
  * or fallback to associated labels.
  *
  * Examples:
- *  Lib.templateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
- *  Lib.templateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
- *  Lib.templateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'
+ *  Lib.hovertemplateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
+ *  Lib.hovertemplateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
+ *  Lib.hovertemplateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'
  *
+ * @param {obj}     d3 locale
  * @param {string}  input string containing %{...:...} template strings
  * @param {obj}     data object containing fallback text when no formatting is specified, ex.: {yLabel: 'formattedYValue'}
  * @param {obj}     data objects containing substitution values
  *
  * @return {string} templated string
  */
-lib.hovertemplateString = function(string, labels) {
+lib.hovertemplateString = function(string, labels, d3locale) {
     var args = arguments;
     // Not all that useful, but cache nestedProperty instantiation
     // just in case it speeds things up *slightly*:
@@ -1049,7 +1050,7 @@ lib.hovertemplateString = function(string, labels) {
 
     return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, key, format) {
         var obj, value, i;
-        for(i = 2; i < args.length; i++) {
+        for(i = 3; i < args.length; i++) {
             obj = args[i];
             if(obj.hasOwnProperty(key)) {
                 value = obj[key];
@@ -1076,7 +1077,13 @@ lib.hovertemplateString = function(string, labels) {
         }
 
         if(format) {
-            value = d3.format(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
+            var fmt;
+            if(d3locale) {
+                fmt = d3locale.numberFormat;
+            } else {
+                fmt = d3.format;
+            }
+            value = fmt(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
         } else {
             if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label'];
         }
diff --git a/test/image/mocks/sankey_link_concentration.json b/test/image/mocks/sankey_link_concentration.json
index 789380539d0..a4b4564fff0 100644
--- a/test/image/mocks/sankey_link_concentration.json
+++ b/test/image/mocks/sankey_link_concentration.json
@@ -63,7 +63,7 @@
                   }
                 ],
 
-                "hovertemplate": "%{label}
flow.labelConcentration: %{flow.labelConcentration:%0.2f}
flow.concentration: %{flow.concentration:%0.2f}
flow.value: %{flow.value}"
+                "hovertemplate": "%{label}
flow.labelConcentration: %{flow.labelConcentration:0.2%}
flow.concentration: %{flow.concentration:0.2%}
flow.value: %{flow.value}"
             }
 
         }],
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index c8178a78fe2..4eabcc61a9f 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -1688,12 +1688,18 @@ describe('hover info', function() {
     });
 
     describe('hovertemplate', function() {
-        var mockCopy = Lib.extendDeep({}, mock);
+        var mockCopy;
 
         beforeEach(function(done) {
+            mockCopy = Lib.extendDeep({}, mock);
             Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done);
         });
 
+        afterEach(function() {
+            Plotly.purge('graph');
+            destroyGraphDiv();
+        });
+
         it('should format labels according to a template string', function(done) {
             var gd = document.getElementById('graph');
             Plotly.restyle(gd, 'hovertemplate', '%{y:$.2f}trace 0')
@@ -1717,6 +1723,45 @@ describe('hover info', function() {
             .then(done);
         });
 
+        it('should format labels according to a template string and locale', function(done) {
+            var gd = document.getElementById('graph');
+            mockCopy.layout.separators = undefined;
+            Plotly.newPlot(gd, mockCopy.data, mockCopy.layout, {
+                locale: 'fr-eu',
+                locales: {
+                    'fr-eu': {
+                        format: {
+                            currency: ['€', ''],
+                            decimal: ',',
+                            thousands: ' ',
+                            grouping: [3]
+                        }
+                    }
+                }
+            })
+            .then(function() {
+                Plotly.restyle(gd, 'hovertemplate', '%{y:$010,.2f}trace 0');
+            })
+            .then(function() {
+                Fx.hover('graph', evt, 'xy');
+
+                var hoverTrace = gd._hoverdata[0];
+
+                expect(hoverTrace.curveNumber).toEqual(0);
+                expect(hoverTrace.pointNumber).toEqual(17);
+                expect(hoverTrace.x).toEqual(0.388);
+                expect(hoverTrace.y).toEqual(1);
+
+                assertHoverLabelContent({
+                    nums: '€000 001,00',
+                    name: 'trace 0',
+                    axis: '0,388'
+                });
+            })
+            .catch(failTest)
+            .then(done);
+        });
+
         it('should format secondary label with extra tag', function(done) {
             var gd = document.getElementById('graph');
             Plotly.restyle(gd, 'hovertemplate', 'trace 20 %{y:$.2f}')
diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js
index 00351710619..25b9e0e1278 100644
--- a/test/jasmine/tests/lib_test.js
+++ b/test/jasmine/tests/lib_test.js
@@ -2181,28 +2181,29 @@ describe('Test lib.js:', function() {
     });
 
     describe('hovertemplateString', function() {
+        var locale = false;
         it('evaluates attributes', function() {
-            expect(Lib.hovertemplateString('foo %{bar}', {}, {bar: 'baz'})).toEqual('foo baz');
+            expect(Lib.hovertemplateString('foo %{bar}', {}, locale, {bar: 'baz'})).toEqual('foo baz');
         });
 
         it('evaluates attributes with a dot in their name', function() {
-            expect(Lib.hovertemplateString('%{marker.size}', {}, {'marker.size': 12}, {marker: {size: 14}})).toEqual('12');
+            expect(Lib.hovertemplateString('%{marker.size}', {}, locale, {'marker.size': 12}, {marker: {size: 14}})).toEqual('12');
         });
 
         it('evaluates nested properties', function() {
-            expect(Lib.hovertemplateString('foo %{bar.baz}', {}, {bar: {baz: 'asdf'}})).toEqual('foo asdf');
+            expect(Lib.hovertemplateString('foo %{bar.baz}', {}, locale, {bar: {baz: 'asdf'}})).toEqual('foo asdf');
         });
 
         it('evaluates array nested properties', function() {
-            expect(Lib.hovertemplateString('foo %{bar[0].baz}', {}, {bar: [{baz: 'asdf'}]})).toEqual('foo asdf');
+            expect(Lib.hovertemplateString('foo %{bar[0].baz}', {}, locale, {bar: [{baz: 'asdf'}]})).toEqual('foo asdf');
         });
 
         it('subtitutes multiple matches', function() {
-            expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, {group: 'asdf', trace: 'jkl;'})).toEqual('foo asdf jkl;');
+            expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, locale, {group: 'asdf', trace: 'jkl;'})).toEqual('foo asdf jkl;');
         });
 
         it('replaces missing matches with template string', function() {
-            expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, {group: 1})).toEqual('foo 1 %{trace}');
+            expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, locale, {group: 1})).toEqual('foo 1 %{trace}');
         });
 
         it('uses the value from the first object with the specified key', function() {
@@ -2210,23 +2211,24 @@ describe('Test lib.js:', function() {
             var obj2 = {a: 'second', foo: {bar: 'bar'}};
 
             // Simple key
-            expect(Lib.hovertemplateString('foo %{a}', {}, obj1, obj2)).toEqual('foo first');
-            expect(Lib.hovertemplateString('foo %{a}', {}, obj2, obj1)).toEqual('foo second');
+            expect(Lib.hovertemplateString('foo %{a}', {}, locale, obj1, obj2)).toEqual('foo first');
+            expect(Lib.hovertemplateString('foo %{a}', {}, locale, obj2, obj1)).toEqual('foo second');
 
             // Nested Keys
-            expect(Lib.hovertemplateString('foo %{foo.bar}', {}, obj1, obj2)).toEqual('foo bar');
+            expect(Lib.hovertemplateString('foo %{foo.bar}', {}, locale, obj1, obj2)).toEqual('foo bar');
 
             // Nested keys with 0
-            expect(Lib.hovertemplateString('y: %{y}', {}, {y: 0}, {y: 1})).toEqual('y: 0');
+            expect(Lib.hovertemplateString('y: %{y}', {}, locale, {y: 0}, {y: 1})).toEqual('y: 0');
         });
 
         it('formats value using d3 mini-language', function() {
-            expect(Lib.hovertemplateString('a: %{a:.0%}', {}, {a: 0.123})).toEqual('a: 12%');
-            expect(Lib.hovertemplateString('b: %{b:2.2f}', {}, {b: 43})).toEqual('b: 43.00');
+            expect(Lib.hovertemplateString('a: %{a:.0%}', {}, locale, {a: 0.123})).toEqual('a: 12%');
+            expect(Lib.hovertemplateString('a: %{a:0.2%}', {}, locale, {a: 0.123})).toEqual('a: 12.30%');
+            expect(Lib.hovertemplateString('b: %{b:2.2f}', {}, locale, {b: 43})).toEqual('b: 43.00');
         });
 
         it('looks for default label if no format is provided', function() {
-            expect(Lib.hovertemplateString('y: %{y}', {yLabel: '0.1'}, {y: 0.123})).toEqual('y: 0.1');
+            expect(Lib.hovertemplateString('y: %{y}', {yLabel: '0.1'}, locale, {y: 0.123})).toEqual('y: 0.1');
         });
 
         it('warns user up to 10 times if a variable cannot be found', function() {