diff --git a/README.md b/README.md index 6ff062b18..2a4085ee7 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,18 @@ Checkout the list of possible variables in the [storybook colors story](https:// A few examples of a theme can be found in the [src/styles/themes/](src/styles/themes/) folder. +## I18n + +Some components, e.g. `ec-amount-input` or `ec-donut` require `Intl` API to format values properly or to detect +what is the decimal/grouping separator for a current locale. They both do that via [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) +which might have issues in some browsers for not having all locales set up properly. See the issues we discovered in this [PR](https://github.com/Ebury/chameleon/pull/156#issuecomment-623705733). +If you need to support every single locale on the planet, we recommend to polyfill the Intl API using [intl](https://www.npmjs.com/package/intl) package +so it's consistent across all browsers. + +```html + +``` + ### CSS variables polyfill If you support **IE11** browser, you have to include the [CSS vars ponyfill](https://jhildenbiddle.github.io/css-vars-ponyfill/#/) when using our components. diff --git a/package-lock.json b/package-lock.json index b545024ad..d65c878b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@ebury/chameleon-components", - "version": "0.1.86", + "version": "0.1.87", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 120c6aef4..b123337bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ebury/chameleon-components", - "version": "0.1.86", + "version": "0.1.87", "main": "src/main.js", "sideEffects": false, "author": "Ebury Team (http://labs.ebury.rocks/)", diff --git a/src/components/ec-amount-input/ec-amount-input.spec.js b/src/components/ec-amount-input/ec-amount-input.spec.js index 4d2f402a2..30fa68a44 100644 --- a/src/components/ec-amount-input/ec-amount-input.spec.js +++ b/src/components/ec-amount-input/ec-amount-input.spec.js @@ -212,4 +212,3 @@ describe('EcAmountInput', () => { expect(wrapper.vm.valueAmount).toBe(null); }); }); - diff --git a/src/components/ec-amount-input/ec-amount-input.story.js b/src/components/ec-amount-input/ec-amount-input.story.js index 93386e060..47acd42ea 100644 --- a/src/components/ec-amount-input/ec-amount-input.story.js +++ b/src/components/ec-amount-input/ec-amount-input.story.js @@ -18,7 +18,7 @@ stories default: boolean('Is Masked', false), }, locale: { - default: select('Locale', ['en', 'es', 'de-ch', 'jp'], 'en'), + default: select('Locale', ['en', 'es', 'de-ch', 'jp', 'sv'], 'en'), }, currency: { default: select('Currency', ['GBP', 'EUR', 'JPY', 'INR', 'USD', 'CAD'], 'GBP'), diff --git a/src/components/ec-amount-input/ec-amount-input.vue b/src/components/ec-amount-input/ec-amount-input.vue index ff92b940a..d35745a22 100644 --- a/src/components/ec-amount-input/ec-amount-input.vue +++ b/src/components/ec-amount-input/ec-amount-input.vue @@ -18,6 +18,7 @@ import EcInputField from '../ec-input-field'; import EcAmount from '../../directives/ec-amount/ec-amount'; import { format, unFormat } from '../../directives/ec-amount/utils'; +import { getDecimalSeparator, getGroupingSeparator } from '../../utils/number-format'; export default { components: { EcInputField }, @@ -61,12 +62,6 @@ export default { const options = new Intl.NumberFormat(this.locale, { style: 'currency', currency: this.currency || 'XYZ' }).resolvedOptions(); return options.maximumFractionDigits; }, - groupingSeparator() { - return this.getSeparator('group'); - }, - decimalSeparator() { - return this.getSeparator('decimal'); - }, }, watch: { value: { @@ -78,7 +73,7 @@ export default { } this.formattedValue = newValue; - this.unformattedValue = +(unFormat(newValue, this.groupingSeparator, this.decimalSeparator)); + this.unformattedValue = +(unFormat(newValue, this.getGroupingSeparator(), this.getDecimalSeparator())); } else { if (newValue === this.unformattedValue) { return; @@ -102,7 +97,7 @@ export default { }, formattedValue(newValue) { if (newValue) { - this.unformattedValue = +(unFormat(newValue, this.groupingSeparator, this.decimalSeparator)); + this.unformattedValue = +(unFormat(newValue, this.getGroupingSeparator(), this.getDecimalSeparator())); } else { this.unformattedValue = null; } @@ -113,20 +108,19 @@ export default { }, }, methods: { - getSeparator(type) { - const numberWithDecimalSeparator = 11111.1; - return new Intl.NumberFormat(this.locale) - .formatToParts(numberWithDecimalSeparator) - .find(part => part.type === type) - .value; - }, getFormattingOptions() { return { precision: this.precision, - decimalSeparator: this.decimalSeparator, - groupingSeparator: this.groupingSeparator, + decimalSeparator: this.getDecimalSeparator(), + groupingSeparator: this.getGroupingSeparator(), }; }, + getGroupingSeparator() { + return getGroupingSeparator(this.locale); + }, + getDecimalSeparator() { + return getDecimalSeparator(this.locale); + }, }, }; diff --git a/src/components/ec-currency-input/ec-currency-input.story.js b/src/components/ec-currency-input/ec-currency-input.story.js index ac4325ed2..1fbcf55d0 100644 --- a/src/components/ec-currency-input/ec-currency-input.story.js +++ b/src/components/ec-currency-input/ec-currency-input.story.js @@ -24,7 +24,7 @@ stories default: text('note', 'Select currency and set amount'), }, locale: { - default: select('locale', ['en', 'es', 'de-ch', 'jp'], 'en'), + default: select('locale', ['en', 'es', 'de-ch', 'jp', 'sv'], 'en'), }, currenciesAreLoading: { default: boolean('currencies are loading', false), diff --git a/src/utils/number-format.js b/src/utils/number-format.js new file mode 100644 index 000000000..85545a963 --- /dev/null +++ b/src/utils/number-format.js @@ -0,0 +1,46 @@ +export function getDecimalSeparator(locale) { + // we could just use formatToParts function but it's not supported on IE11 + // and there's no polyfill for NumberFormat, only for DateTimeFormat + // + // return new Intl.NumberFormat(locale) + // .formatToParts(1111.1) + // .find(part => part.type === 'decimal') + // .value; + // + // so we have to improvised and force the formatter to format number 0.1 in the format + // 01 and then get the 2nd character from the string + + const formatted = new Intl.NumberFormat(locale, { + useGrouping: false, + minimumIntegerDigits: 1, + minimumFractionDigits: 1, + maximumFractionDigits: 1, + minimumSignificantDigits: 1, + maximumSignificantDigits: 1, + }).format(0.1); + return formatted[1]; +} + +export function getGroupingSeparator(locale) { + // we could just use formatToParts function but it's not supported on IE11 + // and there's no polyfill for NumberFormat, only for DateTimeFormat + // + // return new Intl.NumberFormat(locale) + // .formatToParts(1111.1) + // .find(part => part.type === 'group') + // .value; + // + // so we have to improvised and force the formatter to format number 1000 in the format + // 1000 and then get the 2nd character from the string + + const formatted = new Intl.NumberFormat(locale, { + useGrouping: true, + minimumIntegerDigits: 5, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + minimumSignificantDigits: 5, + maximumSignificantDigits: 5, + }).format(10000); + return formatted[2]; +} + diff --git a/src/utils/number-format.spec.js b/src/utils/number-format.spec.js new file mode 100644 index 000000000..1bddcc443 --- /dev/null +++ b/src/utils/number-format.spec.js @@ -0,0 +1,38 @@ +import fs from 'fs'; +import { getDecimalSeparator, getGroupingSeparator } from './number-format'; + +describe('Utils', () => { + // get list of all locales in the world from intl polyfill. + const locales = fs.readdirSync('./node_modules/intl/locale-data/jsonp') + .filter(fileName => fileName.match(/\.js$/)) + .map(fileName => fileName.replace(/\.js$/, '')); + + function getExpectedSeparator(locale, type) { + return new Intl.NumberFormat(locale) + .formatToParts(1111.1) + .find(part => part.type === type) + .value; + } + + describe('getDecimalSeparator ', () => { + for (const locale of locales) { + it(`should get decimal separator for locale ${locale}`, () => { + const expected = getExpectedSeparator(locale, 'decimal'); // get the decimal separator using method that works in every modern browser + const separator = getDecimalSeparator(locale); // get the decimal separator using our method + expect(separator.length).toBe(1); + expect(separator).toBe(expected); + }); + } + }); + + describe('getGroupingSeparator', () => { + for (const locale of locales) { + it(`should get group separator for locale ${locale}`, () => { + const expected = getExpectedSeparator(locale, 'group'); // get the group separator using method that works in every modern browser + const separator = getGroupingSeparator(locale); // get the group separator using our method + expect(separator.length).toBe(1); + expect(separator).toBe(expected); + }); + } + }); +});