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