diff --git a/Gruntfile.js b/Gruntfile.js index 51f9e23..8ae8e75 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -201,8 +201,15 @@ module.exports = function(grunt) { 'tt-default="\'((?:\\\\.|[^\'\\\\])*)\'\\|translate"' ], dest: 'tmp' - } + }, + extract_to_pot: { + adapter: 'pot', + prefix: 'template', + src: [ 'test/fixtures/*.html', 'test/fixtures/*.js' ], + lang: [ '' ], + dest: 'tmp' + } }, // Unit tests. @@ -233,7 +240,7 @@ module.exports = function(grunt) { // Whenever the "test" task is run, first clean the "tmp" dir, then run this // plugin's task(s), then test the result. - grunt.registerTask('test', ['clean', 'i18nextract', 'nodeunit', 'clean']); + grunt.registerTask('test', ['clean', 'i18nextract', 'nodeunit']); // By default, lint and run all tests. grunt.registerTask('default', ['test']); diff --git a/package.json b/package.json index 262727d..6d3758e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "grunt-contrib-clean": "^0.5.0", "grunt-contrib-copy": "^0.5.0", "grunt-contrib-jshint": "^0.10.0", - "grunt-contrib-nodeunit": "~0.1.2", + "grunt-contrib-nodeunit": "~0.4.1", "grunt-markdox": "^0.1.0" }, "peerDependencies": { @@ -53,7 +53,8 @@ "readmeFilename": "README.md", "dependencies": { "flat": "^1.2.0", + "json-stable-stringify": "^1.0.0", "lodash": "~2.4.1", - "json-stable-stringify": "^1.0.0" + "pofile": "^0.2.12" } } diff --git a/tasks/angular-translate.js b/tasks/angular-translate.js index a5afdb8..8e086ca 100644 --- a/tasks/angular-translate.js +++ b/tasks/angular-translate.js @@ -29,33 +29,16 @@ module.exports = function (grunt) { dest = this.data.dest || '.', jsonSrc = _file.expand(this.data.jsonSrc || []), jsonSrcName = _.union(this.data.jsonSrcName || [], ['label']), - defaultLang = this.data.defaultLang || '.', interpolation = this.data.interpolation || {startDelimiter: '{{', endDelimiter: '}}'}, - source = this.data.source || '', nullEmpty = this.data.nullEmpty || false, namespace = this.data.namespace || false, prefix = this.data.prefix || '', safeMode = this.data.safeMode ? true : false, - suffix = this.data.suffix || '.json', + suffix = this.data.suffix, customRegex = _.isArray(this.data.customRegex) ? this.data.customRegex : [], - stringify_options = this.data.stringifyOptions || null, + adapter = this.data.adapter || 'json', results = {}; - var customStringify = function (val) { - if (stringify_options) { - return stringify(val, _.isObject(stringify_options) ? stringify_options : { - space: ' ', - cmp: function (a, b) { - var lower = function (a) { - return a.toLowerCase(); - }; - return lower(a.key) < lower(b.key) ? -1 : 1; - } - }); - } - return JSON.stringify(val, null, 4); - }; - // Use to escape some char into regex patterns var escapeRegExp = function (str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); @@ -286,51 +269,29 @@ module.exports = function (grunt) { "nullEmpty": nullEmpty }, results); - // Build all output langage files - this.data.lang.forEach(function (lang) { - - var destFilename = dest + '/' + prefix + lang + suffix, - filename = source, - translations = {}, - json = {}; - - // Test source filename - if (filename === '' || !_file.exists(filename)) { - filename = destFilename; - } - - _log.subhead('Process ' + lang + ' : ' + filename); - - var isDefaultLang = (defaultLang === lang); - if (!_file.exists(filename)) { - _log.debug('File doesn\'t exist'); - - _log.writeln('Create file: ' + destFilename + (isDefaultLang ? ' (' + lang + ' is the default language)' : '')); - translations = _translation.getMergedTranslations({}, isDefaultLang); - - } else { - _log.debug('File exist'); - json = _file.readJSON(filename); - translations = _translation.getMergedTranslations(Translations.flatten(json), isDefaultLang); - } - - var stats = _translation.getStats(); - var statEmptyType = nullEmpty ? "null" : "empty"; - var statPercentage = Math.round(stats[statEmptyType] / stats["total"] * 100); - statPercentage = isNaN(statPercentage) ? 100 : statPercentage; - var statsString = "Statistics : " + - statEmptyType + ": " + stats[statEmptyType] + " (" + statPercentage + "%)" + - " / Updated: " + stats["updated"] + - " / Deleted: " + stats["deleted"] + - " / New: " + stats["new"]; - - _log.writeln(statsString); - - // Write JSON file for lang - _file.write(destFilename, customStringify(translations)); - - }); + // Prepare some params to pass to the adapter + var params = { + lang: this.data.lang, + dest: dest, + prefix: prefix, + suffix: suffix, + source: this.data.source, + defaultLang: this.data.defaultLang, + stringifyOptions: this.data.stringifyOptions + }; + switch(adapter) { + case 'pot': + var PotAdapter = require('./lib/pot-adapter.js'); + var toPot = new PotAdapter(grunt); + toPot.init(params); + _translation.persist(toPot); + break; + default: + var JsonAdapter = require('./lib/json-adapter.js'); + var toJson = new JsonAdapter(grunt); + toJson.init(params); + _translation.persist(toJson); + } }); - }; diff --git a/tasks/lib/json-adapter.js b/tasks/lib/json-adapter.js new file mode 100644 index 0000000..814a6bb --- /dev/null +++ b/tasks/lib/json-adapter.js @@ -0,0 +1,89 @@ +/** + * grunt-angular-translate + * https://github.com/firehist/grunt-angular-translate + * + * Copyright (c) 2013 "firehist" Benjamin Longearet, contributors + * Licensed under the MIT license. + * + */ + +(function() { + 'use strict'; + + var _log, _file; + var Utils = require('./utils.js'); + var Translations = require('./translations.js'); + + function JsonAdapter(grunt) { + _log = grunt.log; + _file = grunt.file; + } + + JsonAdapter.prototype.init = function(params) { + this.dest = params.dest || '.'; + this.lang = params.lang; + this.prefix = params.prefix; + this.suffix = params.suffix || '.json'; + this.source = params.source; + this.defaultLang = params.defaultLang; + this.stringifyOptions = params.stringifyOptions; + }; + + JsonAdapter.prototype.persist = function(_translation) { + var lang = this.lang; + var dest = this.dest; + var prefix = this.prefix; + var suffix = this.suffix; + var source = this.source || ''; + var defaultLang = this.defaultLang || '.'; + var stringify_options = this.stringifyOptions || null; + + // Build all output language files + lang.forEach(function (lang) { + + var destFilename = dest + '/' + prefix + lang + suffix, + filename = source, + translations = {}, + json = {}; + + // Test source filename + if (filename === '' || !_file.exists(filename)) { + filename = destFilename; + } + + _log.subhead('Process ' + lang + ' : ' + filename); + + var isDefaultLang = (defaultLang === lang); + if (!_file.exists(filename)) { + _log.debug('File doesn\'t exist'); + + _log.writeln('Create file: ' + destFilename + (isDefaultLang ? ' (' + lang + ' is the default language)' : '')); + translations = _translation.getMergedTranslations({}, isDefaultLang); + + } else { + _log.debug('File exist'); + json = _file.readJSON(filename); + translations = _translation.getMergedTranslations(Translations.flatten(json), isDefaultLang); + } + + var stats = _translation.getStats(); + var statEmptyType = _translation.params.nullEmpty ? "null" : "empty"; + var statPercentage = Math.round(stats[statEmptyType] / stats["total"] * 100); + statPercentage = isNaN(statPercentage) ? 100 : statPercentage; + var statsString = "Statistics : " + + statEmptyType + ": " + stats[statEmptyType] + " (" + statPercentage + "%)" + + " / Updated: " + stats["updated"] + + " / Deleted: " + stats["deleted"] + + " / New: " + stats["new"]; + + _log.writeln(statsString); + + // Write JSON file for lang + var utils = new Utils(); + _file.write(destFilename, utils.customStringify(translations, stringify_options)); + + }); + }; + + module.exports = JsonAdapter; +}()); \ No newline at end of file diff --git a/tasks/lib/pot-adapter.js b/tasks/lib/pot-adapter.js new file mode 100644 index 0000000..9658dff --- /dev/null +++ b/tasks/lib/pot-adapter.js @@ -0,0 +1,62 @@ +/** + * grunt-angular-translate + * https://github.com/firehist/grunt-angular-translate + * + * Copyright (c) 2015 "originof" Manuel Mazzuola, contributors + * Licensed under the MIT license. + * + */ + +(function() { + 'use strict'; + + var _file; + var Po = require('pofile'); + + function PotObject(id, msg, ctx) { + this.id = id; + this.msg = msg || ''; + this.ctx = ctx || ''; + } + + PotObject.prototype.toString = function() { + return "" + + "msgctxt \"" + String(this.ctx).replace(/"/g, '\\"') + "\"\n" + + "msgid \"" + String(this.id).replace(/"/g, '\\"') + "\"\n" + + "msgstr \"" + String(this.msg).replace(/"/g, '\\"') + "\""; + }; + + function PotAdapter(grunt) { + _file = grunt.file; + } + + PotAdapter.prototype.init = function(params) { + this.dest = params.dest || '.'; + this.prefix = params.prefix; + this.suffix = params.suffix || '.pot'; + }; + + PotAdapter.prototype.persist = function(_translation) { + var translations = _translation.getMergedTranslations({}); + var catalog = new Po(); + + catalog.headers = { + 'Content-Type': 'text/plain; charset=UTF-8', + 'Content-Transfer-Encoding': '8bit', + 'Project-Id-Version': '' + }; + + for (var msg in translations) { + catalog.items.push(new PotObject(msg, translations[msg])); + } + + catalog.items.sort(function(a, b) { + return a.id.toLowerCase().localeCompare(b.id.toLowerCase()); + }); + + _file.write(this.dest + '/' + this.prefix + this.suffix, catalog.toString()); + }; + + module.exports = PotAdapter; +}()); + diff --git a/tasks/lib/translations.js b/tasks/lib/translations.js index 94b50d2..0b47184 100644 --- a/tasks/lib/translations.js +++ b/tasks/lib/translations.js @@ -292,6 +292,14 @@ Translations.prototype.incStat = function (type) { this._stats[type]++; } } + +/** + * Call the adapter to persist to disk + * @param {Function} adapter Function to call + */ +Translations.prototype.persist = function (adapter) { + adapter.persist(this); +} /** * Wrap of flat.flatten method * @type {Function} diff --git a/tasks/lib/utils.js b/tasks/lib/utils.js new file mode 100644 index 0000000..10c8b43 --- /dev/null +++ b/tasks/lib/utils.js @@ -0,0 +1,26 @@ +(function() { + 'use strict'; + + var _ = require('lodash'); + var stringify = require('json-stable-stringify'); + + function Utils() { + } + + Utils.prototype.customStringify = function(val, options) { + if (options) { + return stringify(val, _.isObject(options) ? options : { + space: ' ', + cmp: function (a, b) { + var lower = function (a) { + return a.toLowerCase(); + }; + return lower(a.key) < lower(b.key) ? -1 : 1; + } + }); + } + return JSON.stringify(val, null, 4); + }; + + module.exports = Utils; +}()); \ No newline at end of file diff --git a/test/angular-translate_test.js b/test/angular-translate_test.js index 3ce968e..f17b438 100644 --- a/test/angular-translate_test.js +++ b/test/angular-translate_test.js @@ -131,6 +131,16 @@ exports.i18nextract = { var expected = grunt.file.read( 'test/expected/10_fr_FR.json' ); test.equal( actual, expected, 'Should equal.' ); + test.done(); + }, + + extract_to_pot: function(test) { + test.expect(1); + + var actual = grunt.file.read( 'tmp/template.pot' ); + var expected = grunt.file.read( 'test/expected/template.pot' ); + test.equal( actual, expected, 'Should equal.' ); + test.done(); } diff --git a/test/expected/template.pot b/test/expected/template.pot new file mode 100644 index 0000000..6fae9ae --- /dev/null +++ b/test/expected/template.pot @@ -0,0 +1,145 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Project-Id-Version: \n" + +msgctxt "" +msgid "all.tato.tati" +msgstr "" + +msgctxt "" +msgid "all.tato.tbti" +msgstr "" + +msgctxt "" +msgid "all.tato.tdti" +msgstr "" + +msgctxt "" +msgid "all.tato.tIiti" +msgstr "" + +msgctxt "" +msgid "all.tato.tZti" +msgstr "" + +msgctxt "" +msgid "all.tato.tzti" +msgstr "" + +msgctxt "" +msgid "all.toto.titi" +msgstr "" + +msgctxt "" +msgid "HtmlDirective 1/2 &é\"'(-è_çà)=$^ù!:;,}@^`[{#~@Ùô" +msgstr "" + +msgctxt "" +msgid "HtmlDirective 2/2" +msgstr "" + +msgctxt "" +msgid "HtmlDirectivePluralFirst" +msgstr "{NB, plural, one{HtmlDirectivePluralFirst for one!} other{# HtmlDirectivePluralFirst for others!}" + +msgctxt "" +msgid "HtmlDirectivePluralSecond" +msgstr "{NB, plural, one{HtmlDirectivePluralSecond for one!} other{# HtmlDirectivePluralSecond for others!}" + +msgctxt "" +msgid "HtmlDirectiveStandalone" +msgstr "" + +msgctxt "" +msgid "HtmlFilterDoubleQuote{} interp\"olation {{xx}}" +msgstr "" + +msgctxt "" +msgid "HtmlFilterDoubleQuote{} on same line 2/2" +msgstr "" + +msgctxt "" +msgid "HtmlFilterDoubleQuote{} {{var}}on same line 1/2" +msgstr "" + +msgctxt "" +msgid "HtmlNgBindHtml Key ' 1/1" +msgstr "" + +msgctxt "" +msgid "JavascriptFilter 1/2 with var \"{name}\"" +msgstr "" + +msgctxt "" +msgid "JavascriptFilter 2/2 without var" +msgstr "" + +msgctxt "" +msgid "JavascriptServiceArrayDoubleQuote 1/2 without var." +msgstr "" + +msgctxt "" +msgid "JavascriptServiceArrayDoubleQuote 2/2 without var." +msgstr "" + +msgctxt "" +msgid "JavascriptServiceArraySimpleQuote 1/2 without var." +msgstr "" + +msgctxt "" +msgid "JavascriptServiceArraySimpleQuote 2/2 without var." +msgstr "" + +msgctxt "" +msgid "JavascriptServiceDoubleQuote 2/2 with var \"{name}\"." +msgstr "" + +msgctxt "" +msgid "JavascriptServiceInstantDoubleQuote 2/2 with var \"{name}\"." +msgstr "" + +msgctxt "" +msgid "JavascriptServiceInstantSimpleQuote 1/2 with var \"{name}\"." +msgstr "" + +msgctxt "" +msgid "JavascriptServiceSimpleQuote 1/2 with var \"{name}\"." +msgstr "" + +msgctxt "" +msgid "myDoubleQuotedCommentTranslation \"e\"s\"" +msgstr "" + +msgctxt "" +msgid "mySingleQ'uotedCommentTranslation '" +msgstr "" + +msgctxt "" +msgid "SUB.NAMESPACE.VAL 1" +msgstr "" + +msgctxt "" +msgid "SUB.NAMESPACE.VAL 2" +msgstr "" + +msgctxt "" +msgid "SUB.NS.vAL 0" +msgstr "" + +msgctxt "" +msgid "SUB.NS.VAL 1" +msgstr "" + +msgctxt "" +msgid "SUB.NS.VAL 1.test 2" +msgstr "" + +msgctxt "" +msgid "SUB.NS.VAL 1.test1" +msgstr "" + +msgctxt "" +msgid "SUB.NS.VAL 2" +msgstr ""