diff --git a/.travis.yml b/.travis.yml index fed11bb742..168469fd0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,6 @@ addons: - g++-4.8 node_js: - "6" - - "5" - "4" branches: only: diff --git a/.tx/config b/.tx/config index 65d891131e..58da2cc97d 100644 --- a/.tx/config +++ b/.tx/config @@ -3,7 +3,7 @@ host = https://www.transifex.com [nodebb.category] file_filter = public/language//category.json -source_file = public/language/en_GB/category.json +source_file = public/language/en-GB/category.json source_lang = en_GB trans.ar = public/language/ar/category.json trans.bn = public/language/bn/category.json @@ -12,11 +12,11 @@ trans.cs = public/language/cs/category.json trans.da = public/language/da/category.json trans.de = public/language/de/category.json trans.el = public/language/el/category.json -trans.en_US = public/language/en_US/category.json -trans.en@pirate = public/language/en@pirate/category.json +trans.en_US = public/language/en-US/category.json +trans.en@pirate = public/language/en-x-pirate/category.json trans.es = public/language/es/category.json trans.et = public/language/et/category.json -trans.fa_IR = public/language/fa_IR/category.json +trans.fa_IR = public/language/fa-IR/category.json trans.fi = public/language/fi/category.json trans.fr = public/language/fr/category.json trans.gl = public/language/gl/category.json @@ -31,7 +31,7 @@ trans.ms = public/language/ms/category.json trans.nb = public/language/nb/category.json trans.nl = public/language/nl/category.json trans.pl = public/language/pl/category.json -trans.pt_BR = public/language/pt_BR/category.json +trans.pt_BR = public/language/pt-BR/category.json trans.ru = public/language/ru/category.json trans.ro = public/language/ro/category.json trans.rw = public/language/rw/category.json @@ -43,13 +43,13 @@ trans.sv = public/language/sv/category.json trans.th = public/language/th/category.json trans.tr = public/language/tr/category.json trans.vi = public/language/vi/category.json -trans.zh_CN = public/language/zh_CN/category.json -trans.zh_TW = public/language/zh_TW/category.json +trans.zh_CN = public/language/zh-CN/category.json +trans.zh_TW = public/language/zh-TW/category.json type = KEYVALUEJSON [nodebb.login] file_filter = public/language//login.json -source_file = public/language/en_GB/login.json +source_file = public/language/en-GB/login.json source_lang = en_GB trans.ar = public/language/ar/login.json trans.bn = public/language/bn/login.json @@ -58,11 +58,11 @@ trans.cs = public/language/cs/login.json trans.da = public/language/da/login.json trans.de = public/language/de/login.json trans.el = public/language/el/login.json -trans.en_US = public/language/en_US/login.json -trans.en@pirate = public/language/en@pirate/login.json +trans.en_US = public/language/en-US/login.json +trans.en@pirate = public/language/en-x-pirate/login.json trans.es = public/language/es/login.json trans.et = public/language/et/login.json -trans.fa_IR = public/language/fa_IR/login.json +trans.fa_IR = public/language/fa-IR/login.json trans.fi = public/language/fi/login.json trans.fr = public/language/fr/login.json trans.gl = public/language/gl/login.json @@ -77,7 +77,7 @@ trans.ms = public/language/ms/login.json trans.nb = public/language/nb/login.json trans.nl = public/language/nl/login.json trans.pl = public/language/pl/login.json -trans.pt_BR = public/language/pt_BR/login.json +trans.pt_BR = public/language/pt-BR/login.json trans.ru = public/language/ru/login.json trans.ro = public/language/ro/login.json trans.rw = public/language/rw/login.json @@ -89,13 +89,13 @@ trans.sv = public/language/sv/login.json trans.th = public/language/th/login.json trans.tr = public/language/tr/login.json trans.vi = public/language/vi/login.json -trans.zh_CN = public/language/zh_CN/login.json -trans.zh_TW = public/language/zh_TW/login.json +trans.zh_CN = public/language/zh-CN/login.json +trans.zh_TW = public/language/zh-TW/login.json type = KEYVALUEJSON [nodebb.recent] file_filter = public/language//recent.json -source_file = public/language/en_GB/recent.json +source_file = public/language/en-GB/recent.json source_lang = en_GB trans.ar = public/language/ar/recent.json trans.bn = public/language/bn/recent.json @@ -104,11 +104,11 @@ trans.cs = public/language/cs/recent.json trans.da = public/language/da/recent.json trans.de = public/language/de/recent.json trans.el = public/language/el/recent.json -trans.en_US = public/language/en_US/recent.json -trans.en@pirate = public/language/en@pirate/recent.json +trans.en_US = public/language/en-US/recent.json +trans.en@pirate = public/language/en-x-pirate/recent.json trans.es = public/language/es/recent.json trans.et = public/language/et/recent.json -trans.fa_IR = public/language/fa_IR/recent.json +trans.fa_IR = public/language/fa-IR/recent.json trans.fi = public/language/fi/recent.json trans.fr = public/language/fr/recent.json trans.gl = public/language/gl/recent.json @@ -123,7 +123,7 @@ trans.ms = public/language/ms/recent.json trans.nb = public/language/nb/recent.json trans.nl = public/language/nl/recent.json trans.pl = public/language/pl/recent.json -trans.pt_BR = public/language/pt_BR/recent.json +trans.pt_BR = public/language/pt-BR/recent.json trans.ru = public/language/ru/recent.json trans.ro = public/language/ro/recent.json trans.rw = public/language/rw/recent.json @@ -135,13 +135,13 @@ trans.sv = public/language/sv/recent.json trans.th = public/language/th/recent.json trans.tr = public/language/tr/recent.json trans.vi = public/language/vi/recent.json -trans.zh_CN = public/language/zh_CN/recent.json -trans.zh_TW = public/language/zh_TW/recent.json +trans.zh_CN = public/language/zh-CN/recent.json +trans.zh_TW = public/language/zh-TW/recent.json type = KEYVALUEJSON [nodebb.unread] file_filter = public/language//unread.json -source_file = public/language/en_GB/unread.json +source_file = public/language/en-GB/unread.json source_lang = en_GB trans.ar = public/language/ar/unread.json trans.bn = public/language/bn/unread.json @@ -150,11 +150,11 @@ trans.cs = public/language/cs/unread.json trans.da = public/language/da/unread.json trans.de = public/language/de/unread.json trans.el = public/language/el/unread.json -trans.en_US = public/language/en_US/unread.json -trans.en@pirate = public/language/en@pirate/unread.json +trans.en_US = public/language/en-US/unread.json +trans.en@pirate = public/language/en-x-pirate/unread.json trans.es = public/language/es/unread.json trans.et = public/language/et/unread.json -trans.fa_IR = public/language/fa_IR/unread.json +trans.fa_IR = public/language/fa-IR/unread.json trans.fi = public/language/fi/unread.json trans.fr = public/language/fr/unread.json trans.gl = public/language/gl/unread.json @@ -169,7 +169,7 @@ trans.ms = public/language/ms/unread.json trans.nb = public/language/nb/unread.json trans.nl = public/language/nl/unread.json trans.pl = public/language/pl/unread.json -trans.pt_BR = public/language/pt_BR/unread.json +trans.pt_BR = public/language/pt-BR/unread.json trans.ru = public/language/ru/unread.json trans.ro = public/language/ro/unread.json trans.rw = public/language/rw/unread.json @@ -181,13 +181,13 @@ trans.sv = public/language/sv/unread.json trans.th = public/language/th/unread.json trans.tr = public/language/tr/unread.json trans.vi = public/language/vi/unread.json -trans.zh_CN = public/language/zh_CN/unread.json -trans.zh_TW = public/language/zh_TW/unread.json +trans.zh_CN = public/language/zh-CN/unread.json +trans.zh_TW = public/language/zh-TW/unread.json type = KEYVALUEJSON [nodebb.modules] file_filter = public/language//modules.json -source_file = public/language/en_GB/modules.json +source_file = public/language/en-GB/modules.json source_lang = en_GB trans.ar = public/language/ar/modules.json trans.bn = public/language/bn/modules.json @@ -196,11 +196,11 @@ trans.cs = public/language/cs/modules.json trans.da = public/language/da/modules.json trans.de = public/language/de/modules.json trans.el = public/language/el/modules.json -trans.en_US = public/language/en_US/modules.json -trans.en@pirate = public/language/en@pirate/modules.json +trans.en_US = public/language/en-US/modules.json +trans.en@pirate = public/language/en-x-pirate/modules.json trans.es = public/language/es/modules.json trans.et = public/language/et/modules.json -trans.fa_IR = public/language/fa_IR/modules.json +trans.fa_IR = public/language/fa-IR/modules.json trans.fi = public/language/fi/modules.json trans.fr = public/language/fr/modules.json trans.gl = public/language/gl/modules.json @@ -215,7 +215,7 @@ trans.ms = public/language/ms/modules.json trans.nb = public/language/nb/modules.json trans.nl = public/language/nl/modules.json trans.pl = public/language/pl/modules.json -trans.pt_BR = public/language/pt_BR/modules.json +trans.pt_BR = public/language/pt-BR/modules.json trans.ru = public/language/ru/modules.json trans.ro = public/language/ro/modules.json trans.rw = public/language/rw/modules.json @@ -227,13 +227,13 @@ trans.sv = public/language/sv/modules.json trans.th = public/language/th/modules.json trans.tr = public/language/tr/modules.json trans.vi = public/language/vi/modules.json -trans.zh_CN = public/language/zh_CN/modules.json -trans.zh_TW = public/language/zh_TW/modules.json +trans.zh_CN = public/language/zh-CN/modules.json +trans.zh_TW = public/language/zh-TW/modules.json type = KEYVALUEJSON [nodebb.register] file_filter = public/language//register.json -source_file = public/language/en_GB/register.json +source_file = public/language/en-GB/register.json source_lang = en_GB trans.ar = public/language/ar/register.json trans.bn = public/language/bn/register.json @@ -242,11 +242,11 @@ trans.cs = public/language/cs/register.json trans.da = public/language/da/register.json trans.de = public/language/de/register.json trans.el = public/language/el/register.json -trans.en_US = public/language/en_US/register.json -trans.en@pirate = public/language/en@pirate/register.json +trans.en_US = public/language/en-US/register.json +trans.en@pirate = public/language/en-x-pirate/register.json trans.es = public/language/es/register.json trans.et = public/language/et/register.json -trans.fa_IR = public/language/fa_IR/register.json +trans.fa_IR = public/language/fa-IR/register.json trans.fi = public/language/fi/register.json trans.fr = public/language/fr/register.json trans.gl = public/language/gl/register.json @@ -261,7 +261,7 @@ trans.ms = public/language/ms/register.json trans.nb = public/language/nb/register.json trans.nl = public/language/nl/register.json trans.pl = public/language/pl/register.json -trans.pt_BR = public/language/pt_BR/register.json +trans.pt_BR = public/language/pt-BR/register.json trans.ru = public/language/ru/register.json trans.ro = public/language/ro/register.json trans.rw = public/language/rw/register.json @@ -273,13 +273,13 @@ trans.sv = public/language/sv/register.json trans.th = public/language/th/register.json trans.tr = public/language/tr/register.json trans.vi = public/language/vi/register.json -trans.zh_CN = public/language/zh_CN/register.json -trans.zh_TW = public/language/zh_TW/register.json +trans.zh_CN = public/language/zh-CN/register.json +trans.zh_TW = public/language/zh-TW/register.json type = KEYVALUEJSON [nodebb.user] file_filter = public/language//user.json -source_file = public/language/en_GB/user.json +source_file = public/language/en-GB/user.json source_lang = en_GB trans.ar = public/language/ar/user.json trans.bn = public/language/bn/user.json @@ -288,11 +288,11 @@ trans.cs = public/language/cs/user.json trans.da = public/language/da/user.json trans.de = public/language/de/user.json trans.el = public/language/el/user.json -trans.en_US = public/language/en_US/user.json -trans.en@pirate = public/language/en@pirate/user.json +trans.en_US = public/language/en-US/user.json +trans.en@pirate = public/language/en-x-pirate/user.json trans.es = public/language/es/user.json trans.et = public/language/et/user.json -trans.fa_IR = public/language/fa_IR/user.json +trans.fa_IR = public/language/fa-IR/user.json trans.fi = public/language/fi/user.json trans.fr = public/language/fr/user.json trans.gl = public/language/gl/user.json @@ -307,7 +307,7 @@ trans.ms = public/language/ms/user.json trans.nb = public/language/nb/user.json trans.nl = public/language/nl/user.json trans.pl = public/language/pl/user.json -trans.pt_BR = public/language/pt_BR/user.json +trans.pt_BR = public/language/pt-BR/user.json trans.ru = public/language/ru/user.json trans.ro = public/language/ro/user.json trans.rw = public/language/rw/user.json @@ -319,13 +319,13 @@ trans.sv = public/language/sv/user.json trans.th = public/language/th/user.json trans.tr = public/language/tr/user.json trans.vi = public/language/vi/user.json -trans.zh_CN = public/language/zh_CN/user.json -trans.zh_TW = public/language/zh_TW/user.json +trans.zh_CN = public/language/zh-CN/user.json +trans.zh_TW = public/language/zh-TW/user.json type = KEYVALUEJSON [nodebb.global] file_filter = public/language//global.json -source_file = public/language/en_GB/global.json +source_file = public/language/en-GB/global.json source_lang = en_GB trans.ar = public/language/ar/global.json trans.bn = public/language/bn/global.json @@ -334,11 +334,11 @@ trans.cs = public/language/cs/global.json trans.da = public/language/da/global.json trans.de = public/language/de/global.json trans.el = public/language/el/global.json -trans.en_US = public/language/en_US/global.json -trans.en@pirate = public/language/en@pirate/global.json +trans.en_US = public/language/en-US/global.json +trans.en@pirate = public/language/en-x-pirate/global.json trans.es = public/language/es/global.json trans.et = public/language/et/global.json -trans.fa_IR = public/language/fa_IR/global.json +trans.fa_IR = public/language/fa-IR/global.json trans.fi = public/language/fi/global.json trans.fr = public/language/fr/global.json trans.gl = public/language/gl/global.json @@ -353,7 +353,7 @@ trans.ms = public/language/ms/global.json trans.nb = public/language/nb/global.json trans.nl = public/language/nl/global.json trans.pl = public/language/pl/global.json -trans.pt_BR = public/language/pt_BR/global.json +trans.pt_BR = public/language/pt-BR/global.json trans.ru = public/language/ru/global.json trans.ro = public/language/ro/global.json trans.rw = public/language/rw/global.json @@ -365,13 +365,13 @@ trans.sv = public/language/sv/global.json trans.th = public/language/th/global.json trans.tr = public/language/tr/global.json trans.vi = public/language/vi/global.json -trans.zh_CN = public/language/zh_CN/global.json -trans.zh_TW = public/language/zh_TW/global.json +trans.zh_CN = public/language/zh-CN/global.json +trans.zh_TW = public/language/zh-TW/global.json type = KEYVALUEJSON [nodebb.notifications] file_filter = public/language//notifications.json -source_file = public/language/en_GB/notifications.json +source_file = public/language/en-GB/notifications.json source_lang = en_GB trans.ar = public/language/ar/notifications.json trans.bn = public/language/bn/notifications.json @@ -380,11 +380,11 @@ trans.cs = public/language/cs/notifications.json trans.da = public/language/da/notifications.json trans.de = public/language/de/notifications.json trans.el = public/language/el/notifications.json -trans.en_US = public/language/en_US/notifications.json -trans.en@pirate = public/language/en@pirate/notifications.json +trans.en_US = public/language/en-US/notifications.json +trans.en@pirate = public/language/en-x-pirate/notifications.json trans.es = public/language/es/notifications.json trans.et = public/language/et/notifications.json -trans.fa_IR = public/language/fa_IR/notifications.json +trans.fa_IR = public/language/fa-IR/notifications.json trans.fi = public/language/fi/notifications.json trans.fr = public/language/fr/notifications.json trans.gl = public/language/gl/notifications.json @@ -399,7 +399,7 @@ trans.ms = public/language/ms/notifications.json trans.nb = public/language/nb/notifications.json trans.nl = public/language/nl/notifications.json trans.pl = public/language/pl/notifications.json -trans.pt_BR = public/language/pt_BR/notifications.json +trans.pt_BR = public/language/pt-BR/notifications.json trans.ru = public/language/ru/notifications.json trans.ro = public/language/ro/notifications.json trans.rw = public/language/rw/notifications.json @@ -411,13 +411,13 @@ trans.sv = public/language/sv/notifications.json trans.th = public/language/th/notifications.json trans.tr = public/language/tr/notifications.json trans.vi = public/language/vi/notifications.json -trans.zh_CN = public/language/zh_CN/notifications.json -trans.zh_TW = public/language/zh_TW/notifications.json +trans.zh_CN = public/language/zh-CN/notifications.json +trans.zh_TW = public/language/zh-TW/notifications.json type = KEYVALUEJSON [nodebb.reset_password] file_filter = public/language//reset_password.json -source_file = public/language/en_GB/reset_password.json +source_file = public/language/en-GB/reset_password.json source_lang = en_GB trans.ar = public/language/ar/reset_password.json trans.bn = public/language/bn/reset_password.json @@ -426,11 +426,11 @@ trans.cs = public/language/cs/reset_password.json trans.da = public/language/da/reset_password.json trans.de = public/language/de/reset_password.json trans.el = public/language/el/reset_password.json -trans.en_US = public/language/en_US/reset_password.json -trans.en@pirate = public/language/en@pirate/reset_password.json +trans.en_US = public/language/en-US/reset_password.json +trans.en@pirate = public/language/en-x-pirate/reset_password.json trans.es = public/language/es/reset_password.json trans.et = public/language/et/reset_password.json -trans.fa_IR = public/language/fa_IR/reset_password.json +trans.fa_IR = public/language/fa-IR/reset_password.json trans.fi = public/language/fi/reset_password.json trans.fr = public/language/fr/reset_password.json trans.gl = public/language/gl/reset_password.json @@ -445,7 +445,7 @@ trans.ms = public/language/ms/reset_password.json trans.nb = public/language/nb/reset_password.json trans.nl = public/language/nl/reset_password.json trans.pl = public/language/pl/reset_password.json -trans.pt_BR = public/language/pt_BR/reset_password.json +trans.pt_BR = public/language/pt-BR/reset_password.json trans.ru = public/language/ru/reset_password.json trans.ro = public/language/ro/reset_password.json trans.rw = public/language/rw/reset_password.json @@ -457,13 +457,13 @@ trans.sv = public/language/sv/reset_password.json trans.th = public/language/th/reset_password.json trans.tr = public/language/tr/reset_password.json trans.vi = public/language/vi/reset_password.json -trans.zh_CN = public/language/zh_CN/reset_password.json -trans.zh_TW = public/language/zh_TW/reset_password.json +trans.zh_CN = public/language/zh-CN/reset_password.json +trans.zh_TW = public/language/zh-TW/reset_password.json type = KEYVALUEJSON [nodebb.users] file_filter = public/language//users.json -source_file = public/language/en_GB/users.json +source_file = public/language/en-GB/users.json source_lang = en_GB trans.ar = public/language/ar/users.json trans.bn = public/language/bn/users.json @@ -472,11 +472,11 @@ trans.cs = public/language/cs/users.json trans.da = public/language/da/users.json trans.de = public/language/de/users.json trans.el = public/language/el/users.json -trans.en_US = public/language/en_US/users.json -trans.en@pirate = public/language/en@pirate/users.json +trans.en_US = public/language/en-US/users.json +trans.en@pirate = public/language/en-x-pirate/users.json trans.es = public/language/es/users.json trans.et = public/language/et/users.json -trans.fa_IR = public/language/fa_IR/users.json +trans.fa_IR = public/language/fa-IR/users.json trans.fi = public/language/fi/users.json trans.fr = public/language/fr/users.json trans.gl = public/language/gl/users.json @@ -491,7 +491,7 @@ trans.ms = public/language/ms/users.json trans.nb = public/language/nb/users.json trans.nl = public/language/nl/users.json trans.pl = public/language/pl/users.json -trans.pt_BR = public/language/pt_BR/users.json +trans.pt_BR = public/language/pt-BR/users.json trans.ru = public/language/ru/users.json trans.ro = public/language/ro/users.json trans.rw = public/language/rw/users.json @@ -503,13 +503,13 @@ trans.sv = public/language/sv/users.json trans.th = public/language/th/users.json trans.tr = public/language/tr/users.json trans.vi = public/language/vi/users.json -trans.zh_CN = public/language/zh_CN/users.json -trans.zh_TW = public/language/zh_TW/users.json +trans.zh_CN = public/language/zh-CN/users.json +trans.zh_TW = public/language/zh-TW/users.json type = KEYVALUEJSON [nodebb.language-1] file_filter = public/language//language.json -source_file = public/language/en_GB/language.json +source_file = public/language/en-GB/language.json source_lang = en_GB trans.ar = public/language/ar/language.json trans.bn = public/language/bn/language.json @@ -518,11 +518,11 @@ trans.cs = public/language/cs/language.json trans.da = public/language/da/language.json trans.de = public/language/de/language.json trans.el = public/language/el/language.json -trans.en_US = public/language/en_US/language.json -trans.en@pirate = public/language/en@pirate/language.json +trans.en_US = public/language/en-US/language.json +trans.en@pirate = public/language/en-x-pirate/language.json trans.es = public/language/es/language.json trans.et = public/language/et/language.json -trans.fa_IR = public/language/fa_IR/language.json +trans.fa_IR = public/language/fa-IR/language.json trans.fi = public/language/fi/language.json trans.fr = public/language/fr/language.json trans.gl = public/language/gl/language.json @@ -537,7 +537,7 @@ trans.ms = public/language/ms/language.json trans.nb = public/language/nb/language.json trans.nl = public/language/nl/language.json trans.pl = public/language/pl/language.json -trans.pt_BR = public/language/pt_BR/language.json +trans.pt_BR = public/language/pt-BR/language.json trans.ru = public/language/ru/language.json trans.ro = public/language/ro/language.json trans.rw = public/language/rw/language.json @@ -549,13 +549,13 @@ trans.sv = public/language/sv/language.json trans.th = public/language/th/language.json trans.tr = public/language/tr/language.json trans.vi = public/language/vi/language.json -trans.zh_CN = public/language/zh_CN/language.json -trans.zh_TW = public/language/zh_TW/language.json +trans.zh_CN = public/language/zh-CN/language.json +trans.zh_TW = public/language/zh-TW/language.json type = KEYVALUEJSON [nodebb.pages] file_filter = public/language//pages.json -source_file = public/language/en_GB/pages.json +source_file = public/language/en-GB/pages.json source_lang = en_GB trans.ar = public/language/ar/pages.json trans.bn = public/language/bn/pages.json @@ -564,11 +564,11 @@ trans.cs = public/language/cs/pages.json trans.da = public/language/da/pages.json trans.de = public/language/de/pages.json trans.el = public/language/el/pages.json -trans.en_US = public/language/en_US/pages.json -trans.en@pirate = public/language/en@pirate/pages.json +trans.en_US = public/language/en-US/pages.json +trans.en@pirate = public/language/en-x-pirate/pages.json trans.es = public/language/es/pages.json trans.et = public/language/et/pages.json -trans.fa_IR = public/language/fa_IR/pages.json +trans.fa_IR = public/language/fa-IR/pages.json trans.fi = public/language/fi/pages.json trans.fr = public/language/fr/pages.json trans.gl = public/language/gl/pages.json @@ -583,7 +583,7 @@ trans.ms = public/language/ms/pages.json trans.nb = public/language/nb/pages.json trans.nl = public/language/nl/pages.json trans.pl = public/language/pl/pages.json -trans.pt_BR = public/language/pt_BR/pages.json +trans.pt_BR = public/language/pt-BR/pages.json trans.ru = public/language/ru/pages.json trans.ro = public/language/ro/pages.json trans.rw = public/language/rw/pages.json @@ -595,13 +595,13 @@ trans.sv = public/language/sv/pages.json trans.th = public/language/th/pages.json trans.tr = public/language/tr/pages.json trans.vi = public/language/vi/pages.json -trans.zh_CN = public/language/zh_CN/pages.json -trans.zh_TW = public/language/zh_TW/pages.json +trans.zh_CN = public/language/zh-CN/pages.json +trans.zh_TW = public/language/zh-TW/pages.json type = KEYVALUEJSON [nodebb.topic] file_filter = public/language//topic.json -source_file = public/language/en_GB/topic.json +source_file = public/language/en-GB/topic.json source_lang = en_GB trans.ar = public/language/ar/topic.json trans.bn = public/language/bn/topic.json @@ -610,11 +610,11 @@ trans.cs = public/language/cs/topic.json trans.da = public/language/da/topic.json trans.de = public/language/de/topic.json trans.el = public/language/el/topic.json -trans.en_US = public/language/en_US/topic.json -trans.en@pirate = public/language/en@pirate/topic.json +trans.en_US = public/language/en-US/topic.json +trans.en@pirate = public/language/en-x-pirate/topic.json trans.es = public/language/es/topic.json trans.et = public/language/et/topic.json -trans.fa_IR = public/language/fa_IR/topic.json +trans.fa_IR = public/language/fa-IR/topic.json trans.fi = public/language/fi/topic.json trans.fr = public/language/fr/topic.json trans.gl = public/language/gl/topic.json @@ -629,7 +629,7 @@ trans.ms = public/language/ms/topic.json trans.nb = public/language/nb/topic.json trans.nl = public/language/nl/topic.json trans.pl = public/language/pl/topic.json -trans.pt_BR = public/language/pt_BR/topic.json +trans.pt_BR = public/language/pt-BR/topic.json trans.ru = public/language/ru/topic.json trans.ro = public/language/ro/topic.json trans.rw = public/language/rw/topic.json @@ -641,13 +641,13 @@ trans.sv = public/language/sv/topic.json trans.th = public/language/th/topic.json trans.tr = public/language/tr/topic.json trans.vi = public/language/vi/topic.json -trans.zh_CN = public/language/zh_CN/topic.json -trans.zh_TW = public/language/zh_TW/topic.json +trans.zh_CN = public/language/zh-CN/topic.json +trans.zh_TW = public/language/zh-TW/topic.json type = KEYVALUEJSON [nodebb.success] file_filter = public/language//success.json -source_file = public/language/en_GB/success.json +source_file = public/language/en-GB/success.json source_lang = en_GB trans.ar = public/language/ar/success.json trans.bn = public/language/bn/success.json @@ -656,11 +656,11 @@ trans.cs = public/language/cs/success.json trans.da = public/language/da/success.json trans.de = public/language/de/success.json trans.el = public/language/el/success.json -trans.en_US = public/language/en_US/success.json -trans.en@pirate = public/language/en@pirate/success.json +trans.en_US = public/language/en-US/success.json +trans.en@pirate = public/language/en-x-pirate/success.json trans.es = public/language/es/success.json trans.et = public/language/et/success.json -trans.fa_IR = public/language/fa_IR/success.json +trans.fa_IR = public/language/fa-IR/success.json trans.fi = public/language/fi/success.json trans.fr = public/language/fr/success.json trans.gl = public/language/gl/success.json @@ -675,7 +675,7 @@ trans.ms = public/language/ms/success.json trans.nb = public/language/nb/success.json trans.nl = public/language/nl/success.json trans.pl = public/language/pl/success.json -trans.pt_BR = public/language/pt_BR/success.json +trans.pt_BR = public/language/pt-BR/success.json trans.ru = public/language/ru/success.json trans.ro = public/language/ro/success.json trans.rw = public/language/rw/success.json @@ -687,13 +687,13 @@ trans.sv = public/language/sv/success.json trans.th = public/language/th/success.json trans.tr = public/language/tr/success.json trans.vi = public/language/vi/success.json -trans.zh_CN = public/language/zh_CN/success.json -trans.zh_TW = public/language/zh_TW/success.json +trans.zh_CN = public/language/zh-CN/success.json +trans.zh_TW = public/language/zh-TW/success.json type = KEYVALUEJSON [nodebb.error] file_filter = public/language//error.json -source_file = public/language/en_GB/error.json +source_file = public/language/en-GB/error.json source_lang = en_GB trans.ar = public/language/ar/error.json trans.bn = public/language/bn/error.json @@ -702,11 +702,11 @@ trans.cs = public/language/cs/error.json trans.da = public/language/da/error.json trans.de = public/language/de/error.json trans.el = public/language/el/error.json -trans.en_US = public/language/en_US/error.json -trans.en@pirate = public/language/en@pirate/error.json +trans.en_US = public/language/en-US/error.json +trans.en@pirate = public/language/en-x-pirate/error.json trans.es = public/language/es/error.json trans.et = public/language/et/error.json -trans.fa_IR = public/language/fa_IR/error.json +trans.fa_IR = public/language/fa-IR/error.json trans.fi = public/language/fi/error.json trans.fr = public/language/fr/error.json trans.gl = public/language/gl/error.json @@ -721,7 +721,7 @@ trans.ms = public/language/ms/error.json trans.nb = public/language/nb/error.json trans.nl = public/language/nl/error.json trans.pl = public/language/pl/error.json -trans.pt_BR = public/language/pt_BR/error.json +trans.pt_BR = public/language/pt-BR/error.json trans.ru = public/language/ru/error.json trans.ro = public/language/ro/error.json trans.rw = public/language/rw/error.json @@ -733,13 +733,13 @@ trans.sv = public/language/sv/error.json trans.th = public/language/th/error.json trans.tr = public/language/tr/error.json trans.vi = public/language/vi/error.json -trans.zh_CN = public/language/zh_CN/error.json -trans.zh_TW = public/language/zh_TW/error.json +trans.zh_CN = public/language/zh-CN/error.json +trans.zh_TW = public/language/zh-TW/error.json type = KEYVALUEJSON [nodebb.tags] file_filter = public/language//tags.json -source_file = public/language/en_GB/tags.json +source_file = public/language/en-GB/tags.json source_lang = en_GB trans.ar = public/language/ar/tags.json trans.bn = public/language/bn/tags.json @@ -748,11 +748,11 @@ trans.cs = public/language/cs/tags.json trans.da = public/language/da/tags.json trans.de = public/language/de/tags.json trans.el = public/language/el/tags.json -trans.en_US = public/language/en_US/tags.json -trans.en@pirate = public/language/en@pirate/tags.json +trans.en_US = public/language/en-US/tags.json +trans.en@pirate = public/language/en-x-pirate/tags.json trans.es = public/language/es/tags.json trans.et = public/language/et/tags.json -trans.fa_IR = public/language/fa_IR/tags.json +trans.fa_IR = public/language/fa-IR/tags.json trans.fi = public/language/fi/tags.json trans.fr = public/language/fr/tags.json trans.gl = public/language/gl/tags.json @@ -767,7 +767,7 @@ trans.ms = public/language/ms/tags.json trans.nb = public/language/nb/tags.json trans.nl = public/language/nl/tags.json trans.pl = public/language/pl/tags.json -trans.pt_BR = public/language/pt_BR/tags.json +trans.pt_BR = public/language/pt-BR/tags.json trans.ru = public/language/ru/tags.json trans.ro = public/language/ro/tags.json trans.rw = public/language/rw/tags.json @@ -779,13 +779,13 @@ trans.sv = public/language/sv/tags.json trans.th = public/language/th/tags.json trans.tr = public/language/tr/tags.json trans.vi = public/language/vi/tags.json -trans.zh_CN = public/language/zh_CN/tags.json -trans.zh_TW = public/language/zh_TW/tags.json +trans.zh_CN = public/language/zh-CN/tags.json +trans.zh_TW = public/language/zh-TW/tags.json type = KEYVALUEJSON [nodebb.email] file_filter = public/language//email.json -source_file = public/language/en_GB/email.json +source_file = public/language/en-GB/email.json source_lang = en_GB trans.ar = public/language/ar/email.json trans.bn = public/language/bn/email.json @@ -794,11 +794,11 @@ trans.cs = public/language/cs/email.json trans.da = public/language/da/email.json trans.de = public/language/de/email.json trans.el = public/language/el/email.json -trans.en_US = public/language/en_US/email.json -trans.en@pirate = public/language/en@pirate/email.json +trans.en_US = public/language/en-US/email.json +trans.en@pirate = public/language/en-x-pirate/email.json trans.es = public/language/es/email.json trans.et = public/language/et/email.json -trans.fa_IR = public/language/fa_IR/email.json +trans.fa_IR = public/language/fa-IR/email.json trans.fi = public/language/fi/email.json trans.fr = public/language/fr/email.json trans.gl = public/language/gl/email.json @@ -813,7 +813,7 @@ trans.ms = public/language/ms/email.json trans.nb = public/language/nb/email.json trans.nl = public/language/nl/email.json trans.pl = public/language/pl/email.json -trans.pt_BR = public/language/pt_BR/email.json +trans.pt_BR = public/language/pt-BR/email.json trans.ru = public/language/ru/email.json trans.ro = public/language/ro/email.json trans.rw = public/language/rw/email.json @@ -825,13 +825,13 @@ trans.sv = public/language/sv/email.json trans.th = public/language/th/email.json trans.tr = public/language/tr/email.json trans.vi = public/language/vi/email.json -trans.zh_CN = public/language/zh_CN/email.json -trans.zh_TW = public/language/zh_TW/email.json +trans.zh_CN = public/language/zh-CN/email.json +trans.zh_TW = public/language/zh-TW/email.json type = KEYVALUEJSON [nodebb.search] file_filter = public/language//search.json -source_file = public/language/en_GB/search.json +source_file = public/language/en-GB/search.json source_lang = en_GB trans.ar = public/language/ar/search.json trans.bn = public/language/bn/search.json @@ -840,11 +840,11 @@ trans.cs = public/language/cs/search.json trans.da = public/language/da/search.json trans.de = public/language/de/search.json trans.el = public/language/el/search.json -trans.en_US = public/language/en_US/search.json -trans.en@pirate = public/language/en@pirate/search.json +trans.en_US = public/language/en-US/search.json +trans.en@pirate = public/language/en-x-pirate/search.json trans.es = public/language/es/search.json trans.et = public/language/et/search.json -trans.fa_IR = public/language/fa_IR/search.json +trans.fa_IR = public/language/fa-IR/search.json trans.fi = public/language/fi/search.json trans.fr = public/language/fr/search.json trans.gl = public/language/gl/search.json @@ -859,7 +859,7 @@ trans.ms = public/language/ms/search.json trans.nb = public/language/nb/search.json trans.nl = public/language/nl/search.json trans.pl = public/language/pl/search.json -trans.pt_BR = public/language/pt_BR/search.json +trans.pt_BR = public/language/pt-BR/search.json trans.ru = public/language/ru/search.json trans.ro = public/language/ro/search.json trans.rw = public/language/rw/search.json @@ -871,13 +871,13 @@ trans.sv = public/language/sv/search.json trans.th = public/language/th/search.json trans.tr = public/language/tr/search.json trans.vi = public/language/vi/search.json -trans.zh_CN = public/language/zh_CN/search.json -trans.zh_TW = public/language/zh_TW/search.json +trans.zh_CN = public/language/zh-CN/search.json +trans.zh_TW = public/language/zh-TW/search.json type = KEYVALUEJSON [nodebb.groups] file_filter = public/language//groups.json -source_file = public/language/en_GB/groups.json +source_file = public/language/en-GB/groups.json source_lang = en_GB trans.ar = public/language/ar/groups.json trans.bn = public/language/bn/groups.json @@ -886,11 +886,11 @@ trans.cs = public/language/cs/groups.json trans.da = public/language/da/groups.json trans.de = public/language/de/groups.json trans.el = public/language/el/groups.json -trans.en_US = public/language/en_US/groups.json -trans.en@pirate = public/language/en@pirate/groups.json +trans.en_US = public/language/en-US/groups.json +trans.en@pirate = public/language/en-x-pirate/groups.json trans.es = public/language/es/groups.json trans.et = public/language/et/groups.json -trans.fa_IR = public/language/fa_IR/groups.json +trans.fa_IR = public/language/fa-IR/groups.json trans.fi = public/language/fi/groups.json trans.fr = public/language/fr/groups.json trans.gl = public/language/gl/groups.json @@ -905,7 +905,7 @@ trans.ms = public/language/ms/groups.json trans.nb = public/language/nb/groups.json trans.nl = public/language/nl/groups.json trans.pl = public/language/pl/groups.json -trans.pt_BR = public/language/pt_BR/groups.json +trans.pt_BR = public/language/pt-BR/groups.json trans.ru = public/language/ru/groups.json trans.ro = public/language/ro/groups.json trans.rw = public/language/rw/groups.json @@ -917,13 +917,13 @@ trans.sv = public/language/sv/groups.json trans.th = public/language/th/groups.json trans.tr = public/language/tr/groups.json trans.vi = public/language/vi/groups.json -trans.zh_CN = public/language/zh_CN/groups.json -trans.zh_TW = public/language/zh_TW/groups.json +trans.zh_CN = public/language/zh-CN/groups.json +trans.zh_TW = public/language/zh-TW/groups.json type = KEYVALUEJSON [nodebb.uploads] file_filter = public/language//uploads.json -source_file = public/language/en_GB/uploads.json +source_file = public/language/en-GB/uploads.json source_lang = en_GB trans.ar = public/language/ar/uploads.json trans.bn = public/language/bn/uploads.json @@ -932,11 +932,11 @@ trans.cs = public/language/cs/uploads.json trans.da = public/language/da/uploads.json trans.de = public/language/de/uploads.json trans.el = public/language/el/uploads.json -trans.en_US = public/language/en_US/uploads.json -trans.en@pirate = public/language/en@pirate/uploads.json +trans.en_US = public/language/en-US/uploads.json +trans.en@pirate = public/language/en-x-pirate/uploads.json trans.es = public/language/es/uploads.json trans.et = public/language/et/uploads.json -trans.fa_IR = public/language/fa_IR/uploads.json +trans.fa_IR = public/language/fa-IR/uploads.json trans.fi = public/language/fi/uploads.json trans.fr = public/language/fr/uploads.json trans.gl = public/language/gl/uploads.json @@ -951,7 +951,7 @@ trans.ms = public/language/ms/uploads.json trans.nb = public/language/nb/uploads.json trans.nl = public/language/nl/uploads.json trans.pl = public/language/pl/uploads.json -trans.pt_BR = public/language/pt_BR/uploads.json +trans.pt_BR = public/language/pt-BR/uploads.json trans.ru = public/language/ru/uploads.json trans.ro = public/language/ro/uploads.json trans.rw = public/language/rw/uploads.json @@ -963,6 +963,6 @@ trans.sv = public/language/sv/uploads.json trans.th = public/language/th/uploads.json trans.tr = public/language/tr/uploads.json trans.vi = public/language/vi/uploads.json -trans.zh_CN = public/language/zh_CN/uploads.json -trans.zh_TW = public/language/zh_TW/uploads.json +trans.zh_CN = public/language/zh-CN/uploads.json +trans.zh_TW = public/language/zh-TW/uploads.json type = KEYVALUEJSON \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 9f1585f301..be761a16cf 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,8 +2,9 @@ var fork = require('child_process').fork, env = process.env, - worker, - incomplete = []; + worker, updateWorker, + incomplete = [], + running = 0; module.exports = function (grunt) { @@ -19,38 +20,43 @@ module.exports = function (grunt) { time = Date.now(); if (target === 'lessUpdated_Client') { - fromFile = ['js', 'tpl', 'acpLess']; - compiling = 'clientLess'; + compiling = 'clientCSS'; } else if (target === 'lessUpdated_Admin') { - fromFile = ['js', 'tpl', 'clientLess']; - compiling = 'acpLess'; + compiling = 'acpCSS'; } else if (target === 'clientUpdated') { - fromFile = ['clientLess', 'acpLess', 'tpl']; compiling = 'js'; } else if (target === 'templatesUpdated') { - fromFile = ['js', 'clientLess', 'acpLess']; compiling = 'tpl'; } else if (target === 'serverUpdated') { - fromFile = ['clientLess', 'acpLess', 'js', 'tpl']; + // Do nothing, just restart } - fromFile = fromFile.filter(function (ext) { - return incomplete.indexOf(ext) === -1; - }); + if (incomplete.indexOf(compiling) === -1) { + incomplete.push(compiling); + } - updateArgs.push('--from-file=' + fromFile.join(',')); - incomplete.push(compiling); + updateArgs.push('--build'); + updateArgs.push(incomplete.join(',')); worker.kill(); - worker = fork('app.js', updateArgs, { env: env }); + if (updateWorker) { + updateWorker.kill('SIGKILL'); + } + updateWorker = fork('app.js', updateArgs, { env: env }); + ++running; + updateWorker.on('exit', function () { + --running; + if (running === 0) { + worker = fork('app.js', args, { env: env }); + worker.on('message', function () { + if (incomplete.length) { + incomplete = []; - worker.on('message', function () { - if (incomplete.length) { - incomplete = []; - - if (grunt.option('verbose')) { - grunt.log.writeln('NodeBB restarted in ' + (Date.now() - time) + ' ms'); - } + if (grunt.option('verbose')) { + grunt.log.writeln('NodeBB restarted in ' + (Date.now() - time) + ' ms'); + } + } + }); } }); } diff --git a/README.md b/README.md index 07906badb5..3667684b36 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,18 @@ Additional functionality is enabled through the use of third-party plugins. -* [Get NodeBB](http://www.nodebb.org/ "NodeBB") * [Demo & Meta Discussion](http://community.nodebb.org) * [Documentation & Installation Instructions](http://docs.nodebb.org) * [Help translate NodeBB](https://www.transifex.com/projects/p/nodebb/) * [NodeBB Blog](http://blog.nodebb.org) -* [Join us on IRC](https://kiwiirc.com/client/irc.freenode.net/nodebb) - #nodebb on Freenode +* [Premium Hosting for NodeBB](http://www.nodebb.org/ "NodeBB") * [Follow us on Twitter](http://www.twitter.com/NodeBB/ "NodeBB Twitter") * [Like us on Facebook](http://www.facebook.com/NodeBB/ "NodeBB Facebook") ## Screenshots +NodeBB's theming engine is highly flexible and does not restrict your design choices. Check out some themed installs in these screenshots below: + [![](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/Ud1LrfIb.png)](http://i.imgur.com/Ud1LrfI.png) @@ -31,6 +32,8 @@ Additional functionality is enabled through the use of third-party plugins. [![](http://i.imgur.com/LmHtPhob.png)](http://i.imgur.com/LmHtPho.png) [![](http://i.imgur.com/paiJPJkb.jpg)](http://i.imgur.com/paiJPJk.jpg) +Our minimalist "Persona" theme gets you going right away, no coding experience required. + [![](http://i.imgur.com/HwNEXGu.png)](http://i.imgur.com/HwNEXGu.png) [![](http://i.imgur.com/II1byYs.png)](http://i.imgur.com/II1byYs.png) @@ -47,7 +50,7 @@ Additional functionality is enabled through the use of third-party plugins. NodeBB requires the following software to be installed: -* A version of Node.js at least 4 or greater +* A version of Node.js at least 4 or greater ([installation/upgrade instructions](https://github.com/nodesource/distributions)) * 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) diff --git a/app.js b/app.js index b2397f3080..b3fe8f0fc2 100644 --- a/app.js +++ b/app.js @@ -23,13 +23,12 @@ var nconf = require('nconf'); nconf.argv().env('__'); -var url = require('url'), - async = require('async'), - winston = require('winston'), - colors = require('colors'), - path = require('path'), - pkg = require('./package.json'), - file = require('./src/file'); +var url = require('url'); +var async = require('async'); +var winston = require('winston'); +var path = require('path'); +var pkg = require('./package.json'); +var file = require('./src/file'); global.env = process.env.NODE_ENV || 'production'; @@ -38,11 +37,11 @@ winston.add(winston.transports.Console, { colorize: true, timestamp: function () { var date = new Date(); - return (global.env === 'production') ? date.toJSON() : date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; + return (!!nconf.get('json-logging')) ? date.toJSON() : date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; }, level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose'), - json: (global.env === 'production'), - stringify: (global.env === 'production') + json: (!!nconf.get('json-logging')), + stringify: (!!nconf.get('json-logging')) }); @@ -79,11 +78,13 @@ if (nconf.get('setup') || nconf.get('install')) { activate(); } else if (nconf.get('plugins')) { listPlugins(); +} else if (nconf.get('build')) { + require('./build').build(nconf.get('build')); } else { start(); } -function loadConfig() { +function loadConfig(callback) { winston.verbose('* using configuration stored in: %s', configFile); nconf.file({ @@ -110,6 +111,10 @@ function loadConfig() { if (nconf.get('url')) { nconf.set('url_parsed', url.parse(nconf.get('url'))); } + + if (typeof callback === 'function') { + callback(); + } } @@ -153,25 +158,11 @@ function start() { return; } var meta = require('./src/meta'); - var emitter = require('./src/emitter'); + switch (message.action) { case 'reload': meta.reload(); break; - case 'js-propagate': - meta.js.target = message.data; - emitter.emit('meta:js.compiled'); - winston.verbose('[cluster] Client-side javascript and mapping propagated to worker %s', process.pid); - break; - case 'css-propagate': - meta.css.cache = message.cache; - meta.css.acpCache = message.acpCache; - emitter.emit('meta:css.compiled'); - winston.verbose('[cluster] Stylesheets propagated to worker %s', process.pid); - break; - case 'templates:compiled': - emitter.emit('templates:compiled'); - break; } }); @@ -209,7 +200,7 @@ function start() { require('./src/user').startJobs(); } - webserver.listen(); + webserver.listen(next); } ], function (err) { if (err) { @@ -227,17 +218,19 @@ function start() { winston.warn(' ./nodebb upgrade'); break; default: - if (err.stacktrace !== false) { - winston.error(err.stack); - } else { - winston.error(err.message); - } + winston.error(err); break; } // Either way, bad stuff happened. Abort start. process.exit(); } + + if (process.send) { + process.send({ + action: 'listening' + }); + } }); } @@ -245,12 +238,20 @@ function setup() { winston.info('NodeBB Setup Triggered via Command Line'); var install = require('./src/install'); + var build = require('./build'); 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('Press enter to accept the default setting (shown in brackets).\n'); - install.setup(function (err, data) { + async.series([ + async.apply(install.setup), + async.apply(loadConfig), + async.apply(build.build, true) + ], function (err, data) { + // Disregard build step data + data = data[0]; + var separator = ' '; if (process.stdout.columns > 10) { for(var x = 0,cols = process.stdout.columns - 10; x < cols; x++) { @@ -282,14 +283,23 @@ function setup() { } function upgrade() { - require('./src/database').init(function (err) { + var db = require('./src/database'); + var meta = require('./src/meta'); + var upgrade = require('./src/upgrade'); + var build = require('./build'); + + async.series([ + async.apply(db.init), + async.apply(meta.configs.init), + async.apply(upgrade.upgrade), + async.apply(build.build, true) + ], function (err) { if (err) { winston.error(err.stack); - process.exit(); + process.exit(1); + } else { + process.exit(0); } - require('./src/meta').configs.init(function () { - require('./src/upgrade').upgrade(); - }); }); } diff --git a/build.js b/build.js new file mode 100644 index 0000000000..4ae2d34c77 --- /dev/null +++ b/build.js @@ -0,0 +1,109 @@ +'use strict'; + +var async = require('async'); +var winston = require('winston'); + +var buildStart; + +exports.build = function build(targets, callback) { + buildStart = Date.now(); + + var db = require('./src/database'); + var meta = require('./src/meta'); + var plugins = require('./src/plugins'); + var valid = ['js', 'clientCSS', 'acpCSS', 'tpl']; + + targets = (targets === true ? valid : targets.split(',').filter(function (target) { + return valid.indexOf(target) !== -1; + })); + + if (!targets) { + winston.error('[build] No valid build targets found. Aborting.'); + return process.exit(0); + } + + async.series([ + async.apply(db.init), + async.apply(meta.themes.setupPaths), + async.apply(plugins.prepareForBuild) + ], function (err) { + if (err) { + winston.error('[build] Encountered error preparing for build: ' + err.message); + return process.exit(1); + } + + exports.buildTargets(targets, callback); + }); +}; + +exports.buildTargets = function (targets, callback) { + var meta = require('./src/meta'); + buildStart = buildStart || Date.now(); + + var step = function (startTime, target, next) { + winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's'); + next(); + }; + + async.parallel([ + function (next) { + if (targets.indexOf('js') !== -1) { + winston.info('[build] Building javascript'); + var startTime = Date.now(); + async.series([ + async.apply(meta.js.minify, 'nodebb.min.js'), + async.apply(meta.js.minify, 'acp.min.js') + ], step.bind(this, startTime, 'js', next)); + } else { + setImmediate(next); + } + }, + function (next) { + async.eachSeries(targets, function (target, next) { + var startTime; + switch(target) { + case 'js': + setImmediate(next); + break; + case 'clientCSS': + winston.info('[build] Building client-side CSS'); + startTime = Date.now(); + meta.css.minify('stylesheet.css', step.bind(this, startTime, target, next)); + break; + + case 'acpCSS': + winston.info('[build] Building admin control panel CSS'); + startTime = Date.now(); + meta.css.minify('admin.css', step.bind(this, startTime, target, next)); + break; + + case 'tpl': + winston.info('[build] Building templates'); + startTime = Date.now(); + meta.templates.compile(step.bind(this, startTime, target, next)); + break; + + default: + winston.warn('[build] Unknown build target: \'' + target + '\''); + setImmediate(next); + break; + } + }, next); + } + ], function (err) { + if (err) { + winston.error('[build] Encountered error during build step: ' + err.message); + return process.exit(1); + } + + var time = (Date.now() - buildStart) / 1000; + + winston.info('[build] Asset compilation successful. Completed in ' + time + 's.'); + + if (typeof callback === 'function') { + callback(); + } else { + process.exit(0); + } + }); +}; \ No newline at end of file diff --git a/install/web.js b/install/web.js index 72284fea5f..b81fdeb545 100644 --- a/install/web.js +++ b/install/web.js @@ -124,11 +124,6 @@ function launch(req, res) { } function compileLess(callback) { - if ((nconf.get('from-file') || '').indexOf('less') !== -1) { - winston.info('LESS compilation skipped'); - return callback(false); - } - fs.readFile(path.join(__dirname, '../public/less/install.less'), function (err, style) { if (err) { return winston.error('Unable to read LESS install file: ', err); @@ -145,11 +140,6 @@ function compileLess(callback) { } function compileJS(callback) { - if ((nconf.get('from-file') || '').indexOf('js') !== -1) { - winston.info('Client-side JS compilation skipped'); - return callback(false); - } - var scriptPath = path.join(__dirname, '..'); var result = uglify.minify(scripts.map(function (script) { return path.join(scriptPath, script); diff --git a/loader.js b/loader.js index 02a668c174..df528871e9 100644 --- a/loader.js +++ b/loader.js @@ -22,15 +22,7 @@ var pidFilePath = __dirname + '/pidfile', workers = [], Loader = { - timesStarted: 0, - js: { - target: {} - }, - css: { - cache: undefined, - acpCache: undefined - }, - templatesCompiled: false + timesStarted: 0 }; Loader.init = function (callback) { @@ -86,30 +78,6 @@ Loader.addWorkerEvents = function (worker) { worker.on('message', function (message) { if (message && typeof message === 'object' && message.action) { switch (message.action) { - case 'ready': - if (Loader.js.target['nodebb.min.js'] && Loader.js.target['acp.min.js'] && !worker.isPrimary) { - worker.send({ - action: 'js-propagate', - data: Loader.js.target - }); - } - - if (Loader.css.cache && !worker.isPrimary) { - worker.send({ - action: 'css-propagate', - cache: Loader.css.cache, - acpCache: Loader.css.acpCache - }); - } - - if (Loader.templatesCompiled && !worker.isPrimary) { - worker.send({ - action: 'templates:compiled' - }); - } - - - break; case 'restart': console.log('[cluster] Restarting...'); Loader.restart(); @@ -118,31 +86,6 @@ Loader.addWorkerEvents = function (worker) { console.log('[cluster] Reloading...'); Loader.reload(); break; - case 'js-propagate': - Loader.js.target = message.data; - - Loader.notifyWorkers({ - action: 'js-propagate', - data: message.data - }, worker.pid); - break; - case 'css-propagate': - Loader.css.cache = message.cache; - Loader.css.acpCache = message.acpCache; - - Loader.notifyWorkers({ - action: 'css-propagate', - cache: message.cache, - acpCache: message.acpCache - }, worker.pid); - break; - case 'templates:compiled': - Loader.templatesCompiled = true; - - Loader.notifyWorkers({ - action: 'templates:compiled', - }, worker.pid); - break; } } }); @@ -163,6 +106,7 @@ Loader.start = function (callback) { function forkWorker(index, isPrimary) { var ports = getPorts(); + var args = []; if(!ports[index]) { return console.log('[cluster] invalid port for worker : ' + index + ' ports: ' + ports.length); @@ -172,7 +116,7 @@ function forkWorker(index, isPrimary) { process.env.isCluster = ports.length > 1 ? true : false; process.env.port = ports[index]; - var worker = fork('app.js', [], { + var worker = fork('app.js', args, { silent: silent, env: process.env }); diff --git a/nodebb b/nodebb index 342032fe1a..62115e110b 100755 --- a/nodebb +++ b/nodebb @@ -335,6 +335,13 @@ switch(process.argv[2]) { }); break; + case 'build': + var args = process.argv.slice(0); + args[2] = '--' + args[2]; + + fork(args); + break; + case 'setup': cproc.fork('app.js', ['--setup'], { cwd: __dirname, @@ -401,13 +408,14 @@ switch(process.argv[2]) { default: process.stdout.write('\nWelcome to NodeBB\n\n'.bold); - process.stdout.write('Usage: ./nodebb {start|stop|reload|restart|log|setup|reset|upgrade|dev}\n\n'); + process.stdout.write('Usage: ./nodebb {start|slog|stop|reload|restart|log|build|setup|reset|upgrade|dev}\n\n'); process.stdout.write('\t' + 'start'.yellow + '\t\tStart the NodeBB server\n'); process.stdout.write('\t' + 'slog'.yellow + '\t\tStarts the NodeBB server and displays the live output log\n'); process.stdout.write('\t' + 'stop'.yellow + '\t\tStops the NodeBB server\n'); process.stdout.write('\t' + 'reload'.yellow + '\t\tRestarts NodeBB\n'); process.stdout.write('\t' + 'restart'.yellow + '\t\tRestarts NodeBB\n'); process.stdout.write('\t' + 'log'.yellow + '\t\tOpens the logging interface (useful for debugging)\n'); + process.stdout.write('\t' + 'build'.yellow + '\t\tCompiles javascript, css stylesheets, and templates\n'); process.stdout.write('\t' + 'setup'.yellow + '\t\tRuns the NodeBB setup script\n'); process.stdout.write('\t' + 'reset'.yellow + '\t\tDisables all plugins, restores the default theme.\n'); process.stdout.write('\t' + 'activate'.yellow + '\tActivates a plugin for the next startup of NodeBB.\n'); diff --git a/package.json b/package.json index 654c45bff8..b03549529a 100644 --- a/package.json +++ b/package.json @@ -52,17 +52,17 @@ "morgan": "^1.3.2", "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "4.2.13", - "nodebb-plugin-dbsearch": "1.0.3", + "nodebb-plugin-composer-default": "4.3.0", + "nodebb-plugin-dbsearch": "1.0.4", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.1.5", - "nodebb-plugin-markdown": "6.0.2", + "nodebb-plugin-markdown": "7.0.1", "nodebb-plugin-mentions": "1.1.3", "nodebb-plugin-soundpack-default": "0.1.6", "nodebb-plugin-spam-be-gone": "0.4.10", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "3.0.15", - "nodebb-theme-persona": "4.1.86", + "nodebb-theme-persona": "4.1.88", "nodebb-theme-vanilla": "5.1.56", "nodebb-widget-essentials": "2.0.13", "nodemailer": "2.6.4", @@ -81,9 +81,9 @@ "semver": "^5.1.0", "serve-favicon": "^2.1.5", "sitemap": "^1.4.0", - "socket.io": "^1.4.8", - "socket.io-client": "^1.4.0", - "socket.io-redis": "1.1.1", + "socket.io": "1.7.1", + "socket.io-client": "1.7.1", + "socket.io-redis": "2.0.0", "socketio-wildcard": "~0.3.0", "string": "^3.0.0", "templates.js": "0.3.4", diff --git a/public/language/ar/user.json b/public/language/ar/user.json index b3515464d8..1387e8b2bb 100644 --- a/public/language/ar/user.json +++ b/public/language/ar/user.json @@ -31,7 +31,8 @@ "signature": "توقيع", "birthday": "عيد ميلاد", "chat": "محادثة", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "تابع", "unfollow": "إلغاء المتابعة", "more": "المزيد", diff --git a/public/language/bg/user.json b/public/language/bg/user.json index c9e8d97b78..61b6fa839d 100644 --- a/public/language/bg/user.json +++ b/public/language/bg/user.json @@ -31,7 +31,8 @@ "signature": "Подпис", "birthday": "Рождена дата", "chat": "Разговор", - "chat_with": "Разговор с %1", + "chat_with": "Продължаване на разговора с %1", + "new_chat_with": "Започване на нов разговор с %1", "follow": "Следване", "unfollow": "Спиране на следването", "more": "Още", diff --git a/public/language/bn/user.json b/public/language/bn/user.json index 40aa4b92a2..296614eeb8 100644 --- a/public/language/bn/user.json +++ b/public/language/bn/user.json @@ -31,7 +31,8 @@ "signature": "স্বাক্ষর", "birthday": "জন্মদিন", "chat": "বার্তালাপ", - "chat_with": "চ্যাট উইথ %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "অনুসরন করুন", "unfollow": "অনুসরন করা থেকে বিরত থাকুন", "more": "আরো...", diff --git a/public/language/cs/user.json b/public/language/cs/user.json index 7aff6b7cbf..8f25d1a1d1 100644 --- a/public/language/cs/user.json +++ b/public/language/cs/user.json @@ -31,7 +31,8 @@ "signature": "Podpis", "birthday": "Datum narození", "chat": "Chat", - "chat_with": "Chatovat s %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Sledovat", "unfollow": "Nesledovat", "more": "Více", diff --git a/public/language/da/user.json b/public/language/da/user.json index c47328889b..a83e677248 100644 --- a/public/language/da/user.json +++ b/public/language/da/user.json @@ -31,7 +31,8 @@ "signature": "Signatur", "birthday": "Fødselsdag", "chat": "Chat", - "chat_with": "Chat med %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Følg", "unfollow": "Følg ikke", "more": "Mere", diff --git a/public/language/de/user.json b/public/language/de/user.json index 2b8e884fa8..5d861943c4 100644 --- a/public/language/de/user.json +++ b/public/language/de/user.json @@ -31,7 +31,8 @@ "signature": "Signatur", "birthday": "Geburtstag", "chat": "Chat", - "chat_with": "Chat mit %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Folgen", "unfollow": "Nicht mehr folgen", "more": "Mehr", diff --git a/public/language/el/user.json b/public/language/el/user.json index de5f4aa56b..796edb79b7 100644 --- a/public/language/el/user.json +++ b/public/language/el/user.json @@ -31,7 +31,8 @@ "signature": "Υπογραφή", "birthday": "Γενέθλια", "chat": "Συνομιλία", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Ακολούθησε", "unfollow": "Μην Ακολουθείς", "more": "More", diff --git a/public/language/en_GB/category.json b/public/language/en-GB/category.json similarity index 100% rename from public/language/en_GB/category.json rename to public/language/en-GB/category.json diff --git a/public/language/en_GB/email.json b/public/language/en-GB/email.json similarity index 100% rename from public/language/en_GB/email.json rename to public/language/en-GB/email.json diff --git a/public/language/en_GB/error.json b/public/language/en-GB/error.json similarity index 100% rename from public/language/en_GB/error.json rename to public/language/en-GB/error.json diff --git a/public/language/en_GB/global.json b/public/language/en-GB/global.json similarity index 100% rename from public/language/en_GB/global.json rename to public/language/en-GB/global.json diff --git a/public/language/en_GB/groups.json b/public/language/en-GB/groups.json similarity index 100% rename from public/language/en_GB/groups.json rename to public/language/en-GB/groups.json diff --git a/public/language/en_GB/language.json b/public/language/en-GB/language.json similarity index 77% rename from public/language/en_GB/language.json rename to public/language/en-GB/language.json index 41d8e9a6c2..dab8ef1141 100644 --- a/public/language/en_GB/language.json +++ b/public/language/en-GB/language.json @@ -1,5 +1,5 @@ { "name": "English (United Kingdom/Canada)", - "code": "en_GB", + "code": "en-GB", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/en_GB/login.json b/public/language/en-GB/login.json similarity index 100% rename from public/language/en_GB/login.json rename to public/language/en-GB/login.json diff --git a/public/language/en_GB/modules.json b/public/language/en-GB/modules.json similarity index 100% rename from public/language/en_GB/modules.json rename to public/language/en-GB/modules.json diff --git a/public/language/en_GB/notifications.json b/public/language/en-GB/notifications.json similarity index 100% rename from public/language/en_GB/notifications.json rename to public/language/en-GB/notifications.json diff --git a/public/language/en_GB/pages.json b/public/language/en-GB/pages.json similarity index 100% rename from public/language/en_GB/pages.json rename to public/language/en-GB/pages.json diff --git a/public/language/en_GB/recent.json b/public/language/en-GB/recent.json similarity index 100% rename from public/language/en_GB/recent.json rename to public/language/en-GB/recent.json diff --git a/public/language/en_GB/register.json b/public/language/en-GB/register.json similarity index 100% rename from public/language/en_GB/register.json rename to public/language/en-GB/register.json diff --git a/public/language/en_GB/reset_password.json b/public/language/en-GB/reset_password.json similarity index 100% rename from public/language/en_GB/reset_password.json rename to public/language/en-GB/reset_password.json diff --git a/public/language/en_GB/search.json b/public/language/en-GB/search.json similarity index 100% rename from public/language/en_GB/search.json rename to public/language/en-GB/search.json diff --git a/public/language/en_GB/success.json b/public/language/en-GB/success.json similarity index 100% rename from public/language/en_GB/success.json rename to public/language/en-GB/success.json diff --git a/public/language/en_GB/tags.json b/public/language/en-GB/tags.json similarity index 100% rename from public/language/en_GB/tags.json rename to public/language/en-GB/tags.json diff --git a/public/language/en_GB/topic.json b/public/language/en-GB/topic.json similarity index 100% rename from public/language/en_GB/topic.json rename to public/language/en-GB/topic.json diff --git a/public/language/en_GB/unread.json b/public/language/en-GB/unread.json similarity index 100% rename from public/language/en_GB/unread.json rename to public/language/en-GB/unread.json diff --git a/public/language/en_GB/uploads.json b/public/language/en-GB/uploads.json similarity index 100% rename from public/language/en_GB/uploads.json rename to public/language/en-GB/uploads.json diff --git a/public/language/en_GB/user.json b/public/language/en-GB/user.json similarity index 100% rename from public/language/en_GB/user.json rename to public/language/en-GB/user.json diff --git a/public/language/en_GB/users.json b/public/language/en-GB/users.json similarity index 100% rename from public/language/en_GB/users.json rename to public/language/en-GB/users.json diff --git a/public/language/en_US/category.json b/public/language/en-US/category.json similarity index 100% rename from public/language/en_US/category.json rename to public/language/en-US/category.json diff --git a/public/language/en_US/email.json b/public/language/en-US/email.json similarity index 100% rename from public/language/en_US/email.json rename to public/language/en-US/email.json diff --git a/public/language/en@pirate/error.json b/public/language/en-US/error.json similarity index 100% rename from public/language/en@pirate/error.json rename to public/language/en-US/error.json diff --git a/public/language/en_US/global.json b/public/language/en-US/global.json similarity index 100% rename from public/language/en_US/global.json rename to public/language/en-US/global.json diff --git a/public/language/en_US/groups.json b/public/language/en-US/groups.json similarity index 100% rename from public/language/en_US/groups.json rename to public/language/en-US/groups.json diff --git a/public/language/en_US/language.json b/public/language/en-US/language.json similarity index 73% rename from public/language/en_US/language.json rename to public/language/en-US/language.json index 64fa5738f6..0967664491 100644 --- a/public/language/en_US/language.json +++ b/public/language/en-US/language.json @@ -1,5 +1,5 @@ { "name": "English (United States)", - "code": "en_US", + "code": "en-US", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/en_US/login.json b/public/language/en-US/login.json similarity index 100% rename from public/language/en_US/login.json rename to public/language/en-US/login.json diff --git a/public/language/en_US/modules.json b/public/language/en-US/modules.json similarity index 100% rename from public/language/en_US/modules.json rename to public/language/en-US/modules.json diff --git a/public/language/en_US/notifications.json b/public/language/en-US/notifications.json similarity index 100% rename from public/language/en_US/notifications.json rename to public/language/en-US/notifications.json diff --git a/public/language/en@pirate/pages.json b/public/language/en-US/pages.json similarity index 100% rename from public/language/en@pirate/pages.json rename to public/language/en-US/pages.json diff --git a/public/language/en_US/recent.json b/public/language/en-US/recent.json similarity index 100% rename from public/language/en_US/recent.json rename to public/language/en-US/recent.json diff --git a/public/language/en_US/register.json b/public/language/en-US/register.json similarity index 100% rename from public/language/en_US/register.json rename to public/language/en-US/register.json diff --git a/public/language/en@pirate/reset_password.json b/public/language/en-US/reset_password.json similarity index 100% rename from public/language/en@pirate/reset_password.json rename to public/language/en-US/reset_password.json diff --git a/public/language/en@pirate/search.json b/public/language/en-US/search.json similarity index 100% rename from public/language/en@pirate/search.json rename to public/language/en-US/search.json diff --git a/public/language/en@pirate/success.json b/public/language/en-US/success.json similarity index 100% rename from public/language/en@pirate/success.json rename to public/language/en-US/success.json diff --git a/public/language/en@pirate/tags.json b/public/language/en-US/tags.json similarity index 100% rename from public/language/en@pirate/tags.json rename to public/language/en-US/tags.json diff --git a/public/language/en@pirate/topic.json b/public/language/en-US/topic.json similarity index 100% rename from public/language/en@pirate/topic.json rename to public/language/en-US/topic.json diff --git a/public/language/en_US/unread.json b/public/language/en-US/unread.json similarity index 100% rename from public/language/en_US/unread.json rename to public/language/en-US/unread.json diff --git a/public/language/en_US/uploads.json b/public/language/en-US/uploads.json similarity index 100% rename from public/language/en_US/uploads.json rename to public/language/en-US/uploads.json diff --git a/public/language/en_US/user.json b/public/language/en-US/user.json similarity index 98% rename from public/language/en_US/user.json rename to public/language/en-US/user.json index 05002d86af..00fc3826df 100644 --- a/public/language/en_US/user.json +++ b/public/language/en-US/user.json @@ -31,7 +31,8 @@ "signature": "Signature", "birthday": "Birthday", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Follow", "unfollow": "Unfollow", "more": "More", diff --git a/public/language/en_US/users.json b/public/language/en-US/users.json similarity index 100% rename from public/language/en_US/users.json rename to public/language/en-US/users.json diff --git a/public/language/en@pirate/category.json b/public/language/en-x-pirate/category.json similarity index 100% rename from public/language/en@pirate/category.json rename to public/language/en-x-pirate/category.json diff --git a/public/language/en@pirate/email.json b/public/language/en-x-pirate/email.json similarity index 100% rename from public/language/en@pirate/email.json rename to public/language/en-x-pirate/email.json diff --git a/public/language/en_US/error.json b/public/language/en-x-pirate/error.json similarity index 100% rename from public/language/en_US/error.json rename to public/language/en-x-pirate/error.json diff --git a/public/language/en@pirate/global.json b/public/language/en-x-pirate/global.json similarity index 100% rename from public/language/en@pirate/global.json rename to public/language/en-x-pirate/global.json diff --git a/public/language/en@pirate/groups.json b/public/language/en-x-pirate/groups.json similarity index 100% rename from public/language/en@pirate/groups.json rename to public/language/en-x-pirate/groups.json diff --git a/public/language/en@pirate/language.json b/public/language/en-x-pirate/language.json similarity index 65% rename from public/language/en@pirate/language.json rename to public/language/en-x-pirate/language.json index 214c4b0c2d..34b9317fb5 100644 --- a/public/language/en@pirate/language.json +++ b/public/language/en-x-pirate/language.json @@ -1,5 +1,5 @@ { "name": "English (Pirate)", - "code": "en@pirate", + "code": "en-x-pirate", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/en@pirate/login.json b/public/language/en-x-pirate/login.json similarity index 100% rename from public/language/en@pirate/login.json rename to public/language/en-x-pirate/login.json diff --git a/public/language/en@pirate/modules.json b/public/language/en-x-pirate/modules.json similarity index 100% rename from public/language/en@pirate/modules.json rename to public/language/en-x-pirate/modules.json diff --git a/public/language/en@pirate/notifications.json b/public/language/en-x-pirate/notifications.json similarity index 100% rename from public/language/en@pirate/notifications.json rename to public/language/en-x-pirate/notifications.json diff --git a/public/language/en_US/pages.json b/public/language/en-x-pirate/pages.json similarity index 100% rename from public/language/en_US/pages.json rename to public/language/en-x-pirate/pages.json diff --git a/public/language/en@pirate/recent.json b/public/language/en-x-pirate/recent.json similarity index 100% rename from public/language/en@pirate/recent.json rename to public/language/en-x-pirate/recent.json diff --git a/public/language/en@pirate/register.json b/public/language/en-x-pirate/register.json similarity index 100% rename from public/language/en@pirate/register.json rename to public/language/en-x-pirate/register.json diff --git a/public/language/en_US/reset_password.json b/public/language/en-x-pirate/reset_password.json similarity index 100% rename from public/language/en_US/reset_password.json rename to public/language/en-x-pirate/reset_password.json diff --git a/public/language/en_US/search.json b/public/language/en-x-pirate/search.json similarity index 100% rename from public/language/en_US/search.json rename to public/language/en-x-pirate/search.json diff --git a/public/language/en_US/success.json b/public/language/en-x-pirate/success.json similarity index 100% rename from public/language/en_US/success.json rename to public/language/en-x-pirate/success.json diff --git a/public/language/en_US/tags.json b/public/language/en-x-pirate/tags.json similarity index 100% rename from public/language/en_US/tags.json rename to public/language/en-x-pirate/tags.json diff --git a/public/language/en_US/topic.json b/public/language/en-x-pirate/topic.json similarity index 100% rename from public/language/en_US/topic.json rename to public/language/en-x-pirate/topic.json diff --git a/public/language/en@pirate/unread.json b/public/language/en-x-pirate/unread.json similarity index 100% rename from public/language/en@pirate/unread.json rename to public/language/en-x-pirate/unread.json diff --git a/public/language/en@pirate/uploads.json b/public/language/en-x-pirate/uploads.json similarity index 100% rename from public/language/en@pirate/uploads.json rename to public/language/en-x-pirate/uploads.json diff --git a/public/language/en@pirate/user.json b/public/language/en-x-pirate/user.json similarity index 98% rename from public/language/en@pirate/user.json rename to public/language/en-x-pirate/user.json index 4ebf6d9984..eec5dc8cd8 100644 --- a/public/language/en@pirate/user.json +++ b/public/language/en-x-pirate/user.json @@ -31,7 +31,8 @@ "signature": "Signature", "birthday": "Birthday", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Follow", "unfollow": "Unfollow", "more": "More", diff --git a/public/language/en@pirate/users.json b/public/language/en-x-pirate/users.json similarity index 100% rename from public/language/en@pirate/users.json rename to public/language/en-x-pirate/users.json diff --git a/public/language/es/user.json b/public/language/es/user.json index 972031128d..7f2b0e6674 100644 --- a/public/language/es/user.json +++ b/public/language/es/user.json @@ -31,7 +31,8 @@ "signature": "Firma", "birthday": "Cumpleaños", "chat": "Chat", - "chat_with": "Chatear con %1", + "chat_with": "Continuar chat con %1", + "new_chat_with": "Empezar chat con %1", "follow": "Seguir", "unfollow": "Dejar de seguir", "more": "Más", diff --git a/public/language/et/user.json b/public/language/et/user.json index f37129994a..e01449cc31 100644 --- a/public/language/et/user.json +++ b/public/language/et/user.json @@ -31,7 +31,8 @@ "signature": "Allkiri", "birthday": "Sünnipäev", "chat": "Vestlus", - "chat_with": "Vestle kasutajaga %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Jälgi", "unfollow": "Ära jälgi enam", "more": "Rohkem", diff --git a/public/language/fa_IR/category.json b/public/language/fa-IR/category.json similarity index 100% rename from public/language/fa_IR/category.json rename to public/language/fa-IR/category.json diff --git a/public/language/fa_IR/email.json b/public/language/fa-IR/email.json similarity index 100% rename from public/language/fa_IR/email.json rename to public/language/fa-IR/email.json diff --git a/public/language/fa_IR/error.json b/public/language/fa-IR/error.json similarity index 100% rename from public/language/fa_IR/error.json rename to public/language/fa-IR/error.json diff --git a/public/language/fa_IR/global.json b/public/language/fa-IR/global.json similarity index 100% rename from public/language/fa_IR/global.json rename to public/language/fa-IR/global.json diff --git a/public/language/fa_IR/groups.json b/public/language/fa-IR/groups.json similarity index 100% rename from public/language/fa_IR/groups.json rename to public/language/fa-IR/groups.json diff --git a/public/language/fa_IR/language.json b/public/language/fa-IR/language.json similarity index 68% rename from public/language/fa_IR/language.json rename to public/language/fa-IR/language.json index c387007d53..53117369f8 100644 --- a/public/language/fa_IR/language.json +++ b/public/language/fa-IR/language.json @@ -1,5 +1,5 @@ { "name": "فارسی", - "code": "fa_IR", + "code": "fa-IR", "dir": "rtl" } \ No newline at end of file diff --git a/public/language/fa_IR/login.json b/public/language/fa-IR/login.json similarity index 100% rename from public/language/fa_IR/login.json rename to public/language/fa-IR/login.json diff --git a/public/language/fa_IR/modules.json b/public/language/fa-IR/modules.json similarity index 100% rename from public/language/fa_IR/modules.json rename to public/language/fa-IR/modules.json diff --git a/public/language/fa_IR/notifications.json b/public/language/fa-IR/notifications.json similarity index 100% rename from public/language/fa_IR/notifications.json rename to public/language/fa-IR/notifications.json diff --git a/public/language/fa_IR/pages.json b/public/language/fa-IR/pages.json similarity index 100% rename from public/language/fa_IR/pages.json rename to public/language/fa-IR/pages.json diff --git a/public/language/fa_IR/recent.json b/public/language/fa-IR/recent.json similarity index 100% rename from public/language/fa_IR/recent.json rename to public/language/fa-IR/recent.json diff --git a/public/language/fa_IR/register.json b/public/language/fa-IR/register.json similarity index 100% rename from public/language/fa_IR/register.json rename to public/language/fa-IR/register.json diff --git a/public/language/fa_IR/reset_password.json b/public/language/fa-IR/reset_password.json similarity index 100% rename from public/language/fa_IR/reset_password.json rename to public/language/fa-IR/reset_password.json diff --git a/public/language/fa_IR/search.json b/public/language/fa-IR/search.json similarity index 100% rename from public/language/fa_IR/search.json rename to public/language/fa-IR/search.json diff --git a/public/language/fa_IR/success.json b/public/language/fa-IR/success.json similarity index 100% rename from public/language/fa_IR/success.json rename to public/language/fa-IR/success.json diff --git a/public/language/fa_IR/tags.json b/public/language/fa-IR/tags.json similarity index 100% rename from public/language/fa_IR/tags.json rename to public/language/fa-IR/tags.json diff --git a/public/language/fa_IR/topic.json b/public/language/fa-IR/topic.json similarity index 100% rename from public/language/fa_IR/topic.json rename to public/language/fa-IR/topic.json diff --git a/public/language/fa_IR/unread.json b/public/language/fa-IR/unread.json similarity index 100% rename from public/language/fa_IR/unread.json rename to public/language/fa-IR/unread.json diff --git a/public/language/fa_IR/uploads.json b/public/language/fa-IR/uploads.json similarity index 100% rename from public/language/fa_IR/uploads.json rename to public/language/fa-IR/uploads.json diff --git a/public/language/fa_IR/user.json b/public/language/fa-IR/user.json similarity index 98% rename from public/language/fa_IR/user.json rename to public/language/fa-IR/user.json index 0c320a6e98..6a92520690 100644 --- a/public/language/fa_IR/user.json +++ b/public/language/fa-IR/user.json @@ -31,7 +31,8 @@ "signature": "امضا", "birthday": "روز تولد", "chat": "چت", - "chat_with": "چت کردن با %1", + "chat_with": "ادامه چت با %1", + "new_chat_with": "شروع چت جدید با %1", "follow": "دنبال کن", "unfollow": "دنبال نکن", "more": "بیشتر", diff --git a/public/language/fa_IR/users.json b/public/language/fa-IR/users.json similarity index 100% rename from public/language/fa_IR/users.json rename to public/language/fa-IR/users.json diff --git a/public/language/fi/user.json b/public/language/fi/user.json index f445bd1e7d..7fe5ad7de0 100644 --- a/public/language/fi/user.json +++ b/public/language/fi/user.json @@ -31,7 +31,8 @@ "signature": "Allekirjoitus", "birthday": "Syntymäpäivä", "chat": "Keskustele", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Seuraa", "unfollow": "Älä seuraa", "more": "More", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index ee051ba871..8bf9995e41 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -29,7 +29,7 @@ "username-too-long": "Nom d'utilisateur trop long", "password-too-long": "Mot de passe trop long", "user-banned": "Utilisateur banni", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Désolé, ce compte a été banni (Raison : %1)", "user-too-new": "Désolé, vous devez attendre encore %1 seconde(s) avant d'envoyer votre premier message", "blacklisted-ip": "Désolé, votre adresse IP a été bannie de cette communauté. Si vous pensez que c'est une erreur, veuillez contacter un administrateur.", "ban-expiry-missing": "Veuillez entrer une date de fin de banissement.", diff --git a/public/language/fr/global.json b/public/language/fr/global.json index 4cb94114ea..d45e801270 100644 --- a/public/language/fr/global.json +++ b/public/language/fr/global.json @@ -100,7 +100,7 @@ "unsaved-changes": "Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir naviguer tout de même ?", "reconnecting-message": "Il semble que votre connexion ait été perdue, veuillez patienter pendant que nous vous re-connectons.", "play": "Lire", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "cookies.message": "Ce site utilise des cookies pour vous permettre d'avoir la meilleure expérience possible.", + "cookies.accept": "Compris !", + "cookies.learn_more": "En savoir plus" } \ No newline at end of file diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json index eb3604f04e..8d4144760e 100644 --- a/public/language/fr/topic.json +++ b/public/language/fr/topic.json @@ -13,7 +13,7 @@ "notify_me": "Être notifié des réponses dans ce sujet", "quote": "Citer", "reply": "Répondre", - "replies_to_this_post": "Replies: %1", + "replies_to_this_post": "Réponses : %1", "reply-as-topic": "Répondre à l'aide d'un sujet", "guest-login-reply": "Se connecter pour répondre", "edit": "Éditer", diff --git a/public/language/fr/user.json b/public/language/fr/user.json index 98cb990439..6f9dc88759 100644 --- a/public/language/fr/user.json +++ b/public/language/fr/user.json @@ -31,7 +31,8 @@ "signature": "Signature", "birthday": "Anniversaire", "chat": "Discussion", - "chat_with": "Discussion avec %1", + "chat_with": "Continuer la discussion avec %1", + "new_chat_with": "Commencer une nouvelle discussion avec %1", "follow": "S'abonner", "unfollow": "Se désabonner", "more": "Plus", diff --git a/public/language/gl/category.json b/public/language/gl/category.json index 4d97b7dd45..d1668273a3 100644 --- a/public/language/gl/category.json +++ b/public/language/gl/category.json @@ -6,7 +6,7 @@ "no_topics": "Non hai temas nesta categoría.
Por que non abres un?", "browsing": "vendo agora", "no_replies": "Ninguén respondeu", - "no_new_posts": "Non hai novas publicacións.", + "no_new_posts": "Non hai publicacións novas.", "share_this_category": "Comparte esta categoría", "watch": "Vixiar", "ignore": "Ignorar", diff --git a/public/language/gl/email.json b/public/language/gl/email.json index 9d13746d08..7bcedbf37e 100644 --- a/public/language/gl/email.json +++ b/public/language/gl/email.json @@ -5,8 +5,8 @@ "greeting_no_name": "Ola", "greeting_with_name": "Ola %1", "welcome.text1": "Grazas por rexistrarte %1!", - "welcome.text2": "Para activar a túa conta, precisamos que a verifiques o enderezo de correo electrónico co que te rexistraches. ", - "welcome.text3": "Un administrador aceptou a súa solicitude de rexistro. Agora pódeste conectar co teu nome de usuario e contrasinal. ", + "welcome.text2": "Para activar a túa conta, precisamos que a verifiques co enderezo de correo electrónico co que te rexistraches. ", + "welcome.text3": "Un administrador aceptou a túa solicitude de rexistro. Agora pódeste conectar co teu nome de usuario e contrasinal. ", "welcome.cta": "Fai clic aquí para confirmar o teu enderezo de correo electrónico ", "invitation.text1": "%1 convidoute a unirte %2", "invitation.ctr": "Pica aquí para crear a túa conta.", @@ -14,7 +14,7 @@ "reset.text2": "Para continuar co reincio do contrasinal, por favor pica no seguinte ligazón:", "reset.cta": "Pica aquí para reiniciar o teu contrasinal", "reset.notify.subject": "Contrasinal cambiado", - "reset.notify.text1": "Estámoslle a notificar que nun %1, o seu contrasinal foi cambiado correctamente.", + "reset.notify.text1": "Estámosche a notificar que nun %1, o seu contrasinal foi cambiado correctamente.", "reset.notify.text2": "Se ti non autorizache isto, por favor notifica inmediatamente a un administrador.", "digest.notifications": "Tes unha notificación non lida de %1:", "digest.latest_topics": "Últimos temas de %1", diff --git a/public/language/gl/error.json b/public/language/gl/error.json index 3df65a1567..86c84601f6 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -14,7 +14,7 @@ "invalid-password": "Contrasinal Inválido", "invalid-username-or-password": "Especifica ámbolos dous por favor, nome de usuario e contrasinal", "invalid-search-term": "Termo de búsqueda inválido", - "csrf-invalid": "Non fomos capaces de entrar, probablemente debido a que a sesión expirou. Por favor, téntao de novo", + "csrf-invalid": "Non fomos capaces de entrar, probablemente porque a que a sesión expirou. Por favor, téntao de novo", "invalid-pagination-value": "Valor de paxinación incorreto, ten que estar entre %1 e %2", "username-taken": "Nome de usuario en uso", "email-taken": "Enderezo electrónico en uso", @@ -29,7 +29,7 @@ "username-too-long": "Nome de usuario demasiado longo.", "password-too-long": "Contrasinal moi longa", "user-banned": "Usuario expulsado", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Desculpa, esta conta foi baneada (Razón: %1)", "user-too-new": "Desculpa, agarda %1 second(s) antes de facer a túa primeira publicación.", "blacklisted-ip": "Sentímolo, o teu enderezo IP foi baneado desta comunidade. Se crees que se debe a un erro, por favor, contacte cun administrador.", "ban-expiry-missing": "Por favor, engade unha data de fin do ban", diff --git a/public/language/gl/global.json b/public/language/gl/global.json index 245508a2b6..1efd67853e 100644 --- a/public/language/gl/global.json +++ b/public/language/gl/global.json @@ -16,8 +16,8 @@ "please_log_in": "Por favor, conéctate", "logout": "Desconectarse", "posting_restriction_info": "As publicacións están restrinxidas a membros rexistrados, pica aquí para rexistrarte.", - "welcome_back": "Benvido de novo", - "you_have_successfully_logged_in": "Fuches rexistrado con éxito.", + "welcome_back": "Benvido de novo!", + "you_have_successfully_logged_in": "Sentidiño!", "save_changes": "Gardar Cambios", "save": "Gardar", "close": "Pechar ", @@ -60,7 +60,7 @@ "views": "Vistas", "reputation": "Reputación", "read_more": "ler máis", - "more": "máis", + "more": "Máis", "posted_ago_by_guest": "Publicado %1 por Invitado", "posted_ago_by": "Publicado %1 por %2", "posted_ago": "Publicado %1", @@ -100,7 +100,7 @@ "unsaved-changes": "Non gardaches tódolos cambios. Queres continuar e saír da páxina?", "reconnecting-message": "Conexión perdida. Reconectando a %1.", "play": "Reproducir", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "cookies.message": "Esta web emprega cookies para asegurar que recibes unha mellor experiencia de navegación.", + "cookies.accept": "De Acordo!", + "cookies.learn_more": "Saber máis" } \ No newline at end of file diff --git a/public/language/gl/topic.json b/public/language/gl/topic.json index 8c1de7740e..9b29bb6b69 100644 --- a/public/language/gl/topic.json +++ b/public/language/gl/topic.json @@ -13,7 +13,7 @@ "notify_me": "Serás notificado canto haxa novas respostas neste tema", "quote": "Citar", "reply": "Responder", - "replies_to_this_post": "Replies: %1", + "replies_to_this_post": "Respostas: %1", "reply-as-topic": "Responder como tema", "guest-login-reply": "Identifícate para responder", "edit": "Editar", diff --git a/public/language/gl/user.json b/public/language/gl/user.json index 235222da8d..8f2017b185 100644 --- a/public/language/gl/user.json +++ b/public/language/gl/user.json @@ -31,7 +31,8 @@ "signature": "Firma", "birthday": "Aniversario", "chat": "Chat", - "chat_with": "Parolando con %1", + "chat_with": "Continuar a falar con %1", + "new_chat_with": "Comezar a falar con %1", "follow": "Seguir", "unfollow": "Deixar de seguir", "more": "máis", diff --git a/public/language/he/user.json b/public/language/he/user.json index 014fc5053e..40e3ce5bb8 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -31,7 +31,8 @@ "signature": "חתימה", "birthday": "יום הולדת", "chat": "צ'אט", - "chat_with": "צ'אט עם %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "עקוב", "unfollow": "הפסק לעקוב", "more": "עוד", diff --git a/public/language/hu/email.json b/public/language/hu/email.json index df46d7bb63..67f241f750 100644 --- a/public/language/hu/email.json +++ b/public/language/hu/email.json @@ -1,36 +1,36 @@ { - "password-reset-requested": "Jelszó-visszaállítási kérelem - %1!", - "welcome-to": "Üdvözlet a %1-on", + "password-reset-requested": "Jelszó visszaállítási kérelem - %1!", + "welcome-to": "Üdvözlet a(z) %1 oldalon", "invite": "Meghívó a(z) %1 oldalra", - "greeting_no_name": "Üdvözlet", - "greeting_with_name": "Üdvözlet %1", + "greeting_no_name": "Helló", + "greeting_with_name": "Helló %1", "welcome.text1": "Köszönjük a regisztrációt %1!", "welcome.text2": "A fiók aktiválásához ellenőriznünk kell, hogy valós e-mail cím lett e megadva.", "welcome.text3": "Egy adminisztrátor elfogadta a regisztrációdat. Mostantól a felhasználónév/jelszó párosoddal be tudsz lépni.", - "welcome.cta": "Kattintsunk ide az e-mail cím megerősítéséhez", + "welcome.cta": "Kattints ide az e-mail címed megerősítéséhez", "invitation.text1": "%1 meghívott ide: %2", "invitation.ctr": "Kattints ide a fiókod létrehozásához.", - "reset.text1": "Kaptunk egy kérést jelszava visszaállításához, valószínűleg azért, mert elfelejtette azt. Ha ez nem így van, hagyja figyelmen kívül ezt a levelet.", - "reset.text2": "Ha szeretné, hogy továbbra alaphelyzetbe a jelszavát, kérjük kattintson az alábbi linkre:", + "reset.text1": "Kaptunk egy kérést jelszavad visszaállításához, valószínűleg azért, mert elfelejtetted azt. Ha ez nem így van, hagyd figyelmen kívül ezt a levelet.", + "reset.text2": "A jelszó visszaállításának folytatásához kattints az alábbi linkre:", "reset.cta": "Kattints ide a jelszavad visszaállításához", - "reset.notify.subject": "Jelszó sikeresen megváltoztatva", - "reset.notify.text1": "Értesítünk, hogy %1 névhez tartozó jelszavad sikeresen megváltozott.", + "reset.notify.subject": "Jelszó sikeresen módosítva", + "reset.notify.text1": "Értesítünk, hogy a(z) %1 névhez tartozó jelszavad sikeresen megváltozott.", "reset.notify.text2": "Ha nem te voltál az, kérlek értesíts egy adminisztrátort azonnal.", "digest.notifications": "Olvasatlan értesítéseid vannak a következőtől: %1", - "digest.latest_topics": "Legutóbbi témák a következőből: %1", - "digest.cta": "Kattints ide, hogy meglátogasd a következőt: %1", + "digest.latest_topics": "Legutóbbi témakörök a következőből: %1", + "digest.cta": "Kattints ide a(z) %1 meglátogatásához", "digest.unsub.info": "Ez a hírlevél a feliratkozási beállításaid miatt lett kiküldve.", "digest.no_topics": "Nem volt aktív témakör az elmúlt %1", "digest.day": "napban", "digest.week": "héten", "digest.month": "hónapban", - "digest.subject": "Digest for %1", + "digest.subject": "%1 hírlevél", "notif.chat.subject": "Új chat üzenet érkezett a következőtől: %1", "notif.chat.cta": "Kattints ide a beszélgetés folytatásához", "notif.chat.unsub.info": "Ez a chat-értesítés a feliratkozási beállításaid miatt lett kiküldve.", "notif.post.cta": "Kattints ide a teljes téma olvasásához", "notif.post.unsub.info": "Ez a hozzászólás-értesítés a feliratkozási beállításaid miatt lett kiküldve.", - "test.text1": "Ez egy teszt levél, ami által ellenőrizzük, hogy a levelező helyesen lett beállítva a fórumodon.", - "unsub.cta": "Kattintson ide megváltoztatni ezeket a beállításokat", + "test.text1": "Ez egy teszt levél, ami által ellenőrizzük, hogy a levelező helyesen lett beállítva a NodeBB-ben.", + "unsub.cta": "Kattintson ide a beállítások módosításához", "closing": "Köszönjük!" } \ No newline at end of file diff --git a/public/language/hu/global.json b/public/language/hu/global.json index b5b3eac53c..40d584921d 100644 --- a/public/language/hu/global.json +++ b/public/language/hu/global.json @@ -1,19 +1,19 @@ { "home": "Kezdőlap", "search": "Keresés", - "buttons.close": "Mégsem", + "buttons.close": "Bezárás", "403.title": "Hozzáférés megtagadva", "403.message": "Úgy tűnik, hogy rábukkantál egy olyan oldalra, amihez nincs hozzáférésed.", - "403.login": "Talán meg kellene próbálnod belépni?", + "403.login": "Talán próbálj meg belépni?", "404.title": "Nincs találat", "404.message": "Úgy tűnik, hogy rábukkantál egy olyan oldalra ami nem létezik. Visszatérés a kezdőlapra", "500.title": "Belső hiba.", "500.message": "Hoppá! Úgy tűnik valami hiba történt!", "400.title": "Hibás kérelem.", "400.message": "Úgy látszik, a hivatkozás formátuma hibás, ellenőrizd és próbáld újra. Egyéb esetben térj vissza a kezdőoldalra.", - "register": "Regisztráció", + "register": "Regisztrálás", "login": "Belépés", - "please_log_in": "Jelentkezzünk be", + "please_log_in": "Jelentkezz be", "logout": "Kijelentkezés", "posting_restriction_info": "A hozzászólás regisztrációhoz kötött, kérlek kattints ide a bejelentkezéshez.", "welcome_back": "Üdvözlet", @@ -22,7 +22,7 @@ "save": "Mentés", "close": "Bezárás", "pagination": "Lapozás", - "pagination.out_of": "%1 - %2", + "pagination.out_of": "%1 / %2", "pagination.enter_index": "Írj be egy számot", "header.admin": "Admin", "header.categories": "Kategóriák", @@ -37,11 +37,11 @@ "header.search": "Keresés", "header.profile": "Profil", "header.navigation": "Navigáció", - "notifications.loading": "Értesítések Betöltése", - "chats.loading": "Chat Betöltése", + "notifications.loading": "Értesítések betöltése", + "chats.loading": "Chat betöltése", "motd.welcome": "Üdvözlet a NodeBB-n, a jövő fórum platformján.", - "previouspage": "Előző Oldal", - "nextpage": "Következő Oldal", + "previouspage": "Előző oldal", + "nextpage": "Következő oldal", "alert.success": "Sikeres", "alert.error": "Hiba", "alert.banned": "Tiltva", @@ -53,34 +53,34 @@ "topics": "Témák", "posts": "Hozzászólások", "best": "Legjobb", - "upvoters": "Upvoters", - "upvoted": "Upvoted", - "downvoters": "Downvoters", - "downvoted": "Downvoted", + "upvoters": "Kedvelők", + "upvoted": "Kedvel", + "downvoters": "Nem kedvelők", + "downvoted": "Nem kedvel", "views": "Megtekintések", "reputation": "Hírnév", "read_more": "tovább olvas", - "more": "More", - "posted_ago_by_guest": "posted %1 by Guest", - "posted_ago_by": "posted %1 by %2", - "posted_ago": "posted %1", - "posted_in": "posted in %1", - "posted_in_by": "posted in %1 by %2", - "posted_in_ago": "posted in %1 %2", - "posted_in_ago_by": "posted in %1 %2 by %3", - "user_posted_ago": "%1 posted %2", - "guest_posted_ago": "Guest posted %1", - "last_edited_by": "last edited by %1", + "more": "Több", + "posted_ago_by_guest": "Vendég hozzászólás %1", + "posted_ago_by": "%2 hozzászólás %1", + "posted_ago": "%1 hozzászólás", + "posted_in": "hozzászólt itt: %1", + "posted_in_by": "%2 hozzászólt itt: %1", + "posted_in_ago": "hozzászólva: %1, %2", + "posted_in_ago_by": "%3 hozzászólt: %1, %2", + "user_posted_ago": "%1 hozzászólt %2", + "guest_posted_ago": "Vendég hozzászólás %1", + "last_edited_by": "utoljára %1 szerkesztette", "norecentposts": "Nincs legutóbbi hozzászólás", "norecenttopics": "Nincs friss téma", "recentposts": "Friss hozzászólások", "recentips": "Utoljára bejelentkezett IP címek", "moderator_tools": "Moderátori eszközök", - "away": "Távol van", + "away": "Nincs a gépnél", "dnd": "Ne zavarj", "invisible": "Láthatatlan", "offline": "Offline", - "email": "Email", + "email": "E-mail", "language": "Nyelv", "guest": "Vendég", "guests": "Vendég", @@ -91,16 +91,16 @@ "unfollow": "Nem követem", "delete_all": "Összes törlése", "map": "Térkép", - "sessions": "Login Sessions", + "sessions": "Belépési munkamenetek", "ip_address": "IP-cím", - "enter_page_number": "Enter page number", + "enter_page_number": "Oldalszám megadása", "upload_file": "Fájl feltöltése", "upload": "Feltöltés", "allowed-file-types": "Támogatott fájltípusok: %1", "unsaved-changes": "Mentetlen módosításaid vannak. Biztos el akarsz innen menni?", "reconnecting-message": "Úgy látszik, a(z) %1 csatlakozásod megszakadt, várj, míg megpróbáljuk helyreállítani.", - "play": "Play", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "play": "Lejátszás", + "cookies.message": "A weboldal sütiket használ, a legjobb weboldalas élmény érdekében.", + "cookies.accept": "Értem!", + "cookies.learn_more": "Tudnivalók" } \ No newline at end of file diff --git a/public/language/hu/language.json b/public/language/hu/language.json index 7c6d5e8fb0..486b16d227 100644 --- a/public/language/hu/language.json +++ b/public/language/hu/language.json @@ -1,5 +1,5 @@ { - "name": "Magyar Nyelv", - "code": "hu", + "name": "Magyar (Hungarian)", + "code": "hu_HU", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/hu/recent.json b/public/language/hu/recent.json index a656abe7ce..bddd57893f 100644 --- a/public/language/hu/recent.json +++ b/public/language/hu/recent.json @@ -1,11 +1,11 @@ { - "title": "Legújabb", + "title": "Legrissebb", "day": "Nap", "week": "Hét", "month": "Hónap", "year": "Év", "alltime": "Minden idők", - "no_recent_topics": "Nincs friss topik.", + "no_recent_topics": "Nincs friss témakör.", "no_popular_topics": "Nincs népszerű témakör.", "there-is-a-new-topic": "Van egy új témakör.", "there-is-a-new-topic-and-a-new-post": "Van egy új témakör és új hozzászólás.", diff --git a/public/language/hu/register.json b/public/language/hu/register.json index e6f17fff96..2540521ac2 100644 --- a/public/language/hu/register.json +++ b/public/language/hu/register.json @@ -2,18 +2,18 @@ "register": "Regisztráció", "cancel_registration": "Regisztráció megszakítása", "help.email": "Az e-mail cím nem lesz közzétéve.", - "help.username_restrictions": "Egyedi felhasználói név %1 és %2 karakterek között. A többiek megemlíthetnek az alábbi módon @nicknév.", + "help.username_restrictions": "Egyedi felhasználói név %1 és %2 karakterek között. A többiek az alábbi módon említhetnek meg: @nicknév.", "help.minimum_password_length": "A jelszónak legalább %1 karakter hosszúnak kell lennie.", "email_address": "E-mail cím", - "email_address_placeholder": "Írd be az e-mail címed", - "username": "Felhasználó Név", - "username_placeholder": "Írd be a nickneved", + "email_address_placeholder": "Add meg az e-mail címed", + "username": "Felhasználónév", + "username_placeholder": "Felhasználónév megadása", "password": "Jelszó", - "password_placeholder": "Írd be a jelszavad", + "password_placeholder": "Jelszó megadása", "confirm_password": "Jelszó megerősítése", "confirm_password_placeholder": "Jelszó megerősítése", - "register_now_button": "Regisztrálás Most", - "alternative_registration": "Alternatív Regisztráció", + "register_now_button": "Regisztrálás", + "alternative_registration": "Alternatív regisztráció", "terms_of_use": "Használtai feltételek", "agree_to_terms_of_use": "Egyetértek a Használtai Feltételekkel", "terms_of_use_error": "El kell fogadnod a használati feltételeket", diff --git a/public/language/hu/reset_password.json b/public/language/hu/reset_password.json index a9eedc958e..71e2868191 100644 --- a/public/language/hu/reset_password.json +++ b/public/language/hu/reset_password.json @@ -1,16 +1,16 @@ { - "reset_password": "Jelszó visszaállítás", - "update_password": "Jelszó Frissítés", - "password_changed.title": "Jelszó megváltozott", + "reset_password": "Jelszó visszaállítása", + "update_password": "Jelszó frissítése", + "password_changed.title": "A jelszó megváltozott", "password_changed.message": "

A jelszavad sikereresen visszaállítva, kérlek lép be újra.", "wrong_reset_code.title": "Helytelen visszaállítási-kód", "wrong_reset_code.message": "A visszaállítási-kód helytelen. Kérlek próbáld újra, vagy kérj egy új kódot.", "new_password": "Új jelszó", "repeat_password": "Jelszó megerősítése", "enter_email": "Kérlek add meg az e-mail címedet, ahová elküldjük a további teendőket a jelszavad visszaállításával kapcsolatban.", - "enter_email_address": "Email cím megadása", - "password_reset_sent": "Jelszó-visszaállítás elküldve", - "invalid_email": "Helytelen E-mail cím / Nem létező E-mail cím!", + "enter_email_address": "E-mail cím megadása", + "password_reset_sent": "Jelszó visszaállítás elküldve", + "invalid_email": "Helytelen e-mail cím / Nem létező e-mail cím!", "password_too_short": "A megadott jelszó túl rövid, válassz másik jelszót.", "passwords_do_not_match": "A két megadott jelszó nem egyezik.", "password_expired": "Lejárt a jelszavad, válassz új jelszót." diff --git a/public/language/hu/success.json b/public/language/hu/success.json index 9a1d318187..f1b3f19230 100644 --- a/public/language/hu/success.json +++ b/public/language/hu/success.json @@ -1,6 +1,6 @@ { "success": "Sikeres", - "topic-post": "A bejegyzés sikeresen beküldésre került.", + "topic-post": "Sikeres hozzászólás.", "authentication-successful": "Sikeres hitelesítés", "settings-saved": "Beállítások mentve!" } \ No newline at end of file diff --git a/public/language/hu/unread.json b/public/language/hu/unread.json index 9e87a05671..35b797c482 100644 --- a/public/language/hu/unread.json +++ b/public/language/hu/unread.json @@ -2,9 +2,9 @@ "title": "Olvasatlan", "no_unread_topics": "Nincs olvasatlan témakör.", "load_more": "Több betöltése", - "mark_as_read": "Olvasottnak jelölés", + "mark_as_read": "Megjelölés olvasottként", "selected": "Kiválasztva", - "all": "Összes", + "all": "Mind", "all_categories": "Minden kategória", "topics_marked_as_read.success": "Témakör olvasottnak jelölve!", "all-topics": "Minden témakör", diff --git a/public/language/hu/user.json b/public/language/hu/user.json index fbfc423747..f8edd1f335 100644 --- a/public/language/hu/user.json +++ b/public/language/hu/user.json @@ -2,51 +2,52 @@ "banned": "Kitíltva", "offline": "Nem elérhető", "username": "Felhasználónév", - "joindate": "Regisztráció dátum", + "joindate": "Regisztráció dátuma", "postcount": "Bejegyzés megtekintés", "email": "E-mail", "confirm_email": "E-mail megerősítése", - "account_info": "Account Info", - "ban_account": "Ban Account", - "ban_account_confirm": "Do you really want to ban this user?", + "account_info": "Fiók információ", + "ban_account": "Fiók tiltása", + "ban_account_confirm": "Biztos ki akarod tiltani a felhasználót?", "unban_account": "Fiók feloldása", "delete_account": "Fiók törlése", - "delete_account_confirm": "Biztosan törölni szeretnéd a fiókodat?
Ez a művelet nem visszafordítható, így ha folytatod, nem tudod majd visszaállítani az adataidat.

Amennyiben továbbra is törölni szeretnéd a fiókodat, add meg a felhasználónevedet!", - "delete_this_account_confirm": "Are you sure you want to delete this account?
This action is irreversible and you will not be able to recover any data

", + "delete_account_confirm": "Biztosan törölni szeretnéd a fiókodat?
Ez a művelet nem visszafordítható, így ha folytatod, nem tudod majd visszaállítani az adataidat.

A fiókod törlésének megerősítéséhez add meg a felhasználónevedet!", + "delete_this_account_confirm": "iztosan törölni szeretnéd a fiókodat?
Ez a művelet nem visszafordítható, így ha folytatod, nem tudod majd visszaállítani az adataidat.
", "account-deleted": "Fiók törölve", - "fullname": "Teljes Név", + "fullname": "Teljes név", "website": "Weboldal", "location": "Lakhely", "age": "Kor", "joined": "Csatlakozott", - "lastonline": "Utoljára Online", + "lastonline": "Utoljára elérhető", "profile": "Profil", "profile_views": "Megtekintések", "reputation": "Hírnév", "bookmarks": "Könyvjelzők", - "watched": "Megfigyeli", + "watched": "Megtekintve", "followers": "Követők", "following": "Követve", "aboutme": "Rólam", "signature": "Aláírás", - "birthday": "Szülinap", + "birthday": "Születésnap", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Chat folytatása %1 felhasználóval", + "new_chat_with": "Új chat indítása %1 felhasználóval", "follow": "Követés", "unfollow": "Nem követem", - "more": "More", + "more": "Több", "profile_update_success": "Profil sikeresen frissítve!", - "change_picture": "Kép megváltoztatása", + "change_picture": "Kép módosítása", "change_username": "Felhasználónév módosítása", "change_email": "E-mail cím módosítása", - "edit": "Szerkeszt", + "edit": "Szerkesztés", "edit-profile": "Profil szerkesztése", "default_picture": "Alapértelmezett ikon", "uploaded_picture": "Feltöltött kép", "upload_new_picture": "Új kép feltöltése", - "upload_new_picture_from_url": "Új kép feltöltése adott URL-ről", + "upload_new_picture_from_url": "Új kép feltöltése adott hivatkozásról", "current_password": "Jelenlegi jelszó", - "change_password": "Jelszó megváltoztatása", + "change_password": "Jelszó módosítása", "change_password_error": "Helytelen jelszó!", "change_password_error_wrong_current": "A jelenlegi jelszavad nem megfelelő!", "change_password_error_length": "A jelszó túl rövid!", @@ -55,7 +56,7 @@ "change_password_success": "A jelszavad frissítve!", "confirm_password": "Jelszó megerősítése", "password": "Jelszó", - "username_taken_workaround": "A kívánt felhasználónév már foglalt, így változtatnunk kellett rajta egy kicsit. Mostantól %1 nicknév alatt vagy ismert.", + "username_taken_workaround": "A kívánt felhasználónév már foglalt, így változtatnunk kellett rajta egy kicsit. Mostantól %1 név alatt vagy ismert.", "password_same_as_username": "A jelszavad megegyezik a felhasználóneveddel, kérlek válassz másik jelszót.", "password_same_as_email": "A jelszavad megegyezik az e-mail címeddel, kérlek válassz másik jelszót.", "upload_picture": "Kép feltöltése", @@ -72,20 +73,20 @@ "digest_daily": "Napi", "digest_weekly": "Heti", "digest_monthly": "Havi", - "send_chat_notifications": "E-mail küldése, amennyiben chat üzenetem érkezett és nem vagyok aktív", - "send_post_notifications": "E-mail küldése, amikor válasz érkezik azokhoz a témákhoz, amelyekre feliratkoztam", - "settings-require-reload": "Néhány módosítás újratöltést igényel. Kattints ide az újratöltéshez.", + "send_chat_notifications": "E-mail küldése, ha új chat üzenetem érkezett és nem vagyok aktív", + "send_post_notifications": "E-mail küldése, ha válasz érkezik a feliratkozott témakörökbe", + "settings-require-reload": "Néhány módosítás újratöltést igényel. Kattints ide az oldal frissítéséhez.", "has_no_follower": "Ezt a felhasználót nem követi senki :(", - "follows_no_one": "Ez a felhasználó nem követ senkit :(", + "follows_no_one": "Ez a felhasználó már nem követ :(", "has_no_posts": "A felhasználó még nem szólt hozzá semmihez.", "has_no_topics": "A felhasználó még nem szólt hozzá egyik témakörhöz sem.", "has_no_watched_topics": "A felhasználó még nem nézett meg egy témakört sem.", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_upvoted_posts": "A felhasználó még egy hozzászólást sem kedvelt.", + "has_no_downvoted_posts": "A felhasználó még egy hozzászólást sem utált.", "has_no_voted_posts": "A felhasználó még nem szavazott hozzászólásra.", "email_hidden": "E-mail rejtett", "hidden": "rejtett", - "paginate_description": "Paginate topics and posts instead of using infinite scroll", + "paginate_description": "Témakörök és hosszászólasok lapozása a végtelen görgetés helyett.", "topics_per_page": "Téma oldalanként", "posts_per_page": "Hozzászólás oldalanként", "notification_sounds": "Hang lejátszása, mikor értesítést kapsz", @@ -94,11 +95,11 @@ "outgoing-message-sound": "Kimenő üzenet hangja", "notification-sound": "Értesítési hang", "no-sound": "Nincs hang", - "browsing": "Browsing Settings", + "browsing": "Böngészési beállítások", "open_links_in_new_tab": "Kimenő hivatkozások megnyitása új lapon", "enable_topic_searching": "Témán belüli keresés bekapcsolása", "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", - "delay_image_loading": "Delay Image Loading", + "delay_image_loading": "Képbetöltési késleltetés", "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", "follow_topics_you_reply_to": "Watch topics that you reply to", diff --git a/public/language/id/user.json b/public/language/id/user.json index fa486a4d85..48f835b5dc 100644 --- a/public/language/id/user.json +++ b/public/language/id/user.json @@ -31,7 +31,8 @@ "signature": "Tanda Pengenal", "birthday": "Hari Lahir", "chat": "Percakapan", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Ikuti", "unfollow": "Tinggalkan", "more": "More", diff --git a/public/language/it/user.json b/public/language/it/user.json index 259aa5ca7c..faa884c131 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -31,7 +31,8 @@ "signature": "Firma", "birthday": "Data di nascita", "chat": "Chat", - "chat_with": "Chatta con %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Segui", "unfollow": "Smetti di seguire", "more": "Altro", diff --git a/public/language/ja/user.json b/public/language/ja/user.json index 14081a8b2b..e4180d8fe1 100644 --- a/public/language/ja/user.json +++ b/public/language/ja/user.json @@ -31,7 +31,8 @@ "signature": "署名", "birthday": "誕生日", "chat": "チャット", - "chat_with": "%1とチャットをする", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "フォロー", "unfollow": "フォロー解除", "more": "つづき", diff --git a/public/language/ko/user.json b/public/language/ko/user.json index aadde8ab0a..5317c21dbc 100644 --- a/public/language/ko/user.json +++ b/public/language/ko/user.json @@ -31,7 +31,8 @@ "signature": "서명", "birthday": "생일", "chat": "채팅", - "chat_with": "%1 님과 대화", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "팔로우", "unfollow": "팔로우 취소", "more": "더 보기", diff --git a/public/language/lt/user.json b/public/language/lt/user.json index 5a484bb03d..6924abc638 100644 --- a/public/language/lt/user.json +++ b/public/language/lt/user.json @@ -31,7 +31,8 @@ "signature": "Parašas", "birthday": "Gimimo diena", "chat": "Susirašinėti", - "chat_with": "Susirašinėti su %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Sekti", "unfollow": "Nesekti", "more": "Daugiau", diff --git a/public/language/ms/user.json b/public/language/ms/user.json index 0857bfea14..bc4e59e3ec 100644 --- a/public/language/ms/user.json +++ b/public/language/ms/user.json @@ -31,7 +31,8 @@ "signature": "Tandatangan", "birthday": "Tarikh lahir", "chat": "Bersembang", - "chat_with": "Sembang dengan %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Ikuti", "unfollow": "Henti mengikuti", "more": "Lagi", diff --git a/public/language/nb/user.json b/public/language/nb/user.json index 5cf658d9c5..68f52d6882 100644 --- a/public/language/nb/user.json +++ b/public/language/nb/user.json @@ -31,7 +31,8 @@ "signature": "Signatur", "birthday": "Bursdag", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Følg", "unfollow": "Avfølg", "more": "Mer", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 4172257db5..850ebe7db9 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -29,7 +29,7 @@ "username-too-long": "Gebruikersnaam is te lang", "password-too-long": "Wachtwoord is te lang", "user-banned": "Gebruiker verbannen", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Sorry, dit account is verbannen (Reden: %1)", "user-too-new": "Helaas, het is een vereiste om %1 seconde(n) te wachten voordat het eerste bericht geplaatst kan worden.", "blacklisted-ip": "Sorry, uw IP-adres is verbannen uit deze community. Als u meent dat dit onterecht is, neem dan contact op met een beheerder.", "ban-expiry-missing": "Geef een einddatum op voor deze ban.", @@ -72,8 +72,8 @@ "still-uploading": "Een moment geduld tot alle bestanden overgebracht zijn...", "file-too-big": "Maximum toegestane bestandsgrootte is %1 kB - probeer een kleiner bestand te verzenden", "guest-upload-disabled": "Uploads voor gasten zijn uitgeschaleld ", - "already-bookmarked": "You have already bookmarked this post", - "already-unbookmarked": "You have already unbookmarked this post", + "already-bookmarked": "Je hebt dit bericht al als favoriet toegevoegd", + "already-unbookmarked": "Je hebt dit bericht al verwijderd uit je favorieten", "cant-ban-other-admins": "Het is niet toegestaan andere beheerders te verbannen!", "cant-remove-last-admin": "Je bent de enige beheerder. Stel eerst een andere gebruiker als beheerder in voordat je jezelf geen beheerder meer maakt.", "cant-delete-admin": "Verwijder administratieve rechten van dit account voordat je probeert deze te verwijderen", diff --git a/public/language/nl/global.json b/public/language/nl/global.json index cdf5fcdb61..cb140d2974 100644 --- a/public/language/nl/global.json +++ b/public/language/nl/global.json @@ -100,7 +100,7 @@ "unsaved-changes": "Je hebt niet opgeslagen wijzigingen aangebracht. Weet je zeker dat je de pagina wilt verlaten?", "reconnecting-message": "Het lijkt erop dat je verbinding naar %1 verloren is gegaan, wacht even terwijl we de verbinding proberen te herstellen.", "play": "Afspelen", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "cookies.message": "Deze website gebruikt cookies om je ervan te verzekeren dat je de beste ervaring krijgt tijdens het gebruik van onze website.", + "cookies.accept": "Begrepen", + "cookies.learn_more": "Meer" } \ No newline at end of file diff --git a/public/language/nl/pages.json b/public/language/nl/pages.json index d8cd881c80..700b5de892 100644 --- a/public/language/nl/pages.json +++ b/public/language/nl/pages.json @@ -7,7 +7,7 @@ "popular-alltime": "De populaire onderwerpen", "recent": "Recente onderwerpen", "flagged-posts": "Ongepaste berichten", - "ip-blacklist": "IP Blacklist", + "ip-blacklist": "IP zwarte lijst", "users/online": "Online Gebruikers", "users/latest": "Meest recente gebruikers", "users/sort-posts": "Gebruikers met de meeste berichten", @@ -37,7 +37,7 @@ "account/posts": "Berichten geplaatst door %1", "account/topics": "Onderwerpen begonnen door %1", "account/groups": "%1's groepen", - "account/bookmarks": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Favoriete Berichten", "account/settings": "Gebruikersinstellingen", "account/watched": "Berichten die door %1 bekeken worden", "account/upvoted": "Berichten omhoog gestemd door %1", diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index d6b32fcdeb..94a652d728 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -13,7 +13,7 @@ "notify_me": "Krijg een melding wanneer nieuwe reacties volgen", "quote": "Citeren", "reply": "Reageren", - "replies_to_this_post": "Replies: %1", + "replies_to_this_post": "Reacties: %1 ", "reply-as-topic": "Reageren als onderwerp", "guest-login-reply": "Aanmelden om te reageren", "edit": "Aanpassen", @@ -90,9 +90,9 @@ "disabled_categories_note": "Uitgeschakelde Categorieën zijn grijs", "confirm_move": "Verplaatsen", "confirm_fork": "Splits", - "bookmark": "Bookmark", - "bookmarks": "Bookmarks", - "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "bookmark": "Favoriet", + "bookmarks": "Favorieten", + "bookmarks.has_no_bookmarks": "Je hebt nog geen berichten aan je favorieten toegevoegd.", "loading_more_posts": "Meer berichten laden...", "move_topic": "Onderwerp verplaatsen", "move_topics": "Verplaats onderwerpen", diff --git a/public/language/nl/user.json b/public/language/nl/user.json index de02a1b8c5..9900701511 100644 --- a/public/language/nl/user.json +++ b/public/language/nl/user.json @@ -23,7 +23,7 @@ "profile": "Profiel", "profile_views": "Bekeken", "reputation": "Reputatie", - "bookmarks": "Bookmarks", + "bookmarks": "Favorieten", "watched": "Bekeken", "followers": "Volgers", "following": "Volgend", @@ -31,7 +31,8 @@ "signature": "Handtekening", "birthday": "Verjaardag", "chat": "Chat", - "chat_with": "Chatten met %1", + "chat_with": "Chat verder met %1", + "new_chat_with": "Begin een chat met %1", "follow": "Volgen", "unfollow": "Ontvolgen", "more": "Meer", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 16bbd15697..5da5652888 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -29,7 +29,7 @@ "username-too-long": "Zbyt długa nazwa użytkownika", "password-too-long": "Hasło jest za długie", "user-banned": "Użytkownik zbanowany", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Twoje konto zostało zablokowane (Powód: %1)", "user-too-new": "Przepraszamy, musisz odczekać %1 sekund(y) przed utworzeniem pierwszego posta", "blacklisted-ip": "Twój adres IP został zablokowany na tej społeczności. Jeśli uważasz to za błąd, zgłoś to administratorowi", "ban-expiry-missing": "Wprowadź datę końca blokady", diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index 01a26e94da..e821a3f114 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -13,7 +13,7 @@ "notify_me": "Powiadamiaj mnie o nowych odpowiedziach w tym temacie", "quote": "Cytuj", "reply": "Odpowiedz", - "replies_to_this_post": "Replies: %1", + "replies_to_this_post": "Odpowiedzi: %1", "reply-as-topic": "Odpowiedz na temat", "guest-login-reply": "Zaloguj się, aby odpowiedzieć.", "edit": "Edytuj", diff --git a/public/language/pl/user.json b/public/language/pl/user.json index 03ef649329..3578277457 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -31,7 +31,8 @@ "signature": "Sygnatura", "birthday": "Urodziny", "chat": "Rozmawiaj", - "chat_with": "Rozmawiaj z %1", + "chat_with": "Kontynuuj rozmowę z %1", + "new_chat_with": "Rozpocznij rozmowę z %1", "follow": "Śledź", "unfollow": "Przestań śledzić", "more": "Więcej", diff --git a/public/language/pt_BR/category.json b/public/language/pt-BR/category.json similarity index 100% rename from public/language/pt_BR/category.json rename to public/language/pt-BR/category.json diff --git a/public/language/pt_BR/email.json b/public/language/pt-BR/email.json similarity index 100% rename from public/language/pt_BR/email.json rename to public/language/pt-BR/email.json diff --git a/public/language/pt_BR/error.json b/public/language/pt-BR/error.json similarity index 100% rename from public/language/pt_BR/error.json rename to public/language/pt-BR/error.json diff --git a/public/language/pt_BR/global.json b/public/language/pt-BR/global.json similarity index 100% rename from public/language/pt_BR/global.json rename to public/language/pt-BR/global.json diff --git a/public/language/pt_BR/groups.json b/public/language/pt-BR/groups.json similarity index 100% rename from public/language/pt_BR/groups.json rename to public/language/pt-BR/groups.json diff --git a/public/language/pt_BR/language.json b/public/language/pt-BR/language.json similarity index 72% rename from public/language/pt_BR/language.json rename to public/language/pt-BR/language.json index 5a353a4610..a506508cc6 100644 --- a/public/language/pt_BR/language.json +++ b/public/language/pt-BR/language.json @@ -1,5 +1,5 @@ { "name": "Português (Brasil)", - "code": "pt_BR", + "code": "pt-BR", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/pt_BR/login.json b/public/language/pt-BR/login.json similarity index 100% rename from public/language/pt_BR/login.json rename to public/language/pt-BR/login.json diff --git a/public/language/pt_BR/modules.json b/public/language/pt-BR/modules.json similarity index 100% rename from public/language/pt_BR/modules.json rename to public/language/pt-BR/modules.json diff --git a/public/language/pt_BR/notifications.json b/public/language/pt-BR/notifications.json similarity index 100% rename from public/language/pt_BR/notifications.json rename to public/language/pt-BR/notifications.json diff --git a/public/language/pt_BR/pages.json b/public/language/pt-BR/pages.json similarity index 100% rename from public/language/pt_BR/pages.json rename to public/language/pt-BR/pages.json diff --git a/public/language/pt_BR/recent.json b/public/language/pt-BR/recent.json similarity index 100% rename from public/language/pt_BR/recent.json rename to public/language/pt-BR/recent.json diff --git a/public/language/pt_BR/register.json b/public/language/pt-BR/register.json similarity index 100% rename from public/language/pt_BR/register.json rename to public/language/pt-BR/register.json diff --git a/public/language/pt_BR/reset_password.json b/public/language/pt-BR/reset_password.json similarity index 100% rename from public/language/pt_BR/reset_password.json rename to public/language/pt-BR/reset_password.json diff --git a/public/language/pt_BR/search.json b/public/language/pt-BR/search.json similarity index 100% rename from public/language/pt_BR/search.json rename to public/language/pt-BR/search.json diff --git a/public/language/pt_BR/success.json b/public/language/pt-BR/success.json similarity index 100% rename from public/language/pt_BR/success.json rename to public/language/pt-BR/success.json diff --git a/public/language/pt_BR/tags.json b/public/language/pt-BR/tags.json similarity index 100% rename from public/language/pt_BR/tags.json rename to public/language/pt-BR/tags.json diff --git a/public/language/pt_BR/topic.json b/public/language/pt-BR/topic.json similarity index 100% rename from public/language/pt_BR/topic.json rename to public/language/pt-BR/topic.json diff --git a/public/language/pt_BR/unread.json b/public/language/pt-BR/unread.json similarity index 100% rename from public/language/pt_BR/unread.json rename to public/language/pt-BR/unread.json diff --git a/public/language/pt_BR/uploads.json b/public/language/pt-BR/uploads.json similarity index 100% rename from public/language/pt_BR/uploads.json rename to public/language/pt-BR/uploads.json diff --git a/public/language/pt_BR/user.json b/public/language/pt-BR/user.json similarity index 98% rename from public/language/pt_BR/user.json rename to public/language/pt-BR/user.json index da3dba46e5..74345ca2d3 100644 --- a/public/language/pt_BR/user.json +++ b/public/language/pt-BR/user.json @@ -31,7 +31,8 @@ "signature": "Assinatura", "birthday": "Aniversário", "chat": "Chat", - "chat_with": "Conversar com %1", + "chat_with": "Continuar a conversa com %1", + "new_chat_with": "Iniciar uma nova conversa com %1", "follow": "Seguir", "unfollow": "Deixar de Seguir", "more": "Mais", diff --git a/public/language/pt_BR/users.json b/public/language/pt-BR/users.json similarity index 100% rename from public/language/pt_BR/users.json rename to public/language/pt-BR/users.json diff --git a/public/language/ro/user.json b/public/language/ro/user.json index d38857c5b0..ce17895230 100644 --- a/public/language/ro/user.json +++ b/public/language/ro/user.json @@ -31,7 +31,8 @@ "signature": "Semnătură", "birthday": "Zi de naștere", "chat": "Conversație", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Urmărește", "unfollow": "Oprește urmărirea", "more": "Mai multe", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index c87d49da8d..7b2d73ccc4 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -29,7 +29,7 @@ "username-too-long": "Имя пользователя слишком длинное", "password-too-long": "Пароль слишком длинный", "user-banned": "Участник заблокирован", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Учетная запись заблокирована (Причина: %1)", "user-too-new": "Вы можете написать своё первое сообщение через %1 сек.", "blacklisted-ip": "Извините, ваш IP адрес был забанен этим сообществом. Если вы считаете, что это ошибка, пожалуйста, свяжитесь с администратором.", "ban-expiry-missing": "Пожалуйста, укажите дату окончания этой блокировки", diff --git a/public/language/ru/global.json b/public/language/ru/global.json index 3564ddfeca..e86cad1624 100644 --- a/public/language/ru/global.json +++ b/public/language/ru/global.json @@ -100,7 +100,7 @@ "unsaved-changes": "У вас есть несохранённые изменения. Вы уверены, что хотите уйти?", "reconnecting-message": "Похоже, подключение к %1 было разорвано, подождите, пока мы пытаемся восстановить соединение.", "play": "Воспроизвести", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "cookies.message": "Этот сайт использует cookies для более удобного взаимодействия.", + "cookies.accept": "Понял", + "cookies.learn_more": "Подробнее" } \ No newline at end of file diff --git a/public/language/ru/user.json b/public/language/ru/user.json index a2fbf3f9a0..3e63279cec 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -31,7 +31,8 @@ "signature": "Подпись", "birthday": "День рождения", "chat": "Чат", - "chat_with": "Чат с участником %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Подписаться", "unfollow": "Отписаться", "more": "Больше", diff --git a/public/language/rw/user.json b/public/language/rw/user.json index e569405b0b..97a41fc438 100644 --- a/public/language/rw/user.json +++ b/public/language/rw/user.json @@ -31,7 +31,8 @@ "signature": "Intero", "birthday": "Itariki y'Amavuko", "chat": "Mu Gikari", - "chat_with": "Ganira na %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Kurikira", "unfollow": "Ntukurikire", "more": "Ibindi", diff --git a/public/language/sc/user.json b/public/language/sc/user.json index ff9e4b9478..003b4d9860 100644 --- a/public/language/sc/user.json +++ b/public/language/sc/user.json @@ -31,7 +31,8 @@ "signature": "Firma", "birthday": "Cumpleannu", "chat": "Tzarra", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Sighi", "unfollow": "Non sighes prus", "more": "More", diff --git a/public/language/sk/category.json b/public/language/sk/category.json index 08e29d0b5a..3813d56908 100644 --- a/public/language/sk/category.json +++ b/public/language/sk/category.json @@ -11,8 +11,8 @@ "watch": "Sledovať", "ignore": "Ignorovať", "watching": "Sledovať", - "ignoring": "Ignorované", - "watching.description": "Zobraziť témy v neprečítaných", + "ignoring": "Ignorovať", + "watching.description": "Zobrazovať témy v neprečítaných", "ignoring.description": "Nezobrazovať témy v neprečítaných", "watch.message": "Práve sledujete novinky s tejto kategórie a všetkých podkategórií", "ignore.message": "Práve ignorujete novinky s tejto kategórie a všetkých podkategórií", diff --git a/public/language/sk/error.json b/public/language/sk/error.json index 5941a0f3b1..93505ca3a5 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -49,15 +49,15 @@ "post-edit-duration-expired-hours-minutes": "Upravovať príspevky môžete až za %1 hodinu(y) %2 minút(y) po umiestnení", "post-edit-duration-expired-days": "Upravovať príspevky môžete až za %1 deň(dni) po umiestnení", "post-edit-duration-expired-days-hours": "Upravovať príspevky môžete až za %1 deň(dni) %2 hodinu(y) po umiestnení", - "post-delete-duration-expired": "Vymazať príspevky môžete až za %1 sekúnd(y) po umiestnení", - "post-delete-duration-expired-minutes": "Vymazať príspevky môžete až za %1 minút(y) po umiestnení", - "post-delete-duration-expired-minutes-seconds": "Vymazať príspevky môžete až za %1 minút(y) %2 sekúnd(y) po umiestnení", - "post-delete-duration-expired-hours": "Vymazať príspevky môžete až za %1 hodinu(y) po umiestnení", - "post-delete-duration-expired-hours-minutes": "Vymazať príspevky môžete až za %1 hodinu(y) %2 minút(y) po umiestnení", - "post-delete-duration-expired-days": "Vymazať príspevky môžete až za %1 deň(dni) po umiestnení", - "post-delete-duration-expired-days-hours": "Vymazať príspevky môžete až za %1 deň(dni) %2 hodinu(y) po umiestnení", - "cant-delete-topic-has-reply": "Nemôžete odstrániť svoju tému po tom, ako obsahuje odpoveď", - "cant-delete-topic-has-replies": "Nemôžete odstrániť túto tému po tom, ako obsahuje %1 odpovede", + "post-delete-duration-expired": "Odstrániť príspevky môžete až za %1 sekúnd(y) po umiestnení", + "post-delete-duration-expired-minutes": "Odstrániť príspevky môžete až za %1 minút(y) po umiestnení", + "post-delete-duration-expired-minutes-seconds": "Odstrániť príspevky môžete až za %1 minút(y) %2 sekúnd(y) po umiestnení", + "post-delete-duration-expired-hours": "Odstrániť príspevky môžete až za %1 hodinu(y) po umiestnení", + "post-delete-duration-expired-hours-minutes": "Odstrániť príspevky môžete až za %1 hodinu(y) %2 minút(y) po umiestnení", + "post-delete-duration-expired-days": "Odstrániť príspevky môžete až za %1 deň(dni) po umiestnení", + "post-delete-duration-expired-days-hours": "Odstrániť príspevky môžete až za %1 deň(dni) %2 hodinu(y) po umiestnení", + "cant-delete-topic-has-reply": "Nemôžete odstrániť svoju tému po tom, ak už obsahuje odpoveď", + "cant-delete-topic-has-replies": "Nemôžete odstrániť túto tému po tom, ak už obsahuje %1 odpovede", "content-too-short": "Prosím, zadajte dlhší príspevok. Príspevky musia obsahovať najmenej %1 znak(y).", "content-too-long": "Prosím, zadajte kratší príspevok. Príspevky nemôžu byť dlhšie ako %1 znaky(ov).", "title-too-short": "Prosím, zadajte dlhší názov. Názvy musia obsahovať najmenej %1 znak(y).", @@ -89,11 +89,11 @@ "group-needs-owner": "Táto skupina vyžaduje aspoň jedného vlastníka", "group-already-invited": "Tento užívateľ už bol pozvaný", "group-already-requested": "Vaša požiadavka na členstvo už bola predložená", - "post-already-deleted": "Tento príspevok bol vymazaný", + "post-already-deleted": "Tento príspevok bol odstránený", "post-already-restored": "Tento príspevok bol obnovený", - "topic-already-deleted": "Táto téma bola vymazaná", + "topic-already-deleted": "Táto téma bola odstránená", "topic-already-restored": "Táto téma bola obnovená", - "cant-purge-main-post": "Nemôžete očistiť hlavný príspevok, namiesto toho prosíme vymažte tému", + "cant-purge-main-post": "Nemôžete očistiť hlavný príspevok, namiesto toho prosíme odstráňte tému", "topic-thumbnails-are-disabled": "Náhľady tém sú zablokované.", "invalid-file": "Neplatný súbor", "uploads-are-disabled": "Nahrávanie je zablokované", @@ -107,7 +107,7 @@ "chat-message-too-long": "Správa v konverzácií je príliš dlhá", "cant-edit-chat-message": "Nemáte oprávnenie k úprave tejto správy", "cant-remove-last-user": "Nemôžete odstrániť posledného užívateľa", - "cant-delete-chat-message": "Nemáte oprávanie k vymazaniu tejto správy", + "cant-delete-chat-message": "Nemáte oprávanie k odstráneniu tejto správy", "already-voting-for-this-post": "Za tento príspevok ste už hlasovali.", "reputation-system-disabled": "Systém reputácie je zablokovaný.", "downvoting-disabled": "Hlasovanie proti je zablokované", diff --git a/public/language/sk/global.json b/public/language/sk/global.json index c27e4f755c..de6a8763a4 100644 --- a/public/language/sk/global.json +++ b/public/language/sk/global.json @@ -37,7 +37,7 @@ "header.search": "Hľadať", "header.profile": "Profil", "header.navigation": "Navigácia", - "notifications.loading": "Načítanie oznámení", + "notifications.loading": "Načítavanie oznámení", "chats.loading": "Načítanie konverzácií", "motd.welcome": "Vitajte na NodeBB, diskusná platforma budúcnosti.", "previouspage": "Predchádzajúca strana", @@ -54,9 +54,9 @@ "posts": "Príspevky", "best": "Najlepšie", "upvoters": "Hlasovali za", - "upvoted": "Pridať hlas", + "upvoted": "Pridaný hlas", "downvoters": "Hlasovali proti", - "downvoted": "Odobrať hlas", + "downvoted": "Odobratý hlas", "views": "Zhliadnutí", "reputation": "Reputácia", "read_more": "čítaj viac", @@ -89,13 +89,13 @@ "privacy": "Súkromie", "follow": "Sledovať", "unfollow": "Prestať sledovať", - "delete_all": "Vymazať všetko", + "delete_all": "Odstrániť všetko", "map": "Mapa", "sessions": "Prihlásiť sa do relácie", "ip_address": "IP Adresa", "enter_page_number": "Zadajte číslo stránky", "upload_file": "Nahrať súbor", - "upload": "Nahraný", + "upload": "Nahrať", "allowed-file-types": "Povolené typy súborov sú %1", "unsaved-changes": "Máte neuložené zmeny. Ste si istý, že chcete opustiť stránku?", "reconnecting-message": "Vyzerá to tak, že pripojenie k %1 bolo stratené. Prosím chvíľku počkajte, snažíme sa pripojiť znovu.", diff --git a/public/language/sk/modules.json b/public/language/sk/modules.json index 00ba3893eb..97c8c0db69 100644 --- a/public/language/sk/modules.json +++ b/public/language/sk/modules.json @@ -17,7 +17,7 @@ "chat.seven_days": "7 dní", "chat.thirty_days": "30 dní", "chat.three_months": "3 mesiace", - "chat.delete_message_confirm": "Ste si istý, že chcete vymazať túto správu?", + "chat.delete_message_confirm": "Ste si istý, že chcete odstrániť túto správu?", "chat.add-users-to-room": "Pridať užívateľa do miestnosti", "composer.compose": "Zostaviť", "composer.show_preview": "Zobraziť náhľad", diff --git a/public/language/sk/notifications.json b/public/language/sk/notifications.json index 475263bf23..0b5ad76572 100644 --- a/public/language/sk/notifications.json +++ b/public/language/sk/notifications.json @@ -1,7 +1,7 @@ { "title": "Oznámenia", "no_notifs": "Nemáte žiadne nové oznámenia", - "see_all": "Zobraz všetky oznámenia", + "see_all": "Zobraziť všetky oznámenia", "mark_all_read": "Označiť všetky oznámenia ako prečítané", "back_to_home": "Naspäť na %1", "outgoing_link": "Odkaz mimo fórum", diff --git a/public/language/sk/pages.json b/public/language/sk/pages.json index 6e0cc9125e..c9328ef0dd 100644 --- a/public/language/sk/pages.json +++ b/public/language/sk/pages.json @@ -40,9 +40,9 @@ "account/bookmarks": "%1 príspevky v záložkach", "account/settings": "Užívateľské nastavenia", "account/watched": "Témy sledovalo %1", - "account/upvoted": "Príspevku dali hlas %1", - "account/downvoted": "Príspevku odobrali hlas %1", - "account/best": "Najlepšie príspevky uskutočnil %1", + "account/upvoted": "Príspevky, ktorým užívateľ %1 dal hlas", + "account/downvoted": "Príspevky, ktorým užívateľ %1 odobral hlas", + "account/best": "Najlepšie príspevky vytvorené užívateľom %1", "confirm": "E-mail potvrdený", "maintenance.text": "%1 v súčasnej dobe prebieha údržba. Prosíme, vráťte sa neskôr.", "maintenance.messageIntro": "Správca, dodatočne zanechal túto správu:", diff --git a/public/language/sk/topic.json b/public/language/sk/topic.json index 68531a636c..9050c74a1b 100644 --- a/public/language/sk/topic.json +++ b/public/language/sk/topic.json @@ -4,8 +4,8 @@ "topic_id_placeholder": "Zadajte ID témy", "no_topics_found": "Neboli nájdené žiadne témy!", "no_posts_found": "Neboli nájdené žiadne príspevky", - "post_is_deleted": "Tento príspevok bol vymazaný!", - "topic_is_deleted": "Táto téma je odstránená!", + "post_is_deleted": "Tento príspevok bol odstránený!", + "topic_is_deleted": "Táto téma bola odstránená!", "profile": "Profil", "posted_by": "Uverejnené %1", "posted_by_guest": "Uverejnené od hosťa", @@ -17,7 +17,7 @@ "reply-as-topic": "Odpovedať ako téma", "guest-login-reply": "Pre odpoveď sa najprv prihláste", "edit": "Upraviť", - "delete": "Zmazať", + "delete": "Odstrániť", "purge": "Vyčistiť", "restore": "Obnoviť", "move": "Presunúť", @@ -26,7 +26,7 @@ "share": "Zdieľaj", "tools": "Nástroje", "flag": "Označiť", - "locked": "Uzamknutý", + "locked": "Uzamknuté", "pinned": "Pripnuté", "moved": "Presunuté", "bookmark_instructions": "Kliknite sem pre návrat k poslednému prečítanému príspevku vo vlákne.", @@ -75,15 +75,15 @@ "thread_tools.move": "Presunúť tému", "thread_tools.move_all": "Presunúť všetko", "thread_tools.fork": "Rozvetviť tému", - "thread_tools.delete": "Vymazať tému", - "thread_tools.delete-posts": "Vymazať príspevky", - "thread_tools.delete_confirm": "Ste si istý že chcete vymazať túto tému?", + "thread_tools.delete": "Odstrániť tému", + "thread_tools.delete-posts": "Odstrániť príspevky", + "thread_tools.delete_confirm": "Ste si istý že chcete odstrániť túto tému?", "thread_tools.restore": "Obnoviť tému", "thread_tools.restore_confirm": "Ste si naozaj istý že chcete obnoviť túto tému?", "thread_tools.purge": "Vyčistiť tému", "thread_tools.purge_confirm": "Ste si naozaj istý že chcete vyčistiť túto tému?", "topic_move_success": "Téma bola úspešne presunutá do %1", - "post_delete_confirm": "Ste si istý, že chcete vymazať tento príspevok?", + "post_delete_confirm": "Ste si istý, že chcete odstrániť tento príspevok?", "post_restore_confirm": "Ste si istí, že chcete obnoviť tento príspevok?", "post_purge_confirm": "Ste si istý že chcete naozaj vyčistiť tento príspevok?", "load_categories": "Načítanie kategórií", @@ -104,11 +104,11 @@ "fork_no_pids": "Žiadne príspevky neboli vybraté!", "fork_pid_count": "%1 príspevky(ov) vybraté", "fork_success": "Rozdelenie témy bolo úspešné! Kliknutím sem sa dostanete na rozdelenú tému", - "delete_posts_instruction": "Kliknite na príspevky, ktoré chcete vymazať/očistiť", + "delete_posts_instruction": "Kliknite na príspevky, ktoré chcete odstrániť/očistiť", "composer.title_placeholder": "Sem zadajte názov témy...", "composer.handle_placeholder": "Meno", "composer.discard": "Zahodiť", - "composer.submit": "Poslať", + "composer.submit": "Odoslať", "composer.replying_to": "Odpovedám užívateľovi %1", "composer.new_topic": "Nová téma", "composer.uploading": "nahrávanie...", diff --git a/public/language/sk/user.json b/public/language/sk/user.json index 354a2c084d..c05e20cf73 100644 --- a/public/language/sk/user.json +++ b/public/language/sk/user.json @@ -13,7 +13,7 @@ "delete_account": "Odstrániť účet", "delete_account_confirm": "Naozaj ste si istý že chcete odstrániť tento účet?
Táto akcia je nezvratná a taktiež nebudete môcť obnoviť žiadne Vaše dáta

Zadajte svoje používateľské meno pre potvrdenie, že chcete zničiť tento účet.", "delete_this_account_confirm": "Naozaj ste si istý že chcete odstrániť tento účet?
Táto akcia je nezvratná a taktiež nebudete môcť obnoviť žiadne Vaše dáta

", - "account-deleted": "Účet odstránený", + "account-deleted": "Účet bol odstránený", "fullname": "Meno a Priezvisko", "website": "Webová stránka", "location": "Poloha", @@ -21,7 +21,7 @@ "joined": "Registrovaný", "lastonline": "Naposledy online", "profile": "Profil", - "profile_views": "Zobrazenie profilu", + "profile_views": "Zobrazenia profilu", "reputation": "Reputácia", "bookmarks": "Záložky", "watched": "Sledované", @@ -31,7 +31,8 @@ "signature": "Podpis", "birthday": "Dátum narodenia", "chat": "Konverzácia", - "chat_with": "Rozhovor s %1", + "chat_with": "Pokračovať v konverzácií s %1", + "new_chat_with": "Začať novú konverzáciu s %1", "follow": "Nasledovať", "unfollow": "Prestať sledovať", "more": "Viac", diff --git a/public/language/sl/user.json b/public/language/sl/user.json index aee9a0107b..88ec8e16f0 100644 --- a/public/language/sl/user.json +++ b/public/language/sl/user.json @@ -31,7 +31,8 @@ "signature": "Podpis", "birthday": "Rojstni datum", "chat": "Klepet", - "chat_with": "Klepet z %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Spremljaj", "unfollow": "Ne spremljaj", "more": "Več", diff --git a/public/language/sr/category.json b/public/language/sr/category.json index 3b987be556..7952be22e8 100644 --- a/public/language/sr/category.json +++ b/public/language/sr/category.json @@ -1,20 +1,20 @@ { "category": "Категорија", - "subcategories": "Подкатегорије", - "new_topic_button": "Nova Tema", - "guest-login-post": "Пријавите се за слање порука", - "no_topics": "Ne postoji nijedna tema u ovoj kategoriji.
Zasto ne bi postavio jednu?", - "browsing": "gleda", - "no_replies": "Jos uvek nema odgovora", + "subcategories": "Поткатегорије", + "new_topic_button": "Нова тема", + "guest-login-post": "Пријавите се да бисте послали поруку", + "no_topics": "Нема тема у овој категорији.
Зашто не бисте поставили једну?", + "browsing": "гледа", + "no_replies": "Још увек нема одговора", "no_new_posts": "Нема нових порука", - "share_this_category": "Podeli ovu kategoriju", + "share_this_category": "Дели ову категорију", "watch": "Надгледај", "ignore": "Игнориши", - "watching": "Надгледање", - "ignoring": "Игнорисање", + "watching": "Надгледај", + "ignoring": "Игнориши", "watching.description": "Прикажи теме у непрочитаним", "ignoring.description": "Не приказуј теме у непрочитаним", - "watch.message": "Сада надгледате ажурирања из ове категорије и свих подкатегорија", - "ignore.message": "Сада игноришете ажурирања из ове категорије и свих подкатегорија", + "watch.message": "Сада надгледате ажурирања из ове категорије и свих поткатегорија", + "ignore.message": "Сада игноришете ажурирања из ове категорије и свих поткатегорија", "watched-categories": "Надгледане категорије" } \ No newline at end of file diff --git a/public/language/sr/email.json b/public/language/sr/email.json index 3558af9a41..d47a4abd13 100644 --- a/public/language/sr/email.json +++ b/public/language/sr/email.json @@ -4,7 +4,7 @@ "invite": "Позивница од %1", "greeting_no_name": "Здраво", "greeting_with_name": "Здраво %1", - "welcome.text1": "Хвала што сте се рагистровали на %1!", + "welcome.text1": "Хвала што сте се регистровали на %1!", "welcome.text2": "Да бисте у потпуности активирали ваш налог, потребно је да проверимо да ли стварно поседујете адресу е-поште којом сте се регистровали. ", "welcome.text3": "Администратор је прихватио вашу регистрацију. Можете се пријавити са вашим именом и лозинком.", "welcome.cta": "Кликните овде за потврду адресе ваше е-поште", @@ -19,7 +19,7 @@ "digest.notifications": "Имате непрочитана обавештења од %1:", "digest.latest_topics": "Недавне теме од %1", "digest.cta": "Кликните овде да посетите %1", - "digest.unsub.info": "Овај сажетак вам је послат због претплате.", + "digest.unsub.info": "Овај сажетак вам је послат услед вашег подешавања претплате.", "digest.no_topics": "Није било активних тема у последњих %1", "digest.day": "Дан", "digest.week": "Недеља", @@ -27,10 +27,10 @@ "digest.subject": "Сажетак за %1", "notif.chat.subject": "Примљена је нова порука ћаскања од %1", "notif.chat.cta": "Кликните овде да наставите са разговором", - "notif.chat.unsub.info": "Ова обавештење о ћаскању вам је послато услед подешавања претплате.", + "notif.chat.unsub.info": "Ова обавештење о ћаскању вам је послато услед вашег подешавања претплате.", "notif.post.cta": "Кликните овде за приказ целе теме", - "notif.post.unsub.info": "Ова обавештење о објави вам је послато услед подешавања претплате.", + "notif.post.unsub.info": "Ово обавештење вам је послато услед вашег подешавања претплате.", "test.text1": "Ово је пробно е-писмо за проверу исправности поставки е-поштара у NodeBB.", - "unsub.cta": "Кликните овде да измените те поставке", + "unsub.cta": "Кликните овде да измените та подешавања", "closing": "Хвала!" } \ No newline at end of file diff --git a/public/language/sr/error.json b/public/language/sr/error.json index 9ba08a8e32..b119cdfe34 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -17,7 +17,7 @@ "csrf-invalid": "Нисмо успели да вас пријавимо, вероватно због истека сесије. Молимо покушајте поново", "invalid-pagination-value": "Неважећа вредност при обележавању страна, мора бити најмање %1 а највише %2 ", "username-taken": "Корисничко име је заузето", - "email-taken": "Адреса е-поште је заусета", + "email-taken": "Адреса е-поште је заузета", "email-not-confirmed": "Ваша адреса е-поште још увек није оверена, кликните овде да би сте то учинили.", "email-not-confirmed-chat": "Није вам дозвољено да ћаскате док не оверите вашу е-пошту, кликните овде да то учините.", "email-not-confirmed-email-sent": "Ваша е-пошта још увек није потврђена, молимо проверите ваше пријемно сандуче.", @@ -42,20 +42,20 @@ "no-privileges": "Немате довољне привилегије за обављање ове радње.", "category-disabled": "Категорија је онемогућена", "topic-locked": "Тема је закључана", - "post-edit-duration-expired": "Дозвољено вам је да уређујете поруке само у %1 секунде/и након објављивања", - "post-edit-duration-expired-minutes": "Дозвољено вам је да уређујете поруке само у %1 минута након објављивања", - "post-edit-duration-expired-minutes-seconds": "Дозвољено вам је да уређујете поруке само у %1 минута и %2 секунде/и након објављивања", - "post-edit-duration-expired-hours": "Дозвољено вам је да уређујете поруке само у %1 сат/а/и након објављивања", - "post-edit-duration-expired-hours-minutes": "Дозвољено вам је да уређујете поруке само у %1 сат/а/и %2 минута након објављивања", - "post-edit-duration-expired-days": "Дозвољено вам је да уређујете поруке само у %1 дан/а након објављивања", - "post-edit-duration-expired-days-hours": "Дозвољено вам је да уређујете поруке само у %1 сат/а/и %2 сат/а/и након објављивања", - "post-delete-duration-expired": "Дозвољено вам је да бришете поруке само у %1 секунде/и након објављивања", - "post-delete-duration-expired-minutes": "Дозвољено вам је да бришете поруке само у %1 минута након објављивања", - "post-delete-duration-expired-minutes-seconds": "Дозвољено вам је да бришете поруке само у %1 минута и %2 секунде/и након објављивања", - "post-delete-duration-expired-hours": "Дозвољено вам је да бришете поруке само у %1 сат/а/и након објављивања", - "post-delete-duration-expired-hours-minutes": "Дозвољено вам је да бришете поруке само у %1 сат/а/и %2 минута након објављивања", - "post-delete-duration-expired-days": "Дозвољено вам је да бришете поруке само у %1 дан/а након објављивања", - "post-delete-duration-expired-days-hours": "Дозвољено вам је да бришете поруке само у %1 сат/а/и %2 сат/а/и након објављивања", + "post-edit-duration-expired": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 сек.", + "post-edit-duration-expired-minutes": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 мин.", + "post-edit-duration-expired-minutes-seconds": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 мин. и %2 сек.", + "post-edit-duration-expired-hours": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 час.", + "post-edit-duration-expired-hours-minutes": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 час. и %2 мин.", + "post-edit-duration-expired-days": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 дан.", + "post-edit-duration-expired-days-hours": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 дан. и %2 час.", + "post-delete-duration-expired": "Време у којем вам је дозвољено брисање порука након објављивања: %1 сек.", + "post-delete-duration-expired-minutes": "Време у којем вам је дозвољено брисање порука након објављивања: %1 мин.", + "post-delete-duration-expired-minutes-seconds": "Време у којем вам је дозвољено брисање порука након објављивања: %1 мин. и %2 сек.", + "post-delete-duration-expired-hours": "Време у којем вам је дозвољено брисање порука након објављивања: %1 час.", + "post-delete-duration-expired-hours-minutes": "Време у којем вам је дозвољено брисање порука након објављивања: %1 час. и %2 мин.", + "post-delete-duration-expired-days": "Време у којем вам је дозвољено брисање порука након објављивања: %1 дан.", + "post-delete-duration-expired-days-hours": "Време у којем вам је дозвољено брисање порука након објављивања: %1 дан. и %2 час.", "cant-delete-topic-has-reply": "Не можете обрисати вашу тему након што је на њу одговорено", "cant-delete-topic-has-replies": "Не можете обрисати вашу тему након што добије %1 одговора", "content-too-short": "Унесите дужу поруку. Порука мора имати најмање %1 карактера.", diff --git a/public/language/sr/global.json b/public/language/sr/global.json index 4071ddc6e1..48cc2f7304 100644 --- a/public/language/sr/global.json +++ b/public/language/sr/global.json @@ -1,6 +1,6 @@ { "home": "Почетна", - "search": "Претрага", + "search": "Претражи", "buttons.close": "Затвори", "403.title": "Приступ одбијен", "403.message": "Изгледа да сте набасали на страницу којој немате дозвољен приступ.", @@ -18,8 +18,8 @@ "posting_restriction_info": "Слање порука је тренутно ограничено само на пријављене кориснике, кликните овде да се пријавите.", "welcome_back": "Добродошли поново", "you_have_successfully_logged_in": "Успешно сте се пријавили", - "save_changes": "Сними измене", - "save": "Сними", + "save_changes": "Сачувај измене", + "save": "Сачувај", "close": "Затвори", "pagination": "Обележавање страна", "pagination.out_of": "%1 од %2", @@ -48,17 +48,17 @@ "alert.banned.message": "Управо сте добили забрану, бићете одјављени.", "alert.unfollow": "Не пратите више %1!", "alert.follow": "Сада пратите %1!", - "online": "На вези", + "online": "На мрежи", "users": "Корисници", "topics": "Теме", - "posts": "Поруке", + "posts": "Порука", "best": "Најбоље", - "upvoters": "Позитивно гласају", - "upvoted": "Позитивни гласано", - "downvoters": "Негативно гласају", - "downvoted": "Негативни гласано", - "views": "Прегледи", - "reputation": "Репутација", + "upvoters": "Позитивно гласали", + "upvoted": "Позитивно гласано", + "downvoters": "Негативно гласали", + "downvoted": "Негативно гласано", + "views": "Прегледа", + "reputation": "Углед", "read_more": "прочитајте више", "more": "Више", "posted_ago_by_guest": "објављено %1 од стране госта.", @@ -72,14 +72,14 @@ "guest_posted_ago": "Гост је објавио %1", "last_edited_by": "последњи пут уредио %1", "norecentposts": "Нема недавних порука", - "norecenttopics": "Нема скорашњих тема", + "norecenttopics": "Нема недавних тема", "recentposts": "Недавне поруке", "recentips": "Недавно забележене IP адресе", "moderator_tools": "Алати модератора", "away": "Одсутан", "dnd": "Не узнемиравај", "invisible": "Невидљив", - "offline": "Ван везе", + "offline": "Ван мреже", "email": "Е-пошта", "language": "Језик", "guest": "Гост", diff --git a/public/language/sr/groups.json b/public/language/sr/groups.json index 5b029efc0b..8866b3acbb 100644 --- a/public/language/sr/groups.json +++ b/public/language/sr/groups.json @@ -15,8 +15,8 @@ "invited.notification_title": "Добили сте позив да се придружите %1", "request.notification_title": "Чланство групе затражено од стране %1", "request.notification_text": "%1 је затражио да постане члан %2", - "cover-save": "Сними", - "cover-saving": "Снимање", + "cover-save": "Сачувај", + "cover-saving": "Чување", "details.title": "Детаљи о групи", "details.members": "Списак чланова", "details.pending": "Чланови на чекању", @@ -40,7 +40,7 @@ "details.private_help": "Уколико је укључено, приступање групи захтева одобрење власника групе.", "details.hidden": "Скривена", "details.hidden_help": "Уколико је укључено, група неће бити видљива на списку група, и корисницима се позивнице морају слати ручно.", - "details.delete_group": "Избришите групу", + "details.delete_group": "Избриши групу", "details.private_system_help": "Приватне групе су искључене на системском нивоу, ова опција нема ефекта", "event.updated": "Детаљи групе су ажурирани", "event.deleted": "Група „%1“ је избрисана", diff --git a/public/language/sr/modules.json b/public/language/sr/modules.json index 5b186e556a..778f9e7019 100644 --- a/public/language/sr/modules.json +++ b/public/language/sr/modules.json @@ -19,7 +19,7 @@ "chat.three_months": "3 месеца", "chat.delete_message_confirm": "Да ли сте сигурни да желите да избришете ову поруку?", "chat.add-users-to-room": "Додајте кориснике у собу", - "composer.compose": "Састави", + "composer.compose": "Писање поруке", "composer.show_preview": "Прикажи преглед", "composer.hide_preview": "Сакриј преглед", "composer.user_said_in": "%1 је рекао у %2", @@ -28,8 +28,8 @@ "composer.submit_and_lock": "Пошаљи и закључај", "composer.toggle_dropdown": "Подесите \"Dropdown\"", "composer.uploading": "Отпремање %1", - "composer.formatting.bold": "подебљано", - "composer.formatting.italic": "курзив", + "composer.formatting.bold": "Подебљано", + "composer.formatting.italic": "Курзив", "composer.formatting.list": "Листа", "composer.formatting.strikethrough": "Прецртано", "composer.formatting.link": "Веза", diff --git a/public/language/sr/pages.json b/public/language/sr/pages.json index a31e439ec1..24fafe64ba 100644 --- a/public/language/sr/pages.json +++ b/public/language/sr/pages.json @@ -5,10 +5,10 @@ "popular-week": "Популарне теме ове седмице", "popular-month": "Популарне теме овог месеца", "popular-alltime": "Популарне теме свих времена", - "recent": "Скорашње теме", + "recent": "Недавне теме", "flagged-posts": "Означене поруке", "ip-blacklist": "Црна листа IP адреса", - "users/online": "Корисници на вези", + "users/online": "Корисници на мрежи", "users/latest": "Најновији корисници", "users/sort-posts": "Корисници са највише порука", "users/sort-reputation": "Корисници са највећим угледом", @@ -38,8 +38,8 @@ "account/topics": "Теме од %1", "account/groups": "Групе корисника %1", "account/bookmarks": "Омиљене поруке корисника $1", - "account/settings": "Корисничке поставке", - "account/watched": "Теме које %1 прати", + "account/settings": "Корисничка подешавања", + "account/watched": "Теме које %1 надгледа", "account/upvoted": "Поруке које је позитивно гласао %1", "account/downvoted": "Поруке које је негативно гласао %1", "account/best": "Најбоље поруке од %1", diff --git a/public/language/sr/recent.json b/public/language/sr/recent.json index 65f4b5ff28..7358156231 100644 --- a/public/language/sr/recent.json +++ b/public/language/sr/recent.json @@ -5,15 +5,15 @@ "month": "Месец", "year": "Година", "alltime": "Одувек", - "no_recent_topics": "Нема скорашњих тема.", + "no_recent_topics": "Нема недавних тема.", "no_popular_topics": "Нема популарних тема.", "there-is-a-new-topic": "Постоји нова тема.", - "there-is-a-new-topic-and-a-new-post": "Постоји нова тема и нова порука.", - "there-is-a-new-topic-and-new-posts": "Постоји нова тема и %1 нових порука.", - "there-are-new-topics": "Постоје %1 нове теме.", - "there-are-new-topics-and-a-new-post": "Попстоје %1 нове теме и нова порука.", - "there-are-new-topics-and-new-posts": "Постоје %1 нове теме и %2 нових порука.", + "there-is-a-new-topic-and-a-new-post": "Постоје нова тема и нова порука.", + "there-is-a-new-topic-and-new-posts": "Постоји нова тема и нових порука: %1.", + "there-are-new-topics": "Број нових тема: %1.", + "there-are-new-topics-and-a-new-post": "Постоји нова порука и нових тема: %1.", + "there-are-new-topics-and-new-posts": "Број нових тема: %1 и нових порука: %2.", "there-is-a-new-post": "Постоји нова порука.", - "there-are-new-posts": "Постоје %1 нове поруке.", + "there-are-new-posts": "Број нових порука: %1.", "click-here-to-reload": "Кликните овде за поновно учитавање." } \ No newline at end of file diff --git a/public/language/sr/search.json b/public/language/sr/search.json index 77193c652d..cadfeb8327 100644 --- a/public/language/sr/search.json +++ b/public/language/sr/search.json @@ -1,40 +1,40 @@ { - "results_matching": "%1 резултат(а) се поклапају са „%2“, (%3 секунди)", - "no-matches": "Нема поклапања", + "results_matching": "%1 резултат(а) се подудара са „%2“, (%3 секунди)", + "no-matches": "Нема подударања", "advanced-search": "Напредна претрага", "in": "У", "titles": "Наслови", "titles-posts": "Наслови и поруке", "posted-by": "Објавио", "in-categories": "У категоријама", - "search-child-categories": "Претражи подкатегорије", + "search-child-categories": "Претражи поткатегорије", "reply-count": "Број одговора", "at-least": "Најмање", "at-most": "Највише", - "post-time": "Време објаве", + "post-time": "Времену објаве", "newer-than": "Новије од", "older-than": "Старије од", "any-date": "Било који датум", "yesterday": "Јуче", "one-week": "Једне седмице", "two-weeks": "Две седмице", - "one-month": "Један месец", + "one-month": "Једног месеца", "three-months": "Три месеца", "six-months": "Шест месеци", "one-year": "Једне године", - "sort-by": "Сложи према", - "last-reply-time": "Вермену последњег одговора", + "sort-by": "Поређај по", + "last-reply-time": "Времену последњег одговора", "topic-title": "Наслову теме", "number-of-replies": "Броју одговора", "number-of-views": "Броју прегледа", "topic-start-date": "Датуму настанка теме", - "username": "Корисничном имену", + "username": "Корисничком имену", "category": "Категорији", "descending": "У опадајућем низу", "ascending": "У растућем низу", - "save-preferences": "Сними поставке", + "save-preferences": "Сачувај поставке", "clear-preferences": "Очисти поставке", - "search-preferences-saved": "Поставке претраге су снимљене", + "search-preferences-saved": "Поставке претраге су сачуване", "search-preferences-cleared": "Поставке претраге су очишћене", "show-results-as": "Прикажи резултате као" } \ No newline at end of file diff --git a/public/language/sr/success.json b/public/language/sr/success.json index f16c4a4cc5..6a033f0492 100644 --- a/public/language/sr/success.json +++ b/public/language/sr/success.json @@ -2,5 +2,5 @@ "success": "Успешно", "topic-post": "Успешно сте послали поруку.", "authentication-successful": "Успешна аутентификација", - "settings-saved": "Поставке су сачуване!" + "settings-saved": "Подешавања су сачувана!" } \ No newline at end of file diff --git a/public/language/sr/tags.json b/public/language/sr/tags.json index 1fb87cf29b..146d889e3a 100644 --- a/public/language/sr/tags.json +++ b/public/language/sr/tags.json @@ -1,7 +1,7 @@ { "no_tag_topics": "Нема тема са овом ознаком.", "tags": "Ознаке", - "enter_tags_here": "Овде унесите ознаке, између %1 и %2 знакова за сваку.", + "enter_tags_here": "Овде унесите ознаке, од %1 до %2 знакова за сваку.", "enter_tags_here_short": "Унесите ознаке...", "no_tags": "Још увек нема ознака." } \ No newline at end of file diff --git a/public/language/sr/topic.json b/public/language/sr/topic.json index df2c049c17..f2ca42051f 100644 --- a/public/language/sr/topic.json +++ b/public/language/sr/topic.json @@ -2,7 +2,7 @@ "topic": "Тема", "topic_id": "ID теме", "topic_id_placeholder": "Унесите ID теме", - "no_topics_found": "Нису пронађене теме!", + "no_topics_found": "Нема пронађених тема!", "no_posts_found": "Нису пронађене поруке!", "post_is_deleted": "Ова порука је избрисана!", "topic_is_deleted": "Ова тема је избрисана!", @@ -55,14 +55,14 @@ "markAsUnreadForAll.success": "Тема је свима означена као непрочитана.", "mark_unread": "Означи као непрочитано", "mark_unread.success": "Тема је означена као непрочитана", - "watch": "Прати", - "unwatch": "Не прати", + "watch": "Надгледај", + "unwatch": "Не надгледај", "watch.title": "Будите обавештени о новим одговорима у овој теми", - "unwatch.title": "Заустави праћење ове теме", + "unwatch.title": "Заустави надгледање ове теме", "share_this_post": "Дели ову поруку", - "watching": "Прати се", - "not-watching": "Не прати се", - "ignoring": "Игнорише се", + "watching": "Надгледај", + "not-watching": "Не надгледај", + "ignoring": "Игнориши", "watching.description": "Обавести ме о новим одговорима.
Прикажи тему у непрочитаним", "not-watching.description": "Немој ме обавештавати о новим одговорима.
Прикажи тему у непрочитаним ако категорија није игнорисана.", "ignoring.description": "Немој ме обавештавати о новим одговорима.
Не приказуј тему у непрочитаним", @@ -108,8 +108,8 @@ "composer.title_placeholder": "Унесите овде назив теме...", "composer.handle_placeholder": "Име", "composer.discard": "Одбаци", - "composer.submit": "Проследи", - "composer.replying_to": "Одговарање на %1", + "composer.submit": "Пошаљи", + "composer.replying_to": "Писање одговора на %1", "composer.new_topic": "Нова тема", "composer.uploading": "отпремање...", "composer.thumb_url_label": "Налепи адресу сличице теме", @@ -122,11 +122,11 @@ "more_users": "још %1 корисник/а", "more_guests": "још %1 гост/а", "users_and_others": "%1 и %2 осталих", - "sort_by": "Сортирај по", + "sort_by": "Поређај", "oldest_to_newest": "Од старијих ка новијим", "newest_to_oldest": "Од новијих ка старијим", - "most_votes": "Највише гласова", - "most_posts": "Највише порука", + "most_votes": "По броју гласова", + "most_posts": "По броју порука", "stale.title": "Креирати нову тему уместо тога?", "stale.warning": "Тема у којој желите да одговорите је сувише стара. Да ли желите да уместо тога креирате нову тему и упутите на ову у вашем одговору?", "stale.create": "Креирај нову тему", diff --git a/public/language/sr/user.json b/public/language/sr/user.json index b30b6825c2..8976d2c179 100644 --- a/public/language/sr/user.json +++ b/public/language/sr/user.json @@ -1,13 +1,13 @@ { "banned": "Забрањен", - "offline": "Ван везе", + "offline": "Ван мреже", "username": "Корисничко име", "joindate": "Датум регистрације", "postcount": "Број порука", "email": "Е-пошта", "confirm_email": "Потврда е-поште", "account_info": "Информације о налогу", - "ban_account": "Забрани корисника", + "ban_account": "Забрани налог", "ban_account_confirm": "Да ли заиста желите да забраните овог корисника?", "unban_account": "Скини забрану налогу", "delete_account": "Брисање налога", @@ -19,19 +19,20 @@ "location": "Локација", "age": "Старост", "joined": "Датум регстрације", - "lastonline": "Последњи пут на вези", + "lastonline": "Последњи пут на мрежи", "profile": "Профил", "profile_views": "Прегледа профила", "reputation": "Репутација", "bookmarks": " Обележивачи", - "watched": "Надгледани", + "watched": "Надгледано", "followers": "Пратиоци", - "following": "Прати", + "following": "Праћења", "aboutme": "О мени", "signature": "Потпис", "birthday": "Рођендан", "chat": "Ђаскање", - "chat_with": "Ћаскање са %1", + "chat_with": "Настави ћаскање са %1", + "new_chat_with": "Започни ново ћаскање са %1", "follow": "Прати", "unfollow": "Не прати", "more": "Више", @@ -39,7 +40,7 @@ "change_picture": "Промена слике", "change_username": "Промена корисничког имена", "change_email": "Промена е-поште", - "edit": "Уређивање", + "edit": "Уреди", "edit-profile": "Уреди профил", "default_picture": "Подразумевана икона", "uploaded_picture": "Отпремљена слика", @@ -50,7 +51,7 @@ "change_password_error": "Неисправна лозинка", "change_password_error_wrong_current": "Ваша тренутна лозинка није исправна!", "change_password_error_length": "Лозинка је прекратка!", - "change_password_error_match": "Лозинке се морају поклапати!", + "change_password_error_match": "Лозинке се морају подударати!", "change_password_error_privileges": "Немате дозволу за мењање ове лозинке.", "change_password_success": "Ваша лозинка је ажурирана!", "confirm_password": "Потврда лозинке", @@ -72,7 +73,7 @@ "digest_daily": "Дневно", "digest_weekly": "Седмично", "digest_monthly": "Месечно", - "send_chat_notifications": "Пошаљи е-поруку ако пристигне нова порука ћаскања а ја нисам на вези", + "send_chat_notifications": "Пошаљи е-поруку ако пристигне нова порука ћаскања а ја нисам на мрежи", "send_post_notifications": "Пошаљи е-поруку када се појаве одговори на теме на које сам претплаћен", "settings-require-reload": "Неке измене у подешавањима захтевају поновно учитавање. Кликните овде да бисте поново учитали страницу.", "has_no_follower": "Овај корисник нема пратиоце :(", @@ -106,11 +107,11 @@ "grouptitle": "Назив групе", "no-group-title": "Без назива групе", "select-skin": "Изаберите маску", - "select-homepage": "Изабери почетну страницу", + "select-homepage": "Изаберите почетну страницу", "homepage": "Почетна страница", - "homepage_description": "Изабери страницу која ће се користити као почетна страница форума или \"Ниједна\" да би се користила подразумевана почетна страница.", + "homepage_description": "Изаберите страницу која ће се користити као почетна страница форума или \"None\" да би се користила подразумевана почетна страница.", "custom_route": "Прилагођена путања почетне странице", - "custom_route_help": "Овде унесите назив путање , без икакве претходне косе црте (нпр. \"недавно\" или \"популарно\")", + "custom_route_help": "Овде унесите назив путање, без икакве претходне косе црте (нпр. \"недавно\" или \"популарно\")", "sso.title": "Једноструки Sign-on сервиси", "sso.associated": "Повезано са", "sso.not-associated": "Кликните овде за повезивање са", diff --git a/public/language/sr/users.json b/public/language/sr/users.json index e630813348..d95aa78aae 100644 --- a/public/language/sr/users.json +++ b/public/language/sr/users.json @@ -8,11 +8,11 @@ "load_more": "Учитај више", "users-found-search-took": "Нађено је %1 корисника! Претрага је завршена за %2 секунде.", "filter-by": "Филтрирај према", - "online-only": "Само корисници на вези", + "online-only": "Само корисници на мрежи", "invite": "Позови", "invitation-email-sent": "Е-пошта са позивом је послата на %1", "user_list": "Листа корисника", - "recent_topics": "Скорашње теме", + "recent_topics": "Недавне теме", "popular_topics": "Популарне теме", "unread_topics": "Непрочитане теме", "categories": "Категорије", diff --git a/public/language/sv/user.json b/public/language/sv/user.json index 6116e08ba7..e058143f6d 100644 --- a/public/language/sv/user.json +++ b/public/language/sv/user.json @@ -31,7 +31,8 @@ "signature": "Signatur", "birthday": "Födelsedag", "chat": "Chatta", - "chat_with": "Chatta med %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Följ", "unfollow": "Sluta följ", "more": "Mer", diff --git a/public/language/th/user.json b/public/language/th/user.json index c40a748a91..1bd6509f61 100644 --- a/public/language/th/user.json +++ b/public/language/th/user.json @@ -31,7 +31,8 @@ "signature": "ลายเซ็น", "birthday": "วันเกิด", "chat": "แชท", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "ติดตาม", "unfollow": "เลิกติดตาม", "more": "More", diff --git a/public/language/tr/user.json b/public/language/tr/user.json index d507332b0c..2d8e654944 100644 --- a/public/language/tr/user.json +++ b/public/language/tr/user.json @@ -31,7 +31,8 @@ "signature": "İmza", "birthday": "Doğum Tarihi", "chat": "Sohbet", - "chat_with": "%1 ile Sohbet", + "chat_with": "%1 ile sohbete devam et", + "new_chat_with": "%1 ile yeni sohbete başla", "follow": "Takip Et", "unfollow": "Takip etme", "more": "Daha Fazla", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index f9f0ec4bc3..5e45cc0ee1 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -29,7 +29,7 @@ "username-too-long": "Tên đăng nhập quá dài", "password-too-long": "Mật khẩu quá dài", "user-banned": "Tài khoản bị ban", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Xin lỗi, tài khoản này đã bị khóa (Lí do: %1)", "user-too-new": "Rất tiếc, bạn phải chờ %1 giây để đăng bài viết đầu tiên.", "blacklisted-ip": "Rất tiếc, địa chỉ IP của bạn đã bị cấm khỏi cộng đồng. Nếu bạn cảm thấy có gì không đúng, hãy liên lạc với người quản trị.", "ban-expiry-missing": "Vui lòng cung cấp ngày hết hạn của lệnh cấm", @@ -56,13 +56,13 @@ "post-delete-duration-expired-hours-minutes": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giờ(s) 2 phút(s)", "post-delete-duration-expired-days": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s)", "post-delete-duration-expired-days-hours": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s) %2 giờ(s)", - "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", - "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "cant-delete-topic-has-reply": "Bạn không thể xóa chủ đề vì đã có 1 bình luận", + "cant-delete-topic-has-replies": "Bạn không thể xóa chủ đề này vì đã có %1 bình luận", "content-too-short": "Vui lòng nhập một bài viết dài hơn. Bài viết phải có tối thiểu %1 ký tự.", "content-too-long": "Vui lòng nhập một bài viết ngắn hơn. Bài viết chỉ có thể có tối đa %1 ký tự.", "title-too-short": "Vui lòng nhập tiêu đề dài hơn. Tiêu đề phải có tối thiểu %1 ký tự.", "title-too-long": "Vui lòng nhập tiêu đề ngắn hơn. Tiêu đề chỉ có thể có tối đa %1 ký tự.", - "category-not-selected": "Category not selected.", + "category-not-selected": "Chưa chọn category", "too-many-posts": "Bạn chỉ có đăng bài mới mỗi %1 giây - vui lòng đợi để tiếp tục đăng bài.", "too-many-posts-newbie": "Bạn chỉ có thể đăng bài mỗi %1 giây cho đến khi bạn tích luỹ được %2 điểm tín nhiệm - vui lòng đợi để tiếp tục đăng bài.", "tag-too-short": "Vui lòng nhập tag dài hơn. Tag phải có tối thiểu %1 ký tự.", @@ -72,8 +72,8 @@ "still-uploading": "Vui lòng chờ upload", "file-too-big": "Kích cỡ file được cho phép tối đa là %1 kB - vui lòng tải lên file có dung lượng nhỏ hơn.", "guest-upload-disabled": "Khách (chưa có tài khoản) không có quyền tải lên file.", - "already-bookmarked": "You have already bookmarked this post", - "already-unbookmarked": "You have already unbookmarked this post", + "already-bookmarked": "Bạn đã bookmark chủ đề này rồi", + "already-unbookmarked": "Bạn đã hủy bookmark chủ đề này rồi", "cant-ban-other-admins": "Bạn không thể cấm được các quản trị viên khác", "cant-remove-last-admin": "Bạn là quản trị viên duy nhất. Hãy cho thành viên khác làm quản trị viên trước khi huỷ bỏ quyền quản trị của bạn.", "cant-delete-admin": "Hủy quyền quản trị của tài khoản này trước khi xóa", @@ -126,6 +126,6 @@ "cant-kick-self": "Bạn không thể kick chính bạn ra khỏi nhóm", "no-users-selected": "Chưa có người dùng(s) nào", "invalid-home-page-route": "Đường dẫn trang chủ không hợp lệ", - "invalid-session": "Session Mismatch", - "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." + "invalid-session": "Không đúng session", + "invalid-session-text": "Có vẻ như phiên đăng nhập của bạn đã không còn hoạt động nữa, hoặc không còn đúng với thông tin trên máy chủ. Vui lòng tải lại trang này" } \ No newline at end of file diff --git a/public/language/vi/global.json b/public/language/vi/global.json index 2af8e015f9..e151b6f629 100644 --- a/public/language/vi/global.json +++ b/public/language/vi/global.json @@ -10,7 +10,7 @@ "500.title": "Internal Error.", "500.message": "Úi chà! Có vẻ như có trục trặc rồi!", "400.title": "Bad Request.", - "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "400.message": "Có vẻ như đường dẫn này không hợp lệ, vui lòng kiểm tra và thử lại lần nữa. Bạn cũng có thể quay về trang chủ ngay.", "register": "Đăng ký", "login": "Đăng nhập", "please_log_in": "Xin hãy đăng nhập", @@ -75,7 +75,7 @@ "norecenttopics": "Không có chủ đề gần đây", "recentposts": "Số bài viết gần đây", "recentips": "Các IP vừa mới đăng nhập", - "moderator_tools": "Moderator Tools", + "moderator_tools": "Công cụ quản lí", "away": "Đang đi vắng", "dnd": "Đừng làm phiền", "invisible": "Ẩn", @@ -97,10 +97,10 @@ "upload_file": "Tải file lên", "upload": "Tải lên", "allowed-file-types": "Các định dạng file được cho phép là %1", - "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", - "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", - "play": "Play", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "unsaved-changes": "Có một vài thay đổi chưa được lưu. Bạn muốn rời đi ngay?", + "reconnecting-message": "Có vẻ như bạn đã mất kết nối tới %1, vui lòng đợi một lúc để chúng tôi thử kết nối lại.", + "play": "Chơi", + "cookies.message": "Trang web này sử dụng cookie để đảm bảo trải nghiệm tốt nhất cho người dùng", + "cookies.accept": "Đã rõ!", + "cookies.learn_more": "Xem thêm" } \ No newline at end of file diff --git a/public/language/vi/groups.json b/public/language/vi/groups.json index d8a037b662..7ae06112fd 100644 --- a/public/language/vi/groups.json +++ b/public/language/vi/groups.json @@ -51,6 +51,6 @@ "membership.reject": "Từ chối", "new-group.group_name": "Tên nhóm", "upload-group-cover": "Tải ảnh bìa lên cho nhóm", - "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", - "bulk-invite": "Bulk Invite" + "bulk-invite-instructions": "Nhập danh sách username, ngăn cách bằng dấu phẩy, để mời vào nhóm", + "bulk-invite": "Mời nhiều người" } \ No newline at end of file diff --git a/public/language/vi/login.json b/public/language/vi/login.json index ea130d9ada..5097ddf701 100644 --- a/public/language/vi/login.json +++ b/public/language/vi/login.json @@ -8,5 +8,5 @@ "failed_login_attempt": "Đăng nhập không thành công", "login_successful": "Bạn đã đăng nhập thành công!", "dont_have_account": "Chưa có tài khoản?", - "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" + "logged-out-due-to-inactivity": "Bạn đã bị đăng xuất khỏi Admin Control Panel do không hoạt động quá lâu" } \ No newline at end of file diff --git a/public/language/vi/pages.json b/public/language/vi/pages.json index 5adfa71850..4ac7cf00a9 100644 --- a/public/language/vi/pages.json +++ b/public/language/vi/pages.json @@ -13,7 +13,7 @@ "users/sort-posts": "Thành viên có nhiều bài đăng nhất", "users/sort-reputation": "Thành viên có điểm tín nhiệm cao nhất", "users/banned": "Thành viên đã bị cấm", - "users/most-flags": "Most flagged users", + "users/most-flags": "Những thành viên bị gắn cờ nhiều nhất", "users/search": "Tìm kiếm thành viên", "notifications": "Thông báo", "tags": "Tag", @@ -37,7 +37,7 @@ "account/posts": "Bài viết được đăng bởi %1", "account/topics": "Chủ đề được tạo bởi %1", "account/groups": "Nhóm của %1", - "account/bookmarks": "%1's Bookmarked Posts", + "account/bookmarks": "Đã bookmark %1's chủ đề", "account/settings": "Thiết lập", "account/watched": "Chủ đề %1 đang theo dõi", "account/upvoted": "Bài viết %1 tán thành", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 971c13a978..6ef833d39e 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -13,7 +13,7 @@ "notify_me": "Được thông báo khi có trả lời mới trong chủ đề này", "quote": "Trích dẫn", "reply": "Trả lời", - "replies_to_this_post": "Replies: %1", + "replies_to_this_post": "%1 trả lời", "reply-as-topic": "Trả lời dưới dạng chủ đề", "guest-login-reply": "Hãy đăng nhập để trả lời", "edit": "Chỉnh sửa", @@ -32,21 +32,21 @@ "bookmark_instructions": "Bấm vào đây để quay về đọc bài viết mới nhất trong chủ đề này.", "flag_title": "Flag bài viết này để chỉnh sửa", "flag_success": "Chủ đề này đã được flag để chỉnh sửa", - "flag_manage_title": "Flagged post in %1", - "flag_manage_history": "Action History", - "flag_manage_no_history": "No event history to report", - "flag_manage_assignee": "Assignee", - "flag_manage_state": "State", + "flag_manage_title": "Bài viết bị gắn cờ trong %1", + "flag_manage_history": "Lịch sử hoạt động", + "flag_manage_no_history": "Không có lịch sử sự kiện nào", + "flag_manage_assignee": "Người giao việc", + "flag_manage_state": "Trạng thái", "flag_manage_state_open": "New/Open", - "flag_manage_state_wip": "Work in Progress", - "flag_manage_state_resolved": "Resolved", - "flag_manage_state_rejected": "Rejected", - "flag_manage_notes": "Shared Notes", - "flag_manage_update": "Update Flag Status", - "flag_manage_history_assignee": "Assigned to %1", - "flag_manage_history_state": "Updated state to %1", - "flag_manage_history_notes": "Updated flag notes", - "flag_manage_saved": "Flag Details Updated", + "flag_manage_state_wip": "Công việc đang thực thi", + "flag_manage_state_resolved": "Đã hoàn thành", + "flag_manage_state_rejected": "Đã từ chối", + "flag_manage_notes": "Những ghi chú được chia sẻ", + "flag_manage_update": "Cập nhật trạng thái gắn c", + "flag_manage_history_assignee": "Đã giao cho %1", + "flag_manage_history_state": "Cập nhật trạng thái thành %1", + "flag_manage_history_notes": "Đã cập nhật ghi chú gắn c", + "flag_manage_saved": "Đã cập nhật nội dung gắn c", "deleted_message": "Chủ đề này đã bị xóa. Chỉ ban quản trị mới xem được.", "following_topic.message": "Từ giờ bạn sẽ nhận được thông báo khi có ai đó gửi bài viết trong chủ đề này", "not_following_topic.message": "Bạn có thể xem chủ đề này trong danh sách chủ đề chưa xem, nhưng bạn sẽ không nhận thông báo khi có ai đó đăng bài viết trong chủ đề này", @@ -67,7 +67,7 @@ "not-watching.description": "Không thông báo tôi các trả lời mới.
Hiển thị mục chưa đọc nếu danh mục bị bỏ qua.", "ignoring.description": "Không thông báo tôi các trả lời mới.
Không hiển thị các mục chưa đọc.", "thread_tools.title": "Công cụ", - "thread_tools.markAsUnreadForAll": "Mark unread for all", + "thread_tools.markAsUnreadForAll": "Đánh dấu tất cả thành chưa đọc", "thread_tools.pin": "Pin chủ đề", "thread_tools.unpin": "Bỏ pin chủ đề", "thread_tools.lock": "Khóa chủ đề", @@ -92,7 +92,7 @@ "confirm_fork": "Tạo bảo sao", "bookmark": "Bookmark", "bookmarks": "Bookmarks", - "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "bookmarks.has_no_bookmarks": "Bạn chưa bookmark bài viết nào cả.", "loading_more_posts": "Tải thêm các bài gửi khác", "move_topic": "Chuyển chủ đề", "move_topics": "Di chuyển chủ đề", diff --git a/public/language/vi/user.json b/public/language/vi/user.json index 61100679e7..6b8678c82c 100644 --- a/public/language/vi/user.json +++ b/public/language/vi/user.json @@ -31,7 +31,8 @@ "signature": "Chữ ký", "birthday": "Ngày sinh ", "chat": "Chat", - "chat_with": "Chat với %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Theo dõi", "unfollow": "Hủy theo dõi", "more": "Xem thêm", @@ -89,11 +90,11 @@ "topics_per_page": "Số chủ đề trong một trang", "posts_per_page": "Số bài viết trong một trang", "notification_sounds": "Phát âm thanh khi bạn nhận được thông báo mới", - "notifications_and_sounds": "Notifications & Sounds", - "incoming-message-sound": "Incoming message sound", - "outgoing-message-sound": "Outgoing message sound", - "notification-sound": "Notification sound", - "no-sound": "No sound", + "notifications_and_sounds": "Thông báo & Âm thanh", + "incoming-message-sound": "Âm báo tin nhắn tới", + "outgoing-message-sound": "Âm báo tin nhắn đi", + "notification-sound": "Âm thanh thông báo", + "no-sound": "Không có âm thanh", "browsing": "Đang xem cài đặt", "open_links_in_new_tab": "Mở link trong tab mới.", "enable_topic_searching": "Bật In-topic Searching", @@ -101,8 +102,8 @@ "delay_image_loading": "Việc tải ảnh đang bị chậm", "image_load_delay_help": "Nếu được bật, toàn bộ ảnh trong chủ đề sẽ chỉ được tải khi người dùng kéo chuột tới", "scroll_to_my_post": "Sau khi đăng một trả lời thì hiển thị bài viết mới", - "follow_topics_you_reply_to": "Watch topics that you reply to", - "follow_topics_you_create": "Watch topics you create", + "follow_topics_you_reply_to": "Theo dõi những chủ đề mà bạn đã bình luận", + "follow_topics_you_create": "Theo dõi những chủ đề do bạn t", "grouptitle": "Tên nhóm", "no-group-title": "Không có tên nhóm", "select-skin": "Chọn một giao diện", @@ -114,16 +115,16 @@ "sso.title": "Đăng nhập một lần", "sso.associated": "Đã liên kết với", "sso.not-associated": "Nhấn vào đây để liên kết với", - "info.latest-flags": "Latest Flags", - "info.no-flags": "No Flagged Posts Found", - "info.ban-history": "Recent Ban History", - "info.no-ban-history": "This user has never been banned", - "info.banned-until": "Banned until %1", + "info.latest-flags": "Cờ mới nhất", + "info.no-flags": "Không có bài viết nào bị gắn c", + "info.ban-history": "Lịch sử khóa tài khoản gần đây", + "info.no-ban-history": "Người dùng này chưa từng bị khóa tài khoản", + "info.banned-until": "Bị khóa tài khoản tới %1", "info.banned-permanently": "Bị cấm vĩnh viễn", "info.banned-reason-label": "Lý do", - "info.banned-no-reason": "No reason given.", - "info.username-history": "Username History", - "info.email-history": "Email History", - "info.moderation-note": "Moderation Note", - "info.moderation-note.success": "Moderation note saved" + "info.banned-no-reason": "Không có lí do.", + "info.username-history": "Lịch sử tên người d", + "info.email-history": "Lịch sử email", + "info.moderation-note": "Ghi chú quản lí", + "info.moderation-note.success": "Đã lưu ghi chú quản l" } \ No newline at end of file diff --git a/public/language/vi/users.json b/public/language/vi/users.json index 5a830cf2de..b694a2bd32 100644 --- a/public/language/vi/users.json +++ b/public/language/vi/users.json @@ -2,7 +2,7 @@ "latest_users": "Thành viên mới nhất", "top_posters": "Thành viên đăng bài nhiều nhất", "most_reputation": "Thành viên có điểm tín nhiệm cao nhất", - "most_flags": "Most Flags", + "most_flags": "Bị gắn cờ nhiều nhất", "search": "Tìm kiếm", "enter_username": "Gõ tên thành viên để tìm kiếm", "load_more": "Tải thêm", diff --git a/public/language/zh_CN/category.json b/public/language/zh-CN/category.json similarity index 100% rename from public/language/zh_CN/category.json rename to public/language/zh-CN/category.json diff --git a/public/language/zh_CN/email.json b/public/language/zh-CN/email.json similarity index 100% rename from public/language/zh_CN/email.json rename to public/language/zh-CN/email.json diff --git a/public/language/zh_CN/error.json b/public/language/zh-CN/error.json similarity index 100% rename from public/language/zh_CN/error.json rename to public/language/zh-CN/error.json diff --git a/public/language/zh_CN/global.json b/public/language/zh-CN/global.json similarity index 100% rename from public/language/zh_CN/global.json rename to public/language/zh-CN/global.json diff --git a/public/language/zh_CN/groups.json b/public/language/zh-CN/groups.json similarity index 100% rename from public/language/zh_CN/groups.json rename to public/language/zh-CN/groups.json diff --git a/public/language/zh_CN/language.json b/public/language/zh-CN/language.json similarity index 69% rename from public/language/zh_CN/language.json rename to public/language/zh-CN/language.json index 282a107861..251bc4ef91 100644 --- a/public/language/zh_CN/language.json +++ b/public/language/zh-CN/language.json @@ -1,5 +1,5 @@ { "name": "简体中文", - "code": "zh_CN", + "code": "zh-CN", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/zh_CN/login.json b/public/language/zh-CN/login.json similarity index 100% rename from public/language/zh_CN/login.json rename to public/language/zh-CN/login.json diff --git a/public/language/zh_CN/modules.json b/public/language/zh-CN/modules.json similarity index 100% rename from public/language/zh_CN/modules.json rename to public/language/zh-CN/modules.json diff --git a/public/language/zh_CN/notifications.json b/public/language/zh-CN/notifications.json similarity index 100% rename from public/language/zh_CN/notifications.json rename to public/language/zh-CN/notifications.json diff --git a/public/language/zh_CN/pages.json b/public/language/zh-CN/pages.json similarity index 100% rename from public/language/zh_CN/pages.json rename to public/language/zh-CN/pages.json diff --git a/public/language/zh_CN/recent.json b/public/language/zh-CN/recent.json similarity index 100% rename from public/language/zh_CN/recent.json rename to public/language/zh-CN/recent.json diff --git a/public/language/zh_CN/register.json b/public/language/zh-CN/register.json similarity index 100% rename from public/language/zh_CN/register.json rename to public/language/zh-CN/register.json diff --git a/public/language/zh_CN/reset_password.json b/public/language/zh-CN/reset_password.json similarity index 100% rename from public/language/zh_CN/reset_password.json rename to public/language/zh-CN/reset_password.json diff --git a/public/language/zh_CN/search.json b/public/language/zh-CN/search.json similarity index 100% rename from public/language/zh_CN/search.json rename to public/language/zh-CN/search.json diff --git a/public/language/zh_CN/success.json b/public/language/zh-CN/success.json similarity index 100% rename from public/language/zh_CN/success.json rename to public/language/zh-CN/success.json diff --git a/public/language/zh_CN/tags.json b/public/language/zh-CN/tags.json similarity index 100% rename from public/language/zh_CN/tags.json rename to public/language/zh-CN/tags.json diff --git a/public/language/zh_CN/topic.json b/public/language/zh-CN/topic.json similarity index 100% rename from public/language/zh_CN/topic.json rename to public/language/zh-CN/topic.json diff --git a/public/language/zh_CN/unread.json b/public/language/zh-CN/unread.json similarity index 100% rename from public/language/zh_CN/unread.json rename to public/language/zh-CN/unread.json diff --git a/public/language/zh_CN/uploads.json b/public/language/zh-CN/uploads.json similarity index 100% rename from public/language/zh_CN/uploads.json rename to public/language/zh-CN/uploads.json diff --git a/public/language/zh_CN/user.json b/public/language/zh-CN/user.json similarity index 98% rename from public/language/zh_CN/user.json rename to public/language/zh-CN/user.json index a487b7ee56..816df8ef63 100644 --- a/public/language/zh_CN/user.json +++ b/public/language/zh-CN/user.json @@ -31,7 +31,8 @@ "signature": "签名档", "birthday": "生日", "chat": "聊天", - "chat_with": "与 %1 聊天", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "关注", "unfollow": "取消关注", "more": "更多", diff --git a/public/language/zh_CN/users.json b/public/language/zh-CN/users.json similarity index 100% rename from public/language/zh_CN/users.json rename to public/language/zh-CN/users.json diff --git a/public/language/zh_TW/category.json b/public/language/zh-TW/category.json similarity index 100% rename from public/language/zh_TW/category.json rename to public/language/zh-TW/category.json diff --git a/public/language/zh_TW/email.json b/public/language/zh-TW/email.json similarity index 100% rename from public/language/zh_TW/email.json rename to public/language/zh-TW/email.json diff --git a/public/language/zh_TW/error.json b/public/language/zh-TW/error.json similarity index 100% rename from public/language/zh_TW/error.json rename to public/language/zh-TW/error.json diff --git a/public/language/zh_TW/global.json b/public/language/zh-TW/global.json similarity index 100% rename from public/language/zh_TW/global.json rename to public/language/zh-TW/global.json diff --git a/public/language/zh_TW/groups.json b/public/language/zh-TW/groups.json similarity index 100% rename from public/language/zh_TW/groups.json rename to public/language/zh-TW/groups.json diff --git a/public/language/zh_TW/language.json b/public/language/zh-TW/language.json similarity index 69% rename from public/language/zh_TW/language.json rename to public/language/zh-TW/language.json index a27c0a9ef8..de9713d95a 100644 --- a/public/language/zh_TW/language.json +++ b/public/language/zh-TW/language.json @@ -1,5 +1,5 @@ { "name": "繁體中文", - "code": "zh_TW", + "code": "zh-TW", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/zh_TW/login.json b/public/language/zh-TW/login.json similarity index 100% rename from public/language/zh_TW/login.json rename to public/language/zh-TW/login.json diff --git a/public/language/zh_TW/modules.json b/public/language/zh-TW/modules.json similarity index 100% rename from public/language/zh_TW/modules.json rename to public/language/zh-TW/modules.json diff --git a/public/language/zh_TW/notifications.json b/public/language/zh-TW/notifications.json similarity index 100% rename from public/language/zh_TW/notifications.json rename to public/language/zh-TW/notifications.json diff --git a/public/language/zh_TW/pages.json b/public/language/zh-TW/pages.json similarity index 100% rename from public/language/zh_TW/pages.json rename to public/language/zh-TW/pages.json diff --git a/public/language/zh_TW/recent.json b/public/language/zh-TW/recent.json similarity index 100% rename from public/language/zh_TW/recent.json rename to public/language/zh-TW/recent.json diff --git a/public/language/zh_TW/register.json b/public/language/zh-TW/register.json similarity index 100% rename from public/language/zh_TW/register.json rename to public/language/zh-TW/register.json diff --git a/public/language/zh_TW/reset_password.json b/public/language/zh-TW/reset_password.json similarity index 100% rename from public/language/zh_TW/reset_password.json rename to public/language/zh-TW/reset_password.json diff --git a/public/language/zh_TW/search.json b/public/language/zh-TW/search.json similarity index 100% rename from public/language/zh_TW/search.json rename to public/language/zh-TW/search.json diff --git a/public/language/zh_TW/success.json b/public/language/zh-TW/success.json similarity index 100% rename from public/language/zh_TW/success.json rename to public/language/zh-TW/success.json diff --git a/public/language/zh_TW/tags.json b/public/language/zh-TW/tags.json similarity index 100% rename from public/language/zh_TW/tags.json rename to public/language/zh-TW/tags.json diff --git a/public/language/zh_TW/topic.json b/public/language/zh-TW/topic.json similarity index 100% rename from public/language/zh_TW/topic.json rename to public/language/zh-TW/topic.json diff --git a/public/language/zh_TW/unread.json b/public/language/zh-TW/unread.json similarity index 100% rename from public/language/zh_TW/unread.json rename to public/language/zh-TW/unread.json diff --git a/public/language/zh_TW/uploads.json b/public/language/zh-TW/uploads.json similarity index 100% rename from public/language/zh_TW/uploads.json rename to public/language/zh-TW/uploads.json diff --git a/public/language/zh_TW/user.json b/public/language/zh-TW/user.json similarity index 98% rename from public/language/zh_TW/user.json rename to public/language/zh-TW/user.json index 7eebcd2ec8..ff00e53d09 100644 --- a/public/language/zh_TW/user.json +++ b/public/language/zh-TW/user.json @@ -31,7 +31,8 @@ "signature": "簽名", "birthday": "生日", "chat": "聊天", - "chat_with": "與 %1 聊天", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "跟隨", "unfollow": "取消跟隨", "more": "更多", diff --git a/public/language/zh_TW/users.json b/public/language/zh-TW/users.json similarity index 100% rename from public/language/zh_TW/users.json rename to public/language/zh-TW/users.json diff --git a/public/less/admin/manage/categories.less b/public/less/admin/manage/categories.less index 76e68f340a..0f13af4deb 100644 --- a/public/less/admin/manage/categories.less +++ b/public/less/admin/manage/categories.less @@ -88,3 +88,30 @@ div.categories { } } + +.category { + .privilege-table { + tr > th:first-child { + min-width: 150px; + } + + .privilege-table-header { + background: white; + + th { + text-align: center; + border-top: 0; + text-transform: uppercase; + font-size: 9px; + } + + .arrowed:after { + border-bottom: 1px dashed #ccc; + content: ""; + width: 100%; + display: block; + padding-top: 5px; + } + } + } +} \ No newline at end of file diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index 13a2340d5f..c7a874e96a 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -124,6 +124,16 @@ } function setupRestartLinks() { + $('.reload').off('click').on('click', function () { + bootbox.confirm('Are you sure you wish to reload NodeBB?', function (confirm) { + if (confirm) { + require(['admin/modules/instance'], function (instance) { + instance.reload(); + }); + } + }); + }); + $('.restart').off('click').on('click', function () { bootbox.confirm('Are you sure you wish to restart NodeBB?', function (confirm) { if (confirm) { @@ -133,13 +143,7 @@ } }); }); - - $('.reload').off('click').on('click', function () { - require(['admin/modules/instance'], function (instance) { - instance.reload(); - }); - }); - } + }; function launchSnackbar(params) { var message = (params.title ? "" + params.title + "" : '') + (params.message ? params.message : ''); @@ -148,7 +152,7 @@ translator.translate(message, function (html) { var bar = $.snackbar({ content: html, - timeout: 3000, + timeout: params.timeout || 3000, htmlAllowed: true }); diff --git a/public/src/admin/extend/plugins.js b/public/src/admin/extend/plugins.js index bb89256959..59ef9348ab 100644 --- a/public/src/admin/extend/plugins.js +++ b/public/src/admin/extend/plugins.js @@ -1,7 +1,7 @@ "use strict"; /* global define, app, socket, bootbox */ -define('admin/extend/plugins', function () { +define('admin/extend/plugins', ['jqueryui'], function (jqueryui) { var Plugins = {}; Plugins.init = function () { var pluginsList = $('.plugins'), diff --git a/public/src/admin/modules/instance.js b/public/src/admin/modules/instance.js index d5c2164155..d057ff9853 100644 --- a/public/src/admin/modules/instance.js +++ b/public/src/admin/modules/instance.js @@ -14,45 +14,12 @@ define('admin/modules/instance', function () { timeout: 5000 }); - socket.emit('admin.reload', function (err) { - if (!err) { - app.alert({ - alert_id: 'instance_reload', - type: 'success', - title: ' Success', - message: 'NodeBB has successfully reloaded.', - timeout: 5000 - }); - } else { - app.alert({ - alert_id: 'instance_reload', - type: 'danger', - title: '[[global:alert.error]]', - message: '[[error:reload-failed, ' + err.message + ']]' - }); - } - - if (typeof callback === 'function') { - callback(); - } - }); - }; - - instance.restart = function (callback) { - app.alert({ - alert_id: 'instance_restart', - type: 'info', - title: 'Restarting... ', - message: 'NodeBB is restarting.', - timeout: 5000 - }); - $(window).one('action:reconnected', function () { app.alert({ - alert_id: 'instance_restart', + alert_id: 'instance_reload', type: 'success', title: ' Success', - message: 'NodeBB has successfully restarted.', + message: 'NodeBB has reloaded successfully.', timeout: 5000 }); @@ -61,7 +28,41 @@ define('admin/modules/instance', function () { } }); - socket.emit('admin.restart'); + socket.emit('admin.reload'); + }; + + instance.restart = function (callback) { + app.alert({ + alert_id: 'instance_restart', + type: 'info', + title: 'Rebuilding... ', + message: 'NodeBB is rebiulding front-end assets (css, javascript, etc).', + timeout: 10000 + }); + + $(window).one('action:reconnected', function () { + app.alert({ + alert_id: 'instance_restart', + type: 'success', + title: ' Success', + message: 'NodeBB has successfully restarted.', + timeout: 10000 + }); + + if (typeof callback === 'function') { + callback(); + } + }); + + socket.emit('admin.restart', function () { + app.alert({ + alert_id: 'instance_restart', + type: 'info', + title: 'Build Complete!... ', + message: 'NodeBB is reloading.', + timeout: 10000 + }); + }); }; return instance; diff --git a/public/src/app.js b/public/src/app.js index 8ac3e1ebff..7c69d9fb61 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -23,6 +23,8 @@ app.cacheBuster = null; app.load = function () { app.loadProgressiveStylesheet(); + overrides.overrideTimeago(); + var url = ajaxify.start(window.location.pathname.slice(1) + window.location.search + window.location.hash); ajaxify.updateHistory(url, true); ajaxify.parseData(); @@ -52,7 +54,6 @@ app.cacheBuster = null; }); overrides.overrideBootbox(); - overrides.overrideTimeago(); createHeaderTooltips(); app.showEmailConfirmWarning(); app.showCookieWarning(); @@ -88,6 +89,14 @@ app.cacheBuster = null; app.logout = function () { $(window).trigger('action:app.logout'); + + /* + Set session refresh flag (otherwise the session check will trip and throw invalid session modal) + We know the session is/will be invalid (uid mismatch) because the user is logging out + */ + app.flags = app.flags || {}; + app.flags._sessionRefresh = true; + $.ajax(config.relative_path + '/logout', { type: 'POST', headers: { @@ -126,6 +135,8 @@ app.cacheBuster = null; }; app.alertError = function (message, timeout) { + message = message.message || message; + if (message === '[[error:invalid-session]]') { return app.handleInvalidSession(); } @@ -630,18 +641,23 @@ app.cacheBuster = null; } else if (window.localStorage.getItem('cookieconsent') === '1') { return; } + require(['translator'], function (translator) { + config.cookies.message = translator.unescape(config.cookies.message); + config.cookies.dismiss = translator.unescape(config.cookies.dismiss); + config.cookies.link = translator.unescape(config.cookies.link); - templates.parse('partials/cookie-consent', config.cookies, function (html) { - $(document.body).append(html); + app.parseAndTranslate('partials/cookie-consent', config.cookies, function (html) { + $(document.body).append(html); - var warningEl = $('.cookie-consent'); - var dismissEl = warningEl.find('button'); - dismissEl.on('click', function () { - // Save consent cookie and remove warning element - var now = new Date(); - window.localStorage.setItem('cookieconsent', '1'); - warningEl.remove(); + var warningEl = $('.cookie-consent'); + var dismissEl = warningEl.find('button'); + dismissEl.on('click', function () { + // Save consent cookie and remove warning element + window.localStorage.setItem('cookieconsent', '1'); + warningEl.remove(); + }); }); }); + }; }()); diff --git a/public/src/client/categories.js b/public/src/client/categories.js index 672fa38cf9..bd27aed0a2 100644 --- a/public/src/client/categories.js +++ b/public/src/client/categories.js @@ -63,7 +63,7 @@ define('forum/categories', ['components', 'translator'], function (components, t templates.parse('categories', '(categories.)?posts', {categories: {posts: posts}}, function (html) { translator.translate(html, function (translatedHTML) { translatedHTML = $(translatedHTML); - translatedHTML.find('img:not(.not-responsive)').addClass('img-responsive'); + translatedHTML.find('.post-content img:not(.not-responsive)').addClass('img-responsive'); callback(translatedHTML); }); diff --git a/public/src/client/category.js b/public/src/client/category.js index 7f64fd4f7f..4592c551a1 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -139,7 +139,7 @@ define('forum/category', [ $('[component="category"]').empty(); - loadTopicsAfter(Math.max(0, bookmarkIndex - 1), 1, function () { + loadTopicsAfter(Math.max(0, bookmarkIndex - 1) + 1, 1, function () { Category.scrollToTopic(bookmarkIndex, clickedIndex, 0); }); } @@ -261,7 +261,7 @@ define('forum/category', [ var topics = $('[component="category/topic"]'); var afterEl = direction > 0 ? topics.last() : topics.first(); - var after = parseInt(afterEl.attr('data-index'), 10) || 0; + var after = (parseInt(afterEl.attr('data-index'), 10) || 0) + 1; loadTopicsAfter(after, direction); }; diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index 3bce27f416..1ac8e2a2ff 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -27,6 +27,7 @@ define('forum/chats/messages', ['components', 'sounds', 'translator'], function message: msg }, function (err) { if (err) { + inputEl.val(msg); if (err.message === '[[error:email-not-confirmed-chat]]') { return app.showEmailConfirmWarning(err); } @@ -42,6 +43,8 @@ define('forum/chats/messages', ['components', 'sounds', 'translator'], function message: msg }, function (err) { if (err) { + inputEl.val(msg); + inputEl.attr('data-mid', mid); return app.alertError(err.message); } }); diff --git a/public/src/client/login.js b/public/src/client/login.js index f798347c73..1de1218fd0 100644 --- a/public/src/client/login.js +++ b/public/src/client/login.js @@ -23,6 +23,14 @@ define('forum/login', ['translator'], function (translator) { } submitEl.addClass('disabled'); + + /* + Set session refresh flag (otherwise the session check will trip and throw invalid session modal) + We know the session is/will be invalid (uid mismatch) because the user is attempting a login + */ + app.flags = app.flags || {}; + app.flags._sessionRefresh = true; + formEl.ajaxSubmit({ headers: { 'x-csrf-token': config.csrf_token @@ -37,6 +45,7 @@ define('forum/login', ['translator'], function (translator) { errorEl.find('p').translateText(data.responseText); errorEl.show(); submitEl.removeClass('disabled'); + app.flags._sessionRefresh = false; // Select the entire password if that field has focus if ($('#password:focus').size()) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 2283695d39..d90dc210d4 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -258,7 +258,7 @@ define('chat', [ Chats.addScrollHandler(chatModal.attr('roomId'), data.uid, chatModal.find('.chat-content')); taskbar.push('chat', chatModal.attr('UUID'), { - title: data.users.length ? data.users[0].username : '', + title: data.roomName || (data.users.length ? data.users[0].username : ''), roomId: data.roomId, icon: 'fa-comment', state: '' diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index fe377f987e..0771ab56a5 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -1,9 +1,9 @@ -/* global define, jQuery, config, RELATIVE_PATH, utils, window, Promise */ +/* global define, jQuery, config, utils, window, Promise */ (function (factory) { 'use strict'; - function loadClient(language, filename) { - return Promise.resolve(jQuery.getJSON(config.relative_path + '/language/' + language + '/' + (filename + '.json?v=' + config['cache-buster']))); + function loadClient(language, namespace) { + return Promise.resolve(jQuery.getJSON(config.relative_path + '/api/language/' + language + '/' + namespace)); } if (typeof define === 'function' && define.amd) { // AMD. Register as a named module @@ -16,9 +16,9 @@ require('promise-polyfill'); var languages = require('../../../src/languages'); - function loadServer(language, filename) { + function loadServer(language, namespace) { return new Promise(function (resolve, reject) { - languages.get(language, filename + '.json', function (err, data) { + languages.get(language, namespace, function (err, data) { if (err) { reject(err); } else { @@ -297,10 +297,10 @@ var lang; if (typeof window === 'object' && window.config && window.utils) { - lang = utils.params().lang || config.userLang || config.defaultLang || 'en_GB'; + lang = utils.params().lang || config.userLang || config.defaultLang || 'en-GB'; } else { var meta = require('../../../src/meta'); - lang = meta.config.defaultLang || 'en_GB'; + lang = meta.config.defaultLang || 'en-GB'; } return lang; @@ -308,7 +308,7 @@ /** * Create and cache a new Translator instance, or return a cached one - * @param {string} [language] - ('en_GB') Language string + * @param {string} [language] - ('en-GB') Language string * @returns {Translator} */ Translator.create = function create(language) { @@ -401,8 +401,8 @@ /** * Add translations to the cache */ - addTranslation: function addTranslation(language, filename, translation) { - Translator.create(language).getTranslation(filename).then(function (translations) { + addTranslation: function addTranslation(language, namespace, translation) { + Translator.create(language).getTranslation(namespace).then(function (translations) { assign(translations, translation); }); }, @@ -410,16 +410,16 @@ /** * Get the translations object */ - getTranslations: function getTranslations(language, filename, callback) { + getTranslations: function getTranslations(language, namespace, callback) { callback = callback || function () {}; - Translator.create(language).getTranslation(filename).then(callback); + Translator.create(language).getTranslation(namespace).then(callback); }, /** * Alias of getTranslations */ - load: function load(language, filename, callback) { - adaptor.getTranslations(language, filename, callback); + load: function load(language, namespace, callback) { + adaptor.getTranslations(language, namespace, callback); }, /** @@ -437,16 +437,16 @@ // and correct NodeBB language codes to timeago codes, if necessary var languageCode = void 0; switch (config.userLang) { - case 'en_GB': - case 'en_US': + case 'en-GB': + case 'en-US': languageCode = 'en'; break; - case 'fa_IR': + case 'fa-IR': languageCode = 'fa'; break; - case 'pt_BR': + case 'pt-BR': languageCode = 'pt-br'; break; @@ -454,25 +454,17 @@ languageCode = 'no'; break; - case 'zh_TW': - languageCode = 'zh-TW'; - break; - - case 'zh_CN': - languageCode = 'zh-CN'; - break; - default: languageCode = config.userLang; break; } - jQuery.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function () { + jQuery.getScript(config.relative_path + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function () { jQuery('.timeago').timeago(); adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings); // Retrieve the shorthand timeago values as well - jQuery.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () { + jQuery.getScript(config.relative_path + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () { // Switch back to long-form adaptor.toggleTimeagoShorthand(); }); diff --git a/public/src/overrides.js b/public/src/overrides.js index a2d213f45c..a5216910d2 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -150,15 +150,25 @@ if ('undefined' !== typeof window) { overrides.overrideTimeago = function () { var timeagoFn = $.fn.timeago; - $.timeago.settings.cutoff = 1000 * 60 * 60 * 24 * 30; - $.fn.timeago = function () { - var els = timeagoFn.apply(this, arguments); + if (parseInt(config.timeagoCutoff, 10) === 0) { + $.timeago.settings.cutoff = 1; + } else if (parseInt(config.timeagoCutoff, 10) > 0) { + $.timeago.settings.cutoff = 1000 * 60 * 60 * 24 * (parseInt(config.timeagoCutoff, 10) || 30); + } - if (els) { - els.each(function () { - $(this).attr('title', (new Date($(this).attr('title'))).toString()); - }); - } + $.fn.timeago = function () { + var els = $(this); + + // Convert "old" format to new format (#5108) + var options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; + var iso; + els.each(function () { + iso = this.getAttribute('title'); + this.setAttribute('datetime', iso); + $(this).text(new Date(iso).toLocaleString(config.userLang.replace('_', '-'), options)); + }); + + timeagoFn.apply(this, arguments); }; }; diff --git a/public/vendor/jquery/timeago/jquery.timeago.js b/public/vendor/jquery/timeago/jquery.timeago.js index 8727fa7adb..c85ebcc5b0 100644 --- a/public/vendor/jquery/timeago/jquery.timeago.js +++ b/public/vendor/jquery/timeago/jquery.timeago.js @@ -194,41 +194,7 @@ $(this).text(inWords(data.datetime)); } else { if ($(this).attr('title').length > 0) { - //$(this).text($(this).attr('title')); - var languageCode = void 0; - switch (config.userLang) { - case 'en_GB': - case 'en_US': - languageCode = 'en'; - break; - - case 'fa_IR': - languageCode = 'fa'; - break; - - case 'pt_BR': - languageCode = 'pt-br'; - break; - - case 'nb': - languageCode = 'no'; - break; - - case 'zh_TW': - languageCode = 'zh-TW'; - break; - - case 'zh_CN': - languageCode = 'zh-CN'; - break; - - default: - languageCode = config.userLang; - break; - } - - var options = { year: 'numeric', month: 'long', day: 'numeric' }; - $(this).text(new Date($(this).attr('title')).toLocaleString(languageCode, options)); + $(this).text($(this).attr('title')); } } } diff --git a/src/analytics.js b/src/analytics.js index ab834b75b2..6b248057da 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -6,192 +6,194 @@ var winston = require('winston'); var db = require('./database'); -(function (Analytics) { - var counters = {}; +var Analytics = module.exports; - var pageViews = 0; - var uniqueIPCount = 0; - var uniquevisitors = 0; +var counters = {}; - var isCategory = /^(?:\/api)?\/category\/(\d+)/; +var pageViews = 0; +var uniqueIPCount = 0; +var uniquevisitors = 0; - new cronJob('*/10 * * * *', function () { - Analytics.writeData(); - }, null, true); +var isCategory = /^(?:\/api)?\/category\/(\d+)/; - Analytics.increment = function (keys) { - keys = Array.isArray(keys) ? keys : [keys]; +new cronJob('*/10 * * * *', function () { + Analytics.writeData(); +}, null, true); - keys.forEach(function (key) { - counters[key] = counters[key] || 0; - ++counters[key]; - }); - }; +Analytics.increment = function (keys) { + keys = Array.isArray(keys) ? keys : [keys]; - Analytics.pageView = function (payload) { - ++pageViews; + keys.forEach(function (key) { + counters[key] = counters[key] || 0; + ++counters[key]; + }); +}; - if (payload.ip) { - db.sortedSetScore('ip:recent', payload.ip, function (err, score) { - if (err) { - return; - } - if (!score) { - ++uniqueIPCount; - } - var today = new Date(); - today.setHours(today.getHours(), 0, 0, 0); - if (!score || score < today.getTime()) { - ++uniquevisitors; - db.sortedSetAdd('ip:recent', Date.now(), payload.ip); - } - }); - } +Analytics.pageView = function (payload) { + ++pageViews; - if (payload.path) { - var categoryMatch = payload.path.match(isCategory), - cid = categoryMatch ? parseInt(categoryMatch[1], 10) : null; - - if (cid) { - Analytics.increment(['pageviews:byCid:' + cid]); - } - } - }; - - Analytics.writeData = function () { - var today = new Date(); - var month = new Date(); - var dbQueue = []; - - today.setHours(today.getHours(), 0, 0, 0); - month.setMonth(month.getMonth(), 1); - month.setHours(0, 0, 0, 0); - - if (pageViews > 0) { - dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews', pageViews, today.getTime())); - dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month', pageViews, month.getTime())); - pageViews = 0; - } - - if (uniquevisitors > 0) { - dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:uniquevisitors', uniquevisitors, today.getTime())); - uniquevisitors = 0; - } - - if (uniqueIPCount > 0) { - dbQueue.push(async.apply(db.incrObjectFieldBy, 'global', 'uniqueIPCount', uniqueIPCount)); - uniqueIPCount = 0; - } - - if (Object.keys(counters).length > 0) { - for(var key in counters) { - if (counters.hasOwnProperty(key)) { - dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:' + key, counters[key], today.getTime())); - delete counters[key]; - } - } - } - - async.parallel(dbQueue, function (err) { + if (payload.ip) { + db.sortedSetScore('ip:recent', payload.ip, function (err, score) { if (err) { - winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message); + return; + } + if (!score) { + ++uniqueIPCount; + } + var today = new Date(); + today.setHours(today.getHours(), 0, 0, 0); + if (!score || score < today.getTime()) { + ++uniquevisitors; + db.sortedSetAdd('ip:recent', Date.now(), payload.ip); } }); - }; + } - Analytics.getHourlyStatsForSet = function (set, hour, numHours, callback) { - var terms = {}, - hoursArr = []; + if (payload.path) { + var categoryMatch = payload.path.match(isCategory), + cid = categoryMatch ? parseInt(categoryMatch[1], 10) : null; - hour = new Date(hour); - hour.setHours(hour.getHours(), 0, 0, 0); + if (cid) { + Analytics.increment(['pageviews:byCid:' + cid]); + } + } +}; - for (var i = 0, ii = numHours; i < ii; i++) { - hoursArr.push(hour.getTime()); - hour.setHours(hour.getHours() - 1, 0, 0, 0); +Analytics.writeData = function (callback) { + callback = callback || function () {}; + var today = new Date(); + var month = new Date(); + var dbQueue = []; + + today.setHours(today.getHours(), 0, 0, 0); + month.setMonth(month.getMonth(), 1); + month.setHours(0, 0, 0, 0); + + if (pageViews > 0) { + dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews', pageViews, today.getTime())); + dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month', pageViews, month.getTime())); + pageViews = 0; + } + + if (uniquevisitors > 0) { + dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:uniquevisitors', uniquevisitors, today.getTime())); + uniquevisitors = 0; + } + + if (uniqueIPCount > 0) { + dbQueue.push(async.apply(db.incrObjectFieldBy, 'global', 'uniqueIPCount', uniqueIPCount)); + uniqueIPCount = 0; + } + + if (Object.keys(counters).length > 0) { + for(var key in counters) { + if (counters.hasOwnProperty(key)) { + dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:' + key, counters[key], today.getTime())); + delete counters[key]; + } + } + } + + async.parallel(dbQueue, function (err) { + if (err) { + winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message); + } + callback(err); + }); +}; + +Analytics.getHourlyStatsForSet = function (set, hour, numHours, callback) { + var terms = {}, + hoursArr = []; + + hour = new Date(hour); + hour.setHours(hour.getHours(), 0, 0, 0); + + for (var i = 0, ii = numHours; i < ii; i++) { + hoursArr.push(hour.getTime()); + hour.setHours(hour.getHours() - 1, 0, 0, 0); + } + + db.sortedSetScores(set, hoursArr, function (err, counts) { + if (err) { + return callback(err); } - db.sortedSetScores(set, hoursArr, function (err, counts) { + hoursArr.forEach(function (term, index) { + terms[term] = parseInt(counts[index], 10) || 0; + }); + + var termsArr = []; + + hoursArr.reverse(); + hoursArr.forEach(function (hour) { + termsArr.push(terms[hour]); + }); + + callback(null, termsArr); + }); +}; + +Analytics.getDailyStatsForSet = function (set, day, numDays, callback) { + var daysArr = []; + + day = new Date(day); + day.setDate(day.getDate() + 1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values + day.setHours(0, 0, 0, 0); + + async.whilst(function () { + return numDays--; + }, function (next) { + Analytics.getHourlyStatsForSet(set, day.getTime() - (1000 * 60 * 60 * 24 * numDays), 24, function (err, day) { if (err) { - return callback(err); + return next(err); } - hoursArr.forEach(function (term, index) { - terms[term] = parseInt(counts[index], 10) || 0; - }); - - var termsArr = []; - - hoursArr.reverse(); - hoursArr.forEach(function (hour) { - termsArr.push(terms[hour]); - }); - - callback(null, termsArr); + daysArr.push(day.reduce(function (cur, next) { + return cur + next; + })); + next(); }); - }; + }, function (err) { + callback(err, daysArr); + }); +}; - Analytics.getDailyStatsForSet = function (set, day, numDays, callback) { - var daysArr = []; +Analytics.getUnwrittenPageviews = function () { + return pageViews; +}; - day = new Date(day); - day.setDate(day.getDate() + 1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values - day.setHours(0, 0, 0, 0); +Analytics.getMonthlyPageViews = function (callback) { + var thisMonth = new Date(); + var lastMonth = new Date(); + thisMonth.setMonth(thisMonth.getMonth(), 1); + thisMonth.setHours(0, 0, 0, 0); + lastMonth.setMonth(thisMonth.getMonth() - 1, 1); + lastMonth.setHours(0, 0, 0, 0); - async.whilst(function () { - return numDays--; - }, function (next) { - Analytics.getHourlyStatsForSet(set, day.getTime() - (1000 * 60 * 60 * 24 * numDays), 24, function (err, day) { - if (err) { - return next(err); - } + var values = [thisMonth.getTime(), lastMonth.getTime()]; - daysArr.push(day.reduce(function (cur, next) { - return cur + next; - })); - next(); - }); - }, function (err) { - callback(err, daysArr); - }); - }; + db.sortedSetScores('analytics:pageviews:month', values, function (err, scores) { + if (err) { + return callback(err); + } + callback(null, {thisMonth: scores[0] || 0, lastMonth: scores[1] || 0}); + }); +}; - Analytics.getUnwrittenPageviews = function () { - return pageViews; - }; +Analytics.getCategoryAnalytics = function (cid, callback) { + async.parallel({ + 'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24), + 'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30), + 'topics:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:topics:byCid:' + cid, Date.now(), 7), + 'posts:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:posts:byCid:' + cid, Date.now(), 7), + }, callback); +}; - Analytics.getMonthlyPageViews = function (callback) { - var thisMonth = new Date(); - var lastMonth = new Date(); - thisMonth.setMonth(thisMonth.getMonth(), 1); - thisMonth.setHours(0, 0, 0, 0); - lastMonth.setMonth(thisMonth.getMonth() - 1, 1); - lastMonth.setHours(0, 0, 0, 0); +Analytics.getErrorAnalytics = function (callback) { + async.parallel({ + 'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7), + 'toobusy': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7) + }, callback); +}; - var values = [thisMonth.getTime(), lastMonth.getTime()]; - - db.sortedSetScores('analytics:pageviews:month', values, function (err, scores) { - if (err) { - return callback(err); - } - callback(null, {thisMonth: scores[0] || 0, lastMonth: scores[1] || 0}); - }); - }; - - Analytics.getCategoryAnalytics = function (cid, callback) { - async.parallel({ - 'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24), - 'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30), - 'topics:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:topics:byCid:' + cid, Date.now(), 7), - 'posts:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:posts:byCid:' + cid, Date.now(), 7), - }, callback); - }; - - Analytics.getErrorAnalytics = function (callback) { - async.parallel({ - 'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7), - 'toobusy': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7) - }, callback); - }; - -}(exports)); \ No newline at end of file diff --git a/src/categories.js b/src/categories.js index 65c60bb58c..28d99a7d7f 100644 --- a/src/categories.js +++ b/src/categories.js @@ -67,16 +67,7 @@ var privileges = require('./privileges'); }; Categories.isIgnored = function (cids, uid, callback) { - user.getIgnoredCategories(uid, function (err, ignoredCids) { - if (err) { - return callback(err); - } - - cids = cids.map(function (cid) { - return ignoredCids.indexOf(cid.toString()) !== -1; - }); - callback(null, cids); - }); + db.isSortedSetMembers('uid:' + uid + ':ignored:cids', cids, callback); }; Categories.getPageCount = function (cid, uid, callback) { diff --git a/src/categories/delete.js b/src/categories/delete.js index 5c8e5bdf9a..f91842e487 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -11,19 +11,30 @@ var privileges = require('../privileges'); module.exports = function (Categories) { Categories.purge = function (cid, uid, callback) { - batch.processSortedSet('cid:' + cid + ':tids', function (tids, next) { - async.eachLimit(tids, 10, function (tid, next) { - topics.purgePostsAndTopic(tid, uid, next); - }, next); - }, {alwaysStartAt: 0}, function (err) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + batch.processSortedSet('cid:' + cid + ':tids', function (tids, next) { + async.eachLimit(tids, 10, function (tid, next) { + topics.purgePostsAndTopic(tid, uid, next); + }, next); + }, {alwaysStartAt: 0}, next); + }, + function (next) { + Categories.getPinnedTids('cid:' + cid + ':tids:pinned', 0, -1, next); + }, + function (pinnedTids, next) { + async.eachLimit(pinnedTids, 10, function (tid, next) { + topics.purgePostsAndTopic(tid, uid, next); + }, next); + }, + function (next) { + purgeCategory(cid, next); + }, + function (next) { + plugins.fireHook('action:category.delete', cid); + next(); } - async.series([ - async.apply(purgeCategory, cid), - async.apply(plugins.fireHook, 'action:category.delete', cid) - ], callback); - }); + ], callback); }; function purgeCategory(cid, callback) { @@ -37,6 +48,7 @@ module.exports = function (Categories) { function (next) { db.deleteAll([ 'cid:' + cid + ':tids', + 'cid:' + cid + ':tids:pinned', 'cid:' + cid + ':tids:posts', 'cid:' + cid + ':pids', 'cid:' + cid + ':read_by_uid', @@ -50,7 +62,9 @@ module.exports = function (Categories) { groups.destroy('cid:' + cid + ':privileges:' + privilege, next); }, next); } - ], callback); + ], function (err) { + callback(err); + }); } function removeFromParent(cid, callback) { diff --git a/src/categories/topics.js b/src/categories/topics.js index ec211da960..c60050c667 100644 --- a/src/categories/topics.js +++ b/src/categories/topics.js @@ -14,7 +14,7 @@ module.exports = function (Categories) { plugins.fireHook('filter:category.topics.prepare', data, next); }, function (data, next) { - Categories.getTopicIds(data.set, data.reverse, data.start, data.stop, next); + Categories.getTopicIds(data.cid, data.set, data.reverse, data.start, data.stop, next); }, function (tids, next) { topics.getTopicsByTids(tids, data.uid, next); @@ -36,6 +36,58 @@ module.exports = function (Categories) { ], callback); }; + Categories.getTopicIds = function (cid, set, reverse, start, stop, callback) { + var pinnedTids; + var pinnedCount; + var totalPinnedCount; + + async.waterfall([ + function (next) { + Categories.getPinnedTids(cid, 0, -1, next); + }, + function (_pinnedTids, next) { + totalPinnedCount = _pinnedTids.length; + + pinnedTids = _pinnedTids.slice(start, stop === -1 ? undefined : stop + 1); + + pinnedCount = pinnedTids.length; + + var topicsPerPage = stop - start + 1; + + var normalTidsToGet = Math.max(0, topicsPerPage - pinnedCount); + + if (!normalTidsToGet && stop !== -1) { + return next(null, []); + } + if (start > 0 && totalPinnedCount) { + start -= totalPinnedCount - pinnedCount; + } + stop = stop === -1 ? stop : start + normalTidsToGet - 1; + + if (Array.isArray(set)) { + db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({sets: set, start: start, stop: stop}, next); + } else { + db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, next); + } + }, + function (normalTids, next) { + normalTids = normalTids.filter(function (tid) { + return pinnedTids.indexOf(tid) === -1; + }); + + next(null, pinnedTids.concat(normalTids)); + } + ], callback); + }; + + Categories.getAllTopicIds = function (cid, start, stop, callback) { + db.getSortedSetRange(['cid:' + cid + ':tids:pinned', 'cid:' + cid + ':tids'], start, stop, callback); + }; + + Categories.getPinnedTids = function (cid, start, stop, callback) { + db.getSortedSetRevRange('cid:' + cid + ':tids:pinned', start, stop, callback); + }; + Categories.modifyTopicsByPrivilege = function (topics, privileges) { if (!Array.isArray(topics) || !topics.length || privileges.isAdminOrMod) { return; @@ -52,22 +104,9 @@ module.exports = function (Categories) { }); }; - Categories.getTopicIds = function (set, reverse, start, stop, callback) { - if (Array.isArray(set)) { - db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({sets: set, start: start, stop: stop}, callback); - } else { - db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback); - } - }; - Categories.getTopicIndex = function (tid, callback) { - topics.getTopicField(tid, 'cid', function (err, cid) { - if (err) { - return callback(err); - } - - db.sortedSetRevRank('cid:' + cid + ':tids', tid, callback); - }); + console.warn('[Categories.getTopicIndex] deprecated'); + callback(null, 1); }; Categories.onNewPostMade = function (cid, pinned, postData, callback) { diff --git a/src/categories/unread.js b/src/categories/unread.js index 3805e1e78f..3a1ba27277 100644 --- a/src/categories/unread.js +++ b/src/categories/unread.js @@ -33,6 +33,9 @@ module.exports = function (Categories) { }; Categories.markAsUnreadForAll = function (cid, callback) { + if (!parseInt(cid, 10)) { + return callback(); + } callback = callback || function () {}; db.delete('cid:' + cid + ':read_by_uid', callback); }; diff --git a/src/categories/update.js b/src/categories/update.js index 4f4229b5fc..485d3e3834 100644 --- a/src/categories/update.js +++ b/src/categories/update.js @@ -21,24 +21,30 @@ module.exports = function (Categories) { }; function updateCategory(cid, modifiedFields, callback) { - Categories.exists(cid, function (err, exists) { - if (err || !exists) { - return callback(err); - } - - - if (modifiedFields.hasOwnProperty('name')) { - translator.translate(modifiedFields.name, function (translated) { - modifiedFields.slug = cid + '/' + utils.slugify(translated); - }); - } - - plugins.fireHook('filter:category.update', {category: modifiedFields}, function (err, categoryData) { - if (err) { - return callback(err); + var category; + async.waterfall([ + function (next) { + Categories.exists(cid, next); + }, + function (exists, next) { + if (!exists) { + return callback(); } - var category = categoryData.category; + if (modifiedFields.hasOwnProperty('name')) { + translator.translate(modifiedFields.name, function (translated) { + modifiedFields.slug = cid + '/' + utils.slugify(translated); + next(); + }); + } else { + next(); + } + }, + function (next) { + plugins.fireHook('filter:category.update', {category: modifiedFields}, next); + }, + function (categoryData, next) { + category = categoryData.category; var fields = Object.keys(category); // move parent to front, so its updated first var parentCidIndex = fields.indexOf('parentCid'); @@ -48,15 +54,13 @@ module.exports = function (Categories) { async.eachSeries(fields, function (key, next) { updateCategoryField(cid, key, category[key], next); - }, function (err) { - if (err) { - return callback(err); - } - plugins.fireHook('action:category.update', {cid: cid, modified: category}); - callback(); - }); - }); - }); + }, next); + }, + function (next) { + plugins.fireHook('action:category.update', {cid: cid, modified: category}); + next(); + } + ], callback); } function updateCategoryField(cid, key, value, callback) { @@ -64,73 +68,80 @@ module.exports = function (Categories) { return updateParent(cid, value, callback); } - db.setObjectField('category:' + cid, key, value, function (err) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.setObjectField('category:' + cid, key, value, next); + }, + function (next) { + if (key === 'order') { + updateOrder(cid, value, next); + } else if (key === 'description') { + Categories.parseDescription(cid, value, next); + } else { + next(); + } } - - if (key === 'order') { - updateOrder(cid, value, callback); - } else if (key === 'description') { - Categories.parseDescription(cid, value, callback); - } else { - callback(); - } - }); + ], callback); } function updateParent(cid, newParent, callback) { if (parseInt(cid, 10) === parseInt(newParent, 10)) { return callback(new Error('[[error:cant-set-self-as-parent]]')); } - Categories.getCategoryField(cid, 'parentCid', function (err, oldParent) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + Categories.getCategoryField(cid, 'parentCid', next); + }, + function (oldParent, next) { + async.series([ + function (next) { + oldParent = parseInt(oldParent, 10) || 0; + db.sortedSetRemove('cid:' + oldParent + ':children', cid, next); + }, + function (next) { + newParent = parseInt(newParent, 10) || 0; + db.sortedSetAdd('cid:' + newParent + ':children', cid, cid, next); + }, + function (next) { + db.setObjectField('category:' + cid, 'parentCid', newParent, next); + } + ], next); } - - async.series([ - function (next) { - oldParent = parseInt(oldParent, 10) || 0; - db.sortedSetRemove('cid:' + oldParent + ':children', cid, next); - }, - function (next) { - newParent = parseInt(newParent, 10) || 0; - db.sortedSetAdd('cid:' + newParent + ':children', cid, cid, next); - }, - function (next) { - db.setObjectField('category:' + cid, 'parentCid', newParent, next); - } - ], function (err) { - callback(err); - }); + ], function (err) { + callback(err); }); } function updateOrder(cid, order, callback) { - Categories.getCategoryField(cid, 'parentCid', function (err, parentCid) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + Categories.getCategoryField(cid, 'parentCid', next); + }, + function (parentCid, next) { + async.parallel([ + function (next) { + db.sortedSetAdd('categories:cid', order, cid, next); + }, + function (next) { + parentCid = parseInt(parentCid, 10) || 0; + db.sortedSetAdd('cid:' + parentCid + ':children', order, cid, next); + } + ], next); } - - async.parallel([ - function (next) { - db.sortedSetAdd('categories:cid', order, cid, next); - }, - function (next) { - parentCid = parseInt(parentCid, 10) || 0; - db.sortedSetAdd('cid:' + parentCid + ':children', order, cid, next); - } - ], callback); + ], function (err) { + callback(err); }); } Categories.parseDescription = function (cid, description, callback) { - plugins.fireHook('filter:parse.raw', description, function (err, parsedDescription) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + plugins.fireHook('filter:parse.raw', description, next); + }, + function (parsedDescription, next) { + Categories.setCategoryField(cid, 'descriptionParsed', parsedDescription, next); } - Categories.setCategoryField(cid, 'descriptionParsed', parsedDescription, callback); - }); + ], callback); }; }; diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index f70b9b4db8..c951bafdbd 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -56,13 +56,14 @@ chatsController.get = function (req, res, callback) { } async.parallel({ users: async.apply(messaging.getUsersInRoom, req.params.roomid, 0, -1), + canReply: async.apply(messaging.canReply, req.params.roomid, req.uid), + room: async.apply(messaging.getRoomData, req.params.roomid), messages: async.apply(messaging.getMessages, { callerUid: req.uid, uid: uid, roomId: req.params.roomid, isNew: false - }), - room: async.apply(messaging.getRoomData, req.params.roomid) + }) }, next); } ], function (err, data) { @@ -77,6 +78,7 @@ chatsController.get = function (req, res, callback) { return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid; }); + room.canReply = data.canReply; room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; room.rooms = recentChats.rooms; room.uid = uid; diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index 3fe2c57092..c9549a1def 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -12,6 +12,7 @@ var plugins = require('../../plugins'); var helpers = require('../helpers'); var groups = require('../../groups'); var accountHelpers = require('./helpers'); +var privileges = require('../../privileges'); var editController = {}; @@ -62,7 +63,7 @@ editController.email = function (req, res, next) { function renderRoute(name, req, res, next) { getUserData(req, next, function (err, userData) { - if (err) { + if (err || !userData) { return next(err); } if ((name === 'username' && userData['username:disableEdit']) || (name === 'email' && userData['email:disableEdit'])) { @@ -93,7 +94,7 @@ function getUserData(req, next, callback) { function (data, next) { userData = data; if (!userData) { - return next(); + return callback(); } db.getObjectField('user:' + userData.uid, 'password', next); } @@ -118,11 +119,8 @@ editController.uploadPicture = function (req, res, next) { }, function (uid, next) { updateUid = uid; - if (parseInt(req.uid, 10) === parseInt(uid, 10)) { - return next(null, true); - } - user.isAdminOrGlobalMod(req.uid, next); + privileges.users.canEdit(req.uid, uid, next); }, function (isAllowed, next) { if (!isAllowed) { diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 1e120cc0d0..7a90930053 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -24,13 +24,16 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { } async.parallel({ - userData : function (next) { + userData: function (next) { user.getUserData(uid, next); }, - userSettings : function (next) { + isTargetAdmin: function (next) { + user.isAdministrator(uid, next); + }, + userSettings: function (next) { user.getSettings(uid, next); }, - isAdmin : function (next) { + isAdmin: function (next) { user.isAdministrator(callerUID, next); }, isGlobalModerator: function (next) { @@ -87,7 +90,7 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { userData.fullname = ''; } - if (isAdmin || isGlobalModerator || isSelf) { + if (isAdmin || isSelf || (isGlobalModerator && !results.isTargetAdmin)) { userData.ips = results.ips; } @@ -98,16 +101,18 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { userData.uid = userData.uid; userData.yourid = callerUID; userData.theirid = userData.uid; + userData.isTargetAdmin = results.isTargetAdmin; userData.isAdmin = isAdmin; userData.isGlobalModerator = isGlobalModerator; userData.isModerator = isModerator; userData.isAdminOrGlobalModerator = isAdmin || isGlobalModerator; userData.isAdminOrGlobalModeratorOrModerator = isAdmin || isGlobalModerator || isModerator; - userData.canBan = isAdmin || isGlobalModerator; + userData.canEdit = isAdmin || (isGlobalModerator && !results.isTargetAdmin); + userData.canBan = isAdmin || (isGlobalModerator && !results.isTargetAdmin); userData.canChangePassword = isAdmin || (isSelf && parseInt(meta.config['password:disableEdit'], 10) !== 1); userData.isSelf = isSelf; userData.isFollowing = results.isFollowing; - userData.showHidden = isSelf || isAdmin || isGlobalModerator; + userData.showHidden = isSelf || isAdmin || (isGlobalModerator && !results.isTargetAdmin); userData.groups = Array.isArray(results.groups) && results.groups.length ? results.groups[0] : []; userData.disableSignatures = meta.config.disableSignatures !== undefined && parseInt(meta.config.disableSignatures, 10) === 1; userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js index 53ff073dad..15cab4f4ca 100644 --- a/src/controllers/accounts/posts.js +++ b/src/controllers/accounts/posts.js @@ -97,56 +97,63 @@ postsController.getTopics = function (req, res, next) { getFromUserSet(data, req, res, next); }; -function getFromUserSet(data, req, res, next) { - async.parallel({ - settings: function (next) { - user.getSettings(req.uid, next); - }, - userData: function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); - } - }, function (err, results) { - if (err || !results.userData) { - return next(err); - } - - var userData = results.userData; - - var setName = 'uid:' + userData.uid + ':' + data.set; - - var page = Math.max(1, parseInt(req.query.page, 10) || 1); - var itemsPerPage = (data.template === 'account/topics' || data.template === 'account/watched') ? results.settings.topicsPerPage : results.settings.postsPerPage; - - async.parallel({ - itemCount: function (next) { - if (results.settings.usePagination) { - db.sortedSetCard(setName, next); - } else { - next(null, 0); +function getFromUserSet(data, req, res, callback) { + var userData; + var itemsPerPage; + var page = Math.max(1, parseInt(req.query.page, 10) || 1); + async.waterfall([ + function (next) { + async.parallel({ + settings: function (next) { + user.getSettings(req.uid, next); + }, + userData: function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); } - }, - data: function (next) { - var start = (page - 1) * itemsPerPage; - var stop = start + itemsPerPage - 1; - data.method(setName, req.uid, start, stop, next); - } - }, function (err, results) { - if (err) { - return next(err); + }, next); + }, + function (results, next) { + if (!results.userData) { + return callback(); } - userData[data.type] = results.data[data.type]; - userData.nextStart = results.data.nextStart; + userData = results.userData; - var pageCount = Math.ceil(results.itemCount / itemsPerPage); - userData.pagination = pagination.create(page, pageCount); + var setName = 'uid:' + userData.uid + ':' + data.set; - userData.noItemsFoundKey = data.noItemsFoundKey; - userData.title = '[[pages:' + data.template + ', ' + userData.username + ']]'; - userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: data.crumb}]); + itemsPerPage = (data.template === 'account/topics' || data.template === 'account/watched') ? results.settings.topicsPerPage : results.settings.postsPerPage; - res.render(data.template, userData); - }); + async.parallel({ + itemCount: function (next) { + if (results.settings.usePagination) { + db.sortedSetCard(setName, next); + } else { + next(null, 0); + } + }, + data: function (next) { + var start = (page - 1) * itemsPerPage; + var stop = start + itemsPerPage - 1; + data.method(setName, req.uid, start, stop, next); + } + }, next); + } + ], function (err, results) { + if (err) { + return callback(err); + } + + userData[data.type] = results.data[data.type]; + userData.nextStart = results.data.nextStart; + + var pageCount = Math.ceil(results.itemCount / itemsPerPage); + userData.pagination = pagination.create(page, pageCount); + + userData.noItemsFoundKey = data.noItemsFoundKey; + userData.title = '[[pages:' + data.template + ', ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: data.crumb}]); + + res.render(data.template, userData); }); } diff --git a/src/controllers/admin/groups.js b/src/controllers/admin/groups.js index 38563d96a5..db940c8324 100644 --- a/src/controllers/admin/groups.js +++ b/src/controllers/admin/groups.js @@ -44,7 +44,7 @@ groupsController.list = function (req, res, next) { res.render('admin/manage/groups', { groups: data.groups, pagination: data.pagination, - yourid: req.user.uid + yourid: req.uid }); }); }; diff --git a/src/controllers/admin/languages.js b/src/controllers/admin/languages.js index 292cd2a3b4..2b458d1508 100644 --- a/src/controllers/admin/languages.js +++ b/src/controllers/admin/languages.js @@ -13,7 +13,7 @@ languagesController.get = function (req, res, next) { } languages.forEach(function (language) { - language.selected = language.code === (meta.config.defaultLang || 'en_GB'); + language.selected = language.code === (meta.config.defaultLang || 'en-GB'); }); res.render('admin/general/languages', { diff --git a/src/controllers/api.js b/src/controllers/api.js index cd877aea7d..f9e317cee7 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -12,6 +12,7 @@ var categories = require('../categories'); var privileges = require('../privileges'); var plugins = require('../plugins'); var widgets = require('../widgets'); +var translator = require('../../public/src/modules/translator'); var accountHelpers = require('../controllers/accounts/helpers'); var apiController = {}; @@ -52,7 +53,7 @@ apiController.getConfig = function (req, res, next) { config.maximumFileSize = meta.config.maximumFileSize; config['theme:id'] = meta.config['theme:id']; config['theme:src'] = meta.config['theme:src']; - config.defaultLang = meta.config.defaultLang || 'en_GB'; + config.defaultLang = meta.config.defaultLang || 'en-GB'; config.userLang = req.query.lang ? validator.escape(String(req.query.lang)) : config.defaultLang; config.loggedIn = !!req.user; config['cache-buster'] = meta.config['cache-buster'] || ''; @@ -63,11 +64,14 @@ apiController.getConfig = function (req, res, next) { config.searchEnabled = plugins.hasListeners('filter:search.query'); config.bootswatchSkin = 'default'; + var timeagoCutoff = meta.config.timeagoCutoff === undefined ? 30 : meta.config.timeagoCutoff; + config.timeagoCutoff = timeagoCutoff !== '' ? Math.max(0, parseInt(timeagoCutoff, 10)) : timeagoCutoff; + config.cookies = { enabled: parseInt(meta.config.cookieConsentEnabled, 10) === 1, - message: meta.config.cookieConsentMessage || '[[global:cookies.message]]', - dismiss: meta.config.cookieConsentDismiss || '[[global:cookies.accept]]', - link: meta.config.cookieConsentLink || '[[global:cookies.learn_more]]' + message: translator.escape(meta.config.cookieConsentMessage || '[[global:cookies.message]]').replace(/\\/g, '\\\\'), + dismiss: translator.escape(meta.config.cookieConsentDismiss || '[[global:cookies.accept]]').replace(/\\/g, '\\\\'), + link: translator.escape(meta.config.cookieConsentLink || '[[global:cookies.learn_more]]').replace(/\\/g, '\\\\') }; async.waterfall([ diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 260a0e7281..4612226d59 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -15,6 +15,8 @@ var plugins = require('../plugins'); var utils = require('../../public/src/utils'); var Password = require('../password'); +var sockets = require('../socket.io'); + var authenticationController = {}; authenticationController.register = function (req, res, next) { @@ -326,6 +328,10 @@ authenticationController.onSuccessfulLogin = function (req, uid, callback) { if (err) { return callback(err); } + + // Force session check for all connected socket.io clients with the same session id + sockets.in('sess_' + req.sessionID).emit('checkSession', uid); + plugins.fireHook('action:user.loggedIn', uid); callback(); }); @@ -381,7 +387,11 @@ authenticationController.localLogin = function (req, username, password, next) { // Retrieve ban reason and show error return user.getLatestBanInfo(uid, function (err, banInfo) { if (err) { - next(err); + if (err.message === 'no-ban-info') { + next(new Error('[[error:user-banned]]')); + } else { + next(err); + } } else if (banInfo.reason) { next(new Error('[[error:user-banned-reason, ' + banInfo.reason + ']]')); } else { @@ -405,7 +415,9 @@ authenticationController.localLogin = function (req, username, password, next) { authenticationController.logout = function (req, res, next) { if (req.user && parseInt(req.user.uid, 10) > 0 && req.sessionID) { var uid = parseInt(req.user.uid, 10); - user.auth.revokeSession(req.sessionID, uid, function (err) { + var sessionID = req.sessionID; + + user.auth.revokeSession(sessionID, uid, function (err) { if (err) { return next(err); } @@ -416,6 +428,9 @@ authenticationController.logout = function (req, res, next) { plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function () { res.status(200).send(''); + + // Force session check for all connected socket.io clients with the same session id + sockets.in('sess_' + sessionID).emit('checkSession', 0); }); }); } else { diff --git a/src/controllers/index.js b/src/controllers/index.js index 8bf4a798e4..e6b0d3753c 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -104,7 +104,7 @@ Controllers.login = function (req, res, next) { var registrationType = meta.config.registrationType || 'normal'; var allowLoginWith = (meta.config.allowLoginWith || 'username-email'); - var returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('url'), ''); + var returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('base_url'), ''); var errorText; if (req.query.error === 'csrf-invalid') { @@ -132,7 +132,7 @@ Controllers.login = function (req, res, next) { external: data.authentication[0].url }); } else { - return res.redirect(data.authentication[0].url); + return res.redirect(nconf.get('relative_path') + data.authentication[0].url); } } if (req.uid) { @@ -351,7 +351,7 @@ Controllers.ping = function (req, res) { Controllers.handle404 = function (req, res) { var relativePath = nconf.get('relative_path'); - var isLanguage = new RegExp('^' + relativePath + '/language/.*/.*.json'); + var isLanguage = new RegExp('^' + relativePath + '/api/language/.*/.*'); var isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js'); if (plugins.hasListeners('action:meta.override404')) { diff --git a/src/controllers/topics.js b/src/controllers/topics.js index e6072be9f2..1224de5bcd 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -66,7 +66,7 @@ topicsController.get = function (req, res, callback) { settings = results.settings; var postCount = parseInt(results.topic.postcount, 10); - pageCount = Math.max(1, Math.ceil((postCount - 1) / settings.postsPerPage)); + pageCount = Math.max(1, Math.ceil(postCount / settings.postsPerPage)); if (utils.isNumber(req.params.post_index) && (req.params.post_index < 1 || req.params.post_index > postCount)) { return helpers.redirect(res, '/topic/' + req.params.topic_id + '/' + req.params.slug + (req.params.post_index > postCount ? '/' + postCount : '')); @@ -105,9 +105,9 @@ topicsController.get = function (req, res, callback) { } else if (!req.query.page) { var index; if (reverse) { - index = Math.max(0, postCount - (req.params.post_index || postCount)); + index = Math.max(0, postCount - (req.params.post_index || postCount) + 2); } else { - index = Math.max(0, req.params.post_index - 1) || 0; + index = Math.max(0, req.params.post_index) || 0; } currentPage = Math.max(1, Math.ceil(index / settings.postsPerPage)); diff --git a/src/database/mongo.js b/src/database/mongo.js index b5af8b57ab..1528671770 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -141,7 +141,7 @@ function createIndices() { winston.info('[database] Checking database indices.'); - async.parallel([ + async.series([ async.apply(createIndex, 'objects', {_key: 1, score: -1}, {background: true}), async.apply(createIndex, 'objects', {_key: 1, value: -1}, {background: true, unique: true, sparse: true}), async.apply(createIndex, 'objects', {expireAt: 1}, {expireAfterSeconds: 0, background: true}) @@ -155,7 +155,7 @@ } function createIndex(collection, index, options, callback) { - db.collection(collection).ensureIndex(index, options, callback); + db.collection(collection).createIndex(index, options, callback); } }); }; @@ -171,6 +171,9 @@ }; module.info = function (db, callback) { + if (!db) { + return callback(); + } async.parallel({ serverStatus: function (next) { db.command({'serverStatus': 1}, next); diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index 7baf70b401..4951b44529 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -5,7 +5,7 @@ module.exports = function (db, module) { module.setObject = function (key, data, callback) { callback = callback || helpers.noop; - if (!key) { + if (!key || !data) { return callback(); } diff --git a/src/database/redis.js b/src/database/redis.js index 46c1748fc3..e583aff4ff 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -127,6 +127,9 @@ }; module.info = function (cxn, callback) { + if (!cxn) { + return callback(); + } cxn.info(function (err, data) { if (err) { return callback(err); diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index a728852f1a..f679f7637d 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -6,6 +6,14 @@ module.exports = function (redisClient, module) { module.setObject = function (key, data, callback) { callback = callback || function () {}; + if (!key || !data) { + return callback(); + } + Object.keys(data).forEach(function (key) { + if (data[key] === undefined) { + delete data[key]; + } + }); redisClient.hmset(key, data, function (err) { callback(err); }); diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index 576b2fe363..2230f0498a 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -51,6 +51,9 @@ module.exports = function (redisClient, module) { module.sortedSetRemove = function (key, value, callback) { callback = callback || function () {}; + if (!value) { + return callback(); + } if (!Array.isArray(value)) { value = [value]; } diff --git a/src/emailer.js b/src/emailer.js index 4196fbb333..1e66ce2c1a 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -73,7 +73,7 @@ var fallbackTransport; Emailer.sendToEmail = function (template, email, language, params, callback) { callback = callback || function () {}; - var lang = language || meta.config.defaultLang || 'en_GB'; + var lang = language || meta.config.defaultLang || 'en-GB'; async.waterfall([ function (next) { diff --git a/src/groups/create.js b/src/groups/create.js index 0b38b3ebcc..1d16ea33cf 100644 --- a/src/groups/create.js +++ b/src/groups/create.js @@ -92,7 +92,7 @@ module.exports = function (Groups) { return callback(new Error('[[error:group-name-too-long]]')); } - if (name.indexOf('/') !== -1) { + if (name.indexOf('/') !== -1 || !utils.slugify(name)) { return callback(new Error('[[error:invalid-group-name]]')); } diff --git a/src/groups/search.js b/src/groups/search.js index 55bf5e5789..3b6bfab9cd 100644 --- a/src/groups/search.js +++ b/src/groups/search.js @@ -1,10 +1,10 @@ 'use strict'; -var async = require('async'), +var async = require('async'); + +var user = require('../user'); +var db = require('./../database'); - user = require('../user'), - db = require('./../database'), - groups = module.parent.exports; module.exports = function (Groups) { @@ -17,7 +17,7 @@ module.exports = function (Groups) { async.apply(db.getObjectValues, 'groupslug:groupname'), function (groupNames, next) { // Ephemeral groups and the registered-users groups are searchable - groupNames = groups.getEphemeralGroups().concat(groupNames).concat('registered-users'); + groupNames = Groups.getEphemeralGroups().concat(groupNames).concat('registered-users'); groupNames = groupNames.filter(function (name) { return name.toLowerCase().indexOf(query) !== -1 && name !== 'administrators' && !Groups.isPrivilegeGroup(name); }); @@ -66,10 +66,6 @@ module.exports = function (Groups) { Groups.searchMembers = function (data, callback) { function findUids(query, searchBy, callback) { - if (!query) { - return Groups.getMembers(data.groupName, 0, -1, callback); - } - query = query.toLowerCase(); async.waterfall([ diff --git a/src/groups/update.js b/src/groups/update.js index fa998bafe6..4dfe760a96 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -12,48 +12,57 @@ module.exports = function (Groups) { Groups.update = function (groupName, values, callback) { callback = callback || function () {}; - db.exists('group:' + groupName, function (err, exists) { - if (err || !exists) { - return callback(err || new Error('[[error:no-group]]')); - } - plugins.fireHook('filter:group.update', { - groupName: groupName, - values: values - }, function (err) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.exists('group:' + groupName, next); + }, + function (exists, next) { + if (!exists) { + return next(new Error('[[error:no-group]]')); } + plugins.fireHook('filter:group.update', { + groupName: groupName, + values: values + }, next); + }, + function (result, next) { + values = result.values; var payload = { description: values.description || '', icon: values.icon || '', labelColor: values.labelColor || '#000000' }; - + if (values.hasOwnProperty('userTitle')) { payload.userTitle = values.userTitle || ''; } - + if (values.hasOwnProperty('userTitleEnabled')) { payload.userTitleEnabled = values.userTitleEnabled ? '1' : '0'; } - + if (values.hasOwnProperty('hidden')) { payload.hidden = values.hidden ? '1' : '0'; } - + if (values.hasOwnProperty('private')) { payload.private = values.private ? '1' : '0'; } - + if (values.hasOwnProperty('disableJoinRequests')) { payload.disableJoinRequests = values.disableJoinRequests ? '1' : '0'; } - async.series([ async.apply(checkNameChange, groupName, values.name), - async.apply(updatePrivacy, groupName, values.private), + function (next) { + if (values.hasOwnProperty('private')) { + updatePrivacy(groupName, values.private, next); + } else { + next(); + } + }, function (next) { if (values.hasOwnProperty('hidden')) { updateVisibility(groupName, values.hidden, next); @@ -63,19 +72,16 @@ module.exports = function (Groups) { }, async.apply(db.setObject, 'group:' + groupName, payload), async.apply(renameGroup, groupName, values.name) - ], function (err) { - if (err) { - return callback(err); - } - - plugins.fireHook('action:group.update', { - name: groupName, - values: values - }); - callback(); + ], next); + }, + function (result, next) { + plugins.fireHook('action:group.update', { + name: groupName, + values: values }); - }); - }); + next(); + } + ], callback); }; function updateVisibility(groupName, hidden, callback) { @@ -118,35 +124,33 @@ module.exports = function (Groups) { }); } - function updatePrivacy(groupName, newValue, callback) { - if (!newValue) { - return callback(); - } + function updatePrivacy(groupName, isPrivate, callback) { + async.waterfall([ + function (next) { + Groups.getGroupFields(groupName, ['private'], next); + }, + function (currentValue, next) { + var currentlyPrivate = parseInt(currentValue.private, 10) === 1; + if (!currentlyPrivate || currentlyPrivate === isPrivate) { + return callback(); + } + db.getSetMembers('group:' + groupName + ':pending', next); + }, + function (uids, next) { + if (!uids.length) { + return callback(); + } + var now = Date.now(); + var scores = uids.map(function () { return now; }); - Groups.getGroupFields(groupName, ['private'], function (err, currentValue) { - if (err) { - return callback(err); - } - currentValue = currentValue.private === '1'; - - if (currentValue !== newValue && currentValue === true) { - // Group is now public, so all pending users are automatically considered members - db.getSetMembers('group:' + groupName + ':pending', function (err, uids) { - if (err) { return callback(err); } - else if (!uids) { return callback(); } // No pending users, we're good to go - - var now = Date.now(), - scores = uids.map(function () { return now; }); // There's probably a better way to initialise an Array of size x with the same value... - - winston.verbose('[groups.update] Group is now public, automatically adding ' + uids.length + ' new members, who were pending prior.'); - async.series([ - async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', scores, uids), - async.apply(db.delete, 'group:' + groupName + ':pending') - ], callback); - }); - } else { - callback(); + winston.verbose('[groups.update] Group is now public, automatically adding ' + uids.length + ' new members, who were pending prior.'); + async.series([ + async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', scores, uids), + async.apply(db.delete, 'group:' + groupName + ':pending') + ], next); } + ], function (err) { + callback(err); }); } diff --git a/src/languages.js b/src/languages.js index 51def2f922..86563628b2 100644 --- a/src/languages.js +++ b/src/languages.js @@ -1,14 +1,14 @@ 'use strict'; -var fs = require('fs'), - path = require('path'), - async = require('async'), - LRU = require('lru-cache'), - _ = require('underscore'); +var fs = require('fs'); +var path = require('path'); +var async = require('async'); +var LRU = require('lru-cache'); var plugins = require('./plugins'); var Languages = {}; +var languagesPath = path.join(__dirname, '../public/language'); Languages.init = function (next) { if (Languages.hasOwnProperty('_cache')) { @@ -20,16 +20,16 @@ Languages.init = function (next) { next(); }; -Languages.get = function (code, key, callback) { - var combined = [code, key].join('/'); +Languages.get = function (language, namespace, callback) { + var langNamespace = language + '/' + namespace; - if (Languages._cache && Languages._cache.has(combined)) { - return callback(null, Languages._cache.get(combined)); + if (Languages._cache && Languages._cache.has(langNamespace)) { + return callback(null, Languages._cache.get(langNamespace)); } var languageData; - fs.readFile(path.join(__dirname, '../public/language/', code, key), { encoding: 'utf-8' }, function (err, data) { + fs.readFile(path.join(languagesPath, language, namespace + '.json'), { encoding: 'utf-8' }, function (err, data) { if (err && err.code !== 'ENOENT') { return callback(err); } @@ -41,12 +41,12 @@ Languages.get = function (code, key, callback) { languageData = {}; } - if (plugins.customLanguages.hasOwnProperty(combined)) { - _.extendOwn(languageData, plugins.customLanguages[combined]); + if (plugins.customLanguages.hasOwnProperty(langNamespace)) { + Object.assign(languageData, plugins.customLanguages[langNamespace]); } if (Languages._cache) { - Languages._cache.set(combined, languageData); + Languages._cache.set(langNamespace, languageData); } callback(null, languageData); @@ -54,8 +54,7 @@ Languages.get = function (code, key, callback) { }; Languages.list = function (callback) { - var languagesPath = path.join(__dirname, '../public/language'), - languages = []; + var languages = []; fs.readdir(languagesPath, function (err, files) { if (err) { diff --git a/src/messaging.js b/src/messaging.js index 494a354da0..d5d43e1041 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -46,7 +46,6 @@ var userNotifications = require('./user/notifications'); var isNew = params.isNew || false; var start = params.hasOwnProperty('start') ? params.start : 0; var stop = parseInt(start, 10) + ((params.count || 50) - 1); - var markRead = params.markRead || true; var indices = {}; async.waterfall([ @@ -79,16 +78,6 @@ var userNotifications = require('./user/notifications'); next(null, messageData); } ], callback); - - if (markRead) { - notifications.markRead('chat_' + roomId + '_' + uid, uid, function (err) { - if (err) { - winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message); - } - - userNotifications.pushCount(uid); - }); - } }; function canGetMessages(callerUid, uid, callback) { diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 2687448c56..bea909946a 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -209,4 +209,18 @@ module.exports = function (Messaging) { ], callback); }; + Messaging.canReply = function (roomId, uid, callback) { + async.waterfall([ + function (next) { + db.isSortedSetMember('chat:room:' + roomId + ':uids', uid, next); + }, + function (inRoom, next) { + plugins.fireHook('filter:messaging.canReply', {uid: uid, roomId: roomId, inRoom: inRoom, canReply: inRoom}, next); + }, + function (data, next) { + next(null, data.canReply); + } + ], callback); + }; + }; \ No newline at end of file diff --git a/src/meta/css.js b/src/meta/css.js index d33245f5bb..583927e04a 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -11,7 +11,6 @@ var postcss = require('postcss'); var clean = require('postcss-clean'); var plugins = require('../plugins'); -var emitter = require('../emitter'); var db = require('../database'); var file = require('../file'); var utils = require('../../public/src/utils'); @@ -22,12 +21,8 @@ module.exports = function (Meta) { Meta.css.cache = undefined; Meta.css.acpCache = undefined; - Meta.css.minify = function (callback) { + Meta.css.minify = function (target, callback) { callback = callback || function () {}; - if (nconf.get('isPrimary') !== 'true') { - winston.verbose('[meta/css] Cluster worker ' + process.pid + ' skipping LESS/CSS compilation'); - return callback(); - } winston.verbose('[meta/css] Minifying LESS/CSS'); db.getObjectFields('config', ['theme:type', 'theme:id'], function (err, themeData) { @@ -66,62 +61,39 @@ module.exports = function (Meta) { var acpSource = source; - source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; - source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";'; - source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";'; - source = '@import "./theme";\n' + source; + if (target !== 'admin.css') { + source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; + source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";'; + source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";'; + source = '@import "./theme";\n' + source; - acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n'; - acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n'; - acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n'; - acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; + minify(source, paths, 'cache', callback); + } else { + acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n'; + acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n'; + acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n'; + acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; - var fromFile = nconf.get('from-file') || ''; - - async.series([ - function (next) { - if (fromFile.match('clientLess')) { - winston.info('[minifier] Compiling front-end LESS files skipped'); - return Meta.css.getFromFile(path.join(__dirname, '../../public/stylesheet.css'), 'cache', next); - } - - minify(source, paths, 'cache', next); - }, - function (next) { - if (fromFile.match('acpLess')) { - winston.info('[minifier] Compiling ACP LESS files skipped'); - return Meta.css.getFromFile(path.join(__dirname, '../../public/admin.css'), 'acpCache', next); - } - - minify(acpSource, paths, 'acpCache', next); - } - ], function (err, minified) { - if (err) { - return callback(err); - } - - // Propagate to other workers - if (process.send) { - process.send({ - action: 'css-propagate', - cache: fromFile.match('clientLess') ? Meta.css.cache : minified[0], - acpCache: fromFile.match('acpLess') ? Meta.css.acpCache : minified[1] - }); - } - - emitter.emit('meta:css.compiled'); - - callback(); - }); + minify(acpSource, paths, 'acpCache', callback); + } }); }); }; + Meta.css.getFromFile = function (callback) { + async.series([ + async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/stylesheet.css'), 'cache'), + async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/admin.css'), 'acpCache') + ], function (err) { + callback(err); + }); + }; + function getStyleSource(files, prefix, extension, callback) { var pluginDirectories = [], source = ''; @@ -166,7 +138,7 @@ module.exports = function (Meta) { }); }; - Meta.css.getFromFile = function (filePath, filename, callback) { + Meta.css.loadFile = function (filePath, filename, callback) { winston.verbose('[meta/css] Reading stylesheet ' + filePath.split('/').pop() + ' from file'); fs.readFile(filePath, function (err, file) { diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js index f115ff6bfc..6c5be29e99 100644 --- a/src/meta/dependencies.js +++ b/src/meta/dependencies.js @@ -5,6 +5,7 @@ var fs = require('fs'); var async = require('async'); var semver = require('semver'); var winston = require('winston'); +var colors = require('colors'); var pkg = require('../../package.json'); @@ -15,7 +16,7 @@ module.exports = function (Meta) { var modules = Object.keys(pkg.dependencies); var depsOutdated = false; var depsMissing = false; - + winston.verbose('Checking dependencies for outdated modules'); async.every(modules, function (module, next) { diff --git a/src/meta/js.js b/src/meta/js.js index 11ec5c2ca5..626fa0ecd8 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -6,9 +6,7 @@ var path = require('path'); var async = require('async'); var nconf = require('nconf'); var fs = require('fs'); -var file = require('../file'); var plugins = require('../plugins'); -var emitter = require('../emitter'); var utils = require('../../public/src/utils'); module.exports = function (Meta) { @@ -89,47 +87,30 @@ module.exports = function (Meta) { Meta.js.bridgeModules = function (app, callback) { // Add routes for AMD-type modules to serve those files - var numBridged = 0, - addRoute = function (relPath) { - var relativePath = nconf.get('relative_path'); + function addRoute(relPath) { + var relativePath = nconf.get('relative_path'); - app.get(relativePath + '/src/modules/' + relPath, function (req, res) { - return res.sendFile(path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]), { - maxAge: app.enabled('cache') ? 5184000000 : 0 - }); + app.get(relativePath + '/src/modules/' + relPath, function (req, res) { + return res.sendFile(path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]), { + maxAge: app.enabled('cache') ? 5184000000 : 0 }); - }; + }); + } - async.series([ - function (next) { - for(var relPath in Meta.js.scripts.modules) { - if (Meta.js.scripts.modules.hasOwnProperty(relPath)) { - addRoute(relPath); - ++numBridged; - } - } + var numBridged = 0; - next(); - } - ], function (err) { - if (err) { - winston.error('[meta/js] Encountered error while bridging modules:' + err.message); + for(var relPath in Meta.js.scripts.modules) { + if (Meta.js.scripts.modules.hasOwnProperty(relPath)) { + addRoute(relPath); + ++numBridged; } + } - winston.verbose('[meta/js] ' + numBridged + ' of ' + Object.keys(Meta.js.scripts.modules).length + ' modules bridged'); - callback(err); - }); + winston.verbose('[meta/js] ' + numBridged + ' of ' + Object.keys(Meta.js.scripts.modules).length + ' modules bridged'); + callback(); }; Meta.js.minify = function (target, callback) { - if (nconf.get('isPrimary') !== 'true') { - if (typeof callback === 'function') { - callback(); - } - - return; - } - winston.verbose('[meta/js] Minifying ' + target); var forkProcessParams = setupDebugging(); @@ -137,7 +118,10 @@ module.exports = function (Meta) { Meta.js.target[target] = {}; - Meta.js.prepare(target, function () { + Meta.js.prepare(target, function (err) { + if (err) { + return callback(err); + } minifier.send({ action: 'js', minify: global.env !== 'development', @@ -153,24 +137,10 @@ module.exports = function (Meta) { winston.verbose('[meta/js] ' + target + ' minification complete'); minifier.kill(); - if (process.send && Meta.js.target['nodebb.min.js'] && Meta.js.target['acp.min.js']) { - process.send({ - action: 'js-propagate', - data: Meta.js.target - }); - } - if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) { - return Meta.js.commitToFile(target, function () { - if (typeof callback === 'function') { - callback(); - } - }); + return Meta.js.commitToFile(target, callback); } else { - emitter.emit('meta:js.compiled'); - if (typeof callback === 'function') { - return callback(); - } + return callback(); } break; @@ -178,11 +148,7 @@ module.exports = function (Meta) { winston.error('[meta/js] Could not compile ' + target + ': ' + message.message); minifier.kill(); - if (typeof callback === 'function') { - callback(new Error(message.message)); - } else { - process.exit(0); - } + callback(new Error(message.message)); break; } }); @@ -236,51 +202,44 @@ module.exports = function (Meta) { Meta.js.commitToFile = function (target, callback) { fs.writeFile(path.join(__dirname, '../../public/' + target), Meta.js.target[target].cache, function (err) { - if (err) { - winston.error('[meta/js] ' + err.message); - process.exit(0); - } - - emitter.emit('meta:js.compiled'); - callback(); + callback(err); }); }; Meta.js.getFromFile = function (target, callback) { - var scriptPath = path.join(__dirname, '../../public/' + target), - mapPath = path.join(__dirname, '../../public/' + target + '.map'), - paths = [scriptPath]; - - file.exists(scriptPath, function (exists) { - if (!exists) { - winston.warn('[meta/js] ' + target + ' not found on disk, re-minifying'); - Meta.js.minify(target, callback); - return; - } - - if (nconf.get('isPrimary') !== 'true') { - return callback(); - } - - file.exists(mapPath, function (exists) { - if (exists) { - paths.push(mapPath); - } - - async.map(paths, fs.readFile, function (err, files) { - if (err) { - return callback(err); + function readFile(filePath, next) { + fs.readFile(filePath, function (err, contents) { + if (err) { + if (err.code === 'ENOENT') { + if (!filePath.endsWith('.map')) { + winston.warn('[meta/js] ' + filePath + ' not found on disk, did you run ./nodebb build?'); + } + return next(null, ''); } - - Meta.js.target[target] = { - cache: files[0], - map: files[1] || '' - }; - - emitter.emit('meta:js.compiled'); - callback(); - }); + } + next(err, contents); }); + } + + var scriptPath = path.join(nconf.get('base_dir'), 'public/' + target); + var mapPath = path.join(nconf.get('base_dir'), 'public/' + target + '.map'); + + async.parallel({ + script: function (next) { + readFile(scriptPath, next); + }, + map: function (next) { + readFile(mapPath, next); + } + }, function (err, results) { + if (err) { + return callback(err); + } + Meta.js.target[target] = { + cache: results.script, + map: results.map + }; + callback(); }); }; diff --git a/src/meta/logs.js b/src/meta/logs.js index b335ff281e..32f6d7a141 100644 --- a/src/meta/logs.js +++ b/src/meta/logs.js @@ -1,17 +1,17 @@ 'use strict'; var path = require('path'); +var nconf = require('nconf'); var fs = require('fs'); var winston = require('winston'); module.exports = function (Meta) { - Meta.logs = { - path: path.join('logs', path.sep, 'output.log') + path: path.join(nconf.get('base_dir'), 'logs', 'output.log') }; Meta.logs.get = function (callback) { - fs.readFile(this.path, { + fs.readFile(Meta.logs.path, { encoding: 'utf-8' }, function (err, logs) { if (err) { @@ -23,6 +23,7 @@ module.exports = function (Meta) { }; Meta.logs.clear = function (callback) { - fs.truncate(this.path, 0, callback); + fs.truncate(Meta.logs.path, 0, callback); }; + }; \ No newline at end of file diff --git a/src/meta/templates.js b/src/meta/templates.js index d335709461..4e7f934624 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -8,7 +8,6 @@ var path = require('path'); var fs = require('fs'); var nconf = require('nconf'); -var emitter = require('../emitter'); var plugins = require('../plugins'); var utils = require('../../public/src/utils'); @@ -17,16 +16,6 @@ var searchIndex = {}; Templates.compile = function (callback) { callback = callback || function () {}; - var fromFile = nconf.get('from-file') || ''; - - if (nconf.get('isPrimary') === 'false' || fromFile.match('tpl')) { - if (fromFile.match('tpl')) { - emitter.emit('templates:compiled'); - winston.info('[minifier] Compiling templates skipped'); - } - - return callback(); - } compile(callback); }; @@ -154,15 +143,12 @@ function compile(callback) { return callback(err); } - compileIndex(viewsPath, function () { + compileIndex(viewsPath, function (err) { + if (err) { + return callback(err); + } winston.verbose('[meta/templates] Successfully compiled templates.'); - emitter.emit('templates:compiled'); - if (process.send) { - process.send({ - action: 'templates:compiled' - }); - } callback(); }); }); diff --git a/src/middleware/header.js b/src/middleware/header.js index 53a20e33eb..c1b4175b5b 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -136,7 +136,7 @@ module.exports = function (middleware) { templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1; templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : ''; templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin; - templateValues.defaultLang = meta.config.defaultLang || 'en_GB'; + templateValues.defaultLang = meta.config.defaultLang || 'en-GB'; templateValues.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1; templateValues.privateTagListing = parseInt(meta.config.privateTagListing, 10) === 1; diff --git a/src/middleware/headers.js b/src/middleware/headers.js index 64d6684824..66f0603b0d 100644 --- a/src/middleware/headers.js +++ b/src/middleware/headers.js @@ -1,32 +1,21 @@ 'use strict'; - -var _ = require('underscore'); - var meta = require('../meta'); module.exports = function (middleware) { middleware.addHeaders = function (req, res, next) { - var defaults = { - 'X-Powered-By': 'NodeBB', - 'X-Frame-Options': 'SAMEORIGIN', - 'Access-Control-Allow-Origin': 'null' // yes, string null. - }; var headers = { - 'X-Powered-By': meta.config['powered-by'], - 'X-Frame-Options': meta.config['allow-from-uri'] ? 'ALLOW-FROM ' + meta.config['allow-from-uri'] : undefined, - 'Access-Control-Allow-Origin': meta.config['access-control-allow-origin'], - 'Access-Control-Allow-Methods': meta.config['access-control-allow-methods'], - 'Access-Control-Allow-Headers': meta.config['access-control-allow-headers'] + 'X-Powered-By': encodeURI(meta.config['powered-by'] || 'NodeBB'), + 'X-Frame-Options': meta.config['allow-from-uri'] ? 'ALLOW-FROM ' + encodeURI(meta.config['allow-from-uri']) : 'SAMEORIGIN', + 'Access-Control-Allow-Origin': encodeURI(meta.config['access-control-allow-origin'] || 'null'), + 'Access-Control-Allow-Methods': encodeURI(meta.config['access-control-allow-methods'] || ''), + 'Access-Control-Allow-Headers': encodeURI(meta.config['access-control-allow-headers'] || '') }; - _.defaults(headers, defaults); - headers = _.pick(headers, Boolean); // Remove falsy headers - for (var key in headers) { - if (headers.hasOwnProperty(key)) { - res.setHeader(key, encodeURIComponent(headers[key])); + if (headers.hasOwnProperty(key) && headers[key]) { + res.setHeader(key, headers[key]); } } diff --git a/src/middleware/index.js b/src/middleware/index.js index fdef19db32..7f55388804 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -159,17 +159,17 @@ middleware.applyBlacklist = function (req, res, next) { }); }; -middleware.processLanguages = function (req, res, next) { - var code = req.params.code; - var key = req.path.match(/[\w]+\.json/); +middleware.getTranslation = function (req, res, next) { + var language = req.params.language; + var namespace = req.params.namespace; - if (code && key) { - languages.get(code, key[0], function (err, language) { + if (language && namespace) { + languages.get(language, namespace, function (err, translations) { if (err) { return next(err); } - res.status(200).json(language); + res.status(200).json(translations); }); } else { res.status(404).json('{}'); diff --git a/src/middleware/maintenance.js b/src/middleware/maintenance.js index 5ef04ef8fa..5199fb9332 100644 --- a/src/middleware/maintenance.js +++ b/src/middleware/maintenance.js @@ -24,23 +24,9 @@ module.exports = function (middleware) { '^/templates/[\\w/]+.tpl', '^/api/login', '^/api/widgets/render', - '^/language/.+', + '^/api/language/.+', '^/uploads/system/site-logo.png' ]; - var render = function () { - res.status(503); - var data = { - site_title: meta.config.title || 'NodeBB', - message: meta.config.maintenanceModeMessage - }; - if (!isApiRoute.test(url)) { - middleware.buildHeader(req, res, function () { - res.render('503', data); - }); - } else { - res.json(data); - } - }; var isAllowed = function (url) { for(var x = 0,numAllowed = allowedRoutes.length,route; x < numAllowed; x++) { @@ -52,25 +38,28 @@ module.exports = function (middleware) { return false; }; - var isApiRoute = /^\/api/; - if (isAllowed(url)) { return next(); } - if (!req.user) { - return render(); - } - - user.isAdministrator(req.user.uid, function (err, isAdmin) { - if (err) { + user.isAdministrator(req.uid, function (err, isAdmin) { + if (err || isAdmin) { return next(err); } - if (!isAdmin) { - render(); - } else { - next(); + + res.status(503); + var data = { + site_title: meta.config.title || 'NodeBB', + message: meta.config.maintenanceModeMessage + }; + + if (res.locals.isAPI) { + return res.json(data); } + + middleware.buildHeader(req, res, function () { + res.render('503', data); + }); }); }; diff --git a/src/middleware/render.js b/src/middleware/render.js index f9124971c0..399f81abbe 100644 --- a/src/middleware/render.js +++ b/src/middleware/render.js @@ -100,7 +100,7 @@ module.exports = function (middleware) { } function translate(str, req, res, next) { - var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB'; + var language = res.locals.config && res.locals.config.userLang || 'en-GB'; language = req.query.lang ? validator.escape(String(req.query.lang)) : language; translator.translate(str, language, function (translated) { next(null, translator.unescape(translated)); diff --git a/src/middleware/user.js b/src/middleware/user.js index c4c39a1e81..4b4af39c5f 100644 --- a/src/middleware/user.js +++ b/src/middleware/user.js @@ -2,8 +2,10 @@ var async = require('async'); var nconf = require('nconf'); + var meta = require('../meta'); var user = require('../user'); +var privileges = require('../privileges'); var controllers = { helpers: require('../controllers/helpers') @@ -29,11 +31,7 @@ module.exports = function (middleware) { user.getUidByUserslug(req.params.userslug, next); }, function (uid, next) { - if (parseInt(uid, 10) === req.uid) { - return next(null, true); - } - - user.isAdminOrGlobalMod(req.uid, next); + privileges.users.canEdit(req.uid, uid, next); }, function (allowed, next) { if (allowed) { diff --git a/src/notifications.js b/src/notifications.js index c70c0cbb0a..b99700be01 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -236,6 +236,19 @@ var utils = require('../public/src/utils'); }); }; + Notifications.pushGroups = function (notification, groupNames, callback) { + callback = callback || function () {}; + groups.getMembersOfGroups(groupNames, function (err, groupMembers) { + if (err) { + return callback(err); + } + + var members = _.unique(_.flatten(groupMembers)); + + Notifications.push(notification, members, callback); + }); + }; + Notifications.rescind = function (nid, callback) { callback = callback || function () {}; @@ -339,7 +352,9 @@ var utils = require('../public/src/utils'); function (next) { db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next); } - ], callback); + ], function (err) { + callback(err); + }); }); }; diff --git a/src/pagination.js b/src/pagination.js index 347c71ae5c..ba7dbc8436 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -22,7 +22,10 @@ pagination.create = function (currentPage, pageCount, queryObj) { var previous = Math.max(1, currentPage - 1); var next = Math.min(pageCount, currentPage + 1); - var startPage = currentPage - 2; + var startPage = Math.max(1, currentPage - 2); + if (startPage > pageCount - 5) { + startPage -= 2 - (pageCount - currentPage); + } for(var i = 0; i < 5; ++i) { pagesToShow.push(startPage + i); } @@ -43,7 +46,9 @@ pagination.create = function (currentPage, pageCount, queryObj) { }); for (i = pages.length - 1; i > 0; --i) { - if (pages[i - 1].page !== pages[i].page - 1) { + if (pages[i].page - 2 === pages[i - 1].page) { + pages.splice(i, 0, {page: pages[i].page - 1, active: false, qs: qs.stringify(queryObj)}); + } else if (pages[i].page - 1 !== pages[i - 1].page) { pages.splice(i, 0, {separator: true}); } } diff --git a/src/plugins.js b/src/plugins.js index 042561759a..8f9d0a3a45 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -9,10 +9,10 @@ var express = require('express'); var nconf = require('nconf'); var db = require('./database'); -var emitter = require('./emitter'); var utils = require('../public/src/utils'); var hotswap = require('./hotswap'); var file = require('./file'); +var languages = require('./languages'); var app; var middleware; @@ -48,9 +48,11 @@ var middleware; return callback(); } - app = nbbApp; - middleware = nbbMiddleware; - hotswap.prepare(nbbApp); + if (nbbApp) { + app = nbbApp; + middleware = nbbMiddleware; + hotswap.prepare(nbbApp); + } if (global.env === 'development') { winston.verbose('[plugins] Initializing plugins system'); @@ -67,7 +69,6 @@ var middleware; } Plugins.initialized = true; - emitter.emit('plugins:loaded'); callback(); }); }; @@ -87,35 +88,21 @@ var middleware; async.waterfall([ function (next) { // Build language code list - fs.readdir(path.join(__dirname, '../public/language'), function (err, directories) { + languages.list(function (err, languages) { if (err) { return next(err); } - Plugins.languageCodes = directories.filter(function (code) { - return code !== 'TODO'; + Plugins.languageCodes = languages.map(function (data) { + return data.code; }); next(); }); }, - function (next) { - db.getSortedSetRange('plugins:active', 0, -1, next); - }, - function (plugins, next) { - if (!Array.isArray(plugins)) { - return next(); - } - - plugins = plugins.filter(function (plugin) { - return plugin && typeof plugin === 'string'; - }).map(function (plugin) { - return path.join(__dirname, '../node_modules/', plugin); - }); - - async.filter(plugins, file.exists, function (plugins) { - async.eachSeries(plugins, Plugins.loadPlugin, next); - }); + async.apply(Plugins.getPluginPaths), + function (paths, next) { + async.eachSeries(paths, Plugins.loadPlugin, next); }, function (next) { // If some plugins are incompatible, throw the warning here diff --git a/src/plugins/install.js b/src/plugins/install.js index acde7a22e7..c9b57909c9 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -1,15 +1,15 @@ 'use strict'; -var winston = require('winston'), - async = require('async'), - path = require('path'), - fs = require('fs'), - nconf = require('nconf'), - os = require('os'), +var winston = require('winston'); +var async = require('async'); +var path = require('path'); +var fs = require('fs'); +var nconf = require('nconf'); +var os = require('os'); - db = require('../database'), - meta = require('../meta'), - pubsub = require('../pubsub'); +var db = require('../database'); +var meta = require('../meta'); +var pubsub = require('../pubsub'); module.exports = function (Plugins) { @@ -68,43 +68,38 @@ module.exports = function (Plugins) { }; function toggleInstall(id, version, callback) { - Plugins.isInstalled(id, function (err, installed) { - if (err) { - return callback(err); + var type; + var installed; + async.waterfall([ + function (next) { + Plugins.isInstalled(id, next); + }, + function (_installed, next) { + installed = _installed; + type = installed ? 'uninstall' : 'install'; + Plugins.isActive(id, next); + }, + function (active, next) { + if (active) { + Plugins.toggleActive(id, function (err, status) { + next(err); + }); + return; + } + next(); + }, + function (next) { + var command = installed ? ('npm uninstall ' + id) : ('npm install ' + id + '@' + (version || 'latest')); + runNpmCommand(command, next); + }, + function (next) { + Plugins.get(id, next); + }, + function (pluginData, next) { + Plugins.fireHook('action:plugin.' + type, id); + next(null, pluginData); } - var type = installed ? 'uninstall' : 'install'; - async.waterfall([ - function (next) { - Plugins.isActive(id, next); - }, - function (active, next) { - if (active) { - Plugins.toggleActive(id, function (err, status) { - next(err); - }); - return; - } - next(); - }, - function (next) { - var command = installed ? ('npm uninstall ' + id) : ('npm install ' + id + '@' + (version || 'latest')); - runNpmCommand(command, next); - } - ], function (err) { - if (err) { - return callback(err); - } - - Plugins.get(id, function (err, pluginData) { - if (err) { - return callback(err); - } - - Plugins.fireHook('action:plugin.' + type, id); - callback(null, pluginData); - }); - }); - }); + ], callback); } function runNpmCommand(command, callback) { @@ -112,8 +107,8 @@ module.exports = function (Plugins) { if (err) { return callback(err); } - winston.info('[plugins] ' + stdout); - callback(err); + winston.verbose('[plugins] ' + stdout); + callback(); }); } diff --git a/src/plugins/load.js b/src/plugins/load.js index 7027c31d73..2d2a377070 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -1,5 +1,6 @@ 'use strict'; +var db = require('../database'); var fs = require('fs'); var path = require('path'); var semver = require('semver'); @@ -14,6 +15,48 @@ var meta = require('../meta'); module.exports = function (Plugins) { + Plugins.getPluginPaths = function (callback) { + async.waterfall([ + function (next) { + db.getSortedSetRange('plugins:active', 0, -1, next); + }, + function (plugins, next) { + if (!Array.isArray(plugins)) { + return next(); + } + + plugins = plugins.filter(function (plugin) { + return plugin && typeof plugin === 'string'; + }).map(function (plugin) { + return path.join(__dirname, '../../node_modules/', plugin); + }); + + async.filter(plugins, file.exists, function (plugins) { + next(null, plugins); + }); + }, + ], callback); + }; + + Plugins.prepareForBuild = function (callback) { + async.waterfall([ + async.apply(Plugins.getPluginPaths), + function (paths, next) { + async.map(paths, function (path, next) { + Plugins.loadPluginInfo(path, next); + }, next); + }, + function (plugins, next) { + async.each(plugins, function (pluginData, next) { + async.parallel([ + async.apply(mapFiles, pluginData, 'css', 'cssFiles'), + async.apply(mapFiles, pluginData, 'less', 'lessFiles'), + async.apply(mapClientSideScripts, pluginData) + ], next); + }, next); + } + ], callback); + }; Plugins.loadPlugin = function (pluginPath, callback) { Plugins.loadPluginInfo(pluginPath, function (err, pluginData) { @@ -218,6 +261,7 @@ module.exports = function (Plugins) { } var pathToFolder = path.join(__dirname, '../../node_modules/', pluginData.id, pluginData.languages); + var defaultLang = (pluginData.defaultLang || 'en_GB').replace('_', '-').replace('@', '-x-'); utils.walk(pathToFolder, function (err, languages) { if (err) { @@ -230,7 +274,9 @@ module.exports = function (Plugins) { return next(err); } var data; - var route = pathToLang.replace(pathToFolder + '/', ''); + var language = path.dirname(pathToLang).split(/[\/\\]/).pop().replace('_', '-').replace('@', '-x-'); + var namespace = path.basename(pathToLang, '.json'); + var langNamespace = language + '/' + namespace; try { data = JSON.parse(file.toString()); @@ -239,18 +285,15 @@ module.exports = function (Plugins) { return next(err); } - Plugins.customLanguages[route] = Plugins.customLanguages[route] || {}; - _.extendOwn(Plugins.customLanguages[route], data); + Plugins.customLanguages[langNamespace] = Plugins.customLanguages[langNamespace] || {}; + Object.assign(Plugins.customLanguages[langNamespace], data); - if (pluginData.defaultLang && pathToLang.endsWith(pluginData.defaultLang + '/' + path.basename(pathToLang))) { - Plugins.languageCodes.map(function (code) { - if (pluginData.defaultLang !== code) { - return code + '/' + path.basename(pathToLang); - } else { - return null; - } - }).filter(Boolean).forEach(function (key) { - Plugins.customLanguages[key] = _.defaults(Plugins.customLanguages[key] || {}, data); + if (defaultLang && defaultLang === language) { + Plugins.languageCodes.filter(function (lang) { + return defaultLang !== lang; + }).forEach(function (lang) { + var langNS = lang + '/' + namespace; + Plugins.customLanguages[langNS] = Object.assign(Plugins.customLanguages[langNS] || {}, data); }); } @@ -303,9 +346,11 @@ module.exports = function (Plugins) { if (err) { return callback(err); } + var pluginData; + var packageData; try { - var pluginData = JSON.parse(results.plugin); - var packageData = JSON.parse(results.package); + pluginData = JSON.parse(results.plugin); + packageData = JSON.parse(results.package); pluginData.id = packageData.name; pluginData.name = packageData.name; @@ -313,16 +358,15 @@ module.exports = function (Plugins) { pluginData.version = packageData.version; pluginData.repository = packageData.repository; pluginData.nbbpm = packageData.nbbpm; - - callback(null, pluginData); } catch(err) { var pluginDir = pluginPath.split(path.sep); pluginDir = pluginDir[pluginDir.length - 1]; winston.error('[plugins/' + pluginDir + '] Error in plugin.json or package.json! ' + err.message); - callback(new Error('[[error:parse-error]]')); + return callback(new Error('[[error:parse-error]]')); } + callback(null, pluginData); }); }; }; diff --git a/src/posts/parse.js b/src/posts/parse.js index 28af97c5e6..e4b4cc7d3c 100644 --- a/src/posts/parse.js +++ b/src/posts/parse.js @@ -3,7 +3,9 @@ var nconf = require('nconf'); var url = require('url'); var winston = require('winston'); +var S = require('string'); +var meta = require('../meta'); var cache = require('./cache'); var plugins = require('../plugins'); var translator = require('../../public/src/modules/translator'); @@ -41,8 +43,7 @@ module.exports = function (Posts) { }; Posts.parseSignature = function (userData, uid, callback) { - userData.signature = userData.signature || ''; - + userData.signature = sanitizeSignature(userData.signature || ''); plugins.fireHook('filter:parse.signature', {userData: userData, uid: uid}, callback); }; @@ -73,4 +74,19 @@ module.exports = function (Posts) { return content; }; + + function sanitizeSignature(signature) { + var string = S(signature), + tagsToStrip = []; + + if (parseInt(meta.config['signatures:disableLinks'], 10) === 1) { + tagsToStrip.push('a'); + } + + if (parseInt(meta.config['signatures:disableImages'], 10) === 1) { + tagsToStrip.push('img'); + } + + return tagsToStrip.length ? string.stripTags.apply(string, tagsToStrip).s : signature; + } }; diff --git a/src/posts/user.js b/src/posts/user.js index 9a7fe38893..7f387ef894 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -84,7 +84,6 @@ module.exports = function (Posts) { } userData.custom_profile_info = results.customProfileInfo.profile; - userData.signature = sanitizeSignature(userData.signature); plugins.fireHook('filter:posts.modifyUserInfo', userData, next); }); @@ -128,19 +127,4 @@ module.exports = function (Posts) { user.isModerator(uid, cids, callback); }); }; -}; - -function sanitizeSignature(signature) { - var string = require('string')(signature), - tagsToStrip = []; - - if (parseInt(meta.config['signatures:disableLinks'], 10) === 1) { - tagsToStrip.push('a'); - } - - if (parseInt(meta.config['signatures:disableImages'], 10) === 1) { - tagsToStrip.push('img'); - } - - return tagsToStrip.length ? string.stripTags.apply(string, tagsToStrip).s : signature; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/privileges/users.js b/src/privileges/users.js index 4f6341be9f..82553647a4 100644 --- a/src/privileges/users.js +++ b/src/privileges/users.js @@ -136,4 +136,29 @@ module.exports = function (privileges) { }); } + privileges.users.canEdit = function (callerUid, uid, callback) { + if (parseInt(callerUid, 10) === parseInt(uid, 10)) { + return process.nextTick(callback, null, true); + } + + async.parallel({ + isAdmin: function (next) { + privileges.users.isAdministrator(callerUid, next); + }, + isGlobalMod: function (next) { + privileges.users.isGlobalModerator(callerUid, next); + }, + isTargetAdmin: function (next) { + privileges.users.isAdministrator(uid, next); + } + }, function (err, results) { + if (err) { + return callback(err); + } + var canEdit = results.isAdmin || (results.isGlobalMod && !results.isTargetAdmin); + + callback(null, canEdit); + }); + }; + }; \ No newline at end of file diff --git a/src/routes/accounts.js b/src/routes/accounts.js index abe1929ce5..14382bd568 100644 --- a/src/routes/accounts.js +++ b/src/routes/accounts.js @@ -7,7 +7,7 @@ module.exports = function (app, middleware, controllers) { var middlewares = [middleware.checkGlobalPrivacySettings]; var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions]; - setupPageRoute(app, '/uid/:uid/:section?', middleware, [], middleware.redirectUidToUserslug); + setupPageRoute(app, '/uid/:uid/:section1?/:section2?', middleware, [], middleware.redirectUidToUserslug); setupPageRoute(app, '/user/:userslug', middleware, middlewares, controllers.accounts.profile.get); setupPageRoute(app, '/user/:userslug/following', middleware, middlewares, controllers.accounts.follow.getFollowing); diff --git a/src/routes/index.js b/src/routes/index.js index 0433377303..f36ad1468a 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -112,12 +112,12 @@ module.exports = function (app, middleware, hotswapIds) { pluginRouter.hotswapId = 'plugins'; authRouter.hotswapId = 'auth'; - app.use(middleware.maintenanceMode); - app.all(relativePath + '(/api|/api/*?)', middleware.prepareAPI); app.all(relativePath + '(/api/admin|/api/admin/*?)', middleware.isAdmin); app.all(relativePath + '(/admin|/admin/*?)', ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login?local=1'), middleware.applyCSRF, middleware.isAdmin); + app.use(middleware.maintenanceMode); + adminRoutes(router, middleware, controllers); metaRoutes(router, middleware, controllers); apiRoutes(router, middleware, controllers); @@ -144,7 +144,7 @@ module.exports = function (app, middleware, hotswapIds) { } app.use(middleware.privateUploads); - app.use(relativePath + '/language/:code', middleware.processLanguages); + app.use(relativePath + '/api/language/:language/:namespace', middleware.getTranslation); app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), { maxAge: app.enabled('cache') ? 5184000000 : 0 })); diff --git a/src/sitemap.js b/src/sitemap.js index 9e52985c02..ec068d71d3 100644 --- a/src/sitemap.js +++ b/src/sitemap.js @@ -9,13 +9,14 @@ var categories = require('./categories'); var topics = require('./topics'); var privileges = require('./privileges'); var meta = require('./meta'); +var plugins = require('./plugins'); var utils = require('../public/src/utils'); var sitemap = { - maps: { - topics: [] - } - }; + maps: { + topics: [] + } +}; sitemap.render = function (callback) { var numTopics = parseInt(meta.config.sitemapTopics, 10) || 500; @@ -71,13 +72,18 @@ sitemap.getPages = function (callback) { priority: 0.4 }]; - sitemap.maps.pages = sm.createSitemap({ - hostname: nconf.get('url'), - cacheTime: 1000 * 60 * 60 * 24, // Cached for 24 hours - urls: urls - }); + plugins.fireHook('filter:sitemap.getPages', {urls: urls}, function (err, data) { + if (err) { + return callback(err); + } + sitemap.maps.pages = sm.createSitemap({ + hostname: nconf.get('url'), + cacheTime: 1000 * 60 * 60 * 24, // Cached for 24 hours + urls: data.urls + }); - sitemap.maps.pages.toXML(callback); + sitemap.maps.pages.toXML(callback); + }); }; sitemap.getCategories = function (callback) { diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index e59e15d55c..c3220ed094 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -3,6 +3,7 @@ var async = require('async'); var winston = require('winston'); var nconf = require('nconf'); +var path = require('path'); var meta = require('../meta'); var plugins = require('../plugins'); @@ -49,7 +50,7 @@ SocketAdmin.before = function (socket, method, data, next) { }); }; -SocketAdmin.restart = function (socket, data, callback) { +SocketAdmin.reload = function (socket, data, callback) { events.log({ type: 'restart', uid: socket.uid, @@ -59,10 +60,31 @@ SocketAdmin.restart = function (socket, data, callback) { callback(); }; -/** - * Reload deprecated as of v1.1.2+, remove in v2.x - */ -SocketAdmin.reload = SocketAdmin.restart; +SocketAdmin.restart = function (socket, data, callback) { + // Rebuild assets and reload NodeBB + var child_process = require('child_process'); + var build_worker = child_process.fork('app.js', ['--build'], { + cwd: path.join(__dirname, '../../'), + stdio: 'pipe' + }); + + build_worker.on('exit', function () { + events.log({ + type: 'build', + uid: socket.uid, + ip: socket.ip + }); + + events.log({ + type: 'restart', + uid: socket.uid, + ip: socket.ip + }); + + meta.restart(); + callback(); + }); +}; SocketAdmin.fireEvent = function (socket, data, callback) { index.server.emit(data.name, data.payload || {}); @@ -120,7 +142,7 @@ SocketAdmin.plugins.upgrade = function (socket, data, callback) { }; SocketAdmin.widgets.set = function (socket, data, callback) { - if(!data) { + if (!data) { return callback(new Error('[[error:invalid-data]]')); } diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index 31fa107cfb..fb6d6e31ec 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -60,7 +60,6 @@ User.createUser = function (socket, userData, callback) { user.create(userData, callback); }; - User.resetLockouts = function (socket, uids, callback) { if (!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); @@ -185,25 +184,25 @@ function deleteUsers(socket, uids, method, callback) { } User.search = function (socket, data, callback) { - user.search({query: data.query, searchBy: data.searchBy, uid: socket.uid}, function (err, searchData) { - if (err) { - return callback(err); - } - if (!searchData.users.length) { - return callback(null, searchData); - } - - var userData = searchData.users; - var uids = userData.map(function (user) { - return user && user.uid; - }); - - user.getUsersFields(uids, ['email', 'flags', 'lastonline', 'joindate'], function (err, userInfo) { - if (err) { - return callback(err); + var searchData; + async.waterfall([ + function (next) { + user.search({query: data.query, searchBy: data.searchBy, uid: socket.uid}, next); + }, + function (_searchData, next) { + searchData = _searchData; + if (!searchData.users.length) { + return callback(null, searchData); } - userData.forEach(function (user, index) { + var uids = searchData.users.map(function (user) { + return user && user.uid; + }); + + user.getUsersFields(uids, ['email', 'flags', 'lastonline', 'joindate'], next); + }, + function (userInfo, next) { + searchData.users.forEach(function (user, index) { if (user && userInfo[index]) { user.email = validator.escape(String(userInfo[index].email || '')); user.flags = userInfo[index].flags || 0; @@ -211,10 +210,9 @@ User.search = function (socket, data, callback) { user.joindateISO = userInfo[index].joindateISO; } }); - - callback(null, searchData); - }); - }); + next(null, searchData); + } + ], callback); }; User.deleteInvitation = function (socket, data, callback) { diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index acc8652057..885af43516 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -38,7 +38,7 @@ SocketCategories.get = function (socket, data, callback) { SocketCategories.getWatchedCategories = function (socket, data, callback) { async.parallel({ - categories: async.apply(categories.getCategoriesByPrivilege, socket.uid, 'find'), + categories: async.apply(categories.getCategoriesByPrivilege, 'cid:0:children', socket.uid, 'find'), ignoredCids: async.apply(user.getIgnoredCategories, socket.uid) }, function (err, results) { if (err) { @@ -47,6 +47,7 @@ SocketCategories.getWatchedCategories = function (socket, data, callback) { var watchedCategories = results.categories.filter(function (category) { return category && results.ignoredCids.indexOf(category.cid.toString()) === -1; }); + callback(null, watchedCategories); }); }; @@ -90,7 +91,7 @@ SocketCategories.loadMore = function (socket, data, callback) { set = 'cid:' + data.cid + ':tids:posts'; } - var start = Math.max(0, parseInt(data.after, 10)) + 1; + var start = Math.max(0, parseInt(data.after, 10)); if (data.direction === -1) { start = start - (reverse ? infScrollTopicsPerPage : -infScrollTopicsPerPage); @@ -156,6 +157,9 @@ SocketCategories.getMoveCategories = function (socket, data, callback) { function (next) { db.getSortedSetRange('cid:0:children', 0, -1, next); }, + function (cids, next) { + privileges.categories.filterCids('read', cids, socket.uid, next); + }, function (cids, next) { categories.getCategories(cids, socket.uid, next); } diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 25d92003e9..858d9bdeff 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -1,14 +1,14 @@ "use strict"; -var async = require('async'), +var async = require('async'); - groups = require('../groups'), - meta = require('../meta'), - user = require('../user'), - utils = require('../../public/src/utils'), - groupsController = require('../controllers/groups'), +var groups = require('../groups'); +var meta = require('../meta'); +var user = require('../user'); +var utils = require('../../public/src/utils'); +var groupsController = require('../controllers/groups'); - SocketGroups = {}; +var SocketGroups = {}; SocketGroups.before = function (socket, method, data, next) { @@ -27,34 +27,36 @@ SocketGroups.join = function (socket, data, callback) { return callback(new Error('[[error:not-allowed]]')); } - groups.exists(data.groupName, function (err, exists) { - if (err || !exists) { - return callback(err || new Error('[[error:no-group]]')); - } - - if (parseInt(meta.config.allowPrivateGroups, 10) !== 1) { - return groups.join(data.groupName, socket.uid, callback); - } - - async.parallel({ - isAdmin: async.apply(user.isAdministrator, socket.uid), - groupData: async.apply(groups.getGroupData, data.groupName) - }, function (err, results) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + groups.exists(data.groupName, next); + }, + function (exists, next) { + if (!exists) { + return next(new Error('[[error:no-group]]')); } + if (parseInt(meta.config.allowPrivateGroups, 10) !== 1) { + return groups.join(data.groupName, socket.uid, callback); + } + + async.parallel({ + isAdmin: async.apply(user.isAdministrator, socket.uid), + groupData: async.apply(groups.getGroupData, data.groupName) + }, next); + }, + function (results, next) { if (results.groupData.private && results.groupData.disableJoinRequests) { - return callback(new Error('[[error:join-requests-disabled]]')); + return next(new Error('[[error:join-requests-disabled]]')); } if (!results.groupData.private || results.isAdmin) { - groups.join(data.groupName, socket.uid, callback); + groups.join(data.groupName, socket.uid, next); } else { - groups.requestMembership(data.groupName, socket.uid, callback); + groups.requestMembership(data.groupName, socket.uid, next); } - }); - }); + } + ], callback); }; SocketGroups.leave = function (socket, data, callback) { @@ -235,7 +237,7 @@ SocketGroups.search = function (socket, data, callback) { return; } - groups.search(data.query, data.options || {}, callback); + groups.search(data.query, data.options, callback); }; SocketGroups.loadMore = function (socket, data, callback) { diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 41f6739d36..38e73c1cc1 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -57,6 +57,7 @@ var ratelimit = require('../middleware/ratelimit'); socket.join('online_guests'); } + socket.join('sess_' + socket.request.signedCookies[nconf.get('sessionKey')]); io.sockets.sockets[socket.id].emit('checkSession', socket.uid); } diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js index 13fe018654..baa0abc0aa 100644 --- a/src/socket.io/meta.js +++ b/src/socket.io/meta.js @@ -1,30 +1,22 @@ 'use strict'; -var meta = require('../meta'), - user = require('../user'), - topics = require('../topics'), - emitter = require('../emitter'), - websockets = require('./'), +var user = require('../user'); +var topics = require('../topics'); - SocketMeta = { - rooms: {} - }; +var SocketMeta = { + rooms: {} +}; SocketMeta.reconnected = function (socket, data, callback) { + callback = callback || function () {}; if (socket.uid) { topics.pushUnreadCount(socket.uid); user.notifications.pushCount(socket.uid); } + callback(); }; -emitter.on('nodebb:ready', function () { - websockets.server.emit('event:nodebb.ready', { - 'cache-buster': meta.config['cache-buster'] - }); -}); - - /* Rooms */ SocketMeta.rooms.enter = function (socket, data, callback) { diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index f74e077c5c..a23e9c64bb 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -114,6 +114,7 @@ SocketModules.chats.loadRoom = function (socket, data, callback) { async.parallel({ roomData: async.apply(Messaging.getRoomData, data.roomId), + canReply: async.apply(Messaging.canReply, data.roomId, socket.uid), users: async.apply(Messaging.getUsersInRoom, data.roomId, 0, -1), messages: async.apply(Messaging.getMessages, { callerUid: socket.uid, @@ -125,6 +126,7 @@ SocketModules.chats.loadRoom = function (socket, data, callback) { }, function (results, next) { results.roomData.users = results.users; + results.roomData.canReply = results.canReply; results.roomData.usernames = Messaging.generateUsernames(results.users, socket.uid); results.roomData.messages = results.messages; results.roomData.groupChat = results.roomData.hasOwnProperty('groupChat') ? results.roomData.groupChat : results.users.length > 2; @@ -243,7 +245,7 @@ SocketModules.chats.markRead = function (socket, roomId, callback) { return callback(new Error('[[error:invalid-data]]')); } async.parallel({ - usersInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1), + uidsInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1), markRead: async.apply(Messaging.markRead, socket.uid, roomId) }, function (err, results) { if (err) { @@ -251,9 +253,14 @@ SocketModules.chats.markRead = function (socket, roomId, callback) { } Messaging.pushUnreadCount(socket.uid); + server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', {roomId: roomId}); + + if (results.uidsInRoom.indexOf(socket.uid.toString()) === -1) { + return callback(); + } // Mark notification read - var nids = results.usersInRoom.filter(function (uid) { + var nids = results.uidsInRoom.filter(function (uid) { return parseInt(uid, 10) !== socket.uid; }).map(function (uid) { return 'chat_' + uid + '_' + roomId; @@ -263,7 +270,6 @@ SocketModules.chats.markRead = function (socket, roomId, callback) { user.notifications.pushCount(socket.uid); }); - server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', {roomId: roomId}); callback(); }); }; @@ -328,14 +334,9 @@ SocketModules.chats.getMessages = function (socket, data, callback) { uid: data.uid, roomId: data.roomId, start: parseInt(data.start, 10) || 0, - count: 50, - markRead: false + count: 50 }; - if (data.hasOwnProperty('markRead')) { - params.markRead = data.markRead; - } - Messaging.getMessages(params, callback); }; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index 7cc43178d6..fe729a5c11 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -44,7 +44,7 @@ SocketPosts.reply = function (socket, data, callback) { callback(null, postData); - socket.emit('event:new_post', result); + websockets.in('uid_' + socket.uid).emit('event:new_post', result); user.updateOnlineUsers(socket.uid); diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js index 42c789eb54..077b88bfc9 100644 --- a/src/socket.io/posts/flag.js +++ b/src/socket.io/posts/flag.js @@ -11,6 +11,7 @@ var privileges = require('../../privileges'); var notifications = require('../../notifications'); var plugins = require('../../plugins'); var meta = require('../../meta'); +var utils = require('../../../public/src/utils'); module.exports = function (SocketPosts) { @@ -51,7 +52,8 @@ module.exports = function (SocketPosts) { }, next); }, function (user, next) { - if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < parseInt(meta.config['privileges:flag'] || 1, 10)) { + var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1; + if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) { return next(new Error('[[error:not-enough-reputation-to-flag]]')); } @@ -163,9 +165,8 @@ module.exports = function (SocketPosts) { return memo; }, payload); - next(null, socket.uid, data.pid, payload); - }, - async.apply(posts.updateFlagData) + posts.updateFlagData(socket.uid, data.pid, payload, next); + } ], callback); }; }; diff --git a/src/socket.io/posts/helpers.js b/src/socket.io/posts/helpers.js index 657ab0f64a..4c04f82afc 100644 --- a/src/socket.io/posts/helpers.js +++ b/src/socket.io/posts/helpers.js @@ -13,59 +13,64 @@ helpers.postCommand = function (socket, command, eventName, notification, data, if (!socket.uid) { return callback(new Error('[[error:not-logged-in]]')); } + if (!data || !data.pid || !data.room_id) { return callback(new Error('[[error:invalid-data]]')); } - async.parallel({ - exists: function (next) { - posts.exists(data.pid, next); + + async.waterfall([ + function (next) { + async.parallel({ + exists: function (next) { + posts.exists(data.pid, next); + }, + deleted: function (next) { + posts.getPostField(data.pid, 'deleted', next); + } + }, next); }, - deleted: function (next) { - posts.getPostField(data.pid, 'deleted', next); - } - }, function (err, results) { - if (err || !results.exists) { - return callback(err || new Error('[[error:invalid-pid]]')); - } - - if (parseInt(results.deleted, 10) === 1) { - return callback(new Error('[[error:post-deleted]]')); - } - - /* - hooks: - filter:post.upvote - filter:post.downvote - filter:post.unvote - filter:post.bookmark - filter:post.unbookmark - */ - plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function (err, filteredData) { - if (err) { - return callback(err); + function (results, next) { + if (!results.exists) { + return next(new Error('[[error:invalid-pid]]')); } - executeCommand(socket, command, eventName, notification, filteredData.data, callback); - }); - }); + if (parseInt(results.deleted, 10) === 1) { + return next(new Error('[[error:post-deleted]]')); + } + + /* + hooks: + filter:post.upvote + filter:post.downvote + filter:post.unvote + filter:post.bookmark + filter:post.unbookmark + */ + plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, next); + }, + function (filteredData, next) { + executeCommand(socket, command, eventName, notification, filteredData.data, next); + } + ], callback); }; function executeCommand(socket, command, eventName, notification, data, callback) { - posts[command](data.pid, socket.uid, function (err, result) { - if (err) { - return callback(err); - } + async.waterfall([ + function (next) { + posts[command](data.pid, socket.uid, next); + }, + function (result, next) { + if (result && eventName) { + websockets.in('uid_' + socket.uid).emit('posts.' + command, result); + websockets.in(data.room_id).emit('event:' + eventName, result); + } - if (result && eventName) { - socket.emit('posts.' + command, result); - websockets.in(data.room_id).emit('event:' + eventName, result); + if (result && notification) { + socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, command, notification); + } else if (result && command === 'unvote') { + socketHelpers.rescindUpvoteNotification(data.pid, socket.uid); + } + next(null, result); } - - if (result && notification) { - socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, command, notification); - } else if (result && command === 'unvote') { - socketHelpers.rescindUpvoteNotification(data.pid, socket.uid); - } - callback(); - }); + ], callback); } \ No newline at end of file diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js index 699c5e5d35..25f5f8a19d 100644 --- a/src/socket.io/topics/move.js +++ b/src/socket.io/topics/move.js @@ -62,7 +62,7 @@ module.exports = function (SocketTopics) { return callback(new Error('[[error:no-privileges]]')); } - categories.getTopicIds('cid:' + data.currentCid + ':tids', true, 0, -1, next); + categories.getAllTopicIds(data.currentCid, 0, -1, next); }, function (tids, next) { async.eachLimit(tids, 50, function (tid, next) { diff --git a/src/socket.io/topics/unread.js b/src/socket.io/topics/unread.js index 029a0c9e74..39c6485a26 100644 --- a/src/socket.io/topics/unread.js +++ b/src/socket.io/topics/unread.js @@ -11,61 +11,67 @@ module.exports = function (SocketTopics) { if (!Array.isArray(tids) || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } + async.waterfall([ + function (next) { + topics.markAsRead(tids, socket.uid, next); + }, + function (hasMarked, next) { + if (hasMarked) { + topics.pushUnreadCount(socket.uid); - topics.markAsRead(tids, socket.uid, function (err) { - if (err) { - return callback(err); + topics.markTopicNotificationsRead(tids, socket.uid); + } + next(); } - - topics.pushUnreadCount(socket.uid); - - topics.markTopicNotificationsRead(tids, socket.uid); - - callback(); - }); + ], callback); }; SocketTopics.markTopicNotificationsRead = function (socket, tids, callback) { if (!Array.isArray(tids) || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } - topics.markTopicNotificationsRead(tids, socket.uid); + topics.markTopicNotificationsRead(tids, socket.uid, callback); }; SocketTopics.markAllRead = function (socket, data, callback) { - topics.markAllRead(socket.uid, function (err) { - if (err) { - return callback(err); + if (!socket.uid) { + return callback(new Error('[[error:invalid-uid]]')); + } + async.waterfall([ + function (next) { + topics.markAllRead(socket.uid, next); + }, + function (next) { + topics.pushUnreadCount(socket.uid); + next(); } - - topics.pushUnreadCount(socket.uid); - - callback(); - }); + ], callback); }; SocketTopics.markCategoryTopicsRead = function (socket, cid, callback) { - topics.getUnreadTids(cid, socket.uid, '', function (err, tids) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + topics.getUnreadTids(cid, socket.uid, '', next); + }, + function (tids, next) { + SocketTopics.markAsRead(socket, tids, next); } - - SocketTopics.markAsRead(socket, tids, callback); - }); + ], callback); }; SocketTopics.markUnread = function (socket, tid, callback) { if (!tid || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } - topics.markUnread(tid, socket.uid, function (err) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + topics.markUnread(tid, socket.uid, next); + }, + function (next) { + topics.pushUnreadCount(socket.uid); + next(); } - - topics.pushUnreadCount(socket.uid); - callback(); - }); + ], callback); }; SocketTopics.markAsUnreadForAll = function (socket, tids, callback) { @@ -77,42 +83,41 @@ module.exports = function (SocketTopics) { return callback(new Error('[[error:no-privileges]]')); } - user.isAdministrator(socket.uid, function (err, isAdmin) { - if (err) { - return callback(err); - } - - async.each(tids, function (tid, next) { - async.waterfall([ - function (next) { - topics.exists(tid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:invalid-tid]]')); + async.waterfall([ + function (next) { + user.isAdministrator(socket.uid, next); + }, + function (isAdmin, next) { + async.each(tids, function (tid, next) { + async.waterfall([ + function (next) { + topics.exists(tid, next); + }, + function (exists, next) { + if (!exists) { + return next(new Error('[[error:no-topic]]')); + } + topics.getTopicField(tid, 'cid', next); + }, + function (cid, next) { + user.isModerator(socket.uid, cid, next); + }, + function (isMod, next) { + if (!isAdmin && !isMod) { + return next(new Error('[[error:no-privileges]]')); + } + topics.markAsUnreadForAll(tid, next); + }, + function (next) { + topics.updateRecent(tid, Date.now(), next); } - topics.getTopicField(tid, 'cid', next); - }, - function (cid, next) { - user.isModerator(socket.uid, cid, next); - }, - function (isMod, next) { - if (!isAdmin && !isMod) { - return next(new Error('[[error:no-privileges]]')); - } - topics.markAsUnreadForAll(tid, next); - }, - function (next) { - topics.updateRecent(tid, Date.now(), next); - } - ], next); - }, function (err) { - if (err) { - return callback(err); - } + ], next); + }, next); + }, + function (next) { topics.pushUnreadCount(socket.uid); - callback(); - }); - }); + next(); + } + ], callback); }; }; \ No newline at end of file diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 4d5f752b5d..69b229b4b1 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -1,6 +1,6 @@ 'use strict'; -var async = require('async'); +var async = require('async'); var winston = require('winston'); var user = require('../user'); @@ -13,6 +13,7 @@ var events = require('../events'); var emailer = require('../emailer'); var db = require('../database'); var apiController = require('../controllers/api'); +var privileges = require('../privileges'); var SocketUser = {}; @@ -45,7 +46,7 @@ SocketUser.deleteAccount = function (socket, data, callback) { user.deleteAccount(socket.uid, next); }, function (next) { - socket.broadcast.emit('event:user_status_change', {uid: socket.uid, status: 'offline'}); + require('./index').server.sockets.emit('event:user_status_change', {uid: socket.uid, status: 'offline'}); events.log({ type: 'user-delete', @@ -211,10 +212,7 @@ SocketUser.saveSettings = function (socket, data, callback) { async.waterfall([ function (next) { - if (socket.uid === parseInt(data.uid, 10)) { - return next(null, true); - } - user.isAdminOrGlobalMod(socket.uid, next); + privileges.users.canEdit(socket.uid, data.uid, next); }, function (allowed, next) { if (!allowed) { @@ -326,7 +324,7 @@ SocketUser.setModerationNote = function (socket, data, callback) { async.waterfall([ function (next) { - user.isAdminOrGlobalMod(socket.uid, next); + privileges.users.canEdit(socket.uid, data.uid, next); }, function (allowed, next) { if (allowed) { diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index 8e2cbda7bd..8a59fbbff2 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -5,6 +5,7 @@ var async = require('async'); var user = require('../../user'); var meta = require('../../meta'); var events = require('../../events'); +var privileges = require('../../privileges'); module.exports = function (SocketUser) { @@ -27,18 +28,14 @@ module.exports = function (SocketUser) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } - - user.isAdministrator(socket.uid, function (err, isAdmin) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + user.isAdminOrSelf(socket.uid, data.uid, next); + }, + function (next) { + user.updateCoverPicture(data, next); } - - if (!isAdmin && data.uid !== socket.uid) { - return callback(new Error('[[error:no-privileges]]')); - } - - user.updateCoverPicture(data, callback); - }); + ], callback); }; SocketUser.removeCover = function (socket, data, callback) { @@ -46,12 +43,14 @@ module.exports = function (SocketUser) { return callback(new Error('[[error:no-privileges]]')); } - user.isAdminOrSelf(socket.uid, data.uid, function (err) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + user.isAdminOrSelf(socket.uid, data.uid, next); + }, + function (next) { + user.removeCoverPicture(data, next); } - user.removeCoverPicture(data, callback); - }); + ], callback); }; function isAdminOrSelfAndPasswordMatch(uid, data, callback) { @@ -69,13 +68,13 @@ module.exports = function (SocketUser) { if (err) { return callback(err); } - var self = parseInt(uid, 10) === parseInt(data.uid, 10); + var isSelf = parseInt(uid, 10) === parseInt(data.uid, 10); - if (!results.isAdmin && !self) { + if (!results.isAdmin && !isSelf) { return callback(new Error('[[error:no-privileges]]')); } - if (self && results.hasPassword && !results.passwordMatch) { + if (isSelf && results.hasPassword && !results.passwordMatch) { return callback(new Error('[[error:invalid-password]]')); } @@ -127,18 +126,25 @@ module.exports = function (SocketUser) { return next(new Error('[[error:invalid-data]]')); } - user.isAdminOrGlobalMod(socket.uid, next); + async.parallel({ + isAdminOrGlobalMod: function (next) { + user.isAdminOrGlobalMod(socket.uid, next); + }, + canEdit: function (next) { + privileges.users.canEdit(socket.uid, data.uid, next); + } + }, next); }, - function (isAdminOrGlobalMod, next) { - if (!isAdminOrGlobalMod && socket.uid !== parseInt(data.uid, 10)) { + function (results, next) { + if (!results.canEdit) { return next(new Error('[[error:no-privileges]]')); } - if (!isAdminOrGlobalMod && parseInt(meta.config['username:disableEdit'], 10) === 1) { + if (!results.isAdminOrGlobalMod && parseInt(meta.config['username:disableEdit'], 10) === 1) { data.username = oldUserData.username; } - if (!isAdminOrGlobalMod && parseInt(meta.config['email:disableEdit'], 10) === 1) { + if (!results.isAdminOrGlobalMod && parseInt(meta.config['email:disableEdit'], 10) === 1) { data.email = oldUserData.email; } diff --git a/src/topics.js b/src/topics.js index de8b795466..ff5d40fc2d 100644 --- a/src/topics.js +++ b/src/topics.js @@ -53,23 +53,8 @@ var social = require('./social'); }; Topics.getTidPage = function (tid, uid, callback) { - if(!tid) { - return callback(new Error('[[error:invalid-tid]]')); - } - - async.parallel({ - index: function (next) { - categories.getTopicIndex(tid, next); - }, - settings: function (next) { - user.getSettings(uid, next); - } - }, function (err, results) { - if (err) { - return callback(err); - } - callback(null, Math.ceil((results.index + 1) / results.settings.topicsPerPage)); - }); + console.warn('[Topics.getTidPage] deprecated!'); + callback(null, 1); }; Topics.getTopicsFromSet = function (set, uid, start, stop, callback) { @@ -234,6 +219,13 @@ var social = require('./social'); function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback) { async.waterfall([ function (next) { + if (stop > 0) { + stop--; + if (start > 0) { + start --; + } + } + posts.getPidsFromSet(set, start, stop, reverse, next); }, function (pids, next) { diff --git a/src/topics/create.js b/src/topics/create.js index 80f6b89be5..852b987ffc 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -3,6 +3,7 @@ var async = require('async'); var validator = require('validator'); +var S = require('string'); var db = require('../database'); var utils = require('../../public/src/utils'); var plugins = require('../plugins'); @@ -89,7 +90,7 @@ module.exports = function (Topics) { Topics.post = function (data, callback) { var uid = data.uid; - var title = data.title ? data.title.trim() : data.title; + var title = String(data.title).trim(); data.tags = data.tags || []; async.waterfall([ @@ -327,6 +328,11 @@ module.exports = function (Topics) { } function check(item, min, max, minError, maxError, callback) { + // Trim and remove HTML (latter for composers that send in HTML, like redactor) + if (typeof item === 'string') { + item = S(item.trim()).stripTags().s; + } + if (!item || item.length < parseInt(min, 10)) { return callback(new Error('[[error:' + minError + ', ' + min + ']]')); } else if (item.length > parseInt(max, 10)) { diff --git a/src/topics/delete.js b/src/topics/delete.js index df63ebe3bc..91c1bf53e3 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -177,6 +177,7 @@ module.exports = function (Topics) { function (next) { db.sortedSetsRemove([ 'cid:' + topicData.cid + ':tids', + 'cid:' + topicData.cid + ':tids:pinned', 'cid:' + topicData.cid + ':tids:posts', 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', 'uid:' + topicData.uid + ':topics' diff --git a/src/topics/follow.js b/src/topics/follow.js index 6993920fc0..0667218588 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -74,7 +74,10 @@ module.exports = function (Topics) { function (next) { method2(tid, uid, next); }, - async.apply(plugins.fireHook, hook, {uid: uid, tid: tid}) + function (next) { + plugins.fireHook(hook, {uid: uid, tid: tid}); + next(); + } ], callback); } diff --git a/src/topics/recent.js b/src/topics/recent.js index 421bb0cd71..23ca9ff5ba 100644 --- a/src/topics/recent.js +++ b/src/topics/recent.js @@ -7,6 +7,7 @@ var db = require('../database'); var plugins = require('../plugins'); var privileges = require('../privileges'); var user = require('../user'); +var categories = require('../categories'); module.exports = function (Topics) { var terms = { @@ -24,7 +25,11 @@ module.exports = function (Topics) { async.waterfall([ function (next) { - db.getSortedSetRevRange(cid ? 'cid:' + cid + ':tids' : 'topics:recent', 0, 199, next); + if (cid) { + categories.getTopicIds(cid, 'cid:' + cid + ':tids', true, 0, 199, next); + } else { + db.getSortedSetRevRange('topics:recent', 0, 199, next); + } }, function (tids, next) { filterTids(tids, uid, filter, next); diff --git a/src/topics/suggested.js b/src/topics/suggested.js index a7b1190bf1..d51d51827b 100644 --- a/src/topics/suggested.js +++ b/src/topics/suggested.js @@ -1,13 +1,11 @@ 'use strict'; -var async = require('async'), - _ = require('underscore'), - - categories = require('../categories'), - search = require('../search'), - db = require('../database'); +var async = require('async'); +var _ = require('underscore'); +var categories = require('../categories'); +var search = require('../search'); module.exports = function (Topics) { @@ -29,7 +27,13 @@ module.exports = function (Topics) { var tids = results.tagTids.concat(results.searchTids).concat(results.categoryTids); tids = tids.filter(function (_tid, index, array) { return parseInt(_tid, 10) !== parseInt(tid, 10) && array.indexOf(_tid) === index; - }).slice(start, stop + 1); + }); + + if (stop === -1) { + tids = tids.slice(start); + } else { + tids = tids.slice(start, stop + 1); + } Topics.getTopics(tids, uid, callback); }); @@ -63,12 +67,14 @@ module.exports = function (Topics) { } function getCategoryTids(tid, callback) { - Topics.getTopicField(tid, 'cid', function (err, cid) { - if (err || !cid) { - return callback(err, []); + async.waterfall([ + function (next) { + Topics.getTopicField(tid, 'cid', next); + }, + function (cid, next) { + categories.getTopicIds(cid, 'cid:' + cid + ':tids', true, 0, 9, next); } - categories.getTopicIds('cid:' + cid + ':tids', true, 0, 9, callback); - }); + ], callback); } }; \ No newline at end of file diff --git a/src/topics/tools.js b/src/topics/tools.js index 6fce256f27..377b3bdfcd 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -4,7 +4,6 @@ var async = require('async'); var db = require('../database'); var categories = require('../categories'); -var meta = require('../meta'); var plugins = require('../plugins'); var privileges = require('../privileges'); @@ -167,7 +166,7 @@ module.exports = function (Topics) { if (!exists) { return callback(new Error('[[error:no-topic]]')); } - Topics.getTopicFields(tid, ['cid', 'lastposttime'], next); + Topics.getTopicFields(tid, ['cid', 'lastposttime', 'postcount'], next); }, function (_topicData, next) { topicData = _topicData; @@ -177,9 +176,24 @@ module.exports = function (Topics) { if (!isAdminOrMod) { return next(new Error('[[error:no-privileges]]')); } + async.parallel([ async.apply(Topics.setTopicField, tid, 'pinned', pin ? 1 : 0), - async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', pin ? Math.pow(2, 53) : topicData.lastposttime, tid) + function (next) { + if (pin) { + async.parallel([ + async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:pinned', Date.now(), tid), + async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids', tid), + async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:posts', tid), + ], next); + } else { + async.parallel([ + async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:pinned', tid), + async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', topicData.lastposttime, tid), + async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:posts', topicData.postcount, tid), + ], next); + } + } ], next); }, function (results, next) { @@ -213,20 +227,24 @@ module.exports = function (Topics) { topic = topicData; db.sortedSetsRemove([ 'cid:' + topicData.cid + ':tids', + 'cid:' + topicData.cid + ':tids:pinned', 'cid:' + topicData.cid + ':tids:posts' ], tid, next); }, function (next) { - var timestamp = parseInt(topic.pinned, 10) ? Math.pow(2, 53) : topic.lastposttime; - async.parallel([ - function (next) { - db.sortedSetAdd('cid:' + cid + ':tids', timestamp, tid, next); - }, - function (next) { - topic.postcount = topic.postcount || 0; - db.sortedSetAdd('cid:' + cid + ':tids:posts', topic.postcount, tid, next); - } - ], next); + if (parseInt(topic.pinned, 10)) { + db.sortedSetAdd('cid:' + cid + ':tids:pinned', Date.now(), tid, next); + } else { + async.parallel([ + function (next) { + db.sortedSetAdd('cid:' + cid + ':tids', topic.lastposttime, tid, next); + }, + function (next) { + topic.postcount = topic.postcount || 0; + db.sortedSetAdd('cid:' + cid + ':tids:posts', topic.postcount, tid, next); + } + ], next); + } } ], function (err) { if (err) { diff --git a/src/topics/unread.js b/src/topics/unread.js index c7cabb2881..e61ca46d59 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -2,7 +2,6 @@ 'use strict'; var async = require('async'); -var winston = require('winston'); var db = require('../database'); var user = require('../user'); @@ -277,9 +276,10 @@ module.exports = function (Topics) { ], callback); }; - Topics.markTopicNotificationsRead = function (tids, uid) { + Topics.markTopicNotificationsRead = function (tids, uid, callback) { + callback = callback || function () {}; if (!Array.isArray(tids) || !tids.length) { - return; + return callback(); } async.waterfall([ @@ -288,23 +288,23 @@ module.exports = function (Topics) { }, function (nids, next) { notifications.markReadMultiple(nids, uid, next); + }, + function (next) { + user.notifications.pushCount(uid); + next(); } - ], function (err) { - if (err) { - return winston.error(err); - } - user.notifications.pushCount(uid); - }); + ], callback); }; Topics.markCategoryUnreadForAll = function (tid, callback) { - Topics.getTopicField(tid, 'cid', function (err, cid) { - if(err) { - return callback(err); + async.waterfall([ + function (next) { + Topics.getTopicField(tid, 'cid', next); + }, + function (cid, next) { + categories.markAsUnreadForAll(cid, next); } - - categories.markAsUnreadForAll(cid, callback); - }); + ], callback); }; Topics.hasReadTopics = function (tids, uid, callback) { diff --git a/src/upgrade.js b/src/upgrade.js index 9aa66e8f22..79ffa6b5ee 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -1,5 +1,7 @@ "use strict"; +/* globals console, require */ + var db = require('./database'), async = require('async'), winston = require('winston'), @@ -10,7 +12,7 @@ var db = require('./database'), schemaDate, thisSchemaDate, // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema - latestSchema = Date.UTC(2016, 9, 14); + latestSchema = Date.UTC(2016, 10, 22); Upgrade.check = function (callback) { db.get('schemaDate', function (err, value) { @@ -946,7 +948,117 @@ Upgrade.upgrade = function (callback) { winston.info('[2016/10/14] Creating sorted sets for post replies - skipped!'); next(); } - } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 10, 22); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/11/22] Update global and user language keys'); + + var user = require('./user'); + var meta = require('./meta'); + var batch = require('./batch'); + var newLanguage; + var i = 0; + var j = 0; + async.parallel([ + function (next) { + meta.configs.get('defaultLang', function (err, defaultLang) { + if (err) { + return next(err); + } + + if (!defaultLang) { + return setImmediate(next); + } + + newLanguage = defaultLang.replace('_', '-').replace('@', '-x-'); + if (newLanguage !== defaultLang) { + meta.configs.set('defaultLang', newLanguage, next); + } else { + setImmediate(next); + } + }); + }, + function (next) { + batch.processSortedSet('users:joindate', function (ids, next) { + async.each(ids, function (uid, next) { + async.waterfall([ + async.apply(db.getObjectField, 'user:' + uid + ':settings', 'userLang'), + function (language, next) { + ++i; + if (!language) { + return setImmediate(next); + } + + newLanguage = language.replace('_', '-').replace('@', '-x-'); + if (newLanguage !== language) { + ++j; + user.setSetting(uid, 'userLang', newLanguage, next); + } else { + setImmediate(next); + } + } + ], next); + }, next); + }, next); + } + ], function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/11/22] Update global and user language keys - done (' + i + ' processed, ' + j + ' updated)'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/11/22] Update global and user language keys - skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 10, 25); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/11/25] Creating sorted sets for pinned topcis'); + + var topics = require('./topics'); + var batch = require('./batch'); + batch.processSortedSet('topics:tid', function (ids, next) { + topics.getTopicsFields(ids, ['tid', 'cid', 'pinned', 'lastposttime'], function (err, data) { + if (err) { + return next(err); + } + + data = data.filter(function (topicData) { + return parseInt(topicData.pinned, 10) === 1; + }); + + async.eachSeries(data, function (topicData, next) { + console.log('processing tid: ' + topicData.tid); + + async.parallel([ + async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:pinned', Date.now(), topicData.tid), + async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids', topicData.tid), + async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:posts', topicData.tid) + ], next); + }, next); + }); + }, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/11/25] Creating sorted sets for pinned topics - done'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/11/25] Creating sorted sets for pinned topics - skipped!'); + next(); + } + }, // Add new schema updates here // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!! ], function (err) { diff --git a/src/user.js b/src/user.js index 31dda0d991..7c0fa5e67e 100644 --- a/src/user.js +++ b/src/user.js @@ -222,6 +222,13 @@ var meta = require('./meta'); db.sortedSetScore('email:uid', email.toLowerCase(), callback); }; + User.getUidsByEmails = function (emails, callback) { + emails = emails.map(function (email) { + return email && email.toLowerCase(); + }); + db.sortedSetScores('email:uid', emails, callback); + }; + User.getUsernameByEmail = function (email, callback) { db.sortedSetScore('email:uid', email.toLowerCase(), function (err, uid) { if (err) { diff --git a/src/user/admin.js b/src/user/admin.js index 44aa72e059..8b5a6ebef4 100644 --- a/src/user/admin.js +++ b/src/user/admin.js @@ -28,7 +28,7 @@ module.exports = function (User) { }; User.getUsersCSV = function (callback) { - winston.info('[user/getUsersCSV] Compiling User CSV data'); + winston.verbose('[user/getUsersCSV] Compiling User CSV data'); var csvContent = ''; var uids; async.waterfall([ diff --git a/src/user/auth.js b/src/user/auth.js index a60f59fea4..a6222728e4 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -10,43 +10,37 @@ module.exports = function (User) { User.auth = {}; User.auth.logAttempt = function (uid, ip, callback) { - db.exists('lockout:' + uid, function (err, exists) { - if (err) { - return callback(err); - } - - if (exists) { - return callback(new Error('[[error:account-locked]]')); - } - - db.increment('loginAttempts:' + uid, function (err, attempts) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.exists('lockout:' + uid, next); + }, + function (exists, next) { + if (exists) { + return callback(new Error('[[error:account-locked]]')); } - - if ((meta.config.loginAttempts || 5) < attempts) { - // Lock out the account - db.set('lockout:' + uid, '', function (err) { - if (err) { - return callback(err); - } - var duration = 1000 * 60 * (meta.config.lockoutDuration || 60); - - db.delete('loginAttempts:' + uid); - db.pexpire('lockout:' + uid, duration); - events.log({ - type: 'account-locked', - uid: uid, - ip: ip - }); - callback(new Error('[[error:account-locked]]')); - }); - } else { - db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60); - callback(); + db.increment('loginAttempts:' + uid, next); + }, + function (attemps, next) { + var loginAttempts = parseInt(meta.config.loginAttempts, 10) || 5; + if (attemps <= loginAttempts) { + return db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60, callback); } - }); - }); + // Lock out the account + db.set('lockout:' + uid, '', next); + }, + function (next) { + var duration = 1000 * 60 * (meta.config.lockoutDuration || 60); + + db.delete('loginAttempts:' + uid); + db.pexpire('lockout:' + uid, duration); + events.log({ + type: 'account-locked', + uid: uid, + ip: ip + }); + next(new Error('[[error:account-locked]]')); + } + ], callback); }; User.auth.clearLoginAttempts = function (uid) { diff --git a/src/user/delete.js b/src/user/delete.js index e03333d7c1..e648c5dfb0 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -15,6 +15,7 @@ module.exports = function (User) { if (!parseInt(uid, 10)) { return callback(new Error('[[error:invalid-uid]]')); } + async.waterfall([ function (next) { deletePosts(callerUid, uid, next); @@ -48,6 +49,12 @@ module.exports = function (User) { var userData; async.waterfall([ function (next) { + User.exists(uid, next); + }, + function (exists, next) { + if (!exists) { + return callback(); + } User.getUserFields(uid, ['username', 'userslug', 'fullname', 'email'], next); }, function (_userData, next) { diff --git a/src/user/digest.js b/src/user/digest.js index 82ff02b823..84cb8af489 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -1,6 +1,6 @@ "use strict"; -var async = require('async'); +var async = require('async'); var winston = require('winston'); var nconf = require('nconf'); @@ -13,109 +13,119 @@ var emailer = require('../emailer'); var utils = require('../../public/src/utils'); (function (Digest) { - Digest.execute = function (interval) { - var digestsDisabled = meta.config.disableEmailSubscriptions !== undefined && parseInt(meta.config.disableEmailSubscriptions, 10) === 1; + Digest.execute = function (interval, callback) { + callback = callback || function () {}; + + var digestsDisabled = parseInt(meta.config.disableEmailSubscriptions, 10) === 1; if (digestsDisabled) { - return winston.verbose('[user/jobs] Did not send digests (' + interval + ') because subscription system is disabled.'); + winston.verbose('[user/jobs] Did not send digests (' + interval + ') because subscription system is disabled.'); + return callback(); } if (!interval) { // interval is one of: day, week, month, or year interval = 'day'; } - - async.parallel({ - topics: async.apply(topics.getLatestTopics, 0, 0, 9, interval), - subscribers: async.apply(Digest.getSubscribers, interval) - }, function (err, data) { - if (err) { - return winston.error('[user/jobs] Could not send digests (' + interval + '): ' + err.message); - } - - // Fix relative paths in topic data - data.topics.topics = data.topics.topics.map(function (topicObj) { - var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user; - if (user && user.picture && utils.isRelativeUrl(user.picture)) { - user.picture = nconf.get('base_url') + user.picture; + var subscribers; + async.waterfall([ + function (next) { + async.parallel({ + topics: async.apply(topics.getLatestTopics, 0, 0, 9, interval), + subscribers: async.apply(Digest.getSubscribers, interval) + }, next); + }, + function (data, next) { + subscribers = data.subscribers; + if (!data.subscribers.length) { + return callback(); } - return topicObj; - }); - - data.interval = interval; - - if (data.subscribers.length) { - Digest.send(data, function (err) { - if (err) { - winston.error('[user/jobs] Could not send digests (' + interval + '): ' + err.message); - } else { - winston.info('[user/jobs] Digest (' + interval + ') scheduling completed. ' + data.subscribers.length + ' email(s) sent.'); + // Fix relative paths in topic data + data.topics.topics = data.topics.topics.map(function (topicObj) { + var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user; + if (user && user.picture && utils.isRelativeUrl(user.picture)) { + user.picture = nconf.get('base_url') + user.picture; } + + return topicObj; }); - } else { - winston.verbose('[user/jobs] No users subscribing to digest (' + interval + '). Digest not sent.'); + + data.interval = interval; + Digest.send(data, next); } + ], function (err) { + if (err) { + winston.error('[user/jobs] Could not send digests (' + interval + '): ' + err.message); + } else { + winston.verbose('[user/jobs] Digest (' + interval + ') scheduling completed. ' + subscribers.length + ' email(s) sent.'); + } + + callback(err); }); }; Digest.getSubscribers = function (interval, callback) { - db.getSortedSetRange('digest:' + interval + ':uids', 0, -1, function (err, subscribers) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.getSortedSetRange('digest:' + interval + ':uids', 0, -1, next); + }, + function (subscribers, next) { + plugins.fireHook('filter:digest.subscribers', { + interval: interval, + subscribers: subscribers + }, next); + }, + function (results, next) { + next(null, results.subscribers); } - - plugins.fireHook('filter:digest.subscribers', { - interval: interval, - subscribers: subscribers - }, function (err, returnData) { - callback(err, returnData.subscribers); - }); - }); + ], callback); }; Digest.send = function (data, callback) { - var now = new Date(); + if (!data || !data.subscribers || !data.subscribers.length) { + return callback(); + } + var now = new Date(); - user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline'], function (err, users) { - if (err) { - winston.error('[user/jobs] Could not send digests (' + data.interval + '): ' + err.message); - return callback(err); - } + async.waterfall([ + function (next) { + user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline'], next); + }, + function (users, next) { + async.eachLimit(users, 100, function (userObj, next) { + async.waterfall([ + function (next) { + user.notifications.getDailyUnread(userObj.uid, next); + }, + function (notifications, next) { + notifications = notifications.filter(Boolean); + // If there are no notifications and no new topics, don't bother sending a digest + if (!notifications.length && !data.topics.topics.length) { + return next(); + } - async.eachLimit(users, 100, function (userObj, next) { - user.notifications.getDailyUnread(userObj.uid, function (err, notifications) { - if (err) { - winston.error('[user/jobs] Could not send digests (' + data.interval + '): ' + err.message); - return next(err); - } - - notifications = notifications.filter(Boolean); - - // If there are no notifications and no new topics, don't bother sending a digest - if (!notifications.length && !data.topics.topics.length) { - return next(); - } - - for(var i = 0; i < notifications.length; ++i) { - if (notifications[i].image && notifications[i].image.indexOf('http') !== 0) { - notifications[i].image = nconf.get('url') + notifications[i].image; + notifications.forEach(function (notification) { + if (notification.image && !notification.image.startsWith('http')) { + notification.image = nconf.get('url') + notification.image; + } + }); + emailer.send('digest', userObj.uid, { + subject: '[' + meta.config.title + '] [[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', + username: userObj.username, + userslug: userObj.userslug, + url: nconf.get('url'), + site_title: meta.config.title || meta.config.browserTitle || 'NodeBB', + notifications: notifications, + recent: data.topics.topics, + interval: data.interval + }); + next(); } - } - - emailer.send('digest', userObj.uid, { - subject: '[' + meta.config.title + '] [[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', - username: userObj.username, - userslug: userObj.userslug, - url: nconf.get('url'), - site_title: meta.config.title || meta.config.browserTitle || 'NodeBB', - notifications: notifications, - recent: data.topics.topics, - interval: data.interval - }); - - next(); - }); - }, callback); + ], next); + }, next); + } + ], function (err) { + callback(err); }); }; diff --git a/src/user/follow.js b/src/user/follow.js index 0812c1004d..e6c9624018 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -1,9 +1,9 @@ 'use strict'; -var async = require('async'), - plugins = require('../plugins'), - db = require('../database'); +var async = require('async'); +var plugins = require('../plugins'); +var db = require('../database'); module.exports = function (User) { @@ -73,25 +73,22 @@ module.exports = function (User) { if (!parseInt(uid, 10)) { return callback(null, []); } - - db.getSortedSetRevRange(type + ':' + uid, start, stop, function (err, uids) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.getSortedSetRevRange(type + ':' + uid, start, stop, next); + }, + function (uids, next) { + plugins.fireHook('filter:user.' + type, { + uids: uids, + uid: uid, + start: start, + stop: stop + }, next); + }, + function (data, next) { + User.getUsers(data.uids, uid, next); } - - plugins.fireHook('filter:user.' + type, { - uids: uids, - uid: uid, - start: start, - stop: stop - }, function (err, data) { - if (err) { - return callback(err); - } - - User.getUsers(data.uids, uid, callback); - }); - }); + ], callback); } User.isFollowing = function (uid, theirid, callback) { diff --git a/src/user/info.js b/src/user/info.js index 1ed9cdef14..296dd9a723 100644 --- a/src/user/info.js +++ b/src/user/info.js @@ -16,6 +16,10 @@ module.exports = function (User) { async.waterfall([ async.apply(db.getSortedSetRevRangeWithScores, 'uid:' + uid + ':bans', 0, 0), function (record, next) { + if (!record.length) { + return next(new Error('no-ban-info')); + } + timestamp = record[0].score; expiry = record[0].value; diff --git a/src/user/settings.js b/src/user/settings.js index 29666cfce5..3a5b53b9c3 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -65,7 +65,7 @@ module.exports = function (User) { settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1; settings.topicsPerPage = Math.min(settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, defaultTopicsPerPage); settings.postsPerPage = Math.min(settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, defaultPostsPerPage); - settings.userLang = settings.userLang || meta.config.defaultLang || 'en_GB'; + settings.userLang = settings.userLang || meta.config.defaultLang || 'en-GB'; settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest'); settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest'); settings.followTopicsOnCreate = parseInt(getSetting(settings, 'followTopicsOnCreate', 1), 10) === 1; diff --git a/src/views/admin/extend/widgets.tpl b/src/views/admin/extend/widgets.tpl index aa313b9a4d..381d03794e 100644 --- a/src/views/admin/extend/widgets.tpl +++ b/src/views/admin/extend/widgets.tpl @@ -33,7 +33,7 @@

Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.

-
No widgets found! Activate the essential widgets plugin in the plugins control panel.
+
No widgets found! Activate the essential widgets plugin in the plugins control panel.


- Click here to visit the tag settings page. + Click here to visit the tag settings page.

diff --git a/src/views/admin/partials/categories/privileges.tpl b/src/views/admin/partials/categories/privileges.tpl index 497fa71ac0..28b5f0849f 100644 --- a/src/views/admin/partials/categories/privileges.tpl +++ b/src/views/admin/partials/categories/privileges.tpl @@ -1,4 +1,10 @@ - +
+ + + + + + @@ -34,7 +40,13 @@
Viewing PrivilegesPosting PrivilegesModeration Privileges
User
- +
+ + + + + + diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 7e4f890730..0fea719e46 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -122,6 +122,11 @@
Viewing PrivilegesPosting PrivilegesModeration Privileges
Group