UNPKG

29.3 kBJavaScriptView Raw
1/* @flow */
2
3import { install, Vue } from './install'
4import {
5 warn,
6 error,
7 isNull,
8 parseArgs,
9 isPlainObject,
10 isObject,
11 looseClone,
12 remove,
13 merge,
14 numberFormatKeys
15} from './util'
16import BaseFormatter from './format'
17import I18nPath from './path'
18
19import type { PathValue } from './path'
20
21const htmlTagMatcher = /<\/?[\w\s="/.':;#-\/]+>/
22const linkKeyMatcher = /(?:@(?:\.[a-z]+)?:(?:[\w\-_|.]+|\([\w\-_|.]+\)))/g
23const linkKeyPrefixMatcher = /^@(?:\.([a-z]+))?:/
24const bracketsMatcher = /[()]/g
25const defaultModifiers = {
26 'upper': str => str.toLocaleUpperCase(),
27 'lower': str => str.toLocaleLowerCase()
28}
29
30const defaultFormatter = new BaseFormatter()
31
32export default class VueI18n {
33 static install: () => void
34 static version: string
35 static availabilities: IntlAvailability
36
37 _vm: any
38 _formatter: Formatter
39 _modifiers: Modifiers
40 _root: any
41 _sync: boolean
42 _fallbackRoot: boolean
43 _missing: ?MissingHandler
44 _exist: Function
45 _silentTranslationWarn: boolean | RegExp
46 _silentFallbackWarn: boolean | RegExp
47 _formatFallbackMessages: boolean
48 _dateTimeFormatters: Object
49 _numberFormatters: Object
50 _path: I18nPath
51 _dataListeners: Array<any>
52 _preserveDirectiveContent: boolean
53 _warnHtmlInMessage: WarnHtmlInMessageLevel
54 pluralizationRules: {
55 [lang: string]: (choice: number, choicesLength: number) => number
56 }
57
58 constructor (options: I18nOptions = {}) {
59 // Auto install if it is not done yet and `window` has `Vue`.
60 // To allow users to avoid auto-installation in some cases,
61 // this code should be placed here. See #290
62 /* istanbul ignore if */
63 if (!Vue && typeof window !== 'undefined' && window.Vue) {
64 install(window.Vue)
65 }
66
67 const locale: Locale = options.locale || 'en-US'
68 const fallbackLocale: Locale = options.fallbackLocale || 'en-US'
69 const messages: LocaleMessages = options.messages || {}
70 const dateTimeFormats = options.dateTimeFormats || {}
71 const numberFormats = options.numberFormats || {}
72
73 this._vm = null
74 this._formatter = options.formatter || defaultFormatter
75 this._modifiers = options.modifiers || {}
76 this._missing = options.missing || null
77 this._root = options.root || null
78 this._sync = options.sync === undefined ? true : !!options.sync
79 this._fallbackRoot = options.fallbackRoot === undefined
80 ? true
81 : !!options.fallbackRoot
82 this._formatFallbackMessages = options.formatFallbackMessages === undefined
83 ? false
84 : !!options.formatFallbackMessages
85 this._silentTranslationWarn = options.silentTranslationWarn === undefined
86 ? false
87 : options.silentTranslationWarn
88 this._silentFallbackWarn = options.silentFallbackWarn === undefined
89 ? false
90 : !!options.silentFallbackWarn
91 this._dateTimeFormatters = {}
92 this._numberFormatters = {}
93 this._path = new I18nPath()
94 this._dataListeners = []
95 this._preserveDirectiveContent = options.preserveDirectiveContent === undefined
96 ? false
97 : !!options.preserveDirectiveContent
98 this.pluralizationRules = options.pluralizationRules || {}
99 this._warnHtmlInMessage = options.warnHtmlInMessage || 'off'
100
101 this._exist = (message: Object, key: Path): boolean => {
102 if (!message || !key) { return false }
103 if (!isNull(this._path.getPathValue(message, key))) { return true }
104 // fallback for flat key
105 if (message[key]) { return true }
106 return false
107 }
108
109 if (this._warnHtmlInMessage === 'warn' || this._warnHtmlInMessage === 'error') {
110 Object.keys(messages).forEach(locale => {
111 this._checkLocaleMessage(locale, this._warnHtmlInMessage, messages[locale])
112 })
113 }
114
115 this._initVM({
116 locale,
117 fallbackLocale,
118 messages,
119 dateTimeFormats,
120 numberFormats
121 })
122 }
123
124 _checkLocaleMessage (locale: Locale, level: WarnHtmlInMessageLevel, message: LocaleMessageObject): void {
125 const paths: Array<string> = []
126
127 const fn = (level: WarnHtmlInMessageLevel, locale: Locale, message: any, paths: Array<string>) => {
128 if (isPlainObject(message)) {
129 Object.keys(message).forEach(key => {
130 const val = message[key]
131 if (isPlainObject(val)) {
132 paths.push(key)
133 paths.push('.')
134 fn(level, locale, val, paths)
135 paths.pop()
136 paths.pop()
137 } else {
138 paths.push(key)
139 fn(level, locale, val, paths)
140 paths.pop()
141 }
142 })
143 } else if (Array.isArray(message)) {
144 message.forEach((item, index) => {
145 if (isPlainObject(item)) {
146 paths.push(`[${index}]`)
147 paths.push('.')
148 fn(level, locale, item, paths)
149 paths.pop()
150 paths.pop()
151 } else {
152 paths.push(`[${index}]`)
153 fn(level, locale, item, paths)
154 paths.pop()
155 }
156 })
157 } else if (typeof message === 'string') {
158 const ret = htmlTagMatcher.test(message)
159 if (ret) {
160 const msg = `Detected HTML in message '${message}' of keypath '${paths.join('')}' at '${locale}'. Consider component interpolation with '<i18n>' to avoid XSS. See https://bit.ly/2ZqJzkp`
161 if (level === 'warn') {
162 warn(msg)
163 } else if (level === 'error') {
164 error(msg)
165 }
166 }
167 }
168 }
169
170 fn(level, locale, message, paths)
171 }
172
173 _initVM (data: {
174 locale: Locale,
175 fallbackLocale: Locale,
176 messages: LocaleMessages,
177 dateTimeFormats: DateTimeFormats,
178 numberFormats: NumberFormats
179 }): void {
180 const silent = Vue.config.silent
181 Vue.config.silent = true
182 this._vm = new Vue({ data })
183 Vue.config.silent = silent
184 }
185
186 destroyVM (): void {
187 this._vm.$destroy()
188 }
189
190 subscribeDataChanging (vm: any): void {
191 this._dataListeners.push(vm)
192 }
193
194 unsubscribeDataChanging (vm: any): void {
195 remove(this._dataListeners, vm)
196 }
197
198 watchI18nData (): Function {
199 const self = this
200 return this._vm.$watch('$data', () => {
201 let i = self._dataListeners.length
202 while (i--) {
203 Vue.nextTick(() => {
204 self._dataListeners[i] && self._dataListeners[i].$forceUpdate()
205 })
206 }
207 }, { deep: true })
208 }
209
210 watchLocale (): ?Function {
211 /* istanbul ignore if */
212 if (!this._sync || !this._root) { return null }
213 const target: any = this._vm
214 return this._root.$i18n.vm.$watch('locale', (val) => {
215 target.$set(target, 'locale', val)
216 target.$forceUpdate()
217 }, { immediate: true })
218 }
219
220 get vm (): any { return this._vm }
221
222 get messages (): LocaleMessages { return looseClone(this._getMessages()) }
223 get dateTimeFormats (): DateTimeFormats { return looseClone(this._getDateTimeFormats()) }
224 get numberFormats (): NumberFormats { return looseClone(this._getNumberFormats()) }
225 get availableLocales (): Locale[] { return Object.keys(this.messages).sort() }
226
227 get locale (): Locale { return this._vm.locale }
228 set locale (locale: Locale): void {
229 this._vm.$set(this._vm, 'locale', locale)
230 }
231
232 get fallbackLocale (): Locale { return this._vm.fallbackLocale }
233 set fallbackLocale (locale: Locale): void {
234 this._vm.$set(this._vm, 'fallbackLocale', locale)
235 }
236
237 get formatFallbackMessages (): boolean { return this._formatFallbackMessages }
238 set formatFallbackMessages (fallback: boolean): void { this._formatFallbackMessages = fallback }
239
240 get missing (): ?MissingHandler { return this._missing }
241 set missing (handler: MissingHandler): void { this._missing = handler }
242
243 get formatter (): Formatter { return this._formatter }
244 set formatter (formatter: Formatter): void { this._formatter = formatter }
245
246 get silentTranslationWarn (): boolean | RegExp { return this._silentTranslationWarn }
247 set silentTranslationWarn (silent: boolean | RegExp): void { this._silentTranslationWarn = silent }
248
249 get silentFallbackWarn (): boolean | RegExp { return this._silentFallbackWarn }
250 set silentFallbackWarn (silent: boolean | RegExp): void { this._silentFallbackWarn = silent }
251
252 get preserveDirectiveContent (): boolean { return this._preserveDirectiveContent }
253 set preserveDirectiveContent (preserve: boolean): void { this._preserveDirectiveContent = preserve }
254
255 get warnHtmlInMessage (): WarnHtmlInMessageLevel { return this._warnHtmlInMessage }
256 set warnHtmlInMessage (level: WarnHtmlInMessageLevel): void {
257 const orgLevel = this._warnHtmlInMessage
258 this._warnHtmlInMessage = level
259 if (orgLevel !== level && (level === 'warn' || level === 'error')) {
260 const messages = this._getMessages()
261 Object.keys(messages).forEach(locale => {
262 this._checkLocaleMessage(locale, this._warnHtmlInMessage, messages[locale])
263 })
264 }
265 }
266
267 _getMessages (): LocaleMessages { return this._vm.messages }
268 _getDateTimeFormats (): DateTimeFormats { return this._vm.dateTimeFormats }
269 _getNumberFormats (): NumberFormats { return this._vm.numberFormats }
270
271 _warnDefault (locale: Locale, key: Path, result: ?any, vm: ?any, values: any): ?string {
272 if (!isNull(result)) { return result }
273 if (this._missing) {
274 const missingRet = this._missing.apply(null, [locale, key, vm, values])
275 if (typeof missingRet === 'string') {
276 return missingRet
277 }
278 } else {
279 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key)) {
280 warn(
281 `Cannot translate the value of keypath '${key}'. ` +
282 'Use the value of keypath as default.'
283 )
284 }
285 }
286
287 if (this._formatFallbackMessages) {
288 const parsedArgs = parseArgs(...values)
289 return this._render(key, 'string', parsedArgs.params, key)
290 } else {
291 return key
292 }
293 }
294
295 _isFallbackRoot (val: any): boolean {
296 return !val && !isNull(this._root) && this._fallbackRoot
297 }
298
299 _isSilentFallbackWarn (key: Path): boolean {
300 return this._silentFallbackWarn instanceof RegExp
301 ? this._silentFallbackWarn.test(key)
302 : this._silentFallbackWarn
303 }
304
305 _isSilentFallback (locale: Locale, key: Path): boolean {
306 return this._isSilentFallbackWarn(key) && (this._isFallbackRoot() || locale !== this.fallbackLocale)
307 }
308
309 _isSilentTranslationWarn (key: Path): boolean {
310 return this._silentTranslationWarn instanceof RegExp
311 ? this._silentTranslationWarn.test(key)
312 : this._silentTranslationWarn
313 }
314
315 _interpolate (
316 locale: Locale,
317 message: LocaleMessageObject,
318 key: Path,
319 host: any,
320 interpolateMode: string,
321 values: any,
322 visitedLinkStack: Array<string>
323 ): any {
324 if (!message) { return null }
325
326 const pathRet: PathValue = this._path.getPathValue(message, key)
327 if (Array.isArray(pathRet) || isPlainObject(pathRet)) { return pathRet }
328
329 let ret: mixed
330 if (isNull(pathRet)) {
331 /* istanbul ignore else */
332 if (isPlainObject(message)) {
333 ret = message[key]
334 if (typeof ret !== 'string') {
335 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallback(locale, key)) {
336 warn(`Value of key '${key}' is not a string!`)
337 }
338 return null
339 }
340 } else {
341 return null
342 }
343 } else {
344 /* istanbul ignore else */
345 if (typeof pathRet === 'string') {
346 ret = pathRet
347 } else {
348 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallback(locale, key)) {
349 warn(`Value of key '${key}' is not a string!`)
350 }
351 return null
352 }
353 }
354
355 // Check for the existence of links within the translated string
356 if (ret.indexOf('@:') >= 0 || ret.indexOf('@.') >= 0) {
357 ret = this._link(locale, message, ret, host, 'raw', values, visitedLinkStack)
358 }
359
360 return this._render(ret, interpolateMode, values, key)
361 }
362
363 _link (
364 locale: Locale,
365 message: LocaleMessageObject,
366 str: string,
367 host: any,
368 interpolateMode: string,
369 values: any,
370 visitedLinkStack: Array<string>
371 ): any {
372 let ret: string = str
373
374 // Match all the links within the local
375 // We are going to replace each of
376 // them with its translation
377 const matches: any = ret.match(linkKeyMatcher)
378 for (const idx in matches) {
379 // ie compatible: filter custom array
380 // prototype method
381 if (!matches.hasOwnProperty(idx)) {
382 continue
383 }
384 const link: string = matches[idx]
385 const linkKeyPrefixMatches: any = link.match(linkKeyPrefixMatcher)
386 const [linkPrefix, formatterName] = linkKeyPrefixMatches
387
388 // Remove the leading @:, @.case: and the brackets
389 const linkPlaceholder: string = link.replace(linkPrefix, '').replace(bracketsMatcher, '')
390
391 if (visitedLinkStack.includes(linkPlaceholder)) {
392 if (process.env.NODE_ENV !== 'production') {
393 warn(`Circular reference found. "${link}" is already visited in the chain of ${visitedLinkStack.reverse().join(' <- ')}`)
394 }
395 return ret
396 }
397 visitedLinkStack.push(linkPlaceholder)
398
399 // Translate the link
400 let translated: any = this._interpolate(
401 locale, message, linkPlaceholder, host,
402 interpolateMode === 'raw' ? 'string' : interpolateMode,
403 interpolateMode === 'raw' ? undefined : values,
404 visitedLinkStack
405 )
406
407 if (this._isFallbackRoot(translated)) {
408 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(linkPlaceholder)) {
409 warn(`Fall back to translate the link placeholder '${linkPlaceholder}' with root locale.`)
410 }
411 /* istanbul ignore if */
412 if (!this._root) { throw Error('unexpected error') }
413 const root: any = this._root.$i18n
414 translated = root._translate(
415 root._getMessages(), root.locale, root.fallbackLocale,
416 linkPlaceholder, host, interpolateMode, values
417 )
418 }
419 translated = this._warnDefault(
420 locale, linkPlaceholder, translated, host,
421 Array.isArray(values) ? values : [values]
422 )
423
424 if (this._modifiers.hasOwnProperty(formatterName)) {
425 translated = this._modifiers[formatterName](translated)
426 } else if (defaultModifiers.hasOwnProperty(formatterName)) {
427 translated = defaultModifiers[formatterName](translated)
428 }
429
430 visitedLinkStack.pop()
431
432 // Replace the link with the translated
433 ret = !translated ? ret : ret.replace(link, translated)
434 }
435
436 return ret
437 }
438
439 _render (message: string, interpolateMode: string, values: any, path: string): any {
440 let ret = this._formatter.interpolate(message, values, path)
441
442 // If the custom formatter refuses to work - apply the default one
443 if (!ret) {
444 ret = defaultFormatter.interpolate(message, values, path)
445 }
446
447 // if interpolateMode is **not** 'string' ('row'),
448 // return the compiled data (e.g. ['foo', VNode, 'bar']) with formatter
449 return interpolateMode === 'string' ? ret.join('') : ret
450 }
451
452 _translate (
453 messages: LocaleMessages,
454 locale: Locale,
455 fallback: Locale,
456 key: Path,
457 host: any,
458 interpolateMode: string,
459 args: any
460 ): any {
461 let res: any =
462 this._interpolate(locale, messages[locale], key, host, interpolateMode, args, [key])
463 if (!isNull(res)) { return res }
464
465 res = this._interpolate(fallback, messages[fallback], key, host, interpolateMode, args, [key])
466 if (!isNull(res)) {
467 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
468 warn(`Fall back to translate the keypath '${key}' with '${fallback}' locale.`)
469 }
470 return res
471 } else {
472 return null
473 }
474 }
475
476 _t (key: Path, _locale: Locale, messages: LocaleMessages, host: any, ...values: any): any {
477 if (!key) { return '' }
478
479 const parsedArgs = parseArgs(...values)
480 const locale: Locale = parsedArgs.locale || _locale
481
482 const ret: any = this._translate(
483 messages, locale, this.fallbackLocale, key,
484 host, 'string', parsedArgs.params
485 )
486 if (this._isFallbackRoot(ret)) {
487 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
488 warn(`Fall back to translate the keypath '${key}' with root locale.`)
489 }
490 /* istanbul ignore if */
491 if (!this._root) { throw Error('unexpected error') }
492 return this._root.$t(key, ...values)
493 } else {
494 return this._warnDefault(locale, key, ret, host, values)
495 }
496 }
497
498 t (key: Path, ...values: any): TranslateResult {
499 return this._t(key, this.locale, this._getMessages(), null, ...values)
500 }
501
502 _i (key: Path, locale: Locale, messages: LocaleMessages, host: any, values: Object): any {
503 const ret: any =
504 this._translate(messages, locale, this.fallbackLocale, key, host, 'raw', values)
505 if (this._isFallbackRoot(ret)) {
506 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key)) {
507 warn(`Fall back to interpolate the keypath '${key}' with root locale.`)
508 }
509 if (!this._root) { throw Error('unexpected error') }
510 return this._root.$i18n.i(key, locale, values)
511 } else {
512 return this._warnDefault(locale, key, ret, host, [values])
513 }
514 }
515
516 i (key: Path, locale: Locale, values: Object): TranslateResult {
517 /* istanbul ignore if */
518 if (!key) { return '' }
519
520 if (typeof locale !== 'string') {
521 locale = this.locale
522 }
523
524 return this._i(key, locale, this._getMessages(), null, values)
525 }
526
527 _tc (
528 key: Path,
529 _locale: Locale,
530 messages: LocaleMessages,
531 host: any,
532 choice?: number,
533 ...values: any
534 ): any {
535 if (!key) { return '' }
536 if (choice === undefined) {
537 choice = 1
538 }
539
540 const predefined = { 'count': choice, 'n': choice }
541 const parsedArgs = parseArgs(...values)
542 parsedArgs.params = Object.assign(predefined, parsedArgs.params)
543 values = parsedArgs.locale === null ? [parsedArgs.params] : [parsedArgs.locale, parsedArgs.params]
544 return this.fetchChoice(this._t(key, _locale, messages, host, ...values), choice)
545 }
546
547 fetchChoice (message: string, choice: number): ?string {
548 /* istanbul ignore if */
549 if (!message && typeof message !== 'string') { return null }
550 const choices: Array<string> = message.split('|')
551
552 choice = this.getChoiceIndex(choice, choices.length)
553 if (!choices[choice]) { return message }
554 return choices[choice].trim()
555 }
556
557 /**
558 * @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)`
559 * @param choicesLength {number} an overall amount of available choices
560 * @returns a final choice index
561 */
562 getChoiceIndex (choice: number, choicesLength: number): number {
563 // Default (old) getChoiceIndex implementation - english-compatible
564 const defaultImpl = (_choice: number, _choicesLength: number) => {
565 _choice = Math.abs(_choice)
566
567 if (_choicesLength === 2) {
568 return _choice
569 ? _choice > 1
570 ? 1
571 : 0
572 : 1
573 }
574
575 return _choice ? Math.min(_choice, 2) : 0
576 }
577
578 if (this.locale in this.pluralizationRules) {
579 return this.pluralizationRules[this.locale].apply(this, [choice, choicesLength])
580 } else {
581 return defaultImpl(choice, choicesLength)
582 }
583 }
584
585 tc (key: Path, choice?: number, ...values: any): TranslateResult {
586 return this._tc(key, this.locale, this._getMessages(), null, choice, ...values)
587 }
588
589 _te (key: Path, locale: Locale, messages: LocaleMessages, ...args: any): boolean {
590 const _locale: Locale = parseArgs(...args).locale || locale
591 return this._exist(messages[_locale], key)
592 }
593
594 te (key: Path, locale?: Locale): boolean {
595 return this._te(key, this.locale, this._getMessages(), locale)
596 }
597
598 getLocaleMessage (locale: Locale): LocaleMessageObject {
599 return looseClone(this._vm.messages[locale] || {})
600 }
601
602 setLocaleMessage (locale: Locale, message: LocaleMessageObject): void {
603 if (this._warnHtmlInMessage === 'warn' || this._warnHtmlInMessage === 'error') {
604 this._checkLocaleMessage(locale, this._warnHtmlInMessage, message)
605 if (this._warnHtmlInMessage === 'error') { return }
606 }
607 this._vm.$set(this._vm.messages, locale, message)
608 }
609
610 mergeLocaleMessage (locale: Locale, message: LocaleMessageObject): void {
611 if (this._warnHtmlInMessage === 'warn' || this._warnHtmlInMessage === 'error') {
612 this._checkLocaleMessage(locale, this._warnHtmlInMessage, message)
613 if (this._warnHtmlInMessage === 'error') { return }
614 }
615 this._vm.$set(this._vm.messages, locale, merge(this._vm.messages[locale] || {}, message))
616 }
617
618 getDateTimeFormat (locale: Locale): DateTimeFormat {
619 return looseClone(this._vm.dateTimeFormats[locale] || {})
620 }
621
622 setDateTimeFormat (locale: Locale, format: DateTimeFormat): void {
623 this._vm.$set(this._vm.dateTimeFormats, locale, format)
624 }
625
626 mergeDateTimeFormat (locale: Locale, format: DateTimeFormat): void {
627 this._vm.$set(this._vm.dateTimeFormats, locale, merge(this._vm.dateTimeFormats[locale] || {}, format))
628 }
629
630 _localizeDateTime (
631 value: number | Date,
632 locale: Locale,
633 fallback: Locale,
634 dateTimeFormats: DateTimeFormats,
635 key: string
636 ): ?DateTimeFormatResult {
637 let _locale: Locale = locale
638 let formats: DateTimeFormat = dateTimeFormats[_locale]
639
640 // fallback locale
641 if (isNull(formats) || isNull(formats[key])) {
642 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
643 warn(`Fall back to '${fallback}' datetime formats from '${locale}' datetime formats.`)
644 }
645 _locale = fallback
646 formats = dateTimeFormats[_locale]
647 }
648
649 if (isNull(formats) || isNull(formats[key])) {
650 return null
651 } else {
652 const format: ?DateTimeFormatOptions = formats[key]
653 const id = `${_locale}__${key}`
654 let formatter = this._dateTimeFormatters[id]
655 if (!formatter) {
656 formatter = this._dateTimeFormatters[id] = new Intl.DateTimeFormat(_locale, format)
657 }
658 return formatter.format(value)
659 }
660 }
661
662 _d (value: number | Date, locale: Locale, key: ?string): DateTimeFormatResult {
663 /* istanbul ignore if */
664 if (process.env.NODE_ENV !== 'production' && !VueI18n.availabilities.dateTimeFormat) {
665 warn('Cannot format a Date value due to not supported Intl.DateTimeFormat.')
666 return ''
667 }
668
669 if (!key) {
670 return new Intl.DateTimeFormat(locale).format(value)
671 }
672
673 const ret: ?DateTimeFormatResult =
674 this._localizeDateTime(value, locale, this.fallbackLocale, this._getDateTimeFormats(), key)
675 if (this._isFallbackRoot(ret)) {
676 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
677 warn(`Fall back to datetime localization of root: key '${key}'.`)
678 }
679 /* istanbul ignore if */
680 if (!this._root) { throw Error('unexpected error') }
681 return this._root.$i18n.d(value, key, locale)
682 } else {
683 return ret || ''
684 }
685 }
686
687 d (value: number | Date, ...args: any): DateTimeFormatResult {
688 let locale: Locale = this.locale
689 let key: ?string = null
690
691 if (args.length === 1) {
692 if (typeof args[0] === 'string') {
693 key = args[0]
694 } else if (isObject(args[0])) {
695 if (args[0].locale) {
696 locale = args[0].locale
697 }
698 if (args[0].key) {
699 key = args[0].key
700 }
701 }
702 } else if (args.length === 2) {
703 if (typeof args[0] === 'string') {
704 key = args[0]
705 }
706 if (typeof args[1] === 'string') {
707 locale = args[1]
708 }
709 }
710
711 return this._d(value, locale, key)
712 }
713
714 getNumberFormat (locale: Locale): NumberFormat {
715 return looseClone(this._vm.numberFormats[locale] || {})
716 }
717
718 setNumberFormat (locale: Locale, format: NumberFormat): void {
719 this._vm.$set(this._vm.numberFormats, locale, format)
720 }
721
722 mergeNumberFormat (locale: Locale, format: NumberFormat): void {
723 this._vm.$set(this._vm.numberFormats, locale, merge(this._vm.numberFormats[locale] || {}, format))
724 }
725
726 _getNumberFormatter (
727 value: number,
728 locale: Locale,
729 fallback: Locale,
730 numberFormats: NumberFormats,
731 key: string,
732 options: ?NumberFormatOptions
733 ): ?Object {
734 let _locale: Locale = locale
735 let formats: NumberFormat = numberFormats[_locale]
736
737 // fallback locale
738 if (isNull(formats) || isNull(formats[key])) {
739 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
740 warn(`Fall back to '${fallback}' number formats from '${locale}' number formats.`)
741 }
742 _locale = fallback
743 formats = numberFormats[_locale]
744 }
745
746 if (isNull(formats) || isNull(formats[key])) {
747 return null
748 } else {
749 const format: ?NumberFormatOptions = formats[key]
750
751 let formatter
752 if (options) {
753 // If options specified - create one time number formatter
754 formatter = new Intl.NumberFormat(_locale, Object.assign({}, format, options))
755 } else {
756 const id = `${_locale}__${key}`
757 formatter = this._numberFormatters[id]
758 if (!formatter) {
759 formatter = this._numberFormatters[id] = new Intl.NumberFormat(_locale, format)
760 }
761 }
762 return formatter
763 }
764 }
765
766 _n (value: number, locale: Locale, key: ?string, options: ?NumberFormatOptions): NumberFormatResult {
767 /* istanbul ignore if */
768 if (!VueI18n.availabilities.numberFormat) {
769 if (process.env.NODE_ENV !== 'production') {
770 warn('Cannot format a Number value due to not supported Intl.NumberFormat.')
771 }
772 return ''
773 }
774
775 if (!key) {
776 const nf = !options ? new Intl.NumberFormat(locale) : new Intl.NumberFormat(locale, options)
777 return nf.format(value)
778 }
779
780 const formatter: ?Object = this._getNumberFormatter(value, locale, this.fallbackLocale, this._getNumberFormats(), key, options)
781 const ret: ?NumberFormatResult = formatter && formatter.format(value)
782 if (this._isFallbackRoot(ret)) {
783 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
784 warn(`Fall back to number localization of root: key '${key}'.`)
785 }
786 /* istanbul ignore if */
787 if (!this._root) { throw Error('unexpected error') }
788 return this._root.$i18n.n(value, Object.assign({}, { key, locale }, options))
789 } else {
790 return ret || ''
791 }
792 }
793
794 n (value: number, ...args: any): NumberFormatResult {
795 let locale: Locale = this.locale
796 let key: ?string = null
797 let options: ?NumberFormatOptions = null
798
799 if (args.length === 1) {
800 if (typeof args[0] === 'string') {
801 key = args[0]
802 } else if (isObject(args[0])) {
803 if (args[0].locale) {
804 locale = args[0].locale
805 }
806 if (args[0].key) {
807 key = args[0].key
808 }
809
810 // Filter out number format options only
811 options = Object.keys(args[0]).reduce((acc, key) => {
812 if (numberFormatKeys.includes(key)) {
813 return Object.assign({}, acc, { [key]: args[0][key] })
814 }
815 return acc
816 }, null)
817 }
818 } else if (args.length === 2) {
819 if (typeof args[0] === 'string') {
820 key = args[0]
821 }
822 if (typeof args[1] === 'string') {
823 locale = args[1]
824 }
825 }
826
827 return this._n(value, locale, key, options)
828 }
829
830 _ntp (value: number, locale: Locale, key: ?string, options: ?NumberFormatOptions): NumberFormatToPartsResult {
831 /* istanbul ignore if */
832 if (!VueI18n.availabilities.numberFormat) {
833 if (process.env.NODE_ENV !== 'production') {
834 warn('Cannot format to parts a Number value due to not supported Intl.NumberFormat.')
835 }
836 return []
837 }
838
839 if (!key) {
840 const nf = !options ? new Intl.NumberFormat(locale) : new Intl.NumberFormat(locale, options)
841 return nf.formatToParts(value)
842 }
843
844 const formatter: ?Object = this._getNumberFormatter(value, locale, this.fallbackLocale, this._getNumberFormats(), key, options)
845 const ret: ?NumberFormatToPartsResult = formatter && formatter.formatToParts(value)
846 if (this._isFallbackRoot(ret)) {
847 if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key)) {
848 warn(`Fall back to format number to parts of root: key '${key}' .`)
849 }
850 /* istanbul ignore if */
851 if (!this._root) { throw Error('unexpected error') }
852 return this._root.$i18n._ntp(value, locale, key, options)
853 } else {
854 return ret || []
855 }
856 }
857}
858
859let availabilities: IntlAvailability
860// $FlowFixMe
861Object.defineProperty(VueI18n, 'availabilities', {
862 get () {
863 if (!availabilities) {
864 const intlDefined = typeof Intl !== 'undefined'
865 availabilities = {
866 dateTimeFormat: intlDefined && typeof Intl.DateTimeFormat !== 'undefined',
867 numberFormat: intlDefined && typeof Intl.NumberFormat !== 'undefined'
868 }
869 }
870
871 return availabilities
872 }
873})
874
875VueI18n.install = install
876VueI18n.version = '__VERSION__'