Skip to content

Commit b640c13

Browse files
authored
number format component: i18n-n (#9)
* feat: format option overriding * feat: number format component
1 parent ed1b8bc commit b640c13

File tree

8 files changed

+322
-14
lines changed

8 files changed

+322
-14
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
;['composable', 'legacy'].forEach(pattern => {
2+
describe(`${pattern}`, () => {
3+
beforeAll(async () => {
4+
await page.goto(
5+
`http://localhost:8080/examples/${pattern}/components/number-format.html`
6+
)
7+
})
8+
9+
test('rendering', async () => {
10+
await expect(page).toMatch('100')
11+
await expect(page).toMatch('$100.00')
12+
await expect(page).toMatch('¥100')
13+
await expect(page).toMatch('€1,234.00')
14+
})
15+
})
16+
})
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>NumberFormat component examples</title>
6+
<script src="../../../node_modules/vue/dist/vue.global.js"></script>
7+
<script src="../../../dist/vue-i18n.global.js"></script>
8+
</head>
9+
<body>
10+
<h1>NumberFormat component examples</h1>
11+
12+
<div id="app">
13+
<h2>basic usages:</h2>
14+
<i18n-n tag="p" :value="100"></i18n-n>
15+
<i18n-n tag="p" :value="100" format="currency"></i18n-n>
16+
<i18n-n tag="p" :value="100" format="currency" locale="ja-JP"></i18n-n>
17+
18+
<h2>slot usages:</h2>
19+
<i18n-n :value="1234" :format="{ key: 'currency', currency: 'EUR' }">
20+
<template #currency="props"><span style="color: green">{{ props.currency }}</span></template>
21+
<template #integer="props"><span style="font-weight: bold">{{ props.integer }}</span></template>
22+
<template #group="props"><span style="font-weight: bold">{{ props.group }}</span></template>
23+
<template #fraction="props"><span style="font-size: small">{{ props.fraction }}</span></template>
24+
</i18n-n>
25+
</div>
26+
<script>
27+
const { createApp } = Vue
28+
const { createI18n, useI18n } = VueI18n
29+
30+
const i18n = createI18n({
31+
locale: 'en-US',
32+
numberFormats: {
33+
'en-US': {
34+
currency: {
35+
style: 'currency',
36+
currency: 'USD',
37+
currencyDisplay: 'symbol'
38+
},
39+
decimal: {
40+
style: 'decimal',
41+
useGrouping: false
42+
}
43+
},
44+
'ja-JP': {
45+
currency: {
46+
style: 'currency',
47+
currency: 'JPY', currencyDisplay: 'symbol'
48+
},
49+
numeric: {
50+
style: 'decimal',
51+
useGrouping: false
52+
},
53+
percent: {
54+
style: 'percent',
55+
useGrouping: false
56+
}
57+
}
58+
}
59+
})
60+
61+
const app = createApp({
62+
setup() {
63+
return useI18n()
64+
}
65+
})
66+
app.use(i18n)
67+
app.mount('#app')
68+
</script>
69+
</body>
70+
</html>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>NumberFormat component examples</title>
6+
<script src="../../../node_modules/vue/dist/vue.global.js"></script>
7+
<script src="../../../dist/vue-i18n.global.js"></script>
8+
</head>
9+
<body>
10+
<h1>NumberFormat component examples</h1>
11+
12+
<div id="app">
13+
<h2>basic usages:</h2>
14+
<i18n-n tag="p" :value="100"></i18n-n>
15+
<i18n-n tag="p" :value="100" format="currency"></i18n-n>
16+
<i18n-n tag="p" :value="100" format="currency" locale="ja-JP"></i18n-n>
17+
18+
<h2>slot usages:</h2>
19+
<i18n-n :value="1234" :format="{ key: 'currency', currency: 'EUR' }">
20+
<template #currency="props"><span style="color: green">{{ props.currency }}</span></template>
21+
<template #integer="props"><span style="font-weight: bold">{{ props.integer }}</span></template>
22+
<template #group="props"><span style="font-weight: bold">{{ props.group }}</span></template>
23+
<template #fraction="props"><span style="font-size: small">{{ props.fraction }}</span></template>
24+
</i18n-n>
25+
</div>
26+
<script>
27+
const { createApp } = Vue
28+
const { createI18n } = VueI18n
29+
30+
const i18n = createI18n({
31+
legacy: true,
32+
locale: 'en-US',
33+
numberFormats: {
34+
'en-US': {
35+
currency: {
36+
style: 'currency',
37+
currency: 'USD',
38+
currencyDisplay: 'symbol'
39+
},
40+
decimal: {
41+
style: 'decimal',
42+
useGrouping: false
43+
}
44+
},
45+
'ja-JP': {
46+
currency: {
47+
style: 'currency',
48+
currency: 'JPY', currencyDisplay: 'symbol'
49+
},
50+
numeric: {
51+
style: 'decimal',
52+
useGrouping: false
53+
},
54+
percent: {
55+
style: 'percent',
56+
useGrouping: false
57+
}
58+
}
59+
}
60+
})
61+
62+
const app = createApp({})
63+
app.use(i18n)
64+
app.mount('#app')
65+
</script>
66+
</body>
67+
</html>

src/components/NumberFormat.ts

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,87 @@
1-
// TODO:
1+
import { h, defineComponent, SetupContext, VNodeArrayChildren } from 'vue'
2+
import { useI18n } from '../i18n'
3+
import { NumberOptions } from '../runtime'
4+
import { isString, isPlainObject, isArray } from '../utils'
25

3-
export const NumberFormat = {
4-
name: 'i18n-n'
5-
}
6+
const NUMBER_FORMAT_KEYS = [
7+
'localeMatcher',
8+
'style',
9+
'unit',
10+
'unitDisplay',
11+
'currency',
12+
'currencyDisplay',
13+
'useGrouping',
14+
'numberingSystem',
15+
'minimumIntegerDigits',
16+
'minimumFractionDigits',
17+
'maximumFractionDigits',
18+
'minimumSignificantDigits',
19+
'maximumSignificantDigits',
20+
'notation',
21+
'formatMatcher'
22+
]
23+
24+
export const NumberFormat = defineComponent({
25+
name: 'i18n-n',
26+
props: {
27+
tag: {
28+
type: String
29+
},
30+
value: {
31+
type: [Number, Date],
32+
required: true
33+
},
34+
format: {
35+
type: [String, Object]
36+
},
37+
locale: {
38+
type: String
39+
}
40+
},
41+
setup(props, context: SetupContext) {
42+
const { slots, attrs } = context
43+
const i18n = useI18n()
44+
45+
return () => {
46+
const options = { part: true } as NumberOptions
47+
let orverrides = {} as Intl.NumberFormatOptions
48+
49+
if (props.locale) {
50+
options.locale = props.locale
51+
}
52+
53+
if (isString(props.format)) {
54+
options.key = props.format
55+
} else if (isPlainObject(props.format)) {
56+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
57+
if (isString((props.format as any).key)) {
58+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59+
options.key = (props.format as any).key
60+
}
61+
// Filter out number format options only
62+
orverrides = Object.keys(props.format).reduce((options, prop) => {
63+
return NUMBER_FORMAT_KEYS.includes(prop)
64+
? Object.assign({}, options, { [prop]: props.format[prop] })
65+
: options
66+
}, {})
67+
}
68+
69+
const parts = i18n.__numberParts(...[props.value, options, orverrides])
70+
let children = [options.key] as VNodeArrayChildren
71+
if (isArray(parts)) {
72+
children = parts.map((part, index) => {
73+
const slot = slots[part.type]
74+
return slot
75+
? slot({ [part.type]: part.value, index, parts })
76+
: [part.value]
77+
})
78+
} else if (isString(parts)) {
79+
children = [parts]
80+
}
81+
82+
return props.tag
83+
? h(props.tag, { ...attrs }, children)
84+
: h('span', { ...attrs }, children)
85+
}
86+
}
87+
})

src/composer.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export type Composer = {
162162
setMissingHandler(handler: MissingHandler | null): void
163163
install: Plugin
164164
__transrateVNode(...args: unknown[]): unknown // for internal
165+
__numberParts(...args: unknown[]): string | Intl.NumberFormatPart[] // for internal
165166
}
166167

167168
let composerID = 0
@@ -242,7 +243,10 @@ export function createComposer(options: ComposerOptions = {}): Composer {
242243
// prettier-ignore
243244
__root
244245
? __root.fallbackLocale.value
245-
: isString(options.fallbackLocale) || isArray(options.fallbackLocale) || isPlainObject(options.fallbackLocale) || options.fallbackLocale === false
246+
: isString(options.fallbackLocale) ||
247+
isArray(options.fallbackLocale) ||
248+
isPlainObject(options.fallbackLocale) ||
249+
options.fallbackLocale === false
246250
? options.fallbackLocale
247251
: _locale.value
248252
)
@@ -458,7 +462,7 @@ export function createComposer(options: ComposerOptions = {}): Composer {
458462
interpolate
459463
} as MessageProcessor
460464

461-
// _transrateVNode, using for `i18n-t` component
465+
// __transrateVNode, using for `i18n-t` component
462466
const __transrateVNode = (...args: unknown[]): unknown => {
463467
return computed<unknown>((): unknown => {
464468
let ret: unknown
@@ -483,6 +487,29 @@ export function createComposer(options: ComposerOptions = {}): Composer {
483487
}).value
484488
}
485489

490+
// __numberParts, using for `i18n-n` component
491+
const __numberParts = (
492+
...args: unknown[]
493+
): string | Intl.NumberFormatPart[] => {
494+
return computed<string | Intl.NumberFormatPart[]>(():
495+
| string
496+
| Intl.NumberFormatPart[] => {
497+
const ret = number(_context, ...args)
498+
if (isNumber(ret) && ret === NOT_REOSLVED) {
499+
const [, options] = parseNumberArgs(...args)
500+
if (__DEV__ && _fallbackRoot && __root) {
501+
const key = isString(options.key) ? options.key : ''
502+
warn(`Fall back to number format '${key}' with root locale.`)
503+
}
504+
return _fallbackRoot && __root ? __root.__numberParts(...args) : []
505+
} else if (isString(ret) || isArray(ret)) {
506+
return ret
507+
} else {
508+
throw new Error('TODO:') // TODO
509+
}
510+
}).value
511+
}
512+
486513
// getLocaleMessage
487514
const getLocaleMessage = (locale: Locale): LocaleMessage =>
488515
_messages.value[locale] || {}
@@ -621,7 +648,8 @@ export function createComposer(options: ComposerOptions = {}): Composer {
621648
install(app: App, ...options: any[]): void {
622649
apply(app, composer, ...options)
623650
},
624-
__transrateVNode
651+
__transrateVNode,
652+
__numberParts
625653
}
626654

627655
return composer

src/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function apply(app: App, composer: Composer, ...options: any[]): void {
2121

2222
// install components
2323
app.component(pluginOptions['i18n-t'] || Translation.name, Translation)
24-
app.component(Number.name, Number)
24+
app.component(NumberFormat.name, NumberFormat)
2525

2626
// install directive
2727
app.directive('t', vT as FunctionDirective) // TODO:

src/runtime/number.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ import { warn, isString, isBoolean, isPlainObject, isNumber } from '../utils'
4242
*
4343
* // if you specify `part` options, you can get an array of objects containing the formatted number in parts
4444
* number(context, value, { key: 'currenty', part: true })
45+
*
46+
* // orverride context.numberFormats[locale] options with functino options
47+
* number(cnotext, value, 'currency', { currency: 'EUR' })
48+
* number(cnotext, value, 'currency', 'ja-JP', { currency: 'EUR' })
49+
* number(context, value, { key: 'currenty', part: true }, { currency: 'EUR'})
4550
*/
4651

4752
export type NumberOptions = {
@@ -95,7 +100,7 @@ export function number(
95100
return MISSING_RESOLVE_VALUE
96101
}
97102

98-
const [value, options] = parseNumberArgs(...args)
103+
const [value, options, orverrides] = parseNumberArgs(...args)
99104
const { key } = options
100105
const missingWarn = isBoolean(options.missingWarn)
101106
? options.missingWarn
@@ -137,18 +142,24 @@ export function number(
137142
return unresolving ? NOT_REOSLVED : key
138143
}
139144

140-
const id = `${targetLocale}__${key}`
145+
const id = `${targetLocale}__${key}__${JSON.stringify(orverrides)}`
141146
let formatter = _numberFormatters.get(id)
142147
if (!formatter) {
143-
formatter = new Intl.NumberFormat(targetLocale, format)
148+
formatter = new Intl.NumberFormat(
149+
targetLocale,
150+
Object.assign({}, format, orverrides)
151+
)
144152
_numberFormatters.set(id, formatter)
145153
}
146154
return !part ? formatter.format(value) : formatter.formatToParts(value)
147155
}
148156

149-
export function parseNumberArgs(...args: unknown[]): [number, NumberOptions] {
150-
const [arg1, arg2, arg3] = args
157+
export function parseNumberArgs(
158+
...args: unknown[]
159+
): [number, NumberOptions, Intl.NumberFormatOptions] {
160+
const [arg1, arg2, arg3, arg4] = args
151161
let options = {} as NumberOptions
162+
let orverrides = {} as Intl.NumberFormatOptions
152163

153164
if (!isNumber(arg1)) {
154165
throw new Error('TODO')
@@ -163,9 +174,15 @@ export function parseNumberArgs(...args: unknown[]): [number, NumberOptions] {
163174

164175
if (isString(arg3)) {
165176
options.locale = arg3
177+
} else if (isPlainObject(arg3)) {
178+
orverrides = arg3
179+
}
180+
181+
if (isPlainObject(arg4)) {
182+
orverrides = arg4
166183
}
167184

168-
return [value, options]
185+
return [value, options, orverrides]
169186
}
170187

171188
export function clearNumberFormat(

0 commit comments

Comments
 (0)