diff --git a/.gitmodules b/.gitmodules index 7376cc5..b676168 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "support/vows"] path = support/vows url = http://github.com/cloudhead/vows.git +[submodule "support/lingo"] + path = support/lingo + url = git@github.com:masylum/lingo.git diff --git a/Readme.md b/Readme.md index 7674990..fb6815d 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,12 @@ -# Dialect + ,, ,, ,, + `7MM db `7MM mm + MM MM MM + ,M""bMM `7MM ,6"Yb. MM .gP"Ya ,p6"bo mmMMmm + ,AP MM MM 8) MM MM ,M' Yb 6M' OO MM + 8MI MM MM ,pm9MM MM 8M"""""" 8M MM + `Mb MM MM 8M MM MM YM. , YM. , MM + `Wbmd"MML..JMML.`Moo9^Yo..JMML.`Mbmmd' YMbmd' `Mbmo + Dialect is a painless nodejs library to deal with i18n, and L10n. @@ -85,4 +93,6 @@ This module is currently *under construction* Dialect is heavily tested using a mix of Vows and node asserts module. + npm install eyes + make test diff --git a/lib/dialect.js b/lib/dialect.js index 867bd78..74327ff 100644 --- a/lib/dialect.js +++ b/lib/dialect.js @@ -3,7 +3,19 @@ var fs = require('fs'); module.exports = function (options) { var dialect = {}, funk = require('./../support/funk/lib/funk'), + lingo = require('./../support/lingo'), + Language = lingo.Language, dictionaries = {}, + current_language = null, + + parse = function (str, params) { + var matches = /(\{(.*?)\})+/g.exec(str); + + // shameless copy/inspiration from lingo (TJ) + return str.replace(/\{([^}]+)\}/g, function (_, key) { + return params[key]; + }); + }, loadDictionariesToMemory = function (reload) { options.locales.forEach(function (locale) { @@ -23,15 +35,11 @@ module.exports = function (options) { options.store.get({locale: locale}, fk.add(function (err, data) { var json = {}, i = null; for (i in data) { - if (Object.keys(data[i]).length === 4) { - json[data[i].original] = data[i].translation; - } else { - json[data[i].original] = { - translation: data[i].translation, - count: data[i].count, - context: data[i].context - }; - } + json[data[i].original] = { + translation: data[i].translation, + count: data[i].count, + context: data[i].context + }; } fs.writeFileSync( options.path + locale + '.js', @@ -106,9 +114,20 @@ module.exports = function (options) { }; dialect.config = function (key, value) { + if (value !== undefined) { options[key] = value; + + switch (key) { + case 'current_locale': + current_language = new Language(value); + break; + case 'path': + options.path = value.replace(/(.*)\/?/, '$1/'); + break; + } } + return options[key]; }; @@ -124,6 +143,9 @@ module.exports = function (options) { */ dialect.getTranslation = function (original, callback) { var translation = '', + params = Array.isArray(original) ? original.slice(-1)[0] : {}, + index = 0, + str_original = original, i = 0; if ((typeof original !== 'string' && !Array.isArray(original)) || original.length === 0) { @@ -138,12 +160,25 @@ module.exports = function (options) { if (Object.keys(dictionaries).length === 0) { loadDictionariesToMemory(); } - translation = dictionaries[options.current_locale][original]; + + if (params.count) { + index = current_language.isPlural(params.count) ? 0 : 1; + str_original = Array.isArray(original) ? original[index] : original; + } + + if (Object.keys(dictionaries[options.current_locale]).length > 0) { + translation = dictionaries[options.current_locale][str_original]; + if (typeof translation === 'object') { + translation = translation.translation; // lol + } + } else { + translation = undefined; + } if (translation === undefined) { storeNewTranslation(original, callback); } - return translation || original; + return parse(translation || str_original, params); } }; @@ -163,18 +198,29 @@ module.exports = function (options) { * @api public */ dialect.setTranslation = function (query, translation, callback) { - var fk = funk(); + var fk = funk(), + update = {translation: translation}; + + // COUNT + if (query.count) { + query = {original: query.original, locale: query.locale, count: query.count}; + } + + // CONTEXT + if (query.context) { + update.context = query.context; + } // DB if (options.store) { - options.store.set(query, translation, fk.nothing()); // fire and forget + options.store.set(query, update, fk.nothing()); // fire and forget } // JSON store - addTranslationToJSON(query, translation, fk.nothing()); + addTranslationToJSON(query, update, fk.nothing()); // Memory - dictionaries[query.locale][query.original] = translation; + dictionaries[query.locale][query.original] = update; fk.parallel(function () { if (callback) { @@ -187,11 +233,13 @@ module.exports = function (options) { // INIT if (!options || !options.path) { throw new Error("You need to provide the path where you want to store the JSON dictionaries"); - } else { - options.path = options.path.replace(/(.*)\/?/, '$1/'); } options.base_locale = options.base_locale || 'en'; + ['path', 'current_locale'].forEach(function (conf) { + dialect.config(conf, options[conf]); + }); + return dialect; }; diff --git a/lib/stores/mongodb.js b/lib/stores/mongodb.js index d860813..b9e9111 100644 --- a/lib/stores/mongodb.js +++ b/lib/stores/mongodb.js @@ -79,6 +79,7 @@ module.exports = function (options, callback) { } else { doc = {original: query.original, locale: query.locale, translation: translation}; } + this.collection.findOne({original: query.original, locale: query.locale}, function (err, data) { if (!data) { self.collection.insert( @@ -111,11 +112,11 @@ module.exports = function (options, callback) { * @param {Function} callback * @api public */ - mongoStore.set = function (query, translation, callback) { + mongoStore.set = function (query, update, callback) { try { this.collection.update( query, - {'$set': {translation: translation}}, + {'$set': update}, function (err, data) { if (callback) { callback(err, mongoStore); diff --git a/support/lingo b/support/lingo new file mode 160000 index 0000000..262e011 --- /dev/null +++ b/support/lingo @@ -0,0 +1 @@ +Subproject commit 262e01199540e1881381dd1f39494f2daa24d5ff diff --git a/test/data/integration/ca.js b/test/data/integration/ca.js deleted file mode 100644 index 1164d0e..0000000 --- a/test/data/integration/ca.js +++ /dev/null @@ -1 +0,0 @@ -{"E muito bom": "Esta molt be"} \ No newline at end of file diff --git a/test/data/integration/en.js b/test/data/integration/en.js deleted file mode 100644 index eaabf4e..0000000 --- a/test/data/integration/en.js +++ /dev/null @@ -1 +0,0 @@ -{"I love gazpacho":"I love gazpacho"} \ No newline at end of file diff --git a/test/data/integration/en_en.js b/test/data/integration/en_en.js deleted file mode 100644 index 505a7f1..0000000 --- a/test/data/integration/en_en.js +++ /dev/null @@ -1 +0,0 @@ -{"You have {count} {what} friend":{"translation":null,"count":"singular","context":null},"You have {count} {what} friends":{"translation":null,"count":"plural","context":null}} \ No newline at end of file diff --git a/test/data/integration/es.js b/test/data/integration/es.js deleted file mode 100644 index d8a4df5..0000000 --- a/test/data/integration/es.js +++ /dev/null @@ -1 +0,0 @@ -{"I love gazpacho":"Me encanta el gazpacho"} \ No newline at end of file diff --git a/test/data/integration/es_es.js b/test/data/integration/es_es.js deleted file mode 100644 index 505a7f1..0000000 --- a/test/data/integration/es_es.js +++ /dev/null @@ -1 +0,0 @@ -{"You have {count} {what} friend":{"translation":null,"count":"singular","context":null},"You have {count} {what} friends":{"translation":null,"count":"plural","context":null}} \ No newline at end of file diff --git a/test/data/integration/fr.js b/test/data/integration/fr.js deleted file mode 100644 index 1eb4785..0000000 --- a/test/data/integration/fr.js +++ /dev/null @@ -1 +0,0 @@ -{"Ciao":"Alo"} \ No newline at end of file diff --git a/test/data/integration/it.js b/test/data/integration/it.js deleted file mode 100644 index 3fb0c90..0000000 --- a/test/data/integration/it.js +++ /dev/null @@ -1 +0,0 @@ -{"Ciao":"Ciao"} \ No newline at end of file diff --git a/test/data/integration/pt.js b/test/data/integration/pt.js deleted file mode 100644 index fcf7a36..0000000 --- a/test/data/integration/pt.js +++ /dev/null @@ -1 +0,0 @@ -{"E muito bom": "E muito bom"} \ No newline at end of file diff --git a/test/helpers/stores.js b/test/helpers/stores.js deleted file mode 100644 index e0b35b0..0000000 --- a/test/helpers/stores.js +++ /dev/null @@ -1,28 +0,0 @@ -var assert = require('assert'); - -// ALL stores should add those tests -module.exports = function (args) { - return { - 'GIVEN a store': { - topic: function () { - require('./../..').store(args, this.callback); - }, - - 'THEN it should have a GET method': function (store) { - assert.isFunction(store.get); - }, - - 'THEN it should have a SET method': function (store) { - assert.isFunction(store.set); - }, - - 'THEN it should have a DESTROY method': function (store) { - assert.isFunction(store.destroy); - }, - - 'THEN it should have a LENGTH method': function (store) { - assert.isFunction(store.length); - } - } - }; -} diff --git a/test/integration_test.js b/test/integration_test.js index f45d47e..4910cdf 100644 --- a/test/integration_test.js +++ b/test/integration_test.js @@ -101,9 +101,15 @@ var fs = require('fs'), // JSON if (locale === dialect.config('base_locale')) { - test(assert.equal, [original, JSON.parse(fs.readFileSync(dialect.config('path') + locale + '.js').toString())[original]]); + test(assert.equal, [ + original, + JSON.parse(fs.readFileSync(dialect.config('path') + locale + '.js').toString())[original].translation + ]); } else { - test(assert.equal, [JSON.parse(fs.readFileSync(dialect.config('path') + locale + '.js').toString())[original], null]); + test(assert.equal, [ + JSON.parse(fs.readFileSync(dialect.config('path') + locale + '.js').toString())[original].translation, + null + ]); } // Memory (Missing translations always get same str) @@ -129,7 +135,10 @@ var fs = require('fs'), test(assert.equal, [data[0].translation, translation]); // JSON available on this local machine, but on other they need to sync - test(assert.equal, [JSON.parse(fs.readFileSync(dialect.config('path') + locale + '.js').toString())[original], translation]); + test(assert.equal, [ + JSON.parse(fs.readFileSync(dialect.config('path') + locale + '.js').toString())[original].translation, + translation + ]); // Memory available on this local machine test(assert.equal, [dialect.getTranslation(original), translation]); @@ -161,7 +170,6 @@ var fs = require('fs'), // get Store and empty the DB require('./..').store({store: 'mongodb', database: 'test_2'}, function (error, store) { store.collection.remove({}, function (err, data) { - // GIVEN a store // ============= var original = 'E muito bom', translation = 'Esta molt be'; @@ -224,8 +232,14 @@ var fs = require('fs'), // THEN it should get it from DB files and populate JSON and memory again // ====================================================================== translated_string = dialect.getTranslation(original, function (err, data) { - test(assert.equal, [JSON.parse(fs.readFileSync(dialect.config('path') + 'it.js').toString())[original], original]); - test(assert.equal, [JSON.parse(fs.readFileSync(dialect.config('path') + 'fr.js').toString())[original], translation]); + test(assert.equal, [JSON.parse(fs.readFileSync( + dialect.config('path') + 'it.js').toString())[original].translation, + original + ]); + test(assert.equal, [JSON.parse(fs.readFileSync( + dialect.config('path') + 'fr.js').toString())[original].translation, + translation + ]); exit(); }); @@ -258,13 +272,14 @@ var fs = require('fs'), require('./..').store({store: 'mongodb', database: 'test_4'}, function (error, store) { store.collection.remove({}, function (err, data) { - var options = {count: 1, context: 'females', what: 'good'}, + var options = {count: 1, context: 'females', name: 'Anna'}, original = [ - 'You have {count} {what} friend', - 'You have {count} {what} friends', + 'You have {count} friend called {name}', + 'You have {count} friends called {name}', options ], - translation = 'Tienes 1 buena amiga'; // If this works, I'm a genius + translation = 'Tienes {count} amiga llamada {name}', + parsed_translation = 'Tienes 1 amiga llamada Anna'; // If this works, I'm a genius dialect.config('store', store); @@ -324,38 +339,46 @@ var fs = require('fs'), } }()); - // Memory (Missing translations always get same str) - test(assert.equal, [dialect.getTranslation(original), original]); + // Memory (Missing translations get same str but parsed) + test(assert.equal, [dialect.getTranslation(original), 'You have 1 friend called Anna']); }); fk.parallel(function () { - exit(); - /* // WHEN a translator introduces a translation to a target locale // ============================================================= - translation = 'Me encanta el gazpacho'; - locale = 'es'; - dialect.setTranslation({original: original, locale: locale}, translation, function () { + dialect.setTranslation({ + original: original[0], + locale: dialect.config('current_locale'), + context: 'female', + count: 'singular' + }, translation, function () { + // THEN it should save it to the Store, JSON and Memory for each locale // ==================================================================== - dialect.config('store').get({original: original, locale: locale}, function (err, data) { + dialect.config('store').get( + {original: original[0], locale: dialect.config('current_locale')}, function (err, data) { + var locale = dialect.config('current_locale'); // DB test(assert.equal, [data[0].locale, locale]); - test(assert.equal, [data[0].original, original]); + test(assert.equal, [data[0].original, original[0]]); test(assert.equal, [data[0].translation, translation]); // JSON available on this local machine, but on other they need to sync - test(assert.equal, [JSON.parse(fs.readFileSync(dialect.config('path') + locale + '.js').toString())[original], translation]); + test(assert.equal, [ + JSON.parse(fs.readFileSync(dialect.config('path') + locale + '.js').toString())[original[0]].translation, + 'Tienes {count} amiga llamada {name}' + ]); // Memory available on this local machine - test(assert.equal, [dialect.getTranslation(original), translation]); + test(assert.equal, [dialect.getTranslation(original), parsed_translation]); + + exit(); // OMG, I'm a genius! }); }); - */ }); }); diff --git a/test/store_test.js b/test/store_test.js deleted file mode 100644 index e557c97..0000000 --- a/test/store_test.js +++ /dev/null @@ -1,39 +0,0 @@ -GLOBAL.inspect = require('eyes').inspector({ styles: { all: 'yellow', label: 'underline', other: 'inverted', key: 'bold', special: 'grey', string: 'green', number: 'red', bool: 'blue', regexp: 'green' }, maxLength: 9999999999 }); - -var suite = require('./../support/vows/lib/vows').describe('STORE'), - assert = require('assert'); - -suite -.addBatch({ - 'GIVEN a store': { - 'WHEN we try to instantiate it without any params': { - topic: [], - - 'THEN it should raise an error': function (topic) { - assert.throws(function () { - require('./..').store.apply(this, topic); - }, Error); - } - }, - - 'WHEN we try to instantiate it with an unexistant store': { - topic: [{store: 'foo'}], - - 'THEN it should raise an error': function (topic) { - assert.throws(function () { - require('./..').store.apply(this, topic); - }, Error); - } - }, - - 'WHEN we try to instantiate it with a proper store': { - topic: [{store: 'mongodb', database: 'bla'}], - - 'THEN it should NOT raise a store error': function (topic) { - assert.doesNotThrow(function () { - require('./..').store.apply(this, topic); - }, Error, "This store is not available"); - } - } - } -}).export(module); diff --git a/test/stores/mongodb_test.js b/test/stores/mongodb_test.js deleted file mode 100644 index 3bd59f1..0000000 --- a/test/stores/mongodb_test.js +++ /dev/null @@ -1,80 +0,0 @@ -GLOBAL.inspect = require('eyes').inspector({ styles: { all: 'yellow', label: 'underline', other: 'inverted', key: 'bold', special: 'grey', string: 'green', number: 'red', bool: 'blue', regexp: 'green' }, maxLength: 9999999999 }); -var assert = require('assert'); - -exports.getCleanDatabase = function (callback) { - require(__dirname + '/../..').store({store: 'mongodb', database: 'test'}, function (error, mongoStore) { - mongoStore.collection.remove({}, function (err, data) { - callback(null, mongoStore); - }); - }); -}; - -exports.common = require('./../helpers/stores')({store: 'mongodb', database: 'test'}); - -exports.spec = { - 'GIVEN a new mongo store': { - 'WHEN we try to instantiate it without a database': { - topic: [{store: 'mongodb'}], - - 'THEN it should raise an error': function (topic) { - assert.throws(function () { - require('./../..').store.apply(this, topic); - }, Error); - } - }, - - 'WHEN we try to instantiate it with a database': { - topic: [{store: 'mongodb', database: 'test'}, function () {}], - - 'THEN it should NOT raise an error': function (topic) { - assert.doesNotThrow(function () { - require('./../..').store.apply(this, topic); - }, Error); - } - } - }, - - 'GIVEN a correct mongo store': { - topic: function () { - exports.getCleanDatabase(this.callback); - }, - - 'WHEN we INSERT a translation': { - topic: function (mongoStore) { - mongoStore.set('Hello', 'Hola', 'es', this.callback); - }, - - // CLEAN this rubish. I still don't get how vow deals with async... - 'THEN it should be inserted correctly to the database': function (error, mongoStore) { - mongoStore.get({original: 'Hello', locale: 'es'}, function (err, data) { - assert.equal(data[0].locale, 'es'); - assert.equal(data[0].original, 'Hello'); - assert.equal(data[0].translation, 'Hola'); - - var funk = require('./../../support/funk/lib/funk')(); - - mongoStore.length('es', funk.add(function (err, length) { - assert.equal(length, 1); - })); - - mongoStore.length('en', funk.add(function (err, length) { - assert.isZero(length); - })); - - funk.parallel(function () { - mongoStore.destroy({original: 'Hello', locale: 'es'}, function (err, data) { - // not found - mongoStore.get({original: 'Hello', locale: 'es'}, function (err, data) { - assert.isEmpty(data); - }); - - mongoStore.length('es', function (err, length) { - assert.isZero(length); - }); - }); - }); - }); - } - } - } -}; diff --git a/test/stores/redis_test.js b/test/stores/redis_test.js deleted file mode 100644 index 356b2cb..0000000 --- a/test/stores/redis_test.js +++ /dev/null @@ -1,5 +0,0 @@ -var assert = require('assert'); - -exports.common = require('./../helpers/stores')({store: 'redis', database: 'dev'}); - -exports.spec = {};