index.js 33 KB

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