1 |
|
2 |
|
3 | import { install, Vue } from './install'
|
4 | import {
|
5 | warn,
|
6 | error,
|
7 | isNull,
|
8 | parseArgs,
|
9 | isPlainObject,
|
10 | isObject,
|
11 | looseClone,
|
12 | remove,
|
13 | merge,
|
14 | numberFormatKeys
|
15 | } from './util'
|
16 | import BaseFormatter from './format'
|
17 | import I18nPath from './path'
|
18 |
|
19 | import type { PathValue } from './path'
|
20 |
|
21 | const htmlTagMatcher = /<\/?[\w\s="/.':;#-\/]+>/
|
22 | const linkKeyMatcher = /(?:@(?:\.[a-z]+)?:(?:[\w\-_|.]+|\([\w\-_|.]+\)))/g
|
23 | const linkKeyPrefixMatcher = /^@(?:\.([a-z]+))?:/
|
24 | const bracketsMatcher = /[()]/g
|
25 | const defaultModifiers = {
|
26 | 'upper': str => str.toLocaleUpperCase(),
|
27 | 'lower': str => str.toLocaleLowerCase()
|
28 | }
|
29 |
|
30 | const defaultFormatter = new BaseFormatter()
|
31 |
|
32 | export 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 |
|
60 |
|
61 |
|
62 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
375 |
|
376 |
|
377 | const matches: any = ret.match(linkKeyMatcher)
|
378 | for (const idx in matches) {
|
379 |
|
380 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
443 | if (!ret) {
|
444 | ret = defaultFormatter.interpolate(message, values, path)
|
445 | }
|
446 |
|
447 |
|
448 |
|
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 |
|
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 |
|
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 |
|
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 |
|
559 |
|
560 |
|
561 |
|
562 | getChoiceIndex (choice: number, choicesLength: number): number {
|
563 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
859 | let availabilities: IntlAvailability
|
860 |
|
861 | Object.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 |
|
875 | VueI18n.install = install
|
876 | VueI18n.version = '__VERSION__'
|