index.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. var fs = require('fs')
  2. var path = require('path')
  3. var util = require('util')
  4. function Y18N (opts) {
  5. // configurable options.
  6. opts = opts || {}
  7. this.directory = opts.directory || './locales'
  8. this.updateFiles = typeof opts.updateFiles === 'boolean' ? opts.updateFiles : true
  9. this.locale = opts.locale || 'en'
  10. this.fallbackToLanguage = typeof opts.fallbackToLanguage === 'boolean' ? opts.fallbackToLanguage : true
  11. // internal stuff.
  12. this.cache = Object.create(null)
  13. this.writeQueue = []
  14. }
  15. Y18N.prototype.__ = function () {
  16. var args = Array.prototype.slice.call(arguments)
  17. var str = args.shift()
  18. var cb = function () {} // start with noop.
  19. if (typeof args[args.length - 1] === 'function') cb = args.pop()
  20. cb = cb || function () {} // noop.
  21. if (!this.cache[this.locale]) this._readLocaleFile()
  22. // we've observed a new string, update the language file.
  23. if (!this.cache[this.locale][str] && this.updateFiles) {
  24. this.cache[this.locale][str] = str
  25. // include the current directory and locale,
  26. // since these values could change before the
  27. // write is performed.
  28. this._enqueueWrite([this.directory, this.locale, cb])
  29. } else {
  30. cb()
  31. }
  32. return util.format.apply(util, [this.cache[this.locale][str] || str].concat(args))
  33. }
  34. Y18N.prototype._enqueueWrite = function (work) {
  35. this.writeQueue.push(work)
  36. if (this.writeQueue.length === 1) this._processWriteQueue()
  37. }
  38. Y18N.prototype._processWriteQueue = function () {
  39. var _this = this
  40. var work = this.writeQueue[0]
  41. // destructure the enqueued work.
  42. var directory = work[0]
  43. var locale = work[1]
  44. var cb = work[2]
  45. var languageFile = this._resolveLocaleFile(directory, locale)
  46. var serializedLocale = JSON.stringify(this.cache[locale], null, 2)
  47. fs.writeFile(languageFile, serializedLocale, 'utf-8', function (err) {
  48. _this.writeQueue.shift()
  49. if (_this.writeQueue.length > 0) _this._processWriteQueue()
  50. cb(err)
  51. })
  52. }
  53. Y18N.prototype._readLocaleFile = function () {
  54. var localeLookup = {}
  55. var languageFile = this._resolveLocaleFile(this.directory, this.locale)
  56. try {
  57. localeLookup = JSON.parse(fs.readFileSync(languageFile, 'utf-8'))
  58. } catch (err) {
  59. if (err instanceof SyntaxError) {
  60. err.message = 'syntax error in ' + languageFile
  61. }
  62. if (err.code === 'ENOENT') localeLookup = {}
  63. else throw err
  64. }
  65. this.cache[this.locale] = localeLookup
  66. }
  67. Y18N.prototype._resolveLocaleFile = function (directory, locale) {
  68. var file = path.resolve(directory, './', locale + '.json')
  69. if (this.fallbackToLanguage && !this._fileExistsSync(file) && ~locale.lastIndexOf('_')) {
  70. // attempt fallback to language only
  71. var languageFile = path.resolve(directory, './', locale.split('_')[0] + '.json')
  72. if (this._fileExistsSync(languageFile)) file = languageFile
  73. }
  74. return file
  75. }
  76. // this only exists because fs.existsSync() "will be deprecated"
  77. // see https://nodejs.org/api/fs.html#fs_fs_existssync_path
  78. Y18N.prototype._fileExistsSync = function (file) {
  79. try {
  80. return fs.statSync(file).isFile()
  81. } catch (err) {
  82. return false
  83. }
  84. }
  85. Y18N.prototype.__n = function () {
  86. var args = Array.prototype.slice.call(arguments)
  87. var singular = args.shift()
  88. var plural = args.shift()
  89. var quantity = args.shift()
  90. var cb = function () {} // start with noop.
  91. if (typeof args[args.length - 1] === 'function') cb = args.pop()
  92. if (!this.cache[this.locale]) this._readLocaleFile()
  93. var str = quantity === 1 ? singular : plural
  94. if (this.cache[this.locale][singular]) {
  95. str = this.cache[this.locale][singular][quantity === 1 ? 'one' : 'other']
  96. }
  97. // we've observed a new string, update the language file.
  98. if (!this.cache[this.locale][singular] && this.updateFiles) {
  99. this.cache[this.locale][singular] = {
  100. one: singular,
  101. other: plural
  102. }
  103. // include the current directory and locale,
  104. // since these values could change before the
  105. // write is performed.
  106. this._enqueueWrite([this.directory, this.locale, cb])
  107. } else {
  108. cb()
  109. }
  110. // if a %d placeholder is provided, add quantity
  111. // to the arguments expanded by util.format.
  112. var values = [str]
  113. if (~str.indexOf('%d')) values.push(quantity)
  114. return util.format.apply(util, values.concat(args))
  115. }
  116. Y18N.prototype.setLocale = function (locale) {
  117. this.locale = locale
  118. }
  119. Y18N.prototype.getLocale = function () {
  120. return this.locale
  121. }
  122. Y18N.prototype.updateLocale = function (obj) {
  123. if (!this.cache[this.locale]) this._readLocaleFile()
  124. for (var key in obj) {
  125. this.cache[this.locale][key] = obj[key]
  126. }
  127. }
  128. module.exports = function (opts) {
  129. var y18n = new Y18N(opts)
  130. // bind all functions to y18n, so that
  131. // they can be used in isolation.
  132. for (var key in y18n) {
  133. if (typeof y18n[key] === 'function') {
  134. y18n[key] = y18n[key].bind(y18n)
  135. }
  136. }
  137. return y18n
  138. }