Merge branch 'master' into bookmark2

v1.18.x
bdharrington7 10 years ago
commit 2a82b5db91

13
.gitattributes vendored

@ -0,0 +1,13 @@
# These files are text and should be normalized (convert crlf => lf)
*.json text
*.css text
*.less text
*.tpl text
*.html text
*.js text
*.md text
# Images should be treated as binary
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary

@ -1,75 +1,75 @@
# <img alt="NodeBB" src="http://i.imgur.com/mYxPPtB.png" /> # <img alt="NodeBB" src="http://i.imgur.com/mYxPPtB.png" />
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/NodeBB/NodeBB?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/NodeBB/NodeBB?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/NodeBB/NodeBB.svg?branch=master)](https://travis-ci.org/NodeBB/NodeBB) [![Build Status](https://travis-ci.org/NodeBB/NodeBB.svg?branch=master)](https://travis-ci.org/NodeBB/NodeBB)
[![Dependency Status](https://david-dm.org/nodebb/nodebb.svg)](https://david-dm.org/nodebb/nodebb) [![Dependency Status](https://david-dm.org/nodebb/nodebb.svg)](https://david-dm.org/nodebb/nodebb)
[![Code Climate](https://codeclimate.com/github/NodeBB/NodeBB/badges/gpa.svg)](https://codeclimate.com/github/NodeBB/NodeBB) [![Code Climate](https://codeclimate.com/github/NodeBB/NodeBB/badges/gpa.svg)](https://codeclimate.com/github/NodeBB/NodeBB)
[![Documentation Status](https://readthedocs.org/projects/nodebb/badge/?version=latest)](https://readthedocs.org/projects/nodebb/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/nodebb/badge/?version=latest)](https://readthedocs.org/projects/nodebb/?badge=latest)
**NodeBB Forum Software** is powered by Node.js and built on either a Redis or MongoDB database. It utilizes web sockets for instant interactions and real-time notifications. NodeBB is compatible down to IE8 and has many modern features out of the box such as social network integration and streaming discussions. **NodeBB Forum Software** is powered by Node.js and built on either a Redis or MongoDB database. It utilizes web sockets for instant interactions and real-time notifications. NodeBB is compatible down to IE8 and has many modern features out of the box such as social network integration and streaming discussions.
Additional functionality is enabled through the use of third-party plugins. Additional functionality is enabled through the use of third-party plugins.
* [Get NodeBB](http://www.nodebb.org/ "NodeBB") * [Get NodeBB](http://www.nodebb.org/ "NodeBB")
* [Demo & Meta Discussion](http://community.nodebb.org) * [Demo & Meta Discussion](http://community.nodebb.org)
* [Documentation & Installation Instructions](http://docs.nodebb.org) * [Documentation & Installation Instructions](http://docs.nodebb.org)
* [Help translate NodeBB](https://www.transifex.com/projects/p/nodebb/) * [Help translate NodeBB](https://www.transifex.com/projects/p/nodebb/)
* [NodeBB Blog](http://blog.nodebb.org) * [NodeBB Blog](http://blog.nodebb.org)
* [Join us on IRC](https://kiwiirc.com/client/irc.freenode.net/nodebb) - #nodebb on Freenode * [Join us on IRC](https://kiwiirc.com/client/irc.freenode.net/nodebb) - #nodebb on Freenode
* [Follow us on Twitter](http://www.twitter.com/NodeBB/ "NodeBB Twitter") * [Follow us on Twitter](http://www.twitter.com/NodeBB/ "NodeBB Twitter")
* [Like us on Facebook](http://www.facebook.com/NodeBB/ "NodeBB Facebook") * [Like us on Facebook](http://www.facebook.com/NodeBB/ "NodeBB Facebook")
## Screenshots ## Screenshots
[![](http://i.imgur.com/VCoOFyqb.png)](http://i.imgur.com/VCoOFyq.png) [![](http://i.imgur.com/VCoOFyqb.png)](http://i.imgur.com/VCoOFyq.png)
[![](http://i.imgur.com/FLOUuIqb.png)](http://i.imgur.com/FLOUuIq.png) [![](http://i.imgur.com/FLOUuIqb.png)](http://i.imgur.com/FLOUuIq.png)
[![](http://i.imgur.com/Ud1LrfIb.png)](http://i.imgur.com/Ud1LrfI.png) [![](http://i.imgur.com/Ud1LrfIb.png)](http://i.imgur.com/Ud1LrfI.png)
[![](http://i.imgur.com/h6yZ66sb.png)](http://i.imgur.com/h6yZ66s.png) [![](http://i.imgur.com/h6yZ66sb.png)](http://i.imgur.com/h6yZ66s.png)
[![](http://i.imgur.com/o90kVPib.png)](http://i.imgur.com/o90kVPi.png) [![](http://i.imgur.com/o90kVPib.png)](http://i.imgur.com/o90kVPi.png)
[![](http://i.imgur.com/AaRRrU2b.png)](http://i.imgur.com/AaRRrU2.png) [![](http://i.imgur.com/AaRRrU2b.png)](http://i.imgur.com/AaRRrU2.png)
[![](http://i.imgur.com/LmHtPhob.png)](http://i.imgur.com/LmHtPho.png) [![](http://i.imgur.com/LmHtPhob.png)](http://i.imgur.com/LmHtPho.png)
[![](http://i.imgur.com/paiJPJkb.jpg)](http://i.imgur.com/paiJPJk.jpg) [![](http://i.imgur.com/paiJPJkb.jpg)](http://i.imgur.com/paiJPJk.jpg)
[![](http://i.imgur.com/8OLssij.png)](http://i.imgur.com/8OLssij.png) [![](http://i.imgur.com/8OLssij.png)](http://i.imgur.com/8OLssij.png)
[![](http://i.imgur.com/JKOc0LZ.png)](http://i.imgur.com/JKOc0LZ.png) [![](http://i.imgur.com/JKOc0LZ.png)](http://i.imgur.com/JKOc0LZ.png)
## How can I follow along/contribute? ## How can I follow along/contribute?
* Our feature roadmap is hosted on the project wiki's [Version History / Roadmap](https://github.com/NodeBB/NodeBB/wiki/Version-History-%26-Roadmap) * Our feature roadmap is hosted on the project wiki's [Version History / Roadmap](https://github.com/NodeBB/NodeBB/wiki/Version-History-%26-Roadmap)
* If you are a developer, feel free to check out the source and submit pull requests. We also have a wide array of [plugins](http://community.nodebb.org/category/7/nodebb-plugins) which would be a great starting point for learning the codebase. * If you are a developer, feel free to check out the source and submit pull requests. We also have a wide array of [plugins](http://community.nodebb.org/category/7/nodebb-plugins) which would be a great starting point for learning the codebase.
* If you are a designer, [NodeBB needs themes](http://community.nodebb.org/category/10/nodebb-themes)! NodeBB's theming system allows extention of the base templates as well as styling via LESS or CSS. NodeBB's base theme utilizes [Bootstrap 3](http://getbootstrap.com/) but themes can choose to use a different framework altogether. * If you are a designer, [NodeBB needs themes](http://community.nodebb.org/category/10/nodebb-themes)! NodeBB's theming system allows extention of the base templates as well as styling via LESS or CSS. NodeBB's base theme utilizes [Bootstrap 3](http://getbootstrap.com/) but themes can choose to use a different framework altogether.
* If you know languages other than English you can help us translate NodeBB. We use [Transifex](https://www.transifex.com/projects/p/nodebb/) for internationalization. * If you know languages other than English you can help us translate NodeBB. We use [Transifex](https://www.transifex.com/projects/p/nodebb/) for internationalization.
* Please don't forget to **like**, **follow**, and **star our repo**! Join our growing [community](http://community.nodebb.org) to keep up to date with the latest NodeBB development. * Please don't forget to **like**, **follow**, and **star our repo**! Join our growing [community](http://community.nodebb.org) to keep up to date with the latest NodeBB development.
## Requirements ## Requirements
NodeBB requires the following software to be installed: NodeBB requires the following software to be installed:
* A version of Node.js at least 0.10 or greater * A version of Node.js at least 0.10 or greater
* Redis, version 2.8.9 or greater **or** MongoDB, version 2.6 or greater * Redis, version 2.8.9 or greater **or** MongoDB, version 2.6 or greater
* nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB) * nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB)
## Installation ## Installation
[Please refer to platform-specific installation documentation](http://docs.nodebb.org/en/latest/installing/os.html) [Please refer to platform-specific installation documentation](http://docs.nodebb.org/en/latest/installing/os.html)
## Securing NodeBB ## Securing NodeBB
It is important to ensure that your NodeBB and database servers are secured. Bear these points in mind: It is important to ensure that your NodeBB and database servers are secured. Bear these points in mind:
1. While some distributions set up Redis with a more restrictive configuration, Redis by default listens to all interfaces, which is especially dangerous when a server is open to the public. Some suggestions: 1. While some distributions set up Redis with a more restrictive configuration, Redis by default listens to all interfaces, which is especially dangerous when a server is open to the public. Some suggestions:
* Set `bind_address` to `127.0.0.1` so as to restrict access to the local machine only * Set `bind_address` to `127.0.0.1` so as to restrict access to the local machine only
* Use `requirepass` to secure Redis behind a password (preferably a long one) * Use `requirepass` to secure Redis behind a password (preferably a long one)
* Familiarise yourself with [Redis Security](http://redis.io/topics/security) * Familiarise yourself with [Redis Security](http://redis.io/topics/security)
2. Use `iptables` to secure your server from unintended open ports. In Ubuntu, `ufw` provides a friendlier interface to working with `iptables`. 2. Use `iptables` to secure your server from unintended open ports. In Ubuntu, `ufw` provides a friendlier interface to working with `iptables`.
* e.g. If your NodeBB is proxied, no ports should be open except 80 (and possibly 22, for SSH access) * e.g. If your NodeBB is proxied, no ports should be open except 80 (and possibly 22, for SSH access)
## Upgrading NodeBB ## Upgrading NodeBB
Detailed upgrade instructions are listed in [Upgrading NodeBB](https://docs.nodebb.org/en/latest/upgrading/index.html) Detailed upgrade instructions are listed in [Upgrading NodeBB](https://docs.nodebb.org/en/latest/upgrading/index.html)
## License ## License
NodeBB is licensed under the **GNU General Public License v3 (GPL-3)** (http://www.gnu.org/copyleft/gpl.html). NodeBB is licensed under the **GNU General Public License v3 (GPL-3)** (http://www.gnu.org/copyleft/gpl.html).
Interested in a sublicense agreement for use of NodeBB in a non-free/restrictive environment? Contact us at sales@nodebb.org. Interested in a sublicense agreement for use of NodeBB in a non-free/restrictive environment? Contact us at sales@nodebb.org.

832
app.js

@ -1,416 +1,416 @@
/* /*
NodeBB - A better forum platform for the modern web NodeBB - A better forum platform for the modern web
https://github.com/NodeBB/NodeBB/ https://github.com/NodeBB/NodeBB/
Copyright (C) 2013-2014 NodeBB Inc. Copyright (C) 2013-2014 NodeBB Inc.
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
"use strict"; "use strict";
/*global require, global, process*/ /*global require, global, process*/
var nconf = require('nconf'); var nconf = require('nconf');
nconf.argv().env('__'); nconf.argv().env('__');
var fs = require('fs'), var fs = require('fs'),
os = require('os'), os = require('os'),
url = require('url'), url = require('url'),
async = require('async'), async = require('async'),
semver = require('semver'), semver = require('semver'),
winston = require('winston'), winston = require('winston'),
colors = require('colors'), colors = require('colors'),
path = require('path'), path = require('path'),
pkg = require('./package.json'), pkg = require('./package.json'),
utils = require('./public/src/utils.js'); utils = require('./public/src/utils.js');
global.env = process.env.NODE_ENV || 'production'; global.env = process.env.NODE_ENV || 'production';
winston.remove(winston.transports.Console); winston.remove(winston.transports.Console);
winston.add(winston.transports.Console, { winston.add(winston.transports.Console, {
colorize: true, colorize: true,
timestamp: function() { timestamp: function() {
var date = new Date(); var date = new Date();
return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']';
}, },
level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose') level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose')
}); });
if(os.platform() === 'linux') { if(os.platform() === 'linux') {
require('child_process').exec('/usr/bin/which convert', function(err, stdout, stderr) { require('child_process').exec('/usr/bin/which convert', function(err, stdout, stderr) {
if(err || !stdout) { if(err || !stdout) {
winston.warn('Couldn\'t find convert. Did you install imagemagick?'); winston.warn('Couldn\'t find convert. Did you install imagemagick?');
} }
}); });
} }
// Alternate configuration file support // Alternate configuration file support
var configFile = path.join(__dirname, '/config.json'), var configFile = path.join(__dirname, '/config.json'),
configExists; configExists;
if (nconf.get('config')) { if (nconf.get('config')) {
configFile = path.resolve(__dirname, nconf.get('config')); configFile = path.resolve(__dirname, nconf.get('config'));
} }
configExists = fs.existsSync(configFile); configExists = fs.existsSync(configFile);
if (!nconf.get('setup') && !nconf.get('install') && !nconf.get('upgrade') && !nconf.get('reset') && configExists) { if (!nconf.get('setup') && !nconf.get('install') && !nconf.get('upgrade') && !nconf.get('reset') && configExists) {
start(); start();
} else if (nconf.get('setup') || nconf.get('install')) { } else if (nconf.get('setup') || nconf.get('install')) {
setup(); setup();
} else if (!configExists) { } else if (!configExists) {
require('./install/web').install(nconf.get('port')); require('./install/web').install(nconf.get('port'));
} else if (nconf.get('upgrade')) { } else if (nconf.get('upgrade')) {
upgrade(); upgrade();
} else if (nconf.get('reset')) { } else if (nconf.get('reset')) {
reset(); reset();
} }
function loadConfig() { function loadConfig() {
nconf.file({ nconf.file({
file: configFile file: configFile
}); });
nconf.defaults({ nconf.defaults({
base_dir: __dirname, base_dir: __dirname,
themes_path: path.join(__dirname, 'node_modules'), themes_path: path.join(__dirname, 'node_modules'),
views_dir: path.join(__dirname, 'public/templates'), views_dir: path.join(__dirname, 'public/templates'),
version: pkg.version version: pkg.version
}); });
if (!nconf.get('isCluster')) { if (!nconf.get('isCluster')) {
nconf.set('isPrimary', 'true'); nconf.set('isPrimary', 'true');
nconf.set('isCluster', 'false'); nconf.set('isCluster', 'false');
} }
// Ensure themes_path is a full filepath // Ensure themes_path is a full filepath
nconf.set('themes_path', path.resolve(__dirname, nconf.get('themes_path'))); nconf.set('themes_path', path.resolve(__dirname, nconf.get('themes_path')));
nconf.set('core_templates_path', path.join(__dirname, 'src/views')); nconf.set('core_templates_path', path.join(__dirname, 'src/views'));
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-vanilla/templates')); nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-vanilla/templates'));
if (!process.send) { if (!process.send) {
// If run using `node app`, log GNU copyright info along with server info // If run using `node app`, log GNU copyright info along with server info
winston.info('NodeBB v' + nconf.get('version') + ' Copyright (C) 2013-2014 NodeBB Inc.'); winston.info('NodeBB v' + nconf.get('version') + ' Copyright (C) 2013-2014 NodeBB Inc.');
winston.info('This program comes with ABSOLUTELY NO WARRANTY.'); winston.info('This program comes with ABSOLUTELY NO WARRANTY.');
winston.info('This is free software, and you are welcome to redistribute it under certain conditions.'); winston.info('This is free software, and you are welcome to redistribute it under certain conditions.');
winston.info(''); winston.info('');
} }
} }
function start() { function start() {
loadConfig(); loadConfig();
var db = require('./src/database'); var db = require('./src/database');
// nconf defaults, if not set in config // nconf defaults, if not set in config
if (!nconf.get('upload_path')) { if (!nconf.get('upload_path')) {
nconf.set('upload_path', '/public/uploads'); nconf.set('upload_path', '/public/uploads');
} }
// Parse out the relative_url and other goodies from the configured URL // Parse out the relative_url and other goodies from the configured URL
var urlObject = url.parse(nconf.get('url')); var urlObject = url.parse(nconf.get('url'));
var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : ''; var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host); nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
nconf.set('use_port', !!urlObject.port); nconf.set('use_port', !!urlObject.port);
nconf.set('relative_path', relativePath); nconf.set('relative_path', relativePath);
nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || 4567); nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || 4567);
nconf.set('upload_url', '/uploads/'); nconf.set('upload_url', '/uploads/');
if (nconf.get('isPrimary') === 'true') { if (nconf.get('isPrimary') === 'true') {
winston.info('Time: %s', (new Date()).toString()); winston.info('Time: %s', (new Date()).toString());
winston.info('Initializing NodeBB v%s', nconf.get('version')); winston.info('Initializing NodeBB v%s', nconf.get('version'));
winston.verbose('* using configuration stored in: %s', configFile); winston.verbose('* using configuration stored in: %s', configFile);
var host = nconf.get(nconf.get('database') + ':host'), var host = nconf.get(nconf.get('database') + ':host'),
storeLocation = host ? 'at ' + host + (host.indexOf('/') === -1 ? ':' + nconf.get(nconf.get('database') + ':port') : '') : ''; storeLocation = host ? 'at ' + host + (host.indexOf('/') === -1 ? ':' + nconf.get(nconf.get('database') + ':port') : '') : '';
winston.verbose('* using %s store %s', nconf.get('database'), storeLocation); winston.verbose('* using %s store %s', nconf.get('database'), storeLocation);
winston.verbose('* using themes stored in: %s', nconf.get('themes_path')); winston.verbose('* using themes stored in: %s', nconf.get('themes_path'));
} }
process.on('SIGTERM', shutdown); process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown); process.on('SIGINT', shutdown);
process.on('SIGHUP', restart); process.on('SIGHUP', restart);
process.on('message', function(message) { process.on('message', function(message) {
if (typeof message !== 'object') { if (typeof message !== 'object') {
return; return;
} }
var meta = require('./src/meta'); var meta = require('./src/meta');
var emitter = require('./src/emitter'); var emitter = require('./src/emitter');
switch (message.action) { switch (message.action) {
case 'reload': case 'reload':
meta.reload(); meta.reload();
break; break;
case 'js-propagate': case 'js-propagate':
meta.js.cache = message.cache; meta.js.cache = message.cache;
meta.js.map = message.map; meta.js.map = message.map;
meta.js.hash = message.hash; meta.js.hash = message.hash;
emitter.emit('meta:js.compiled'); emitter.emit('meta:js.compiled');
winston.verbose('[cluster] Client-side javascript and mapping propagated to worker %s', process.pid); winston.verbose('[cluster] Client-side javascript and mapping propagated to worker %s', process.pid);
break; break;
case 'css-propagate': case 'css-propagate':
meta.css.cache = message.cache; meta.css.cache = message.cache;
meta.css.acpCache = message.acpCache; meta.css.acpCache = message.acpCache;
meta.css.hash = message.hash; meta.css.hash = message.hash;
emitter.emit('meta:css.compiled'); emitter.emit('meta:css.compiled');
winston.verbose('[cluster] Stylesheets propagated to worker %s', process.pid); winston.verbose('[cluster] Stylesheets propagated to worker %s', process.pid);
break; break;
case 'templates:compiled': case 'templates:compiled':
emitter.emit('templates:compiled'); emitter.emit('templates:compiled');
break; break;
} }
}); });
process.on('uncaughtException', function(err) { process.on('uncaughtException', function(err) {
winston.error(err.stack); winston.error(err.stack);
console.log(err.stack); console.log(err.stack);
require('./src/meta').js.killMinifier(); require('./src/meta').js.killMinifier();
shutdown(1); shutdown(1);
}); });
async.waterfall([ async.waterfall([
async.apply(db.init), async.apply(db.init),
async.apply(db.checkCompatibility), async.apply(db.checkCompatibility),
function(next) { function(next) {
require('./src/meta').configs.init(next); require('./src/meta').configs.init(next);
}, },
function(next) { function(next) {
require('./src/meta').dependencies.check(next); require('./src/meta').dependencies.check(next);
}, },
function(next) { function(next) {
require('./src/upgrade').check(next); require('./src/upgrade').check(next);
}, },
function(next) { function(next) {
var webserver = require('./src/webserver'); var webserver = require('./src/webserver');
require('./src/socket.io').init(webserver.server); require('./src/socket.io').init(webserver.server);
if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) { if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) {
require('./src/notifications').init(); require('./src/notifications').init();
require('./src/user').startJobs(); require('./src/user').startJobs();
} }
webserver.listen(); webserver.listen();
} }
], function(err) { ], function(err) {
if (err) { if (err) {
switch(err.message) { switch(err.message) {
case 'schema-out-of-date': case 'schema-out-of-date':
winston.warn('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:'); winston.warn('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:');
winston.warn(' ./nodebb upgrade'); winston.warn(' ./nodebb upgrade');
break; break;
case 'dependencies-out-of-date': case 'dependencies-out-of-date':
winston.warn('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:'); winston.warn('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:');
winston.warn(' ./nodebb upgrade'); winston.warn(' ./nodebb upgrade');
break; break;
default: default:
if (err.stacktrace !== false) { if (err.stacktrace !== false) {
winston.error(err.stack); winston.error(err.stack);
} else { } else {
winston.error(err.message); winston.error(err.message);
} }
break; break;
} }
// Either way, bad stuff happened. Abort start. // Either way, bad stuff happened. Abort start.
process.exit(); process.exit();
} }
}); });
} }
function setup() { function setup() {
loadConfig(); loadConfig();
winston.info('NodeBB Setup Triggered via Command Line'); winston.info('NodeBB Setup Triggered via Command Line');
var install = require('./src/install'); var install = require('./src/install');
process.stdout.write('\nWelcome to NodeBB!\n'); process.stdout.write('\nWelcome to NodeBB!\n');
process.stdout.write('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.\n'); process.stdout.write('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.\n');
process.stdout.write('Press enter to accept the default setting (shown in brackets).\n'); process.stdout.write('Press enter to accept the default setting (shown in brackets).\n');
install.setup(function (err, data) { install.setup(function (err, data) {
var separator = ' '; var separator = ' ';
if (process.stdout.columns > 10) { if (process.stdout.columns > 10) {
for(var x=0,cols=process.stdout.columns-10;x<cols;x++) { for(var x=0,cols=process.stdout.columns-10;x<cols;x++) {
separator += '='; separator += '=';
} }
} }
process.stdout.write('\n' + separator + '\n\n'); process.stdout.write('\n' + separator + '\n\n');
if (err) { if (err) {
winston.error('There was a problem completing NodeBB setup: ', err.message); winston.error('There was a problem completing NodeBB setup: ', err.message);
} else { } else {
if (data.hasOwnProperty('password')) { if (data.hasOwnProperty('password')) {
process.stdout.write('An administrative user was automatically created for you:\n'); process.stdout.write('An administrative user was automatically created for you:\n');
process.stdout.write(' Username: ' + data.username + '\n'); process.stdout.write(' Username: ' + data.username + '\n');
process.stdout.write(' Password: ' + data.password + '\n'); process.stdout.write(' Password: ' + data.password + '\n');
process.stdout.write('\n'); process.stdout.write('\n');
} }
process.stdout.write('NodeBB Setup Completed. Run \'./nodebb start\' to manually start your NodeBB server.\n'); process.stdout.write('NodeBB Setup Completed. Run \'./nodebb start\' to manually start your NodeBB server.\n');
// If I am a child process, notify the parent of the returned data before exiting (useful for notifying // If I am a child process, notify the parent of the returned data before exiting (useful for notifying
// hosts of auto-generated username/password during headless setups) // hosts of auto-generated username/password during headless setups)
if (process.send) { if (process.send) {
process.send(data); process.send(data);
} }
} }
process.exit(); process.exit();
}); });
} }
function upgrade() { function upgrade() {
loadConfig(); loadConfig();
require('./src/database').init(function(err) { require('./src/database').init(function(err) {
if (err) { if (err) {
winston.error(err.stack); winston.error(err.stack);
process.exit(); process.exit();
} }
require('./src/meta').configs.init(function () { require('./src/meta').configs.init(function () {
require('./src/upgrade').upgrade(); require('./src/upgrade').upgrade();
}); });
}); });
} }
function reset() { function reset() {
loadConfig(); loadConfig();
require('./src/database').init(function(err) { require('./src/database').init(function(err) {
if (err) { if (err) {
winston.error(err.message); winston.error(err.message);
process.exit(); process.exit();
} }
if (nconf.get('t')) { if (nconf.get('t')) {
resetThemes(); resetThemes();
} else if (nconf.get('p')) { } else if (nconf.get('p')) {
if (nconf.get('p') === true) { if (nconf.get('p') === true) {
resetPlugins(); resetPlugins();
} else { } else {
resetPlugin(nconf.get('p')); resetPlugin(nconf.get('p'));
} }
} else if (nconf.get('w')) { } else if (nconf.get('w')) {
resetWidgets(); resetWidgets();
} else if (nconf.get('s')) { } else if (nconf.get('s')) {
resetSettings(); resetSettings();
} else if (nconf.get('a')) { } else if (nconf.get('a')) {
require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function(err) { require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function(err) {
if (!err) { if (!err) {
winston.info('[reset] Reset complete.'); winston.info('[reset] Reset complete.');
} else { } else {
winston.error('[reset] Errors were encountered while resetting your forum settings: %s', err.message); winston.error('[reset] Errors were encountered while resetting your forum settings: %s', err.message);
} }
process.exit(); process.exit();
}); });
} else { } else {
process.stdout.write('\nNodeBB Reset\n'.bold); process.stdout.write('\nNodeBB Reset\n'.bold);
process.stdout.write('No arguments passed in, so nothing was reset.\n\n'.yellow); process.stdout.write('No arguments passed in, so nothing was reset.\n\n'.yellow);
process.stdout.write('Use ./nodebb reset ' + '{-t|-p|-w|-s|-a}\n'.red); process.stdout.write('Use ./nodebb reset ' + '{-t|-p|-w|-s|-a}\n'.red);
process.stdout.write(' -t\tthemes\n'); process.stdout.write(' -t\tthemes\n');
process.stdout.write(' -p\tplugins\n'); process.stdout.write(' -p\tplugins\n');
process.stdout.write(' -w\twidgets\n'); process.stdout.write(' -w\twidgets\n');
process.stdout.write(' -s\tsettings\n'); process.stdout.write(' -s\tsettings\n');
process.stdout.write(' -a\tall of the above\n'); process.stdout.write(' -a\tall of the above\n');
process.stdout.write('\nPlugin reset flag (-p) can take a single argument\n'); process.stdout.write('\nPlugin reset flag (-p) can take a single argument\n');
process.stdout.write(' e.g. ./nodebb reset -p nodebb-plugin-mentions\n'); process.stdout.write(' e.g. ./nodebb reset -p nodebb-plugin-mentions\n');
process.exit(); process.exit();
} }
}); });
} }
function resetSettings(callback) { function resetSettings(callback) {
var meta = require('./src/meta'); var meta = require('./src/meta');
meta.configs.set('allowLocalLogin', 1, function(err) { meta.configs.set('allowLocalLogin', 1, function(err) {
winston.info('[reset] Settings reset to default'); winston.info('[reset] Settings reset to default');
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(err); callback(err);
} else { } else {
process.exit(); process.exit();
} }
}); });
} }
function resetThemes(callback) { function resetThemes(callback) {
var meta = require('./src/meta'); var meta = require('./src/meta');
meta.themes.set({ meta.themes.set({
type: 'local', type: 'local',
id: 'nodebb-theme-vanilla' id: 'nodebb-theme-vanilla'
}, function(err) { }, function(err) {
winston.info('[reset] Theme reset to Vanilla'); winston.info('[reset] Theme reset to Vanilla');
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(err); callback(err);
} else { } else {
process.exit(); process.exit();
} }
}); });
} }
function resetPlugin(pluginId) { function resetPlugin(pluginId) {
var db = require('./src/database'); var db = require('./src/database');
db.sortedSetRemove('plugins:active', pluginId, function(err) { db.sortedSetRemove('plugins:active', pluginId, function(err) {
if (err) { if (err) {
winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err.message); winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err.message);
} else { } else {
winston.info('[reset] Plugin `%s` disabled', pluginId); winston.info('[reset] Plugin `%s` disabled', pluginId);
} }
process.exit(); process.exit();
}); });
} }
function resetPlugins(callback) { function resetPlugins(callback) {
var db = require('./src/database'); var db = require('./src/database');
db.delete('plugins:active', function(err) { db.delete('plugins:active', function(err) {
winston.info('[reset] All Plugins De-activated'); winston.info('[reset] All Plugins De-activated');
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(err); callback(err);
} else { } else {
process.exit(); process.exit();
} }
}); });
} }
function resetWidgets(callback) { function resetWidgets(callback) {
require('./src/widgets').reset(function(err) { require('./src/widgets').reset(function(err) {
winston.info('[reset] All Widgets moved to Draft Zone'); winston.info('[reset] All Widgets moved to Draft Zone');
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(err); callback(err);
} else { } else {
process.exit(); process.exit();
} }
}); });
} }
function shutdown(code) { function shutdown(code) {
winston.info('[app] Shutdown (SIGTERM/SIGINT) Initialised.'); winston.info('[app] Shutdown (SIGTERM/SIGINT) Initialised.');
require('./src/database').close(); require('./src/database').close();
winston.info('[app] Database connection closed.'); winston.info('[app] Database connection closed.');
require('./src/webserver').server.close(); require('./src/webserver').server.close();
winston.info('[app] Web server closed to connections.'); winston.info('[app] Web server closed to connections.');
winston.info('[app] Shutdown complete.'); winston.info('[app] Shutdown complete.');
process.exit(code || 0); process.exit(code || 0);
} }
function restart() { function restart() {
if (process.send) { if (process.send) {
winston.info('[app] Restarting...'); winston.info('[app] Restarting...');
process.send({ process.send({
action: 'restart' action: 'restart'
}); });
} else { } else {
winston.error('[app] Could not restart server. Shutting down.'); winston.error('[app] Could not restart server. Shutting down.');
shutdown(1); shutdown(1);
} }
} }

@ -1,34 +1,34 @@
[ [
{ {
"name": "Announcements", "name": "Announcements",
"description": "Announcements regarding our community", "description": "Announcements regarding our community",
"bgColor": "#fda34b", "bgColor": "#fda34b",
"color": "#fff", "color": "#fff",
"icon" : "fa-bullhorn", "icon" : "fa-bullhorn",
"order": 1 "order": 1
}, },
{ {
"name": "General Discussion", "name": "General Discussion",
"description": "A place to talk about whatever you want", "description": "A place to talk about whatever you want",
"bgColor": "#59b3d0", "bgColor": "#59b3d0",
"color": "#fff", "color": "#fff",
"icon" : "fa-comments-o", "icon" : "fa-comments-o",
"order": 2 "order": 2
}, },
{ {
"name": "Blogs", "name": "Blogs",
"description": "Blog posts from individual members", "description": "Blog posts from individual members",
"bgColor": "#86ba4b", "bgColor": "#86ba4b",
"color": "#fff", "color": "#fff",
"icon" : "fa-newspaper-o", "icon" : "fa-newspaper-o",
"order": 4 "order": 4
}, },
{ {
"name": "Comments & Feedback", "name": "Comments & Feedback",
"description": "Got a question? Ask away!", "description": "Got a question? Ask away!",
"bgColor": "#e95c5a", "bgColor": "#e95c5a",
"color": "#fff", "color": "#fff",
"icon" : "fa-question", "icon" : "fa-question",
"order": 3 "order": 3
} }
] ]

@ -2,7 +2,7 @@
"name": "nodebb", "name": "nodebb",
"license": "GPL-3.0", "license": "GPL-3.0",
"description": "NodeBB Forum", "description": "NodeBB Forum",
"version": "0.7.1-dev", "version": "0.7.2-dev",
"homepage": "http://www.nodebb.org", "homepage": "http://www.nodebb.org",
"repository": { "repository": {
"type": "git", "type": "git",

@ -1,15 +1,15 @@
{ {
"password-reset-requested": "Zurücksetzung des Passworts beantragt - %1!", "password-reset-requested": "Zurücksetzung des Passworts beantragt - %1!",
"welcome-to": "Willkommen bei %1", "welcome-to": "Willkommen bei %1",
"invite": "Invitation from %1", "invite": "Einladung von %1",
"greeting_no_name": "Hallo", "greeting_no_name": "Hallo",
"greeting_with_name": "Hallo %1", "greeting_with_name": "Hallo %1",
"welcome.text1": "Vielen Dank für die Registrierung bei %1!", "welcome.text1": "Vielen Dank für die Registrierung bei %1!",
"welcome.text2": "Um dein Konto vollständig zu aktivieren, müssen wir überprüfen, ob du Besitzer der E-Mail-Adresse bist, mit der du dich registriert hast.", "welcome.text2": "Um dein Konto vollständig zu aktivieren, müssen wir überprüfen, ob du Besitzer der E-Mail-Adresse bist, mit der du dich registriert hast.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", "welcome.text3": "Ein Administrator hat deine Registration aktzeptiert. Du kannst dich jetzt mit deinem Benutzernamen/Passwort einloggen.",
"welcome.cta": "Klicke hier, um deine E-Mail-Adresse zu bestätigen.", "welcome.cta": "Klicke hier, um deine E-Mail-Adresse zu bestätigen.",
"invitation.text1": "%1 has invited you to join %2", "invitation.text1": "%1 hat dich eingeladen %2 beizutreten",
"invitation.ctr": "Click here to create your account.", "invitation.ctr": "Klicke hier, um ein Konto zu erstellen.",
"reset.text1": "Wir haben eine Anfrage auf Zurücksetzung deines Passworts erhalten, wahrscheinlich, weil du es vergessen hast. Falls dies nicht der Fall ist, ignoriere bitte diese E-Mail.", "reset.text1": "Wir haben eine Anfrage auf Zurücksetzung deines Passworts erhalten, wahrscheinlich, weil du es vergessen hast. Falls dies nicht der Fall ist, ignoriere bitte diese E-Mail.",
"reset.text2": "Klicke bitte auf den folgenden Link, um mit der Zurücksetzung deines Passworts fortzufahren:", "reset.text2": "Klicke bitte auf den folgenden Link, um mit der Zurücksetzung deines Passworts fortzufahren:",
"reset.cta": "Klicke hier, um dein Passwort zurückzusetzen", "reset.cta": "Klicke hier, um dein Passwort zurückzusetzen",

@ -51,7 +51,7 @@
"already-favourited": "Dieser Beitrag ist bereits in deinen Favoriten enthalten", "already-favourited": "Dieser Beitrag ist bereits in deinen Favoriten enthalten",
"already-unfavourited": "Du hast diesen Beitrag bereits aus deinen Favoriten entfernt", "already-unfavourited": "Du hast diesen Beitrag bereits aus deinen Favoriten entfernt",
"cant-ban-other-admins": "Du kannst andere Administratoren nicht sperren!", "cant-ban-other-admins": "Du kannst andere Administratoren nicht sperren!",
"cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", "cant-remove-last-admin": "Du bist der einzige Administrator. Füge zuerst einen anderen Administrator hinzu, bevor du dich selbst als Administrator entfernst",
"invalid-image-type": "Falsche Bildart. Erlaubte Arten sind: %1", "invalid-image-type": "Falsche Bildart. Erlaubte Arten sind: %1",
"invalid-image-extension": "Ungültige Dateinamenerweiterung", "invalid-image-extension": "Ungültige Dateinamenerweiterung",
"invalid-file-type": "Ungültiger Dateityp. Erlaubte Typen sind: %1", "invalid-file-type": "Ungültiger Dateityp. Erlaubte Typen sind: %1",
@ -60,8 +60,8 @@
"group-name-change-not-allowed": "Du kannst den Namen der Gruppe nicht ändern", "group-name-change-not-allowed": "Du kannst den Namen der Gruppe nicht ändern",
"group-already-member": "Du bist bereits Teil dieser Gruppe", "group-already-member": "Du bist bereits Teil dieser Gruppe",
"group-needs-owner": "Diese Gruppe muss mindestens einen Besitzer vorweisen", "group-needs-owner": "Diese Gruppe muss mindestens einen Besitzer vorweisen",
"group-already-invited": "This user has already been invited", "group-already-invited": "Dieser Benutzer wurde bereits eingeladen",
"group-already-requested": "Your membership request has already been submitted", "group-already-requested": "Deine Mitgliedsanfrage wurde bereits eingereicht",
"post-already-deleted": "Dieser Beitrag ist bereits gelöscht worden", "post-already-deleted": "Dieser Beitrag ist bereits gelöscht worden",
"post-already-restored": "Dieser Beitrag ist bereits wiederhergestellt worden", "post-already-restored": "Dieser Beitrag ist bereits wiederhergestellt worden",
"topic-already-deleted": "Dieses Thema ist bereits gelöscht worden", "topic-already-deleted": "Dieses Thema ist bereits gelöscht worden",
@ -79,7 +79,7 @@
"downvoting-disabled": "Downvotes sind deaktiviert.", "downvoting-disabled": "Downvotes sind deaktiviert.",
"not-enough-reputation-to-downvote": "Deine Reputation ist zu niedrig, um diesen Beitrag negativ zu bewerten.", "not-enough-reputation-to-downvote": "Deine Reputation ist zu niedrig, um diesen Beitrag negativ zu bewerten.",
"not-enough-reputation-to-flag": "Deine Reputation ist nicht gut genug, um diesen Beitrag zu melden", "not-enough-reputation-to-flag": "Deine Reputation ist nicht gut genug, um diesen Beitrag zu melden",
"already-flagged": "You have already flagged this post", "already-flagged": "Du hast diesen Beitrag bereits gemeldet",
"reload-failed": "Es ist ein Problem während des Reloads von NodeBB aufgetreten: \"%1\". NodeBB wird weiterhin clientseitige Assets bereitstellen, allerdings solltest du das, was du vor dem Reload gemacht hast, rückgängig machen.", "reload-failed": "Es ist ein Problem während des Reloads von NodeBB aufgetreten: \"%1\". NodeBB wird weiterhin clientseitige Assets bereitstellen, allerdings solltest du das, was du vor dem Reload gemacht hast, rückgängig machen.",
"registration-error": "Registrierungsfehler", "registration-error": "Registrierungsfehler",
"parse-error": "Beim auswerten der Serverantwort ist etwas schiefgegangen", "parse-error": "Beim auswerten der Serverantwort ist etwas schiefgegangen",

@ -50,7 +50,7 @@
"views": "Aufrufe", "views": "Aufrufe",
"reputation": "Reputation", "reputation": "Reputation",
"read_more": "weiterlesen", "read_more": "weiterlesen",
"more": "More", "more": "Mehr",
"posted_ago_by_guest": "%1 von einem Gast geschrieben", "posted_ago_by_guest": "%1 von einem Gast geschrieben",
"posted_ago_by": "%1 von %2 geschrieben", "posted_ago_by": "%1 von %2 geschrieben",
"posted_ago": "%1 geschrieben", "posted_ago": "%1 geschrieben",

@ -6,12 +6,12 @@
"no_groups_found": "Es sind keine Gruppen vorhanden", "no_groups_found": "Es sind keine Gruppen vorhanden",
"pending.accept": "Annehmen", "pending.accept": "Annehmen",
"pending.reject": "Abweisen", "pending.reject": "Abweisen",
"pending.accept_all": "Accept All", "pending.accept_all": "Alle annehmen",
"pending.reject_all": "Reject All", "pending.reject_all": "Alle ablehnen",
"pending.none": "There are no pending members at this time", "pending.none": "Es sind zur Zeit keine unvearbeiteten Mitglieder vorhanden",
"invited.none": "There are no invited members at this time", "invited.none": "Es sind zur Zeit keine weiteren Mitglieder eingeladen",
"invited.uninvite": "Rescind Invitation", "invited.uninvite": "Einladung zurücknehmen",
"invited.search": "Search for a user to invite to this group", "invited.search": "Suche nach einem Benutzer um ihn in diese Gruppe aufzunehmen",
"cover-instructions": "Foto auf eine Position bewegen, und <strong>Speichern</strong> drücken", "cover-instructions": "Foto auf eine Position bewegen, und <strong>Speichern</strong> drücken",
"cover-change": "Ändern", "cover-change": "Ändern",
"cover-save": "Speichern", "cover-save": "Speichern",
@ -19,7 +19,7 @@
"details.title": "Gruppendetails", "details.title": "Gruppendetails",
"details.members": "Mitgliederliste", "details.members": "Mitgliederliste",
"details.pending": "Mitglieder in Schwebe", "details.pending": "Mitglieder in Schwebe",
"details.invited": "Invited Members", "details.invited": "Eingeladene Mitglieder",
"details.has_no_posts": "Die Mitglieder dieser Gruppe haben keine Beiträge verfasst.", "details.has_no_posts": "Die Mitglieder dieser Gruppe haben keine Beiträge verfasst.",
"details.latest_posts": "Neueste Beiträge", "details.latest_posts": "Neueste Beiträge",
"details.private": "Privat", "details.private": "Privat",

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> hat ein neues Thema erstellt: <strong>%2</strong>", "user_posted_topic": "<strong>%1</strong> hat ein neues Thema erstellt: <strong>%2</strong>",
"user_mentioned_you_in": "<strong>%1</strong> erwähnte dich in <strong>%2</strong>", "user_mentioned_you_in": "<strong>%1</strong> erwähnte dich in <strong>%2</strong>",
"user_started_following_you": "<strong>%1</strong> folgt dir jetzt.", "user_started_following_you": "<strong>%1</strong> folgt dir jetzt.",
"new_register": "<strong>%1</strong> sent a registration request.", "new_register": "<strong>%1</strong> hat eine Registrationsanfrage geschickt.",
"email-confirmed": "E-Mail bestätigt", "email-confirmed": "E-Mail bestätigt",
"email-confirmed-message": "Vielen Dank für Ihre E-Mail-Validierung. Ihr Konto ist nun vollständig aktiviert.", "email-confirmed-message": "Vielen Dank für Ihre E-Mail-Validierung. Ihr Konto ist nun vollständig aktiviert.",
"email-confirm-error-message": "Es gab ein Problem bei der Validierung Ihrer E-Mail-Adresse. Möglicherweise ist der Code ungültig oder abgelaufen.", "email-confirm-error-message": "Es gab ein Problem bei der Validierung Ihrer E-Mail-Adresse. Möglicherweise ist der Code ungültig oder abgelaufen.",

@ -15,5 +15,5 @@
"alternative_registration": "Alternative Registrierung", "alternative_registration": "Alternative Registrierung",
"terms_of_use": "Nutzungsbedingungen", "terms_of_use": "Nutzungsbedingungen",
"agree_to_terms_of_use": "Ich stimme den Nutzungsbedingungen zu", "agree_to_terms_of_use": "Ich stimme den Nutzungsbedingungen zu",
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." "registration-added-to-queue": "Deine Registration wurde abgeschickt. Du wirst eine E-Mail erhalten, sobald sie von einem Administrator akzeptiert wird."
} }

@ -5,6 +5,6 @@
"mark_as_read": "Als gelesen markieren", "mark_as_read": "Als gelesen markieren",
"selected": "Ausgewählte", "selected": "Ausgewählte",
"all": "Alle", "all": "Alle",
"all_categories": "All categories", "all_categories": "Alle Kategorien",
"topics_marked_as_read.success": "Themen als gelesen markiert!" "topics_marked_as_read.success": "Themen als gelesen markiert!"
} }

@ -6,12 +6,12 @@
"postcount": "Beiträge", "postcount": "Beiträge",
"email": "E-Mail", "email": "E-Mail",
"confirm_email": "E-Mail bestätigen", "confirm_email": "E-Mail bestätigen",
"ban_account": "Ban Account", "ban_account": "Konto sperren",
"ban_account_confirm": "Do you really want to ban this user?", "ban_account_confirm": "Sind Sie sicher, dass Sie diesen Benutzer sperren möchten?",
"unban_account": "Unban Account", "unban_account": "Konto entsperren",
"delete_account": "Konto löschen", "delete_account": "Konto löschen",
"delete_account_confirm": "Bist du sicher, dass du dein Konto löschen möchtest? <br /><strong>Diese Aktion kann nicht rückgängig gemacht werden und du kannst deine Daten nicht wiederherstellen</strong><br /><br />Gebe deinen Benutzernamen ein, um zu bestätigen, dass du dieses Konto löschen möchtest.", "delete_account_confirm": "Bist du sicher, dass du dein Konto löschen möchtest? <br /><strong>Diese Aktion kann nicht rückgängig gemacht werden und du kannst deine Daten nicht wiederherstellen</strong><br /><br />Gebe deinen Benutzernamen ein, um zu bestätigen, dass du dieses Konto löschen möchtest.",
"delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />", "delete_this_account_confirm": "Bist du sicher, dass du dieses Konto löschen möchtest?<br /><strong>Diese Aktion kann nicht rückgangig gemacht werden und du kannst die Daten nicht wiederherstellen</strong><br /><br />",
"fullname": "Kompletter Name", "fullname": "Kompletter Name",
"website": "Homepage", "website": "Homepage",
"location": "Wohnort", "location": "Wohnort",
@ -68,9 +68,9 @@
"settings-require-reload": "Manche Einstellungsänderung benötigt ein aktualisieren. Drücke hier um die Seite neu zu laden.", "settings-require-reload": "Manche Einstellungsänderung benötigt ein aktualisieren. Drücke hier um die Seite neu zu laden.",
"has_no_follower": "Dieser User hat noch keine Follower.", "has_no_follower": "Dieser User hat noch keine Follower.",
"follows_no_one": "Dieser User folgt noch niemandem :(", "follows_no_one": "Dieser User folgt noch niemandem :(",
"has_no_posts": "This user hasn't posted anything yet.", "has_no_posts": "Dieser Nutzer hat noch nichts gepostet.",
"has_no_topics": "This user hasn't posted any topics yet.", "has_no_topics": "Dieser Nutzer hat noch keine Themen gepostet.",
"has_no_watched_topics": "This user hasn't watched any topics yet.", "has_no_watched_topics": "Dieser Nutzer beobachtet keine Themen.",
"email_hidden": "E-Mail Adresse versteckt", "email_hidden": "E-Mail Adresse versteckt",
"hidden": "versteckt", "hidden": "versteckt",
"paginate_description": "Themen und Beiträge in Seiten aufteilen, anstelle unendlich zu scrollen", "paginate_description": "Themen und Beiträge in Seiten aufteilen, anstelle unendlich zu scrollen",

@ -9,13 +9,13 @@
"filter-by": "Filtern nach", "filter-by": "Filtern nach",
"online-only": "Nur Online", "online-only": "Nur Online",
"picture-only": "Nur mit Bildern", "picture-only": "Nur mit Bildern",
"invite": "Invite", "invite": "Einladen",
"invitation-email-sent": "An invitation email has been sent to %1", "invitation-email-sent": "Eine Einladungsemail wurde an %1 verschickt",
"user_list": "User List", "user_list": "Nutzerliste",
"recent_topics": "Recent Topics", "recent_topics": "Neueste Themen",
"popular_topics": "Popular Topics", "popular_topics": "Beliebte Themen",
"unread_topics": "Unread Topics", "unread_topics": "Ungelesen Themen",
"categories": "Categories", "categories": "Kategorien",
"tags": "Tags", "tags": "Stichwörter",
"map": "Map" "map": "Karte"
} }

@ -1,15 +1,15 @@
{ {
"password-reset-requested": "درخواست بازیابی گذرواژه- %1!", "password-reset-requested": "درخواست بازیابی گذرواژه- %1!",
"welcome-to": "به 1% خوش آمدید", "welcome-to": "به 1% خوش آمدید",
"invite": "Invitation from %1", "invite": "دعوتنامه از %1",
"greeting_no_name": "سلام", "greeting_no_name": "سلام",
"greeting_with_name": "سلام 1%", "greeting_with_name": "سلام 1%",
"welcome.text1": "متشکر بابت ثبت نام در %1!", "welcome.text1": "متشکر بابت ثبت نام در %1!",
"welcome.text2": "برای فعال کردن کامل اکانت شما، ما نیاز داریم تا اطمینان حاصل کنیم که شما مالک ایمیلی که با ان ثبت نام کردید هستید.", "welcome.text2": "برای فعال کردن کامل اکانت شما، ما نیاز داریم تا اطمینان حاصل کنیم که شما مالک ایمیلی که با ان ثبت نام کردید هستید.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", "welcome.text3": "ِک مدیر درخواست ثبت نام شما را قبول کرده. اکنون میتوانید با نام کاربری/رمز عبور خود وارد شوید",
"welcome.cta": "برای تأیید آدرس ایمیل خود اینجا کلیک کنید", "welcome.cta": "برای تأیید آدرس ایمیل خود اینجا کلیک کنید",
"invitation.text1": "%1 has invited you to join %2", "invitation.text1": "%1 شما را برای پیوستن به %2 دعوت کرده",
"invitation.ctr": "Click here to create your account.", "invitation.ctr": "برای ساخت حسابتان اینجا را کلیک کنید",
"reset.text1": "ما یک درخواست برای بازنشانی رمزعبور شما دریافت کرده ایم، احتمالا به این دلیل که شما آن را فراموش کرده اید. اگر این مورد نیست و شما رمز خود را به یاد دارید، لطفا این ایمیل را نادیده بگیرید.", "reset.text1": "ما یک درخواست برای بازنشانی رمزعبور شما دریافت کرده ایم، احتمالا به این دلیل که شما آن را فراموش کرده اید. اگر این مورد نیست و شما رمز خود را به یاد دارید، لطفا این ایمیل را نادیده بگیرید.",
"reset.text2": "برای ادامه بازنشانی رمز، لطفابر روی این لینک کلیک کنید:", "reset.text2": "برای ادامه بازنشانی رمز، لطفابر روی این لینک کلیک کنید:",
"reset.cta": "برای تنظیم مجدد گذرواژه‌ی خود اینجا کلیک کنید", "reset.cta": "برای تنظیم مجدد گذرواژه‌ی خود اینجا کلیک کنید",

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> یک جستار جدید ارسال کرده: <strong>%2</strong>", "user_posted_topic": "<strong>%1</strong> یک جستار جدید ارسال کرده: <strong>%2</strong>",
"user_mentioned_you_in": "<strong>%1</strong> در \n<strong>%1</strong> mentioned you in <strong>%2</strong> از شما نام برده", "user_mentioned_you_in": "<strong>%1</strong> در \n<strong>%1</strong> mentioned you in <strong>%2</strong> از شما نام برده",
"user_started_following_you": "<strong>%1</strong> شروع به دنبال کردن شما کرده", "user_started_following_you": "<strong>%1</strong> شروع به دنبال کردن شما کرده",
"new_register": "<strong>%1</strong> sent a registration request.", "new_register": "<strong>%1</strong> یک درخواست ثبت نام ارسال کرده است",
"email-confirmed": "رایانامه تایید شد", "email-confirmed": "رایانامه تایید شد",
"email-confirmed-message": "بابت تایید ایمیلتان سپاس‌گزاریم. حساب کاربری شما اکنون به صورت کامل فعال شده است.", "email-confirmed-message": "بابت تایید ایمیلتان سپاس‌گزاریم. حساب کاربری شما اکنون به صورت کامل فعال شده است.",
"email-confirm-error-message": "خطایی در تایید آدرس ایمیل شما پیش آمده است. ممکن است کد نا‌معتبر و یا منقضی شده باشد.", "email-confirm-error-message": "خطایی در تایید آدرس ایمیل شما پیش آمده است. ممکن است کد نا‌معتبر و یا منقضی شده باشد.",

@ -15,5 +15,5 @@
"alternative_registration": "روش نام‌نویسی جایگزین", "alternative_registration": "روش نام‌نویسی جایگزین",
"terms_of_use": "شرایط استفاده", "terms_of_use": "شرایط استفاده",
"agree_to_terms_of_use": "با شرایط استفاده موافقم", "agree_to_terms_of_use": "با شرایط استفاده موافقم",
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." "registration-added-to-queue": "ثبت نام شما به صف تایید اضافه شد. وقتی توسط یک مدیر تایید شد شما رایانامه ای دریافت خواهید کرد."
} }

@ -5,7 +5,7 @@
"no_topics_found": "هیچ جستاری یافت نشد!", "no_topics_found": "هیچ جستاری یافت نشد!",
"no_posts_found": "دیدگاهی یافت نشد!", "no_posts_found": "دیدگاهی یافت نشد!",
"post_is_deleted": "این دیدگاه پاک شده!", "post_is_deleted": "این دیدگاه پاک شده!",
"topic_is_deleted": "This topic is deleted!", "topic_is_deleted": "جستار حذف شده است!",
"profile": "نمایه", "profile": "نمایه",
"posted_by": "ارسال شده توسط %1", "posted_by": "ارسال شده توسط %1",
"posted_by_guest": "ارسال شده توسط مهمان", "posted_by_guest": "ارسال شده توسط مهمان",

@ -5,6 +5,6 @@
"mark_as_read": "خوانده شده بگیر", "mark_as_read": "خوانده شده بگیر",
"selected": "برگزیده", "selected": "برگزیده",
"all": "همه", "all": "همه",
"all_categories": "All categories", "all_categories": "تمام دسته ها",
"topics_marked_as_read.success": "همه جستارها خوانده شدند" "topics_marked_as_read.success": "همه جستارها خوانده شدند"
} }

@ -25,7 +25,7 @@
"watched": "پاییده شده", "watched": "پاییده شده",
"followers": "دنبال‌کننده‌ها", "followers": "دنبال‌کننده‌ها",
"following": "دنبال‌شونده‌ها", "following": "دنبال‌شونده‌ها",
"aboutme": "About me", "aboutme": "درباره ی من",
"signature": "امضا", "signature": "امضا",
"gravatar": "گراواتار", "gravatar": "گراواتار",
"birthday": "روز تولد", "birthday": "روز تولد",
@ -68,21 +68,21 @@
"settings-require-reload": "تغییر برخی تنظیمات مستلزم بارگذاری مجدد هستند. برای بارگذاری مجدد صفحه اینجا کلیک کنید.", "settings-require-reload": "تغییر برخی تنظیمات مستلزم بارگذاری مجدد هستند. برای بارگذاری مجدد صفحه اینجا کلیک کنید.",
"has_no_follower": "این کاربر هیچ دنبال‌کننده‌ای ندارد :(", "has_no_follower": "این کاربر هیچ دنبال‌کننده‌ای ندارد :(",
"follows_no_one": "این کاربر هیچ کسی را دنبال نمی‌کند :(", "follows_no_one": "این کاربر هیچ کسی را دنبال نمی‌کند :(",
"has_no_posts": "This user hasn't posted anything yet.", "has_no_posts": "این کاربر تا به حال هیچ چیزی ارسال نکرده است.",
"has_no_topics": "This user hasn't posted any topics yet.", "has_no_topics": "این کاربر تا به حال هیچ جستاری ارسال نکرده است",
"has_no_watched_topics": "This user hasn't watched any topics yet.", "has_no_watched_topics": "این کاربر تا به حال هیچ جستاری را نپاییده است",
"email_hidden": "رایانامه پنهان شده", "email_hidden": "رایانامه پنهان شده",
"hidden": "پنهان", "hidden": "پنهان",
"paginate_description": "Paginate topics and posts instead of using infinite scroll", "paginate_description": "Paginate topics and posts instead of using infinite scroll",
"topics_per_page": "شمار جستارها در هر برگه", "topics_per_page": "شمار جستارها در هر برگه",
"posts_per_page": "شمار دیدگاه‌ها در هر برگه", "posts_per_page": "شمار دیدگاه‌ها در هر برگه",
"notification_sounds": "Play a sound when you receive a notification", "notification_sounds": "پخش صدا زمانی که یک آگاه سازی دریافت میکنید",
"browsing": "تنظیمات مرور", "browsing": "تنظیمات مرور",
"open_links_in_new_tab": "Open outgoing links in new tab", "open_links_in_new_tab": "پیوندهای به بیرون را در برگ جدید باز کن",
"enable_topic_searching": "فعال کردن جستجوی داخل-جستار ", "enable_topic_searching": "فعال کردن جستجوی داخل-جستار ",
"topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen",
"follow_topics_you_reply_to": "Follow topics that you reply to", "follow_topics_you_reply_to": "تاپیک هایی که پاسخ داده ای را دنبال کن",
"follow_topics_you_create": "Follow topics you create", "follow_topics_you_create": "جستارهایی که ایجاد کرده ای را دنبال کن",
"grouptitle": "عنوان گروهی که میخواهید نشان داده شود را انتخاب کنید.", "grouptitle": "عنوان گروهی که میخواهید نشان داده شود را انتخاب کنید.",
"no-group-title": "عنوان گروه ای نیست" "no-group-title": "عنوان گروه ای نیست"
} }

@ -9,13 +9,13 @@
"filter-by": "غربال با", "filter-by": "غربال با",
"online-only": "فقط آنلاین", "online-only": "فقط آنلاین",
"picture-only": "عکس فقط", "picture-only": "عکس فقط",
"invite": "Invite", "invite": "دعوت",
"invitation-email-sent": "An invitation email has been sent to %1", "invitation-email-sent": "رایانامه ی دعوتنامه به %1 ارسال شد",
"user_list": "User List", "user_list": "فهرست کاربران",
"recent_topics": "Recent Topics", "recent_topics": "جستارهای اخیر",
"popular_topics": "Popular Topics", "popular_topics": "جستارهای محبوب",
"unread_topics": "Unread Topics", "unread_topics": "جستارهای خوانده نشده",
"categories": "Categories", "categories": "دسته ها",
"tags": "Tags", "tags": "برچسب‌ها",
"map": "Map" "map": "نقشه"
} }

@ -1,15 +1,15 @@
{ {
"password-reset-requested": "Demande de réinitialisation du mot de passe - %1!", "password-reset-requested": "Demande de réinitialisation du mot de passe - %1!",
"welcome-to": "Bienvenue sur %1", "welcome-to": "Bienvenue sur %1",
"invite": "Invitation from %1", "invite": "Invitation de %1",
"greeting_no_name": "Bonjour", "greeting_no_name": "Bonjour",
"greeting_with_name": "Bonjour %1", "greeting_with_name": "Bonjour %1",
"welcome.text1": "Merci de vous être inscrit sur %1!", "welcome.text1": "Merci de vous être inscrit sur %1!",
"welcome.text2": "Pour activer totalement votre compte, nous devons vérifier que vous êtes bien propriétaire de l'adresse email que vous avez utilisé pour vous inscrire.", "welcome.text2": "Pour activer totalement votre compte, nous devons vérifier que vous êtes bien propriétaire de l'adresse email que vous avez utilisé pour vous inscrire.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", "welcome.text3": "Un administrateur a accepté votre demande d'inscription. Vous pouvez maintenant vous connecter avec vos identifiants/mots de passe.",
"welcome.cta": "Cliquez ici pour confirmer votre adresse email", "welcome.cta": "Cliquez ici pour confirmer votre adresse email",
"invitation.text1": "%1 has invited you to join %2", "invitation.text1": "%1 vous a invité à joindre %2",
"invitation.ctr": "Click here to create your account.", "invitation.ctr": "Cliquer ici pour créer votre compte.",
"reset.text1": "Nous avons reçu une demande de réinitialisation de votre mot de passe, probablement parce que vous l'avez oublié. Si ce n'est pas le cas, veuillez ignorer cet email.", "reset.text1": "Nous avons reçu une demande de réinitialisation de votre mot de passe, probablement parce que vous l'avez oublié. Si ce n'est pas le cas, veuillez ignorer cet email.",
"reset.text2": "Pour confirmer la réinitialisation de votre mot de passe, veuillez cliquer sur le lien suivant :", "reset.text2": "Pour confirmer la réinitialisation de votre mot de passe, veuillez cliquer sur le lien suivant :",
"reset.cta": "Cliquez ici pour réinitialiser votre mot de passe", "reset.cta": "Cliquez ici pour réinitialiser votre mot de passe",

@ -51,7 +51,7 @@
"already-favourited": "Vous avez déjà mis ce message en favoris", "already-favourited": "Vous avez déjà mis ce message en favoris",
"already-unfavourited": "Vous avez déjà retiré ce message des favoris", "already-unfavourited": "Vous avez déjà retiré ce message des favoris",
"cant-ban-other-admins": "Vous ne pouvez pas bannir les autres administrateurs !", "cant-ban-other-admins": "Vous ne pouvez pas bannir les autres administrateurs !",
"cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", "cant-remove-last-admin": "Vous seul êtes administrateur. Ajouter un autre utilisateur en tant qu'administrateur avant de vous en retirer.",
"invalid-image-type": "Type d'image invalide. Les types autorisés sont: %1", "invalid-image-type": "Type d'image invalide. Les types autorisés sont: %1",
"invalid-image-extension": "Extension d'image invalide", "invalid-image-extension": "Extension d'image invalide",
"invalid-file-type": "Type de fichier non valide. Les types autorisés sont : %1", "invalid-file-type": "Type de fichier non valide. Les types autorisés sont : %1",
@ -60,8 +60,8 @@
"group-name-change-not-allowed": "Modification du nom de groupe non permise", "group-name-change-not-allowed": "Modification du nom de groupe non permise",
"group-already-member": "Vous faites déjà parti de ce groupe", "group-already-member": "Vous faites déjà parti de ce groupe",
"group-needs-owner": "Ce groupe nécessite au moins un propriétaire", "group-needs-owner": "Ce groupe nécessite au moins un propriétaire",
"group-already-invited": "This user has already been invited", "group-already-invited": "Cet utilisateur a déjà été invité.",
"group-already-requested": "Your membership request has already been submitted", "group-already-requested": "Votre demande d'adhésion a déjà été envoyée.",
"post-already-deleted": "Message déjà supprimé", "post-already-deleted": "Message déjà supprimé",
"post-already-restored": "Message déjà restauré", "post-already-restored": "Message déjà restauré",
"topic-already-deleted": "Sujet déjà supprimé", "topic-already-deleted": "Sujet déjà supprimé",
@ -79,7 +79,7 @@
"downvoting-disabled": "Les votes négatifs ne sont pas autorisés", "downvoting-disabled": "Les votes négatifs ne sont pas autorisés",
"not-enough-reputation-to-downvote": "Vous n'avez pas une réputation assez élevée pour noter négativement ce message", "not-enough-reputation-to-downvote": "Vous n'avez pas une réputation assez élevée pour noter négativement ce message",
"not-enough-reputation-to-flag": "Vous n'avez pas une réputation assez élevée pour signaler ce message", "not-enough-reputation-to-flag": "Vous n'avez pas une réputation assez élevée pour signaler ce message",
"already-flagged": "You have already flagged this post", "already-flagged": "Vous avez déjà signalé ce message",
"reload-failed": "NodeBB a rencontré un problème lors du rechargement : \"% 1\" . NodeBB continuera de fonctionner côté client, même si vous devez annuler ce que vous avez fait juste avant de recharger .", "reload-failed": "NodeBB a rencontré un problème lors du rechargement : \"% 1\" . NodeBB continuera de fonctionner côté client, même si vous devez annuler ce que vous avez fait juste avant de recharger .",
"registration-error": "Erreur d'enregistrement", "registration-error": "Erreur d'enregistrement",
"parse-error": "Une erreur est survenue en analysant la réponse du serveur", "parse-error": "Une erreur est survenue en analysant la réponse du serveur",

@ -50,7 +50,7 @@
"views": "Vues", "views": "Vues",
"reputation": "Réputation", "reputation": "Réputation",
"read_more": "En lire plus", "read_more": "En lire plus",
"more": "More", "more": "Plus",
"posted_ago_by_guest": "posté %1 par un invité", "posted_ago_by_guest": "posté %1 par un invité",
"posted_ago_by": "posté %1 par %2", "posted_ago_by": "posté %1 par %2",
"posted_ago": "posté %1", "posted_ago": "posté %1",

@ -6,12 +6,12 @@
"no_groups_found": "Il n'y a aucun groupe", "no_groups_found": "Il n'y a aucun groupe",
"pending.accept": "Accepter", "pending.accept": "Accepter",
"pending.reject": "Refuser", "pending.reject": "Refuser",
"pending.accept_all": "Accept All", "pending.accept_all": "Tout accepter",
"pending.reject_all": "Reject All", "pending.reject_all": "Tout rejeter",
"pending.none": "There are no pending members at this time", "pending.none": "Il n'y a aucun membre en attente pour le moment",
"invited.none": "There are no invited members at this time", "invited.none": "Il n'y a aucun membre invité pour le moment",
"invited.uninvite": "Rescind Invitation", "invited.uninvite": "Résilier l'invitation",
"invited.search": "Search for a user to invite to this group", "invited.search": "Chercher un utilisateur a inviter dans ce groupe",
"cover-instructions": "Glissez-déposez une image, ajustez la position, et cliquez sur <strong>Enregistrer</strong>", "cover-instructions": "Glissez-déposez une image, ajustez la position, et cliquez sur <strong>Enregistrer</strong>",
"cover-change": "Modifier", "cover-change": "Modifier",
"cover-save": "Enregistrer", "cover-save": "Enregistrer",
@ -19,7 +19,7 @@
"details.title": "Informations du groupe", "details.title": "Informations du groupe",
"details.members": "Liste des membres", "details.members": "Liste des membres",
"details.pending": "Membres en attente", "details.pending": "Membres en attente",
"details.invited": "Invited Members", "details.invited": "Inviter des Membres",
"details.has_no_posts": "Les membres de ce groupe n'ont envoyé aucun message.", "details.has_no_posts": "Les membres de ce groupe n'ont envoyé aucun message.",
"details.latest_posts": "Derniers messages", "details.latest_posts": "Derniers messages",
"details.private": "Privé", "details.private": "Privé",

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> a posté un nouveau sujet: <strong>%2</strong>.", "user_posted_topic": "<strong>%1</strong> a posté un nouveau sujet: <strong>%2</strong>.",
"user_mentioned_you_in": "<strong>%1</strong> vous a mentionné dans <strong>%2</strong>", "user_mentioned_you_in": "<strong>%1</strong> vous a mentionné dans <strong>%2</strong>",
"user_started_following_you": "<strong>%1</strong> vous suit.", "user_started_following_you": "<strong>%1</strong> vous suit.",
"new_register": "<strong>%1</strong> sent a registration request.", "new_register": "<strong>%1</strong> a envoyé une demande d'incription.",
"email-confirmed": "Email vérifié", "email-confirmed": "Email vérifié",
"email-confirmed-message": "Merci pour la validation de votre adresse email. Votre compte est désormais activé.", "email-confirmed-message": "Merci pour la validation de votre adresse email. Votre compte est désormais activé.",
"email-confirm-error-message": "Il y a un un problème dans la vérification de votre adresse email. Le code est peut être invalide ou a expiré.", "email-confirm-error-message": "Il y a un un problème dans la vérification de votre adresse email. Le code est peut être invalide ou a expiré.",

@ -15,5 +15,5 @@
"alternative_registration": "Autres méthodes d'inscription", "alternative_registration": "Autres méthodes d'inscription",
"terms_of_use": "Conditions d'utilisation", "terms_of_use": "Conditions d'utilisation",
"agree_to_terms_of_use": "J'accepte les Conditions d'utilisation", "agree_to_terms_of_use": "J'accepte les Conditions d'utilisation",
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." "registration-added-to-queue": "Votre inscription a été ajoutée à la liste d'approbation. Vous recevrez un email quand celle-ci sera acceptée par un administrateur."
} }

@ -5,6 +5,6 @@
"mark_as_read": "Marquer comme lu", "mark_as_read": "Marquer comme lu",
"selected": "Sélectionnés", "selected": "Sélectionnés",
"all": "Tous", "all": "Tous",
"all_categories": "All categories", "all_categories": "Toutes Catégories",
"topics_marked_as_read.success": "Sujets marqués comme lus !" "topics_marked_as_read.success": "Sujets marqués comme lus !"
} }

@ -6,12 +6,12 @@
"postcount": "Nombre de messages", "postcount": "Nombre de messages",
"email": "Email", "email": "Email",
"confirm_email": "Confirmer l'adresse email", "confirm_email": "Confirmer l'adresse email",
"ban_account": "Bannir un compte", "ban_account": "Bannir",
"ban_account_confirm": "Êtes-vous sûr de bien vouloir bannir cet utilisateur ?", "ban_account_confirm": "Êtes-vous sûr de bien vouloir bannir cet utilisateur ?",
"unban_account": "Unban Account", "unban_account": "Restaurer le Compte",
"delete_account": "Supprimer le compte", "delete_account": "Supprimer le compte",
"delete_account_confirm": "Êtes-vous sûr de vouloir supprimer votre compte? <br /> <strong> Cette action est irréversible et vous ne serez pas en mesure de récupérer vos données</ strong> <br /> <br /> Entrez votre nom d'utilisateur pour confirmer que vous souhaitez détruire votre compte.", "delete_account_confirm": "Êtes-vous sûr de vouloir supprimer votre compte? <br /> <strong> Cette action est irréversible et vous ne serez pas en mesure de récupérer vos données</ strong> <br /> <br /> Entrez votre nom d'utilisateur pour confirmer que vous souhaitez détruire votre compte.",
"delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />", "delete_this_account_confirm": "Etes-vous sûr de vouloir supprimer ce compte? <br /><strong>Cette action est irréversible et vous ne pourrez récupérer aucune donnée.",
"fullname": "Nom", "fullname": "Nom",
"website": "Site web", "website": "Site web",
"location": "Emplacement", "location": "Emplacement",
@ -68,9 +68,9 @@
"settings-require-reload": "Certains réglages nécessitent un rechargement. Cliquez ici pour recharger la page.", "settings-require-reload": "Certains réglages nécessitent un rechargement. Cliquez ici pour recharger la page.",
"has_no_follower": "Cet utilisateur n'est suivi par personne :(", "has_no_follower": "Cet utilisateur n'est suivi par personne :(",
"follows_no_one": "Cet utilisateur ne suit personne :(", "follows_no_one": "Cet utilisateur ne suit personne :(",
"has_no_posts": "This user hasn't posted anything yet.", "has_no_posts": "Cet utilisateur n'a encore rien posté.",
"has_no_topics": "This user hasn't posted any topics yet.", "has_no_topics": "Cet utilisateur n'a encore créé aucun sujet.",
"has_no_watched_topics": "This user hasn't watched any topics yet.", "has_no_watched_topics": "Cet utilisateur n'a encore consulté aucun sujet.",
"email_hidden": "Email masqué", "email_hidden": "Email masqué",
"hidden": "masqué", "hidden": "masqué",
"paginate_description": "Utiliser la pagination des sujets et des messages à la place du défilement infini.", "paginate_description": "Utiliser la pagination des sujets et des messages à la place du défilement infini.",

@ -9,13 +9,13 @@
"filter-by": "Filtrer par", "filter-by": "Filtrer par",
"online-only": "En ligne uniquement", "online-only": "En ligne uniquement",
"picture-only": "Avec image uniquement", "picture-only": "Avec image uniquement",
"invite": "Invite", "invite": "Invitation",
"invitation-email-sent": "An invitation email has been sent to %1", "invitation-email-sent": "Un email d'invitation a été envoyé à %1",
"user_list": "User List", "user_list": "Liste d'Utilisateurs",
"recent_topics": "Recent Topics", "recent_topics": "Sujets Récents",
"popular_topics": "Popular Topics", "popular_topics": "Sujets Populaires",
"unread_topics": "Unread Topics", "unread_topics": "Sujets Non-Lus",
"categories": "Categories", "categories": "Catégories",
"tags": "Tags", "tags": "Mots-clés",
"map": "Map" "map": "Carte"
} }

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> ha postato un nuovo Topic: <strong>%2</strong>", "user_posted_topic": "<strong>%1</strong> ha postato un nuovo Topic: <strong>%2</strong>",
"user_mentioned_you_in": "<strong>%1</strong> ti ha menzionato in <strong>%2</strong>", "user_mentioned_you_in": "<strong>%1</strong> ti ha menzionato in <strong>%2</strong>",
"user_started_following_you": "<strong>%1</strong> ha iniziato a seguirti.", "user_started_following_you": "<strong>%1</strong> ha iniziato a seguirti.",
"new_register": "<strong>%1</strong> sent a registration request.", "new_register": "<strong>%1</strong> ha inviato una richiesta di registrazione.",
"email-confirmed": "Email Confermata", "email-confirmed": "Email Confermata",
"email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.", "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.",
"email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.", "email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.",

@ -2,7 +2,7 @@
"new_topic_button": "Nieuw onderwerp", "new_topic_button": "Nieuw onderwerp",
"guest-login-post": "Log in om een reactie te plaatsen", "guest-login-post": "Log in om een reactie te plaatsen",
"no_topics": "<strong>Er zijn geen onderwerpen in deze categorie.</strong><br />Waarom maak je er niet een aan?", "no_topics": "<strong>Er zijn geen onderwerpen in deze categorie.</strong><br />Waarom maak je er niet een aan?",
"browsing": "verkennen", "browsing": "browsing",
"no_replies": "Niemand heeft gereageerd", "no_replies": "Niemand heeft gereageerd",
"share_this_category": "Deel deze categorie", "share_this_category": "Deel deze categorie",
"watch": "Volgen", "watch": "Volgen",

@ -1,15 +1,15 @@
{ {
"password-reset-requested": "Om wachtwoordherstel verzocht - %1!", "password-reset-requested": "Wachtwoord reset gevraagd - %1!",
"welcome-to": "Welkom bij %1", "welcome-to": "Welkom bij %1",
"invite": "Invitation from %1", "invite": "Uitnodiging van %1 ",
"greeting_no_name": "Hallo", "greeting_no_name": "Hallo",
"greeting_with_name": "Hallo %1", "greeting_with_name": "Hallo %1",
"welcome.text1": "Bedank voor het registreren bij %1!", "welcome.text1": "Bedank voor het registreren bij %1!",
"welcome.text2": "Om de account volledig te activeren, dienen de instructies uit het bevestigingsbericht opgevolgd te worden. Controleer daarom nu eerst de e-mail inbox voor de activeringscode en volg de link in het bericht.", "welcome.text2": "Om de account volledig te activeren, dienen de instructies uit het bevestigingsbericht opgevolgd te worden. Controleer daarom nu eerst de e-mail inbox voor de activeringscode en volg de link in het bericht.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", "welcome.text3": "Een administrator heeft uw registratie geaccepteerd. U kan nu inloggen met uw gebruikersnaam en wachtwoord.",
"welcome.cta": "Klik hier voor bevestigen van het e-mailadres", "welcome.cta": "Klik hier voor bevestigen van het e-mailadres",
"invitation.text1": "%1 has invited you to join %2", "invitation.text1": "%1 heeft u uitgenodigd voor %2 ",
"invitation.ctr": "Click here to create your account.", "invitation.ctr": "Klik hier om uw account aan te maken.",
"reset.text1": "Wij ontvingen zojuist een verzoek om het wachtwoord van de account, bij onze website bekend als gekoppeld aan dit e-mailadres, te herstellen. Is dit verzoek niet bekend en geautoriseerd, dan kan dit bericht genegeerd worden.", "reset.text1": "Wij ontvingen zojuist een verzoek om het wachtwoord van de account, bij onze website bekend als gekoppeld aan dit e-mailadres, te herstellen. Is dit verzoek niet bekend en geautoriseerd, dan kan dit bericht genegeerd worden.",
"reset.text2": "Om het wachtwoord opnieuw in te stellen, klik op deze link:", "reset.text2": "Om het wachtwoord opnieuw in te stellen, klik op deze link:",
"reset.cta": "Klik hier voor wachtwoordherstel", "reset.cta": "Klik hier voor wachtwoordherstel",

@ -51,7 +51,7 @@
"already-favourited": "Dit bericht staat al tussen de favorieten", "already-favourited": "Dit bericht staat al tussen de favorieten",
"already-unfavourited": "Dit bericht is al uit favorieten verwijderd", "already-unfavourited": "Dit bericht is al uit favorieten verwijderd",
"cant-ban-other-admins": "Het is niet toegestaan andere beheerders te verbannen!", "cant-ban-other-admins": "Het is niet toegestaan andere beheerders te verbannen!",
"cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", "cant-remove-last-admin": "U bent de enigen administrator. Voeg een andere gebruiker toe als administrator voordat u uw zelf verweiderd als admin",
"invalid-image-type": "Ongeldig bestandstype afbeelding. Deze afbeelding is van een bestandstype dat niet ondersteund wordt. Toegestane bestandstypes voor afbeeldingsbestanden zijn: %1", "invalid-image-type": "Ongeldig bestandstype afbeelding. Deze afbeelding is van een bestandstype dat niet ondersteund wordt. Toegestane bestandstypes voor afbeeldingsbestanden zijn: %1",
"invalid-image-extension": "Ongeldige bestandstype afbeelding", "invalid-image-extension": "Ongeldige bestandstype afbeelding",
"invalid-file-type": "Dit bestandstype wordt niet ondersteund. Toegestane bestandstypen zijn: %1", "invalid-file-type": "Dit bestandstype wordt niet ondersteund. Toegestane bestandstypen zijn: %1",
@ -60,8 +60,8 @@
"group-name-change-not-allowed": "Het veranderen van de groepsnaam is niet toegestaan!", "group-name-change-not-allowed": "Het veranderen van de groepsnaam is niet toegestaan!",
"group-already-member": "Groepslidmaatschap al aanwezig", "group-already-member": "Groepslidmaatschap al aanwezig",
"group-needs-owner": "De groep vereist ten minste 1 eigenaar", "group-needs-owner": "De groep vereist ten minste 1 eigenaar",
"group-already-invited": "This user has already been invited", "group-already-invited": "Deze gebruiker is all uitgenodigt ",
"group-already-requested": "Your membership request has already been submitted", "group-already-requested": "Uw lidmaatschap aanvraag is all verstuurd",
"post-already-deleted": "Dit bericht is al verwijderd", "post-already-deleted": "Dit bericht is al verwijderd",
"post-already-restored": "Dit bericht is al hersteld", "post-already-restored": "Dit bericht is al hersteld",
"topic-already-deleted": "Dit onderwerp is al verwijderd", "topic-already-deleted": "Dit onderwerp is al verwijderd",
@ -79,7 +79,7 @@
"downvoting-disabled": "Negatief stemmen staat uitgeschakeld.", "downvoting-disabled": "Negatief stemmen staat uitgeschakeld.",
"not-enough-reputation-to-downvote": "Deze gebruikersaccount beschikt over onvoldoende reputatie om een negatieve stem uit te mogen brengen.", "not-enough-reputation-to-downvote": "Deze gebruikersaccount beschikt over onvoldoende reputatie om een negatieve stem uit te mogen brengen.",
"not-enough-reputation-to-flag": "Onvoldoende reputatie om dit bericht aan beheerders te mogen melden.", "not-enough-reputation-to-flag": "Onvoldoende reputatie om dit bericht aan beheerders te mogen melden.",
"already-flagged": "You have already flagged this post", "already-flagged": "U heeft deze post all gerapporteerd ",
"reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegen gekomen. NodeBB blijft operationeel echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.", "reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegen gekomen. NodeBB blijft operationeel echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.",
"registration-error": "Fout tijdens registratie", "registration-error": "Fout tijdens registratie",
"parse-error": "Tijdens het verwerken van het antwoord van de server is iets misgegaan.", "parse-error": "Tijdens het verwerken van het antwoord van de server is iets misgegaan.",

@ -50,7 +50,7 @@
"views": "Gezien", "views": "Gezien",
"reputation": "Reputatie", "reputation": "Reputatie",
"read_more": "Lees meer", "read_more": "Lees meer",
"more": "More", "more": "Meer",
"posted_ago_by_guest": "geplaatst %1 door gast", "posted_ago_by_guest": "geplaatst %1 door gast",
"posted_ago_by": "geplaatst %1 door %2", "posted_ago_by": "geplaatst %1 door %2",
"posted_ago": "geplaatst door %1", "posted_ago": "geplaatst door %1",

@ -6,12 +6,12 @@
"no_groups_found": "Geen groepen voor weergave", "no_groups_found": "Geen groepen voor weergave",
"pending.accept": "Accepteer", "pending.accept": "Accepteer",
"pending.reject": "Afwijzen", "pending.reject": "Afwijzen",
"pending.accept_all": "Accept All", "pending.accept_all": "Iedereen accepteren",
"pending.reject_all": "Reject All", "pending.reject_all": "Iedereen afwijzen",
"pending.none": "There are no pending members at this time", "pending.none": "Er zijn geen afwachtende leden op het moment",
"invited.none": "There are no invited members at this time", "invited.none": "Er zijn geen uitgenodigde leden op het moment",
"invited.uninvite": "Rescind Invitation", "invited.uninvite": "Uitnodiging intrekken",
"invited.search": "Search for a user to invite to this group", "invited.search": "Zoek naar een gebruiker om uit te nodigen voor deze groep",
"cover-instructions": "Sleep een afbeelding, sleep om te positioneren en klik tenslotte op <strong>Opslaan</strong>", "cover-instructions": "Sleep een afbeelding, sleep om te positioneren en klik tenslotte op <strong>Opslaan</strong>",
"cover-change": "Bewerken", "cover-change": "Bewerken",
"cover-save": "Opslaan", "cover-save": "Opslaan",
@ -19,22 +19,22 @@
"details.title": "Groepsdetails", "details.title": "Groepsdetails",
"details.members": "Ledenlijst", "details.members": "Ledenlijst",
"details.pending": "Nog niet geaccepteerde leden", "details.pending": "Nog niet geaccepteerde leden",
"details.invited": "Invited Members", "details.invited": "Uitgenodigde leden",
"details.has_no_posts": "Deze groepleden hebben nog geen berichten geplaatst", "details.has_no_posts": "Deze groepleden hebben nog geen berichten geplaatst",
"details.latest_posts": "Meest recente berichten", "details.latest_posts": "Meest recente berichten",
"details.private": "Prive", "details.private": "Prive",
"details.grant": "Toekennen/herroepen van eigendom", "details.grant": "Toekennen/herroepen van eigendom",
"details.kick": "Schoppen", "details.kick": "Kick",
"details.owner_options": "Groepsadministratie", "details.owner_options": "Groepsadministratie",
"details.group_name": "Groepsnaam", "details.group_name": "Groepsnaam",
"details.member_count": "Ledentelling", "details.member_count": "Ledentelling",
"details.creation_date": "Aangemaakt op", "details.creation_date": "Aangemaakt op",
"details.description": "Beschrijving", "details.description": "Beschrijving",
"details.badge_preview": "Draaginsigne voorvertoning", "details.badge_preview": "Badge Voorbeeld",
"details.change_icon": "Wijzig icoon", "details.change_icon": "Wijzig icoon",
"details.change_colour": "Wijzig kleur", "details.change_colour": "Wijzig kleur",
"details.badge_text": "Draaginsigne tekst", "details.badge_text": "Badge Tekst",
"details.userTitleEnabled": "Draaginsignes weergeven", "details.userTitleEnabled": "Badge Weergeven",
"details.private_help": "Wanneer ingeschakeld, zal eerst een groepseigenaar goedkeuring moeten verlenen voordat nieuwe leden kunnen toetreden", "details.private_help": "Wanneer ingeschakeld, zal eerst een groepseigenaar goedkeuring moeten verlenen voordat nieuwe leden kunnen toetreden",
"details.hidden": "Niet getoond", "details.hidden": "Niet getoond",
"details.hidden_help": "Indien geactiveerd zal deze groep niet getoond worden in de groepslijst en zullen gebruikers handmatig uitgenodigd moeten worden.", "details.hidden_help": "Indien geactiveerd zal deze groep niet getoond worden in de groepslijst en zullen gebruikers handmatig uitgenodigd moeten worden.",

@ -20,7 +20,7 @@
"user_posted_topic": "<strong>%1</strong> heeft een nieuw onderwerp geplaatst: <strong>%2</strong>", "user_posted_topic": "<strong>%1</strong> heeft een nieuw onderwerp geplaatst: <strong>%2</strong>",
"user_mentioned_you_in": "Onze naam is genoemd door <strong>%1</strong> in <strong>%2</strong>.", "user_mentioned_you_in": "Onze naam is genoemd door <strong>%1</strong> in <strong>%2</strong>.",
"user_started_following_you": "<strong>%1</strong> volgt ons nu.", "user_started_following_you": "<strong>%1</strong> volgt ons nu.",
"new_register": "<strong>%1</strong> sent a registration request.", "new_register": "<strong>%1</strong> heeft een registratie verzoek aangevraagd.",
"email-confirmed": "E-mailadres bevestigd", "email-confirmed": "E-mailadres bevestigd",
"email-confirmed-message": "Bedankt voor het bevestigen van het e-mailadres. Deze account is nu volledig geactiveerd.", "email-confirmed-message": "Bedankt voor het bevestigen van het e-mailadres. Deze account is nu volledig geactiveerd.",
"email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.", "email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.",

@ -4,7 +4,7 @@
"week": "Week", "week": "Week",
"month": "Maand", "month": "Maand",
"year": "Jaar", "year": "Jaar",
"alltime": "Intussen", "alltime": "altijd",
"no_recent_topics": "Er zijn geen recente reacties.", "no_recent_topics": "Er zijn geen recente reacties.",
"no_popular_topics": "Er zijn geen populaire onderwerpen.", "no_popular_topics": "Er zijn geen populaire onderwerpen.",
"there-is-a-new-topic": "Er is een nieuw onderwerp", "there-is-a-new-topic": "Er is een nieuw onderwerp",

@ -15,5 +15,5 @@
"alternative_registration": "Alternatieve Registratie", "alternative_registration": "Alternatieve Registratie",
"terms_of_use": "Gebruiksvoorwaarden", "terms_of_use": "Gebruiksvoorwaarden",
"agree_to_terms_of_use": "Ik ga akkoord van de Gebruiksvoorwaarden", "agree_to_terms_of_use": "Ik ga akkoord van de Gebruiksvoorwaarden",
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." "registration-added-to-queue": "Uw registratie is toegevoegd aan de wachtrij. U krijgt een email wanneer uw registratie is geaccepteerd "
} }

@ -9,8 +9,8 @@
"in-categories": "In categorieën", "in-categories": "In categorieën",
"search-child-categories": "Doorzoek subcategorieën ", "search-child-categories": "Doorzoek subcategorieën ",
"reply-count": "Aantal reacties", "reply-count": "Aantal reacties",
"at-least": "Minimaal", "at-least": "op zijn minst",
"at-most": "Maximaal", "at-most": "op zijn meest",
"post-time": "Geplaatst op", "post-time": "Geplaatst op",
"newer-than": "Nieuwer dan", "newer-than": "Nieuwer dan",
"older-than": "Ouder dan", "older-than": "Ouder dan",

@ -27,15 +27,15 @@
"locked": "Gesloten", "locked": "Gesloten",
"bookmark_instructions": "Klik hier om naar de vorige positie terug te keren of sluit af om te verwerpen.", "bookmark_instructions": "Klik hier om naar de vorige positie terug te keren of sluit af om te verwerpen.",
"flag_title": "Bericht aan beheerders melden", "flag_title": "Bericht aan beheerders melden",
"flag_confirm": "Is het echt de bedoeling dit bericht aan beheerders te rapporteren?", "flag_confirm": "Weet u het zeker dat u dit bericht wilt rapporteren?",
"flag_success": "Het bericht is gerapporteerd aan beheer.", "flag_success": "Het bericht is gerapporteerd aan beheer.",
"deleted_message": "Dit onderwerp is verwijderd. Alleen gebruikers met beheerrechten op onderwerpniveau kunnen dit inzien.", "deleted_message": "Dit onderwerp is verwijderd. Alleen gebruikers met beheerrechten op onderwerpniveau kunnen dit inzien.",
"following_topic.message": "Vanaf nu worden meldingen ontvangen zodra iemand een reactie op dit onderwerp geeft.", "following_topic.message": "Vanaf nu worden meldingen ontvangen zodra iemand een reactie op dit onderwerp geeft.",
"not_following_topic.message": "Er worden niet langer meldingen ontvangen over dit onderwerp.", "not_following_topic.message": "U ontvangt geen notificaties over dit onderwerp.",
"login_to_subscribe": "Aanmelden om op dit onderwerp te abonneren", "login_to_subscribe": "Log in or registreer om dit onderwerp te volgen.",
"markAsUnreadForAll.success": "Onderwerp is voor iedereen als 'gelezen' gemarkeerd.", "markAsUnreadForAll.success": "Onderwerp is voor iedereen als 'gelezen' gemarkeerd.",
"watch": "Volgen", "watch": "Volgen",
"unwatch": "Niet volgen", "unwatch": "Unfollow",
"watch.title": "Krijg meldingen van nieuwe reacties op dit onderwerp", "watch.title": "Krijg meldingen van nieuwe reacties op dit onderwerp",
"unwatch.title": "Dit onderwerp niet langer volgen", "unwatch.title": "Dit onderwerp niet langer volgen",
"share_this_post": "Deel dit bericht", "share_this_post": "Deel dit bericht",
@ -49,8 +49,8 @@
"thread_tools.move_all": "Verplaats alles", "thread_tools.move_all": "Verplaats alles",
"thread_tools.fork": "Onderwerp afsplitsen", "thread_tools.fork": "Onderwerp afsplitsen",
"thread_tools.delete": "Onderwerp verwijderen", "thread_tools.delete": "Onderwerp verwijderen",
"thread_tools.delete_confirm": "Is het echt de bedoeling dit onderwerp te verwijderen?", "thread_tools.delete_confirm": "Weet u het zeker dat u dit onderwerp wilt verwijderen?",
"thread_tools.restore": "Onderwerp erstellen", "thread_tools.restore": "Onderwerp herstellen",
"thread_tools.restore_confirm": "Zeker weten dit onderwerp te herstellen?", "thread_tools.restore_confirm": "Zeker weten dit onderwerp te herstellen?",
"thread_tools.purge": "Wis onderwerp ", "thread_tools.purge": "Wis onderwerp ",
"thread_tools.purge_confirm": "Is het echt de bedoeling dit onderwerp definitief te wissen?", "thread_tools.purge_confirm": "Is het echt de bedoeling dit onderwerp definitief te wissen?",
@ -59,7 +59,7 @@
"post_restore_confirm": "Is het de bedoeling dit bericht te herstellen?", "post_restore_confirm": "Is het de bedoeling dit bericht te herstellen?",
"post_purge_confirm": "Is het absoluut zeker dat dit bericht volledig verwijderd kan worden?", "post_purge_confirm": "Is het absoluut zeker dat dit bericht volledig verwijderd kan worden?",
"load_categories": "Categorieën laden", "load_categories": "Categorieën laden",
"disabled_categories_note": "Uitgeschakelde categorieën zijn grijs", "disabled_categories_note": "Uitgeschakelde Categorieën zijn grijs",
"confirm_move": "Verplaatsen", "confirm_move": "Verplaatsen",
"confirm_fork": "Splits", "confirm_fork": "Splits",
"favourite": "Favoriet", "favourite": "Favoriet",
@ -86,7 +86,7 @@
"composer.thumb_title": "Voeg een miniatuurweergave toe aan dit onderwerp", "composer.thumb_title": "Voeg een miniatuurweergave toe aan dit onderwerp",
"composer.thumb_url_placeholder": "http://example.com/thumb.png", "composer.thumb_url_placeholder": "http://example.com/thumb.png",
"composer.thumb_file_label": "Of upload een bestand", "composer.thumb_file_label": "Of upload een bestand",
"composer.thumb_remove": "Velden legen", "composer.thumb_remove": "Velden leegmaken",
"composer.drag_and_drop_images": "Sleep en zet afbeeldingen hier", "composer.drag_and_drop_images": "Sleep en zet afbeeldingen hier",
"more_users_and_guests": "%1 of meerdere gebruiker(s) en %2 gast(en)", "more_users_and_guests": "%1 of meerdere gebruiker(s) en %2 gast(en)",
"more_users": "%1 meer gebruiker(s)", "more_users": "%1 meer gebruiker(s)",

@ -5,6 +5,6 @@
"mark_as_read": "Markeer als gelezen", "mark_as_read": "Markeer als gelezen",
"selected": "Geselecteerd", "selected": "Geselecteerd",
"all": "Alles", "all": "Alles",
"all_categories": "All categories", "all_categories": "Alle categorieën",
"topics_marked_as_read.success": "Onderwerp gemarkeerd als gelezen!" "topics_marked_as_read.success": "Onderwerp gemarkeerd als gelezen!"
} }

@ -6,12 +6,12 @@
"postcount": "Aantal geplaatste berichten", "postcount": "Aantal geplaatste berichten",
"email": "E-mail", "email": "E-mail",
"confirm_email": "Bevestig e-mail", "confirm_email": "Bevestig e-mail",
"ban_account": "Ban Account", "ban_account": "Verban Account",
"ban_account_confirm": "Do you really want to ban this user?", "ban_account_confirm": "Weet u zeker dat u deze gebruiker wilt verbannen",
"unban_account": "Unban Account", "unban_account": "Unban Account",
"delete_account": "Account verwijderen", "delete_account": "Account verwijderen",
"delete_account_confirm": "Controleer of dat het zeker is dat deze account verwijderd moet worden. <br /><strong> Deze actie kan niet ongedaan gemaakt worden en herstellen van gebruiker- of profielgegevens is niet mogelijk</strong><br /><br /> Typ hier de gebruikersnaam als extra controle om te bevestigen dat deze account verwijderd moet worden.", "delete_account_confirm": "Controleer of dat het zeker is dat deze account verwijderd moet worden. <br /><strong> Deze actie kan niet ongedaan gemaakt worden en herstellen van gebruiker- of profielgegevens is niet mogelijk</strong><br /><br /> Typ hier de gebruikersnaam als extra controle om te bevestigen dat deze account verwijderd moet worden.",
"delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />", "delete_this_account_confirm": "Weet u zeker dat u deze account wilt verwijderen? <br /><strong>Deze actie kan niet ongedaan worden en u kunt niet de informatie herstellen</strong><br /><br />",
"fullname": "Volledige naam", "fullname": "Volledige naam",
"website": "Website", "website": "Website",
"location": "Locatie", "location": "Locatie",
@ -68,9 +68,9 @@
"settings-require-reload": "Sommige veranderingen vereisen het herladen van de pagina: klik hier om de pagina te herladen.", "settings-require-reload": "Sommige veranderingen vereisen het herladen van de pagina: klik hier om de pagina te herladen.",
"has_no_follower": "Deze gebruiker heeft geen volgers :(", "has_no_follower": "Deze gebruiker heeft geen volgers :(",
"follows_no_one": "Deze gebruiker volgt niemand :(", "follows_no_one": "Deze gebruiker volgt niemand :(",
"has_no_posts": "This user hasn't posted anything yet.", "has_no_posts": "Deze gebruiker heeft nog geen berichten geplaatst",
"has_no_topics": "This user hasn't posted any topics yet.", "has_no_topics": "Deze gebruiker heeft nog geen onderwerpen gestart.",
"has_no_watched_topics": "This user hasn't watched any topics yet.", "has_no_watched_topics": "Deze gebruiker heeft nog geen onderwerpen gevolgd.",
"email_hidden": "E-mail niet beschikbaar", "email_hidden": "E-mail niet beschikbaar",
"hidden": "verborgen", "hidden": "verborgen",
"paginate_description": "Blader door onderwerpen en berichten in plaats van oneindig scrollen.", "paginate_description": "Blader door onderwerpen en berichten in plaats van oneindig scrollen.",

@ -9,13 +9,13 @@
"filter-by": "Filter op", "filter-by": "Filter op",
"online-only": "Online ", "online-only": "Online ",
"picture-only": "Alleen een afbeelding", "picture-only": "Alleen een afbeelding",
"invite": "Invite", "invite": "Uitnodigen",
"invitation-email-sent": "An invitation email has been sent to %1", "invitation-email-sent": "Een uitnodiging email is verstuurd naar %1 ",
"user_list": "User List", "user_list": "Ledenlijst",
"recent_topics": "Recent Topics", "recent_topics": "Recente onderwerpen",
"popular_topics": "Popular Topics", "popular_topics": "Populaire onderwerpen",
"unread_topics": "Unread Topics", "unread_topics": "Ongelezen onderwerpen",
"categories": "Categories", "categories": "Categorieën",
"tags": "Tags", "tags": "Tags",
"map": "Map" "map": "Map"
} }

@ -5,8 +5,8 @@
"browsing": "正在浏览", "browsing": "正在浏览",
"no_replies": "尚无回复", "no_replies": "尚无回复",
"share_this_category": "分享此版块", "share_this_category": "分享此版块",
"watch": "订阅", "watch": "关注",
"ignore": "忽略", "ignore": "忽略",
"watch.message": "您现在已经订阅了此版块", "watch.message": "您现在已经关注了此版块",
"ignore.message": "您现在已经取消订阅了此版块" "ignore.message": "您现在已经取消了此版块的关注"
} }

@ -9,13 +9,13 @@
"filter-by": "过滤选项", "filter-by": "过滤选项",
"online-only": "只看在线", "online-only": "只看在线",
"picture-only": "只看图片", "picture-only": "只看图片",
"invite": "Invite", "invite": "邀请注册",
"invitation-email-sent": "An invitation email has been sent to %1", "invitation-email-sent": "已发送邀请给 %1",
"user_list": "User List", "user_list": "会员列表",
"recent_topics": "Recent Topics", "recent_topics": "最新主题",
"popular_topics": "Popular Topics", "popular_topics": "热门主题",
"unread_topics": "Unread Topics", "unread_topics": "未读主题",
"categories": "Categories", "categories": "版面",
"tags": "Tags", "tags": "话题",
"map": "Map" "map": "地图"
} }

@ -1,312 +1,312 @@
"use strict"; "use strict";
var ajaxify = ajaxify || {}; var ajaxify = ajaxify || {};
$(document).ready(function() { $(document).ready(function() {
/*global app, templates, utils, socket, config, RELATIVE_PATH*/ /*global app, templates, utils, socket, config, RELATIVE_PATH*/
var location = document.location || window.location, var location = document.location || window.location,
rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''), rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''),
apiXHR = null, apiXHR = null,
translator; translator;
// Dumb hack to fool ajaxify into thinking translator is still a global // Dumb hack to fool ajaxify into thinking translator is still a global
// When ajaxify is migrated to a require.js module, then this can be merged into the "define" call // When ajaxify is migrated to a require.js module, then this can be merged into the "define" call
require(['translator'], function(_translator) { require(['translator'], function(_translator) {
translator = _translator; translator = _translator;
}); });
$(window).on('popstate', function (ev) { $(window).on('popstate', function (ev) {
ev = ev.originalEvent; ev = ev.originalEvent;
if (ev !== null && ev.state && ev.state.url !== undefined) { if (ev !== null && ev.state && ev.state.url !== undefined) {
ajaxify.go(ev.state.url, function() { ajaxify.go(ev.state.url, function() {
$(window).trigger('action:popstate', {url: ev.state.url}); $(window).trigger('action:popstate', {url: ev.state.url});
}, true); }, true);
} }
}); });
ajaxify.currentPage = null; ajaxify.currentPage = null;
ajaxify.go = function (url, callback, quiet, search) { ajaxify.go = function (url, callback, quiet, search) {
if (!socket.connected) { if (!socket.connected) {
if (ajaxify.reconnectAction) { if (ajaxify.reconnectAction) {
$(window).off('action:reconnected', ajaxify.reconnectAction); $(window).off('action:reconnected', ajaxify.reconnectAction);
} }
ajaxify.reconnectAction = function(e) { ajaxify.reconnectAction = function(e) {
ajaxify.go(url, callback, quiet, search); ajaxify.go(url, callback, quiet, search);
$(window).off(e); $(window).off(e);
} }
$(window).on('action:reconnected', ajaxify.reconnectAction); $(window).on('action:reconnected', ajaxify.reconnectAction);
} }
if (ajaxify.handleRedirects(url)) { if (ajaxify.handleRedirects(url)) {
return true; return true;
} }
app.enterRoom(''); app.enterRoom('');
$(window).off('scroll'); $(window).off('scroll');
if ($('#content').hasClass('ajaxifying') && apiXHR) { if ($('#content').hasClass('ajaxifying') && apiXHR) {
apiXHR.abort(); apiXHR.abort();
} }
url = ajaxify.start(url, quiet, search); url = ajaxify.start(url, quiet, search);
$('#footer, #content').removeClass('hide').addClass('ajaxifying'); $('#footer, #content').removeClass('hide').addClass('ajaxifying');
ajaxify.variables.flush(); ajaxify.variables.flush();
ajaxify.loadData(url, function(err, data) { ajaxify.loadData(url, function(err, data) {
if (err) { if (err) {
return onAjaxError(err, url, callback, quiet); return onAjaxError(err, url, callback, quiet);
} }
app.template = data.template.name; app.template = data.template.name;
require(['translator'], function(translator) { require(['translator'], function(translator) {
translator.load(config.defaultLang, data.template.name); translator.load(config.defaultLang, data.template.name);
renderTemplate(url, data.template.name, data, callback); renderTemplate(url, data.template.name, data, callback);
}); });
}); });
return true; return true;
}; };
ajaxify.handleRedirects = function(url) { ajaxify.handleRedirects = function(url) {
url = ajaxify.removeRelativePath(url.replace(/\/$/, '')).toLowerCase(); url = ajaxify.removeRelativePath(url.replace(/\/$/, '')).toLowerCase();
var isAdminRoute = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0; var isAdminRoute = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0;
var uploadsOrApi = url.startsWith('uploads') || url.startsWith('api'); var uploadsOrApi = url.startsWith('uploads') || url.startsWith('api');
if (isAdminRoute || uploadsOrApi) { if (isAdminRoute || uploadsOrApi) {
window.open(RELATIVE_PATH + '/' + url, '_top'); window.open(RELATIVE_PATH + '/' + url, '_top');
return true; return true;
} }
return false; return false;
}; };
ajaxify.start = function(url, quiet, search) { ajaxify.start = function(url, quiet, search) {
url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, '')); url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, ''));
var hash = window.location.hash; var hash = window.location.hash;
search = search || ''; search = search || '';
$(window).trigger('action:ajaxify.start', {url: url}); $(window).trigger('action:ajaxify.start', {url: url});
if (!window.location.pathname.match(/\/(403|404)$/g)) { if (!window.location.pathname.match(/\/(403|404)$/g)) {
app.previousUrl = window.location.href; app.previousUrl = window.location.href;
} }
ajaxify.currentPage = url; ajaxify.currentPage = url;
if (window.history && window.history.pushState) { if (window.history && window.history.pushState) {
window.history[!quiet ? 'pushState' : 'replaceState']({ window.history[!quiet ? 'pushState' : 'replaceState']({
url: url + search + hash url: url + search + hash
}, url, RELATIVE_PATH + '/' + url + search + hash); }, url, RELATIVE_PATH + '/' + url + search + hash);
} }
return url; return url;
}; };
function onAjaxError(err, url, callback, quiet) { function onAjaxError(err, url, callback, quiet) {
var data = err.data, var data = err.data,
textStatus = err.textStatus; textStatus = err.textStatus;
if (data) { if (data) {
var status = parseInt(data.status, 10); var status = parseInt(data.status, 10);
if (status === 403 || status === 404 || status === 500 || status === 502) { if (status === 403 || status === 404 || status === 500 || status === 502) {
if (status === 502) { if (status === 502) {
status = 500; status = 500;
} }
$('#footer, #content').removeClass('hide').addClass('ajaxifying'); $('#footer, #content').removeClass('hide').addClass('ajaxifying');
return renderTemplate(url, status.toString(), data.responseJSON, (new Date()).getTime(), callback); return renderTemplate(url, status.toString(), data.responseJSON, (new Date()).getTime(), callback);
} else if (status === 401) { } else if (status === 401) {
app.alertError('[[global:please_log_in]]'); app.alertError('[[global:please_log_in]]');
app.previousUrl = url; app.previousUrl = url;
return ajaxify.go('login'); return ajaxify.go('login');
} else if (status === 302) { } else if (status === 302) {
if (data.responseJSON.external) { if (data.responseJSON.external) {
window.location.href = data.responseJSON.external; window.location.href = data.responseJSON.external;
} else if (typeof data.responseJSON === 'string') { } else if (typeof data.responseJSON === 'string') {
ajaxify.go(data.responseJSON.slice(1), callback, quiet); ajaxify.go(data.responseJSON.slice(1), callback, quiet);
} }
} }
} else if (textStatus !== 'abort') { } else if (textStatus !== 'abort') {
app.alertError(data.responseJSON.error); app.alertError(data.responseJSON.error);
} }
} }
function renderTemplate(url, tpl_url, data, callback) { function renderTemplate(url, tpl_url, data, callback) {
$(window).trigger('action:ajaxify.loadingTemplates', {}); $(window).trigger('action:ajaxify.loadingTemplates', {});
templates.parse(tpl_url, data, function(template) { templates.parse(tpl_url, data, function(template) {
translator.translate(template, function(translatedTemplate) { translator.translate(template, function(translatedTemplate) {
$('#content').html(translatedTemplate); $('#content').html(translatedTemplate);
ajaxify.end(url, tpl_url); ajaxify.end(url, tpl_url);
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(); callback();
} }
$('#content, #footer').removeClass('ajaxifying'); $('#content, #footer').removeClass('ajaxifying');
app.refreshTitle(url); app.refreshTitle(url);
}); });
}); });
} }
ajaxify.end = function(url, tpl_url) { ajaxify.end = function(url, tpl_url) {
function done() { function done() {
if (--count === 0) { if (--count === 0) {
$(window).trigger('action:ajaxify.end', {url: url}); $(window).trigger('action:ajaxify.end', {url: url});
} }
} }
var count = 2; var count = 2;
ajaxify.variables.parse(); ajaxify.variables.parse();
ajaxify.loadScript(tpl_url, done); ajaxify.loadScript(tpl_url, done);
ajaxify.widgets.render(tpl_url, url, done); ajaxify.widgets.render(tpl_url, url, done);
$(window).trigger('action:ajaxify.contentLoaded', {url: url, tpl: tpl_url}); $(window).trigger('action:ajaxify.contentLoaded', {url: url, tpl: tpl_url});
app.processPage(); app.processPage();
}; };
ajaxify.removeRelativePath = function(url) { ajaxify.removeRelativePath = function(url) {
if (url.startsWith(RELATIVE_PATH.slice(1))) { if (url.startsWith(RELATIVE_PATH.slice(1))) {
url = url.slice(RELATIVE_PATH.length); url = url.slice(RELATIVE_PATH.length);
} }
return url; return url;
}; };
ajaxify.refresh = function(e) { ajaxify.refresh = function(e) {
if (e && e instanceof jQuery.Event) { if (e && e instanceof jQuery.Event) {
e.preventDefault(); e.preventDefault();
} }
ajaxify.go(ajaxify.currentPage, null, true); ajaxify.go(ajaxify.currentPage, null, true);
}; };
ajaxify.loadScript = function(tpl_url, callback) { ajaxify.loadScript = function(tpl_url, callback) {
var location = !app.inAdmin ? 'forum/' : ''; var location = !app.inAdmin ? 'forum/' : '';
require([location + tpl_url], function(script) { require([location + tpl_url], function(script) {
if (script && script.init) { if (script && script.init) {
script.init(); script.init();
} }
if (callback) { if (callback) {
callback(); callback();
} }
}); });
}; };
ajaxify.loadData = function(url, callback) { ajaxify.loadData = function(url, callback) {
url = ajaxify.removeRelativePath(url); url = ajaxify.removeRelativePath(url);
$(window).trigger('action:ajaxify.loadingData', {url: url}); $(window).trigger('action:ajaxify.loadingData', {url: url});
apiXHR = $.ajax({ apiXHR = $.ajax({
url: RELATIVE_PATH + '/api/' + url, url: RELATIVE_PATH + '/api/' + url,
cache: false, cache: false,
success: function(data) { success: function(data) {
if (!data) { if (!data) {
return; return;
} }
ajaxify.data = data; ajaxify.data = data;
data.relative_path = RELATIVE_PATH; data.relative_path = RELATIVE_PATH;
$(window).trigger('action:ajaxify.dataLoaded', {url: url, data: data}); $(window).trigger('action:ajaxify.dataLoaded', {url: url, data: data});
if (callback) { if (callback) {
callback(null, data); callback(null, data);
} }
}, },
error: function(data, textStatus) { error: function(data, textStatus) {
if (data.status === 0 && textStatus === 'error') { if (data.status === 0 && textStatus === 'error') {
data.status = 500; data.status = 500;
} }
callback({ callback({
data: data, data: data,
textStatus: textStatus textStatus: textStatus
}); });
} }
}); });
}; };
ajaxify.loadTemplate = function(template, callback) { ajaxify.loadTemplate = function(template, callback) {
if (templates.cache[template]) { if (templates.cache[template]) {
callback(templates.cache[template]); callback(templates.cache[template]);
} else { } else {
$.ajax({ $.ajax({
url: RELATIVE_PATH + '/templates/' + template + '.tpl' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), url: RELATIVE_PATH + '/templates/' + template + '.tpl' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''),
type: 'GET', type: 'GET',
success: function(data) { success: function(data) {
callback(data.toString()); callback(data.toString());
}, },
error: function(error) { error: function(error) {
throw new Error("Unable to load template: " + template + " (" + error.statusText + ")"); throw new Error("Unable to load template: " + template + " (" + error.statusText + ")");
} }
}); });
} }
}; };
function ajaxifyAnchors() { function ajaxifyAnchors() {
templates.registerLoader(ajaxify.loadTemplate); templates.registerLoader(ajaxify.loadTemplate);
function hrefEmpty(href) { function hrefEmpty(href) {
return href === undefined || href === '' || href === 'javascript:;'; return href === undefined || href === '' || href === 'javascript:;';
} }
// Enhancing all anchors to ajaxify... // Enhancing all anchors to ajaxify...
$(document.body).on('click', 'a', function (e) { $(document.body).on('click', 'a', function (e) {
if (this.target !== '' || (this.protocol !== 'http:' && this.protocol !== 'https:')) { if (this.target !== '' || (this.protocol !== 'http:' && this.protocol !== 'https:')) {
return; return;
} else if (hrefEmpty(this.href) || this.protocol === 'javascript:' || $(this).attr('data-ajaxify') === 'false' || $(this).attr('href') === '#') { } else if (hrefEmpty(this.href) || this.protocol === 'javascript:' || $(this).attr('data-ajaxify') === 'false' || $(this).attr('href') === '#') {
return e.preventDefault(); return e.preventDefault();
} }
if (!e.ctrlKey && !e.shiftKey && !e.metaKey && e.which === 1) { if (!e.ctrlKey && !e.shiftKey && !e.metaKey && e.which === 1) {
if ( if (
this.host === '' || // Relative paths are always internal links... this.host === '' || // Relative paths are always internal links...
(this.host === window.location.host && this.protocol === window.location.protocol && // Otherwise need to check that protocol and host match (this.host === window.location.host && this.protocol === window.location.protocol && // Otherwise need to check that protocol and host match
(RELATIVE_PATH.length > 0 ? this.pathname.indexOf(RELATIVE_PATH) === 0 : true)) // Subfolder installs need this additional check (RELATIVE_PATH.length > 0 ? this.pathname.indexOf(RELATIVE_PATH) === 0 : true)) // Subfolder installs need this additional check
) { ) {
// Internal link // Internal link
var url = this.pathname.replace(RELATIVE_PATH + '/', ''); var url = this.pathname.replace(RELATIVE_PATH + '/', '');
// Special handling for urls with hashes // Special handling for urls with hashes
if (window.location.pathname === this.pathname && this.hash.length) { if (window.location.pathname === this.pathname && this.hash.length) {
window.location.hash = this.hash; window.location.hash = this.hash;
} else { } else {
window.location.hash = ''; window.location.hash = '';
if (ajaxify.go(url)) { if (ajaxify.go(url)) {
e.preventDefault(); e.preventDefault();
} }
} }
} else if (window.location.pathname !== '/outgoing') { } else if (window.location.pathname !== '/outgoing') {
// External Link // External Link
if (config.openOutgoingLinksInNewTab) { if (config.openOutgoingLinksInNewTab) {
window.open(this.href, '_blank'); window.open(this.href, '_blank');
e.preventDefault(); e.preventDefault();
} else if (config.useOutgoingLinksPage) { } else if (config.useOutgoingLinksPage) {
ajaxify.go('outgoing?url=' + encodeURIComponent(this.href)); ajaxify.go('outgoing?url=' + encodeURIComponent(this.href));
e.preventDefault(); e.preventDefault();
} }
} }
} }
}); });
} }
if (window.history && window.history.pushState) { if (window.history && window.history.pushState) {
// Progressive Enhancement, ajaxify available only to modern browsers // Progressive Enhancement, ajaxify available only to modern browsers
ajaxifyAnchors(); ajaxifyAnchors();
} }
app.load(); app.load();
templates.cache['500'] = $('.tpl-500').html(); templates.cache['500'] = $('.tpl-500').html();
}); });

File diff suppressed because it is too large Load Diff

@ -1,29 +1,29 @@
*.pyc *.pyc
*.egg-info *.egg-info
*.db *.db
*.db.old *.db.old
*.swp *.swp
*.db-journal *.db-journal
.coverage .coverage
.DS_Store .DS_Store
.installed.cfg .installed.cfg
.idea/* .idea/*
.svn/* .svn/*
src/website/static/* src/website/static/*
src/website/media/* src/website/media/*
bin bin
build build
cfcache cfcache
develop-eggs develop-eggs
dist dist
downloads downloads
eggs eggs
parts parts
tmp tmp
.sass-cache .sass-cache
src/website/settingslocal.py src/website/settingslocal.py
stunnel.log stunnel.log

@ -1,22 +1,22 @@

// Persian // Persian
// Use DIR attribute for RTL text in Persian Language for ABBR tag . // Use DIR attribute for RTL text in Persian Language for ABBR tag .
// By MB.seifollahi@gmail.com // By MB.seifollahi@gmail.com
jQuery.timeago.settings.strings = { jQuery.timeago.settings.strings = {
prefixAgo: null, prefixAgo: null,
prefixFromNow: null, prefixFromNow: null,
suffixAgo: "پیش", suffixAgo: "پیش",
suffixFromNow: "از حال", suffixFromNow: "از حال",
seconds: "کمتر از یک دقیقه", seconds: "کمتر از یک دقیقه",
minute: "حدود یک دقیقه", minute: "حدود یک دقیقه",
minutes: "%d دقیقه", minutes: "%d دقیقه",
hour: "حدود یک ساعت", hour: "حدود یک ساعت",
hours: "حدود %d ساعت", hours: "حدود %d ساعت",
day: "یک روز", day: "یک روز",
days: "%d روز", days: "%d روز",
month: "حدود یک ماه", month: "حدود یک ماه",
months: "%d ماه", months: "%d ماه",
year: "حدود یک سال", year: "حدود یک سال",
years: "%d سال", years: "%d سال",
wordSeparator: " " wordSeparator: " "
}; };

@ -1,19 +1,19 @@
//Uzbek //Uzbek
jQuery.timeago.settings.strings = { jQuery.timeago.settings.strings = {
prefixAgo: null, prefixAgo: null,
prefixFromNow: "keyin", prefixFromNow: "keyin",
suffixAgo: "avval", suffixAgo: "avval",
suffixFromNow: null, suffixFromNow: null,
seconds: "bir necha soniya", seconds: "bir necha soniya",
minute: "1 daqiqa", minute: "1 daqiqa",
minutes: function(value) { return "%d daqiqa" }, minutes: function(value) { return "%d daqiqa" },
hour: "1 soat", hour: "1 soat",
hours: function(value) { return "%d soat" }, hours: function(value) { return "%d soat" },
day: "1 kun", day: "1 kun",
days: function(value) { return "%d kun" }, days: function(value) { return "%d kun" },
month: "1 oy", month: "1 oy",
months: function(value) { return "%d oy" }, months: function(value) { return "%d oy" },
year: "1 yil", year: "1 yil",
years: function(value) { return "%d yil" }, years: function(value) { return "%d yil" },
wordSeparator: " " wordSeparator: " "
}; };

@ -1,85 +1,85 @@
"use strict"; "use strict";
var express = require('express'); var express = require('express');
function apiRoutes(router, middleware, controllers) { function apiRoutes(router, middleware, controllers) {
router.get('/users/csv', middleware.authenticate, controllers.admin.users.getCSV); router.get('/users/csv', middleware.authenticate, controllers.admin.users.getCSV);
var multipart = require('connect-multiparty'); var multipart = require('connect-multiparty');
var multipartMiddleware = multipart(); var multipartMiddleware = multipart();
var middlewares = [multipartMiddleware, middleware.validateFiles, middleware.applyCSRF, middleware.authenticate]; var middlewares = [multipartMiddleware, middleware.validateFiles, middleware.applyCSRF, middleware.authenticate];
router.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture); router.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture);
router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon); router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon);
router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo); router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
router.post('/uploadgravatardefault', middlewares, controllers.admin.uploads.uploadGravatarDefault); router.post('/uploadgravatardefault', middlewares, controllers.admin.uploads.uploadGravatarDefault);
} }
function adminRouter(middleware, controllers) { function adminRouter(middleware, controllers) {
var router = express.Router(); var router = express.Router();
router.use(middleware.admin.buildHeader); router.use(middleware.admin.buildHeader);
addRoutes(router, middleware, controllers); addRoutes(router, middleware, controllers);
return router; return router;
} }
function apiRouter(middleware, controllers) { function apiRouter(middleware, controllers) {
var router = express.Router(); var router = express.Router();
addRoutes(router, middleware, controllers); addRoutes(router, middleware, controllers);
apiRoutes(router, middleware, controllers); apiRoutes(router, middleware, controllers);
return router; return router;
} }
function addRoutes(router, middleware, controllers) { function addRoutes(router, middleware, controllers) {
router.get('/', controllers.admin.home); router.get('/', controllers.admin.home);
router.get('/general/dashboard', controllers.admin.home); router.get('/general/dashboard', controllers.admin.home);
router.get('/general/languages', controllers.admin.languages.get); router.get('/general/languages', controllers.admin.languages.get);
router.get('/general/sounds', controllers.admin.sounds.get); router.get('/general/sounds', controllers.admin.sounds.get);
router.get('/general/navigation', controllers.admin.navigation.get); router.get('/general/navigation', controllers.admin.navigation.get);
router.get('/general/homepage', controllers.admin.homepage.get); router.get('/general/homepage', controllers.admin.homepage.get);
router.get('/manage/categories', controllers.admin.categories.getAll); router.get('/manage/categories', controllers.admin.categories.getAll);
router.get('/manage/categories/:category_id', controllers.admin.categories.get); router.get('/manage/categories/:category_id', controllers.admin.categories.get);
router.get('/manage/tags', controllers.admin.tags.get); router.get('/manage/tags', controllers.admin.tags.get);
router.get('/manage/flags', controllers.admin.flags.get); router.get('/manage/flags', controllers.admin.flags.get);
router.get('/manage/users', controllers.admin.users.sortByJoinDate); router.get('/manage/users', controllers.admin.users.sortByJoinDate);
router.get('/manage/users/search', controllers.admin.users.search); router.get('/manage/users/search', controllers.admin.users.search);
router.get('/manage/users/latest', controllers.admin.users.sortByJoinDate); router.get('/manage/users/latest', controllers.admin.users.sortByJoinDate);
router.get('/manage/users/sort-posts', controllers.admin.users.sortByPosts); router.get('/manage/users/sort-posts', controllers.admin.users.sortByPosts);
router.get('/manage/users/sort-reputation', controllers.admin.users.sortByReputation); router.get('/manage/users/sort-reputation', controllers.admin.users.sortByReputation);
router.get('/manage/users/banned', controllers.admin.users.banned); router.get('/manage/users/banned', controllers.admin.users.banned);
router.get('/manage/users/registration', controllers.admin.users.registrationQueue); router.get('/manage/users/registration', controllers.admin.users.registrationQueue);
router.get('/manage/groups', controllers.admin.groups.list); router.get('/manage/groups', controllers.admin.groups.list);
router.get('/manage/groups/:name', controllers.admin.groups.get); router.get('/manage/groups/:name', controllers.admin.groups.get);
router.get('/settings/:term?', controllers.admin.settings.get); router.get('/settings/:term?', controllers.admin.settings.get);
router.get('/appearance/:term?', controllers.admin.appearance.get); router.get('/appearance/:term?', controllers.admin.appearance.get);
router.get('/extend/plugins', controllers.admin.plugins.get); router.get('/extend/plugins', controllers.admin.plugins.get);
router.get('/extend/widgets', controllers.admin.extend.widgets); router.get('/extend/widgets', controllers.admin.extend.widgets);
router.get('/extend/rewards', controllers.admin.extend.rewards); router.get('/extend/rewards', controllers.admin.extend.rewards);
router.get('/advanced/database', controllers.admin.database.get); router.get('/advanced/database', controllers.admin.database.get);
router.get('/advanced/events', controllers.admin.events.get); router.get('/advanced/events', controllers.admin.events.get);
router.get('/advanced/logs', controllers.admin.logs.get); router.get('/advanced/logs', controllers.admin.logs.get);
router.get('/advanced/post-cache', controllers.admin.postCache.get); router.get('/advanced/post-cache', controllers.admin.postCache.get);
router.get('/development/logger', controllers.admin.logger.get); router.get('/development/logger', controllers.admin.logger.get);
} }
module.exports = function(app, middleware, controllers) { module.exports = function(app, middleware, controllers) {
app.use('/admin/', adminRouter(middleware, controllers)); app.use('/admin/', adminRouter(middleware, controllers));
app.use('/api/admin/', apiRouter(middleware, controllers)); app.use('/api/admin/', apiRouter(middleware, controllers));
}; };

@ -1,86 +1,86 @@
(function(Auth) { (function(Auth) {
"use strict"; "use strict";
var passport = require('passport'), var passport = require('passport'),
passportLocal = require('passport-local').Strategy, passportLocal = require('passport-local').Strategy,
nconf = require('nconf'), nconf = require('nconf'),
winston = require('winston'), winston = require('winston'),
express = require('express'), express = require('express'),
controllers = require('../controllers'), controllers = require('../controllers'),
plugins = require('../plugins'), plugins = require('../plugins'),
hotswap = require('../hotswap'), hotswap = require('../hotswap'),
loginStrategies = []; loginStrategies = [];
Auth.initialize = function(app, middleware) { Auth.initialize = function(app, middleware) {
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
app.use(function(req, res, next) { app.use(function(req, res, next) {
req.uid = req.user ? parseInt(req.user.uid, 10) : 0; req.uid = req.user ? parseInt(req.user.uid, 10) : 0;
next(); next();
}); });
Auth.app = app; Auth.app = app;
Auth.middleware = middleware; Auth.middleware = middleware;
}; };
Auth.getLoginStrategies = function() { Auth.getLoginStrategies = function() {
return loginStrategies; return loginStrategies;
}; };
Auth.reloadRoutes = function(callback) { Auth.reloadRoutes = function(callback) {
var router = express.Router(); var router = express.Router();
router.hotswapId = 'auth'; router.hotswapId = 'auth';
loginStrategies.length = 0; loginStrategies.length = 0;
if (plugins.hasListeners('action:auth.overrideLogin')) { if (plugins.hasListeners('action:auth.overrideLogin')) {
winston.warn('[authentication] Login override detected, skipping local login strategy.'); winston.warn('[authentication] Login override detected, skipping local login strategy.');
plugins.fireHook('action:auth.overrideLogin'); plugins.fireHook('action:auth.overrideLogin');
} else { } else {
passport.use(new passportLocal({passReqToCallback: true}, controllers.authentication.localLogin)); passport.use(new passportLocal({passReqToCallback: true}, controllers.authentication.localLogin));
} }
plugins.fireHook('filter:auth.init', loginStrategies, function(err) { plugins.fireHook('filter:auth.init', loginStrategies, function(err) {
if (err) { if (err) {
winston.error('filter:auth.init - plugin failure'); winston.error('filter:auth.init - plugin failure');
return callback(err); return callback(err);
} }
loginStrategies.forEach(function(strategy) { loginStrategies.forEach(function(strategy) {
if (strategy.url) { if (strategy.url) {
router.get(strategy.url, passport.authenticate(strategy.name, { router.get(strategy.url, passport.authenticate(strategy.name, {
scope: strategy.scope scope: strategy.scope
})); }));
} }
router.get(strategy.callbackURL, passport.authenticate(strategy.name, { router.get(strategy.callbackURL, passport.authenticate(strategy.name, {
successReturnToOrRedirect: nconf.get('relative_path') + (strategy.successUrl !== undefined ? strategy.successUrl : '/'), successReturnToOrRedirect: nconf.get('relative_path') + (strategy.successUrl !== undefined ? strategy.successUrl : '/'),
failureRedirect: nconf.get('relative_path') + (strategy.failureUrl !== undefined ? strategy.failureUrl : '/login') failureRedirect: nconf.get('relative_path') + (strategy.failureUrl !== undefined ? strategy.failureUrl : '/login')
})); }));
}); });
router.post('/register', Auth.middleware.applyCSRF, controllers.authentication.register); router.post('/register', Auth.middleware.applyCSRF, controllers.authentication.register);
router.post('/login', Auth.middleware.applyCSRF, controllers.authentication.login); router.post('/login', Auth.middleware.applyCSRF, controllers.authentication.login);
router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout); router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout);
hotswap.replace('auth', router); hotswap.replace('auth', router);
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(); callback();
} }
}); });
}; };
passport.serializeUser(function(user, done) { passport.serializeUser(function(user, done) {
done(null, user.uid); done(null, user.uid);
}); });
passport.deserializeUser(function(uid, done) { passport.deserializeUser(function(uid, done) {
done(null, { done(null, {
uid: uid uid: uid
}); });
}); });
}(exports)); }(exports));

@ -1,379 +1,379 @@
"use strict"; "use strict";
var async = require('async'), var async = require('async'),
validator = require('validator'), validator = require('validator'),
_ = require('underscore'), _ = require('underscore'),
db = require('./database'), db = require('./database'),
posts = require('./posts'), posts = require('./posts'),
utils = require('../public/src/utils'), utils = require('../public/src/utils'),
plugins = require('./plugins'), plugins = require('./plugins'),
user = require('./user'), user = require('./user'),
categories = require('./categories'), categories = require('./categories'),
privileges = require('./privileges'); privileges = require('./privileges');
(function(Topics) { (function(Topics) {
require('./topics/create')(Topics); require('./topics/create')(Topics);
require('./topics/delete')(Topics); require('./topics/delete')(Topics);
require('./topics/unread')(Topics); require('./topics/unread')(Topics);
require('./topics/recent')(Topics); require('./topics/recent')(Topics);
require('./topics/popular')(Topics); require('./topics/popular')(Topics);
require('./topics/user')(Topics); require('./topics/user')(Topics);
require('./topics/fork')(Topics); require('./topics/fork')(Topics);
require('./topics/posts')(Topics); require('./topics/posts')(Topics);
require('./topics/follow')(Topics); require('./topics/follow')(Topics);
require('./topics/tags')(Topics); require('./topics/tags')(Topics);
require('./topics/teaser')(Topics); require('./topics/teaser')(Topics);
require('./topics/suggested')(Topics); require('./topics/suggested')(Topics);
Topics.exists = function(tid, callback) { Topics.exists = function(tid, callback) {
db.isSortedSetMember('topics:tid', tid, callback); db.isSortedSetMember('topics:tid', tid, callback);
}; };
Topics.getTopicData = function(tid, callback) { Topics.getTopicData = function(tid, callback) {
db.getObject('topic:' + tid, function(err, topic) { db.getObject('topic:' + tid, function(err, topic) {
if (err || !topic) { if (err || !topic) {
return callback(err); return callback(err);
} }
modifyTopic(topic, callback); modifyTopic(topic, callback);
}); });
}; };
Topics.getTopicsData = function(tids, callback) { Topics.getTopicsData = function(tids, callback) {
var keys = []; var keys = [];
for (var i=0; i<tids.length; ++i) { for (var i=0; i<tids.length; ++i) {
keys.push('topic:' + tids[i]); keys.push('topic:' + tids[i]);
} }
db.getObjects(keys, function(err, topics) { db.getObjects(keys, function(err, topics) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
async.map(topics, modifyTopic, callback); async.map(topics, modifyTopic, callback);
}); });
}; };
function modifyTopic(topic, callback) { function modifyTopic(topic, callback) {
if (!topic) { if (!topic) {
return callback(null, topic); return callback(null, topic);
} }
topic.title = validator.escape(topic.title); topic.title = validator.escape(topic.title);
topic.relativeTime = utils.toISOString(topic.timestamp); topic.relativeTime = utils.toISOString(topic.timestamp);
topic.lastposttimeISO = utils.toISOString(topic.lastposttime); topic.lastposttimeISO = utils.toISOString(topic.lastposttime);
callback(null, topic); callback(null, topic);
} }
Topics.getPageCount = function(tid, uid, callback) { Topics.getPageCount = function(tid, uid, callback) {
Topics.getTopicField(tid, 'postcount', function(err, postCount) { Topics.getTopicField(tid, 'postcount', function(err, postCount) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
if (!parseInt(postCount, 10)) { if (!parseInt(postCount, 10)) {
return callback(null, 1); return callback(null, 1);
} }
user.getSettings(uid, function(err, settings) { user.getSettings(uid, function(err, settings) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
callback(null, Math.ceil((parseInt(postCount, 10) - 1) / settings.postsPerPage)); callback(null, Math.ceil((parseInt(postCount, 10) - 1) / settings.postsPerPage));
}); });
}); });
}; };
Topics.getTidPage = function(tid, uid, callback) { Topics.getTidPage = function(tid, uid, callback) {
if(!tid) { if(!tid) {
return callback(new Error('[[error:invalid-tid]]')); return callback(new Error('[[error:invalid-tid]]'));
} }
async.parallel({ async.parallel({
index: function(next) { index: function(next) {
categories.getTopicIndex(tid, next); categories.getTopicIndex(tid, next);
}, },
settings: function(next) { settings: function(next) {
user.getSettings(uid, next); user.getSettings(uid, next);
} }
}, function(err, results) { }, function(err, results) {
if(err) { if(err) {
return callback(err); return callback(err);
} }
callback(null, Math.ceil((results.index + 1) / results.settings.topicsPerPage)); callback(null, Math.ceil((results.index + 1) / results.settings.topicsPerPage));
}); });
}; };
Topics.getCategoryData = function(tid, callback) { Topics.getCategoryData = function(tid, callback) {
Topics.getTopicField(tid, 'cid', function(err, cid) { Topics.getTopicField(tid, 'cid', function(err, cid) {
if (err) { if (err) {
callback(err); callback(err);
} }
categories.getCategoryData(cid, callback); categories.getCategoryData(cid, callback);
}); });
}; };
Topics.getTopicsFromSet = function(set, uid, start, stop, callback) { Topics.getTopicsFromSet = function(set, uid, start, stop, callback) {
async.waterfall([ async.waterfall([
function(next) { function(next) {
db.getSortedSetRevRange(set, start, stop, next); db.getSortedSetRevRange(set, start, stop, next);
}, },
function(tids, next) { function(tids, next) {
Topics.getTopics(tids, uid, next); Topics.getTopics(tids, uid, next);
}, },
function(topics, next) { function(topics, next) {
next(null, {topics: topics, nextStart: stop + 1}); next(null, {topics: topics, nextStart: stop + 1});
} }
], callback); ], callback);
}; };
Topics.getTopics = function(tids, uid, callback) { Topics.getTopics = function(tids, uid, callback) {
async.waterfall([ async.waterfall([
function(next) { function(next) {
privileges.topics.filterTids('read', tids, uid, next); privileges.topics.filterTids('read', tids, uid, next);
}, },
function(tids, next) { function(tids, next) {
Topics.getTopicsByTids(tids, uid, next); Topics.getTopicsByTids(tids, uid, next);
} }
], callback); ], callback);
}; };
Topics.getTopicsByTids = function(tids, uid, callback) { Topics.getTopicsByTids = function(tids, uid, callback) {
if (!Array.isArray(tids) || !tids.length) { if (!Array.isArray(tids) || !tids.length) {
return callback(null, []); return callback(null, []);
} }
Topics.getTopicsData(tids, function(err, topics) { Topics.getTopicsData(tids, function(err, topics) {
function mapFilter(array, field) { function mapFilter(array, field) {
return array.map(function(topic) { return array.map(function(topic) {
return topic && topic[field] && topic[field].toString(); return topic && topic[field] && topic[field].toString();
}).filter(function(value, index, array) { }).filter(function(value, index, array) {
return utils.isNumber(value) && array.indexOf(value) === index; return utils.isNumber(value) && array.indexOf(value) === index;
}); });
} }
if (err) { if (err) {
return callback(err); return callback(err);
} }
var uids = mapFilter(topics, 'uid'); var uids = mapFilter(topics, 'uid');
var cids = mapFilter(topics, 'cid'); var cids = mapFilter(topics, 'cid');
async.parallel({ async.parallel({
teasers: function(next) { teasers: function(next) {
Topics.getTeasers(topics, next); Topics.getTeasers(topics, next);
}, },
users: function(next) { users: function(next) {
user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture'], next); user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture'], next);
}, },
categories: function(next) { categories: function(next) {
categories.getMultipleCategoryFields(cids, ['cid', 'name', 'slug', 'icon', 'bgColor', 'color', 'disabled'], next); categories.getMultipleCategoryFields(cids, ['cid', 'name', 'slug', 'icon', 'bgColor', 'color', 'disabled'], next);
}, },
hasRead: function(next) { hasRead: function(next) {
Topics.hasReadTopics(tids, uid, next); Topics.hasReadTopics(tids, uid, next);
}, },
tags: function(next) { tags: function(next) {
Topics.getTopicsTagsObjects(tids, next); Topics.getTopicsTagsObjects(tids, next);
} }
}, function(err, results) { }, function(err, results) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
var users = _.object(uids, results.users); var users = _.object(uids, results.users);
var categories = _.object(cids, results.categories); var categories = _.object(cids, results.categories);
for (var i=0; i<topics.length; ++i) { for (var i=0; i<topics.length; ++i) {
if (topics[i]) { if (topics[i]) {
topics[i].category = categories[topics[i].cid]; topics[i].category = categories[topics[i].cid];
topics[i].user = users[topics[i].uid]; topics[i].user = users[topics[i].uid];
topics[i].teaser = results.teasers[i]; topics[i].teaser = results.teasers[i];
topics[i].tags = results.tags[i]; topics[i].tags = results.tags[i];
topics[i].isOwner = parseInt(topics[i].uid, 10) === parseInt(uid, 10); topics[i].isOwner = parseInt(topics[i].uid, 10) === parseInt(uid, 10);
topics[i].pinned = parseInt(topics[i].pinned, 10) === 1; topics[i].pinned = parseInt(topics[i].pinned, 10) === 1;
topics[i].locked = parseInt(topics[i].locked, 10) === 1; topics[i].locked = parseInt(topics[i].locked, 10) === 1;
topics[i].deleted = parseInt(topics[i].deleted, 10) === 1; topics[i].deleted = parseInt(topics[i].deleted, 10) === 1;
topics[i].unread = !results.hasRead[i]; topics[i].unread = !results.hasRead[i];
topics[i].unreplied = parseInt(topics[i].postcount, 10) <= 1; topics[i].unreplied = parseInt(topics[i].postcount, 10) <= 1;
} }
} }
topics = topics.filter(function(topic) { topics = topics.filter(function(topic) {
return topic && topic.category && !topic.category.disabled; return topic && topic.category && !topic.category.disabled;
}); });
plugins.fireHook('filter:topics.get', {topics: topics, uid: uid}, function(err, topicData) { plugins.fireHook('filter:topics.get', {topics: topics, uid: uid}, function(err, topicData) {
callback(err, topicData.topics); callback(err, topicData.topics);
}); });
}); });
}); });
}; };
Topics.getTopicWithPosts = function(tid, set, uid, start, stop, reverse, callback) { Topics.getTopicWithPosts = function(tid, set, uid, start, stop, reverse, callback) {
Topics.getTopicData(tid, function(err, topicData) { Topics.getTopicData(tid, function(err, topicData) {
if (err || !topicData) { if (err || !topicData) {
return callback(err || new Error('[[error:no-topic]]')); return callback(err || new Error('[[error:no-topic]]'));
} }
async.parallel({ async.parallel({
posts: async.apply(getMainPostAndReplies, topicData, set, uid, start, stop, reverse), posts: async.apply(getMainPostAndReplies, topicData, set, uid, start, stop, reverse),
category: async.apply(Topics.getCategoryData, tid), category: async.apply(Topics.getCategoryData, tid),
threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', {topic: topicData, uid: uid, tools: []}), threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', {topic: topicData, uid: uid, tools: []}),
tags: async.apply(Topics.getTopicTagsObjects, tid), tags: async.apply(Topics.getTopicTagsObjects, tid),
isFollowing: async.apply(Topics.isFollowing, [tid], uid), isFollowing: async.apply(Topics.isFollowing, [tid], uid),
bookmark: async.apply(Topics.getUserBookmark, tid, uid) bookmark: async.apply(Topics.getUserBookmark, tid, uid)
}, function(err, results) { }, function(err, results) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
topicData.posts = results.posts; topicData.posts = results.posts;
topicData.category = results.category; topicData.category = results.category;
topicData.thread_tools = results.threadTools.tools; topicData.thread_tools = results.threadTools.tools;
topicData.tags = results.tags; topicData.tags = results.tags;
topicData.isFollowing = results.isFollowing[0]; topicData.isFollowing = results.isFollowing[0];
topicData.bookmark = results.bookmark; topicData.bookmark = results.bookmark;
topicData.unreplied = parseInt(topicData.postcount, 10) === 1; topicData.unreplied = parseInt(topicData.postcount, 10) === 1;
topicData.deleted = parseInt(topicData.deleted, 10) === 1; topicData.deleted = parseInt(topicData.deleted, 10) === 1;
topicData.locked = parseInt(topicData.locked, 10) === 1; topicData.locked = parseInt(topicData.locked, 10) === 1;
topicData.pinned = parseInt(topicData.pinned, 10) === 1; topicData.pinned = parseInt(topicData.pinned, 10) === 1;
plugins.fireHook('filter:topic.get', {topic: topicData, uid: uid}, function(err, data) { plugins.fireHook('filter:topic.get', {topic: topicData, uid: uid}, function(err, data) {
callback(err, data ? data.topic : null); callback(err, data ? data.topic : null);
}); });
}); });
}); });
}; };
function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback) { function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback) {
async.waterfall([ async.waterfall([
function(next) { function(next) {
posts.getPidsFromSet(set, start, stop, reverse, next); posts.getPidsFromSet(set, start, stop, reverse, next);
}, },
function(pids, next) { function(pids, next) {
if ((!Array.isArray(pids) || !pids.length) && !topic.mainPid) { if ((!Array.isArray(pids) || !pids.length) && !topic.mainPid) {
return callback(null, []); return callback(null, []);
} }
if (topic.mainPid) { if (topic.mainPid) {
pids.unshift(topic.mainPid); pids.unshift(topic.mainPid);
} }
posts.getPostsByPids(pids, uid, next); posts.getPostsByPids(pids, uid, next);
}, },
function(posts, next) { function(posts, next) {
if (!posts.length) { if (!posts.length) {
return next(null, []); return next(null, []);
} }
if (topic.mainPid) { if (topic.mainPid) {
posts[0].index = 0; posts[0].index = 0;
} }
var indices = Topics.calculatePostIndices(start, stop, topic.postcount, reverse); var indices = Topics.calculatePostIndices(start, stop, topic.postcount, reverse);
for (var i=1; i<posts.length; ++i) { for (var i=1; i<posts.length; ++i) {
if (posts[i]) { if (posts[i]) {
posts[i].index = indices[i - 1]; posts[i].index = indices[i - 1];
} }
} }
Topics.addPostData(posts, uid, callback); Topics.addPostData(posts, uid, callback);
} }
]); ]);
} }
Topics.getMainPost = function(tid, uid, callback) { Topics.getMainPost = function(tid, uid, callback) {
Topics.getMainPosts([tid], uid, function(err, mainPosts) { Topics.getMainPosts([tid], uid, function(err, mainPosts) {
callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null); callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null);
}); });
}; };
Topics.getMainPids = function(tids, callback) { Topics.getMainPids = function(tids, callback) {
if (!Array.isArray(tids) || !tids.length) { if (!Array.isArray(tids) || !tids.length) {
return callback(null, []); return callback(null, []);
} }
Topics.getTopicsFields(tids, ['mainPid'], function(err, topicData) { Topics.getTopicsFields(tids, ['mainPid'], function(err, topicData) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
var mainPids = topicData.map(function(topic) { var mainPids = topicData.map(function(topic) {
return topic && topic.mainPid; return topic && topic.mainPid;
}); });
callback(null, mainPids); callback(null, mainPids);
}); });
}; };
Topics.getMainPosts = function(tids, uid, callback) { Topics.getMainPosts = function(tids, uid, callback) {
Topics.getMainPids(tids, function(err, mainPids) { Topics.getMainPids(tids, function(err, mainPids) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
getMainPosts(mainPids, uid, callback); getMainPosts(mainPids, uid, callback);
}); });
}; };
function getMainPosts(mainPids, uid, callback) { function getMainPosts(mainPids, uid, callback) {
posts.getPostsByPids(mainPids, uid, function(err, postData) { posts.getPostsByPids(mainPids, uid, function(err, postData) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
postData.forEach(function(post) { postData.forEach(function(post) {
if (post) { if (post) {
post.index = 0; post.index = 0;
} }
}); });
Topics.addPostData(postData, uid, callback); Topics.addPostData(postData, uid, callback);
}); });
} }
Topics.getUserBookmark = function (tid, uid, callback) { Topics.getUserBookmark = function (tid, uid, callback) {
Topics.getTopicField(tid + ':bookmarks', uid, callback); Topics.getTopicField(tid + ':bookmarks', uid, callback);
} }
Topics.setUserBookmark = function(data, callback) { Topics.setUserBookmark = function(data, callback) {
Topics.setTopicField(data.tid + ':bookmarks', data.uid, data.postIndex, callback); Topics.setTopicField(data.tid + ':bookmarks', data.uid, data.postIndex, callback);
} }
Topics.getTopicField = function(tid, field, callback) { Topics.getTopicField = function(tid, field, callback) {
db.getObjectField('topic:' + tid, field, callback); db.getObjectField('topic:' + tid, field, callback);
}; };
Topics.getTopicFields = function(tid, fields, callback) { Topics.getTopicFields = function(tid, fields, callback) {
db.getObjectFields('topic:' + tid, fields, callback); db.getObjectFields('topic:' + tid, fields, callback);
}; };
Topics.getTopicsFields = function(tids, fields, callback) { Topics.getTopicsFields = function(tids, fields, callback) {
if (!Array.isArray(tids) || !tids.length) { if (!Array.isArray(tids) || !tids.length) {
return callback(null, []); return callback(null, []);
} }
var keys = tids.map(function(tid) { var keys = tids.map(function(tid) {
return 'topic:' + tid; return 'topic:' + tid;
}); });
db.getObjectsFields(keys, fields, callback); db.getObjectsFields(keys, fields, callback);
}; };
Topics.setTopicField = function(tid, field, value, callback) { Topics.setTopicField = function(tid, field, value, callback) {
db.setObjectField('topic:' + tid, field, value, callback); db.setObjectField('topic:' + tid, field, value, callback);
}; };
Topics.isLocked = function(tid, callback) { Topics.isLocked = function(tid, callback) {
Topics.getTopicField(tid, 'locked', function(err, locked) { Topics.getTopicField(tid, 'locked', function(err, locked) {
callback(err, parseInt(locked, 10) === 1); callback(err, parseInt(locked, 10) === 1);
}); });
}; };
Topics.search = function(tid, term, callback) { Topics.search = function(tid, term, callback) {
if (plugins.hasListeners('filter:topic.search')) { if (plugins.hasListeners('filter:topic.search')) {
plugins.fireHook('filter:topic.search', { plugins.fireHook('filter:topic.search', {
tid: tid, tid: tid,
term: term term: term
}, callback); }, callback);
} else { } else {
callback(new Error('no-plugins-available'), []); callback(new Error('no-plugins-available'), []);
} }
}; };
}(exports)); }(exports));

@ -1,230 +1,230 @@
'use strict'; 'use strict';
var path = require('path'), var path = require('path'),
fs = require('fs'), fs = require('fs'),
nconf = require('nconf'), nconf = require('nconf'),
express = require('express'), express = require('express'),
app = express(), app = express(),
server, server,
winston = require('winston'), winston = require('winston'),
async = require('async'), async = require('async'),
emailer = require('./emailer'), emailer = require('./emailer'),
meta = require('./meta'), meta = require('./meta'),
logger = require('./logger'), logger = require('./logger'),
plugins = require('./plugins'), plugins = require('./plugins'),
middleware = require('./middleware'), middleware = require('./middleware'),
routes = require('./routes'), routes = require('./routes'),
emitter = require('./emitter'), emitter = require('./emitter'),
helpers = require('../public/src/modules/helpers'); helpers = require('../public/src/modules/helpers');
if (nconf.get('ssl')) { if (nconf.get('ssl')) {
server = require('https').createServer({ server = require('https').createServer({
key: fs.readFileSync(nconf.get('ssl').key), key: fs.readFileSync(nconf.get('ssl').key),
cert: fs.readFileSync(nconf.get('ssl').cert) cert: fs.readFileSync(nconf.get('ssl').cert)
}, app); }, app);
} else { } else {
server = require('http').createServer(app); server = require('http').createServer(app);
} }
module.exports.server = server; module.exports.server = server;
server.on('error', function(err) { server.on('error', function(err) {
winston.error(err); winston.error(err);
if (err.code === 'EADDRINUSE') { if (err.code === 'EADDRINUSE') {
winston.error('NodeBB address in use, exiting...'); winston.error('NodeBB address in use, exiting...');
process.exit(0); process.exit(0);
} else { } else {
throw err; throw err;
} }
}); });
module.exports.listen = function() { module.exports.listen = function() {
emailer.registerApp(app); emailer.registerApp(app);
middleware = middleware(app); middleware = middleware(app);
helpers.register(); helpers.register();
logger.init(app); logger.init(app);
emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function() { emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function() {
winston.info('NodeBB Ready'); winston.info('NodeBB Ready');
emitter.emit('nodebb:ready'); emitter.emit('nodebb:ready');
listen(); listen();
}); });
initializeNodeBB(function(err) { initializeNodeBB(function(err) {
if (err) { if (err) {
winston.error(err); winston.error(err);
process.exit(); process.exit();
} }
if (process.send) { if (process.send) {
process.send({ process.send({
action: 'ready' action: 'ready'
}); });
} }
}); });
}; };
function initializeNodeBB(callback) { function initializeNodeBB(callback) {
var skipJS, skipLess, fromFile = nconf.get('from-file') || ''; var skipJS, skipLess, fromFile = nconf.get('from-file') || '';
if (fromFile.match('js')) { if (fromFile.match('js')) {
winston.info('[minifier] Minifying client-side JS skipped'); winston.info('[minifier] Minifying client-side JS skipped');
skipJS = true; skipJS = true;
} }
if (fromFile.match('less')) { if (fromFile.match('less')) {
winston.info('[minifier] Compiling LESS files skipped'); winston.info('[minifier] Compiling LESS files skipped');
skipLess = true; skipLess = true;
} }
async.waterfall([ async.waterfall([
async.apply(cacheStaticFiles), async.apply(cacheStaticFiles),
async.apply(meta.themes.setupPaths), async.apply(meta.themes.setupPaths),
function(next) { function(next) {
plugins.init(app, middleware, next); plugins.init(app, middleware, next);
}, },
function(next) { function(next) {
async.parallel([ async.parallel([
async.apply(meta.templates.compile), async.apply(meta.templates.compile),
async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, app.enabled('minification')), async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, app.enabled('minification')),
async.apply(!skipLess ? meta.css.minify : meta.css.getFromFile), async.apply(!skipLess ? meta.css.minify : meta.css.getFromFile),
async.apply(meta.sounds.init) async.apply(meta.sounds.init)
], next); ], next);
}, },
function(results, next) { function(results, next) {
plugins.fireHook('static:app.preload', { plugins.fireHook('static:app.preload', {
app: app, app: app,
middleware: middleware middleware: middleware
}, next); }, next);
}, },
function(next) { function(next) {
routes(app, middleware); routes(app, middleware);
next(); next();
} }
], callback); ], callback);
} }
function cacheStaticFiles(callback) { function cacheStaticFiles(callback) {
if (global.env === 'development') { if (global.env === 'development') {
return callback(); return callback();
} }
app.enable('cache'); app.enable('cache');
app.enable('minification'); app.enable('minification');
// Configure cache-buster timestamp // Configure cache-buster timestamp
require('child_process').exec('git describe --tags', { require('child_process').exec('git describe --tags', {
cwd: path.join(__dirname, '../') cwd: path.join(__dirname, '../')
}, function(err, stdOut) { }, function(err, stdOut) {
if (!err) { if (!err) {
meta.config['cache-buster'] = stdOut.trim(); meta.config['cache-buster'] = stdOut.trim();
callback(); callback();
} else { } else {
fs.stat(path.join(__dirname, '../package.json'), function(err, stats) { fs.stat(path.join(__dirname, '../package.json'), function(err, stats) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
meta.config['cache-buster'] = new Date(stats.mtime).getTime(); meta.config['cache-buster'] = new Date(stats.mtime).getTime();
callback(); callback();
}); });
} }
}); });
} }
function listen(callback) { function listen(callback) {
var port = nconf.get('port'); var port = nconf.get('port');
if (Array.isArray(port)) { if (Array.isArray(port)) {
if (!port.length) { if (!port.length) {
winston.error('[startup] empty ports array in config.json'); winston.error('[startup] empty ports array in config.json');
process.exit(); process.exit();
} }
winston.warn('[startup] If you want to start nodebb on multiple ports please use loader.js'); winston.warn('[startup] If you want to start nodebb on multiple ports please use loader.js');
winston.warn('[startup] Defaulting to first port in array, ' + port[0]); winston.warn('[startup] Defaulting to first port in array, ' + port[0]);
port = port[0]; port = port[0];
if (!port) { if (!port) {
winston.error('[startup] Invalid port, exiting'); winston.error('[startup] Invalid port, exiting');
process.exit(); process.exit();
} }
} }
if (port !== 80 && port !== 443 && nconf.get('use_port') === false) { if (port !== 80 && port !== 443 && nconf.get('use_port') === false) {
winston.info('Enabling \'trust proxy\''); winston.info('Enabling \'trust proxy\'');
app.enable('trust proxy'); app.enable('trust proxy');
} }
if ((port === 80 || port === 443) && process.env.NODE_ENV !== 'development') { if ((port === 80 || port === 443) && process.env.NODE_ENV !== 'development') {
winston.info('Using ports 80 and 443 is not recommend; use a proxy instead. See README.md'); winston.info('Using ports 80 and 443 is not recommend; use a proxy instead. See README.md');
} }
var isSocket = isNaN(port), var isSocket = isNaN(port),
args = isSocket ? [port] : [port, nconf.get('bind_address')], args = isSocket ? [port] : [port, nconf.get('bind_address')],
bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port, bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port,
oldUmask; oldUmask;
args.push(function(err) { args.push(function(err) {
if (err) { if (err) {
winston.info('[startup] NodeBB was unable to listen on: ' + bind_address); winston.info('[startup] NodeBB was unable to listen on: ' + bind_address);
process.exit(); process.exit();
} }
winston.info('NodeBB is now listening on: ' + (isSocket ? port : bind_address)); winston.info('NodeBB is now listening on: ' + (isSocket ? port : bind_address));
if (oldUmask) { if (oldUmask) {
process.umask(oldUmask); process.umask(oldUmask);
} }
}); });
// Alter umask if necessary // Alter umask if necessary
if (isSocket) { if (isSocket) {
oldUmask = process.umask('0000'); oldUmask = process.umask('0000');
module.exports.testSocket(port, function(err) { module.exports.testSocket(port, function(err) {
if (!err) { if (!err) {
server.listen.apply(server, args); server.listen.apply(server, args);
} else { } else {
winston.error('[startup] NodeBB was unable to secure domain socket access (' + port + ')'); winston.error('[startup] NodeBB was unable to secure domain socket access (' + port + ')');
winston.error('[startup] ' + err.message); winston.error('[startup] ' + err.message);
process.exit(); process.exit();
} }
}); });
} else { } else {
server.listen.apply(server, args); server.listen.apply(server, args);
} }
} }
module.exports.testSocket = function(socketPath, callback) { module.exports.testSocket = function(socketPath, callback) {
if (typeof socketPath !== 'string') { if (typeof socketPath !== 'string') {
return callback(new Error('invalid socket path : ' + socketPath)); return callback(new Error('invalid socket path : ' + socketPath));
} }
var net = require('net'); var net = require('net');
async.series([ async.series([
function(next) { function(next) {
fs.exists(socketPath, function(exists) { fs.exists(socketPath, function(exists) {
if (exists) { if (exists) {
next(); next();
} else { } else {
callback(); callback();
} }
}); });
}, },
function(next) { function(next) {
var testSocket = new net.Socket(); var testSocket = new net.Socket();
testSocket.on('error', function(err) { testSocket.on('error', function(err) {
next(err.code !== 'ECONNREFUSED' ? err : null); next(err.code !== 'ECONNREFUSED' ? err : null);
}); });
testSocket.connect({ path: socketPath }, function() { testSocket.connect({ path: socketPath }, function() {
// Something's listening here, abort // Something's listening here, abort
callback(new Error('port-in-use')); callback(new Error('port-in-use'));
}); });
}, },
async.apply(fs.unlink, socketPath), // The socket was stale, kick it out of the way async.apply(fs.unlink, socketPath), // The socket was stale, kick it out of the way
], callback); ], callback);
}; };

Loading…
Cancel
Save