diff --git a/src/meta.js b/src/meta.js index 4b1afff218..9c0dc8adad 100644 --- a/src/meta.js +++ b/src/meta.js @@ -1,584 +1,22 @@ "use strict"; -var fs = require('fs'), - path = require('path'), - async = require('async'), +var async = require('async'), winston = require('winston'), - nconf = require('nconf'), - _ = require('underscore'), - less = require('less'), - fork = require('child_process').fork, - rimraf = require('rimraf'), - mkdirp = require('mkdirp'), - - utils = require('./../public/src/utils'), - translator = require('./../public/src/translator'), - db = require('./database'), - plugins = require('./plugins'), user = require('./user'), - groups = require('./groups'), - emitter = require('./emitter'); + groups = require('./groups'); + (function (Meta) { Meta.restartRequired = false; - Meta.config = {}; - - Meta.configs = { - init: function (callback) { - delete Meta.config; - - Meta.configs.list(function (err, config) { - if(err) { - winston.error(err); - return callback(err); - } - - Meta.config = config; - callback(); - }); - }, - list: function (callback) { - db.getObject('config', function (err, config) { - if(err) { - return callback(err); - } - - config = config || {}; - config.status = 'ok'; - callback(err, config); - }); - }, - get: function (field, callback) { - db.getObjectField('config', field, callback); - }, - getFields: function (fields, callback) { - db.getObjectFields('config', fields, callback); - }, - set: function (field, value, callback) { - if(!field) { - return callback(new Error('invalid config field')); - } - - db.setObjectField('config', field, value, function(err, res) { - if (callback) { - if(!err && Meta.config) { - Meta.config[field] = value; - } - - callback(err, res); - } - }); - }, - setOnEmpty: function (field, value, callback) { - Meta.configs.get(field, function (err, curValue) { - if(err) { - return callback(err); - } - - if (!curValue) { - Meta.configs.set(field, value, callback); - } else { - callback(); - } - }); - }, - remove: function (field) { - db.deleteObjectField('config', field); - } - }; - - Meta.themes = { - get: function (callback) { - var themePath = nconf.get('themes_path'); - if (typeof themePath !== 'string') { - return callback(null, []); - } - fs.readdir(themePath, function (err, files) { - async.filter(files, function (file, next) { - fs.stat(path.join(themePath, file), function (err, fileStat) { - if (err) { - return next(false); - } - - next((fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-')); - }); - }, function (themes) { - async.map(themes, function (theme, next) { - var config = path.join(themePath, theme, 'theme.json'); - - if (fs.existsSync(config)) { - fs.readFile(config, function (err, file) { - if (err) { - return next(); - } else { - var configObj = JSON.parse(file.toString()); - next(err, configObj); - } - }); - } else { - next(); - } - }, function (err, themes) { - themes = themes.filter(function (theme) { - return (theme !== undefined); - }); - callback(null, themes); - }); - }); - }); - }, - set: function(data, callback) { - var themeData = { - 'theme:type': data.type, - 'theme:id': data.id, - 'theme:staticDir': '', - 'theme:templates': '', - 'theme:src': '' - }; - - switch(data.type) { - case 'local': - async.waterfall([ - function(next) { - fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function(err, config) { - if (!err) { - config = JSON.parse(config.toString()); - next(null, config); - } else { - next(err); - } - }); - }, - function(config, next) { - themeData['theme:staticDir'] = config.staticDir ? config.staticDir : ''; - themeData['theme:templates'] = config.templates ? config.templates : ''; - themeData['theme:src'] = ''; - - db.setObject('config', themeData, next); - } - ], callback); - - Meta.restartRequired = true; - break; - - case 'bootswatch': - Meta.configs.set('theme:src', data.src, callback); - break; - } - } - }; - - Meta.title = { - tests: { - isCategory: /^category\/\d+\/?/, - isTopic: /^topic\/\d+\/?/, - isUserPage: /^user\/[^\/]+(\/[\w]+)?/ - }, - build: function (urlFragment, language, callback) { - Meta.title.parseFragment(decodeURIComponent(urlFragment), language, function(err, title) { - if (err) { - title = Meta.config.browserTitle || 'NodeBB'; - } else { - title = (title ? title + ' | ' : '') + (Meta.config.browserTitle || 'NodeBB'); - } - - callback(null, title); - }); - }, - parseFragment: function (urlFragment, language, callback) { - var translated = ['', 'recent', 'unread', 'users', 'notifications']; - if (translated.indexOf(urlFragment) !== -1) { - if (!urlFragment.length) { - urlFragment = 'home'; - } - - translator.translate('[[pages:' + urlFragment + ']]', language, function(translated) { - callback(null, translated); - }); - } else if (this.tests.isCategory.test(urlFragment)) { - var cid = urlFragment.match(/category\/(\d+)/)[1]; - - require('./categories').getCategoryField(cid, 'name', function (err, name) { - callback(null, name); - }); - } else if (this.tests.isTopic.test(urlFragment)) { - var tid = urlFragment.match(/topic\/(\d+)/)[1]; - - require('./topics').getTopicField(tid, 'title', function (err, title) { - callback(null, title); - }); - } else if (this.tests.isUserPage.test(urlFragment)) { - var matches = urlFragment.match(/user\/([^\/]+)\/?([\w]+)?/), - userslug = matches[1], - subpage = matches[2]; - - user.getUsernameByUserslug(userslug, function(err, username) { - if (subpage) { - translator.translate('[[pages:user.' + subpage + ', ' + username + ']]', language, function(translated) { - callback(null, translated); - }); - } else { - callback(null, username); - } - }); - } else { - callback(null); - } - } - }; - - Meta.js = { - cache: undefined, - prepared: false, - scripts: [ - 'vendor/jquery/js/jquery.js', - 'vendor/jquery/js/jquery-ui-1.10.4.custom.js', - 'vendor/jquery/timeago/jquery.timeago.min.js', - 'vendor/jquery/js/jquery.form.min.js', - 'vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js', - 'vendor/jquery/deserialize/jquery.deserialize.min.js', - 'vendor/bootstrap/js/bootstrap.min.js', - 'vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js', - 'vendor/requirejs/require.js', - 'vendor/bootbox/bootbox.min.js', - 'vendor/tinycon/tinycon.js', - 'vendor/xregexp/xregexp.js', - 'vendor/xregexp/unicode/unicode-base.js', - 'vendor/buzz/buzz.min.js', - '../node_modules/templates.js/lib/templates.js', - 'src/utils.js', - 'src/app.js', - 'src/ajaxify.js', - 'src/variables.js', - 'src/widgets.js', - 'src/translator.js', - 'src/helpers.js', - 'src/overrides.js' - ], - minFile: 'nodebb.min.js', - loadRJS: function(callback) { - var rjsPath = path.join(__dirname, '..', '/public/src'); - - async.parallel({ - forum: function(next) { - utils.walk(path.join(rjsPath, 'forum'), next); - }, - modules: function(next) { - utils.walk(path.join(rjsPath, 'modules'), next); - } - }, function(err, rjsFiles) { - rjsFiles = rjsFiles.forum.concat(rjsFiles.modules); - - rjsFiles = rjsFiles.filter(function(file) { - return file.match('admin') === null; - }).map(function(file) { - return path.join('src', file.replace(rjsPath, '')); - }); - - Meta.js.scripts = Meta.js.scripts.concat(rjsFiles); - - callback(err); - }); - }, - prepare: function (callback) { - plugins.fireHook('filter:scripts.get', this.scripts, function(err, scripts) { - var jsPaths = scripts.map(function (jsPath) { - jsPath = path.normalize(jsPath); - - if (jsPath.substring(0, 7) === 'plugins') { - var matches = _.map(plugins.staticDirs, function(realPath, mappedPath) { - if (jsPath.match(mappedPath)) { - return mappedPath; - } else { - return null; - } - }).filter(function(a) { return a; }); - - if (matches.length) { - var relPath = jsPath.slice(('plugins/' + matches[0]).length), - pluginId = matches[0].split(path.sep)[0]; - - return plugins.staticDirs[matches[0]] + relPath; - } else { - winston.warn('[meta.scripts.get] Could not resolve mapped path: ' + jsPath + '. Are you sure it is defined by a plugin?'); - return null; - } - } else { - return path.join(__dirname, '..', '/public', jsPath); - } - }); - - Meta.js.scripts = jsPaths.filter(function(path) { - return path !== null; - }); - - var pluginDirectories = []; - - plugins.clientScripts = plugins.clientScripts.filter(function(path) { - if (path.indexOf('.js') !== -1) { - return true; - } else { - pluginDirectories.push(path); - return false; - } - }); - - // Add plugin scripts - Meta.js.scripts = Meta.js.scripts.concat(plugins.clientScripts); - - async.each(pluginDirectories, function(directory, next) { - utils.walk(directory, function(err, scripts) { - Meta.js.scripts = Meta.js.scripts.concat(scripts); - next(err); - }); - }, function(err) { - Meta.js.prepared = true; - callback(err); - }); - }); - }, - minify: function(minify) { - // Prepare js for minification/concatenation - var minifier = this.minifierProc = fork('minifier.js'); - - minifier.on('message', function(payload) { - if (payload.action !== 'error') { - winston.info('[meta/js] Compilation complete'); - Meta.js.cache = payload.data.js; - Meta.js.map = payload.data.map; - minifier.kill(); - - emitter.emit('meta:js.compiled'); - } else { - winston.error('[meta/js] Could not compile client-side scripts! ' + payload.error.message); - minifier.kill(); - process.exit(); - } - }); - - Meta.js.loadRJS(function() { - Meta.js.prepare(function() { - minifier.send({ - action: 'js', - minify: minify, - scripts: Meta.js.scripts - }); - }); - }); - }, - killMinifier: function(callback) { - if (this.minifierProc) { - this.minifierProc.kill('SIGTERM'); - } - } - }; - - /* Themes */ - Meta.css = {}; - Meta.css.cache = undefined; - Meta.css.branding = {}; - Meta.css.defaultBranding = {}; - - Meta.css.minify = function() { - winston.info('[meta/css] Minifying LESS/CSS'); - db.getObjectFields('config', ['theme:type', 'theme:id'], function(err, themeData) { - var themeId = (themeData['theme:id'] || 'nodebb-theme-vanilla'), - baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')), - paths = [ - baseThemePath, - path.join(__dirname, '../node_modules'), - path.join(__dirname, '../public/vendor/fontawesome/less') - ], - source = '@import "./theme";\n@import "font-awesome";', - x; - - - plugins.lessFiles = filterMissingFiles(plugins.lessFiles); - for(x=0; x 1) { + Meta.css.branding[match[1]] = match[2]; + match = re.exec(css); + } + + Meta.css.defaultBranding = Meta.css.branding; + Meta.css.updateBranding(); + + winston.info('[meta/css] Done.'); + emitter.emit('meta:css.compiled'); + }); + }); + }; + + function filterMissingFiles(files) { + return files.filter(function(file) { + var exists = fs.existsSync(path.join(__dirname, '../../node_modules', file)); + if (!exists) { + winston.warn('[meta/css] File not found! ' + file); + } + return exists; + }); + } + + Meta.css.updateBranding = function() { + var Settings = require('../settings'); + var branding = new Settings('branding', '0', {}, function() { + branding = branding.cfg._; + + for (var b in branding) { + if (branding.hasOwnProperty(b)) { + Meta.css.cache = Meta.css.cache.replace(new RegExp(Meta.css.branding[b], 'g'), branding[b]); + } + } + + Meta.css.branding = branding; + }); + }; +}; \ No newline at end of file diff --git a/src/meta/js.js b/src/meta/js.js new file mode 100644 index 0000000000..b772f0ac05 --- /dev/null +++ b/src/meta/js.js @@ -0,0 +1,166 @@ +'use strict'; + +var winston = require('winston'), + fork = require('child_process').fork, + path = require('path'), + async = require('async'), + _ = require('underscore'), + + plugins = require('../plugins'), + emitter = require('../emitter'), + utils = require('../../public/src/utils'); + +module.exports = function(Meta) { + + Meta.js = { + cache: undefined, + prepared: false, + minFile: 'nodebb.min.js', + scripts: [ + 'vendor/jquery/js/jquery.js', + 'vendor/jquery/js/jquery-ui-1.10.4.custom.js', + 'vendor/jquery/timeago/jquery.timeago.min.js', + 'vendor/jquery/js/jquery.form.min.js', + 'vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js', + 'vendor/jquery/deserialize/jquery.deserialize.min.js', + 'vendor/bootstrap/js/bootstrap.min.js', + 'vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js', + 'vendor/requirejs/require.js', + 'vendor/bootbox/bootbox.min.js', + 'vendor/tinycon/tinycon.js', + 'vendor/xregexp/xregexp.js', + 'vendor/xregexp/unicode/unicode-base.js', + 'vendor/buzz/buzz.min.js', + '../node_modules/templates.js/lib/templates.js', + 'src/utils.js', + 'src/app.js', + 'src/ajaxify.js', + 'src/variables.js', + 'src/widgets.js', + 'src/translator.js', + 'src/helpers.js', + 'src/overrides.js' + ] + }; + + Meta.js.loadRJS = function(callback) { + var rjsPath = path.join(__dirname, '../..', '/public/src'); + + async.parallel({ + forum: function(next) { + utils.walk(path.join(rjsPath, 'forum'), next); + }, + modules: function(next) { + utils.walk(path.join(rjsPath, 'modules'), next); + } + }, function(err, rjsFiles) { + if (err) { + return callback(err); + } + rjsFiles = rjsFiles.forum.concat(rjsFiles.modules); + + rjsFiles = rjsFiles.filter(function(file) { + return file.match('admin') === null; + }).map(function(file) { + return path.join('src', file.replace(rjsPath, '')); + }); + + Meta.js.scripts = Meta.js.scripts.concat(rjsFiles); + + callback(); + }); + }; + + Meta.js.prepare = function (callback) { + plugins.fireHook('filter:scripts.get', Meta.js.scripts, function(err, scripts) { + var jsPaths = scripts.map(function (jsPath) { + jsPath = path.normalize(jsPath); + + if (jsPath.substring(0, 7) === 'plugins') { + var matches = _.map(plugins.staticDirs, function(realPath, mappedPath) { + if (jsPath.match(mappedPath)) { + return mappedPath; + } else { + return null; + } + }).filter(function(a) { return a; }); + + if (matches.length) { + var relPath = jsPath.slice(('plugins/' + matches[0]).length), + pluginId = matches[0].split(path.sep)[0]; + + return plugins.staticDirs[matches[0]] + relPath; + } else { + winston.warn('[meta.scripts.get] Could not resolve mapped path: ' + jsPath + '. Are you sure it is defined by a plugin?'); + return null; + } + } else { + return path.join(__dirname, '../..', '/public', jsPath); + } + }); + + Meta.js.scripts = jsPaths.filter(function(path) { + return path !== null; + }); + + var pluginDirectories = []; + + plugins.clientScripts = plugins.clientScripts.filter(function(path) { + if (path.indexOf('.js') !== -1) { + return true; + } else { + pluginDirectories.push(path); + return false; + } + }); + + // Add plugin scripts + Meta.js.scripts = Meta.js.scripts.concat(plugins.clientScripts); + + async.each(pluginDirectories, function(directory, next) { + utils.walk(directory, function(err, scripts) { + Meta.js.scripts = Meta.js.scripts.concat(scripts); + next(err); + }); + }, function(err) { + Meta.js.prepared = true; + callback(err); + }); + }); + }; + + Meta.js.minify = function(minify) { + var minifier = Meta.js.minifierProc = fork('minifier.js'); + + minifier.on('message', function(payload) { + if (payload.action !== 'error') { + winston.info('[meta/js] Compilation complete'); + Meta.js.cache = payload.data.js; + Meta.js.map = payload.data.map; + minifier.kill(); + + emitter.emit('meta:js.compiled'); + } else { + winston.error('[meta/js] Could not compile client-side scripts! ' + payload.error.message); + minifier.kill(); + process.exit(); + } + }); + + Meta.js.loadRJS(function() { + Meta.js.prepare(function() { + minifier.send({ + action: 'js', + minify: minify, + scripts: Meta.js.scripts + }); + }); + }); + }; + + Meta.js.killMinifier = function(callback) { + if (Meta.js.minifierProc) { + Meta.js.minifierProc.kill('SIGTERM'); + } + }; +}; \ No newline at end of file diff --git a/src/meta/settings.js b/src/meta/settings.js new file mode 100644 index 0000000000..4aa60edadf --- /dev/null +++ b/src/meta/settings.js @@ -0,0 +1,43 @@ +'use strict'; + +var db = require('../database'), + plugins = require('../plugins'); + +module.exports = function(Meta) { + + Meta.settings = {}; + + Meta.settings.get = function(hash, callback) { + db.getObject('settings:' + hash, function(err, settings) { + callback(err, settings || {}); + }); + }; + + Meta.settings.getOne = function(hash, field, callback) { + db.getObjectField('settings:' + hash, field, callback); + }; + + Meta.settings.set = function(hash, values, callback) { + hash = 'settings:' + hash; + plugins.fireHook('action:settings.set', hash, values); + db.setObject(hash, values, callback); + }; + + Meta.settings.setOne = function(hash, field, value, callback) { + db.setObjectField('settings:' + hash, field, value, callback); + }; + + Meta.settings.setOnEmpty = function (hash, field, value, callback) { + Meta.settings.getOne(hash, field, function (err, curValue) { + if (err) { + return callback(err); + } + + if (!curValue) { + Meta.settings.setOne(hash, field, value, callback); + } else { + callback(); + } + }); + }; +}; \ No newline at end of file diff --git a/src/meta/sounds.js b/src/meta/sounds.js new file mode 100644 index 0000000000..b14a6375f8 --- /dev/null +++ b/src/meta/sounds.js @@ -0,0 +1,91 @@ +'use strict'; + +var path = require('path'), + fs = require('fs'), + nconf = require('nconf'), + winston = require('winston'), + rimraf = require('rimraf'), + mkdirp = require('mkdirp'), + async = require('async'), + + plugins = require('../plugins'), + db = require('../database'); + +module.exports = function(Meta) { + + Meta.sounds = {}; + + Meta.sounds.init = function() { + var soundsPath = path.join(__dirname, '../../public/sounds'); + + plugins.fireHook('filter:sounds.get', [], function(err, filePaths) { + if (err) { + winston.error('Could not initialise sound files:' + err.message); + return; + } + + // Clear the sounds directory + async.series([ + function(next) { + rimraf(soundsPath, next); + }, + function(next) { + mkdirp(soundsPath, next); + } + ], function(err) { + if (err) { + winston.error('Could not initialise sound files:' + err.message); + return; + } + + // Link paths + async.each(filePaths, function(filePath, next) { + fs[process.platform !== 'win32' ? 'symlink' : 'link'](filePath, path.join(soundsPath, path.basename(filePath)), 'file', next); + }, function(err) { + if (!err) { + winston.info('[sounds] Sounds OK'); + } else { + winston.error('[sounds] Could not initialise sounds: ' + err.message); + } + }); + }); + }); + }; + + Meta.sounds.getFiles = function(callback) { + // todo: Possibly move these into a bundled module? + fs.readdir(path.join(__dirname, '../../public/sounds'), function(err, files) { + var localList = {}; + + if (err) { + winston.error('Could not get local sound files:' + err.message); + console.log(err.stack); + return callback(null, []); + } + + // Return proper paths + files.forEach(function(filename) { + localList[filename] = nconf.get('relative_path') + '/sounds/' + filename; + }); + + callback(null, localList); + }); + }; + + Meta.sounds.getMapping = function(callback) { + db.getObject('settings:sounds', function(err, sounds) { + if (err || !sounds) { + // Send default sounds + var defaults = { + 'notification': 'notification.mp3', + 'chat-incoming': 'waterdrop-high.mp3', + 'chat-outgoing': 'waterdrop-low.mp3' + }; + + return callback(null, defaults); + } + + callback.apply(null, arguments); + }); + }; +}; \ No newline at end of file diff --git a/src/meta/themes.js b/src/meta/themes.js new file mode 100644 index 0000000000..b26253a495 --- /dev/null +++ b/src/meta/themes.js @@ -0,0 +1,98 @@ + +'use strict'; + +var nconf = require('nconf'), + fs = require('fs'), + path = require('path'), + async = require('async'), + db = require('../database'); + +module.exports = function(Meta) { + Meta.themes = {}; + + Meta.themes.get = function (callback) { + var themePath = nconf.get('themes_path'); + if (typeof themePath !== 'string') { + return callback(null, []); + } + + fs.readdir(themePath, function (err, files) { + if (err) { + return callback(err); + } + + async.filter(files, function (file, next) { + fs.stat(path.join(themePath, file), function (err, fileStat) { + if (err) { + return next(false); + } + + next((fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-')); + }); + }, function (themes) { + async.map(themes, function (theme, next) { + var config = path.join(themePath, theme, 'theme.json'); + + if (fs.existsSync(config)) { + fs.readFile(config, function (err, file) { + if (err) { + return next(); + } else { + var configObj = JSON.parse(file.toString()); + next(err, configObj); + } + }); + } else { + next(); + } + }, function (err, themes) { + themes = themes.filter(function (theme) { + return (theme !== undefined); + }); + callback(null, themes); + }); + }); + }); + }; + + Meta.themes.set = function(data, callback) { + var themeData = { + 'theme:type': data.type, + 'theme:id': data.id, + 'theme:staticDir': '', + 'theme:templates': '', + 'theme:src': '' + }; + + switch(data.type) { + case 'local': + async.waterfall([ + function(next) { + fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function(err, config) { + if (!err) { + config = JSON.parse(config.toString()); + next(null, config); + } else { + next(err); + } + }); + }, + function(config, next) { + themeData['theme:staticDir'] = config.staticDir ? config.staticDir : ''; + themeData['theme:templates'] = config.templates ? config.templates : ''; + themeData['theme:src'] = ''; + + db.setObject('config', themeData, next); + } + ], callback); + + Meta.restartRequired = true; + break; + + case 'bootswatch': + Meta.configs.set('theme:src', data.src, callback); + break; + } + }; + +}; \ No newline at end of file diff --git a/src/meta/title.js b/src/meta/title.js new file mode 100644 index 0000000000..58a03876d5 --- /dev/null +++ b/src/meta/title.js @@ -0,0 +1,67 @@ +'use strict'; + +var user = require('../user'), + translator = require('../../public/src/translator'); + +module.exports = function(Meta) { + Meta.title = {}; + + var tests = { + isCategory: /^category\/\d+\/?/, + isTopic: /^topic\/\d+\/?/, + isUserPage: /^user\/[^\/]+(\/[\w]+)?/ + }; + + Meta.title.build = function (urlFragment, language, callback) { + Meta.title.parseFragment(decodeURIComponent(urlFragment), language, function(err, title) { + if (err) { + title = Meta.config.browserTitle || 'NodeBB'; + } else { + title = (title ? title + ' | ' : '') + (Meta.config.browserTitle || 'NodeBB'); + } + + callback(null, title); + }); + }; + + Meta.title.parseFragment = function (urlFragment, language, callback) { + var translated = ['', 'recent', 'unread', 'users', 'notifications']; + if (translated.indexOf(urlFragment) !== -1) { + if (!urlFragment.length) { + urlFragment = 'home'; + } + + translator.translate('[[pages:' + urlFragment + ']]', language, function(translated) { + callback(null, translated); + }); + } else if (tests.isCategory.test(urlFragment)) { + var cid = urlFragment.match(/category\/(\d+)/)[1]; + + require('../categories').getCategoryField(cid, 'name', callback); + } else if (tests.isTopic.test(urlFragment)) { + var tid = urlFragment.match(/topic\/(\d+)/)[1]; + + require('../topics').getTopicField(tid, 'title', callback); + } else if (tests.isUserPage.test(urlFragment)) { + var matches = urlFragment.match(/user\/([^\/]+)\/?([\w]+)?/), + userslug = matches[1], + subpage = matches[2]; + + user.getUsernameByUserslug(userslug, function(err, username) { + if (err) { + return callback(err); + } + + if (!subpage) { + return callback(null, username); + } + + translator.translate('[[pages:user.' + subpage + ', ' + username + ']]', language, function(translated) { + callback(null, translated); + }); + }); + } else { + callback(null); + } + }; +}; \ No newline at end of file