diff --git a/lib/compile.js b/lib/compile.js index 6d3e426..dc6eca6 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -43,6 +43,68 @@ var formats = { }); return JSON.stringify(result); } + }, + csharp: { + addLocale: function (locale, strings) { + var result = 'var dictionary' + locale + ' = new Dictionary();\n'; + for (var key in strings) { + //supports only singular + if (Object.prototype.toString.call(strings[key]) === '[object Array]') { + strings[key] = strings[key][0]; + } + // escape quotes and new lines in order to compile in c# code + var value = strings[key].replace(/(\\)?"/g, function ($0, $1) { return $1 ? $0 : '\\"'; }); + value = value.replace(/[\n\r]/g, ' '); + key = key.replace(/(\\)?"/g, function ($0, $1) { return $1 ? $0 : '\\"'; }); + key = key.replace(/[\n\r]/g, ' '); + result += 'dictionary' + locale + '.Add("' + key + '","' + value + '");\n'; + } + result += '_translations.Add("' + locale + '", dictionary' + locale + ');\n'; + + return result; + }, + format: function (locales, options) { + var module = + 'using System;\n' + + 'using System.Collections.Generic;\n' + + 'namespace ' + options.namespace + '\n' + + '{\n' + + 'public static class Translations' + + '{\n' + + 'public static readonly Dictionary> _translations = new Dictionary>();\n' + + 'private static Dictionary _currentLanguage;\n' + + 'static Translations()\n' + + '{\n' + + locales.join(''); + + if (options.defaultLanguage) { + module += 'SetCurrentLanguage("' + options.defaultLanguage + '");\n'; + } + module += '}\n' + + 'public static void SetCurrentLanguage(string languageCode)\n' + + '{\n' + + 'foreach (var translation in _translations)\n' + + '{\n' + + 'if (translation.Key != languageCode) continue;\n' + + '_currentLanguage = translation.Value;\n' + + 'return;\n' + + '}\n' + + '}\n' + + 'public static string Translate(string key)\n' + + '{\n' + + 'try\n' + + '{\n' + + 'return _currentLanguage != null ? _currentLanguage[key] : key;\n' + + '}\n' + + 'catch(Exception)\n' + + '{\n' + + 'return key;\n' + + '}\n' + + '}\n' + + '}\n' + + '}\n'; + return module; + } } }; @@ -52,7 +114,8 @@ var Compiler = (function () { function Compiler(options) { this.options = _.extend({ format: 'javascript', - module: 'gettext' + module: 'gettext', + namespace: 'Core.Common' }, options); } @@ -60,8 +123,17 @@ var Compiler = (function () { return formats.hasOwnProperty(format); }; - Compiler.prototype.convertPo = function (inputs) { - var format = formats[this.options.format]; + Compiler.prototype.convertPo = function (inputs, filename) { + + var format; + + if (filename) { + var extension = filename.split('.').pop(); + format = extension === 'cs' ? formats.csharp : formats[this.options.format]; + } else { + format = formats[this.options.format]; + } + var locales = []; inputs.forEach(function (input) { diff --git a/lib/extract.js b/lib/extract.js index cae964a..71875ec 100644 --- a/lib/extract.js +++ b/lib/extract.js @@ -79,9 +79,10 @@ var Extractor = (function () { erb: 'html', js: 'js', tag: 'html', - jsp: 'html' + jsp: 'html', + cs: 'c#' }, - postProcess: function (po) {} + postProcess: function (po) { } }, options); this.options.markerNames.unshift(this.options.markerName); this.options.attributes.unshift(this.options.attribute); @@ -285,6 +286,30 @@ var Extractor = (function () { }); }; + + Extractor.prototype.extractCs = function (filename, src) { + var self = this; + var newlines = function (index) { + return src.substr(0, index).match(/\n/g) || []; + }; + + var reference = function (index) { + return { + file: filename, + location: { + start: { + line: newlines(index).length + 1 + } + } + }; + }; + var pattern = /_translate\s*=\s*"(.*?)"\s*;/g; + var match; + while ((match = pattern.exec(src))) { + self.addString(reference(match.index), match[1]); + } + }; + Extractor.prototype.extractHtml = function (filename, src) { var extractHtml = function (src, lineNumber) { var $ = cheerio.load(src, { decodeEntities: false, withStartIndices: true }); @@ -365,6 +390,9 @@ var Extractor = (function () { extractHtml(src, 0); }; + + + Extractor.prototype.isSupportedByStrategy = function (strategy, extension) { return (extension in this.options.extensions) && (this.options.extensions[extension] === strategy); }; @@ -378,6 +406,9 @@ var Extractor = (function () { if (this.isSupportedByStrategy('js', extension)) { this.extractJs(filename, content); } + if (this.isSupportedByStrategy('c#', extension)) { + this.extractCs(filename, content); + } }; Extractor.prototype.toString = function () { diff --git a/test/compile.js b/test/compile.js index 89683b8..bb16104 100644 --- a/test/compile.js +++ b/test/compile.js @@ -54,6 +54,14 @@ function testCompile(filenames, options) { return compiler.convertPo(inputs); } +function testCompileCs(filenames, options) { + var compiler = new Compiler(options); + var inputs = filenames.map(function (filename) { + return fs.readFileSync(filename, 'utf8'); + }); + return compiler.convertPo(inputs, 'test.cs'); +} + describe('Compile', function () { it('Compiles a .po file into a .js catalog', function () { var files = ['test/fixtures/nl.po']; @@ -242,4 +250,180 @@ describe('Compile', function () { vm.runInContext(output, context); assert(catalog.called); }); + + describe('C# dictionary file', function () { + it('Generates the expected c# dictionary', function () { + var files = ['test/fixtures/nl_c#test.po']; + var output = testCompileCs(files); + var expectedOutput = 'using System;\n' + + 'using System.Collections.Generic;\n' + + 'namespace Core.Common\n' + + '{\n' + + 'public static class Translations{\n' + + 'public static readonly Dictionary> _translations = new Dictionary>();\n' + + 'private static Dictionary _currentLanguage;\n' + + 'static Translations()\n' + + '{\n' + + 'var dictionarynl = new Dictionary();\n' + + 'dictionarynl.Add("Hello!","Hallo!");\n' + + '_translations.Add("nl", dictionarynl);\n' + + '}\n' + + 'public static void SetCurrentLanguage(string languageCode)\n' + + '{\n' + + 'foreach (var translation in _translations)\n' + + '{\n' + + 'if (translation.Key != languageCode) continue;\n' + + '_currentLanguage = translation.Value;\n' + + 'return;\n' + + '}\n' + + '}\n' + + 'public static string Translate(string key)\n' + + '{\n' + + 'try\n' + + '{\n' + + 'return _currentLanguage != null ? _currentLanguage[key] : key;\n' + + '}\n' + + 'catch(Exception)\n' + + '{\n' + + 'return key;\n' + + '}\n' + + '}\n' + + '}\n' + + '}\n'; + + assert.equal(output, expectedOutput); + }); + + it('Escapes quotes and new lines', function () { + var files = ['test/fixtures/nl_escape.po']; + var output = testCompileCs(files); + var expectedOutput = 'using System;\n' + + 'using System.Collections.Generic;\n' + + 'namespace Core.Common\n' + + '{\n' + + 'public static class Translations{\n' + + 'public static readonly Dictionary> _translations = new Dictionary>();\n' + + 'private static Dictionary _currentLanguage;\n' + + 'static Translations()\n' + + '{\n' + + 'var dictionarynl = new Dictionary();\n' + + 'dictionarynl.Add("This is a test","Dit is een test");\n' + + 'dictionarynl.Add("Bird","Vogel");\n' + + 'dictionarynl.Add("Hello \\"world\\"","Hallo \\"wereld\\"");\n' + + '_translations.Add("nl", dictionarynl);\n' + + '}\n' + + 'public static void SetCurrentLanguage(string languageCode)\n' + + '{\n' + + 'foreach (var translation in _translations)\n' + + '{\n' + + 'if (translation.Key != languageCode) continue;\n' + + '_currentLanguage = translation.Value;\n' + + 'return;\n' + + '}\n' + + '}\n' + + 'public static string Translate(string key)\n' + + '{\n' + + 'try\n' + + '{\n' + + 'return _currentLanguage != null ? _currentLanguage[key] : key;\n' + + '}\n' + + 'catch(Exception)\n' + + '{\n' + + 'return key;\n' + + '}\n' + + '}\n' + + '}\n' + + '}\n'; + + assert.equal(output, expectedOutput); + }); + + it('Accepts a defaultLanguage parameter', function () { + var files = ['test/fixtures/nl_c#test.po']; + var output = testCompileCs(files, { + defaultLanguage: 'nl' + }); + var expectedOutput = 'using System;\n' + + 'using System.Collections.Generic;\n' + + 'namespace Core.Common\n' + + '{\n' + + 'public static class Translations{\n' + + 'public static readonly Dictionary> _translations = new Dictionary>();\n' + + 'private static Dictionary _currentLanguage;\n' + + 'static Translations()\n' + + '{\n' + + 'var dictionarynl = new Dictionary();\n' + + 'dictionarynl.Add("Hello!","Hallo!");\n' + + '_translations.Add("nl", dictionarynl);\n' + + 'SetCurrentLanguage("nl");\n' + + '}\n' + + 'public static void SetCurrentLanguage(string languageCode)\n' + + '{\n' + + 'foreach (var translation in _translations)\n' + + '{\n' + + 'if (translation.Key != languageCode) continue;\n' + + '_currentLanguage = translation.Value;\n' + + 'return;\n' + + '}\n' + + '}\n' + + 'public static string Translate(string key)\n' + + '{\n' + + 'try\n' + + '{\n' + + 'return _currentLanguage != null ? _currentLanguage[key] : key;\n' + + '}\n' + + 'catch(Exception)\n' + + '{\n' + + 'return key;\n' + + '}\n' + + '}\n' + + '}\n' + + '}\n'; + + assert.equal(output, expectedOutput); + }); + + it('Accepts a namaspace parameter', function () { + var files = ['test/fixtures/nl_c#test.po']; + var output = testCompileCs(files, { + namespace: 'Test.Namaspace' + }); + var expectedOutput = 'using System;\n' + + 'using System.Collections.Generic;\n' + + 'namespace Test.Namaspace\n' + + '{\n' + + 'public static class Translations{\n' + + 'public static readonly Dictionary> _translations = new Dictionary>();\n' + + 'private static Dictionary _currentLanguage;\n' + + 'static Translations()\n' + + '{\n' + + 'var dictionarynl = new Dictionary();\n' + + 'dictionarynl.Add("Hello!","Hallo!");\n' + + '_translations.Add("nl", dictionarynl);\n' + + '}\n' + + 'public static void SetCurrentLanguage(string languageCode)\n' + + '{\n' + + 'foreach (var translation in _translations)\n' + + '{\n' + + 'if (translation.Key != languageCode) continue;\n' + + '_currentLanguage = translation.Value;\n' + + 'return;\n' + + '}\n' + + '}\n' + + 'public static string Translate(string key)\n' + + '{\n' + + 'try\n' + + '{\n' + + 'return _currentLanguage != null ? _currentLanguage[key] : key;\n' + + '}\n' + + 'catch(Exception)\n' + + '{\n' + + 'return key;\n' + + '}\n' + + '}\n' + + '}\n' + + '}\n'; + assert.equal(output, expectedOutput); + }); + }); }); diff --git a/test/extract_csharp.js b/test/extract_csharp.js new file mode 100644 index 0000000..a96b22b --- /dev/null +++ b/test/extract_csharp.js @@ -0,0 +1,21 @@ +'use strict'; + +var assert = require('assert'); +var testExtract = require('./utils').testExtract; + +describe('Extracting from C#', function () { + it('should extract the properties having _translate sufix', function () { + var files = [ + 'test/fixtures/Constants.cs' + ]; + var catalog = testExtract(files); + + assert.equal(catalog.items.length, 2); + assert.equal(catalog.items[0].msgid, 'Successfully saved!'); + assert.equal(catalog.items[0].msgstr, ''); + assert.equal(catalog.items[1].msgid, 'The selected items were deleted successfully.'); + assert.equal(catalog.items[1].msgstr, ''); + assert.deepEqual(catalog.items[0].references, ['test/fixtures/Constants.cs:14']); + assert.deepEqual(catalog.items[1].references, ['test/fixtures/Constants.cs:16']); + }); +}); \ No newline at end of file diff --git a/test/fixtures/Constants.cs b/test/fixtures/Constants.cs new file mode 100644 index 0000000..4188142 --- /dev/null +++ b/test/fixtures/Constants.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; + + +namespace Core.Common +{ + public static class Constants + { + + public static class Messages + { + public const string SuccessfullySaved_translate = + "Successfully saved!" ; + public const string OccurrencesDeleted_translate = "The selected items were deleted successfully." ; + public const string StatusUpdated = "Investigation status was updated successfully."; + + public static class EmailGroupsMessages + { + public const string EmailGroupDeleted = "Email Group successfully deleted!"; + public const string EmailUpdated = "Email successfully updated!"; + public const string EmailDeleted = "Email successfully deleted!"; + } + } + + } +} + + diff --git a/test/fixtures/nl_c#test.po b/test/fixtures/nl_c#test.po new file mode 100644 index 0000000..d1ad8c2 --- /dev/null +++ b/test/fixtures/nl_c#test.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"X-Generator: Poedit 1.5.7\n" + +#: test/fixtures/single.html test/fixtures/second.html +msgid "Hello!" +msgstr "Hallo!" + diff --git a/test/fixtures/nl_escape.po b/test/fixtures/nl_escape.po new file mode 100644 index 0000000..6828b73 --- /dev/null +++ b/test/fixtures/nl_escape.po @@ -0,0 +1,29 @@ +msgid "" +msgstr "" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"X-Generator: Poedit 1.8.6\n" + +#: test/fixtures/second.html +msgid "" +"This is a\n" +"test" +msgstr "Dit is een test" + +#: test/fixtures/plural.html +msgid "Bird" +msgid_plural "Birds" +msgstr[0] "Vogel" +msgstr[1] "Vogels" + +#: test/fixtures/quotes.html +msgid "Hello \"world\"" +msgstr "Hallo \"wereld\""