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