diff --git a/package.json b/package.json index bf1a6ca483..088c564c17 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "start": "node loader.js", "lint": "eslint --cache .", "pretest": "npm run lint", - "test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- ./tests -t 10000" + "test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- ./tests -t 10000", + "test-windows": "./node_modules/.bin/_mocha.cmd ./tests -t 10000" }, "dependencies": { "async": "~1.5.0", @@ -50,7 +51,7 @@ "morgan": "^1.3.2", "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "4.2.2", + "nodebb-plugin-composer-default": "4.2.4", "nodebb-plugin-dbsearch": "1.0.2", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.1.5", @@ -60,8 +61,8 @@ "nodebb-plugin-spam-be-gone": "0.4.10", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "3.0.14", - "nodebb-theme-persona": "4.1.42", - "nodebb-theme-vanilla": "5.1.25", + "nodebb-theme-persona": "4.1.43", + "nodebb-theme-vanilla": "5.1.27", "nodebb-widget-essentials": "2.0.10", "nodemailer": "2.0.0", "nodemailer-sendmail-transport": "1.0.0", @@ -69,6 +70,7 @@ "passport": "^0.3.0", "passport-local": "1.0.0", "postcss": "^5.0.13", + "promise-polyfill": "^6.0.2", "prompt": "^1.0.0", "redis": "~2.6.2", "request": "^2.44.0", @@ -121,4 +123,4 @@ "url": "https://github.com/barisusakli" } ] -} \ No newline at end of file +} diff --git a/public/language/ar/user.json b/public/language/ar/user.json index 65b93f7a46..0cabd41e33 100644 --- a/public/language/ar/user.json +++ b/public/language/ar/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/bg/user.json b/public/language/bg/user.json index 624c56c91f..d02f6fcfc1 100644 --- a/public/language/bg/user.json +++ b/public/language/bg/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Скорошна история на блокиранията", "info.no-ban-history": "Този потребител никога не е бил блокиран", "info.banned-until": "Блокиран до %1", - "info.banned-permanently": "Блокиран за постоянно" + "info.banned-permanently": "Блокиран за постоянно", + "info.banned-reason-label": "Причина", + "info.banned-no-reason": "Няма посочена причина." } \ No newline at end of file diff --git a/public/language/bn/user.json b/public/language/bn/user.json index 94430186a6..aab8625def 100644 --- a/public/language/bn/user.json +++ b/public/language/bn/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/cs/user.json b/public/language/cs/user.json index f4c5c62fd8..74fa70dbac 100644 --- a/public/language/cs/user.json +++ b/public/language/cs/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/da/user.json b/public/language/da/user.json index 3391eb928c..95fb3bb216 100644 --- a/public/language/da/user.json +++ b/public/language/da/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/de/category.json b/public/language/de/category.json index 9169f01756..f76ea9ae94 100644 --- a/public/language/de/category.json +++ b/public/language/de/category.json @@ -10,8 +10,8 @@ "share_this_category": "Teile diese Kategorie", "watch": "Beobachten", "ignore": "Ignorieren", - "watching": "Watching", - "ignoring": "Ignoring", + "watching": "Beobachte", + "ignoring": "Ignoriere", "watching.description": "Show topics in unread", "ignoring.description": "Do not show topics in unread", "watch.message": "Du beobachtest jetzt Änderungen in dieser Kategorie", diff --git a/public/language/de/error.json b/public/language/de/error.json index 19d65e559a..77cea0ae9e 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -20,7 +20,7 @@ "email-taken": "Die E-Mail-Adresse ist bereits vergeben", "email-not-confirmed": "Deine E-Mail wurde noch nicht bestätigt, bitte klicke hier, um deine E-Mail zu bestätigen.", "email-not-confirmed-chat": "Du kannst denn Chat erst nutzen wenn deine E-Mail bestätigt wurde, bitte klicke hier, um deine E-Mail zu bestätigen.", - "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", + "email-not-confirmed-email-sent": "Deine E-Mail wurde noch nicht bestätigt, bitte schaue in deinem Posteingang nach der Bestätigungsmail.", "no-email-to-confirm": "Dieses Forum setzt eine E-Mail-Bestätigung voraus, bitte klicke hier um eine E-Mail-Adresse einzugeben.", "email-confirm-failed": "Wir konnten deine E-Mail-Adresse nicht bestätigen, bitte versuch es später noch einmal", "confirm-email-already-sent": "Die Bestätigungsmail wurde verschickt, bitte warte %1 Minute(n) um eine Weitere zu verschicken.", @@ -48,10 +48,10 @@ "post-edit-duration-expired-hours-minutes": "Du darfst Beiträge lediglich innerhalb von %1 Stunde/n und %2 Minute/n nach dem Erstellen editieren", "post-edit-duration-expired-days": "Du darfst Beiträge lediglich innerhalb von %1 Tag/en nach dem Erstellen editieren", "post-edit-duration-expired-days-hours": "Du darfst Beiträge lediglich innerhalb von %1 Tag/en und %2 Stunde/n nach dem Erstellen editieren", - "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired": "Du darfst Beiträge lediglich innerhalb von %1 Sekunden nach dem Erstellen löschen", "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", - "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours": "Du darfst Beiträge lediglich innerhalb von %1 Stunde/n nach dem Erstellen löschen", "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", @@ -79,7 +79,7 @@ "invalid-image-extension": "Ungültige Dateinamenerweiterung", "invalid-file-type": "Ungültiger Dateityp. Erlaubte Typen sind: %1", "group-name-too-short": "Gruppenname zu kurz", - "group-name-too-long": "Group name too long", + "group-name-too-long": "Gruppenname zu lang", "group-already-exists": "Gruppe existiert bereits", "group-name-change-not-allowed": "Du kannst den Namen der Gruppe nicht ändern", "group-already-member": "Bereits Teil dieser Gruppe", @@ -122,7 +122,7 @@ "not-in-room": "Benutzer nicht im Raum", "no-users-in-room": "In diesem Raum befinden sich keine Benutzer.", "cant-kick-self": "Du kannst dich nicht selber aus der Gruppe entfernen.", - "no-users-selected": "No user(s) selected", + "no-users-selected": "Keine Benutzer ausgewählt", "invalid-home-page-route": "Invalid home page route", "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." diff --git a/public/language/de/topic.json b/public/language/de/topic.json index 816f417397..e5cbbdc755 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -27,13 +27,13 @@ "flag": "Markieren", "locked": "Gesperrt", "pinned": "Pinned", - "moved": "Moved", + "moved": "Verschoben", "bookmark_instructions": "Klicke hier, um zum letzten gelesenen Beitrag des Themas zurückzukehren.", "flag_title": "Diesen Beitrag zur Moderation markieren", "flag_success": "Dieser Beitrag wurde erfolgreich für die Moderation markiert.", "deleted_message": "Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.", "following_topic.message": "Du erhälst nun eine Benachrichtigung, wenn jemand einen Beitrag zu diesem Thema verfasst.", - "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "not_following_topic.message": "Ungelesene Beiträge in diesem Thema werden angezeigt, aber du erhältst keine Benachrichtigung wenn jemand einen Beitrag zu diesem Thema verfasst.", "ignoring_topic.message": "Ungelesene Beiträge in diesem Thema werden nicht mehr angezeigt. Du erhältst eine Benachrichtigung wenn du in diesem Thema erwähnt wirst oder deine Beiträge positiv bewertet werden.", "login_to_subscribe": "Bitte registrieren oder einloggen um dieses Thema zu abonnieren", "markAsUnreadForAll.success": "Thema für Alle als ungelesen markiert.", @@ -86,7 +86,7 @@ "topic_will_be_moved_to": "Dieses Thema wird verschoben nach", "fork_topic_instruction": "Klicke auf die Beiträge, die aufgespaltet werden sollen", "fork_no_pids": "Keine Beiträge ausgewählt!", - "fork_pid_count": "%1 post(s) selected", + "fork_pid_count": "%1 Beiträge ausgewählt", "fork_success": "Thema erfolgreich aufgespalten! Klicke hier, um zum aufgespalteten Thema zu gelangen.", "delete_posts_instruction": "Wähle die zu löschenden Beiträge aus", "composer.title_placeholder": "Hier den Titel des Themas eingeben...", diff --git a/public/language/de/user.json b/public/language/de/user.json index fe75e315ae..d8f852f09b 100644 --- a/public/language/de/user.json +++ b/public/language/de/user.json @@ -6,7 +6,7 @@ "postcount": "Beiträge", "email": "E-Mail", "confirm_email": "E-Mail bestätigen", - "account_info": "Account Info", + "account_info": "Kontoinformationen", "ban_account": "Konto sperren", "ban_account_confirm": "Bist du sicher, dass du diesen Benutzer sperren möchtest?", "unban_account": "Konto entsperren", @@ -89,7 +89,7 @@ "topics_per_page": "Themen pro Seite", "posts_per_page": "Beiträge pro Seite", "notification_sounds": "Ton abspielen, wenn du eine Benachrichtigung erhältst", - "notifications_and_sounds": "Notifications & Sounds", + "notifications_and_sounds": "Benachrichtigungen & Klänge", "incoming-message-sound": "Incoming message sound", "outgoing-message-sound": "Outgoing message sound", "notification-sound": "Notification sound", @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Grund", + "info.banned-no-reason": "Kein Grund angegeben." } \ No newline at end of file diff --git a/public/language/el/user.json b/public/language/el/user.json index 5bb86d6774..44b7a6cde2 100644 --- a/public/language/el/user.json +++ b/public/language/el/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/en@pirate/user.json b/public/language/en@pirate/user.json index 52b1802eeb..c9236ae85b 100644 --- a/public/language/en@pirate/user.json +++ b/public/language/en@pirate/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/en_US/user.json b/public/language/en_US/user.json index d188d01f61..bbe1545754 100644 --- a/public/language/en_US/user.json +++ b/public/language/en_US/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/es/error.json b/public/language/es/error.json index 15cf9dcfec..288e4c4088 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -48,13 +48,13 @@ "post-edit-duration-expired-hours-minutes": "No puedes editar mensajes hasta pasado %1 hora(s) y %2 minuto(s) después de haberlo escrito", "post-edit-duration-expired-days": "No puedes editar mensajes hasta pasado %1 día(s) después de haberlo escrito", "post-edit-duration-expired-days-hours": "No puedes editar mensajes hasta pasado %1 día(s) y %2 hora(s) después de haberlo escrito", - "post-delete-duration-expired": "No puedes borrar mensajes hasta pasado %1 segundo(s) después de haberlo escrito", - "post-delete-duration-expired-minutes": "No puedes borrar mensajes hasta pasado %1 minuto(s) después de haberlo escrito", - "post-delete-duration-expired-minutes-seconds": "No puedes borrar mensajes hasta pasado %1 minuto(s) y %2 segundo(s) después de haberlo escrito", - "post-delete-duration-expired-hours": "No puedes borrar mensajes hasta pasado %1 hora(s) después de haberlo escrito", - "post-delete-duration-expired-hours-minutes": "No puedes borrar mensajes hasta pasado %1 hora(s) y %2 minuto(s) después de haberlo escrito", - "post-delete-duration-expired-days": "No puedes borrar mensajes hasta pasado %1 día(s) después de haberlo escrito", - "post-delete-duration-expired-days-hours": "No puedes borrar mensajes hasta pasado %1 día(s) y %2 hora(s) después de haberlo escrito", + "post-delete-duration-expired": "No puedes borrar mensajes tras pasar %1 segundo(s) después de haberlo escrito", + "post-delete-duration-expired-minutes": "No puedes borrar mensajes tras pasar %1 minuto(s) después de haberlo escrito", + "post-delete-duration-expired-minutes-seconds": "No puedes borrar mensajes tras pasar %1 minuto(s) y %2 segundo(s) después de haberlo escrito", + "post-delete-duration-expired-hours": "No puedes borrar mensajes tras pasar %1 hora(s) después de haberlo escrito", + "post-delete-duration-expired-hours-minutes": "No puedes borrar mensajes tras pasar %1 hora(s) y %2 minuto(s) después de haberlo escrito", + "post-delete-duration-expired-days": "No puedes borrar mensajes tras pasar %1 día(s) después de haberlo escrito", + "post-delete-duration-expired-days-hours": "No puedes borrar mensajes tras pasar %1 día(s) y %2 hora(s) después de haberlo escrito", "cant-delete-topic-has-reply": "No puedes borrar tu tema después de que tenga respuestas", "cant-delete-topic-has-replies": "No puedes borrar tu tema despues de que tenga ℅1 respuestas", "content-too-short": "Por favor introduzca una publicación más larga. Las publicaciones deben contener al menos %1 caractere(s).", @@ -124,6 +124,6 @@ "cant-kick-self": "No te puedes expulsar a ti mismo del grupo", "no-users-selected": "Ningun usuario(s) seleccionado", "invalid-home-page-route": "Ruta de página de inicio invalida", - "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": "No concuerdan los datos de sesión", + "invalid-session-text": "Parece que su sesión ha expirado o no concuerda con el servidor. Por favor vuelva a cargar la página." } \ No newline at end of file diff --git a/public/language/es/user.json b/public/language/es/user.json index da036d0b7e..c9c92b53eb 100644 --- a/public/language/es/user.json +++ b/public/language/es/user.json @@ -89,10 +89,10 @@ "topics_per_page": "Temas por página", "posts_per_page": "Post por página", "notification_sounds": "Reproducir un sonido al recibir una notificación", - "notifications_and_sounds": "Notifications & Sounds", - "incoming-message-sound": "Incoming message sound", - "outgoing-message-sound": "Outgoing message sound", - "notification-sound": "Notification sound", + "notifications_and_sounds": "Notificaciones y Sonidos", + "incoming-message-sound": "Sonido del mensaje entrante", + "outgoing-message-sound": "Sonido del mensaje saliente", + "notification-sound": "Sonido de notificación", "browsing": "Preferencias de navegación.", "open_links_in_new_tab": "Abrir los enlaces externos en una nueva pestaña", "enable_topic_searching": "Activar la búsqueda \"dentro del tema\"", @@ -118,5 +118,7 @@ "info.ban-history": "Histórico reciente de bans", "info.no-ban-history": "Este usuario nunca ha sido baneado", "info.banned-until": "Baneado hasta %1", - "info.banned-permanently": "Baneado permanentemente" + "info.banned-permanently": "Baneado permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Motivo no especificado" } \ No newline at end of file diff --git a/public/language/et/user.json b/public/language/et/user.json index 1d63381814..2a7187b440 100644 --- a/public/language/et/user.json +++ b/public/language/et/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Hiljutiste keeldude ajalugu", "info.no-ban-history": "Seda kasutajat pole kunagi keelustatud", "info.banned-until": "Keelustatud kuni %1", - "info.banned-permanently": "Igavesti keelustatud" + "info.banned-permanently": "Igavesti keelustatud", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/fa_IR/user.json b/public/language/fa_IR/user.json index bb40b5dd73..acccf82a6d 100644 --- a/public/language/fa_IR/user.json +++ b/public/language/fa_IR/user.json @@ -1,20 +1,20 @@ { - "banned": "اخراج شده", + "banned": "مسدود شده", "offline": "آفلاین", "username": "نام کاربری", "joindate": "زمان عضویت", - "postcount": "تعداد پست ها", + "postcount": "تعداد پست‌ها", "email": "رایانامه", "confirm_email": "تأیید ایمیل", "account_info": "اطلاعات شناسه کاربری", - "ban_account": "مسدود کردن حساب کاربری", + "ban_account": "مسدود کردن", "ban_account_confirm": "از مسدود کردن این کاربر اطمینان دارید؟", "unban_account": "آزاد کردن حساب کاربری", "delete_account": "حذف حساب کاربری", "delete_account_confirm": "آیا مطمئنید که میخواهید حساب کاربری خود را حذف کنید؟
این عمل غیر قابل بازگشت است و شما قادر نخواهید بود هیچ کدام از اطلاعات خود را بازیابی کنید./strong>

برای تایید حذف این حساب کاربری، نام کاربری خود را وارد کنید", "delete_this_account_confirm": "آیا مطمئنید که میخواهید این حساب کاربری را حذف کنید؟
این عمل غیر قابل بازگشت است و شما قادر نخواهید بود هیچ کدام از اطلاعات را بازیابی کنید.

", "account-deleted": "حساب کاربری پاک شد", - "fullname": "نام و نام خانوادگی", + "fullname": "نام و نام‌خانوادگی", "website": "تارنما", "location": "محل سکونت", "age": "سن", @@ -63,11 +63,11 @@ "remove_uploaded_picture": "پاک کردن عکس بارگذاری شده", "upload_cover_picture": "بارگذاری عکس کاور", "settings": "تنظیمات", - "show_email": "نمایش ایمیل‌ام", + "show_email": "نمایش ایمیل‌های من", "show_fullname": "نام کامل من را نشان بده", - "restrict_chats": "قبول پیام فقط ازکاربرانی که من را دنبال میکنند", + "restrict_chats": "قبول پیام فقط ازکاربرانی که آن‌ها را دنبال میکنم", "digest_label": "مشترک شدن در چکیده", - "digest_description": "مشترک شدن برای دریافت تازه‌هی این انجمن (موضوع ها و آکاه‌سازی‌های تازه) با ایمیل روی یک برنامه زمان‌بندی", + "digest_description": "مشترک شدن برای دریافت جدیدترین‌های این انجمن (موضوع ها و آکاه‌سازی‌های تازه) با ایمیل روی یک برنامه زمان‌بندی", "digest_off": "خاموش", "digest_daily": "روزانه", "digest_weekly": "هفتگی", @@ -118,5 +118,7 @@ "info.ban-history": "تاریخچه مسدودیت اخیر", "info.no-ban-history": "این کاربر هرگز مسدود نشده است", "info.banned-until": "مسدود شده تا %1", - "info.banned-permanently": "مسدود شده به طور دائم" + "info.banned-permanently": "مسدود شده به طور دائم", + "info.banned-reason-label": "دلیل", + "info.banned-no-reason": "هیچ دلیلی ارایه نشد." } \ No newline at end of file diff --git a/public/language/fa_IR/users.json b/public/language/fa_IR/users.json index 1bb821706d..81b664bbb1 100644 --- a/public/language/fa_IR/users.json +++ b/public/language/fa_IR/users.json @@ -1,21 +1,21 @@ { "latest_users": "آخرین کاربران", - "top_posters": "بهترین فرستنده‌ها", + "top_posters": "برترین فرستنده‌ها", "most_reputation": "بیش‌ترین اعتبار", "most_flags": "بیشترین پرچم‌ها", "search": "جستجو", "enter_username": "یک نام کاربری برای جستجو وارد کنید", "load_more": "بارگذاری بیش‌تر", - "users-found-search-took": "%1 کاربر(ها) یافت شد! جستجو %2 ثانیه طولید", + "users-found-search-took": "%1 کاربر(ها) یافت شد! جستجو %2 ثانیه زمان گرفت.", "filter-by": "فیلتر با", "online-only": "فقط آنلاین", "invite": "دعوت", - "invitation-email-sent": "ایمیل ی دعوتنامه به %1 ارسال شد", + "invitation-email-sent": "ایمیل دعوتنامه برای %1 ارسال شد", "user_list": "فهرست کاربران", - "recent_topics": "موضوع های اخیر", - "popular_topics": "موضوع های پربازدید", - "unread_topics": "موضوع های خوانده نشده", - "categories": "دسته ها", + "recent_topics": "موضوع‌های اخیر", + "popular_topics": "موضوع‌های پربازدید", + "unread_topics": "موضوع‌های خوانده نشده", + "categories": "دسته‌ها", "tags": "برچسب‌ها", "no-users-found": "کاربری پیدا نشد!" } \ No newline at end of file diff --git a/public/language/fi/user.json b/public/language/fi/user.json index 66ca174258..2620d68586 100644 --- a/public/language/fi/user.json +++ b/public/language/fi/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/fr/user.json b/public/language/fr/user.json index 076e154971..05683ca3ad 100644 --- a/public/language/fr/user.json +++ b/public/language/fr/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Historique de bannissement récent", "info.no-ban-history": "Cet utilisateur n'a jamais été banni", "info.banned-until": "Banni jusqu'au %1", - "info.banned-permanently": "Banni de façon permanente" + "info.banned-permanently": "Banni de façon permanente", + "info.banned-reason-label": "Raison", + "info.banned-no-reason": "Aucune raison donnée" } \ No newline at end of file diff --git a/public/language/gl/user.json b/public/language/gl/user.json index 9cdea0f306..b19642dcee 100644 --- a/public/language/gl/user.json +++ b/public/language/gl/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Histórico recente de bans", "info.no-ban-history": "Este usuario nunca foi baneado", "info.banned-until": "Baneado hasta 1%", - "info.banned-permanently": "Baneado permanentemente" + "info.banned-permanently": "Baneado permanentemente", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/he/user.json b/public/language/he/user.json index 7287d5c1f0..06425a7113 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/hu/user.json b/public/language/hu/user.json index 19bd8c3154..d69f15ed01 100644 --- a/public/language/hu/user.json +++ b/public/language/hu/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/id/user.json b/public/language/id/user.json index 715648a4bd..81aad04dbf 100644 --- a/public/language/id/user.json +++ b/public/language/id/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/it/user.json b/public/language/it/user.json index d1f1edafa7..767099e268 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Storico dei Ban recenti", "info.no-ban-history": "Questo utente non è mai stato bannato", "info.banned-until": "Bannato fino %1", - "info.banned-permanently": "Bannato permanentemente" + "info.banned-permanently": "Bannato permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Non è stata data nessuna motivazione." } \ No newline at end of file diff --git a/public/language/ja/user.json b/public/language/ja/user.json index 16cd6a8811..acf7ffdac4 100644 --- a/public/language/ja/user.json +++ b/public/language/ja/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/ko/user.json b/public/language/ko/user.json index bd8404161d..3c12333aa5 100644 --- a/public/language/ko/user.json +++ b/public/language/ko/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/lt/category.json b/public/language/lt/category.json index 8c909bae42..abe245da73 100644 --- a/public/language/lt/category.json +++ b/public/language/lt/category.json @@ -1,20 +1,20 @@ { - "category": "Category", - "subcategories": "Subcategories", + "category": "Kategorija", + "subcategories": "Subkategorijos", "new_topic_button": "Nauja tema", "guest-login-post": "Prisijungti įrašų paskelbimui", "no_topics": "Šioje kategorijoje temų nėra.
Kodėl gi jums nesukūrus naujos?", "browsing": "naršo", - "no_replies": "Niekas dar neatsakė", - "no_new_posts": "No new posts.", + "no_replies": "Nėra atsakymų", + "no_new_posts": "Nėra naujų pranešimų.", "share_this_category": "Pasidalinti šią kategoriją", "watch": "Stebėti", - "ignore": "Nepaisyti", - "watching": "Watching", - "ignoring": "Ignoring", + "ignore": "Ignoruoti", + "watching": "Stebima", + "ignoring": "Ignoruojama", "watching.description": "Show topics in unread", "ignoring.description": "Do not show topics in unread", "watch.message": "Jūs dabar stebite atnaujinimus iš šios kategorijos", "ignore.message": "Jūs dabar ignoruojate atnaujinimus iš šios kategorijos", - "watched-categories": "Watched categories" + "watched-categories": "Stebimos kategorijos" } \ No newline at end of file diff --git a/public/language/lt/email.json b/public/language/lt/email.json index f183b346f2..d4f24c89e5 100644 --- a/public/language/lt/email.json +++ b/public/language/lt/email.json @@ -21,9 +21,9 @@ "digest.cta": "Kad aplankyti %1, spauskite čia", "digest.unsub.info": "Ši santrauka buvo išsiųsta į tavo prenumeratos nustatymus", "digest.no_topics": "Nebuvo aktyvių temų praeityje %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", + "digest.day": "diena", + "digest.week": "savaitė", + "digest.month": "mėnuo", "digest.subject": "Digest for %1", "notif.chat.subject": "Nauja pokalbio žinutė gauta iš %1", "notif.chat.cta": "Pokalbio pratęsimui spauskite čia", diff --git a/public/language/lt/uploads.json b/public/language/lt/uploads.json index 1622cb5693..a838cdf68d 100644 --- a/public/language/lt/uploads.json +++ b/public/language/lt/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "įkeliama...", + "select-file-to-upload": "Pasirinkite failą, kurį norite įkelti.", + "upload-success": "Failas įkeltas sėkmingai!", + "maximum-file-size": "Daugiausiai %1 kb" } \ No newline at end of file diff --git a/public/language/lt/user.json b/public/language/lt/user.json index 31f55b8307..da60c551d8 100644 --- a/public/language/lt/user.json +++ b/public/language/lt/user.json @@ -6,14 +6,14 @@ "postcount": "Įrašų kiekis", "email": "El. paštas", "confirm_email": "Patvirtinti el. paštą", - "account_info": "Account Info", + "account_info": "Paskyros informacija", "ban_account": "Užblokuoti Paskyrą", "ban_account_confirm": "Jūs tikrai norite užblokuoti šį vartotoją?", "unban_account": "Atblokuoti Paskyrą", "delete_account": "Ištrinti paskyrą", "delete_account_confirm": "Ar tikrai norite ištrinti savo paskyrą?
Šis veiksmas yra negrįžtamas, ir jūs negalėsite susigrąžinti jokių duomenų

Įveskite savo vardą, kad patvirtintumėte, jog norite panaikinti šią paskyrą.", "delete_this_account_confirm": "Ar jūs tikrai norite ištrint šią paskyrą?
Šis veiksmas nebesugražinamas ir jūs nebegalėsite atgauti jokių duomenų

", - "account-deleted": "Account deleted", + "account-deleted": "Paskyra ištrinta", "fullname": "Vardas ir pavardė", "website": "Tinklalapis", "location": "Vieta", @@ -23,7 +23,7 @@ "profile": "Profilis", "profile_views": "Profilio peržiūros", "reputation": "Reputacija", - "favourites": "Bookmarks", + "favourites": "Žymės", "watched": "Peržiūrėjo", "followers": "Sekėjai", "following": "Seka", @@ -31,17 +31,17 @@ "signature": "Parašas", "birthday": "Gimimo diena", "chat": "Susirašinėti", - "chat_with": "Chat with %1", + "chat_with": "Susirašinėti su %1", "follow": "Sekti", "unfollow": "Nesekti", "more": "Daugiau", "profile_update_success": "Profilis sėkmingai atnaujintas!", "change_picture": "Pakeisti paveikslėlį", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "Keisti vartotojo vardą", + "change_email": "Keisti el. pašto adresą", "edit": "Redaguoti", - "edit-profile": "Edit Profile", - "default_picture": "Default Icon", + "edit-profile": "Redaguoti profilį", + "default_picture": "Standartinis paveikslėlis", "uploaded_picture": "Įkeltas paveikslėlis", "upload_new_picture": "Įkelti naują paveikslėlį", "upload_new_picture_from_url": "Įkelti naują paveikslėlį iš URL", @@ -56,12 +56,12 @@ "confirm_password": "Patvirtinkite slaptažodį", "password": "Slaptažodis", "username_taken_workaround": "Jūsų norimas vartotojo vardas jau užimtas, todėl mes jį šiek tiek pakeitėme. Dabar jūs esate žinomas kaip %1", - "password_same_as_username": "Your password is the same as your username, please select another password.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_username": "Jūsų slaptažodis sutampa su Jūsų vartotojo vardu. Dėl saugumo, prašome naudoti kitą slaptažodį.", + "password_same_as_email": "Jūsų slaptažodis sutampa su Jūsų el. pašto adresu. Dėl saugumo, prašome naudoti kitą slaptažodį.", "upload_picture": "Įkelti paveikslėlį", "upload_a_picture": "Įkelti paveikslėlį", - "remove_uploaded_picture": "Remove Uploaded Picture", - "upload_cover_picture": "Upload cover picture", + "remove_uploaded_picture": "Ištrinti paveikslėlį", + "upload_cover_picture": "Įkelti viršelio nuotrauką", "settings": "Nustatymai", "show_email": "Rodyti mano el. paštą viešai", "show_fullname": "Rodyti mano vardą ir pavardę", @@ -80,43 +80,45 @@ "has_no_posts": "Šis vartotojas pakolkas neparašė jokių pranešimų", "has_no_topics": "Šis vartotojas pakolkas nesukūrė jokių temų", "has_no_watched_topics": "Šis vartotojas pakolkas nestebėjo jokių temų", - "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_voted_posts": "This user has no voted posts", + "has_no_upvoted_posts": "Šis narys dar neturi teigiamai įvertintų pranešimų.", + "has_no_downvoted_posts": "Šis narys dar neturi neigiamai įvertintų pranešimų.", + "has_no_voted_posts": "Šis narys dar neturi įvertintų pranešimų.", "email_hidden": "El. paštas paslėptas", "hidden": "paslėptas", "paginate_description": "Puslapiavimas temų ir pranešimų, vietoj kad naudoti judėjimą su pelytė į viršų ir į apačia", "topics_per_page": "Temų puslapyje", "posts_per_page": "Pranešimų puslapyje", "notification_sounds": "Paleisti garsą kai jūs gaunate pranešimą", - "notifications_and_sounds": "Notifications & Sounds", - "incoming-message-sound": "Incoming message sound", - "outgoing-message-sound": "Outgoing message sound", - "notification-sound": "Notification sound", + "notifications_and_sounds": "Pranešimai ir garsai", + "incoming-message-sound": "Gaunamos žinutės garsas", + "outgoing-message-sound": "Siunčiamos žinutės garsas", + "notification-sound": "Pranešimo garsas", "browsing": "Naršymo nustatymai", "open_links_in_new_tab": "Atidaryti išeinančias nuorodas naujam skirtuke", "enable_topic_searching": "Įjungti Temų Ieškojimą ", "topic_search_help": "Jeigu įjungtas, temų ieškojimas, nepaisys naršyklės puslapio ieškojimo, ir pradės ieškoti tik toje temoje kuri bus rodoma ekrane", "delay_image_loading": "Delay Image Loading", - "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", - "follow_topics_you_create": "Watch topics you create", - "grouptitle": "Group Title", + "image_load_delay_help": "Jei įjungta, paveikslėliai temose nesikraus, kol nebus iki jų nuslinkta", + "scroll_to_my_post": "Po parašyto atsakymo, rodyti naują pranešimą", + "follow_topics_you_reply_to": "Peržiūrėti temas, kuriose Jūs atsakėte", + "follow_topics_you_create": "Peržiūrėti temas, kurias Jūs sukūrėte", + "grouptitle": "Grupės pavadinimas", "no-group-title": "Nėra grupės pavadinimo", - "select-skin": "Select a Skin", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", - "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", - "custom_route": "Custom Homepage Route", + "select-skin": "Pasirinkite išvaizdą", + "select-homepage": "Pasirinkite pagrindinį puslapį", + "homepage": "Pagrindinis puslapis", + "homepage_description": "Pasirinkite puslapį kaip savo pagrindinį, arba pasirinkite \"Joks\" norėdami naudoti standartinį pagrindinį puslapį.", + "custom_route": "Pagrindinio puslapio vieta", "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", "sso.not-associated": "Click here to associate with", "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.banned-permanently": "Banned permanently" + "info.no-flags": "Nerasta pažymėtų pranešimų", + "info.ban-history": "Blokavimų istorija", + "info.no-ban-history": "Šis narys nebuvo užblokuotas.", + "info.banned-until": "Užblokuotas iki %1", + "info.banned-permanently": "Užblokuotas visam laikui", + "info.banned-reason-label": "Priežastis", + "info.banned-no-reason": "Be priežasties" } \ No newline at end of file diff --git a/public/language/lt/users.json b/public/language/lt/users.json index e55086ed4a..ed6ea22471 100644 --- a/public/language/lt/users.json +++ b/public/language/lt/users.json @@ -17,5 +17,5 @@ "unread_topics": "Neperskaitytos temos", "categories": "Kategorijos", "tags": "Žymos", - "no-users-found": "No users found!" + "no-users-found": "Nerasta vartotojų." } \ No newline at end of file diff --git a/public/language/ms/user.json b/public/language/ms/user.json index 4e89cfb478..954536c92f 100644 --- a/public/language/ms/user.json +++ b/public/language/ms/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/nb/user.json b/public/language/nb/user.json index 056afeb82f..1967d82e28 100644 --- a/public/language/nb/user.json +++ b/public/language/nb/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/nl/user.json b/public/language/nl/user.json index 7d56f88b4a..ef08a49eed 100644 --- a/public/language/nl/user.json +++ b/public/language/nl/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recente verban-geschiedenis", "info.no-ban-history": "Deze gebruiker is nooit eerder verbannen", "info.banned-until": "Verbannen tot %1", - "info.banned-permanently": "Voor altijd verbannen" + "info.banned-permanently": "Voor altijd verbannen", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/pl/user.json b/public/language/pl/user.json index 0a359430c6..302f7b6c7e 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Historia ostatnich banów", "info.no-ban-history": "Ten użytkownik nigdy nie był zbanowany", "info.banned-until": "Zbanowany do %1", - "info.banned-permanently": "Zbanowany permanentnie" + "info.banned-permanently": "Zbanowany permanentnie", + "info.banned-reason-label": "Powód", + "info.banned-no-reason": "Nie podano powodu." } \ No newline at end of file diff --git a/public/language/pt_BR/error.json b/public/language/pt_BR/error.json index adec9bbf06..307bc1c6ae 100644 --- a/public/language/pt_BR/error.json +++ b/public/language/pt_BR/error.json @@ -55,8 +55,8 @@ "post-delete-duration-expired-hours-minutes": "Você só pode deletar posts por %1 hora(s) e %2 minutos(s) depois de postar", "post-delete-duration-expired-days": "Você só pode deletar posts por %1 dia(s) depois de postar", "post-delete-duration-expired-days-hours": "Você só pode deletar posts por %1 dia(s) e %2 hora(s) depois de postar", - "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": "Você não pode excluir o seu tópico após ele ter uma resposta", + "cant-delete-topic-has-replies": "Você não pode excluir o seu tópico após ele ter %1 respostas", "content-too-short": "Por favor digite um post maior. Posts precisam conter ao menos %1 caractere(s).", "content-too-long": "Por favor digite um post mais curto. Posts não podem ser maiores que %1 caractere(s)", "title-too-short": "Por favor digite um título maior. Títulos devem conter no mínimo %1 caractere(s)", @@ -124,6 +124,6 @@ "cant-kick-self": "Você não pode kickar a si mesmo do grupo", "no-users-selected": "Nenhuma escolha de usuário(s) foi feita", "invalid-home-page-route": "Rota de página inicial inválida", - "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": "Erro de Sessão", + "invalid-session-text": "Parece que sua sessão de login não está mais ativa, ou não combina mais com a do servidor. Por gentileza, recarregue esta página." } \ No newline at end of file diff --git a/public/language/pt_BR/global.json b/public/language/pt_BR/global.json index 71543c8896..3b9cb20fea 100644 --- a/public/language/pt_BR/global.json +++ b/public/language/pt_BR/global.json @@ -7,10 +7,10 @@ "403.login": "Talvez você deveria tentar fazer login?", "404.title": "Não Encontrado", "404.message": "Parece que você chegou à uma página que não existe. Voltar para a página inicial.", - "500.title": "Internal Error.", + "500.title": "Erro Interno.", "500.message": "Oops! Parece que algo deu errado!", - "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.title": "Solicitação Inválida.", + "400.message": "Parece que esse link contém informação inválida, por favor o verifique novamente. Senão, retorne para a página inicial.", "register": "Cadastrar", "login": "Login", "please_log_in": "Por Favor Efetue o Login", @@ -95,6 +95,6 @@ "upload_file": "Fazer upload de arquivo", "upload": "Upload", "allowed-file-types": "Os tipos de arquivo permitidos são %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." + "unsaved-changes": "Você tem alterações não salvas. Tem certeza que você deseja sair da página?", + "reconnecting-message": "Parece que sua conexão com %1 caiu, por favor aguarde enquanto tentamos reconectar." } \ No newline at end of file diff --git a/public/language/pt_BR/user.json b/public/language/pt_BR/user.json index 1deb677bd6..cc9fd2bd09 100644 --- a/public/language/pt_BR/user.json +++ b/public/language/pt_BR/user.json @@ -89,10 +89,10 @@ "topics_per_page": "Tópicos por Página", "posts_per_page": "Posts por Página", "notification_sounds": "Tocar um som quando você receber uma notificação.", - "notifications_and_sounds": "Notifications & Sounds", - "incoming-message-sound": "Incoming message sound", - "outgoing-message-sound": "Outgoing message sound", - "notification-sound": "Notification sound", + "notifications_and_sounds": "Notificações & Sons", + "incoming-message-sound": "Som de recebimento de mensagem", + "outgoing-message-sound": "Som de envio de mensagem", + "notification-sound": "Som de notificação", "browsing": "Configurações de Navegação", "open_links_in_new_tab": "Abrir links externos em nova aba", "enable_topic_searching": "Habilitar Pesquisa dentro de Tópico", @@ -118,5 +118,7 @@ "info.ban-history": "Histórico de Banimentos Recentes", "info.no-ban-history": "Este usuário nunca foi banido", "info.banned-until": "Banido até %1", - "info.banned-permanently": "Banido permanentemente" + "info.banned-permanently": "Banido permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Sem motivo escolhido." } \ No newline at end of file diff --git a/public/language/ro/user.json b/public/language/ro/user.json index 31cd396f7f..c2d7d38fa5 100644 --- a/public/language/ro/user.json +++ b/public/language/ro/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/ru/user.json b/public/language/ru/user.json index 33c31e3b43..56516c66cf 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/rw/user.json b/public/language/rw/user.json index a598704593..ced7a21ee2 100644 --- a/public/language/rw/user.json +++ b/public/language/rw/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/sc/user.json b/public/language/sc/user.json index acdd61fe69..11b7d69f80 100644 --- a/public/language/sc/user.json +++ b/public/language/sc/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/sk/user.json b/public/language/sk/user.json index 017be26335..aab0649093 100644 --- a/public/language/sk/user.json +++ b/public/language/sk/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/sl/user.json b/public/language/sl/user.json index 9a778e442c..02d9435768 100644 --- a/public/language/sl/user.json +++ b/public/language/sl/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/sr/user.json b/public/language/sr/user.json index 1016a9feb4..ac03bc3216 100644 --- a/public/language/sr/user.json +++ b/public/language/sr/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/sv/user.json b/public/language/sv/user.json index ba150f3294..5dcdf8a80a 100644 --- a/public/language/sv/user.json +++ b/public/language/sv/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/th/user.json b/public/language/th/user.json index a59c28d7b5..d489070ae9 100644 --- a/public/language/th/user.json +++ b/public/language/th/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/tr/error.json b/public/language/tr/error.json index b3f6216519..f90a44f198 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -124,6 +124,6 @@ "cant-kick-self": "Kendinizi gruptan atamazsınız.", "no-users-selected": "Seçili kullanıcı(s) bulunamadı", "invalid-home-page-route": "Geçersiz anasayfa yolu", - "invalid-session": "Session Mismatch", + "invalid-session": "Oturum Uyuşmazlığı", "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/tr/user.json b/public/language/tr/user.json index 22d9fdaa00..338b0dddf7 100644 --- a/public/language/tr/user.json +++ b/public/language/tr/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Yasaklama Olayları", "info.no-ban-history": "Bu kullanıcı hiç yasaklanmadı", "info.banned-until": "Yasaklama süresi %1", - "info.banned-permanently": "Kalıcı yasakla" + "info.banned-permanently": "Kalıcı yasakla", + "info.banned-reason-label": "Gerekçe", + "info.banned-no-reason": "Gerekçe belirtilmedi." } \ No newline at end of file diff --git a/public/language/vi/user.json b/public/language/vi/user.json index 3c746b87ca..8a00a21653 100644 --- a/public/language/vi/user.json +++ b/public/language/vi/user.json @@ -118,5 +118,7 @@ "info.ban-history": "Recent Ban History", "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", - "info.banned-permanently": "Banned permanently" + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/zh_CN/user.json b/public/language/zh_CN/user.json index 766b80c570..c5fee0f7e2 100644 --- a/public/language/zh_CN/user.json +++ b/public/language/zh_CN/user.json @@ -118,5 +118,7 @@ "info.ban-history": "最近封禁历史", "info.no-ban-history": "该用户从未被封禁", "info.banned-until": "封禁到 %1", - "info.banned-permanently": "永久封禁" + "info.banned-permanently": "永久封禁", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given." } \ No newline at end of file diff --git a/public/language/zh_TW/error.json b/public/language/zh_TW/error.json index 954492cdb6..6c45cf3ff3 100644 --- a/public/language/zh_TW/error.json +++ b/public/language/zh_TW/error.json @@ -12,7 +12,7 @@ "invalid-title": "無效的主題!", "invalid-user-data": "無效的使用者資料", "invalid-password": "無效的密碼", - "invalid-username-or-password": "請指定用戶名和密碼", + "invalid-username-or-password": "請指定帳號和密碼", "invalid-search-term": "無效的搜索字詞", "csrf-invalid": "我們無法讓你登入,似乎是因為連線階段已到期。請再重試一次。", "invalid-pagination-value": "無效的分頁數值, 必需是至少 %1 與最多 %2", @@ -20,7 +20,7 @@ "email-taken": "該信箱已被使用", "email-not-confirmed": "你的電子郵件尚未確認,請點擊此處確認你的電子郵件。", "email-not-confirmed-chat": "你需要先確認電子郵件後才能進行聊天,請點擊這裡來確認你的電子郵件。", - "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", + "email-not-confirmed-email-sent": "你的電子郵件地址還沒有確認,請確認一下你的收信箱是不是有確認信。", "no-email-to-confirm": "討論區要求電子郵件確認,請點擊這裡輸入一個電子郵件。", "email-confirm-failed": "我們無法確認你的Email,請之後再重試。", "confirm-email-already-sent": "確認電子郵件已經寄送,請等待 %1 分鐘才能再寄送另一封。", @@ -43,18 +43,18 @@ "topic-locked": "該主題已被鎖定", "post-edit-duration-expired": "在張貼 %1 秒後,你才能編輯張貼文章", "post-edit-duration-expired-minutes": "在張貼 %1 秒後,你才能編輯張貼文章", - "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", - "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", - "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", - "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", - "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", - "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", - "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", - "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", - "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", - "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", - "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", - "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "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 字元。", @@ -74,7 +74,7 @@ "already-unfavourited": "你已經將這篇張貼移除書籤", "cant-ban-other-admins": "你無法封鎖其他的管理員!", "cant-remove-last-admin": "你是唯一的管理員。在你移除自己為管理員前,需要新增另一個使用者為管理員。", - "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "cant-delete-admin": "在要刪除這個帳戶前,請先移除這個帳戶的管理員權限", "invalid-image-type": "無效的圖像類型。允許的類型:%1", "invalid-image-extension": "無效的圖像擴充元件", "invalid-file-type": "無效的檔案類型。允許的類型:%1", @@ -98,7 +98,7 @@ "signature-too-long": "抱歉,你的簽名長度不能超過 %1 字元。", "about-me-too-long": "抱歉,關於我長度不能超過 %1 字元。", "cant-chat-with-yourself": "你不能與自己聊天!", - "chat-restricted": "此用戶已限制了他的聊天功能。你要在他關注你之後,才能跟他聊天", + "chat-restricted": "此使用者已限制了他的聊天功能。你要在他(她)關注你之後,才能跟他聊天", "chat-disabled": "聊天系統被禁止", "too-many-messages": "你已經送出過多的訊息,請稍等一下。", "invalid-chat-message": "無效的聊天訊息", @@ -125,5 +125,5 @@ "no-users-selected": "沒有選定使用者", "invalid-home-page-route": "無效的首頁路由", "invalid-session": "會話階段錯誤", - "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-text": "看起來你的登入會話階段已經無效,或是不符合於伺服器。請重新整理這個頁面。" } \ No newline at end of file diff --git a/public/language/zh_TW/groups.json b/public/language/zh_TW/groups.json index 511331e301..fc2a300e47 100644 --- a/public/language/zh_TW/groups.json +++ b/public/language/zh_TW/groups.json @@ -11,7 +11,7 @@ "pending.none": "目前沒有等待中的會員", "invited.none": "目前沒有邀請的會員", "invited.uninvite": "撤銷邀請", - "invited.search": "搜尋要邀請加入這個群組的用戶", + "invited.search": "搜尋要邀請加入這個群組的使用者", "invited.notification_title": "你已被邀請加入%1", "request.notification_title": "群組會員要求,來自%1", "request.notification_text": "%1已經要求成為%2群組的會員", @@ -39,7 +39,7 @@ "details.userTitleEnabled": "顯示徽章", "details.private_help": "如果開啟,加入群組需要經過群組擁有者批準", "details.hidden": "隱藏", - "details.hidden_help": "如果開啟的話,群組將不會在群組列表中被看到,而且用戶將需要手動邀請", + "details.hidden_help": "如果開啟的話,群組將不會在群組列表中被看到,而且使用者將需要手動邀請", "details.delete_group": "刪除群組", "details.private_system_help": "私有群組在系統層級被禁用,這個選項沒有任何作用", "event.updated": "群組詳細訊息已被更新", diff --git a/public/language/zh_TW/modules.json b/public/language/zh_TW/modules.json index 61d9b00cea..1e688f79c3 100644 --- a/public/language/zh_TW/modules.json +++ b/public/language/zh_TW/modules.json @@ -8,7 +8,7 @@ "chat.see_all": "顯示全部聊天", "chat.mark_all_read": "所有訊息標為已讀", "chat.no-messages": "請選擇收件人來查看聊天記錄", - "chat.no-users-in-room": "沒有用戶在聊天室中", + "chat.no-users-in-room": "沒有使用者在聊天室中", "chat.recent-chats": "最近的聊天記錄", "chat.contacts": "通訊錄", "chat.message-history": "消息記錄", @@ -19,7 +19,7 @@ "chat.three_months": "3個月", "chat.delete_message_confirm": "你確定要刪除這個訊息?", "chat.roomname": "聊天室 %1", - "chat.add-users-to-room": "將用戶加入聊天室中", + "chat.add-users-to-room": "將使用者加入聊天室中", "composer.compose": "撰寫", "composer.show_preview": "顯示預覽", "composer.hide_preview": "隱藏預覽", diff --git a/public/language/zh_TW/pages.json b/public/language/zh_TW/pages.json index a2b1e523ac..caa4a99d18 100644 --- a/public/language/zh_TW/pages.json +++ b/public/language/zh_TW/pages.json @@ -7,13 +7,13 @@ "popular-alltime": "所有時間受歡迎的主題", "recent": "近期的主題", "flagged-posts": "標記的張貼", - "users/online": "線上用戶", - "users/latest": "最近用戶", + "users/online": "線上使用者", + "users/latest": "最近使用者", "users/sort-posts": "最多張貼的使用者", "users/sort-reputation": "最多聲譽的使用者", - "users/banned": "已封鎖用戶", + "users/banned": "已封鎖使用者", "users/most-flags": "最多標註的使用者", - "users/search": "用戶搜尋", + "users/search": "使用者搜尋", "notifications": "新訊息通知", "tags": "標籤", "tag": "有\"%1\"標籤的主題", @@ -36,7 +36,7 @@ "account/topics": "由 %1 建立的主題", "account/groups": "%1 的群組", "account/favourites": "%1 所加入書籤的張貼", - "account/settings": "用戶設定", + "account/settings": "使用者設定", "account/watched": "%1 所觀看的主題", "account/upvoted": "%1 所正向投票的張貼", "account/downvoted": "%1 所負向投票的張貼", diff --git a/public/language/zh_TW/register.json b/public/language/zh_TW/register.json index 8ab8daca33..9369d59865 100644 --- a/public/language/zh_TW/register.json +++ b/public/language/zh_TW/register.json @@ -1,13 +1,13 @@ { "register": "註冊", "cancel_registration": "取消註冊", - "help.email": "默認情況下,你的郵箱不會公開。", + "help.email": "在預設情況下,你的電子郵件地址不會被公開。", "help.username_restrictions": "獨立的帳號由 %1 到 %2 個字元組成。其他人可以通過 @帳號 提及你。", "help.minimum_password_length": "密碼必須至少包含 %1 個字元。", "email_address": "Email", - "email_address_placeholder": "輸入郵箱地址", - "username": "用戶名", - "username_placeholder": "輸入用戶名", + "email_address_placeholder": "輸入電子郵件地址", + "username": "帳號", + "username_placeholder": "輸入帳號", "password": "密碼", "password_placeholder": "輸入密碼", "confirm_password": "確認密碼", diff --git a/public/language/zh_TW/topic.json b/public/language/zh_TW/topic.json index de06ca59c4..f6a2fe8c46 100644 --- a/public/language/zh_TW/topic.json +++ b/public/language/zh_TW/topic.json @@ -31,7 +31,7 @@ "bookmark_instructions": "點擊這裡返回到這個討論串的最後一篇張貼文", "flag_title": "檢舉這篇文章, 交給仲裁者來審閱.", "flag_success": "這文章已經被檢舉要求仲裁.", - "deleted_message": "此主題已被刪除。只有具有主題管理權限的用戶才能看到它。", + "deleted_message": "此主題已被刪除。只有具有主題管理權限的使用者才能看到它。", "following_topic.message": "有人貼文回覆主題時, 你將會收到新通知.", "not_following_topic.message": "你將會看到這個主題在未讀主題列表中出現,但你將不會在其他人張貼到這個主題時接收到通知。", "ignoring_topic.message": "你將不會再未讀主題列表中看到這個主題。當你被提及或你的張貼被正向投票時,你會被通知。", @@ -102,8 +102,8 @@ "composer.thumb_file_label": "或上傳檔案", "composer.thumb_remove": "清除所有欄目", "composer.drag_and_drop_images": "拖曳影像到此", - "more_users_and_guests": "%1 個用戶和 %2個訪客", - "more_users": "%1 個用戶", + "more_users_and_guests": "%1 個使用者和 %2個訪客", + "more_users": "%1 個使用者", "more_guests": "%1 個訪客", "users_and_others": "%1 和另外 %2 個人", "sort_by": "排序方式", diff --git a/public/language/zh_TW/user.json b/public/language/zh_TW/user.json index 4832a248a5..578b39056a 100644 --- a/public/language/zh_TW/user.json +++ b/public/language/zh_TW/user.json @@ -11,7 +11,7 @@ "ban_account_confirm": "你確定要禁用這個使用者?", "unban_account": "取消禁用帳號", "delete_account": "刪除帳戶", - "delete_account_confirm": "你確定要刪除自己的帳戶?
此操作不能復原,你將無法恢復任何數據

輸入你的帳號,用來確認希望刪除這個帳戶。", + "delete_account_confirm": "你確定要刪除自己的帳戶?
此操作不能復原,你將無法恢復任何資料

輸入你的帳號,來確認你希望刪除這個帳戶。", "delete_this_account_confirm": "你確定要刪除這個帳戶?
此操作是不能還原的,你將無法回復任何資料
", "account-deleted": "帳號已刪除", "fullname": "全名", @@ -63,9 +63,9 @@ "remove_uploaded_picture": "移除上傳的圖片", "upload_cover_picture": "上傳封面圖片", "settings": "設定", - "show_email": "顯示我的郵箱", + "show_email": "顯示我的電子郵件地址", "show_fullname": "顯示我的全名", - "restrict_chats": "只允許我跟隨的用戶和我聊天", + "restrict_chats": "只允許我跟隨的使用者和我聊天", "digest_label": "訂閱摘要", "digest_description": "根據你所設的時間排程,用電子郵件訂閱這個討論區 (新的通知與主題)。", "digest_off": "關閉", @@ -75,15 +75,15 @@ "send_chat_notifications": "如果有新的聊天消息而我不在線,發送郵件給我", "send_post_notifications": "當我訂閱的主題有新回覆時寄送Email給我", "settings-require-reload": "有些設定的更動是需要重新整理。點擊這裡來重新整理頁面。", - "has_no_follower": "該用戶還沒有被任何人關注。", - "follows_no_one": "該用戶還沒有關注過任何人。", + "has_no_follower": "該使用者還沒有被任何人關注。", + "follows_no_one": "該使用者還沒有關注過任何人。", "has_no_posts": "使用者還沒有發表任何張貼", "has_no_topics": "使用者還沒有發表任何主題", "has_no_watched_topics": "使用者還沒有觀看任何主題", "has_no_upvoted_posts": "使用者還沒有對任何主題投正向票", "has_no_downvoted_posts": "使用者還沒有對任何主題投負向票", "has_no_voted_posts": "這個使用者沒有投票的張貼", - "email_hidden": "郵箱被隱藏", + "email_hidden": "電子郵件地址被隱藏", "hidden": "隱藏", "paginate_description": "將主題與張貼用分頁來顯示,取代使用無盡的捲動方式。", "topics_per_page": "每頁的主題數", @@ -118,5 +118,7 @@ "info.ban-history": "最近禁用歷史", "info.no-ban-history": "這個使用者永遠不會被禁用", "info.banned-until": "禁用至 %1", - "info.banned-permanently": "永久禁用" + "info.banned-permanently": "永久禁用", + "info.banned-reason-label": "理由", + "info.banned-no-reason": "沒有給理由" } \ No newline at end of file diff --git a/public/language/zh_TW/users.json b/public/language/zh_TW/users.json index e53abef233..dc08b3056e 100644 --- a/public/language/zh_TW/users.json +++ b/public/language/zh_TW/users.json @@ -1,5 +1,5 @@ { - "latest_users": "最近用戶", + "latest_users": "最近使用者", "top_posters": "發文數最多", "most_reputation": "聲望最高", "most_flags": "最多標註", @@ -11,11 +11,11 @@ "online-only": "線上僅有", "invite": "邀請", "invitation-email-sent": "所有邀請Email已經被寄送到 %1", - "user_list": "用戶列表", + "user_list": "使用者列表", "recent_topics": "最新的主題", "popular_topics": "受歡迎的主題", "unread_topics": "未讀的主題", "categories": "類別", "tags": "標籤", - "no-users-found": "沒有找到用戶!" + "no-users-found": "沒有找到使用者!" } \ No newline at end of file diff --git a/public/src/app.js b/public/src/app.js index 0e357dc50e..b714a441cd 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -488,9 +488,10 @@ app.cacheBuster = null; }); }; - app.newTopic = function (cid) { + app.newTopic = function (cid, tags) { $(window).trigger('action:composer.topic.new', { - cid: cid || ajaxify.data.cid || 0 + cid: cid || ajaxify.data.cid || 0, + tags: tags || (ajaxify.data.tag ? [ajaxify.data.tag] : []) }); }; diff --git a/public/src/client/infinitescroll.js b/public/src/client/infinitescroll.js index 1823b15ca3..aa559c623e 100644 --- a/public/src/client/infinitescroll.js +++ b/public/src/client/infinitescroll.js @@ -52,7 +52,11 @@ define('forum/infinitescroll', function() { return; } loadingMore = true; - socket.emit(method, data, function(err, data) { + + var hookData = {method: method, data: data}; + $(window).trigger('action:infinitescroll.loadmore', hookData); + + socket.emit(hookData.method, hookData.data, function(err, data) { if (err) { loadingMore = false; return app.alertError(err.message); diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 70eaad8d8e..d978abe147 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -1,347 +1,416 @@ -;(function(translator) { - "use strict"; - /* globals RELATIVE_PATH, config, define */ +/* global define, jQuery, config, RELATIVE_PATH, utils, window, Promise, winston */ - var S = null; - var stringDefer = null; - - // export the class if we are in a Node-like system. - if (typeof module === 'object' && module.exports === translator) { - exports = module.exports = translator; - S = require('string'); - } else { - stringDefer = $.Deferred(); - require(['string'], function(stringLib) { - S = stringLib; - stringDefer.resolve(S); - }); +(function (factory) { + 'use strict'; + function loadClient(language, filename) { + return Promise.resolve(jQuery.getJSON(config.relative_path + '/language/' + language + '/' + (filename + '.json?v=' + config['cache-buster']))); } + if (typeof define === 'function' && define.amd) { + // AMD. Register as a named module + define('translator', ['string'], function (string) { + return factory(string, loadClient); + }); + } else if (typeof module === 'object' && module.exports) { + // Node + (function () { + require('promise-polyfill'); + var languages = require('../../../src/languages'); - var languages = {}, - regexes = { - match: /\[\[\w+:[\w\.]+((?!\[\[).)*?\]\]/g, // see tests/translator.js for an explanation re: this monster - split: /[,][\s]*/, - replace: /\]+$/ + function loadServer(language, filename) { + return new Promise(function (resolve, reject) { + languages.get(language, filename + '.json', function (err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + } + + module.exports = factory(require('string'), loadServer); + })(); + } else { + window.translator = factory(window.string, loadClient); + } +})(function (string, load) { + 'use strict'; + var assign = Object.assign || jQuery.extend; + function classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var Translator = function () { + /** + * Construct a new Translator object + * @param {string} language - Language code for this translator instance + */ + function Translator(language) { + classCallCheck(this, Translator); + + if (!language) { + throw new TypeError('Parameter `language` must be a language string. Received ' + language + (language === '' ? '(empty string)' : '')); + } + + this.lang = language; + this.translations = {}; + } + + Translator.prototype.load = load; + + /** + * Parse the translation instructions into the language of the Translator instance + * @param {string} str - Source string + * @returns {Promise} + */ + Translator.prototype.translate = function translate(str) { + // current cursor position + var cursor = 0; + // last break of the input string + var lastBreak = 0; + // length of the input string + var len = str.length; + // array to hold the promises for the translations + // and the strings of untranslated text in between + var toTranslate = []; + + // split a translator string into an array of tokens + // but don't split by commas inside other translator strings + function split(text) { + var len = text.length; + var arr = []; + var i = 0; + var brk = 0; + var level = 0; + + while (i + 2 <= len) { + if (text.slice(i, i + 2) === '[[') { + level += 1; + i += 1; + } else if (text.slice(i, i + 2) === ']]') { + level -= 1; + i += 1; + } else if (level === 0 && text[i] === ',') { + arr.push(text.slice(brk, i).trim()); + i += 1; + brk = i; + } + i += 1; + } + arr.push(text.slice(brk, i + 1).trim()); + return arr; + } + + // the loooop, we'll go to where the cursor + // is equal to the length of the string since + // slice doesn't include the ending index + while (cursor + 2 <= len) { + // if the current position in the string looks + // like the beginning of a translation string + if (str.slice(cursor, cursor + 2) === '[[') { + // split the string from the last break + // to the character before the cursor + // add that to the result array + toTranslate.push(str.slice(lastBreak, cursor)); + // set the cursor position past the beginning + // brackets of the translation string + cursor += 2; + // set the last break to our current + // spot since we just broke the string + lastBreak = cursor; + + // the current level of nesting of the translation strings + var level = 0; + var sliced; + + while (cursor + 2 <= len) { + sliced = str.slice(cursor, cursor + 2); + // if we're at the beginning of another translation string, + // we're nested, so add to our level + if (sliced === '[[') { + level += 1; + cursor += 2; + // if we're at the end of a translation string + } else if (sliced === ']]') { + // if we're at the base level, then this is the end + if (level === 0) { + // so grab the name and args + var result = split(str.slice(lastBreak, cursor)); + var name = result[0]; + var args = result.slice(1); + + // add the translation promise to the array + toTranslate.push(this.translateKey(name, args)); + // skip past the ending brackets + cursor += 2; + // set this as our last break + lastBreak = cursor; + // and we're no longer in a translation string, + // so continue with the main loop + break; + } + // otherwise we lower the level + level -= 1; + // and skip past the ending brackets + cursor += 2; + } else { + // otherwise just move to the next character + cursor += 1; + } + } + } + // move to the next character + cursor += 1; + } + + // add the remaining text after the last translation string + toTranslate.push(str.slice(lastBreak, cursor + 2)); + + // and return a promise for the concatenated translated string + return Promise.all(toTranslate).then(function (translated) { + return translated.join(''); + }); }; - translator.addTranslation = function(language, filename, translations) { - languages[language] = languages[language] || {}; - languages[language].loaded = languages[language].loaded || {}; - languages[language].loading = languages[language].loading || {}; + /** + * Translates a specific key and array of arguments + * @param {string} name - Translation key (ex. 'global:home') + * @param {string[]} args - Arguments for `%1`, `%2`, etc + * @returns {Promise} + */ + Translator.prototype.translateKey = function translateKey(name, args) { + var self = this; - if (languages[language].loaded[filename]) { - for (var t in translations) { - if (translations.hasOwnProperty(t)) { - languages[language].loaded[filename][t] = translations[t]; + var result = name.split(':', 2); + var namespace = result[0]; + var key = result[1]; + + var translation = this.getTranslation(namespace, key); + var argsToTranslate = args.map(function (arg) { + return string(arg).collapseWhitespace().decodeHTMLEntities().escapeHTML().s; + }).map(function (arg) { + return self.translate(arg); + }); + + // so we can await all promises at once + argsToTranslate.unshift(translation); + + return Promise.all(argsToTranslate).then(function (result) { + var translated = result[0]; + var translatedArgs = result.slice(1); + + if (!translated) { + return key; } - } - } else { - languages[language].loaded[filename] = translations; - } - }; - - translator.getTranslations = function(language, filename, callback) { - if (languages[language] && languages[language].loaded[filename]) { - callback(languages[language].loaded[filename]); - } else { - translator.load(language, filename, function() { - callback(languages[language].loaded[filename]); + var out = translated; + translatedArgs.forEach(function (arg, i) { + out = out.replace(new RegExp('%' + (i + 1), 'g'), arg); + }); + return out; }); - } - }; + }; - translator.escape = function(text) { - return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text; - }; - - translator.unescape = function(text) { - return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text; - }; - - translator.getLanguage = function() { - return config.defaultLang; - }; - - translator.prepareDOM = function() { - // Load the appropriate timeago locale file, and correct NodeBB language codes to timeago codes, if necessary - var languageCode; - 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; - } - - $.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function() { - $('.timeago').timeago(); - translator.timeagoShort = $.extend({}, jQuery.timeago.settings.strings); - - // Retrieve the shorthand timeago values as well - $.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function() { - // Switch back to long-form - translator.toggleTimeagoShorthand(); - }); - }); - - // Add directional code if necessary - translator.translate('[[language:dir]]', function(value) { - if (value) { - $('html').css('direction', value).attr('data-dir', value); + /** + * Load translation file (or use a cached version), and optionally return the translation of a certain key + * @param {string} namespace - The file name of the translation namespace + * @param {string} [key] - The key of the specific translation to getJSON + * @returns {Promise} + */ + Translator.prototype.getTranslation = function getTranslation(namespace, key) { + var translation; + if (!namespace) { + winston.warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : '')); + translation = Promise.resolve({}); + } else if (this.translations[namespace]) { + translation = this.translations[namespace]; + } else { + translation = this.load(this.lang, namespace); + this.translations[namespace] = translation; } - }); - }; - translator.toggleTimeagoShorthand = function() { - var tmp = $.extend({}, jQuery.timeago.settings.strings); - jQuery.timeago.settings.strings = $.extend({}, translator.timeagoShort); - translator.timeagoShort = $.extend({}, tmp); - }; + if (key) { + return translation.then(function (x) { + return x[key]; + }); + } + return translation; + }; - translator.translate = function (text, language, callback) { - if (typeof language === 'function') { - callback = language; - if ('undefined' !== typeof window && config) { - language = utils.params().lang || config.userLang || 'en_GB'; + /** + * Get the language of the current environment, falling back to defaults + * @returns {string} + */ + Translator.getLanguage = function getLanguage() { + var lang; + + if (typeof window === 'object' && window.config && window.utils) { + lang = utils.params().lang || config.userLang || config.defaultLang || 'en_GB'; } else { var meta = require('../../../src/meta'); - language = meta.config.defaultLang || 'en_GB'; - } - } - - if (!text) { - return callback(text); - } - - var keys = text.match(regexes.match); - - if (!keys) { - return callback(text); - } - - translateKeys(keys, text, language, function(translated) { - keys = translated.match(regexes.match); - if (!keys) { - callback(translated); - } else { - translateKeys(keys, translated, language, callback); - } - }); - }; - - function translateKeys(keys, text, language, callback) { - - var count = keys.length; - if (!count) { - return callback(text); - } - - if (S === null) { // browser environment and S not yet initialized - // we need to wait for async require call - stringDefer.then(function () { translateKeys(keys, text, language, callback); }); - return; - } - - var data = {text: text}; - keys.forEach(function(key) { - translateKey(key, data, language, function(translated) { - --count; - if (count <= 0) { - callback(translated.text); - } - }); - }); - } - - function translateKey(key, data, language, callback) { - key = '' + key; - var variables = key.split(regexes.split); - - var parsedKey = key.replace('[[', '').replace(']]', '').split(':'); - parsedKey = [parsedKey[0]].concat(parsedKey.slice(1).join(':')); - if (!(parsedKey[0] && parsedKey[1])) { - return callback(data); - } - - var languageFile = parsedKey[0]; - parsedKey = ('' + parsedKey[1]).split(',')[0]; - - translator.load(language, languageFile, function(languageData) { - data.text = insertLanguage(data.text, key, languageData[parsedKey], variables); - callback(data); - }); - } - - function insertLanguage(text, key, value, variables) { - if (value) { - variables.forEach(function(variable, index) { - if (index > 0) { - variable = S(variable).chompRight(']]').collapseWhitespace().decodeHTMLEntities().escapeHTML().s; - value = value.replace('%' + index, function() { return variable; }); - } - }); - - text = text.replace(key, function() { return value; }); - } else { - var string = key.split(':'); - text = text.replace(key, string[string.length-1].replace(regexes.replace, '')); - } - - return text; - } - - translator.compile = function() { - var args = Array.prototype.slice.call(arguments, 0); - - return '[[' + args.join(', ') + ']]'; - }; - - translator.load = function (language, filename, callback) { - if (isLanguageFileLoaded(language, filename)) { - if (callback) { - callback(languages[language].loaded[filename]); - } - } else if (isLanguageFileLoading(language, filename)) { - if (callback) { - addLanguageFileCallback(language, filename, callback); - } - } else { - - languages[language] = languages[language] || {loading: {}, loaded: {}, callbacks: []}; - - languages[language].loading[filename] = true; - - load(language, filename, function(translations) { - - languages[language].loaded[filename] = translations; - - if (callback) { - callback(translations); - } - - while (languages[language].callbacks && languages[language].callbacks[filename] && languages[language].callbacks[filename].length) { - languages[language].callbacks[filename].pop()(translations); - } - - languages[language].loading[filename] = false; - }); - } - }; - - function isLanguageFileLoaded(language, filename) { - var languageObj = languages[language]; - return languageObj && languageObj.loaded && languageObj.loaded[filename] && !languageObj.loading[filename]; - } - - function isLanguageFileLoading(language, filename) { - return languages[language] && languages[language].loading && languages[language].loading[filename]; - } - - function addLanguageFileCallback(language, filename, callback) { - languages[language].callbacks = languages[language].callbacks || {}; - - languages[language].callbacks[filename] = languages[language].callbacks[filename] || []; - languages[language].callbacks[filename].push(callback); - } - - function load(language, filename, callback) { - if ('undefined' !== typeof window) { - loadClient(language, filename, callback); - } else { - loadServer(language, filename, callback); - } - } - - function loadClient(language, filename, callback) { - $.getJSON(config.relative_path + '/language/' + language + '/' + filename + '.json?v=' + config['cache-buster'], callback); - } - - function loadServer(language, filename, callback) { - var fs = require('fs'), - path = require('path'), - winston = require('winston'), - _ = require('underscore'), - file = require('../../../src/file'), - plugins = require('../../../src/plugins'), - meta = require('../../../src/meta'); - - var hash = language + '/' + filename + '.json'; - - language = language || meta.config.defaultLang || 'en_GB'; - - if (!file.existsSync(path.join(__dirname, '../../language', language))) { - winston.warn('[translator] Language \'' + meta.config.defaultLang + '\' not found. Defaulting to \'en_GB\''); - language = 'en_GB'; - } - - fs.readFile(path.join(__dirname, '../../language', hash), function(err, data) { - var onData = function(data) { - try { - data = JSON.parse(data.toString()); - - if (plugins.customLanguages.hasOwnProperty(hash)) { - _.extendOwn(data, plugins.customLanguages[hash]); - } - } catch (e) { - winston.error('Could not parse `' + filename + '.json`, syntax error? Skipping...'); - data = {}; - } - - callback(data); - }; - - if (err && err.code === 'ENOENT') { - data = '{}'; - } else if (err) { - winston.error('[translator] Error while loading language file: ' + err.message); - return callback({}); + lang = meta.config.defaultLang || 'en_GB'; } - onData(data); - }); - } - - // Use the define() function if we're in AMD land - if (typeof define === 'function' && define.amd) { - define('translator', translator); - - var _translator = translator; - - // Expose a global `translator` object for backwards compatibility - window.translator = { - translate: function() { - if (typeof console !== 'undefined' && console.warn) { - console.warn('[translator] Global invocation of the translator is now deprecated, please `require` the module instead.'); - } - _translator.translate.apply(_translator, arguments); - } + return lang; }; - } -})( - typeof exports === 'object' ? exports : - typeof define === 'function' && define.amd ? {} : - translator = {} -); + + /** + * Create and cache a new Translator instance, or return a cached one + * @param {string} [language] - ('en_GB') Language string + * @returns {Translator} + */ + Translator.create = function create(language) { + if (!language) { + language = Translator.getLanguage(); + } + + Translator.cache[language] = Translator.cache[language] || new Translator(language); + + return Translator.cache[language]; + }; + + Translator.cache = {}; + + return Translator; + }(); + + var adaptor = { + /** + * The Translator class + */ + Translator: Translator, + + /** + * Legacy translator function for backwards compatibility + */ + translate: function translate(text, language, callback) { + // console.warn('[translator] `translator.translate(text, [lang, ]callback)` is deprecated. ' + + // 'Use the `translator.Translator` class instead.'); + + var cb = callback; + var lang = language; + if (typeof language === 'function') { + cb = language; + lang = null; + } + + Translator.create(lang).translate(text).then(function (output) { + return cb(output); + }).catch(function (err) { + console.error('Translation failed: ' + err.message); + }); + }, + + /** + * Construct a translator pattern + * @param {string} name - Translation name + * @param {string[]} args - Optional arguments for the pattern + */ + compile: function compile() { + var args = Array.prototype.slice.call(arguments, 0); + + return '[[' + args.join(', ') + ']]'; + }, + + /** + * Escape translation patterns from text + */ + escape: function escape(text) { + return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text; + }, + + /** + * Unescape translation patterns from text + */ + unescape: function unescape(text) { + return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text; + }, + + /** + * Add translations to the cache + */ + addTranslation: function addTranslation(language, filename, translation) { + Translator.create(language).getTranslation(filename).then(function (translations) { + assign(translations, translation); + }); + }, + + /** + * Get the translations object + */ + getTranslations: function getTranslations(language, filename, callback) { + callback = callback || function () {}; + Translator.create(language).getTranslation(filename).then(callback); + }, + + /** + * Alias of getTranslations + */ + load: function load(language, filename, callback) { + adaptor.getTranslations(language, filename, callback); + }, + + /** + * Get the language of the current environment, falling back to defaults + */ + getLanguage: Translator.getLanguage, + + toggleTimeagoShorthand: function toggleTimeagoShorthand() { + var tmp = assign({}, jQuery.timeago.settings.strings); + jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort); + adaptor.timeagoShort = assign({}, tmp); + }, + prepareDOM: function prepareDOM() { + // Load the appropriate timeago locale file, + // and correct NodeBB language codes to timeago codes, if necessary + 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; + } + + jQuery.getScript(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 () { + // Switch back to long-form + adaptor.toggleTimeagoShorthand(); + }); + }); + + // Add directional code if necessary + adaptor.translate('[[language:dir]]', function (value) { + if (value) { + jQuery('html').css('direction', value).attr('data-dir', value); + } + }); + } + }; + + return adaptor; +}); diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index 33e6d4e79b..fbb727d9c9 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -474,48 +474,82 @@ module.exports = function(db, module) { }); }; - module.getSortedSetUnion = function(sets, start, stop, callback) { - getSortedSetUnion(sets, 1, start, stop, callback); + module.sortedSetUnionCard = function(keys, callback) { + if (!Array.isArray(keys) || !keys.length) { + return callback(null, 0); + } + + var pipeline = [ + { $match: { _key: {$in: keys} } }, + { $group: { _id: {value: '$value' } } }, + { $group: { _id: null, count: { $sum: 1 } } } + ]; + + var project = { _id: 0, count: '$count' }; + pipeline.push({ $project: project }); + + db.collection('objects').aggregate(pipeline, function(err, data) { + callback(err, Array.isArray(data) && data.length ? data[0].count : 0); + }); }; - module.getSortedSetRevUnion = function(sets, start, stop, callback) { - getSortedSetUnion(sets, -1, start, stop, callback); + module.getSortedSetUnion = function(params, callback) { + params.sort = 1; + getSortedSetUnion(params, callback); }; + module.getSortedSetRevUnion = function(params, callback) { + params.sort = -1; + getSortedSetUnion(params, callback); + }; - function getSortedSetUnion(sets, sort, start, stop, callback) { - if (!Array.isArray(sets) || !sets.length) { + function getSortedSetUnion(params, callback) { + if (!Array.isArray(params.sets) || !params.sets.length) { return callback(); } - var limit = stop - start + 1; + var limit = params.stop - params.start + 1; if (limit <= 0) { limit = 0; } + var aggregate = {}; + if (params.aggregate) { + aggregate['$' + params.aggregate.toLowerCase()] = '$score'; + } else { + aggregate.$sum = '$score'; + } + var pipeline = [ - { $match: { _key: {$in: sets}} }, - { $group: { _id: {value: '$value'}, totalScore: {$sum : "$score"}} }, - { $sort: { totalScore: sort} } + { $match: { _key: {$in: params.sets}} }, + { $group: { _id: {value: '$value'}, totalScore: aggregate} }, + { $sort: { totalScore: params.sort} } ]; - if (start) { - pipeline.push({ $skip: start }); + if (params.start) { + pipeline.push({ $skip: params.start }); } if (limit > 0) { pipeline.push({ $limit: limit }); } - pipeline.push({ $project: { _id: 0, value: '$_id.value' }}); + var project = { _id: 0, value: '$_id.value' }; + if (params.withScores) { + project.score = '$totalScore'; + } + pipeline.push({ $project: project }); db.collection('objects').aggregate(pipeline, function(err, data) { if (err || !data) { return callback(err); } - data = data.map(function(item) { - return item.value; - }); + if (!params.withScores) { + data = data.map(function(item) { + return item.value; + }); + } + callback(null, data); }); } @@ -613,7 +647,7 @@ module.exports = function(db, module) { getSortedSetRevIntersect(params, callback); }; - function getSortedSetRevIntersect (params, callback) { + function getSortedSetRevIntersect(params, callback) { var sets = params.sets; var start = params.hasOwnProperty('start') ? params.start : 0; var stop = params.hasOwnProperty('stop') ? params.stop : -1; diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index eaa3b2430f..f39189cf0f 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -232,32 +232,51 @@ module.exports = function(redisClient, module) { multi.exec(callback); }; - module.getSortedSetUnion = function(sets, start, stop, callback) { - sortedSetUnion('zrange', sets, start, stop, false, callback); - }; - - module.getSortedSetRevUnion = function(sets, start, stop, callback) { - sortedSetUnion('zrevrange', sets, start, stop, false, callback); - }; - - function sortedSetUnion(method, sets, start, stop, withScores, callback) { + module.sortedSetUnionCard = function(keys, callback) { var tempSetName = 'temp_' + Date.now(); - var params = [tempSetName, start, stop]; - if (withScores) { - params.push('WITHSCORES'); - } - var multi = redisClient.multi(); - multi.zunionstore([tempSetName, sets.length].concat(sets)); - multi[method](params); + multi.zunionstore([tempSetName, keys.length].concat(keys)); + multi.zcard(tempSetName); multi.del(tempSetName); multi.exec(function(err, results) { if (err) { return callback(err); } - if (!withScores) { + + callback(null, Array.isArray(results) && results.length ? results[1] : 0); + }); + }; + + module.getSortedSetUnion = function(params, callback) { + params.method = 'zrange'; + sortedSetUnion(params, callback); + }; + + module.getSortedSetRevUnion = function(params, callback) { + params.method = 'zrevrange'; + sortedSetUnion(params, callback); + }; + + function sortedSetUnion(params, callback) { + + var tempSetName = 'temp_' + Date.now(); + + var rangeParams = [tempSetName, params.start, params.stop]; + if (params.withScores) { + params.push('WITHSCORES'); + } + + var multi = redisClient.multi(); + multi.zunionstore([tempSetName, params.sets.length].concat(params.sets)); + multi[params.method](rangeParams); + multi.del(tempSetName); + multi.exec(function(err, results) { + if (err) { + return callback(err); + } + if (!params.withScores) { return callback(null, results ? results[1] : null); } results = results[1] || []; diff --git a/src/install.js b/src/install.js index 296e30fdc4..5e642d0177 100644 --- a/src/install.js +++ b/src/install.js @@ -179,12 +179,10 @@ function completeConfigSetup(err, config, next) { function setupDefaultConfigs(next) { process.stdout.write('Populating database with default configs, if not already set...\n'); - var meta = require('./meta'), - defaults = require(path.join(__dirname, '../', 'install/data/defaults.json')); + var meta = require('./meta'); + var defaults = require(path.join(__dirname, '../', 'install/data/defaults.json')); - async.each(Object.keys(defaults), function (key, next) { - meta.configs.setOnEmpty(key, defaults[key], next); - }, function (err) { + meta.configs.setOnEmpty(defaults, function (err) { if (err) { return next(err); } diff --git a/src/meta/configs.js b/src/meta/configs.js index 61464c232a..723e58083d 100644 --- a/src/meta/configs.js +++ b/src/meta/configs.js @@ -118,14 +118,20 @@ module.exports = function(Meta) { } }); - Meta.configs.setOnEmpty = function (field, value, callback) { - Meta.configs.get(field, function (err, curValue) { + Meta.configs.setOnEmpty = function (values, callback) { + db.getObject('config', function(err, data) { if (err) { return callback(err); } - - if (!curValue) { - Meta.configs.set(field, value, callback); + data = data || {}; + var empty = {}; + Object.keys(values).forEach(function(key) { + if (!data.hasOwnProperty(key)) { + empty[key] = values[key]; + } + }); + if (Object.keys(empty).length) { + db.setObject('config', empty, callback); } else { callback(); } diff --git a/src/meta/js.js b/src/meta/js.js index 3d85c7efb1..e729937559 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -37,7 +37,8 @@ module.exports = function(Meta) { 'public/src/app.js', 'public/src/ajaxify.js', 'public/src/overrides.js', - 'public/src/widgets.js' + 'public/src/widgets.js', + "./node_modules/promise-polyfill/promise.js" ], // files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load @@ -79,7 +80,7 @@ module.exports = function(Meta) { // modules listed below are routed through express (/src/modules) so they can be defined anonymously modules: { "Chart.js": './node_modules/chart.js/dist/Chart.min.js', - "mousetrap.js": './node_modules/mousetrap/mousetrap.js', + "mousetrap.js": './node_modules/mousetrap/mousetrap.min.js', "jqueryui.js": 'public/vendor/jquery/js/jquery-ui.js', "buzz.js": 'public/vendor/buzz/buzz.js' } diff --git a/src/meta/settings.js b/src/meta/settings.js index ed6702a943..4fba3fcb2e 100644 --- a/src/meta/settings.js +++ b/src/meta/settings.js @@ -38,14 +38,21 @@ module.exports = function(Meta) { db.setObjectField('settings:' + hash, field, value, callback); }; - Meta.settings.setOnEmpty = function (hash, field, value, callback) { - Meta.settings.getOne(hash, field, function (err, curValue) { + Meta.settings.setOnEmpty = function (hash, values, callback) { + db.getObject('settings:' + hash, function(err, settings) { if (err) { return callback(err); } + settings = settings || {}; + var empty = {}; + Object.keys(values).forEach(function(key) { + if (!settings.hasOwnProperty(key)) { + empty[key] = values[key]; + } + }); - if (!curValue) { - Meta.settings.setOne(hash, field, value, callback); + if (Object.keys(empty).length) { + db.setObject('settings:' + hash, empty, callback); } else { callback(); } diff --git a/src/middleware/header.js b/src/middleware/header.js index 67afe93ed1..26e9795019 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -120,6 +120,7 @@ module.exports = function(middleware) { results.user.isAdmin = results.isAdmin; results.user.isGlobalMod = results.isGlobalMod; results.user.uid = parseInt(results.user.uid, 10); + results.user.email = String(results.user.email).replace(/\\/g, '\\\\'); results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1; results.user.isEmailConfirmSent = !!results.isEmailConfirmSent; diff --git a/src/plugins.js b/src/plugins.js index fc56d799c6..0d6bb02fb5 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -406,31 +406,4 @@ var middleware; ], callback); }; - Plugins.clearRequireCache = function(next) { - var cached = Object.keys(require.cache); - async.waterfall([ - async.apply(async.map, Plugins.libraryPaths, fs.realpath), - function(paths, next) { - paths = paths.map(function(pluginLib) { - var parent = path.dirname(pluginLib); - return cached.filter(function(libPath) { - return libPath.indexOf(parent) !== -1; - }); - }).reduce(function(prev, cur) { - return prev.concat(cur); - }); - - Plugins.fireHook('filter:plugins.clearRequireCache', {paths: paths}, next); - }, - function(data, next) { - for (var x=0,numPaths=data.paths.length;x 19) { + break; + } } } - matches = matches.slice(0, 20).sort(function(a, b) { + matches = matches.sort(function(a, b) { return a > b; }); - - plugins.fireHook('filter:tags.search', {data: data, matches: matches}, function(err, data) { - callback(err, data ? data.matches : []); - }); + callback(null, matches); }); - }; + } Topics.searchAndLoadTags = function(data, callback) { var searchResult = { diff --git a/src/user/delete.js b/src/user/delete.js index 6c5a92e225..66d351cdf2 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -94,7 +94,10 @@ module.exports = function(User) { 'users:reputation', 'users:banned', 'users:online', - 'users:notvalidated' + 'users:notvalidated', + 'digest:day:uids', + 'digest:week:uids', + 'digest:month:uids' ], uid, next); }, function(next) { diff --git a/src/views/admin/settings/chat.tpl b/src/views/admin/settings/chat.tpl index 9926e25dbe..5f4d0b8315 100644 --- a/src/views/admin/settings/chat.tpl +++ b/src/views/admin/settings/chat.tpl @@ -22,6 +22,12 @@ + + +
+ + +
diff --git a/tests/database/sorted.js b/tests/database/sorted.js index c1066f5ff4..91da2c657c 100644 --- a/tests/database/sorted.js +++ b/tests/database/sorted.js @@ -436,9 +436,19 @@ describe('Sorted Set methods', function() { }); }); + describe('sortedSetUnionCard', function() { + it('should return the number of elements in the union', function(done) { + db.sortedSetUnionCard(['sortedSetTest2', 'sortedSetTest3'], function(err, count) { + assert.ifError(err); + assert.equal(count, 3); + done(); + }); + }); + }); + describe('getSortedSetUnion()', function() { it('should return an array of values from both sorted sets sorted by scores lowest to highest', function(done) { - db.getSortedSetUnion(['sortedSetTest2', 'sortedSetTest3'], 0, -1, function(err, values) { + db.getSortedSetUnion({sets: ['sortedSetTest2', 'sortedSetTest3'], start: 0, stop: -1}, function(err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value1', 'value2', 'value4']); @@ -449,7 +459,7 @@ describe('Sorted Set methods', function() { describe('getSortedSetRevUnion()', function() { it('should return an array of values from both sorted sets sorted by scores highest to lowest', function(done) { - db.getSortedSetRevUnion(['sortedSetTest2', 'sortedSetTest3'], 0, -1, function(err, values) { + db.getSortedSetRevUnion({sets: ['sortedSetTest2', 'sortedSetTest3'], start: 0, stop: -1}, function(err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value4', 'value2', 'value1']); diff --git a/tests/translator-new.js b/tests/translator-new.js new file mode 100644 index 0000000000..22ddf4e9d5 --- /dev/null +++ b/tests/translator-new.js @@ -0,0 +1,155 @@ +'use strict'; +/*global require*/ + +var assert = require('assert'); +var Translator = require('../public/src/modules/translator.js').Translator; + + +describe('new Translator(language)', function(){ + describe('.translate()', function(){ + it('should handle basic translations', function(done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:home]]').then(function(translated) { + assert.strictEqual(translated, 'Home'); + done(); + }); + }); + + it('should handle language keys in regular text', function(done) { + var translator = new Translator('en_GB'); + + translator.translate('Let\'s go [[global:home]]').then(function(translated) { + assert.strictEqual(translated, 'Let\'s go Home'); + done(); + }); + }); + + it('should accept a language parameter and adjust accordingly', function(done) { + var translator = new Translator('de'); + + translator.translate('[[global:home]]').then(function(translated) { + assert.strictEqual(translated, 'Übersicht'); + done(); + }); + }); + + it('should handle language keys in regular text with another language specified', function(done) { + var translator = new Translator('de'); + + translator.translate('[[global:home]] test').then(function(translated) { + assert.strictEqual(translated, 'Übersicht test'); + done(); + }); + }); + + it('should handle language keys with parameters', function(done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, 1, 5]]').then(function(translated) { + assert.strictEqual(translated, '1 out of 5'); + done(); + }); + }); + + it('should handle language keys inside language keys', function(done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:outgoing_link_message, [[global:guest]]]]').then(function(translated) { + assert.strictEqual(translated, 'You are now leaving Guest'); + done(); + }); + }); + + it('should handle language keys inside language keys with multiple parameters', function(done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:user_posted_to, [[global:guest]], My Topic]]').then(function(translated) { + assert.strictEqual(translated, 'Guest has posted a reply to: My Topic'); + done(); + }); + }); + + it('should handle language keys inside language keys with all parameters as language keys', function(done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:user_posted_to, [[global:guest]], [[global:guest]]]]').then(function(translated) { + assert.strictEqual(translated, 'Guest has posted a reply to: Guest'); + done(); + }); + }); + + it('should properly handle parameters that contain square brackets', function(done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, [guest], [[global:home]]]]').then(function(translated) { + assert.strictEqual(translated, '[guest] out of Home'); + done(); + }); + }); + + it('should properly handle parameters that contain parentheses', function(done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, (foobar), [[global:home]]]]').then(function(translated) { + assert.strictEqual(translated, '(foobar) out of Home'); + done(); + }); + }); + + it('should not translate language key parameters with HTML in them', function(done) { + var translator = new Translator('en_GB'); + + var key = '[[global:403.login, test]]'; + translator.translate(key).then(function(translated) { + assert.strictEqual(translated, 'Perhaps you should try logging in?'); + done(); + }); + }); + + it('should properly escape % and ,', function(done) { + var translator = new Translator('en_GB'); + + var title = 'Test 1, 2, 3 % salmon'; + title = title.replace(/%/g, '%').replace(/,/g, ','); + var key = "[[topic:composer.replying_to, " + title + "]]"; + translator.translate(key).then(function(translated) { + assert.strictEqual(translated, 'Replying to Test 1, 2, 3 % salmon'); + done(); + }); + }); + + it('should throw if not passed a language', function(done) { + assert.throws(function () { + new Translator(); + }, /language string/); + done(); + }); + + }); +}); + +describe('Translator.create()', function(){ + describe('.translate()', function(){ + it('should return an instance of Translator', function(done) { + var translator = Translator.create('en_GB'); + + assert(translator instanceof Translator); + done(); + }); + it('should return the same object for the same language', function(done) { + var one = Translator.create('de'); + var two = Translator.create('de'); + + assert.strictEqual(one, two); + done(); + }); + it('should default to defaultLang', function(done) { + var translator = Translator.create(); + + assert.strictEqual(translator.lang, 'en_GB'); + done(); + }); + + }); +}); diff --git a/tests/translator.js b/tests/translator.js index 7546ec1d90..f1ceeaed0d 100644 --- a/tests/translator.js +++ b/tests/translator.js @@ -4,8 +4,12 @@ var assert = require('assert'); var translator = require('../public/src/modules/translator.js'); +var plugins = require('../src/plugins'); +var languages = require('../src/languages'); -describe('Translator', function(){ +languages.init(function(){}); + +describe('translator adaptor', function(){ describe('.translate()', function(){ it('should handle basic translations', function(done) { translator.translate('[[global:home]]', function(translated) { @@ -95,5 +99,18 @@ describe('Translator', function(){ }); }); + it('should properly handle translations added by plugins', function(done) { + plugins.customLanguages = { + 'en_GB/myplugin.json': { + 'foo': 'bar', + 'bar': 'baz, and %1' + } + }; + + translator.translate('[[myplugin:foo]], [[myplugin:bar, quux]]', function(translated) { + assert.strictEqual(translated, 'bar, baz, and quux'); + done(); + }); + }); }); });