diff --git a/index.js b/index.js index a6350ed..c1c934b 100644 --- a/index.js +++ b/index.js @@ -34,7 +34,10 @@ var defaults = { filename: '${path}/${name}-${lang}.${ext}', blacklist: [], warn: true, - cache: true + cache: true, + ignoreErrors: false, + dryRun: false, + includeOriginal: false }; /** @@ -45,31 +48,32 @@ function load(options) { if (cache[options.locales]) { dictionaries = cache[options.locales]; } else { - var files = fs.readdirSync(options.locales); - for (var i in files) { - var file = files[i]; - switch (path.extname(file)) { - case '.json': - case '.js': - dictionaries[path.basename(file, path.extname(file))] = flat(require(path.join(process.cwd(), options.locales, file))); - break; - case '.ini': - var iniData = fs.readFileSync(path.join(process.cwd(), options.locales, file)); - dictionaries[path.basename(file, path.extname(file))] = flat(ini2json(iniData)); - break; - case '.csv': - var csvData = fs.readFileSync(path.join(process.cwd(), options.locales, file)); - dictionaries[path.basename(file, path.extname(file))] = csv2json(csvData); - break; + try { + var files = fs.readdirSync(options.locales); + for (var i in files) { + var file = files[i]; + switch (path.extname(file)) { + case '.json': + case '.js': + dictionaries[path.basename(file, path.extname(file))] = flat(require(path.join(process.cwd(), options.locales, file))); + break; + case '.ini': + var iniData = fs.readFileSync(path.join(process.cwd(), options.locales, file)); + dictionaries[path.basename(file, path.extname(file))] = flat(ini2json(iniData)); + break; + case '.csv': + var csvData = fs.readFileSync(path.join(process.cwd(), options.locales, file)); + dictionaries[path.basename(file, path.extname(file))] = csv2json(csvData); + break; + } } if (options.cache) { cache[options.locales] = dictionaries; } + } catch (e) { + throw new Error('No translation dictionaries have been found!'); } } - if (!Object.keys(dictionaries).length) { - throw new Error('No translation dictionaries have been found!'); - } } /** @@ -305,11 +309,20 @@ module.exports = function (options) { try { var files = replace(file, options); - for (var i in files) { - this.push(files[i]); + if (options.dryRun) { + this.push(file); + } else { + if (options.includeOriginal) { + this.push(file); + } + for (var i in files) { + this.push(files[i]); + } } } catch (err) { - this.emit('error', new gutil.PluginError('gulp-international', err)); + if (!options.ignoreErrors) { + this.emit('error', new gutil.PluginError('gulp-international', err)); + } } cb(); diff --git a/package.json b/package.json index 11e5459..3553d28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gulp-international", - "version": "0.0.5", + "version": "1.0.0", "description": "A gulp plugin that creates multi language versions of your source files", "license": "Apache-2.0", "homepage": "http://github.com/mallocator/gulp-international", diff --git a/readme.md b/readme.md index 6c65106..477d581 100644 --- a/readme.md +++ b/readme.md @@ -18,6 +18,7 @@ Features (cause we all love features): * Read from .json, .js, .ini or .csv file (for examples check the test folder) + ## Install ``` @@ -25,6 +26,7 @@ $ npm install --save-dev gulp-international ``` + ## Usage ```js @@ -38,9 +40,26 @@ gulp.task('default', function () { }); ``` +Or with your custom options of the out of box config dones't work: + +```js +var gulp = require('gulp'); +var international = require('gulp-international'); + +gulp.task('default', function () { + return gulp.src('src/file.ext') + .pipe(international({ + whitelist: ['en_US'] + })) + .pipe(gulp.dest('dist')); +}); +``` + + ## Options + ### locales Type: string (path) @@ -102,6 +121,7 @@ This option allows to limit the number of translations that can be used. The nam (without the extension). Any other files will still be loaded into the list of available dictionaries, but no files will be generated. The option is ignored if it is missing. + ### blacklist Type: Array(string) @@ -127,6 +147,118 @@ This enables caching of dictionaries, so that reruns of the plugin will not have on your configuration you might want to disable caching based on how your livereloads are configured. +### ignoreErrors + +Type: boolean +Default: ```false``` + +Allows to disable throwing of any errors that might occur so that the pipe will continue executing. + + +### dryRun + +Type: boolean +Default: ```false``` + +When set to true the plugin will perform all operations for a translation, but will pass on the original file along the pipe instead +of the newly generated ones. + + +### includeOriginal + +Type: boolean +Default: ```false``` + +When set to true the original file is passed along the pipe as well along with all translated files. + + + +## Translation Files + +A number of formats are supported from where we can read translations: + + +### JSON + +``` +{ + "token1": "translation1", + "section1": { + "token2": "translation2", + "subsection1": { + "token3": "translation3" + } + } +} +``` + +This results in 3 tokens that look like this (With default delimiter settings): + +``` +R.token1 = translation1 +R.section1.token2 = translation2 +R.section1.subsection1.token3 = translation3 +``` + + +### JS + +Similar to JSON you can just use node.js module and export your json: + +``` +module.exports = { + token1: 'translation1', + section1: { + token2: 'translation2', + subsection1: { + token3: 'translation3' + } + } +} +``` + +The tokens map the same way as a JSON file would. + + +### CSV + +In this format all except for the last readable field will make up the token. The last field will be the translation. +To get the same output as with the previous examples the file would look something like this: + +``` +token1,,,translation1 +token2,section1,,translation2 +token3,section1,subsection1,translation3 +``` + +This library doesn't really check for a correct CSV format and also doesn't assume a header line. It just scans for fields +that it can use to build it's keys. Another valid format to read translations would be this non standard csv file: + +``` +token1,translation1 +token2,section1,translation2 +token3,section1,subsection1,translation3 +``` + +If you have fields with ```,``` in them you can escape them ```"```, which in turn can also be escaped using ```\"```. + + +### INI + +The standard .ini format is supported as well, but only supports one level of nesting. You can simulate multiple levels though: + +``` +token1=translation1 + +[section1] +token2=translation2 + +[section1.subsection1] +token3=translation3 +``` + + + ## Feature Ideas for the future Maybe I'll implement these one day, maybe not. @@ -134,4 +266,5 @@ Maybe I'll implement these one day, maybe not. * Replace links to standard versions with internationalized versions (can probably also just be done with using tokens for imports) * Extract non code strings from source and list them as still missing translations * Warn about unused translation strings - * Make translations available as environment variables in jade/js/coffeescript/etc. + * Make translations available as environment variables in jade/js/coffeescript/etc. (although you can already replace strings anywhere) + * Support streams... although that seems like a pain to implement diff --git a/test/index.test.js b/test/index.test.js index 4f14195..6b0b2c7 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -9,42 +9,6 @@ var File = require('vinyl'); var plugin = require('../'); -/** - * Performs common tasks that need to be run for every test. Makes setting up and understanding tests way easier. - */ -function helper() { - var options = {}; - var content = '

R.token1

'; - var validatorCb; - for(let param of arguments) { - if (_.isString(param)) { - content = param - } else if (_.isFunction(param)) { - validatorCb = param; - } else if (_.isObject(param)) { - options = param; - } - } - expect(validatorCb).to.be.a('function'); - expect(content).to.be.a('string'); - - var stream = plugin(options); - var files = []; - stream.on('data', file => { - files.push(file); - }); - stream.on('finish', () => { - validatorCb(files, plugin.options); - }); - stream.write(new File({ - path: 'test/helloworld.html', - cwd: 'test/', - base: 'test/', - contents: new Buffer(content, 'utf8') - })); - stream.end(); -} - describe('gulp-international', () => { describe('Usage Examples', () => { @@ -134,6 +98,29 @@ describe('gulp-international', () => { done(); }); }); + + + it('should pass on the original file if this is just a dry run', done => { + var options = { locales: 'test/locales', dryRun: true }; + helper(options, files => { + expect(files.length).to.equal(1); + expect(files[0].contents.toString('utf8')).to.equal('

R.token1

'); + expect(files[0].path).to.equal('test/helloworld.html'); + done(); + }); + }); + + it ('should include the original file as well as all translations', done => { + var options = { locales: 'test/locales', includeOriginal: true }; + helper(options, files => { + expect(files.length).to.equal(5); + expect(files[0].contents.toString('utf8')).to.equal('

R.token1

'); + expect(files[0].path).to.equal('test/helloworld.html'); + expect(files[1].contents.toString('utf8')).to.equal('

Inhalt1

'); + expect(files[1].path).to.equal('test/helloworld-de_DE.html'); + done(); + }); + }); }); describe('Error cases', () => { @@ -168,12 +155,12 @@ describe('gulp-international', () => { expect.fail(); } catch (e) { expect(e).to.be.an('Error'); - expect(e.code).to.equal('ENOENT'); + expect(e.message).to.equal('No translation dictionaries have been found!'); } }); - it('should throw an error if a file is a stream', () => { + it('should throw an error if a file is a stream', done => { var stream = plugin({ locales: 'test/locales' }); try { stream.write(new File({ @@ -185,20 +172,35 @@ describe('gulp-international', () => { expect.fail(); } catch (e) { expect(e.message).to.equal('Streaming not supported'); + done(); } }); - it('should throw an error if no dictionaries have been found', () => { + it('should throw an error if no dictionaries have been found', done => { var options = { - locales: 'test/notlocales' + locales: 'test/notlocales', + cache: false }; try { helper(options, files => {}); expect.fail(); } catch (e) { - expect(e).to.be.an('Error'); - expect(e.code).to.equal('ENOENT'); + expect(e.message).to.equal('No translation dictionaries have been found!'); + done(); + } + }); + + + it('should throw an error if no existing dictionaries have been whitelisted', () => { + var options = { + locales: 'test/locales', + whitelist: 'es_ES' + }; + try{ + helper(options, files => {}); + } catch(e) { + expect(e.message).to.equal('No translation dictionaries available to create any files!'); } }); @@ -218,6 +220,26 @@ describe('gulp-international', () => { }); + it('shouldn just continue if the passed in file is null', () => { + var stream = plugin({ locales: 'test/locales'}); + stream.write(null); + }); + + + it('should ignore errors if ignoreErrors is set', () => { + var options = { + locales: 'test/locales', + whitelist: 'es_ES', + ignoreErrors: true + }; + try{ + helper(options, files => {}); + } catch(e) { + expect.fail('No Error should have been thrown.'); + } + }); + + it('should be able to process a larger file with multiple replacements', done => { var content = ` @@ -320,3 +342,39 @@ describe('gulp-international', () => { }); }); }); + +/** + * Performs common tasks that need to be run for every test. Makes setting up and understanding tests way easier. + */ +function helper() { + var options = {}; + var content = '

R.token1

'; + var validatorCb; + for(let param of arguments) { + if (_.isString(param)) { + content = param + } else if (_.isFunction(param)) { + validatorCb = param; + } else if (_.isObject(param)) { + options = param; + } + } + expect(validatorCb).to.be.a('function'); + expect(content).to.be.a('string'); + + var stream = plugin(options); + var files = []; + stream.on('data', file => { + files.push(file); + }); + stream.on('finish', () => { + validatorCb(files, plugin.options); + }); + stream.write(new File({ + path: 'test/helloworld.html', + cwd: 'test/', + base: 'test/', + contents: new Buffer(content, 'utf8') + })); + stream.end(); +}