Skip to content

Commit

Permalink
⭐ new(number): i18n-n functional component (#541) by @bponomarenko
Browse files Browse the repository at this point in the history
  • Loading branch information
bponomarenko authored and kazupon committed Mar 25, 2019
1 parent 1bc3bde commit b33579d
Show file tree
Hide file tree
Showing 14 changed files with 655 additions and 33 deletions.
9 changes: 9 additions & 0 deletions decls/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ declare type DateTimeFormatResult = string;
declare type NumberFormatResult = string;
declare type MissingHandler = (locale: Locale, key: Path, vm?: any) => string | void;

declare type FormattedNumberPartType = 'currency' | 'decimal' | 'fraction' | 'group' | 'infinity' | 'integer' | 'literal' | 'minusSign' | 'nan' | 'plusSign' | 'percentSign';
declare type FormattedNumberPart = {
type: FormattedNumberPartType,
value: string,
};
// This array is the same as Intl.NumberFormat.formatToParts() return value:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat/formatToParts#Return_value
declare type NumberFormatToPartsResult = Array<FormattedNumberPart>;

declare type I18nOptions = {
locale?: Locale,
fallbackLocale?: Locale,
Expand Down
70 changes: 70 additions & 0 deletions examples/number-formatting/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>number custom formatting</title>
<script src="../../node_modules/vue/dist/vue.min.js"></script>
<script src="../../dist/vue-i18n.min.js"></script>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.ja,Intl.~locale.en"></script>
</head>
<body>
<div id="app">
<select v-model="locale">
<option value="en-US">en-US</option>
<option value="ja-JP">ja-JP</option>
</select>
<p>
{{ $t('money') }}:
<i18n-n :value="money" format="currency">
<template slot="decimal" slot-scope="props"><!-- do not render decimal separator --></template>
<span slot="fraction" slot-scope="props" style="vertical-align: super">{{ props.fraction }}</span>
</i18n-n>
</p>
</div>
<script>
var messages = {
'en-US': {
money: 'Money'
},
'ja-JP': {
money: 'お金'
}
}
var numberFormats = {
'en-US': {
currency: {
style: 'currency', currency: 'USD'
}
},
'ja-JP': {
currency: {
style: 'currency', currency: 'JPY', currencyDisplay: 'symbol'
}
}
}

Vue.use(VueI18n)

var initial = 'ja-JP'
var i18n = new VueI18n({
locale: initial,
messages: messages,
numberFormats: numberFormats
})

new Vue({
i18n: i18n,
data: {
locale: initial,
money: 1000
},
watch: {
locale: function (val) {
this.$i18n.locale = val
}
},
el: '#app'
})
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"dist/vue-i18n.min.js",
"dist/vue-i18n.common.js",
"dist/vue-i18n.esm.js",
"src/*.js",
"src/**/*.js",
"types/*.d.ts",
"decls"
],
Expand Down
2 changes: 1 addition & 1 deletion src/component.js → src/components/interpolation.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* @flow */

import { warn } from './util'
import { warn } from '../util'

export default {
name: 'i18n',
Expand Down
63 changes: 63 additions & 0 deletions src/components/number.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* @flow */

import { warn, isObject, numberFormatKeys } from '../util'

export default {
name: 'i18n-n',
functional: true,
props: {
tag: {
type: String,
default: 'span'
},
value: {
type: Number,
required: true
},
format: {
type: [String, Object]
},
locale: {
type: String
}
},
render (h: Function, { props, parent, data }: Object) {
const i18n = parent.$i18n

if (!i18n) {
if (process.env.NODE_ENV !== 'production') {
warn('Cannot find VueI18n instance!')
}
return null
}

let key: ?string = null
let options: ?NumberFormatOptions = null

if (typeof props.format === 'string') {
key = props.format
} else if (isObject(props.format)) {
if (props.format.key) {
key = props.format.key
}

// Filter out number format options only
options = Object.keys(props.format).reduce((acc, prop) => {
if (numberFormatKeys.includes(prop)) {
return Object.assign({}, acc, { [prop]: props.format[prop] })
}
return acc
}, null)
}

const locale: Locale = props.locale || i18n.locale
const parts: NumberFormatToPartsResult = i18n._ntp(props.value, locale, key, options)

const values = parts.map((part, index) => {
const slot: ?Function = data.scopedSlots && data.scopedSlots[part.type]
return slot ? slot({ [part.type]: part.value, index, parts }) : part.value
})

return h(props.tag, values)
}
}
54 changes: 35 additions & 19 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,14 @@ import {
isObject,
looseClone,
remove,
merge
merge,
numberFormatKeys
} from './util'
import BaseFormatter from './format'
import I18nPath from './path'

import type { PathValue } from './path'

const numberFormatKeys = [
'style',
'currency',
'currencyDisplay',
'useGrouping',
'minimumIntegerDigits',
'minimumFractionDigits',
'maximumFractionDigits',
'minimumSignificantDigits',
'maximumSignificantDigits',
'localeMatcher',
'formatMatcher'
]
const linkKeyMatcher = /(?:@(?:\.[a-z]+)?:(?:[\w\-_|.]+|\([\w\-_|.]+\)))/g
const linkKeyPrefixMatcher = /^@(?:\.([a-z]+))?:/
const bracketsMatcher = /[()]/g
Expand Down Expand Up @@ -626,14 +614,14 @@ export default class VueI18n {
this._vm.$set(this._vm.numberFormats, locale, merge(this._vm.numberFormats[locale] || {}, format))
}
_localizeNumber (
_getNumberFormatter (
value: number,
locale: Locale,
fallback: Locale,
numberFormats: NumberFormats,
key: string,
options: ?NumberFormatOptions
): ?NumberFormatResult {
): ?Object {
let _locale: Locale = locale
let formats: NumberFormat = numberFormats[_locale]
Expand Down Expand Up @@ -662,7 +650,7 @@ export default class VueI18n {
formatter = this._numberFormatters[id] = new Intl.NumberFormat(_locale, format)
}
}
return formatter.format(value)
return formatter
}
}
Expand All @@ -680,8 +668,8 @@ export default class VueI18n {
return nf.format(value)
}
const ret: ?NumberFormatResult =
this._localizeNumber(value, locale, this.fallbackLocale, this._getNumberFormats(), key, options)
const formatter: ?Object = this._getNumberFormatter(value, locale, this.fallbackLocale, this._getNumberFormats(), key, options)
const ret: ?NumberFormatResult = formatter && formatter.format(value)
if (this._isFallbackRoot(ret)) {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Fall back to number localization of root: key '${key}' .`)
Expand Down Expand Up @@ -729,6 +717,34 @@ export default class VueI18n {
return this._n(value, locale, key, options)
}
_ntp (value: number, locale: Locale, key: ?string, options: ?NumberFormatOptions): NumberFormatToPartsResult {
/* istanbul ignore if */
if (!VueI18n.availabilities.numberFormat) {
if (process.env.NODE_ENV !== 'production') {
warn('Cannot format to parts a Number value due to not supported Intl.NumberFormat.')
}
return []
}
if (!key) {
const nf = !options ? new Intl.NumberFormat(locale) : new Intl.NumberFormat(locale, options)
return nf.formatToParts(value)
}
const formatter: ?Object = this._getNumberFormatter(value, locale, this.fallbackLocale, this._getNumberFormats(), key, options)
const ret: ?NumberFormatToPartsResult = formatter && formatter.formatToParts(value)
if (this._isFallbackRoot(ret)) {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Fall back to format number to parts of root: key '${key}' .`)
}
/* istanbul ignore if */
if (!this._root) { throw Error('unexpected error') }
return this._root.$i18n._ntp(value, locale, key, options)
} else {
return ret || []
}
}
}

let availabilities: IntlAvailability
Expand Down
6 changes: 4 additions & 2 deletions src/install.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { warn } from './util'
import extend from './extend'
import mixin from './mixin'
import component from './component'
import interpolationComponent from './components/interpolation'
import numberComponent from './components/number'
import { bind, update, unbind } from './directive'

export let Vue
Expand All @@ -26,7 +27,8 @@ export function install (_Vue) {
extend(Vue)
Vue.mixin(mixin)
Vue.directive('t', { bind, update, unbind })
Vue.component(component.name, component)
Vue.component(interpolationComponent.name, interpolationComponent)
Vue.component(numberComponent.name, numberComponent)

// use simple mergeStrategies to prevent i18n instance lose '__proto__'
const strats = Vue.config.optionMergeStrategies
Expand Down
18 changes: 18 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
/* @flow */

/**
* constants
*/

export const numberFormatKeys = [
'style',
'currency',
'currencyDisplay',
'useGrouping',
'minimumIntegerDigits',
'minimumFractionDigits',
'maximumFractionDigits',
'minimumSignificantDigits',
'maximumSignificantDigits',
'localeMatcher',
'formatMatcher'
]

/**
* utilities
*/
Expand Down
11 changes: 11 additions & 0 deletions test/e2e/test/number_formatting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
component: function (browser) {
browser
.url('http://localhost:8080/examples/number-formatting/')
.waitForElementVisible('#app', 1000)
.assert.containsText('p', 'お金: ¥1,000')
.click('select option[value=en-US]')
.assert.attributeContains('p', 'innerHTML', 'Money: $1,000<span style="vertical-align: super">00</span>')
.end()
}
}
2 changes: 1 addition & 1 deletion test/unit/interpolation.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Component from '../../src/component'
import Component from '../../src/components/interpolation'

const messages = {
en: {
Expand Down
Loading

0 comments on commit b33579d

Please sign in to comment.