index.js 36 KB

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