├── .gitignore ├── Makefile ├── Readme.md ├── benchmarks └── hello_world.js ├── examples ├── contexts.js ├── hello_world.js └── plurals.js ├── index.js ├── lib ├── dialect.js ├── helpers │ ├── io.js │ ├── plurals.js │ └── sqlizer.js └── stores │ ├── mongodb.js │ └── sqlite.js ├── package.json └── test ├── dialect.js ├── io.js ├── sqlizer.js └── stores ├── mongodb.js └── sqlite.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NODE = node 2 | 3 | test: test_dialect test_helpers test_stores 4 | 5 | test_dialect: 6 | @$(NODE) test/dialect.js 7 | 8 | test_helpers: 9 | @$(NODE) test/io.js 10 | @$(NODE) test/sqlizer.js 11 | 12 | test_stores: 13 | @$(NODE) test/stores/mongodb.js 14 | @$(NODE) test/stores/sqlite.js 15 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ,, ,, ,, 2 | `7MM db `7MM mm 3 | MM MM MM 4 | ,M""bMM `7MM ,6"Yb. MM .gP"Ya ,p6"bo mmMMmm 5 | ,AP MM MM 8) MM MM ,M' Yb 6M' OO MM 6 | 8MI MM MM ,pm9MM MM 8M"""""" 8M MM 7 | `Mb MM MM 8M MM MM YM. , YM. , MM 8 | `Wbmd"MML..JMML.`Moo9^Yo..JMML.`Mbmmd' YMbmd' `Mbmo 9 | 10 | 11 | Dialect is a painless nodejs module to manage your translations. 12 | 13 | ## Install 14 | 15 | npm install dialect 16 | 17 | ## Tutorial screencast 18 | 19 | [Dealing with translations in nodejs](http://happynerds.tumblr.com/post/5161855930/dealing-with-translations-in-nodejs) 20 | 21 | ## Philosphy 22 | 23 | * Scalable: The translations should be available to any number of machines. 24 | * Fast: Getting translations from memory. 25 | * Reliable: Translations should be always available on a central repository/database. 26 | * Flexible: You should be able to use your favorite storage solution. 27 | 28 | ## Example 29 | 30 | var dialect = require('dialect').dialect({current_locale: 'es', store: {mongodb: {}}}); 31 | 32 | // connects to the store 33 | dialect.connect(function () { 34 | 35 | // syncs the memory dictionaries with the store 36 | dialect.sync({interval:3600}, function (err, foo) { 37 | d.get('Hello World!'); // => Hola mundo 38 | }); 39 | }); 40 | 41 | ## Options 42 | 43 | * `current_locale`: Current locale used on your application. 44 | * `base_locale`: Base locale. Serves as keys on the dictionaries. 45 | * `locales`: Which locales are available on your application. 46 | * `store`: Object containing the store and their options 47 | 48 | ## Store options 49 | * `mongodb` 50 | * `database`: _dialect_ 51 | * `host`: _127.0.0.1_ 52 | * `port`: _27017_ 53 | * `collection`: _translations_ 54 | * `username` (optional) 55 | * `password` (optional) 56 | * `sqlite` 57 | * `database`: _dialect.db_ 58 | * `table`: _dialect_ 59 | 60 | ## API 61 | 62 | * `config (key, value)`: Exposes configuration values. 63 | * `get (query)`: Gets a translation cached in memory. 64 | * `set (query, translation, callback)`: Sets a translation on the store. 65 | * `approve (approve?, query, callback)`: Approve or rejects a translation. 66 | * `sync (locale, repeat, callback)`: Syncs all the approved translations of the store to the memory cache. 67 | * `connect (callback)`: Connects to the database store. 68 | 69 | ### Plurals 70 | 71 | Provide an array with the singular and plural forms of the string, 72 | the last element must contain a `count` param that will determine 73 | which plural form to use. 74 | 75 | dialect.config('current_locale': 'sl'); // slovenian 76 | 77 | [1, 2, 3].forEach(function (i) { 78 | dialect.get(['Beer', 'Beers', {count: i}]); 79 | }); 80 | 81 | +---------------+-------------+ 82 | | found | not found | 83 | +---------------+-------------+ 84 | | Pivo | Beer | 85 | | Pivi | Beers | 86 | | Piva | Beers | 87 | +---------------+-------------+ 88 | 89 | You have an examle using plural forms in `examples/plurals.js` 90 | 91 | 92 | ### Contexts 93 | 94 | A `context` is a param that allows you to give a special meaning 95 | on a string. It helps the translator and it may generate 96 | diferent translations depending on the context. 97 | 98 | dialect.config('current_locale': 'es'); // spanish 99 | 100 | ['female', 'male'].forEach(function (gender) { 101 | dialect.get(['My friends', gender]); 102 | }); 103 | 104 | +---------------+-------------+ 105 | | found | not found | 106 | +---------------+-------------+ 107 | | Mis amigos | My friends | 108 | | Mis amigas | My friends | 109 | +---------------+-------------+ 110 | 111 | You have an examle using contexts in `examples/contexts.js` 112 | 113 | ### String interpolation 114 | 115 | You can put any param you want on the translation strings surrounded 116 | by moustaches `{}`. Remember that `count` and `context` have a special 117 | meaning although they can also be used with interpolations. 118 | 119 | [1, 2].forEach(function (count) { 120 | ['female', 'male'].forEach(function (gender) { 121 | dialect.get([ 122 | 'You have {count} friend called {name}', 123 | 'You have {count} friends called {name}', 124 | {count: count, context: context, name: 'Anna'} 125 | ]); 126 | }); 127 | }); 128 | 129 | +---------------------------------------+-----------------------------------------+ 130 | | found | not found | 131 | +---------------------------------------+-----------------------------------------+ 132 | | Tienes 1 amiga que se llama Anna | You have 1 friend called Anna | 133 | | Tienes 1 amigo que se llama Anna | You have 1 friend called Anna | 134 | | Tienes 2 amigas que se llaman Anna | You have 2 friends called Anna | 135 | | Tienes 2 amigos que se llaman Anna | You have 2 friends called Anna | 136 | +---------------------------------------+-----------------------------------------+ 137 | 138 | You have an examle using contexts in `examples/interpolation.js` 139 | 140 | ### Store translations 141 | 142 | To store a new translation, use the method `set`. 143 | 144 | dialect.set( 145 | {original: 'I love gazpacho', locale: 'es'}, 146 | 'Me encanta el gazpacho' 147 | ); 148 | 149 | ## dialect-http 150 | 151 | Do you need a nice environment for your translators? 152 | 153 | [dialect http](https://github.com/masylum/dialect-http) is an amazing http server to manage your translations. 154 | 155 | ## Test 156 | 157 | Dialect is heavily tested using [testosterone](https://www.github.com/masylum/testosterone) 158 | 159 | make 160 | 161 | ## Benchmarks 162 | 163 | Dialect should not add an overhead to your application on getting translations. 164 | Please run/add benchmarks to ensure that this module performance rocks. 165 | 166 | node benchmakrs/hello_world.js 167 | 168 | 169 | ## License 170 | 171 | (The MIT License) 172 | 173 | Copyright (c) 2010-2011 Pau Ramon 174 | 175 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 176 | 177 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 178 | 179 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 180 | 181 | -------------------------------------------------------------------------------- /benchmarks/hello_world.js: -------------------------------------------------------------------------------- 1 | var dialect = require('..').dialect({ 2 | locales: ['es', 'en'], 3 | current_locale: 'es', 4 | store: {mongodb: {}} // test your store here 5 | }), 6 | funk = require('funk')(), 7 | funk2 = require('funk')(), 8 | times = 300, 9 | _ = dialect.get, 10 | original = 'Hello World!', 11 | translation = 'Hola Mundo!'; 12 | 13 | dialect.connect(function () { 14 | var i = 0, 15 | now = Date.now(), 16 | time = null; 17 | 18 | console.log('Setting ' + times + ' translations...'); 19 | 20 | for (i = 0; i < times; i ++) { 21 | _(original); 22 | dialect.set({original: original, locale: 'es'}, translation, funk.nothing()); 23 | 24 | //dialect.sync({}, function (err, foo) { 25 | //_(original); 26 | //_('Inexistant'); 27 | //}); 28 | } 29 | 30 | funk.parallel(function () { 31 | time = Date.now() - now; 32 | console.log(time + 'ms'); 33 | console.log(parseInt(1000 / ( time / times)) + ' sets/sec'); 34 | }); 35 | 36 | dialect.sync({}, function (err, foo) { 37 | now = Date.now(); 38 | time = null; 39 | 40 | console.log('Getting ' + times + ' translations...'); 41 | for (i = 0; i < times; i ++) { 42 | _(original); 43 | } 44 | time = Date.now() - now; 45 | console.log(time + 'ms'); 46 | console.log(parseInt(1000 / (time / times)) + ' gets/sec'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/contexts.js: -------------------------------------------------------------------------------- 1 | var dialect = require('..').dialect({ 2 | locales: ['es', 'en'], 3 | current_locale: 'es', 4 | store: {mongodb: {}} 5 | }), 6 | _ = dialect.get; 7 | 8 | dialect.connect(function () { 9 | console.log(_(['Fight', {context: 'name'}])); 10 | console.log(_(['Fight', {context: 'verb'}])); 11 | 12 | dialect.set({original: 'Fight', locale: 'es', context: 'name'}, 'Lucha'); 13 | dialect.set({original: 'Fight', locale: 'es', context: 'verb'}, 'Luchar'); 14 | 15 | dialect.approve({original: 'Fight', locale: 'es', context: 'name'}, true); 16 | dialect.approve({original: 'Fight', locale: 'es', context: 'verb'}, true); 17 | 18 | dialect.sync({interval: 3600}, function (err, foo) { 19 | console.log(_(['Fight', {context: 'name'}])); 20 | console.log(_(['Fight', {context: 'verb'}])); 21 | process.exit(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/hello_world.js: -------------------------------------------------------------------------------- 1 | var dialect = require('..').dialect({ 2 | locales: ['es', 'en'], 3 | current_locale: 'es', 4 | store: {mongodb: {}} 5 | }), 6 | _ = dialect.get, 7 | original = 'Hello World!', 8 | translation = 'Hola Mundo!'; 9 | 10 | dialect.connect(function () { 11 | console.log(_(original)); 12 | 13 | dialect.set({original: original, locale: 'es'}, translation); 14 | dialect.approve({original: original, locale: 'es'}, true); 15 | 16 | dialect.sync({interval: 3600}, function (err, foo) { 17 | console.log(_(original)); 18 | console.log(_('Inexistant')); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/plurals.js: -------------------------------------------------------------------------------- 1 | var dialect = require('..').dialect({ 2 | locales: ['es', 'en'], 3 | current_locale: 'es', 4 | store: {mongodb: {}} 5 | }), 6 | _ = dialect.get; 7 | 8 | dialect.connect(function () { 9 | [1, 2, 3].forEach(function (i) { 10 | console.log(_(['{count} Beer', '{count} Beers', {count: i}])); 11 | }); 12 | 13 | dialect.set({original: '{count} Beer', locale: 'es', plural: 1}, '{count} pivo'); 14 | dialect.set({original: '{count} Beer', locale: 'es', plural: 2}, '{count} pivi'); 15 | dialect.set({original: '{count} Beer', locale: 'es', plural: 3}, '{count} piva'); 16 | 17 | dialect.approve({original: '{count} Beer', locale: 'es', plural: 1}, true); 18 | dialect.approve({original: '{count} Beer', locale: 'es', plural: 2}, true); 19 | dialect.approve({original: '{count} Beer', locale: 'es', plural: 3}, true); 20 | 21 | dialect.sync({interval: 3600}, function (err, foo) { 22 | [1, 2, 3].forEach(function (i) { 23 | console.log(_(['{count} Beer', '{count} Beers', {count: i}])); 24 | }); 25 | process.exit(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.dialect = require('./lib/dialect'); 2 | -------------------------------------------------------------------------------- /lib/dialect.js: -------------------------------------------------------------------------------- 1 | if (global.GENTLY) { 2 | require = global.GENTLY.hijack(require); 3 | } 4 | 5 | module.exports = function (options) { 6 | var DIALECT = {}, 7 | 8 | _io = require('./helpers/io').IO(DIALECT), 9 | _options = options || {}, 10 | 11 | _parse = function (str, params) { 12 | var matches = /(\{(.*?)\})+/g.exec(str); 13 | 14 | // shameless copy/inspiration from lingo (TJ) 15 | return str.replace(/\{([^}]+)\}/g, function (_, key) { 16 | return params[key]; 17 | }); 18 | }; 19 | 20 | Object.defineProperty(DIALECT, 'store', {value : null, writable: true}); 21 | Object.defineProperty(DIALECT, 'dictionaries', {value : {}, writable: true}); 22 | 23 | /** 24 | * Set or Get a config value 25 | * 26 | * This exposes the _options and set up 27 | * defaults values. 28 | * 29 | * @param {String} key 30 | * Name of the attribute to set/get. 31 | * @param {*} [value] 32 | * Value to store. 33 | * @returns 34 | * The value at `key` if given 35 | * or the whole _options object 36 | */ 37 | 38 | DIALECT.config = function (key, value) { 39 | if (value !== undefined) { 40 | _options[key] = value; 41 | } 42 | return key ? _options[key] : _options; 43 | }; 44 | 45 | /** 46 | * Connects the store 47 | * 48 | * @param {Function} cb 49 | * @return dialect 50 | */ 51 | DIALECT.connect = function (cb) { 52 | if (DIALECT.store.is_connected()) { 53 | cb('already connected', null); 54 | } else { 55 | DIALECT.store.connect(cb); 56 | } 57 | return DIALECT; 58 | }; 59 | 60 | 61 | /** 62 | * Get a translation from the memory cache. 63 | * 64 | * If the original is not available 65 | * add the new word to the `store` 66 | * 67 | * @param {String} original 68 | * String we want to translate. 69 | */ 70 | DIALECT.get = function (original) { 71 | var translation = null, 72 | key = null, 73 | current_dictionary = DIALECT.dictionaries[_options.current_locale], 74 | params = Array.isArray(original) ? original.pop() : {}, 75 | needs_plural = Array.isArray(original) && original.length === 2, 76 | index = Array.isArray(original) ? original[0] : original, 77 | pluralize = require('./helpers/plurals')(_options.base_locale); 78 | 79 | if ((typeof original !== 'string' && !Array.isArray(original)) || original.length === 0) { 80 | throw Error("Original is not valid"); 81 | } 82 | 83 | if (!current_dictionary || _options.base_locale === _options.current_locale) { 84 | return _parse(needs_plural ? original[pluralize(params.count)] : index, params); 85 | } else { 86 | 87 | key = _io.getKeyFromQuery({ 88 | original: index, 89 | count: params.count, 90 | context: params.context 91 | }); 92 | 93 | if (Object.keys(current_dictionary).length > 0) { 94 | translation = current_dictionary[key]; 95 | } 96 | 97 | if (typeof translation !== 'string') { 98 | DIALECT.store.add( 99 | {original: index, locale: _options.current_locale}, 100 | translation 101 | ); 102 | return _parse(needs_plural ? original[pluralize(params.count)] : index, params); 103 | } else { 104 | return _parse(translation, params); 105 | } 106 | 107 | } 108 | }; 109 | 110 | /** 111 | * Approves or rejects a translation 112 | * 113 | * @param {Object} query {original, locale [, count] [, context]} 114 | * @param {Boolean} aproved 115 | * @param {Function} callback. 116 | * 117 | * @return dialect 118 | */ 119 | DIALECT.approve = function (query, approved, cb) { 120 | if (!query || !query.original || !query.locale) { 121 | throw Error("Original string and target locale are mandatory"); 122 | } 123 | 124 | if (typeof approved === 'function' || approved === undefined) { 125 | throw Error("Approved is mandatory"); 126 | } 127 | 128 | // Database 129 | DIALECT.store.approve(query, !!approved, cb); 130 | 131 | return DIALECT; 132 | }; 133 | 134 | /** 135 | * Sets the translation to the store. 136 | * 137 | * @param {Object} query {original, locale [, count] [, context]} 138 | * @param {String} translation 139 | * @param {Function} callback. 140 | * 141 | * @return dialect 142 | */ 143 | DIALECT.set = function (query, translation, cb) { 144 | if (!query || !query.original || !query.locale) { 145 | throw Error("Original string and target locale are mandatory"); 146 | } 147 | 148 | if (typeof translation !== 'string') { 149 | throw Error("Translation is mandatory"); 150 | } 151 | 152 | if (translation.length === 0) { 153 | translation = null; 154 | } 155 | 156 | // Database 157 | DIALECT.store.set(query, {translation: translation}, cb); 158 | 159 | return DIALECT; 160 | }; 161 | 162 | /** 163 | * Loads the dictionaries from the store 164 | * and caches them to memory 165 | * 166 | * @param {String} locale 167 | * @param {Function} cb 168 | * @return dialect 169 | */ 170 | DIALECT.sync = function (options, cb) { 171 | var executed = false, 172 | funk = require('funk')(), 173 | 174 | run_once = function (cb) { 175 | return function () { 176 | if (!executed) { 177 | executed = true; 178 | cb(); 179 | } 180 | }; 181 | }, 182 | 183 | fn = function (cb) { 184 | if (options.locale === undefined) { 185 | _options.locales.forEach(function (locale) { 186 | _io.cacheDictionary(locale, funk.nothing()); 187 | }); 188 | funk.parallel(run_once(cb)); 189 | } else { 190 | _io.cacheDictionary(options.locale, run_once(cb)); 191 | } 192 | }; 193 | 194 | if (options.interval) { 195 | fn(function () { 196 | cb(); 197 | DIALECT.interval = setInterval(fn, options.interval, function () { }); 198 | }); 199 | } else { 200 | fn(cb); 201 | } 202 | 203 | return DIALECT; 204 | }; 205 | 206 | 207 | // INIT 208 | if (!_options || !_options.store) { 209 | throw Error("You need to provide a store"); 210 | } else { 211 | try { 212 | (function () { 213 | var lib = typeof _options.store === 'string' ? _options.store : Object.keys(_options.store)[0]; 214 | DIALECT.store = require('./stores/' + lib)(_options.store[lib]); 215 | }()); 216 | } catch (exc) { 217 | throw exc; 218 | } 219 | } 220 | 221 | // defaults 222 | _options.base_locale = _options.base_locale || 'en'; 223 | _options.current_locale = _options.current_locale || 'en'; 224 | _options.locales = _options.locales || (_options.base_locale === _options.current_locale 225 | ? [_options.base_locale] 226 | : [_options.base_locale, _options.current_locale]); 227 | 228 | return DIALECT; 229 | }; 230 | -------------------------------------------------------------------------------- /lib/helpers/io.js: -------------------------------------------------------------------------------- 1 | if (global.GENTLY) { 2 | require = global.GENTLY.hijack(require); 3 | } 4 | 5 | var plurals = require('./plurals'); 6 | 7 | module.exports.IO = function (DIALECT) { 8 | var IO = {}, 9 | 10 | /** 11 | * Gets the plural form 12 | * 13 | * @param {Integer} count 14 | * @returns a plural form 15 | */ 16 | _getPluralForm = function (count) { 17 | return plurals(DIALECT.config('current_locale'))(count) + 1; 18 | }; 19 | 20 | /** 21 | * Gets a query and returns a dictionary key 22 | * 23 | * @param {String} query 24 | * @returns a dictionary key 25 | */ 26 | IO.getKeyFromQuery = function (query) { 27 | var key = query.original; 28 | 29 | if (!key) { 30 | throw Error("Original must be provided"); 31 | } 32 | 33 | if (query.count !== undefined) { 34 | key += '|p:' + _getPluralForm(query.count); 35 | } 36 | 37 | if (query.context) { 38 | key += '|c:' + query.context; 39 | } 40 | 41 | return key; 42 | }; 43 | 44 | /** 45 | * Caches the dictionaries from the Store to the JSON files 46 | * 47 | * @param {String} locale 48 | * Target dictionary we want to cache 49 | * @param {Function} cb 50 | * Callback when its done with async 51 | * @returns IO 52 | */ 53 | IO.cacheDictionary = function (locale, cb) { 54 | 55 | if (!locale) { 56 | throw Error("You must provide a locale"); 57 | } 58 | 59 | DIALECT.store.get({locale: locale, approved: true}, function (err, data) { 60 | var dictionary = {}, 61 | key = null, 62 | i = null; 63 | 64 | if (err) { 65 | cb(err, null); 66 | } else { 67 | for (i in data) { 68 | key = IO.getKeyFromQuery({ 69 | original: data[i].original, 70 | count: data[i].plural, 71 | context: data[i].context 72 | }); 73 | dictionary[key] = data[i].translation; 74 | } 75 | 76 | // loads the dictionary to memory 77 | DIALECT.dictionaries[locale] = dictionary; 78 | 79 | cb(err, dictionary); 80 | } 81 | }); 82 | 83 | return IO; 84 | }; 85 | 86 | return IO; 87 | }; 88 | -------------------------------------------------------------------------------- /lib/helpers/plurals.js: -------------------------------------------------------------------------------- 1 | /* plurals.js provide functions that give you the plural index 2 | * for any locale. 3 | * 4 | * Usage: 5 | * require('plurals')('es')(3) => 1; 6 | * require('plurals')('es')(1) => 0; 7 | * 8 | * please, add your language if its not represented. 9 | * 10 | * references: 11 | * 12 | * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html 13 | * http://translate.sourceforge.net/wiki/l10n/pluralforms 14 | * http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms 15 | * 16 | */ 17 | 18 | module.exports = function plurals(locale) { 19 | var parts = locale.split('-'); 20 | 21 | switch (locale) { 22 | 23 | // 1 Plural 24 | case 'ja': 25 | case 'vi': 26 | case 'ko': 27 | return function (n) { 28 | return 1; 29 | }; 30 | 31 | // 2 Plurals 32 | case 'pt-BR': 33 | case 'fr': 34 | return function (n) { 35 | return n > 1 ? 1 : 0; 36 | }; 37 | 38 | // 3 Plurals 39 | case 'lv': 40 | return function (n) { 41 | return n % 10 === 1 && n % 100 !== 11 ? 0 : n !== 0 ? 1 : 2; 42 | }; 43 | case 'br': 44 | case 'ga': 45 | case 'gd': 46 | case 'cy': 47 | return function (n) { 48 | return n === 1 ? 0 : n === 2 ? 1 : 2; 49 | }; 50 | case 'ro': 51 | return function (n) { 52 | return n === 1 ? 0 : (n === 0 || (n % 100 > 0 && n % 100 < 20)) ? 1 : 2; 53 | }; 54 | case 'lt': 55 | return function (n) { 56 | return n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2; 57 | }; 58 | case 'ru': 59 | case 'uk': 60 | case 'sr': 61 | case 'hr': 62 | case 'sh': 63 | return function (n) { 64 | return n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2; 65 | }; 66 | case 'cs': 67 | case 'sk': 68 | return function (n) { 69 | return (n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2; 70 | }; 71 | case 'pl': 72 | return function (n) { 73 | return n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2; 74 | }; 75 | case 'mk': 76 | return function (n) { 77 | return n === 1 ? 0 : n % 10 >= 2 ? 1 : 2; 78 | }; 79 | case 'sl': 80 | return function (n) { 81 | return n % 100 === 1 ? 0 : n % 100 === 2 ? 1 : n % 100 === 3 || n % 100 === 4 ? 2 : 3; 82 | }; 83 | 84 | // 2 Plurals 85 | default: 86 | if (parts.length === 2) { 87 | return plurals(parts[0]); 88 | } else { 89 | return function (n) { 90 | return n === 1 ? 0 : 1; 91 | }; 92 | } 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /lib/helpers/sqlizer.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | var SQLizer = {}, 3 | 4 | _objectNotEmpty = function (o) { 5 | return o && Object.keys(o).length > 0; 6 | }, 7 | 8 | _parse = function (f) { 9 | var handleTypes = function (el) { 10 | switch (typeof el) { 11 | case 'string': 12 | return "'" + el + "'"; 13 | case 'boolean': 14 | return el ? 1 : 0; 15 | default: 16 | return el; 17 | } 18 | }; 19 | 20 | if (Array.isArray(f)) { 21 | return f.map(handleTypes); 22 | } else { 23 | return handleTypes(f); 24 | } 25 | }, 26 | 27 | _parseOptions = function (query) { 28 | var result = []; 29 | 30 | if (typeof query !== 'object' || query === null) { 31 | return [(query === null ? ' IS ' : ' = ') + _parse(query)]; 32 | } else { 33 | Object.keys(query).forEach(function (key) { 34 | var value = query[key], ops; 35 | 36 | switch (key) { 37 | case '$in': 38 | case '$nin': 39 | ops = { 40 | '$in': ' IN ', 41 | '$nin': ' NOT IN ' 42 | }; 43 | result.push(ops[key] + '(' + _parse(value).join(',') + ')'); 44 | break; 45 | case '$exists': 46 | result.push(' IS ' + (value ? 'NOT ' : '') + 'null'); 47 | break; 48 | case '$ne': 49 | case '$lt': 50 | case '$gt': 51 | case '$lte': 52 | case '$gte': 53 | ops = { 54 | '$ne': value === null ? ' IS NOT ' : ' <> ', 55 | '$lt': ' < ', 56 | '$gt': ' > ', 57 | '$lte': ' <= ', 58 | '$gte': ' >= ' 59 | }; 60 | result.push(ops[key] + _parse(value)); 61 | break; 62 | default: 63 | throw Error('`' + key + '` not implemented yet'); 64 | } 65 | }); 66 | 67 | return result; 68 | } 69 | }; 70 | 71 | if (!options.table) { 72 | throw Error('You must specify a table'); 73 | } 74 | 75 | SQLizer.table = options.table; 76 | 77 | SQLizer.parseDoc = function (query) { 78 | var result = []; 79 | 80 | Object.keys(query).forEach(function (key) { 81 | var value = query[key]; 82 | 83 | // ['> 4', '< 8', '= 4'] 84 | if (key === '$or') { 85 | (function () { 86 | var or = []; 87 | value.forEach(function (val) { 88 | or.push(SQLizer.parseDoc(val)); 89 | }); 90 | result.push('(' + or.join(' OR ') + ')'); 91 | }()); 92 | } else { 93 | _parseOptions(value).forEach(function (val) { 94 | result.push(key + val); 95 | }); 96 | } 97 | }); 98 | 99 | return result.join(' AND '); 100 | }; 101 | 102 | SQLizer.find = function (query, fields) { 103 | var i, 104 | selects = []; 105 | 106 | SQLizer.sql = 'SELECT '; 107 | 108 | if (!_objectNotEmpty(fields)) { 109 | SQLizer.sql += '*'; 110 | } else { 111 | for (i in fields) { 112 | if (fields[i] === 1) { 113 | selects.push(i); 114 | } 115 | } 116 | SQLizer.sql += selects.length ? (selects.join(', ')) : '*'; 117 | } 118 | 119 | SQLizer.sql += ' FROM ' + SQLizer.table; 120 | 121 | if (_objectNotEmpty(query)) { 122 | SQLizer.sql += ' WHERE ' + SQLizer.parseDoc(query); 123 | } 124 | 125 | return SQLizer; 126 | }; 127 | 128 | SQLizer.sort = function (doc) { 129 | var i, 130 | sorts = []; 131 | 132 | if (_objectNotEmpty(doc)) { 133 | SQLizer.sql += ' ORDER BY '; 134 | 135 | for (i in doc) { 136 | sorts.push(i + ' ' + (doc[i] > 0 ? 'ASC' : 'DESC')); 137 | } 138 | 139 | SQLizer.sql += sorts.join(', '); 140 | } 141 | 142 | return SQLizer; 143 | }; 144 | 145 | SQLizer.limit = function (num) { 146 | if (/(LIMIT) \d( OFFSET \d)/.test(SQLizer.sql)) { 147 | SQLizer.sql = SQLizer.sql.replace(/(LIMIT) \d( OFFSET \d)/, '$1 ' + num + '$2'); 148 | } else { 149 | SQLizer.sql += ' LIMIT ' + num + ' OFFSET 0'; 150 | } 151 | 152 | return SQLizer; 153 | }; 154 | 155 | SQLizer.skip = function (num) { 156 | if (/(LIMIT \d) (OFFSET )\d/.test(SQLizer.sql)) { 157 | SQLizer.sql = SQLizer.sql.replace(/(LIMIT \d) (OFFSET )\d/, '$1 $2' + num); 158 | } else { 159 | SQLizer.sql += ' LIMIT 0 OFFSET ' + num; 160 | } 161 | 162 | return SQLizer; 163 | }; 164 | 165 | SQLizer.count = function (num) { 166 | if (/SELECT (.*) FROM/.test(SQLizer.sql)) { 167 | SQLizer.sql = SQLizer.sql.replace(/(SELECT) .* (FROM)/, '$1 COUNT(*) $2'); 168 | } 169 | 170 | return SQLizer; 171 | }; 172 | 173 | SQLizer.insert = function (docs) { 174 | var parse = function (doc) { 175 | var i, 176 | values = [], 177 | sql = 'INSERT INTO ' + SQLizer.table + ' (' + Object.keys(doc).join(', ') + ')'; 178 | 179 | for (i in doc) { 180 | values.push(doc[i]); 181 | } 182 | 183 | sql += ' VALUES (' + _parse(values).join(', ') + ')'; 184 | 185 | return sql; 186 | }; 187 | 188 | if (Array.isArray(docs)) { 189 | SQLizer.sql = docs.map(parse).join('; '); 190 | } else { 191 | SQLizer.sql = parse(docs); 192 | } 193 | 194 | return SQLizer; 195 | }; 196 | 197 | SQLizer.update = function (where, update) { 198 | var i, 199 | value = null, 200 | updates = [], 201 | 202 | _parseUpdate = function (key, value) { 203 | var els = []; 204 | 205 | Object.keys(value).forEach(function (i) { 206 | var v = value[i]; 207 | 208 | switch (key) { 209 | case '$set': 210 | els.push(i + ' = ' + _parse(v)); 211 | break; 212 | case '$inc': 213 | if (v !== 0) { 214 | els.push(i + ' = ' + i + (v > 0 ? ' + ' : ' - ') + v); 215 | } 216 | } 217 | }); 218 | 219 | return els.join(', '); 220 | }; 221 | 222 | SQLizer.sql = 'UPDATE ' + SQLizer.table + ' SET '; 223 | 224 | if (_objectNotEmpty(update)) { 225 | for (i in update) { 226 | value = update[i]; 227 | if (typeof value === 'object') { 228 | updates.push(_parseUpdate(i, value)); 229 | } 230 | } 231 | } 232 | 233 | SQLizer.sql += updates.join(', '); 234 | 235 | if (_objectNotEmpty(where)) { 236 | SQLizer.sql += ' WHERE ' + SQLizer.parseDoc(where); 237 | } 238 | 239 | return SQLizer; 240 | }; 241 | 242 | SQLizer.remove = function (where) { 243 | 244 | SQLizer.sql = 'DELETE FROM ' + SQLizer.table; 245 | 246 | if (_objectNotEmpty(where)) { 247 | SQLizer.sql += ' WHERE ' + SQLizer.parseDoc(where); 248 | } 249 | 250 | return SQLizer; 251 | }; 252 | 253 | return SQLizer; 254 | }; 255 | -------------------------------------------------------------------------------- /lib/stores/mongodb.js: -------------------------------------------------------------------------------- 1 | var DB = require('mongodb').Db, 2 | Server = require('mongodb').Server; 3 | 4 | /** 5 | * Initialize Store with the given `options`. 6 | * 7 | * @param {Object} options [database, host, port, collection] 8 | * @return store 9 | */ 10 | 11 | module.exports = function (options) { 12 | 13 | var STORE = {}, 14 | 15 | _default = function (thing) { 16 | return thing || function () { }; 17 | }, 18 | 19 | _is_connected = false; 20 | 21 | options = options || {}; 22 | 23 | Object.defineProperty(STORE, 'db', {value : new DB( 24 | options.database || 'dialect', 25 | new Server( 26 | options.host || '127.0.0.1', 27 | options.port || 27017, 28 | {} 29 | ), 30 | {} 31 | )}); 32 | 33 | /** 34 | * Exposes is_connected 35 | * 36 | * @return is_connected 37 | */ 38 | STORE.is_connected = function () { 39 | return _is_connected; 40 | }; 41 | 42 | /** 43 | * Connects to the Store 44 | * 45 | * @param {Function} callback 46 | * @return store 47 | */ 48 | STORE.connect = function (callback) { 49 | 50 | callback = _default(callback); 51 | 52 | function connect(err, collection) { 53 | _is_connected = true; 54 | STORE.collection = collection; 55 | callback(err, collection); 56 | } 57 | 58 | function collectionSetup() { 59 | STORE.db.collection(options.collection || 'translations', function (err, collection) { 60 | if (collection) { 61 | collection.ensureIndex( 62 | {original: 1, locale: 1, translation: 1}, 63 | {unique: true}, 64 | function (err) { 65 | connect(err, collection); 66 | }); 67 | } else { 68 | STORE.db.createCollection(options.collection || 'translations', function (err, collection) { 69 | connect(err, collection); 70 | }); 71 | } 72 | }); 73 | } 74 | 75 | if (!_is_connected) { 76 | STORE.db.open(function (err, db) { 77 | if (err) { 78 | callback(err, null); 79 | } else { 80 | if (options.username && options.password) { 81 | db.authenticate(options.username, options.password, function (err) { 82 | if (err) { 83 | callback(err, null); 84 | } else { 85 | collectionSetup(); 86 | } 87 | }); 88 | } else { 89 | collectionSetup(); 90 | } 91 | } 92 | }); 93 | } 94 | 95 | return STORE; 96 | }; 97 | 98 | /** 99 | * Attempt to fetch a translation 100 | * 101 | * @param {Object} query 102 | * @param {Function} callback 103 | * @return store 104 | */ 105 | STORE.get = function (query, callback) { 106 | 107 | callback = _default(callback); 108 | query = query || {}; 109 | 110 | STORE.collection.find(query, function (err, cursor) { 111 | if (err) { 112 | callback(err, null); 113 | } else { 114 | cursor.toArray(callback); 115 | } 116 | }); 117 | 118 | return STORE; 119 | }; 120 | 121 | /** 122 | * Add a translation 123 | * 124 | * @param {Object} doc {original, locale, [, plural] [, context]} 125 | * @param {String} translation 126 | * @param {Function} callback 127 | * @return store 128 | */ 129 | STORE.add = function (doc, translation, callback) { 130 | 131 | callback = _default(callback); 132 | 133 | STORE.collection.findOne(doc, function (err, data) { 134 | if (err) { 135 | callback(err); 136 | } else { 137 | 138 | if (!data) { 139 | doc.translation = translation; 140 | doc.approved = false; 141 | STORE.collection.insert(doc, callback); 142 | } else { 143 | callback(Error('This translation already exists'), null); 144 | } 145 | 146 | } 147 | }); 148 | 149 | return STORE; 150 | }; 151 | 152 | /** 153 | * Set a translation 154 | * If the translation is new, set it to null 155 | * 156 | * @param {Object} query {original, locale} 157 | * @param {String} translation 158 | * @param {Function} callback 159 | * @return store 160 | */ 161 | STORE.set = function (query, translation, callback) { 162 | 163 | callback = _default(callback); 164 | query = query || {}; 165 | 166 | translation.approved = false; 167 | 168 | STORE.collection.update(query, {'$set': translation}, {upsert: true}, callback); 169 | 170 | return STORE; 171 | }; 172 | 173 | /** 174 | * Approve or rejects a translation 175 | * 176 | * @param {Object} query {original, locale} 177 | * @param {String} translation 178 | * @param {Function} callback 179 | * @return store 180 | */ 181 | STORE.approve = function (query, approved, callback) { 182 | 183 | callback = _default(callback); 184 | query = query || {}; 185 | 186 | STORE.collection.update(query, {'$set': {approved: approved}}, {}, callback); 187 | 188 | return STORE; 189 | }; 190 | 191 | /** 192 | * Destroy the translation 193 | * 194 | * @param {Object} query {original, locale} 195 | * @param {Function} callback 196 | * @return store 197 | */ 198 | 199 | STORE.destroy = function (query, callback) { 200 | 201 | callback = _default(callback); 202 | query = query || {}; 203 | 204 | STORE.collection.remove(query, callback); 205 | 206 | return STORE; 207 | }; 208 | 209 | /** 210 | * Fetch number of translations. 211 | * 212 | * @param {Object} query {locale, translation...} [optional] 213 | * @param {Function} callback 214 | * @return store 215 | */ 216 | 217 | STORE.count = function (query, callback) { 218 | 219 | callback = _default(callback); 220 | query = query || {}; 221 | 222 | STORE.collection.count(query, callback); 223 | 224 | return STORE; 225 | }; 226 | 227 | return STORE; 228 | }; 229 | -------------------------------------------------------------------------------- /lib/stores/sqlite.js: -------------------------------------------------------------------------------- 1 | var sqlite = require('sqlite'), 2 | sqlizer = require('../helpers/sqlizer'); 3 | 4 | /** 5 | * Initialize Store with the given `options`. 6 | * 7 | * @param {Object} options [database, table] 8 | * @return store 9 | */ 10 | module.exports = function (options) { 11 | 12 | options = options || {}; 13 | 14 | var STORE = {}, 15 | 16 | _table = options.table || 'dialect', 17 | 18 | _default = function (callback) { 19 | return callback || function () { }; 20 | }, 21 | 22 | _is_connected = false; 23 | 24 | Object.defineProperty(STORE, 'db', {value : new sqlite.Database()}); 25 | 26 | /** 27 | * Exposes is_connected 28 | * 29 | * @return is_connected 30 | */ 31 | STORE.is_connected = function () { 32 | return _is_connected; 33 | }; 34 | 35 | /** 36 | * Connects to the Store 37 | * 38 | * @param {Function} callback 39 | * @return store 40 | */ 41 | STORE.connect = function (callback) { 42 | 43 | callback = _default(callback); 44 | 45 | STORE.db.open(options.database || 'dialect.db', function (err) { 46 | if (err) { 47 | callback(err, null); 48 | } else { 49 | STORE.db.execute( 50 | 'CREATE TABLE IF NOT EXISTS ' + _table + 51 | ' (original TEXT, locale TEXT, translation TEXT,' + 52 | ' plural NUMBER, context TEXT, PRIMARY KEY(original, locale, plural, context))', 53 | function (err, data) { 54 | if (data) { 55 | _is_connected = true; 56 | } 57 | callback(err, data); 58 | } 59 | ); 60 | } 61 | }); 62 | 63 | return STORE; 64 | }; 65 | 66 | /** 67 | * Attempt to fetch a translation 68 | * 69 | * @param {Object} query 70 | * @param {Function} callback 71 | * @return store 72 | */ 73 | STORE.get = function (query, callback) { 74 | 75 | callback = _default(callback); 76 | 77 | STORE.db.execute( 78 | sqlizer({table: _table}).find(query).sql, 79 | function (error, rows) { 80 | callback(error, error ? [] : rows); 81 | } 82 | ); 83 | 84 | return STORE; 85 | }; 86 | 87 | /** 88 | * Add a translation 89 | * 90 | * @param {Object} doc {original, locale, [, plural] [, context]} 91 | * @param {String} translation 92 | * @param {Function} callback 93 | * @return store 94 | */ 95 | STORE.add = function (doc, translation, callback) { 96 | 97 | callback = _default(callback); 98 | 99 | STORE.get(doc, function (err, data) { 100 | if (err) { 101 | callback(err, null); 102 | } else { 103 | 104 | if (!data || data.length === 0) { 105 | doc.translation = translation; 106 | doc.approved = false; 107 | STORE.db.execute(sqlizer({table: _table}).insert(doc).sql, callback); 108 | } else { 109 | callback(Error('This translation already exists'), null); 110 | } 111 | 112 | } 113 | }); 114 | 115 | return STORE; 116 | }; 117 | 118 | /** 119 | * Set a translation 120 | * If the translation is new, set it to null 121 | * 122 | * @param {Object} query {original, locale} 123 | * @param {String} translation 124 | * @param {Function} callback 125 | * @return store 126 | */ 127 | STORE.set = function (query, translation, callback) { 128 | 129 | callback = _default(callback); 130 | 131 | STORE.db.execute(sqlizer({table: _table}).update( 132 | query, 133 | {'$set': {translation: translation, approved: false}}).sql, 134 | callback 135 | ); 136 | 137 | return STORE; 138 | }; 139 | 140 | /** 141 | * Approve or rejects a translation 142 | * 143 | * @param {Object} query {original, locale} 144 | * @param {Boolean} approved 145 | * @param {Function} callback 146 | * @return store 147 | */ 148 | STORE.approve = function (query, approved, callback) { 149 | 150 | callback = _default(callback); 151 | 152 | STORE.db.execute(sqlizer({table: _table}).update(query, {'$set': {approved: approved}}).sql, callback); 153 | 154 | return STORE; 155 | }; 156 | 157 | /** 158 | * Destroy the translation 159 | * 160 | * @param {Object} query {original, locale} 161 | * @param {Function} callback 162 | * @return store 163 | */ 164 | 165 | STORE.destroy = function (query, callback) { 166 | 167 | callback = _default(callback); 168 | 169 | STORE.db.execute(sqlizer({table: _table}).remove(query).sql, callback); 170 | 171 | return STORE; 172 | }; 173 | 174 | /** 175 | * Fetch number of translations. 176 | * 177 | * @param {Object} query {locale, translation...} [optional] 178 | * @param {Function} callback 179 | * @return store 180 | */ 181 | STORE.count = function (query, callback) { 182 | 183 | callback = _default(callback); 184 | 185 | STORE.db.execute(sqlizer({table: _table}).find(query).count().sql, function (err, data) { 186 | callback(err, !err && data && data.length ? data[0].count : 0); 187 | }); 188 | 189 | return STORE; 190 | }; 191 | 192 | return STORE; 193 | }; 194 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dialect", 3 | "description": "Translations manager for nodejs", 4 | "version": "1.0.4", 5 | "author": "Pau Ramon ", 6 | "dependencies": { 7 | "mongodb": "0.9.6-23", 8 | "sqlite": "1.0.4", 9 | "funk": "1.0.1" 10 | }, 11 | "devDependencies": { 12 | "testosterone": "1.2.0", 13 | "gently": "0.9.1" 14 | }, 15 | "repository" : {"type": "git" , "url": "http://github.com/masylum/dialect.git" }, 16 | "main": "./", 17 | "engines": { "node": ">= 0.4.0" } 18 | } 19 | -------------------------------------------------------------------------------- /test/dialect.js: -------------------------------------------------------------------------------- 1 | var testosterone = require('testosterone')({title: 'Dialect core', sync: true}), 2 | assert = testosterone.assert, 3 | gently = global.GENTLY = new (require('gently')), 4 | dialect = require('./..').dialect, 5 | 6 | _stubIO = function () { 7 | var io = {}; 8 | gently.expect(gently.hijacked['./helpers/io'], 'IO', function () { 9 | return io; 10 | }); 11 | return io; 12 | }; 13 | 14 | testosterone 15 | 16 | //////////////////////////////////////////// 17 | // General 18 | //////////////////////////////////////////// 19 | 20 | .add('GIVEN a dialect object being created \n' + 21 | ' WHEN no store is given \n' + 22 | ' THEN it should throw an error', function () { 23 | assert.throws(function () { 24 | dialect(); 25 | }); 26 | }) 27 | 28 | .add(' WHEN no `base_locale` or `current_locale` given \n' + 29 | ' THEN it should use `en` as default', function () { 30 | var options = {store: {mongodb: {}}}, 31 | d = dialect(options); 32 | 33 | assert.deepEqual(d.config('base_locale'), 'en'); 34 | assert.deepEqual(d.config('current_locale'), 'en'); 35 | }) 36 | 37 | .add(' WHEN no `locales` given \n' + 38 | ' THEN it should use `[en]` as default', function () { 39 | var options = {store: {mongodb: {}}}, 40 | d = dialect(options); 41 | 42 | assert.deepEqual(d.config('locales'), ['en']); 43 | }) 44 | 45 | //////////////////////////////////////////// 46 | // config 47 | //////////////////////////////////////////// 48 | 49 | .add('GIVEN a call to config \n' + 50 | ' WHEN just param `key` is given \n' + 51 | ' THEN it should return `_option[key]`', function () { 52 | var options = {store: {mongodb: {}}, base_locale: 'en'}, 53 | d = dialect(options); 54 | 55 | assert.equal(d.config('base_locale'), 'en'); 56 | }) 57 | 58 | .add(' WHEN no params given \n' + 59 | ' THEN it should return the whole `_option`', function () { 60 | var options = {store: {mongodb: {}}, base_locale: 'en'}, 61 | d = dialect(options); 62 | 63 | assert.deepEqual(d.config(), {store: {mongodb: {}}, base_locale: 'en', current_locale: 'en', locales: ['en']}); 64 | }) 65 | 66 | .add(' WHEN `key` and `value` params given \n' + 67 | ' THEN it should set the `_option[key]` to `value`', function () { 68 | var options = {store: {mongodb: {}}, base_locale: 'en'}, 69 | d = dialect(options); 70 | 71 | d.config('base_locale', 'es'); 72 | 73 | assert.equal(d.config('base_locale'), 'es'); 74 | }) 75 | 76 | //////////////////////////////////////////// 77 | // sync 78 | //////////////////////////////////////////// 79 | 80 | .add('GIVEN a call to `sync` \n' + 81 | ' WHEN asking for a `locale` \n' + 82 | ' THEN should call `cacheDicionary` of `IO`', function () { 83 | 84 | var store = {mongodb: {}}, 85 | options = {locales: ['en', 'es'], store: store}, 86 | io = _stubIO(), 87 | d = dialect(options), 88 | cb = function () { }; 89 | 90 | gently.expect(io, 'cacheDictionary', function (locale, cb) { 91 | assert.equal(locale, 'en'); 92 | assert.ok(cb); 93 | }); 94 | 95 | d.sync({locale: 'en'}, cb); 96 | }) 97 | 98 | .add(' WHEN not asking for a `locale` \n' + 99 | ' THEN should call `cacheDicionary` for each locale', function () { 100 | 101 | var store = {mongodb: {}}, 102 | options = {locales: ['en', 'es'], store: store}, 103 | io = _stubIO(), 104 | d = dialect(options), 105 | cb = function () { }; 106 | 107 | options.locales.forEach(function (l) { 108 | gently.expect(io, 'cacheDictionary', function (locale, cb) { 109 | assert.equal(locale, l); 110 | assert.ok(cb); 111 | }); 112 | }); 113 | d.sync({}, cb); 114 | 115 | }) 116 | 117 | .add(' WHEN passing a `interval` \n' + 118 | ' THEN should call `cacheDicionary` every `interval` seconds', function () { 119 | 120 | var store = {mongodb: {}}, 121 | options = {locales: ['en', 'es'], store: store}, 122 | io = _stubIO(), 123 | d = dialect(options), 124 | cb = function () { }; 125 | 126 | gently.expect(io, 'cacheDictionary', function (locale, cb) { 127 | assert.equal(locale, 'es'); 128 | assert.ok(cb); 129 | cb(); 130 | }); 131 | 132 | gently.expect(require('timers'), 'setInterval', function (fn, delay) { 133 | assert.ok(fn); 134 | assert.equal(delay, 3000); 135 | }); 136 | 137 | d.sync({locale: 'es', interval: 3000}, cb); 138 | }) 139 | 140 | //////////////////////////////////////////// 141 | // set 142 | //////////////////////////////////////////// 143 | 144 | .add('GIVEN a call to `set` \n' + 145 | ' WHEN `query.original` or `query.locale` or `translation` is missing \n' + 146 | ' THEN an error should be thrown', function () { 147 | 148 | var options = {locales: ['en', 'es'], store: {mongodb: {}}}, 149 | d = dialect(options); 150 | 151 | assert.throws(function () { 152 | d.set(null, null); 153 | }); 154 | 155 | assert.throws(function () { 156 | d.set({original: 'foo'}, null); 157 | }); 158 | 159 | assert.throws(function () { 160 | d.set({locale: 'foo'}, null); 161 | }); 162 | 163 | assert.throws(function () { 164 | d.set({original: 'foo', locale: 'foo'}, null); 165 | }); 166 | }) 167 | 168 | .add(' WHEN `query.original` and `query.locale` and `translation` are valid \n' + 169 | ' THEN should set the translation to the `store`', function () { 170 | 171 | var store = {mongodb: {}}, 172 | query = {original: 'hello', locale: 'foo'}, 173 | translation = 'foola', 174 | options = {locales: ['en', 'es'], store: store}, 175 | callback = function (err, data) { 176 | assert.equal(err, 'foo'); 177 | assert.equal(data, 'bar'); 178 | }, 179 | d = dialect(options); 180 | 181 | gently.expect(d.store, 'set', function (q, u, cb) { 182 | assert.deepEqual(q, query); 183 | assert.deepEqual(u, {translation: translation}); 184 | assert.deepEqual(cb, callback); 185 | cb('foo', 'bar'); 186 | }); 187 | 188 | d.set({original: 'hello', locale: 'foo'}, translation, callback); 189 | }) 190 | 191 | //////////////////////////////////////////// 192 | // approve 193 | //////////////////////////////////////////// 194 | 195 | .add('GIVEN a call to `approve` \n' + 196 | ' WHEN `query.original` or `query.locale` are missing \n' + 197 | ' THEN an error should be thrown', function () { 198 | 199 | var options = {locales: ['en', 'es'], store: {mongodb: {}}}, 200 | d = dialect(options); 201 | 202 | assert.throws(function () { 203 | d.approve(null, true); 204 | }); 205 | 206 | assert.throws(function () { 207 | d.approve({original: 'foo'}, true); 208 | }); 209 | 210 | assert.throws(function () { 211 | d.approve({locale: 'foo'}, true); 212 | }); 213 | 214 | assert.throws(function () { 215 | d.approve({original: 'foo', locale: 'foo'}, true); 216 | }); 217 | }) 218 | 219 | .add(' WHEN `query.original` and `query.locale` are valid \n' + 220 | ' THEN should approve the translation to the `store`', function () { 221 | 222 | var store = {mongodb: {}}, 223 | query = {original: 'hello', locale: 'foo'}, 224 | translation = 'foola', 225 | options = {locales: ['en', 'es'], store: store}, 226 | callback = function (err, data) { 227 | assert.equal(err, 'foo'); 228 | assert.equal(data, 'bar'); 229 | }, 230 | d = dialect(options); 231 | 232 | gently.expect(d.store, 'approve', 2, function (q, u, cb) { 233 | assert.deepEqual(q, query); 234 | assert.deepEqual(u, true); 235 | assert.deepEqual(cb, callback); 236 | cb('foo', 'bar'); 237 | }); 238 | 239 | d.approve({original: 'hello', locale: 'foo'}, true, callback); 240 | d.approve({original: 'hello', locale: 'foo'}, 'fleiba', callback); 241 | 242 | gently.expect(d.store, 'approve', 2, function (q, u, cb) { 243 | assert.deepEqual(q, query); 244 | assert.deepEqual(u, false); 245 | assert.deepEqual(cb, callback); 246 | cb('foo', 'bar'); 247 | }); 248 | 249 | d.approve({original: 'hello', locale: 'foo'}, '', callback); 250 | d.approve({original: 'hello', locale: 'foo'}, false, callback); 251 | }) 252 | 253 | //////////////////////////////////////////// 254 | // get 255 | //////////////////////////////////////////// 256 | 257 | .add('GIVEN a call to `get` \n' + 258 | ' WHEN `original` is not provided or invalid (String|Array) \n' + 259 | ' THEN an error should be thrown', function () { 260 | 261 | var options = {locales: ['en', 'es'], store: {mongodb: {}}}, 262 | d = dialect(options); 263 | 264 | assert.throws(function () { 265 | d.get(); 266 | }); 267 | 268 | [null, {foo: 'bar'}, true, '', [], /foo/].forEach(function (val) { 269 | assert.throws(function () { 270 | d.get(val); 271 | }); 272 | }); 273 | }) 274 | 275 | .add(' WHEN `original` is valid and cached on memory \n' + 276 | ' THEN should return the parsed translation from memory', function () { 277 | 278 | var options = {locales: ['en', 'es', 'sl'], store: {mongodb: {}}, current_locale: 'es'}, 279 | d = dialect(options); 280 | 281 | d.dicionaries = {}; 282 | 283 | // no params 284 | d.dictionaries.es = {'One cat': 'Un gato'}; 285 | assert.equal(d.get('One cat'), 'Un gato'); 286 | 287 | // params 288 | d.dictionaries.es = {'One {animal}': 'Un {animal}'}; 289 | assert.equal(d.get(['One {animal}', {animal: 'gato'}]), 'Un gato'); 290 | 291 | // plural 292 | d.dictionaries.es = { 293 | 'My {animal}|p:1': 'Mi {animal}', 294 | 'My {animal}|p:2': 'Mis {animal}' 295 | }; 296 | assert.equal(d.get(['My {animal}', {animal: 'gatos', count: 2}]), 'Mis gatos'); 297 | 298 | // more than 2 plurals 299 | d.config('current_locale', 'sl'); 300 | d.dictionaries.sl = { 301 | '{count} beer|p:1': '{count} pivo', 302 | '{count} beer|p:2': '{count} pivi', 303 | '{count} beer|p:3': '{count} piva' 304 | }; 305 | assert.equal(d.get(['{count} beer', '{count} beers', {count: 1}]), '1 pivo'); 306 | assert.equal(d.get(['{count} beer', '{count} beers', {count: 2}]), '2 pivi'); 307 | assert.equal(d.get(['{count} beer', '{count} beers', {count: 3}]), '3 piva'); 308 | 309 | // context 310 | d.config('current_locale', 'es'); 311 | d.dictionaries.es = { 312 | 'Smart {animal}|c:clever': '{animal} espavilado', 313 | 'Smart {animal}|c:elegant': '{animal} elegante' 314 | }; 315 | assert.equal(d.get(['Smart {animal}', {animal: 'gato', context: 'elegant'}]), 'gato elegante'); 316 | 317 | // all together 318 | d.dictionaries.es = { 319 | 'My Smart {animal}|p:1|c:clever': 'Mi {animal} espavilado', 320 | 'My Smart {animal}|p:2|c:clever': 'Mis {animal} elegantes', 321 | 'My Smart {animal}|p:1|c:elegant': 'Mi {animal} elegante', 322 | 'My Smart {animal}|p:2|c:elegant': 'Mis {animal} elegantes' 323 | }; 324 | assert.equal(d.get(['My Smart {animal}', {animal: 'gatos', count: 2, context: 'elegant'}]), 'Mis gatos elegantes'); 325 | }) 326 | 327 | .add(' WHEN `original` is valid but not cached on memory \n' + 328 | ' THEN should try to store the new word on the `store` \n' + 329 | ' AND should return original in singular or plural form', function () { 330 | 331 | var store = {mongodb: {}}, 332 | options = {locales: ['en', 'es'], current_locale: 'es', store: store}, 333 | d = dialect(options), 334 | stub_add = function (original) { 335 | gently.expect(d.store, 'add', function (q, u, cb) { 336 | assert.deepEqual(q, {original: original, locale: 'es'}); 337 | assert.deepEqual(u, undefined); 338 | }); 339 | }; 340 | 341 | d.dictionaries = {es: {foo: 'bar'}}; 342 | 343 | // no params 344 | stub_add('One cat'); 345 | assert.equal(d.get('One cat'), 'One cat'); 346 | 347 | // singular 348 | stub_add('cat'); 349 | assert.equal(d.get(['cat', 'cats', {count: 1}]), 'cat'); 350 | 351 | // plural 352 | stub_add('cat'); 353 | assert.equal(d.get(['cat', 'cats', {count: 2}]), 'cats'); 354 | 355 | stub_add('cat'); 356 | assert.equal(d.get(['cat', 'cats', {count: 3}]), 'cats'); 357 | }) 358 | 359 | .serial(function () { }); 360 | -------------------------------------------------------------------------------- /test/io.js: -------------------------------------------------------------------------------- 1 | var testosterone = require('testosterone')({title: 'IO helper lib', sync: true}), 2 | assert = testosterone.assert, 3 | gently = global.GENTLY = new (require('gently')), 4 | dialect = require('./..').dialect({locales: ['en', 'es'], current_locale: 'es', store: {mongodb: {}}}), 5 | io = require('./../lib/helpers/io').IO(dialect); 6 | 7 | dialect.store = {}; 8 | 9 | testosterone 10 | 11 | //////////////////////////////////////////// 12 | // getKeyFromQuery 13 | //////////////////////////////////////////// 14 | 15 | .add(' GIVEN a call to `getKeyFromQuery` \n' + 16 | ' WHEN no `query.original` is provided \n' + 17 | ' THEN it should throw an error', function () { 18 | assert.throws(function () { 19 | io.getKeyFromQuery(); 20 | }); 21 | assert.throws(function () { 22 | io.getKeyFromQuery({foo: 'bar'}); 23 | }); 24 | }) 25 | 26 | .add(' WHEN a query containing `original` and optional `count` and `context` \n' + 27 | ' THEN it should return a valid key', function () { 28 | assert.equal(io.getKeyFromQuery({original: 'foo'}), 'foo'); 29 | assert.equal(io.getKeyFromQuery({original: 'foo', count: 0}), 'foo|p:2'); 30 | assert.equal(io.getKeyFromQuery({original: 'foo', count: 1}), 'foo|p:1'); 31 | assert.equal(io.getKeyFromQuery({original: 'foo', context: 'bar'}), 'foo|c:bar'); 32 | assert.equal(io.getKeyFromQuery({original: 'foo', count: 0, context: 'bar'}), 'foo|p:2|c:bar'); 33 | 34 | dialect.config('current_locale', 'fr'); 35 | assert.equal(io.getKeyFromQuery({original: 'foo', count: 0}), 'foo|p:1'); 36 | }) 37 | 38 | //////////////////////////////////////////// 39 | // cacheDictionary 40 | //////////////////////////////////////////// 41 | 42 | .add(' GIVEN a call to `cacheDictionary` \n' + 43 | ' WHEN no `locale` is provided \n' + 44 | ' THEN it should throw an error', function () { 45 | assert.throws(function () { 46 | io.cacheDictionary(); 47 | }); 48 | }) 49 | 50 | .add(' WHEN `locale` is provided \n' + 51 | ' THEN it should get the dictionary from the store \n' + 52 | ' AND cache it on memory', function () { 53 | gently.expect(dialect.store, 'get', function (query, cb) { 54 | assert.deepEqual(query, {locale: 'es', approved: true}); 55 | assert.ok(query, cb); 56 | cb(null, [{original: 'hello', translation: 'hola'}]); 57 | }); 58 | 59 | io.cacheDictionary('es', function (err, data) { 60 | assert.equal(err, null); 61 | assert.deepEqual(data, {hello: 'hola'}); 62 | }); 63 | 64 | assert.deepEqual(dialect.dictionaries.es, {hello: 'hola'}); 65 | 66 | // with plural and contexts 67 | gently.expect(dialect.store, 'get', function (query, cb) { 68 | assert.deepEqual(query, {locale: 'es', approved: true}); 69 | assert.ok(query, cb); 70 | cb(null, [{original: 'hello', translation: 'hola', context: 'salute', plural: 1}]); 71 | }); 72 | 73 | io.cacheDictionary('es', function (err, data) { 74 | assert.equal(err, null); 75 | assert.deepEqual(data, {'hello|p:1|c:salute': 'hola'}); 76 | }); 77 | 78 | assert.deepEqual(dialect.dictionaries.es, {'hello|p:1|c:salute': 'hola'}); 79 | }) 80 | 81 | .serial(function () { }); 82 | -------------------------------------------------------------------------------- /test/sqlizer.js: -------------------------------------------------------------------------------- 1 | var testosterone = require('testosterone')({title: 'SQLizer helper lib', sync: true}), 2 | assert = testosterone.assert, 3 | gently = global.GENTLY = new (require('gently')), 4 | sqlizer = require('./../lib/helpers/sqlizer')({table: 'test'}); 5 | 6 | testosterone 7 | 8 | //////////////////////////////////////////// 9 | // parseDoc 10 | //////////////////////////////////////////// 11 | 12 | .add('parseDoc', function () { 13 | assert.equal(sqlizer.parseDoc({j: 4}), 'j = 4'); 14 | assert.equal(sqlizer.parseDoc({j: '4'}), "j = '4'"); 15 | assert.equal(sqlizer.parseDoc({j: true}), "j = 1"); 16 | assert.equal(sqlizer.parseDoc({j: false}), "j = 0"); 17 | assert.equal(sqlizer.parseDoc({j: {'$exists': true}, f: null}), 'j IS NOT null AND f IS null'); 18 | assert.equal(sqlizer.parseDoc({j: {'$exists': false}}), 'j IS null'); 19 | assert.equal(sqlizer.parseDoc({j: {'$ne': null}}), 'j IS NOT null'); 20 | assert.equal(sqlizer.parseDoc({j: {'$ne': 4, '$gt': 8}}), 'j <> 4 AND j > 8'); 21 | assert.equal(sqlizer.parseDoc({j: {'$lte': 4}, f: 4}), 'j <= 4 AND f = 4'); 22 | assert.equal(sqlizer.parseDoc({j: {'$lt': 4}, '$or': [{f: 4}, {g: {'$gte': 10}}]}), 'j < 4 AND (f = 4 OR g >= 10)'); 23 | assert.equal(sqlizer.parseDoc({j: {'$in': [2, 3, 4]}}), 'j IN (2,3,4)'); 24 | assert.equal(sqlizer.parseDoc({j: {'$nin': [2, 3, 4]}}), 'j NOT IN (2,3,4)'); 25 | }) 26 | 27 | //////////////////////////////////////////// 28 | // find 29 | //////////////////////////////////////////// 30 | 31 | .add('find', function () { 32 | assert.equal(sqlizer.find().sql, 'SELECT * FROM test'); 33 | assert.equal(sqlizer.find({}).sql, 'SELECT * FROM test'); 34 | assert.equal(sqlizer.find({j: 4}).sql, 'SELECT * FROM test WHERE j = 4'); 35 | assert.equal(sqlizer.find({j: 4, f: 5}, {a: 1}).sql, 'SELECT a FROM test WHERE j = 4 AND f = 5'); 36 | assert.equal(sqlizer.find({j: 4, f: 5}, {a: 1, b: 1}).sql, 'SELECT a, b FROM test WHERE j = 4 AND f = 5'); 37 | assert.equal(sqlizer.find({j: 4, f: 5}, {a: 2, b: 2}).sql, 'SELECT * FROM test WHERE j = 4 AND f = 5'); 38 | }) 39 | 40 | //////////////////////////////////////////// 41 | // sort 42 | //////////////////////////////////////////// 43 | 44 | .add('sort', function () { 45 | assert.equal(sqlizer.find({j: 4}).sort({b: 1}).sql, 'SELECT * FROM test WHERE j = 4 ORDER BY b ASC'); 46 | assert.equal(sqlizer.find({j: 4}).sort({b: -1}).sql, 'SELECT * FROM test WHERE j = 4 ORDER BY b DESC'); 47 | assert.equal(sqlizer.find({}).sort({a: -1, b: 1}).sql, 'SELECT * FROM test ORDER BY a DESC, b ASC'); 48 | }) 49 | 50 | //////////////////////////////////////////// 51 | // skip and limit 52 | //////////////////////////////////////////// 53 | 54 | .add('skip and limit', function () { 55 | assert.equal(sqlizer.find({j: 4}).skip(5).sql, 'SELECT * FROM test WHERE j = 4 LIMIT 0 OFFSET 5'); 56 | assert.equal(sqlizer.find({j: 4}).limit(5).sql, 'SELECT * FROM test WHERE j = 4 LIMIT 5 OFFSET 0'); 57 | assert.equal(sqlizer.find({j: 4}).skip(2).limit(5).sql, 'SELECT * FROM test WHERE j = 4 LIMIT 5 OFFSET 2'); 58 | assert.equal(sqlizer.find({j: 4}).limit(5).skip(2).sql, 'SELECT * FROM test WHERE j = 4 LIMIT 5 OFFSET 2'); 59 | }) 60 | 61 | //////////////////////////////////////////// 62 | // count 63 | //////////////////////////////////////////// 64 | 65 | .add('count', function () { 66 | assert.equal(sqlizer.find({j: 4}).count().sql, 'SELECT COUNT(*) FROM test WHERE j = 4'); 67 | }) 68 | 69 | //////////////////////////////////////////// 70 | // insert 71 | //////////////////////////////////////////// 72 | 73 | .add('insert', function () { 74 | assert.equal(sqlizer.insert({j: 4, f: 3}).sql, 'INSERT INTO test (j, f) VALUES (4, 3)'); 75 | assert.equal(sqlizer.insert({j: 'foo', f: 3}).sql, "INSERT INTO test (j, f) VALUES ('foo', 3)"); 76 | assert.equal(sqlizer.insert([{j: 4, f: 3}, {d: 2}]).sql, 'INSERT INTO test (j, f) VALUES (4, 3); INSERT INTO test (d) VALUES (2)'); 77 | }) 78 | 79 | //////////////////////////////////////////// 80 | // update 81 | //////////////////////////////////////////// 82 | 83 | .add('update', function () { 84 | assert.equal(sqlizer.update({}, {'$set': {j: 4}}).sql, 'UPDATE test SET j = 4'); 85 | assert.equal(sqlizer.update({}, {'$set': {j: 4, i: false}}).sql, 'UPDATE test SET j = 4, i = 0'); 86 | assert.equal(sqlizer.update({}, {'$inc': {j: 2}}).sql, 'UPDATE test SET j = j + 2'); 87 | assert.equal(sqlizer.update({d: 'foo'}, {'$set': {j: 'bar'}}).sql, "UPDATE test SET j = 'bar' WHERE d = 'foo'"); 88 | assert.equal(sqlizer.update({d: 4, f: 3}, {'$inc': {j: 2}}).sql, 'UPDATE test SET j = j + 2 WHERE d = 4 AND f = 3'); 89 | }) 90 | 91 | //////////////////////////////////////////// 92 | // remove 93 | //////////////////////////////////////////// 94 | 95 | .add('remove', function () { 96 | assert.equal(sqlizer.remove({}).sql, 'DELETE FROM test'); 97 | assert.equal(sqlizer.remove({j: 2}).sql, 'DELETE FROM test WHERE j = 2'); 98 | }) 99 | 100 | .serial(function () { }); 101 | -------------------------------------------------------------------------------- /test/stores/mongodb.js: -------------------------------------------------------------------------------- 1 | var testosterone = require('testosterone')({title: 'Mongodb store', sync: true}), 2 | assert = testosterone.assert, 3 | STORE = require('../../lib/stores/mongodb'), 4 | gently = global.GENTLY = new (require('gently')); 5 | 6 | testosterone 7 | 8 | //////////////////////////////////////////// 9 | // connect 10 | //////////////////////////////////////////// 11 | 12 | .add('GIVEN a call to `connect` \n' + 13 | ' WHEN no `callback` is given \n' + 14 | ' THEN it should provide with a default one \n' + 15 | ' AND return the `collection` or `error`', function () { 16 | 17 | var db = {}, 18 | store = STORE(); 19 | 20 | // error 21 | gently.expect(store.db, 'open', function (cb) { 22 | cb('foo', null); 23 | }); 24 | 25 | store.connect(function (err, coll) { 26 | assert.equal(err, 'foo'); 27 | assert.equal(coll, null); 28 | assert.equal(store.collection, null); 29 | assert.equal(store.is_connected(), false); 30 | }); 31 | 32 | // data 33 | store.collection = null; 34 | gently.expect(store.db, 'open', function (cb) { 35 | cb(null, db); 36 | }); 37 | 38 | // no collection yet 39 | gently.expect(store.db, 'collection', function (coll, cb) { 40 | cb('blah', null); 41 | }); 42 | 43 | gently.expect(store.db, 'createCollection', function (coll, cb) { 44 | cb('blah', 'bar'); 45 | }); 46 | 47 | store.connect(function (err, coll) { 48 | assert.equal(err, 'blah'); 49 | assert.equal(coll, 'bar'); 50 | assert.equal(store.collection, 'bar'); 51 | assert.equal(store.is_connected(), true); 52 | }); 53 | }) 54 | 55 | //////////////////////////////////////////// 56 | // get 57 | //////////////////////////////////////////// 58 | 59 | .add('GIVEN a call to `get` \n' + 60 | ' WHEN no `callback` is given \n' + 61 | ' THEN it should provide with a default one \n' + 62 | ' AND return the translation according to `query`', function () { 63 | 64 | var store = STORE(); 65 | 66 | store.collection = {}; 67 | 68 | // error 69 | gently.expect(store.collection, 'find', function (query, cb) { 70 | assert.deepEqual(query, {}); 71 | assert.ok(cb); 72 | cb('foo', null); 73 | }); 74 | 75 | store.get(null, function (err, doc) { 76 | assert.equal(err, 'foo'); 77 | assert.equal(doc, null); 78 | }); 79 | 80 | // data 81 | gently.expect(store.collection, 'find', function (query, cb) { 82 | var cursor = {}; 83 | assert.deepEqual(query, {foo: 'bar'}); 84 | assert.ok(cb); 85 | 86 | gently.expect(cursor, 'toArray', function (cb) { 87 | cb('foo', [{hey: 'ya'}]); 88 | }); 89 | cb(null, cursor); 90 | }); 91 | 92 | store.get({foo: 'bar'}, function (err, doc) { 93 | assert.equal(err, 'foo'); 94 | assert.deepEqual(doc, [{hey: 'ya'}]); 95 | }); 96 | }) 97 | 98 | //////////////////////////////////////////// 99 | // add 100 | //////////////////////////////////////////// 101 | 102 | .add('GIVEN a call to `add` \n' + 103 | ' WHEN no `callback` is given \n' + 104 | ' THEN it should provide with a default one \n' + 105 | ' AND add the unapproved `translation` if is not on the store', function () { 106 | 107 | var store = STORE(), 108 | original = {original: 'hello'}, 109 | new_doc = {original: 'hello', translation: 'hola', approved: false}; 110 | 111 | store.collection = {}; 112 | 113 | // error 114 | gently.expect(store.collection, 'findOne', function (query, cb) { 115 | assert.deepEqual(query, original); 116 | assert.ok(cb); 117 | cb('foo', null); 118 | }); 119 | 120 | store.add(original, 'hola', function (err, doc) { 121 | assert.equal(err, 'foo'); 122 | assert.equal(doc, null); 123 | }); 124 | 125 | // no error, no data 126 | gently.expect(store.collection, 'findOne', function (query, cb) { 127 | assert.deepEqual(query, original); 128 | assert.ok(cb); 129 | cb(null, {original: 'hello', translation: 'hola'}); 130 | }); 131 | 132 | store.add(original, 'hola', function (err, doc) { 133 | assert.ok(err); 134 | assert.equal(doc, null); 135 | }); 136 | 137 | // data 138 | gently.expect(store.collection, 'findOne', function (query, cb) { 139 | assert.deepEqual(query, original); 140 | assert.ok(cb); 141 | gently.expect(store.collection, 'insert', function (doc, cb) { 142 | assert.deepEqual(doc, new_doc); 143 | assert.ok(cb); 144 | cb(null, new_doc); 145 | }); 146 | cb(null, null); 147 | }); 148 | 149 | store.add(original, 'hola', function (err, doc) { 150 | assert.equal(err, null); 151 | assert.equal(doc, new_doc); 152 | }); 153 | }) 154 | 155 | //////////////////////////////////////////// 156 | // set 157 | //////////////////////////////////////////// 158 | 159 | .add('GIVEN a call to `set` \n' + 160 | ' WHEN no `callback` is given \n' + 161 | ' THEN it should provide with a default one \n' + 162 | ' AND update the translations according to `query` and `translation`', function () { 163 | 164 | var store = STORE(), 165 | original = {original: 'hello'}, 166 | translation = {translation: 'hola'}, 167 | new_doc = {original: 'hello', translation: 'hola', approved: false}; 168 | 169 | store.collection = {}; 170 | 171 | gently.expect(store.collection, 'update', function (query, update, options, cb) { 172 | assert.deepEqual(query, original); 173 | assert.deepEqual(update, {'$set': {translation: 'hola', approved: false}}); 174 | assert.deepEqual(options, {upsert: true}); 175 | assert.ok(cb); 176 | cb('foo', new_doc); 177 | }); 178 | 179 | store.set(original, translation, function (err, doc) { 180 | assert.equal(err, 'foo'); 181 | assert.equal(doc, new_doc); 182 | }); 183 | }) 184 | 185 | //////////////////////////////////////////// 186 | // approve 187 | //////////////////////////////////////////// 188 | 189 | .add('GIVEN a call to `approve` \n' + 190 | ' WHEN no `callback` is given \n' + 191 | ' THEN it should provide with a default one \n' + 192 | ' AND update the translations according to `query` and `approved`', function () { 193 | 194 | var store = STORE(), 195 | original = {original: 'hello', translation: 'hola'}, 196 | new_doc = {original: 'hello', translation: 'hola', approved: true}; 197 | 198 | store.collection = {}; 199 | 200 | gently.expect(store.collection, 'update', function (query, update, options, cb) { 201 | assert.deepEqual(query, original); 202 | assert.deepEqual(update, {'$set': {approved: true}}); 203 | assert.deepEqual(options, {}); 204 | assert.ok(cb); 205 | cb('foo', new_doc); 206 | }); 207 | 208 | store.approve(original, true, function (err, doc) { 209 | assert.equal(err, 'foo'); 210 | assert.equal(doc, new_doc); 211 | }); 212 | }) 213 | 214 | //////////////////////////////////////////// 215 | // destroy 216 | //////////////////////////////////////////// 217 | 218 | .add('GIVEN a call to `destroy` \n' + 219 | ' WHEN no `callback` is given \n' + 220 | ' THEN it should provide with a default one \n' + 221 | ' AND remove the translation according to `query`', function () { 222 | 223 | var store = STORE(), 224 | original = {original: 'hello'}; 225 | 226 | store.collection = {}; 227 | 228 | gently.expect(store.collection, 'remove', function (query, cb) { 229 | assert.deepEqual(query, original); 230 | assert.ok(cb); 231 | cb('foo', original); 232 | }); 233 | 234 | store.destroy(original, function (err, doc) { 235 | assert.equal(err, 'foo'); 236 | assert.equal(doc, original); 237 | }); 238 | }) 239 | 240 | //////////////////////////////////////////// 241 | // count 242 | //////////////////////////////////////////// 243 | 244 | .add('GIVEN a call to `count` \n' + 245 | ' WHEN no `callback` is given \n' + 246 | ' THEN it should provide with a default one \n' + 247 | ' AND count the translations according to `query`', function () { 248 | 249 | var store = STORE(), 250 | original = {original: 'hello'}; 251 | 252 | store.collection = {}; 253 | 254 | gently.expect(store.collection, 'count', function (query, cb) { 255 | assert.deepEqual(query, original); 256 | assert.ok(cb); 257 | cb('foo', 3); 258 | }); 259 | 260 | store.count(original, function (err, doc) { 261 | assert.equal(err, 'foo'); 262 | assert.equal(doc, 3); 263 | }); 264 | }) 265 | 266 | .serial(function () { }); 267 | -------------------------------------------------------------------------------- /test/stores/sqlite.js: -------------------------------------------------------------------------------- 1 | var testosterone = require('testosterone')({title: 'SQLite store', sync: true}), 2 | assert = testosterone.assert, 3 | SQLITE = require('../../lib/stores/sqlite'), 4 | gently = global.GENTLY = new (require('gently')); 5 | 6 | testosterone 7 | 8 | //////////////////////////////////////////// 9 | // connect 10 | //////////////////////////////////////////// 11 | 12 | .add('GIVEN a call to `connect` \n' + 13 | ' WHEN no `callback` is given \n' + 14 | ' THEN it should provide with a default one \n' + 15 | ' AND return the `collection` or `error`', function () { 16 | 17 | var db = {}, 18 | store = SQLITE({table: 'test'}); 19 | 20 | // error 21 | gently.expect(store.db, 'open', function (database, cb) { 22 | assert.equal(database, 'dialect.db'); 23 | cb('foo', null); 24 | }); 25 | 26 | store.connect(function (err, coll) { 27 | assert.equal(err, 'foo'); 28 | assert.equal(coll, null); 29 | assert.equal(store.is_connected(), false); 30 | }); 31 | 32 | // data 33 | gently.expect(store.db, 'open', function (database, cb) { 34 | assert.equal(database, 'dialect.db'); 35 | cb(null, db); 36 | }); 37 | 38 | gently.expect(store.db, 'execute', function (sql, cb) { 39 | assert.equal(sql, 40 | 'CREATE TABLE IF NOT EXISTS test' + 41 | ' (original TEXT, locale TEXT, translation TEXT,' + 42 | ' plural NUMBER, context TEXT, PRIMARY KEY(original, locale, plural, context))' 43 | ); 44 | 45 | cb('foo', 'bar'); 46 | }); 47 | 48 | store.connect(function (err, data) { 49 | assert.equal(err, 'foo'); 50 | assert.equal(data, 'bar'); 51 | assert.equal(store.is_connected(), true); 52 | }); 53 | }) 54 | 55 | //////////////////////////////////////////// 56 | // get 57 | //////////////////////////////////////////// 58 | 59 | .add('GIVEN a call to `get` \n' + 60 | ' WHEN no `callback` is given \n' + 61 | ' THEN it should provide with a default one \n' + 62 | ' AND return the translation according to `query`', function () { 63 | 64 | var store = SQLITE(); 65 | 66 | store.collection = {}; 67 | 68 | // error 69 | gently.expect(store.db, 'execute', function (sql, cb) { 70 | assert.equal(sql, "SELECT * FROM dialect WHERE original = 'foo'"); 71 | assert.ok(cb); 72 | cb('foo', null); 73 | }); 74 | 75 | store.get({original: 'foo'}, function (err, doc) { 76 | assert.equal(err, 'foo'); 77 | assert.deepEqual(doc, []); 78 | }); 79 | 80 | // data 81 | gently.expect(store.db, 'execute', function (sql, cb) { 82 | assert.equal(sql, "SELECT * FROM dialect WHERE original = 'foo'"); 83 | assert.ok(cb); 84 | cb(null, 'foo'); 85 | }); 86 | 87 | store.get({original: 'foo'}, function (err, doc) { 88 | assert.equal(err, null); 89 | assert.equal(doc, 'foo'); 90 | }); 91 | }) 92 | 93 | //////////////////////////////////////////// 94 | // add 95 | //////////////////////////////////////////// 96 | 97 | .add('GIVEN a call to `add` \n' + 98 | ' WHEN no `callback` is given \n' + 99 | ' THEN it should provide with a default one \n' + 100 | ' AND add the `translation` if is not on the store', function () { 101 | 102 | var store = SQLITE(), 103 | original = {original: 'hello'}, 104 | new_doc = {original: 'hello', translation: 'hola'}; 105 | 106 | store.collection = {}; 107 | 108 | // error on get 109 | gently.expect(store, 'get', function (doc, cb) { 110 | assert.deepEqual(doc, original); 111 | assert.ok(cb); 112 | cb('foo', []); 113 | }); 114 | 115 | store.add(original, 'hola', function (err, doc) { 116 | assert.equal(err, 'foo'); 117 | assert.equal(doc, null); 118 | }); 119 | 120 | // already exists 121 | gently.expect(store, 'get', function (doc, cb) { 122 | assert.deepEqual(doc, original); 123 | assert.ok(cb); 124 | cb(null, [new_doc]); 125 | }); 126 | 127 | store.add(original, 'hola', function (err, doc) { 128 | assert.ok(err); 129 | }); 130 | 131 | // success 132 | gently.expect(store, 'get', function (doc, cb) { 133 | assert.deepEqual(doc, original); 134 | assert.ok(cb); 135 | cb(null, []); 136 | }); 137 | 138 | gently.expect(store.db, 'execute', function (sql, cb) { 139 | assert.equal(sql, "INSERT INTO dialect (original, translation, approved) VALUES ('hello', 'hola', 0)"); 140 | assert.ok(cb); 141 | cb(null, []); 142 | }); 143 | 144 | store.add(original, 'hola', function (err, doc) { 145 | assert.equal(err, null); 146 | assert.deepEqual(doc, []); 147 | }); 148 | }) 149 | 150 | //////////////////////////////////////////// 151 | // set 152 | //////////////////////////////////////////// 153 | 154 | .add('GIVEN a call to `set` \n' + 155 | ' WHEN no `callback` is given \n' + 156 | ' THEN it should provide with a default one \n' + 157 | ' AND update the translations according to `query` and `translation`', function () { 158 | 159 | var store = SQLITE(); 160 | 161 | store.collection = {}; 162 | 163 | gently.expect(store.db, 'execute', function (sql, cb) { 164 | assert.equal(sql, "UPDATE dialect SET translation = 'bar', approved = 0 WHERE original = 'foo'"); 165 | assert.ok(cb); 166 | cb('foo', 'bar'); 167 | }); 168 | 169 | store.set({original: 'foo'}, 'bar', function (err, doc) { 170 | assert.equal(err, 'foo'); 171 | assert.deepEqual(doc, 'bar'); 172 | }); 173 | }) 174 | 175 | //////////////////////////////////////////// 176 | // approve 177 | //////////////////////////////////////////// 178 | 179 | .add('GIVEN a call to `approve` \n' + 180 | ' WHEN no `callback` is given \n' + 181 | ' THEN it should provide with a default one \n' + 182 | ' AND update the translations according to `query` and `approved`', function () { 183 | 184 | var store = SQLITE(); 185 | 186 | store.collection = {}; 187 | 188 | gently.expect(store.db, 'execute', function (sql, cb) { 189 | assert.equal(sql, "UPDATE dialect SET approved = 1 WHERE original = 'foo'"); 190 | assert.ok(cb); 191 | cb('foo', 'bar'); 192 | }); 193 | 194 | store.approve({original: 'foo'}, true, function (err, doc) { 195 | assert.equal(err, 'foo'); 196 | assert.deepEqual(doc, 'bar'); 197 | }); 198 | }) 199 | 200 | //////////////////////////////////////////// 201 | // destroy 202 | //////////////////////////////////////////// 203 | 204 | .add('GIVEN a call to `destroy` \n' + 205 | ' WHEN no `callback` is given \n' + 206 | ' THEN it should provide with a default one \n' + 207 | ' AND remove the translation according to `query`', function () { 208 | 209 | var store = SQLITE(), 210 | original = {original: 'hello'}; 211 | 212 | store.collection = {}; 213 | 214 | gently.expect(store.db, 'execute', function (sql, cb) { 215 | assert.equal(sql, "DELETE FROM dialect WHERE original = 'foo'"); 216 | assert.ok(cb); 217 | cb('foo', 'bar'); 218 | }); 219 | 220 | store.destroy({original: 'foo'}, function (err, doc) { 221 | assert.equal(err, 'foo'); 222 | assert.deepEqual(doc, 'bar'); 223 | }); 224 | }) 225 | 226 | //////////////////////////////////////////// 227 | // count 228 | //////////////////////////////////////////// 229 | 230 | .add('GIVEN a call to `count` \n' + 231 | ' WHEN no `callback` is given \n' + 232 | ' THEN it should provide with a default one \n' + 233 | ' AND count the translations according to `query`', function () { 234 | 235 | var store = SQLITE(), 236 | original = {original: 'hello'}; 237 | 238 | store.collection = {}; 239 | 240 | // error 241 | gently.expect(store.db, 'execute', function (sql, cb) { 242 | assert.equal(sql, "SELECT COUNT(*) FROM dialect WHERE original = 'foo'"); 243 | assert.ok(cb); 244 | cb('foo', null); 245 | }); 246 | 247 | store.count({original: 'foo'}, function (err, doc) { 248 | assert.equal(err, 'foo'); 249 | assert.deepEqual(doc, 0); 250 | }); 251 | 252 | // succes 253 | gently.expect(store.db, 'execute', function (sql, cb) { 254 | assert.equal(sql, "SELECT COUNT(*) FROM dialect WHERE original = 'foo'"); 255 | assert.ok(cb); 256 | cb(null, [{count: 3}]); 257 | }); 258 | 259 | store.count({original: 'foo'}, function (err, doc) { 260 | assert.equal(err, null); 261 | assert.deepEqual(doc, 3); 262 | }); 263 | }) 264 | 265 | .serial(function () { }); 266 | --------------------------------------------------------------------------------