index.js 36 KB

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