├── .gitignore ├── .npmignore ├── test ├── locales │ ├── en.yml │ ├── pl.yml │ └── ru.json └── translator-gulp.js ├── package.json ├── translator-gulp.js ├── LICENSE ├── README.md └── lib └── translator.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | gulpfile.js 3 | node_modules 4 | app 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | test 4 | app 5 | -------------------------------------------------------------------------------- /test/locales/en.yml: -------------------------------------------------------------------------------- 1 | first: first 2 | title: "Title" 3 | errors: 4 | username: 5 | uniquness: "Username is not unique." 6 | valid: "Username is not valid." 7 | user: 8 | title: "ENGLISH USER TITLE" 9 | -------------------------------------------------------------------------------- /test/locales/pl.yml: -------------------------------------------------------------------------------- 1 | title: "Tytul" 2 | first: pierwszy 3 | errors: 4 | username: 5 | uniquness: "Nazwa uzytkownika nie jest unikalna" 6 | valid: "Nazwa uzytkownika jest niepoprawna" 7 | user: 8 | title: "POLSKI TYTUL" 9 | -------------------------------------------------------------------------------- /test/locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": "первый", 3 | "title": "Заголовок", 4 | "errors": { 5 | "username": { 6 | "uniquness": "Имя пользователя не уникально.", 7 | "valid": "Имя пользователя не валидно." 8 | } 9 | }, 10 | "user": { 11 | "title": "РУССКИЙ ЗАГОЛОВОК" 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-translator", 3 | "main": "translator-gulp.js", 4 | "version": "0.2.0", 5 | "description": "Translate your files by using {{ syntax }} and locales/something.yml or *.json files.", 6 | "licence": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/arathunku/gulp-translator" 10 | }, 11 | "keywords": [ 12 | "gulpplugin", 13 | "i18n", 14 | "translation", 15 | "locale", 16 | "html", 17 | "translate", 18 | "translator", 19 | "gulp" 20 | ], 21 | "author": "arathunku ", 22 | "dependencies": { 23 | "through2": "0.6.1", 24 | "gulp-util": "3.0.0", 25 | "q": "1.0.1", 26 | "yamljs": "^0.2.1" 27 | }, 28 | "devDependencies": { 29 | "gulp": "3.8.7", 30 | "mocha": "~1.17.0", 31 | "vinyl": "~0.2.3" 32 | }, 33 | "scripts": { 34 | "test": "./node_modules/.bin/mocha --reporter spec --timeout 100" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /translator-gulp.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var through = require('through2'); 3 | var gutil = require('gulp-util'); 4 | var PluginError = gutil.PluginError; 5 | 6 | var Translator = require("./lib/translator.js"); 7 | 8 | const PLUGIN_NAME = 'gulp-translator'; 9 | 10 | var plugin = function (options) { 11 | var translator = new Translator(options); 12 | 13 | return through.obj(function(file, enc, cb){ 14 | var self = this; 15 | 16 | if(file.isNull()) { 17 | this.push(file); 18 | return cb(); 19 | } 20 | 21 | if(file.isStream()) { 22 | this.emit('error', new PluginError(PLUGIN_NAME, 'Streaming not supported')); 23 | return cb(); 24 | } 25 | translator.translate(String(file.contents)).then(function(content){ 26 | file.contents = new Buffer(content); 27 | self.push(file); 28 | return cb(); 29 | 30 | }, function(error){ 31 | self.emit('error', new PluginError(PLUGIN_NAME, (error||'') + " and is used in " + file.path)); 32 | return cb(); 33 | }); 34 | }); 35 | }; 36 | 37 | module.exports = plugin; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{year}}} {{{fullname}}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gulp Translator 2 | > Almost like string replace but using locales 3 | Now you can use both .json and .yml files. 4 | 5 | ## Usage 6 | 7 | First, install `gulp-translator` as a development dependency: 8 | 9 | ```shell 10 | npm install --save-dev gulp-translator 11 | ``` 12 | 13 | Then, add it to your `gulpfile.js`: 14 | 15 | ```javascript 16 | var translate = require('gulp-translator'); 17 | 18 | gulp.task('translate', function() { 19 | var translations = ['pl', 'en']; 20 | 21 | translations.forEach(function(translation){ 22 | gulp.src('app/views/**/*.html') 23 | .pipe(translate(options)) 24 | .pipe(gulp.dest('dist/views/' + translation)); 25 | }); 26 | }); 27 | ``` 28 | 29 | or better, handle errors: 30 | ```javascript 31 | gulp.task('translate', function() { 32 | var translations = ['pl', 'en']; 33 | 34 | translations.forEach(function(translation){ 35 | gulp.src('app/views/**/*.html') 36 | .pipe( 37 | translate(options) 38 | .on('error', function(){ 39 | console.error(arguments); 40 | }) 41 | ) 42 | .pipe(gulp.dest('dist/views/' + translation)); 43 | }); 44 | }); 45 | ``` 46 | 47 | ## Options 48 | 49 | 50 | `options` in `translate` function is: 51 | * `String` Path to locale file. 52 | * `Object` 53 | * `.localePath` String. Optional. Path to locale file. 54 | Or you can use `.lang`, `.localeDirectory`. 55 | * `.lang` String. Optional. Target language. 56 | * `.localeDirectory` String. Optional. Directory with locale files. 57 | If no `.localePath` specified, try construct it from `.localeDirectory + .lang`. 58 | * `.localeExt` String. Optional. If you specify path to file will transform 59 | `newLocalePath = oldLocalePath + .localExt`. 60 | * `.pattern` RegExp. Optional. Pattern to find strings to replace. You can specify your own pattern. 61 | To transform strings without translate. 62 | Default: `/\{{2}([\w\.\s\"\']+\s?\|\s?translate[\w\s\|]*)\}{2}/g` 63 | * `.patternSplitter` String. Some to split parts of transform. Default: `'|'`. 64 | * `.transform` Object. Every field is you transform function. 65 | First argument is an `content` to transform it. 66 | Second is an dictionary, that you specified. 67 | Function should return transformed string or `Error` object with some message. 68 | 69 | 70 | 71 | ## Usage 72 | 73 | I'm using angular-like syntax. Expressions in `{{}}` with ` | translate ` 74 | filter will be translated. 75 | 76 | Following examples assume that "title" in locales equals "new TITLE" 77 | 78 | Example: 79 | ``` 80 | {{ title | translate }} will be change to "new TITLE" 81 | 82 | ``` 83 | If you'd like to use filters(look at the bottom to check available filters) just pass them after like that: 84 | 85 | ``` 86 | {{ title | translate | lowercase }} will be change to "new title" 87 | 88 | ``` 89 | 90 | 91 | ``` 92 | {{ title | translate | uppercase }} will be change to "NEW TITLE" 93 | 94 | ``` 95 | 96 | 97 | If you're still not sure, please look at tests. 98 | 99 | ## API 100 | 101 | gulp-translator is called with a string 102 | 103 | ### translate(string) 104 | 105 | #### string 106 | Type: `String` 107 | 108 | The string is a path to a nameOfTheFile.yml with your locales. Please look at test/locales for examples. 109 | 110 | ## Available filters: 111 | 112 | - lowercase 113 | - uppercase 114 | - capitalize to capitalize only first word. 115 | - capitalizeEvery to capitalize every word. 116 | - reverse 117 | 118 | ## User filters: 119 | 120 | You also can specify your own filters. 121 | Just add them to `.transform` parameter of `options`. 122 | First argument is an `content` to transform it. 123 | Second is an dictionary, that you specified. 124 | Function should return transformed string or `Error` object with some message. 125 | 126 | 127 | ## TODO: 128 | 129 | - refactor tests 130 | - work on matchers (sigh...) 131 | 132 | 133 | # License 134 | MIT 135 | -------------------------------------------------------------------------------- /lib/translator.js: -------------------------------------------------------------------------------- 1 | var YAML = require('yamljs'); 2 | var q = require('q'); 3 | var fs = require('fs'); 4 | 5 | var DEFAULT_REGEXP = /\{{2}([\w\.\s\"\']+\s?\|\s?translate[\w\s\|]*)\}{2}/g; 6 | 7 | 8 | var baseTransforms = { 9 | translate: function(content, dictionary){ 10 | if (!content) { 11 | return new Error('No content to transform (translate).'); 12 | } 13 | return content.trim().split('.').reduce(function(dict, key){ 14 | if (!dict) { 15 | return null; 16 | } 17 | return dict[key]; 18 | }, dictionary) || 19 | new Error('No such content (' + content + ') in locale file'); 20 | }, 21 | 22 | uppercase: function(content) { 23 | if (typeof content !== 'string') { 24 | return new Error('No content to transform (uppercase).'); 25 | } 26 | return content.toUpperCase(); 27 | }, 28 | 29 | lowercase: function(content) { 30 | if (typeof content !== 'string') { 31 | return new Error('No content to transform (lowercase).'); 32 | } 33 | return content.toLowerCase(); 34 | }, 35 | 36 | capitalize: function(content){ 37 | if (typeof content !== 'string') { 38 | return new Error('No content to transform (capitalize).'); 39 | } 40 | return content.charAt(0).toUpperCase() + content.substring(1).toLowerCase(); 41 | }, 42 | 43 | capitalizeEvery: function(content){ 44 | if (typeof content !== 'string') { 45 | return new Error('No content to transform (capitalizeEvery).'); 46 | } 47 | return content.toLowerCase().replace(/\b(.)/g, function(equals, word){ 48 | return baseTransforms.capitalize(word); 49 | }); 50 | }, 51 | 52 | reverse: function(content){ 53 | if (typeof content !== 'string') { 54 | return new Error('No content to transform (reverse).'); 55 | } 56 | return content.split('').reduceRight(function(result, letter){ 57 | return result + letter; 58 | }, ''); 59 | } 60 | 61 | }; 62 | 63 | var getDictionary = function(path){ 64 | var dictionary; 65 | 66 | if (path.match(/\.json$/)) { 67 | dictionary = JSON.parse( 68 | fs.readFileSync(path,{ 69 | encoding: 'utf8' 70 | }) 71 | ); 72 | } 73 | else if (path.match(/\.yml$/)){ 74 | dictionary = YAML.load(path); 75 | } 76 | if (!dictionary){ 77 | try{ 78 | dictionary = JSON.parse( 79 | fs.readFileSync(path+'.json',{ 80 | encoding: 'utf8' 81 | }) 82 | ); 83 | } 84 | catch(e){ 85 | dictionary = YAML.load(path + '.yml'); 86 | } 87 | } 88 | return dictionary; 89 | }; 90 | 91 | 92 | 93 | module.exports = (function() { 94 | var Translator = function(options){ 95 | options = options || {}; 96 | this.pattern = options.pattern || DEFAULT_REGEXP; 97 | this.patternSplitter = options.patternSplitter || '|'; 98 | this.userTransform = options.transform || {}; 99 | 100 | if (typeof options === 'string'){ 101 | var lang = options.match(/([\.^\/]*)\.\w{0,4}$/); 102 | this.lang = lang && lang[0] || 'undefined'; 103 | this.localePath = options; 104 | } 105 | else { 106 | 107 | this.lang = options.lang; 108 | this.localePath = options.localePath; 109 | this.localeDirectory = options.localeDirectory; 110 | this.localeExt = options.localeExt; 111 | 112 | if (!this.localePath){ 113 | this.localePath = this.localeDirectory + this.lang; 114 | } 115 | 116 | if (this.localeExt) { 117 | this.localePath += this.localeExt; 118 | } 119 | } 120 | 121 | this.dictionary = getDictionary(this.localePath); 122 | return this; 123 | }; 124 | 125 | Translator.prototype.translate = function(content) { 126 | var resultPromise = q.defer(); 127 | var self = this; 128 | 129 | resultPromise.resolve(content.replace(self.pattern, function (s, data) { 130 | 131 | var transforms = data.split(self.patternSplitter).map(function(filter){ 132 | return filter.trim(); 133 | }); 134 | 135 | var value = transforms.splice(0, 1)[0]; 136 | 137 | var res = transforms.reduce(function(res, transformName){ 138 | if (res instanceof Error){ 139 | return res; 140 | } 141 | if (typeof self.userTransform[transformName] === 'function') { 142 | return self.userTransform[transformName](res, self.dictionary); 143 | } 144 | else if (typeof baseTransforms[transformName] === 'function') { 145 | return baseTransforms[transformName](res, self.dictionary); 146 | } else { 147 | return new Error(transformName + ' filter is not supported'); 148 | } 149 | }, value); 150 | 151 | if (res instanceof Error) { 152 | return resultPromise.reject(res); 153 | } 154 | 155 | return res; 156 | 157 | }, content)); 158 | 159 | return resultPromise.promise; 160 | }; 161 | 162 | return Translator; 163 | }()); 164 | -------------------------------------------------------------------------------- /test/translator-gulp.js: -------------------------------------------------------------------------------- 1 | var File = require('vinyl'); 2 | var through = require('through2'); 3 | var assert = require('assert'); 4 | var Stream = require('stream'); 5 | 6 | var gulpTranslator = require('../translator-gulp.js'); 7 | 8 | describe('gulp-translator', function() { 9 | describe('with null contents', function() { 10 | it('should let null files pass through', function(done) { 11 | var translator = gulpTranslator('./test/locales/en.yml'); 12 | var n = 0; 13 | 14 | var _transform = function(file, enc, callback) { 15 | assert.equal(file.contents, null); 16 | n++; 17 | callback(); 18 | }; 19 | 20 | var _flush = function(callback) { 21 | assert.equal(n, 1); 22 | done(); 23 | callback(); 24 | }; 25 | 26 | var t = through.obj(_transform, _flush); 27 | translator.pipe(t); 28 | translator.end(new File({ 29 | contents: null 30 | })); 31 | }); 32 | }); 33 | 34 | describe('with buffer contents', function() { 35 | it('should interpolate strings from *.yml locale file - ENGLISH', function(done) { 36 | var translator = gulpTranslator('./test/locales/en.yml'); 37 | var n = 0; 38 | var content = new Buffer("{{ user.title | translate }} {{title | translate}}"); 39 | var translated = "ENGLISH USER TITLE Title"; 40 | 41 | var _transform = function(file, enc, callback) { 42 | assert.equal(file.contents.toString('utf8'), translated); 43 | n++; 44 | callback(); 45 | }; 46 | 47 | var _flush = function(callback) { 48 | assert.equal(n, 1); 49 | done(); 50 | callback(); 51 | }; 52 | 53 | var t = through.obj(_transform, _flush); 54 | translator.pipe(t); 55 | translator.end(new File({ 56 | contents: content 57 | })); 58 | }); 59 | 60 | it('should interpolate strings from *.yml locale file - POLISH', function(done) { 61 | var translator = gulpTranslator('./test/locales/pl.yml'); 62 | var n = 0; 63 | var content = new Buffer("{{ user.title | translate }} {{title | translate}}"); 64 | var translated = "POLSKI TYTUL Tytul"; 65 | 66 | var _transform = function(file, enc, callback) { 67 | assert.equal(file.contents.toString('utf8'), translated); 68 | n++; 69 | callback(); 70 | }; 71 | 72 | var _flush = function(callback) { 73 | assert.equal(n, 1); 74 | done(); 75 | callback(); 76 | }; 77 | 78 | var t = through.obj(_transform, _flush); 79 | translator.pipe(t); 80 | translator.end(new File({ 81 | contents: content 82 | })); 83 | }); 84 | 85 | it('should interpolate strings from *.json locale file - RUSSIAN', function(done) { 86 | var translator = gulpTranslator('./test/locales/ru.json'); 87 | var n = 0; 88 | var content = new Buffer("{{ user.title | translate }} {{title | translate}}"); 89 | var translated = "РУССКИЙ ЗАГОЛОВОК Заголовок"; 90 | 91 | var _transform = function(file, enc, callback) { 92 | assert.equal(file.contents.toString('utf8'), translated); 93 | n++; 94 | callback(); 95 | }; 96 | 97 | var _flush = function(callback) { 98 | assert.equal(n, 1); 99 | done(); 100 | callback(); 101 | }; 102 | 103 | var t = through.obj(_transform, _flush); 104 | translator.pipe(t); 105 | translator.end(new File({ 106 | contents: content 107 | })); 108 | }); 109 | 110 | it("should throw error about undefined locale", function(done){ 111 | var translator = gulpTranslator('./test/locales/pl.yml'); 112 | var n = 0; 113 | var content = new Buffer("{{ unsupported | translate }}"); 114 | 115 | var _transform = function(file, enc, callback) { 116 | n++; 117 | callback(); 118 | }; 119 | 120 | var _flush = function(callback) { 121 | assert.equal(n, 0); 122 | callback(); 123 | }; 124 | 125 | 126 | translator.on('error', function(err){ 127 | assert.equal(err.message, 128 | 'Error: No such content (unsupported) in locale file and is used in /path'); 129 | done(); 130 | }); 131 | 132 | var t = through.obj(_transform, _flush); 133 | translator.pipe(t); 134 | translator.end(new File({ 135 | path: '/path', 136 | contents: content 137 | })); 138 | }); 139 | 140 | describe('filters', function() { 141 | it("should lowecase translated text", function(done){ 142 | var translator = gulpTranslator('./test/locales/en.yml'); 143 | var n = 0; 144 | var content = new Buffer("{{ title | translate }} {{title | translate | lowercase}}"); 145 | var translated = "Title title"; 146 | 147 | var _transform = function(file, enc, callback) { 148 | assert.equal(file.contents.toString('utf8'), translated); 149 | n++; 150 | callback(); 151 | done(); 152 | }; 153 | 154 | var _flush = function(callback) { 155 | assert.equal(n, 1); 156 | callback(); 157 | }; 158 | 159 | var t = through.obj(_transform, _flush); 160 | translator.pipe(t); 161 | translator.end(new File({ 162 | contents: content 163 | })); 164 | }); 165 | 166 | it("should uppercase translated text", function(done){ 167 | var translator = gulpTranslator('./test/locales/en.yml'); 168 | var n = 0; 169 | var content = new Buffer("{{ title | translate }} {{title | translate | uppercase}}"); 170 | var translated = "Title TITLE"; 171 | 172 | var _transform = function(file, enc, callback) { 173 | assert.equal(file.contents.toString('utf8'), translated); 174 | n++; 175 | callback(); 176 | }; 177 | 178 | var _flush = function(callback) { 179 | assert.equal(n, 1); 180 | done(); 181 | callback(); 182 | }; 183 | 184 | var t = through.obj(_transform, _flush); 185 | translator.pipe(t); 186 | translator.end(new File({ 187 | contents: content 188 | })); 189 | }); 190 | 191 | it("should capitalize translated text", function(done){ 192 | var translator = gulpTranslator('./test/locales/en.yml'); 193 | var n = 0; 194 | var content = new Buffer("{{ title | translate }} {{user.title | translate | capitalize}}"); 195 | var translated = "Title English user title"; 196 | 197 | var _transform = function(file, enc, callback) { 198 | assert.equal(file.contents.toString('utf8'), translated); 199 | n++; 200 | callback(); 201 | done(); 202 | }; 203 | 204 | var _flush = function(callback) { 205 | assert.equal(n, 1); 206 | callback(); 207 | }; 208 | 209 | var t = through.obj(_transform, _flush); 210 | translator.pipe(t); 211 | translator.end(new File({ 212 | contents: content 213 | })); 214 | }); 215 | 216 | it("should capitalize every word in translated text", function(done){ 217 | var translator = gulpTranslator('./test/locales/en.yml'); 218 | var n = 0; 219 | var content = new Buffer("{{ title | translate }} {{user.title | translate | capitalizeEvery}}"); 220 | var translated = "Title English User Title"; 221 | 222 | var _transform = function(file, enc, callback) { 223 | assert.equal(file.contents.toString('utf8'), translated); 224 | n++; 225 | callback(); 226 | done(); 227 | }; 228 | 229 | var _flush = function(callback) { 230 | assert.equal(n, 1); 231 | callback(); 232 | }; 233 | 234 | var t = through.obj(_transform, _flush); 235 | translator.pipe(t); 236 | translator.end(new File({ 237 | contents: content 238 | })); 239 | }); 240 | 241 | it("should reverse translated text", function(done){ 242 | var translator = gulpTranslator('./test/locales/en.yml'); 243 | var n = 0; 244 | var content = new Buffer("{{ title | translate }} {{user.title | translate | reverse}}"); 245 | var translated = "Title ELTIT RESU HSILGNE"; 246 | 247 | var _transform = function(file, enc, callback) { 248 | assert.equal(file.contents.toString('utf8'), translated); 249 | n++; 250 | callback(); 251 | done(); 252 | }; 253 | 254 | var _flush = function(callback) { 255 | assert.equal(n, 1); 256 | callback(); 257 | }; 258 | 259 | var t = through.obj(_transform, _flush); 260 | translator.pipe(t); 261 | translator.end(new File({ 262 | contents: content 263 | })); 264 | }); 265 | 266 | it("should throw error if unsupported filter", function(done){ 267 | var translator = gulpTranslator('./test/locales/en.yml'); 268 | var n = 0; 269 | var content = new Buffer("{{ title | translate }} {{title | translate | unsupported}}"); 270 | 271 | var _transform = function(file, enc, callback) { 272 | assert.equal(file.contents.toString('utf8'), translated); 273 | n++; 274 | callback(); 275 | }; 276 | 277 | var _flush = function(callback) { 278 | assert.equal(n, 0); 279 | callback(); 280 | }; 281 | 282 | translator.on('error', function(err){ 283 | assert.equal(err.message, 'Error: unsupported filter is not supported and is used in /path'); 284 | done(); 285 | }); 286 | 287 | var t = through.obj(_transform, _flush); 288 | translator.pipe(t); 289 | translator.end(new File({ 290 | path: '/path', 291 | contents: content 292 | })); 293 | }); 294 | }); 295 | }); 296 | 297 | describe('with stream contents', function() { 298 | it('should emit errors', function(done) { 299 | var translator = gulpTranslator('./test/locales/en.yml'); 300 | var content = Readable("{{ title }} {{title}}"); 301 | 302 | var n = 0; 303 | 304 | var _transform = function(file, enc, callback) { 305 | n++; 306 | callback(); 307 | }; 308 | 309 | var _flush = function(callback) { 310 | assert.equal(n, 0); 311 | callback(); 312 | }; 313 | 314 | translator.on('error', function(err){ 315 | assert.equal(err.message, "Streaming not supported"); 316 | done(); 317 | }); 318 | 319 | var t = through.obj(_transform, _flush); 320 | 321 | translator.pipe(t); 322 | translator.end(new File({ 323 | contents: content 324 | })); 325 | }); 326 | }); 327 | }); 328 | 329 | function Readable(content, cb){ 330 | var readable = new Stream.Readable(); 331 | readable._read = function() { 332 | this.push(new Buffer(content)); 333 | this.push(null); // no more data 334 | }; 335 | if (cb) readable.on('end', cb); 336 | return readable; 337 | } 338 | --------------------------------------------------------------------------------