| var fs = require('fs') |
| var path = require('path') |
| var util = require('util') |
| |
| function Y18N (opts) { |
| // configurable options. |
| opts = opts || {} |
| this.directory = opts.directory || './locales' |
| this.updateFiles = typeof opts.updateFiles === 'boolean' ? opts.updateFiles : true |
| this.locale = opts.locale || 'en' |
| this.fallbackToLanguage = typeof opts.fallbackToLanguage === 'boolean' ? opts.fallbackToLanguage : true |
| |
| // internal stuff. |
| this.cache = {} |
| this.writeQueue = [] |
| } |
| |
| Y18N.prototype.__ = function () { |
| if (typeof arguments[0] !== 'string') { |
| return this._taggedLiteral.apply(this, arguments) |
| } |
| var args = Array.prototype.slice.call(arguments) |
| var str = args.shift() |
| var cb = function () {} // start with noop. |
| |
| if (typeof args[args.length - 1] === 'function') cb = args.pop() |
| cb = cb || function () {} // noop. |
| |
| if (!this.cache[this.locale]) this._readLocaleFile() |
| |
| // we've observed a new string, update the language file. |
| if (!this.cache[this.locale][str] && this.updateFiles) { |
| this.cache[this.locale][str] = str |
| |
| // include the current directory and locale, |
| // since these values could change before the |
| // write is performed. |
| this._enqueueWrite([this.directory, this.locale, cb]) |
| } else { |
| cb() |
| } |
| |
| return util.format.apply(util, [this.cache[this.locale][str] || str].concat(args)) |
| } |
| |
| Y18N.prototype._taggedLiteral = function (parts) { |
| var args = arguments |
| var str = '' |
| parts.forEach(function (part, i) { |
| var arg = args[i + 1] |
| str += part |
| if (typeof arg !== 'undefined') { |
| str += '%s' |
| } |
| }) |
| return this.__.apply(null, [str].concat([].slice.call(arguments, 1))) |
| } |
| |
| Y18N.prototype._enqueueWrite = function (work) { |
| this.writeQueue.push(work) |
| if (this.writeQueue.length === 1) this._processWriteQueue() |
| } |
| |
| Y18N.prototype._processWriteQueue = function () { |
| var _this = this |
| var work = this.writeQueue[0] |
| |
| // destructure the enqueued work. |
| var directory = work[0] |
| var locale = work[1] |
| var cb = work[2] |
| |
| var languageFile = this._resolveLocaleFile(directory, locale) |
| var serializedLocale = JSON.stringify(this.cache[locale], null, 2) |
| |
| fs.writeFile(languageFile, serializedLocale, 'utf-8', function (err) { |
| _this.writeQueue.shift() |
| if (_this.writeQueue.length > 0) _this._processWriteQueue() |
| cb(err) |
| }) |
| } |
| |
| Y18N.prototype._readLocaleFile = function () { |
| var localeLookup = {} |
| var languageFile = this._resolveLocaleFile(this.directory, this.locale) |
| |
| try { |
| localeLookup = JSON.parse(fs.readFileSync(languageFile, 'utf-8')) |
| } catch (err) { |
| if (err instanceof SyntaxError) { |
| err.message = 'syntax error in ' + languageFile |
| } |
| |
| if (err.code === 'ENOENT') localeLookup = {} |
| else throw err |
| } |
| |
| this.cache[this.locale] = localeLookup |
| } |
| |
| Y18N.prototype._resolveLocaleFile = function (directory, locale) { |
| var file = path.resolve(directory, './', locale + '.json') |
| if (this.fallbackToLanguage && !this._fileExistsSync(file) && ~locale.lastIndexOf('_')) { |
| // attempt fallback to language only |
| var languageFile = path.resolve(directory, './', locale.split('_')[0] + '.json') |
| if (this._fileExistsSync(languageFile)) file = languageFile |
| } |
| return file |
| } |
| |
| // this only exists because fs.existsSync() "will be deprecated" |
| // see https://nodejs.org/api/fs.html#fs_fs_existssync_path |
| Y18N.prototype._fileExistsSync = function (file) { |
| try { |
| return fs.statSync(file).isFile() |
| } catch (err) { |
| return false |
| } |
| } |
| |
| Y18N.prototype.__n = function () { |
| var args = Array.prototype.slice.call(arguments) |
| var singular = args.shift() |
| var plural = args.shift() |
| var quantity = args.shift() |
| |
| var cb = function () {} // start with noop. |
| if (typeof args[args.length - 1] === 'function') cb = args.pop() |
| |
| if (!this.cache[this.locale]) this._readLocaleFile() |
| |
| var str = quantity === 1 ? singular : plural |
| if (this.cache[this.locale][singular]) { |
| str = this.cache[this.locale][singular][quantity === 1 ? 'one' : 'other'] |
| } |
| |
| // we've observed a new string, update the language file. |
| if (!this.cache[this.locale][singular] && this.updateFiles) { |
| this.cache[this.locale][singular] = { |
| one: singular, |
| other: plural |
| } |
| |
| // include the current directory and locale, |
| // since these values could change before the |
| // write is performed. |
| this._enqueueWrite([this.directory, this.locale, cb]) |
| } else { |
| cb() |
| } |
| |
| // if a %d placeholder is provided, add quantity |
| // to the arguments expanded by util.format. |
| var values = [str] |
| if (~str.indexOf('%d')) values.push(quantity) |
| |
| return util.format.apply(util, values.concat(args)) |
| } |
| |
| Y18N.prototype.setLocale = function (locale) { |
| this.locale = locale |
| } |
| |
| Y18N.prototype.getLocale = function () { |
| return this.locale |
| } |
| |
| Y18N.prototype.updateLocale = function (obj) { |
| if (!this.cache[this.locale]) this._readLocaleFile() |
| |
| for (var key in obj) { |
| this.cache[this.locale][key] = obj[key] |
| } |
| } |
| |
| module.exports = function (opts) { |
| var y18n = new Y18N(opts) |
| |
| // bind all functions to y18n, so that |
| // they can be used in isolation. |
| for (var key in y18n) { |
| if (typeof y18n[key] === 'function') { |
| y18n[key] = y18n[key].bind(y18n) |
| } |
| } |
| |
| return y18n |
| } |