From 8570d35e5430172ec8a115ed3c818c234b8ce536 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Fri, 21 Oct 2016 12:09:27 -0400 Subject: [PATCH 1/3] Use Intl.NumberFormat to format numbers --- README.md | 19 +++++++++++++++++++ index.js | 28 ++++++++++++++++++++++++---- package.json | 3 ++- test/index.js | 17 ++++++++++++++++- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c2d6eac..eeacbe1 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,25 @@ polyglot.t("car", 2); => "2 cars" ``` +Interpolated `Number`s will be number-formatted according to the `locale`: + +```js +polyglot.t("num_cars", 2000); +=> "2,000 cars" +``` + +On a default Node install, this may only work in English. To format in +non-English locales (e.g., to output "2.000" in France or use other numerals), +compile Node with "full" ICU data or include the `full-icu` package in your +project: + +1. `npm install --save full-icu` +2. Run `node --full-data-dir=node_modules/full-icu` instead of just `node`, or + set the `NODE_ICU_DATA=node_modules/full-icu` environment variable. + +If you're running Polyglot within a browser, it can number-format in any +locale the web browser supports. + If you like, you can provide a default value in case the phrase is missing. Use the special option key "_" to specify a default. diff --git a/index.js b/index.js index 84e53a8..ea7e5c5 100644 --- a/index.js +++ b/index.js @@ -119,7 +119,7 @@ var tokenRegex = /%\{(.*?)\}/g; // // You should pass in a third argument, the locale, to specify the correct plural type. // It defaults to `'en'` with 2 plural forms. -function transformPhrase(phrase, substitutions, locale) { +function transformPhrase(phrase, substitutions, locale, numberFormat) { if (typeof phrase !== 'string') { throw new TypeError('Polyglot.transformPhrase expects argument #1 to be string'); } @@ -144,8 +144,14 @@ function transformPhrase(phrase, substitutions, locale) { // Interpolate: Creates a `RegExp` object for each interpolation placeholder. result = result.replace(tokenRegex, function (expression, argument) { if (!has(options, argument)) { return ''; } + + var replacement = options[argument]; + if (typeof replacement === 'number') { + replacement = numberFormat.format(replacement); + } + // Ensure replacement value is escaped to prevent special $-prefixed regex replace tokens. - return replace.call(options[argument], dollarRegex, dollarBillsYall); + return replace.call(replacement, dollarRegex, dollarBillsYall); }); return result; @@ -157,6 +163,12 @@ function Polyglot(options) { this.phrases = {}; this.extend(opts.phrases || {}); this.currentLocale = opts.locale || 'en'; + if (typeof Intl === 'object') { + this.numberFormat = new Intl.NumberFormat(this.currentLocale); + } else { + // Fallback for IE<11 + this.numberFormat = { format: function (n) { return String(n); } }; + } this.allowMissing = !!opts.allowMissing; this.warn = opts.warn || warn; } @@ -165,7 +177,15 @@ function Polyglot(options) { // // Get or set locale. Internally, Polyglot only uses locale for pluralization. Polyglot.prototype.locale = function (newLocale) { - if (newLocale) this.currentLocale = newLocale; + if (newLocale) { + this.currentLocale = newLocale; + if (typeof Intl === 'object') { + this.numberFormat = new Intl.NumberFormat(this.currentLocale); + } else { + // Fallback for IE<11 + this.numberFormat = { format: function (n) { return String(n); } }; + } + } return this.currentLocale; }; @@ -314,7 +334,7 @@ Polyglot.prototype.t = function (key, options) { result = key; } if (typeof phrase === 'string') { - result = transformPhrase(phrase, opts, this.currentLocale); + result = transformPhrase(phrase, opts, this.currentLocale, this.numberFormat); } return result; }; diff --git a/package.json b/package.json index 75a7b7f..12f504b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "pretest": "npm run --silent lint", "test": "npm run --silent tests-only", - "tests-only": "mocha test/*.js --reporter spec", + "tests-only": "NODE_ICU_DATA=node_modules/full-icu mocha test/*.js --reporter spec", "lint": "eslint *.js test/*.js", "docs": "docco -o docs/ index.js" }, @@ -36,6 +36,7 @@ "eslint": "^3.9.1", "eslint-config-airbnb-base": "^10.0.1", "eslint-plugin-import": "^2.2.0", + "full-icu": "^1.0.3", "mocha": "^3.1.2", "should": "^11.1.1", "uglify-js": "2.7.3" diff --git a/test/index.js b/test/index.js index 055ff1e..c51b12a 100644 --- a/test/index.js +++ b/test/index.js @@ -8,7 +8,8 @@ describe('t', function () { hello: 'Hello', hi_name_welcome_to_place: 'Hi, %{name}, welcome to %{place}!', name_your_name_is_name: '%{name}, your name is %{name}!', - empty_string: '' + empty_string: '', + number: '%{number}' }; var polyglot; @@ -89,6 +90,14 @@ describe('t', function () { expect(instance.t('nav.cta.join_now')).to.equal('Join now!'); expect(instance.t('header.sign_in')).to.equal('Sign In'); }); + + it('uses an Intl.NumberFormat', function () { + var en = new Polyglot({ phrases: phrases, locale: 'en' }); + var fr = new Polyglot({ phrases: phrases, locale: 'fr' }); + + expect(en.t('number', { number: 1234.56 })).to.equal('1,234.56'); + expect(fr.t('number', { number: 1234.56 })).to.equal('1 234,56'); + }); }); describe('pluralize', function () { @@ -165,6 +174,12 @@ describe('locale', function () { polyglot.locale('fr'); expect(polyglot.locale()).to.equal('fr'); }); + + it('updates number format when setting locale', function () { + polyglot.locale('fr'); + polyglot.extend({ x: '%{n}' }); + expect(polyglot.t('x', { n: 1234.56 })).to.equal('1 234,56'); + }); }); describe('extend', function () { From 71b562a11b8ae91fe885a7e91395d14fd52a06b8 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Sat, 22 Oct 2016 21:51:08 -0400 Subject: [PATCH 2/3] Use dependency injection instead of Intl --- README.md | 27 ++++++++++++++------------- index.js | 13 +------------ package.json | 1 - test/index.js | 16 ++++++---------- 4 files changed, 21 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index eeacbe1..6b4134c 100644 --- a/README.md +++ b/README.md @@ -243,24 +243,25 @@ polyglot.t("car", 2); => "2 cars" ``` -Interpolated `Number`s will be number-formatted according to the `locale`: +If you pass a `numberFormat` to the constructor, interpolated `Number`s will +be formatted by its `format()` method. That's useful because different locales +have different rules for formatting numbers: `2,000.56` in English versus +`1 234,56` in French, for instance. ```js -polyglot.t("num_cars", 2000); +polyglot = new Polyglot({ + phrases: { num_cars: '%{smart_count} car |||| %{smart_count} cars' }, + numberFormat: new Intl.NumberFormat('en') // Chrome, Firefox, IE11+, Node 0.12+ with ICU +}) +polyglot.t("num_cars", 2000); // internally, calls options.numberFormat.format(2000) => "2,000 cars" ``` -On a default Node install, this may only work in English. To format in -non-English locales (e.g., to output "2.000" in France or use other numerals), -compile Node with "full" ICU data or include the `full-icu` package in your -project: - -1. `npm install --save full-icu` -2. Run `node --full-data-dir=node_modules/full-icu` instead of just `node`, or - set the `NODE_ICU_DATA=node_modules/full-icu` environment variable. - -If you're running Polyglot within a browser, it can number-format in any -locale the web browser supports. +(A primer on [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat) +in Node: Node 0.12+ comes with Intl as long as it's compiled with ICU (which is +the default). By default, the only locale Node supports is en-US. You can add +[full-icu](https://www.npmjs.com/package/full-icu) to your project to support +other locales. If you like, you can provide a default value in case the phrase is missing. Use the special option key "_" to specify a default. diff --git a/index.js b/index.js index ea7e5c5..61702cf 100644 --- a/index.js +++ b/index.js @@ -163,12 +163,7 @@ function Polyglot(options) { this.phrases = {}; this.extend(opts.phrases || {}); this.currentLocale = opts.locale || 'en'; - if (typeof Intl === 'object') { - this.numberFormat = new Intl.NumberFormat(this.currentLocale); - } else { - // Fallback for IE<11 - this.numberFormat = { format: function (n) { return String(n); } }; - } + this.numberFormat = opts.numberFormat || { format: String }; this.allowMissing = !!opts.allowMissing; this.warn = opts.warn || warn; } @@ -179,12 +174,6 @@ function Polyglot(options) { Polyglot.prototype.locale = function (newLocale) { if (newLocale) { this.currentLocale = newLocale; - if (typeof Intl === 'object') { - this.numberFormat = new Intl.NumberFormat(this.currentLocale); - } else { - // Fallback for IE<11 - this.numberFormat = { format: function (n) { return String(n); } }; - } } return this.currentLocale; }; diff --git a/package.json b/package.json index 12f504b..7f469d7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "eslint": "^3.9.1", "eslint-config-airbnb-base": "^10.0.1", "eslint-plugin-import": "^2.2.0", - "full-icu": "^1.0.3", "mocha": "^3.1.2", "should": "^11.1.1", "uglify-js": "2.7.3" diff --git a/test/index.js b/test/index.js index c51b12a..151b66c 100644 --- a/test/index.js +++ b/test/index.js @@ -92,11 +92,13 @@ describe('t', function () { }); it('uses an Intl.NumberFormat', function () { - var en = new Polyglot({ phrases: phrases, locale: 'en' }); - var fr = new Polyglot({ phrases: phrases, locale: 'fr' }); + var instance = new Polyglot({ + phrases: phrases, + // prove we're passed a Number by doing math on it and formatting it + numberFormat: { format: function (n) { return 'x' + (n + 2); } } + }); - expect(en.t('number', { number: 1234.56 })).to.equal('1,234.56'); - expect(fr.t('number', { number: 1234.56 })).to.equal('1 234,56'); + expect(instance.t('number', { number: 1234.56 })).to.equal('x1236.56'); }); }); @@ -174,12 +176,6 @@ describe('locale', function () { polyglot.locale('fr'); expect(polyglot.locale()).to.equal('fr'); }); - - it('updates number format when setting locale', function () { - polyglot.locale('fr'); - polyglot.extend({ x: '%{n}' }); - expect(polyglot.t('x', { n: 1234.56 })).to.equal('1 234,56'); - }); }); describe('extend', function () { From e54353b0b427c2a80ca868487af7c78c7107b18d Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Mon, 24 Oct 2016 16:59:24 -0400 Subject: [PATCH 3/3] Use a Function, not a NumberFormat --- README.md | 14 ++++++++------ index.js | 4 ++-- package.json | 2 +- test/index.js | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6b4134c..51c8904 100644 --- a/README.md +++ b/README.md @@ -243,15 +243,15 @@ polyglot.t("car", 2); => "2 cars" ``` -If you pass a `numberFormat` to the constructor, interpolated `Number`s will -be formatted by its `format()` method. That's useful because different locales -have different rules for formatting numbers: `2,000.56` in English versus -`1 234,56` in French, for instance. +If you pass a `numberFormat` _function_ to the constructor, Polyglot will use +it to translate interpolated `Number`s to `String`s. That's useful because +different locales have different rules for formatting numbers: `2,000.56` in +English versus `1 234,56` in French, for instance. ```js polyglot = new Polyglot({ phrases: { num_cars: '%{smart_count} car |||| %{smart_count} cars' }, - numberFormat: new Intl.NumberFormat('en') // Chrome, Firefox, IE11+, Node 0.12+ with ICU + numberFormat: new Intl.NumberFormat('en').format // Chrome, Firefox, IE11+, Node 0.12+ with ICU }) polyglot.t("num_cars", 2000); // internally, calls options.numberFormat.format(2000) => "2,000 cars" @@ -261,7 +261,9 @@ polyglot.t("num_cars", 2000); // internally, calls options.numberFormat.format(2 in Node: Node 0.12+ comes with Intl as long as it's compiled with ICU (which is the default). By default, the only locale Node supports is en-US. You can add [full-icu](https://www.npmjs.com/package/full-icu) to your project to support -other locales. +other locales. Finally, Polyglot accepts a _function_, not an Intl.NumberFormat +instance: if you have a NumberFormat instance, pass its `.format` property to +Polyglot. If you like, you can provide a default value in case the phrase is missing. Use the special option key "_" to specify a default. diff --git a/index.js b/index.js index 61702cf..576eee9 100644 --- a/index.js +++ b/index.js @@ -147,7 +147,7 @@ function transformPhrase(phrase, substitutions, locale, numberFormat) { var replacement = options[argument]; if (typeof replacement === 'number') { - replacement = numberFormat.format(replacement); + replacement = numberFormat(replacement); } // Ensure replacement value is escaped to prevent special $-prefixed regex replace tokens. @@ -163,7 +163,7 @@ function Polyglot(options) { this.phrases = {}; this.extend(opts.phrases || {}); this.currentLocale = opts.locale || 'en'; - this.numberFormat = opts.numberFormat || { format: String }; + this.numberFormat = opts.numberFormat || String; this.allowMissing = !!opts.allowMissing; this.warn = opts.warn || warn; } diff --git a/package.json b/package.json index 7f469d7..75a7b7f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "pretest": "npm run --silent lint", "test": "npm run --silent tests-only", - "tests-only": "NODE_ICU_DATA=node_modules/full-icu mocha test/*.js --reporter spec", + "tests-only": "mocha test/*.js --reporter spec", "lint": "eslint *.js test/*.js", "docs": "docco -o docs/ index.js" }, diff --git a/test/index.js b/test/index.js index 151b66c..ca9bf96 100644 --- a/test/index.js +++ b/test/index.js @@ -91,11 +91,11 @@ describe('t', function () { expect(instance.t('header.sign_in')).to.equal('Sign In'); }); - it('uses an Intl.NumberFormat', function () { + it('uses numberFormat', function () { var instance = new Polyglot({ phrases: phrases, // prove we're passed a Number by doing math on it and formatting it - numberFormat: { format: function (n) { return 'x' + (n + 2); } } + numberFormat: function (n) { return 'x' + (n + 2); } }); expect(instance.t('number', { number: 1234.56 })).to.equal('x1236.56');