diff --git a/.gitignore b/.gitignore index 28f91f1..6162761 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules benchmark spec/tmp +reports diff --git a/Gruntfile.js b/Gruntfile.js index d80f2f1..6c784dc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -115,7 +115,7 @@ module.exports = function (grunt) { grunt.util.spawn({ cmd: cmd, - args: [options.specs], + args: [options.specs, '--junitreport'], opts: { env: grunt.util._.extend( {}, process.env, { FEST_COMPILE: JSON.stringify(options.compile) } diff --git a/README.md b/README.md index 7ade75d..ce5ad2c 100644 --- a/README.md +++ b/README.md @@ -524,56 +524,22 @@ person.xml: ``` ## Интернационализация - -### fest:plural - -По умолчанию доступна поддержка плюрализации для русского и английского языка. В параметрах `fest.compile` можно передать любую другую функцию плюрализации. - -```xml -один рубль|%s рубля|%s рублей -``` -Или англоязычный вариант: - -```xml -one ruble|%s rubles -``` - -Чтобы вывести символ “%” внутри тега `fest:plural` используйте “%%”: - -```xml -…1%%…|…%s%%…|…%s%%… -``` - -### fest:message и fest:msg - -Позволяет указать границы фразы для перевода и контекст для снятия многозначности. Например, +Для работы интернационализации в `fest` добавлен новый неймспейс i18n. В нем определн +тег msg, пример: ```xml -Лук -Лук -``` - -Для каждого `fest:message`, `fest:msg`, обычного текста, заключенного между XML тегами (опция `auto_message`), или текстового значения некоторых атрибутов компилятор вызывает функцию `events.message` (если такая была указана в параметрах). Данный механизм используется в `fest-build` утилите для построения оригинального PO-файла. - -Пример вызова `fest-build` для создания PO-файла: - -``` -$ fest-build --dir=fest --po=ru_RU.po --compile.auto_message=true -``` - -Пример компиляции локализованных шаблонов: + + + Замок {name} + + { + text: Строка + } + -``` -$ fest-build --dir=fest --translate=en_US.po + ``` -Пример компиляции одного шаблона: - -``` -$ fest-compile path/to/template.xml -$ fest-compile --out=path/to/compiled.js path/to/template.xml -$ fest-compile --out=path/to/compiled.js --translate=path/to/en_US.po path/to/template.xml -``` ## Contribution @@ -594,4 +560,4 @@ Grunt используется для валидации JS (тестов) и з ``` $ ./bin/fest-build --dir=spec/templates --exclude='*error*' --compile.beautify=true --out=spec/expected/build/initial $ ./bin/fest-build --dir=spec/templates --exclude='*error*' --compile.beautify=true --out=spec/expected/build/translated --translate=spec/templates/en_US.po -``` +``` \ No newline at end of file diff --git a/lib/compile.js b/lib/compile.js index 88be38c..6f9db50 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -786,6 +786,7 @@ var compile = (function(){ '__fest_jschars=/' + jschars.source + '/g,' + '__fest_jschars_test=/' + jschars.source + '/,' + '__fest_jshash=' + JSON.stringify(jshash) + ',' + + 'i18n=__fest_self && typeof __fest_self.i18n === "function" ? __fest_self.i18n : function (str) {return str;},' + '___fest_log_error;' + (options.mode === 'function' ? 'function __fest_pushstr(_,s){__fest_buf+=s}' : '') + 'if(typeof __fest_error === "undefined"){___fest_log_error = (typeof console !== "undefined" && console.error) ? function(){return Function.prototype.apply.call(console.error, console, arguments)} : function(){};}else{___fest_log_error=__fest_error};' + diff --git a/lib/compile_tmpl.js b/lib/compile_tmpl.js old mode 100644 new mode 100755 index e4924a2..34e8402 --- a/lib/compile_tmpl.js +++ b/lib/compile_tmpl.js @@ -1,9 +1,11 @@ -function compile_tmpl(file, source, wrapper) { +function compile_tmpl(file, source, wrapper, dir) { var wrappers = { fest: ";(function(){var x=Function('return this')();if(!x.fest)x.fest={};x.fest['##file##']=##source##})();", loader: ";(function(){var x=Function('return this')();if(!x.fest)x.fest={};x.fest['##file##']=##source##; if(x.jsLoader!==undefined&&x.jsLoader.loaded!==undefined&&typeof x.jsLoader.loaded==='function'){x.jsLoader.loaded('{festTemplate}##file##')};})();", source: "##source##", + amd_ajs: "define('festTemplate/##file##', [], function() {var x=Function('return this')();if(!x.fest)x.fest={};x.fest['##file##']=##source##;return x.fest['##file##'];});", amd: "define(function(){return ##source##});", + amd_domains: "define('##file##', [], function() {var x=Function('return this')();if(!x.fest)x.fest={};x.fest['##file##']=x.fest['##file_short##']=##source##;return x.fest['##file##'];});", variable: "var ##file##=##source##;" }; @@ -13,7 +15,24 @@ function compile_tmpl(file, source, wrapper) { } file = file.replace(/\'/g, "\\'").replace(/\"/g, '\\"').replace(/\\/g, '\\'); - return wrappers[wrapper || 'fest'].replace(/##file##/g, file).replace(/##source##/g, source); + + var fileShort = file; + + if (wrapper === 'amd_domains') { // нормальный путь + + var packageName = dir.replace(/\/.*(mail\.[^\/]+)\/fest.*/, "$1"); + + file = packageName + '/fest/' + file; + + if (packageName !== 'mail.core') { // для обратной совместимости + fileShort = file; + } + } + + return wrappers[wrapper || 'fest'] + .replace(/##file##/g, file) + .replace(/##file_short##/g, fileShort) + .replace(/##source##/g, source); } if (typeof module !== 'undefined' && module.exports){ diff --git a/lib/proxy.js b/lib/proxy.js old mode 100644 new mode 100755 index dfbc787..8c04eed --- a/lib/proxy.js +++ b/lib/proxy.js @@ -195,11 +195,11 @@ function compile(file, locale, dir, options, config){ return compile_error_tmpl(file, 'fest.compile failed', options); } - return compile_tmpl(file, source, options); + return compile_tmpl(file, source, options, dir); } -function compile_tmpl(file, source, options){ - return fest.compile_tmpl(file, source, options ? options.wrapper : null); +function compile_tmpl(file, source, options, dir){ + return fest.compile_tmpl(file, source, options ? options.wrapper : null, dir); } function compile_error_tmpl(file, txt, options){ @@ -224,4 +224,4 @@ function mkdir(filename, mode) { fs.mkdirSync(dir, mode); } } - } \ No newline at end of file + } diff --git a/lib/translate.js b/lib/translate.js index 80021a3..55141e9 100644 --- a/lib/translate.js +++ b/lib/translate.js @@ -124,6 +124,54 @@ function translate(data) { } } + function wrap(text) { + var node = stack[stack.length - 1], + parent = stack[stack.length - 2], + regex = /^data\-/, + paramRegex = /^\{.+\}$/, + tokenOpen = '#%', + tokenClose = '%#', + params = {}, + value; + + if (!node || node.ns[node.prefix] !== fest_i18n_ns) { + return escapeHTML(text); + } + + text = translate_message(new Message(text)); + text = 'i18n("' + escapeHTML(text) + '"'; + + for (var key in node.attributes) { + if (regex.test(key)) { + value = node.attributes[key].value; + + if (paramRegex.test(value)) { + value = value.replace(/^{/, tokenOpen).replace(/}$/, tokenClose); + } + + params[key.replace(regex, '')] = value; + } + } + + if (node.attributes.context) { + text += ',' + '"' + node.attributes.context.value + '"'; + } + + if (Object.keys(params).length) { + text += ',' + escapeHTML(JSON.stringify(params)) + .split(tokenOpen).join('" + ') + .split(tokenClose).join(' + "'); + } + + text += ')'; + + if (parent && ['get', 'script', 'value'].indexOf(parent.local) === -1) { + text = '' + text + ';' + ''; + } + + return text; + } + parser.onprocessinginstruction = function(instruction){ result += ''; }; @@ -178,7 +226,7 @@ function translate(data) { parser.ontext = function(text){ var xml; closetag(); - ontext(text, escapeHTML); + ontext(text, wrap); }; parser.oncdata = function(text){ diff --git a/package.json b/package.json index 7696d82..fb283c8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fest", "description": "JavaScript Templates", "keywords": ["template", "templating", "html", "xml"], - "version": "0.9.0", + "version": "0.10.5", "repository": { "type": "git", "url": "http://github.com/mailru/fest.git" @@ -34,6 +34,6 @@ "grunt-contrib-clean": "~0.4.0", "grunt-contrib-jshint": "~0.1.1", "grunt-contrib-watch": "~0.2.0", - "jasmine-node": "~1.3.0" + "jasmine-node": "~1.14.5" } } diff --git a/spec/build.spec.js b/spec/build.spec.js deleted file mode 100644 index 17d1410..0000000 --- a/spec/build.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -var fs = require('fs'); - - -describe('fest-build', function () { - - it('should compile directories with templates', function () { - var actualFiles = fs.readdirSync(__dirname + '/tmp/build/initial'), - expectedFiles = fs.readdirSync(__dirname + '/expected/build/initial'); - expect( - actualFiles.length - ).toBe( - expectedFiles.length - ); - expectedFiles.forEach(function (fn) { - var actual = fs.readFileSync(__dirname + '/tmp/build/initial/' + fn, 'utf8'), - expected = fs.readFileSync(__dirname + '/expected/build/initial/' + fn, 'utf8'); - expect(actual).toBe(expected); - }); - }); - - it('should translate and compile directories with templates', function () { - var actualFiles = fs.readdirSync(__dirname + '/tmp/build/translated'), - expectedFiles = fs.readdirSync(__dirname + '/expected/build/translated'); - expect( - actualFiles.length - ).toBe( - expectedFiles.length - ); - expectedFiles.forEach(function (fn) { - var actual = fs.readFileSync(__dirname + '/tmp/build/translated/' + fn, 'utf8'), - expected = fs.readFileSync(__dirname + '/expected/build/translated/' + fn, 'utf8'); - expect(actual).toBe(expected); - }); - }); - -}); - - -describe('fest-compile', function () { - - it('should compile directories with templates', function () { - var actualFiles = fs.readdirSync(__dirname + '/tmp/compile/initial'), - expectedFiles = fs.readdirSync(__dirname + '/expected/compile/initial'); - expect( - actualFiles.length - ).toBe( - expectedFiles.length - ); - expectedFiles.forEach(function (fn) { - var actual = fs.readFileSync(__dirname + '/tmp/compile/initial/' + fn, 'utf8'), - expected = fs.readFileSync(__dirname + '/expected/compile/initial/' + fn, 'utf8'); - expect(actual).toBe(expected); - }); - }); - - it('should translate and compile directories with templates', function () { - var actualFiles = fs.readdirSync(__dirname + '/tmp/compile/translated'), - expectedFiles = fs.readdirSync(__dirname + '/expected/compile/translated'); - expect( - actualFiles.length - ).toBe( - expectedFiles.length - ); - expectedFiles.forEach(function (fn) { - var actual = fs.readFileSync(__dirname + '/tmp/compile/translated/' + fn, 'utf8'), - expected = fs.readFileSync(__dirname + '/expected/compile/translated/' + fn, 'utf8'); - expect(actual).toBe(expected); - }); - }); - -}); diff --git a/spec/message.spec.js b/spec/message.spec.js index 84b224a..8709158 100644 --- a/spec/message.spec.js +++ b/spec/message.spec.js @@ -53,14 +53,32 @@ describe('fest:message', function () { expect( render('templates/message-with-i18n-ns.xml', {}, { messages: { - 'Строка': 'Line' + 'Use {link}redirect{linkclose}.': 'Какой-то перевод ' } }).contents ).toBe( - 'Line' + 'tstUse {link}redirect{linkclose}.Hello, {name}' ); }); + it('should support external i18n function', function () { + + expect( + render('templates/message-with-i18n-ns.xml', {}, { + messages: { + 'Строка ': 'Line ' + } + }, { + i18n: function (str) { return str + ' test'; } + }).contents + ).toBe( + 'tst testUse {link}redirect{linkclose}. testHello, {name} test' + ); + + delete global.__fest_i18n; + }); + + it('should allow redefine messages via events', function () { var sourceMap = { 'Логотип {json.name}': '0', diff --git a/spec/templates/include.xml b/spec/templates/include.xml index bd57e79..d42a991 100644 --- a/spec/templates/include.xml +++ b/spec/templates/include.xml @@ -3,5 +3,4 @@ - diff --git a/spec/templates/message-with-i18n-ns.xml b/spec/templates/message-with-i18n-ns.xml index 70cd75a..e42b034 100644 --- a/spec/templates/message-with-i18n-ns.xml +++ b/spec/templates/message-with-i18n-ns.xml @@ -1,10 +1,13 @@ - params.text + params.text + params.text2 + params.text3 { - text: 'Строка' + text: tst, + text2: Use {link}redirect{linkclose}., + text3: Hello, {name} } - \ No newline at end of file + +