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