mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-06-25 22:10:50 +02:00
Merge commit '6392cd31df3b311fbbfdf2ff09a1a7509ef62139' into weekly
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -47,4 +47,7 @@ pidfile
|
||||
|
||||
## Transifex
|
||||
tx.exe
|
||||
.transifexrc
|
||||
.transifexrc
|
||||
|
||||
##Coverage output
|
||||
coverage
|
||||
22
package.json
22
package.json
@@ -11,7 +11,7 @@
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node loader.js",
|
||||
"test": "mocha ./tests -t 10000"
|
||||
"test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- ./tests -t 10000"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "~1.5.0",
|
||||
@@ -26,6 +26,7 @@
|
||||
"connect-mongo": "~1.1.0",
|
||||
"connect-multiparty": "^2.0.0",
|
||||
"connect-redis": "~3.0.2",
|
||||
"continuation-local-storage": "^3.1.6",
|
||||
"cookie-parser": "^1.3.3",
|
||||
"cron": "^1.0.5",
|
||||
"csurf": "^1.6.1",
|
||||
@@ -46,18 +47,18 @@
|
||||
"morgan": "^1.3.2",
|
||||
"mousetrap": "^1.5.3",
|
||||
"nconf": "~0.8.2",
|
||||
"nodebb-plugin-composer-default": "3.0.27",
|
||||
"nodebb-plugin-composer-default": "3.0.30",
|
||||
"nodebb-plugin-dbsearch": "1.0.1",
|
||||
"nodebb-plugin-emoji-one": "1.1.3",
|
||||
"nodebb-plugin-emoji-extended": "1.1.0",
|
||||
"nodebb-plugin-markdown": "5.1.3",
|
||||
"nodebb-plugin-mentions": "1.0.21",
|
||||
"nodebb-plugin-markdown": "5.1.4",
|
||||
"nodebb-plugin-mentions": "1.0.24",
|
||||
"nodebb-plugin-soundpack-default": "0.1.6",
|
||||
"nodebb-plugin-spam-be-gone": "0.4.6",
|
||||
"nodebb-rewards-essentials": "0.0.8",
|
||||
"nodebb-theme-lavender": "3.0.9",
|
||||
"nodebb-theme-persona": "4.0.128",
|
||||
"nodebb-theme-vanilla": "5.0.68",
|
||||
"nodebb-theme-lavender": "3.0.10",
|
||||
"nodebb-theme-persona": "4.0.132",
|
||||
"nodebb-theme-vanilla": "5.0.71",
|
||||
"nodebb-widget-essentials": "2.0.9",
|
||||
"nodemailer": "2.0.0",
|
||||
"nodemailer-sendmail-transport": "1.0.0",
|
||||
@@ -88,9 +89,10 @@
|
||||
"xregexp": "~3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "~1.13.0",
|
||||
"grunt": "~0.4.5",
|
||||
"grunt-contrib-watch": "^1.0.0"
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"istanbul": "^0.4.2",
|
||||
"mocha": "~1.13.0"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/NodeBB/NodeBB/issues"
|
||||
@@ -115,4 +117,4 @@
|
||||
"url": "https://github.com/barisusakli"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"invalid-data": "بيانات غير صالحة",
|
||||
"invalid-data": "بيانات غير صحيحة",
|
||||
"not-logged-in": "لم تقم بتسجيل الدخول",
|
||||
"account-locked": "تم حظر حسابك مؤقتًا.",
|
||||
"search-requires-login": "البحث في المنتدى يتطلب حساب - الرجاء تسجيل الدخول أو التسجيل",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"invalid-password": "Invalid Password",
|
||||
"invalid-username-or-password": "Please specify both a username and password",
|
||||
"invalid-search-term": "Invalid search term",
|
||||
"csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again",
|
||||
|
||||
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"remember_me": "Remember Me?",
|
||||
"forgot_password": "Forgot Password?",
|
||||
"alternative_logins": "Alternative Logins",
|
||||
"failed_login_attempt": "Failed login attempt, please try again.",
|
||||
"failed_login_attempt": "Login Unsuccessful",
|
||||
"login_successful": "You have successfully logged in!",
|
||||
"dont_have_account": "Don't have an account?"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"no_topics": "<strong>Det finns inga ämnen i denna kategori.</strong><br />Varför skapar inte du ett ämne?",
|
||||
"browsing": "läser",
|
||||
"no_replies": "Ingen har svarat",
|
||||
"no_new_posts": "Inga nya inlägg",
|
||||
"no_new_posts": "Inga nya inlägg.",
|
||||
"share_this_category": "Dela den här kategorin",
|
||||
"watch": "Bevaka",
|
||||
"ignore": "Ignorera",
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"greeting_no_name": "Hej",
|
||||
"greeting_with_name": "Hej %1",
|
||||
"welcome.text1": "Tack för att du registerar dig på %1!",
|
||||
"welcome.text2": "För att slutföra aktiveringen av ditt konto, behöver vi verifiera att du har tillgång till den epostadress du registrerade dig med.",
|
||||
"welcome.text2": "För att slutföra aktiveringen av ditt konto, behöver vi verifiera att du har tillgång till den e-postadress du registrerade dig med.",
|
||||
"welcome.text3": "En administrator har accepterat din registreringsansökan. Du kan logga in med ditt användarnamn och lösenord nu.",
|
||||
"welcome.cta": "Klicka här för att bekräfta din epostadress ",
|
||||
"welcome.cta": "Klicka här för att bekräfta din e-postadress ",
|
||||
"invitation.text1": "%1 har bjudit in dig till %2",
|
||||
"invitation.ctr": "Klicka här för att skapa ditt konto.",
|
||||
"reset.text1": "Vi fick en förfrågan om att återställa ditt lösenord, möjligen för att du har glömt det. Om detta inte är fallet, så kan du bortse från det här epostmeddelandet. ",
|
||||
@@ -15,22 +15,22 @@
|
||||
"reset.cta": "Klicka här för att återställa ditt lösenord",
|
||||
"reset.notify.subject": "Lösenordet ändrat",
|
||||
"reset.notify.text1": "Vi vill uppmärksamma dig på att ditt lösenord ändrades den %1",
|
||||
"reset.notify.text2": "Om du inte godkänt det här så vänligen kontakta en admin snarast. ",
|
||||
"reset.notify.text2": "Om du inte godkänt det här så vänligen kontakta en administratör snarast. ",
|
||||
"digest.notifications": "Du har olästa notiser från %1:",
|
||||
"digest.latest_topics": "Senaste ämnen från %1",
|
||||
"digest.cta": "Klicka här för att besöka %1",
|
||||
"digest.unsub.info": "Det här meddelandet fick du på grund av dina inställningar för prenumeration. ",
|
||||
"digest.no_topics": "Inga aktiva ämnen dom senaste %1",
|
||||
"digest.day": "day",
|
||||
"digest.week": "week",
|
||||
"digest.month": "month",
|
||||
"digest.subject": "Digest for %1",
|
||||
"digest.no_topics": "Inga aktiva ämnen de senaste %1",
|
||||
"digest.day": "dag",
|
||||
"digest.week": "vecka",
|
||||
"digest.month": "månad",
|
||||
"digest.subject": "Sammanställt flöde för %1",
|
||||
"notif.chat.subject": "Nytt chatt-meddelande från %1",
|
||||
"notif.chat.cta": "Klicka här för att fortsätta konversationen",
|
||||
"notif.chat.unsub.info": "Denna chatt-notifikation skickades till dig på grund av dina inställningar för prenumerationer.",
|
||||
"notif.post.cta": "Klicka här för att läsa hela ämnet",
|
||||
"notif.post.unsub.info": "Det här meddelandet fick du på grund av dina inställningar för prenumeration. ",
|
||||
"test.text1": "\nDet här är ett textmeddelande som verifierar att eposten är korrekt installerat för din NodeBB. ",
|
||||
"unsub.cta": "Klicka här för att ändra dom inställningarna",
|
||||
"test.text1": "\nDet här är ett testmeddelande som verifierar att e-posten är korrekt installerad för din NodeBB. ",
|
||||
"unsub.cta": "Klicka här för att ändra de inställningarna",
|
||||
"closing": "Tack!"
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
"invalid-password": "Ogiltigt lösenord",
|
||||
"invalid-username-or-password": "Specificera både användarnamn och lösenord",
|
||||
"invalid-search-term": "Ogiltig sökterm",
|
||||
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
|
||||
"invalid-pagination-value": "Ogiltigt värde för siduppdelning. Värdet måste vara mellan %1 och %2",
|
||||
"username-taken": "Användarnamn upptaget",
|
||||
"email-taken": "Epostadress upptagen",
|
||||
"email-not-confirmed": "Din epostadress är ännu inte bekräftad. Klicka här för att bekräfta din epostadress.",
|
||||
@@ -27,7 +27,7 @@
|
||||
"password-too-long": "Lösenordet är för långt",
|
||||
"user-banned": "Användare bannlyst",
|
||||
"user-too-new": "När du är ny medlem måste du vänta %1 sekund(er) innan du gör ditt första inlägg",
|
||||
"blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.",
|
||||
"blacklisted-ip": "Din IP-adress har blivit bannlyst från det här forumet. Om du tror att det beror på ett misstag, vad god kontakta en administratör. ",
|
||||
"no-category": "Kategorin finns inte",
|
||||
"no-topic": "Ämnet finns inte",
|
||||
"no-post": "Inlägget finns inte",
|
||||
@@ -44,15 +44,15 @@
|
||||
"title-too-long": "Skriv en kortare rubrik. Rubriker kan inte innehålla mer än %1 tecken.",
|
||||
"too-many-posts": "Du måste vänta minst %1 sekund(er) mellan varje inlägg",
|
||||
"too-many-posts-newbie": "Som ny användare måste du vänta %1 sekund(er) mellan varje inlägg tills dess du har %2 förtroende",
|
||||
"tag-too-short": "Fyll i ett längre märkord. Märkord måste vara minst %1 tecken långa",
|
||||
"tag-too-long": "Fyll i ett kortare märkord. Märkord kan ej vara längre än %1 tecken långa",
|
||||
"not-enough-tags": "Ej tillräckligt många märkord. Ämnen måste ha minst %1 märkord",
|
||||
"too-many-tags": "För många märkord. Ämnen kan ej har mer än %1 märkord",
|
||||
"tag-too-short": "Fyll i en längre tagg. Taggar måste vara minst %1 tecken långa",
|
||||
"tag-too-long": "Fyll i en kortare tagg. Taggar kan ej vara längre än %1 tecken långa",
|
||||
"not-enough-tags": "Otillräckligt antal taggar. Ämnen måste ha minst %1 taggar",
|
||||
"too-many-tags": "För många taggar. Ämnen kan ej har mer än %1 tagg(ar)",
|
||||
"still-uploading": "Vänta medan uppladdningen slutförs.",
|
||||
"file-too-big": "Den maximalt tillåtna filstorleken är %1 kB - ladda upp en mindre fil",
|
||||
"guest-upload-disabled": "Guest uploading has been disabled",
|
||||
"already-favourited": "You have already bookmarked this post",
|
||||
"already-unfavourited": "You have already unbookmarked this post",
|
||||
"file-too-big": "Den maximalt tillåtna filstorleken är %1 kB - var god ladda upp en mindre fil",
|
||||
"guest-upload-disabled": "Uppladdningar av oregistrerade användare har inaktiverats",
|
||||
"already-favourited": "Du har redan lagt till bokmärke för det här inlägget",
|
||||
"already-unfavourited": "Du har redan tagit bort bokmärket för det här inlägget",
|
||||
"cant-ban-other-admins": "Du kan inte bannlysa andra administratörer.",
|
||||
"cant-remove-last-admin": "Du är den enda administratören. Lägg till en annan användare som administratör innan du tar bort dig själv.",
|
||||
"invalid-image-type": "Ogiltig bildtyp. Tillåtna typer är: % 1",
|
||||
@@ -61,8 +61,8 @@
|
||||
"group-name-too-short": "Gruppnamnet är för kort",
|
||||
"group-already-exists": "Gruppen existerar redan",
|
||||
"group-name-change-not-allowed": "Gruppnamnet får inte ändras",
|
||||
"group-already-member": "Already part of this group",
|
||||
"group-not-member": "Not a member of this group",
|
||||
"group-already-member": "Redan i denna grupp",
|
||||
"group-not-member": "Ej medlem av denna grupp",
|
||||
"group-needs-owner": "Gruppen kräver minst en ägare",
|
||||
"group-already-invited": "Användaren har redan bjudits in",
|
||||
"group-already-requested": "Din medlemsskapsförfrågan har redan skickats",
|
||||
@@ -70,22 +70,22 @@
|
||||
"post-already-restored": "Inlägget är redan återställt",
|
||||
"topic-already-deleted": "Ämnet är redan raderat",
|
||||
"topic-already-restored": "Ämnet är redan återställt",
|
||||
"cant-purge-main-post": "Huvudinlägg kan ej rensas, ta bort ämnet istället",
|
||||
"cant-purge-main-post": "Huvudinlägg kan ej rensas bort, ta bort ämnet istället",
|
||||
"topic-thumbnails-are-disabled": "Miniatyrbilder för ämnen är inaktiverat",
|
||||
"invalid-file": "Ogiltig fil",
|
||||
"uploads-are-disabled": "Uppladdningar är inaktiverat",
|
||||
"signature-too-long": "Din signatur kan inte vara längre än %1 tecken.",
|
||||
"about-me-too-long": "Din om mig kan inte vara längre än %1 tecken.",
|
||||
"about-me-too-long": "Din text om dig själv kan inte vara längre än %1 tecken.",
|
||||
"cant-chat-with-yourself": "Du kan inte chatta med dig själv.",
|
||||
"chat-restricted": "Denna användaren har begränsat sina chatt-meddelanden. Användaren måste följa dig innan ni kan chatta med varann",
|
||||
"chat-disabled": "Chat system disabled",
|
||||
"chat-restricted": "Denna användaren har begränsat sina chatt-meddelanden. Användaren måste följa dig innan ni kan chatta med varandra",
|
||||
"chat-disabled": "Chatt är inaktiverat",
|
||||
"too-many-messages": "Du har skickat för många meddelanden, var god vänta",
|
||||
"invalid-chat-message": "Ogiltigt chattmeddelande",
|
||||
"chat-message-too-long": "Chattmeddelande är för långt",
|
||||
"cant-edit-chat-message": "You are not allowed to edit this message",
|
||||
"cant-remove-last-user": "You can't remove the last user",
|
||||
"cant-delete-chat-message": "You are not allowed to delete this message",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"cant-edit-chat-message": "Du har inte rättigheter att redigera det här meddelandet",
|
||||
"cant-remove-last-user": "Du kan inte ta bort den sista användaren",
|
||||
"cant-delete-chat-message": "Du har inte rättigheter att radera det här meddelandet",
|
||||
"already-voting-for-this-post": "Du har redan röstat på det här inlägget.",
|
||||
"reputation-system-disabled": "Ryktessystemet är inaktiverat.",
|
||||
"downvoting-disabled": "Nedröstning är inaktiverat",
|
||||
"not-enough-reputation-to-downvote": "Du har inte tillräckligt förtroende för att rösta ner det här meddelandet",
|
||||
@@ -94,11 +94,11 @@
|
||||
"reload-failed": "NodeBB stötte på problem med att ladda om: \"%1\". NodeBB kommer fortsätta servera den befintliga resurser till klienten, men du borde återställa det du gjorde alldeles innan du försökte ladda om.",
|
||||
"registration-error": "Registreringsfel",
|
||||
"parse-error": "Något gick fel vid tolkning av svar från servern",
|
||||
"wrong-login-type-email": "Använd din e-post adress för att logga in",
|
||||
"wrong-login-type-email": "Använd din e-postadress för att logga in",
|
||||
"wrong-login-type-username": "Använd ditt användarnamn för att logga in",
|
||||
"invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).",
|
||||
"no-session-found": "No login session found!",
|
||||
"not-in-room": "User not in room",
|
||||
"no-users-in-room": "No users in this room",
|
||||
"cant-kick-self": "You can't kick yourself from the group"
|
||||
"invite-maximum-met": "Du har bjudit in det maximala antalet användare (%1 av %2)",
|
||||
"no-session-found": "Ingen login-session hittades!",
|
||||
"not-in-room": "Användaren finns inte i rummet",
|
||||
"no-users-in-room": "Inga användare i det här rummet",
|
||||
"cant-kick-self": "Du kan inte sparka ut dig själv ifrån gruppen"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"404.title": "Sidan saknas",
|
||||
"404.message": "Du verkar ha ramlat in på en sida som inte finns. Återgå till <a href='%1/'>första sidan</a>.",
|
||||
"500.title": "Internt fel.",
|
||||
"500.message": "Hoppsan! Verkar som att något gått snett!",
|
||||
"500.message": "Hoppsan! Något verkar ha gått snett!",
|
||||
"register": "Registrera",
|
||||
"login": "Logga in",
|
||||
"please_log_in": "Var god logga in",
|
||||
@@ -25,7 +25,7 @@
|
||||
"header.categories": "Kategorier",
|
||||
"header.recent": "Senaste",
|
||||
"header.unread": "Olästa",
|
||||
"header.tags": "Märkningar",
|
||||
"header.tags": "Taggar",
|
||||
"header.popular": "Populära",
|
||||
"header.users": "Användare",
|
||||
"header.groups": "Grupper",
|
||||
@@ -33,15 +33,15 @@
|
||||
"header.notifications": "Notiser",
|
||||
"header.search": "Sök",
|
||||
"header.profile": "Profil",
|
||||
"header.navigation": "Navigation",
|
||||
"notifications.loading": "Laddar Notiser",
|
||||
"chats.loading": "Laddar Chattar",
|
||||
"motd.welcome": "Välkommen till NodeBB, framtidens diskussions-plattform.",
|
||||
"header.navigation": "Navigering",
|
||||
"notifications.loading": "Laddar notiser",
|
||||
"chats.loading": "Laddar chattar",
|
||||
"motd.welcome": "Välkommen till NodeBB, framtidens diskussionsplattform.",
|
||||
"previouspage": "Föregående sida",
|
||||
"nextpage": "Nästa sida",
|
||||
"alert.success": "Success",
|
||||
"alert.success": "Lyckat",
|
||||
"alert.error": "Fel",
|
||||
"alert.banned": "Bannad",
|
||||
"alert.banned": "Bannlyst",
|
||||
"alert.banned.message": "Du har blivit bannlyst och kommer nu att loggas ut. ",
|
||||
"alert.unfollow": "Du följer inte längre %1!",
|
||||
"alert.follow": "Du följer nu %1!",
|
||||
@@ -49,46 +49,46 @@
|
||||
"users": "Användare",
|
||||
"topics": "Ämnen",
|
||||
"posts": "Inlägg",
|
||||
"best": "Best",
|
||||
"upvoted": "Upvoted",
|
||||
"downvoted": "Downvoted",
|
||||
"best": "Bästa",
|
||||
"upvoted": "Uppröstad",
|
||||
"downvoted": "Nedröstad",
|
||||
"views": "Visningar",
|
||||
"reputation": "Rykte",
|
||||
"read_more": "läs mer",
|
||||
"more": "Mer",
|
||||
"posted_ago_by_guest": "inskickad %1 av anonym",
|
||||
"posted_ago_by": "inskickad %1 av %2",
|
||||
"posted_ago": "inskickad %1",
|
||||
"posted_in": "posted in %1",
|
||||
"posted_in_by": "posted in %1 by %2",
|
||||
"posted_ago": "postat %1",
|
||||
"posted_in": "postat i %1",
|
||||
"posted_in_by": "postat i %1 av %2",
|
||||
"posted_in_ago": "inskickad i %1 %2",
|
||||
"posted_in_ago_by": "inskickad i %1 %2 av %3",
|
||||
"user_posted_ago": "%1 skickades in %2",
|
||||
"guest_posted_ago": "Anonym skickade in %1",
|
||||
"last_edited_by": "last edited by %1",
|
||||
"posted_in_ago_by": "postat i %1 %2 av %3",
|
||||
"user_posted_ago": "%1 postades %2",
|
||||
"guest_posted_ago": "Anonym postade %1",
|
||||
"last_edited_by": "Senaste redigerad av %1",
|
||||
"norecentposts": "Inga nya inlägg",
|
||||
"norecenttopics": "Inga nya ämnen",
|
||||
"recentposts": "Senaste ämnena",
|
||||
"recentposts": "Senaste inläggen",
|
||||
"recentips": "Nyligen inloggade IPn",
|
||||
"away": "Borta",
|
||||
"dnd": "Do not disturb",
|
||||
"dnd": "Stör inte",
|
||||
"invisible": "Osynlig",
|
||||
"offline": "Offline",
|
||||
"email": "Epost",
|
||||
"email": "E-post",
|
||||
"language": "Språk",
|
||||
"guest": "Anonym",
|
||||
"guests": "Anonyma",
|
||||
"updated.title": "Forum uppdaterades",
|
||||
"updated.title": "Forumet uppdaterades",
|
||||
"updated.message": "Det här forumet har nu uppdaterats till senaste versionen. Klicka här för att ladda om sidan.",
|
||||
"privacy": "Integritet",
|
||||
"follow": "Följ",
|
||||
"unfollow": "Sluta följ",
|
||||
"delete_all": "Ta bort Alla",
|
||||
"map": "Map",
|
||||
"sessions": "Login Sessions",
|
||||
"ip_address": "IP Address",
|
||||
"enter_page_number": "Enter page number",
|
||||
"delete_all": "Ta bort alla",
|
||||
"map": "Karta",
|
||||
"sessions": "Login-sessioner",
|
||||
"ip_address": "IP-adress",
|
||||
"enter_page_number": "Skriv in sidnummer",
|
||||
"upload_file": "Ladda upp en fil",
|
||||
"upload": "Ladda upp",
|
||||
"allowed-file-types": "Allowed file types are %1"
|
||||
"allowed-file-types": "Tillåtna filtyper är %1"
|
||||
}
|
||||
@@ -7,30 +7,30 @@
|
||||
"pending.accept": "Acceptera",
|
||||
"pending.reject": "Neka",
|
||||
"pending.accept_all": "Acceptera alla",
|
||||
"pending.reject_all": "Neka alla",
|
||||
"pending.reject_all": "Avvisa alla",
|
||||
"pending.none": "Det finns inga väntande medlemmar just nu",
|
||||
"invited.none": "Det finns inga inbjudna medlemmar just nu",
|
||||
"invited.uninvite": "Dra tillbaka inbjudan",
|
||||
"invited.search": "Sök efter en användare att lägga till i denna grupp",
|
||||
"invited.notification_title": "You have been invited to join <strong>%1</strong>",
|
||||
"request.notification_title": "Group Membership Request from <strong>%1</strong>",
|
||||
"request.notification_text": "<strong>%1</strong> has requested to become a member of <strong>%2</strong>",
|
||||
"invited.notification_title": "Du har blivit inbjuden att bli medlem i <strong>%1</strong>",
|
||||
"request.notification_title": "Förfrågan om gruppmedlemskap från <strong>%1</strong>",
|
||||
"request.notification_text": "<strong>%1</strong> har skickat en förfrågan om medlemskap i <strong>%2</strong>",
|
||||
"cover-save": "Spara",
|
||||
"cover-saving": "Sparar",
|
||||
"details.title": "Detaljer för gruppen ",
|
||||
"details.members": "Medlemmar",
|
||||
"details.members": "Medlemslista",
|
||||
"details.pending": "Väntande medlemmar",
|
||||
"details.invited": "Inbjudna medlemmar",
|
||||
"details.has_no_posts": "Den här gruppens medlemmar har inte skrivit några inlägg.",
|
||||
"details.latest_posts": "Senaste inlägg",
|
||||
"details.private": "Privat",
|
||||
"details.disableJoinRequests": "Disable join requests",
|
||||
"details.grant": "Ge/Ta ifrån ägarskap",
|
||||
"details.disableJoinRequests": "Inaktivera förfrågningar om att gå med",
|
||||
"details.grant": "Tilldela/Dra tillbaka ägarskap",
|
||||
"details.kick": "Sparka ut",
|
||||
"details.owner_options": "Gruppadministration",
|
||||
"details.group_name": "Gruppnamn",
|
||||
"details.member_count": "Medlemsantal",
|
||||
"details.creation_date": "Skapatdatum",
|
||||
"details.creation_date": "Skapardatum",
|
||||
"details.description": "Beskrivning",
|
||||
"details.badge_preview": "Förhandsgranskning av märke",
|
||||
"details.change_icon": "Byt ikon",
|
||||
@@ -41,14 +41,14 @@
|
||||
"details.hidden": "Dold",
|
||||
"details.hidden_help": "Om aktiverat kommer gruppen inte synas i grupplistan och användare måste bli inbjudna manuellt",
|
||||
"details.delete_group": "Ta bort grupp",
|
||||
"details.private_system_help": "Private groups is disabled at system level, this option does not do anything",
|
||||
"event.updated": "Gruppdetaljerna har uppdaterats",
|
||||
"details.private_system_help": "Privata grupper är ej tillgängligt. Den här inställningen har ingen effekt.",
|
||||
"event.updated": "Gruppinformationen har uppdaterats",
|
||||
"event.deleted": "Gruppen \"%1\" har tagits bort",
|
||||
"membership.accept-invitation": "Acceptera inbjudan",
|
||||
"membership.invitation-pending": "Inbjudan väntar på svar",
|
||||
"membership.join-group": "Gå med i grupp",
|
||||
"membership.leave-group": "Lämna grupp",
|
||||
"membership.reject": "Neka",
|
||||
"new-group.group_name": "Group Name:",
|
||||
"upload-group-cover": "Upload group cover"
|
||||
"new-group.group_name": "Gruppnamn:",
|
||||
"upload-group-cover": "Ladda upp omslagsbild för grupp"
|
||||
}
|
||||
@@ -1,38 +1,38 @@
|
||||
{
|
||||
"chat.chatting_with": "Chatta med <span id=\"chat-with-name\"></span>",
|
||||
"chat.placeholder": "Skriv chatmeddelande här och tryck sen enter för att skicka ",
|
||||
"chat.placeholder": "Skriv chattmeddelande här och tryck sen Enter för att skicka ",
|
||||
"chat.send": "Skicka",
|
||||
"chat.no_active": "Du har inte några aktiva chattar.",
|
||||
"chat.user_typing": "%1 skriver ...",
|
||||
"chat.user_has_messaged_you": "%1 har skickat ett medelande till dig.",
|
||||
"chat.see_all": "Se alla chattar",
|
||||
"chat.mark_all_read": "Mark all chats read",
|
||||
"chat.no-messages": "Välj mottagare för att visa historik för chatmeddelande",
|
||||
"chat.no-users-in-room": "No users in this room",
|
||||
"chat.mark_all_read": "Markera alla chattar som lästa",
|
||||
"chat.no-messages": "Välj mottagare för att visa historik för chattmeddelande",
|
||||
"chat.no-users-in-room": "Inga användare i detta rum",
|
||||
"chat.recent-chats": "Senaste chattarna",
|
||||
"chat.contacts": "Kontakter ",
|
||||
"chat.message-history": "Historik för meddelande",
|
||||
"chat.pop-out": "Utskjutande chatt",
|
||||
"chat.maximize": "Maximera",
|
||||
"chat.seven_days": "7 Dagar",
|
||||
"chat.thirty_days": "30 Dagar",
|
||||
"chat.three_months": "3 Månader",
|
||||
"chat.delete_message_confirm": "Are you sure you wish to delete this message?",
|
||||
"chat.roomname": "Chat Room %1",
|
||||
"chat.add-users-to-room": "Add users to room",
|
||||
"chat.seven_days": "7 dagar",
|
||||
"chat.thirty_days": "30 dagar",
|
||||
"chat.three_months": "3 månader",
|
||||
"chat.delete_message_confirm": "Är du säker på att du vill radera det här meddelandet?",
|
||||
"chat.roomname": "Chattrum %1",
|
||||
"chat.add-users-to-room": "Addera användare till rum",
|
||||
"composer.compose": "Komponera",
|
||||
"composer.show_preview": "Visa förhandsgranskning",
|
||||
"composer.hide_preview": "Dölj förhandsgranskning",
|
||||
"composer.user_said_in": "%1 sa i %2:",
|
||||
"composer.user_said": "%1 sa:",
|
||||
"composer.discard": "Är du säker på att du vill förkasta det här inlägget?",
|
||||
"composer.discard": "Är du säker på att du vill ta bort det här inlägget?",
|
||||
"composer.submit_and_lock": "Skicka och lås",
|
||||
"composer.toggle_dropdown": "Visa/Dölj dropdown",
|
||||
"composer.uploading": "Uploading %1",
|
||||
"composer.uploading": "Laddar upp %1",
|
||||
"bootbox.ok": "OK",
|
||||
"bootbox.cancel": "Cancel",
|
||||
"bootbox.confirm": "Confirm",
|
||||
"cover.dragging_title": "Cover Photo Positioning",
|
||||
"cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"",
|
||||
"cover.saved": "Cover photo image and position saved"
|
||||
"bootbox.cancel": "Avbryt",
|
||||
"bootbox.confirm": "Bekräfta",
|
||||
"cover.dragging_title": "Positionering av omslagsbild",
|
||||
"cover.dragging_message": "Dra omslagsbilden till önskad position och tryck \"Spara\"",
|
||||
"cover.saved": "Omslagsbilden sparad"
|
||||
}
|
||||
@@ -5,34 +5,34 @@
|
||||
"mark_all_read": "Markera alla notiser som lästa",
|
||||
"back_to_home": "Tillbaka till %1",
|
||||
"outgoing_link": "Utgående länk",
|
||||
"outgoing_link_message": "You are now leaving %1",
|
||||
"outgoing_link_message": "Du lämnar nu %1",
|
||||
"continue_to": "Fortsätt till %1",
|
||||
"return_to": "Återgå till %1",
|
||||
"new_notification": "Ny notis",
|
||||
"you_have_unread_notifications": "Du har olästa notiser.",
|
||||
"new_message_from": "Nytt medelande från <strong>%1</strong>",
|
||||
"upvoted_your_post_in": "<strong>%1</strong> har röstat upp ditt inlägg i <strong>%2</strong>",
|
||||
"upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.",
|
||||
"upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.",
|
||||
"moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>",
|
||||
"moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>",
|
||||
"favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.",
|
||||
"favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.",
|
||||
"favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.",
|
||||
"upvoted_your_post_in_dual": "<strong>%1</strong> och <strong>%2</strong> har röstat upp ditt inlägg i <strong>%3</strong>.",
|
||||
"upvoted_your_post_in_multiple": "<strong>%1</strong> och %2 andra har röstat upp ditt inlägg i <strong>%3</strong>.",
|
||||
"moved_your_post": "<strong>%1</strong> har flyttat ditt inlägg till <strong>%2</strong>",
|
||||
"moved_your_topic": "<strong>%1</strong> har flyttat <strong>%2</strong>",
|
||||
"favourited_your_post_in": "<strong>%1</strong> har lagt till bokmärke på ditt inlägg i <strong>%2</strong>.",
|
||||
"favourited_your_post_in_dual": "<strong>%1</strong> och <strong>%2</strong> har lagt till bokmärke på ditt inlägg i <strong>%3</strong>.",
|
||||
"favourited_your_post_in_multiple": "<strong>%1</strong> och %2 andra har lagt till bokmärke på ditt inlägg i <strong>%3</strong>.",
|
||||
"user_flagged_post_in": "<strong>%1</strong> flaggade ett inlägg i <strong>%2</strong>",
|
||||
"user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>",
|
||||
"user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>",
|
||||
"user_flagged_post_in_dual": "<strong>%1</strong> och <strong>%2</strong> rapporterade ett inlägg i <strong>%3</strong>",
|
||||
"user_flagged_post_in_multiple": "<strong>%1</strong> och %2 andra rapporterade ett inlägg i <strong>%3</strong>",
|
||||
"user_posted_to": "<strong>%1</strong> har skrivit ett svar på: <strong>%2</strong>",
|
||||
"user_posted_to_dual": "<strong>%1</strong> and <strong>%2</strong> have posted replies to: <strong>%3</strong>",
|
||||
"user_posted_to_multiple": "<strong>%1</strong> and %2 others have posted replies to: <strong>%3</strong>",
|
||||
"user_posted_to_dual": "<strong>%1</strong> och <strong>%2</strong> har svarat på: <strong>%3</strong>",
|
||||
"user_posted_to_multiple": "<strong>%1</strong> och %2 andra har svarat på: <strong>%3</strong>",
|
||||
"user_posted_topic": "<strong>%1</strong> har skapat ett nytt ämne: <strong>%2</strong>",
|
||||
"user_started_following_you": "<strong>%1</strong> började följa dig.",
|
||||
"user_started_following_you_dual": "<strong>%1</strong> and <strong>%2</strong> started following you.",
|
||||
"user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.",
|
||||
"user_started_following_you_dual": "<strong>%1</strong> och <strong>%2</strong> började följa dig.",
|
||||
"user_started_following_you_multiple": "<strong>%1</strong> och %2 andra började följa dig.",
|
||||
"new_register": "<strong>%1</strong> skickade en registreringsförfrågan.",
|
||||
"new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.",
|
||||
"email-confirmed": "Epost bekräftad",
|
||||
"email-confirmed-message": "Tack för att du bekräftat din epostadress. Ditt konto är nu fullt ut aktiverat.",
|
||||
"email-confirm-error-message": "Det uppstod ett fel med att bekräfta din epostadress. Kanske var koden ogiltig eller har gått ut.",
|
||||
"email-confirm-sent": "Bekräftelseepost skickat."
|
||||
"new_register_multiple": "Det finns <strong>%1</strong> förfrågningar om registrering som inväntar granskning.",
|
||||
"email-confirmed": "E-post bekräftad",
|
||||
"email-confirmed-message": "Tack för att du bekräftat din e-postadress. Ditt konto är nu fullt ut aktiverat.",
|
||||
"email-confirm-error-message": "Det uppstod ett problem med bekräftelsen av din e-postadress. Kanske var koden felaktig eller ogiltig.",
|
||||
"email-confirm-sent": "Bekräftelsemeddelande skickat."
|
||||
}
|
||||
@@ -4,43 +4,43 @@
|
||||
"popular-day": "Populära ämnen idag",
|
||||
"popular-week": "Populära ämnen den här veckan",
|
||||
"popular-month": "Populära ämnen denna månad",
|
||||
"popular-alltime": "All time popular topics",
|
||||
"popular-alltime": "Populäraste ämnena genom tiderna",
|
||||
"recent": "Senaste ämnena",
|
||||
"flagged-posts": "Flaggade inlägg",
|
||||
"users/online": "Användare online",
|
||||
"users/latest": "Senaste Användare",
|
||||
"users/sort-posts": "Användare med flest inlägg",
|
||||
"users/sort-reputation": "Users with the most reputation",
|
||||
"users/banned": "Banned Users",
|
||||
"users/sort-reputation": "Användare med bäst rykte",
|
||||
"users/banned": "Bannlysta användare",
|
||||
"users/search": "Användar Sök",
|
||||
"notifications": "Notiser",
|
||||
"tags": "Etiketter",
|
||||
"tag": "Ämnen märkta med \"%1\"",
|
||||
"register": "Register an account",
|
||||
"register": "Registrera ett konto",
|
||||
"login": "Logga in på ditt konto",
|
||||
"reset": "Återställ lösenord",
|
||||
"categories": "Kategorier",
|
||||
"groups": "Grupper",
|
||||
"group": "%1 group",
|
||||
"chats": "Chats",
|
||||
"chat": "Chatting with %1",
|
||||
"account/edit": "Editing \"%1\"",
|
||||
"account/edit/password": "Editing password of \"%1\"",
|
||||
"account/edit/username": "Editing username of \"%1\"",
|
||||
"account/edit/email": "Editing email of \"%1\"",
|
||||
"account/following": "People %1 follows",
|
||||
"account/followers": "People who follow %1",
|
||||
"account/posts": "Posts made by %1",
|
||||
"account/topics": "Topics created by %1",
|
||||
"account/groups": "%1's Groups",
|
||||
"account/favourites": "%1's Bookmarked Posts",
|
||||
"group": "%1 grupp",
|
||||
"chats": "Chattar",
|
||||
"chat": "Chattar med %1",
|
||||
"account/edit": "Redigerar \"%1\"",
|
||||
"account/edit/password": "Redigerar lösenord för \"%1\"",
|
||||
"account/edit/username": "Redigerar användarnamn för \"%1\"",
|
||||
"account/edit/email": "Redigerar e-postadress för \"%1\"",
|
||||
"account/following": "Användare som %1 följer",
|
||||
"account/followers": "Användare som följer %1",
|
||||
"account/posts": "Inlägg skapade av %1",
|
||||
"account/topics": "Ämnen skapade av %1 ",
|
||||
"account/groups": "%1's grupper",
|
||||
"account/favourites": "%1's bokmärken",
|
||||
"account/settings": "Avnändarinställningar",
|
||||
"account/watched": "Topics watched by %1",
|
||||
"account/upvoted": "Posts upvoted by %1",
|
||||
"account/downvoted": "Posts downvoted by %1",
|
||||
"account/best": "Best posts made by %1",
|
||||
"confirm": "Email Confirmed",
|
||||
"account/watched": "Ämnen som bevakas av %1",
|
||||
"account/upvoted": "Inlägg som röstats upp av %1",
|
||||
"account/downvoted": "Inlägg som röstats ned av %1",
|
||||
"account/best": "Bästa inläggen skapade av %1",
|
||||
"confirm": "E-postadress bekräftad",
|
||||
"maintenance.text": "%1 genomgår underhåll just nu. Vänligen kom tillbaka lite senare.",
|
||||
"maintenance.messageIntro": "Ytterligare så lämnade administratören detta meddelande:",
|
||||
"throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time."
|
||||
"maintenance.messageIntro": "Utöver det så lämnade administratören följande meddelande:",
|
||||
"throttled.text": "%1 ligger tillfälligt nere på grund av överbelastning. Var god återkom senare. "
|
||||
}
|
||||
@@ -6,12 +6,12 @@
|
||||
"year": "År",
|
||||
"alltime": "Alltid",
|
||||
"no_recent_topics": "Det finns inga olästa ämnen.",
|
||||
"no_popular_topics": "Det finns inga populära ämnen",
|
||||
"there-is-a-new-topic": "Det finns ett nytt ämne",
|
||||
"no_popular_topics": "Det finns inga populära ämnen.",
|
||||
"there-is-a-new-topic": "Det finns ett nytt ämne.",
|
||||
"there-is-a-new-topic-and-a-new-post": "Det finns ett nytt ämne och ett nytt inlägg.",
|
||||
"there-is-a-new-topic-and-new-posts": "Det finns ett nytt ämne och %1 nya inlägg.",
|
||||
"there-are-new-topics": "Det finns %1 nya ämnen.",
|
||||
"there-are-new-topics-and-a-new-post": "Det finns %1 nya ämnen och ett nytt inlägg..",
|
||||
"there-are-new-topics-and-a-new-post": "Det finns %1 nya ämnen och ett nytt inlägg.",
|
||||
"there-are-new-topics-and-new-posts": "Det finns %1 nya ämnen och %2 nya inlägg.",
|
||||
"there-is-a-new-post": "Det finns ett nytt inlägg.",
|
||||
"there-are-new-posts": "Det finns %1 nya inlägg.",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"register": "Registrera",
|
||||
"help.email": "Som standard, är din epost-adress dold för allmänheten.",
|
||||
"help.email": "Som standard, är din e-postadress dold för allmänheten.",
|
||||
"help.username_restrictions": "Ett unikt användarnamn mellan %1 och %2 bokstäver. Andra kan nämna dig med @<span id='yourUsername'>användarnamn</span>.",
|
||||
"help.minimum_password_length": "Ditt lösenord måste vara minst %1 bokstäver.",
|
||||
"email_address": "Epost-adress",
|
||||
"email_address_placeholder": "Ange Epost-adress",
|
||||
"email_address": "E-postadress",
|
||||
"email_address_placeholder": "Ange E-postadress",
|
||||
"username": "Användarnamn",
|
||||
"username_placeholder": "Ange användarnamn",
|
||||
"password": "Lösenord",
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"wrong_reset_code.message": "Den mottagna återställningskoden var felaktig. Var god försök igen, eller <a href=\"/reset\">begär en ny återställningskod</a>.",
|
||||
"new_password": "Nytt lösenord",
|
||||
"repeat_password": "Bekräfta lösenord",
|
||||
"enter_email": "Var god fyll i din <strong>epost-adress</strong> så får du snart en epost med instruktioner hur du återsätller ditt konto.",
|
||||
"enter_email_address": "Skriv in epostadress",
|
||||
"enter_email": "Var god fyll i din <strong>e-postadress</strong> så skickas ett e-postmeddelande med instruktioner hur du återställer ditt konto.",
|
||||
"enter_email_address": "Skriv in e-postadress",
|
||||
"password_reset_sent": "Lösenordsåterställning skickad",
|
||||
"invalid_email": "Felaktig epost / Epost finns inte!",
|
||||
"invalid_email": "Felaktig e-post / E-post finns inte!",
|
||||
"password_too_short": "Lösenordet är för kort, var god välj ett annat lösenord.",
|
||||
"passwords_do_not_match": "De två lösenorden du har fyllt i matchar ej varandra.",
|
||||
"password_expired": "Ditt lösenord har gått ut, var god välj ett nytt lösenord."
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"advanced-search": "Avancerad sökning",
|
||||
"in": "i",
|
||||
"titles": "Ämnen",
|
||||
"titles-posts": "Ämnen och Inlägg",
|
||||
"titles-posts": "Ämnen och inlägg",
|
||||
"posted-by": "Skapad av",
|
||||
"in-categories": "I kategorier",
|
||||
"search-child-categories": "Sök i underkategorier",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"no_tag_topics": "Det finns inga ämnen med detta märkord.",
|
||||
"no_tag_topics": "Det finns inga ämnen med denna tagg.",
|
||||
"tags": "Taggar",
|
||||
"enter_tags_here": "Fyll i märkord på mellan %1 och %2 tecken här.",
|
||||
"enter_tags_here": "Fyll i taggar på %1 till %2 tecken här.",
|
||||
"enter_tags_here_short": "Ange taggar...",
|
||||
"no_tags": "Det finns inga märkord ännu."
|
||||
"no_tags": "Det finns inga taggar ännu."
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"notify_me": "Få notiser om nya svar i detta ämne",
|
||||
"quote": "Citera",
|
||||
"reply": "Svara",
|
||||
"reply-as-topic": "Reply as topic",
|
||||
"reply-as-topic": "Svara som ämne",
|
||||
"guest-login-reply": "Logga in för att posta",
|
||||
"edit": "Ändra",
|
||||
"delete": "Ta bort",
|
||||
@@ -26,7 +26,7 @@
|
||||
"tools": "Verktyg",
|
||||
"flag": "Rapportera",
|
||||
"locked": "Låst",
|
||||
"bookmark_instructions": "Click here to return to the last read post in this thread.",
|
||||
"bookmark_instructions": "Klicka här för att återgå till senast lästa inlägg i detta ämne.",
|
||||
"flag_title": "Rapportera detta inlägg för granskning",
|
||||
"flag_success": "Det här inlägget har flaggats för moderering.",
|
||||
"deleted_message": "Det här ämnet har raderats. Endast användare med ämneshanterings-privilegier kan se det.",
|
||||
@@ -34,8 +34,8 @@
|
||||
"not_following_topic.message": "Du kommer inte längre få notiser från detta ämne.",
|
||||
"login_to_subscribe": "Var god registrera eller logga in för att kunna prenumerera på detta ämne.",
|
||||
"markAsUnreadForAll.success": "Ämne markerat som oläst av alla.",
|
||||
"mark_unread": "Mark unread",
|
||||
"mark_unread.success": "Topic marked as unread.",
|
||||
"mark_unread": "Markera som oläst",
|
||||
"mark_unread.success": "Ämne markerat som oläst.",
|
||||
"watch": "Bevaka",
|
||||
"unwatch": "Sluta bevaka",
|
||||
"watch.title": "Få notis om nya svar till det här ämnet",
|
||||
@@ -43,46 +43,46 @@
|
||||
"share_this_post": "Dela detta inlägg",
|
||||
"thread_tools.title": "Ämnesverktyg",
|
||||
"thread_tools.markAsUnreadForAll": "Markera som oläst",
|
||||
"thread_tools.pin": "Fäst ämne",
|
||||
"thread_tools.pin": "Nåla fast ämne",
|
||||
"thread_tools.unpin": "Lösgör ämne",
|
||||
"thread_tools.lock": "Lås ämne",
|
||||
"thread_tools.unlock": "Öppna upp ämne",
|
||||
"thread_tools.unlock": "Lås upp ämne",
|
||||
"thread_tools.move": "Flytta ämne",
|
||||
"thread_tools.move_all": "Flytta alla.",
|
||||
"thread_tools.move_all": "Flytta alla",
|
||||
"thread_tools.fork": "Grena ämne",
|
||||
"thread_tools.delete": "Ta bort ämne",
|
||||
"thread_tools.delete-posts": "Delete Posts",
|
||||
"thread_tools.delete-posts": "Radera inlägg",
|
||||
"thread_tools.delete_confirm": "Är du säker på att du vill ta bort det här ämnet?",
|
||||
"thread_tools.restore": "Återställ ämne",
|
||||
"thread_tools.restore_confirm": "Är du säker på att du vill återställa det här ämnet?",
|
||||
"thread_tools.purge": "Rensa ämne",
|
||||
"thread_tools.purge_confirm": "Är du säker att du vill rensa ut det här ämnet?",
|
||||
"thread_tools.purge": "Rensa bort ämne",
|
||||
"thread_tools.purge_confirm": "Är du säker att du vill rensa bort det här ämnet?",
|
||||
"topic_move_success": "Det här ämnet har flyttats till %1",
|
||||
"post_delete_confirm": "Är du säker på att du vill ta bort det här inlägget?",
|
||||
"post_restore_confirm": "Är du säker på att du vill återställa det här inlägget?",
|
||||
"post_purge_confirm": "Är du säker att du vill rensa ut det här inlägget?",
|
||||
"post_purge_confirm": "Är du säker att du vill rensa bort det här inlägget?",
|
||||
"load_categories": "Laddar kategorier",
|
||||
"disabled_categories_note": "Inaktiverade kategorier är utgråade",
|
||||
"confirm_move": "Flytta",
|
||||
"confirm_fork": "Grena",
|
||||
"favourite": "Bookmark",
|
||||
"favourites": "Bookmarks",
|
||||
"favourites.has_no_favourites": "You haven't bookmarked any posts yet.",
|
||||
"favourite": "Bokmärke",
|
||||
"favourites": "Bokmärken",
|
||||
"favourites.has_no_favourites": "Du har inte lagt till bokmärke på något inlägg än.",
|
||||
"loading_more_posts": "Laddar fler inlägg",
|
||||
"move_topic": "Flytta ämne",
|
||||
"move_topics": "Flytta ämnen",
|
||||
"move_post": "Flytta inlägg",
|
||||
"post_moved": "Inlägget flyttades.",
|
||||
"fork_topic": "Grena ämne",
|
||||
"topic_will_be_moved_to": "Detta ämne kommer bli flytta till kategori",
|
||||
"topic_will_be_moved_to": "Detta ämne kommer att flyttas till kategorin",
|
||||
"fork_topic_instruction": "Klicka på de inlägg du vill grena",
|
||||
"fork_no_pids": "Inga inlägg valda!",
|
||||
"fork_success": "Ämnet har blivit förgrenat. Klicka här för att gå till det förgrenade ämnet.",
|
||||
"delete_posts_instruction": "Click the posts you want to delete/purge",
|
||||
"delete_posts_instruction": "Klicka på inläggen du vill radera/rensa bort",
|
||||
"composer.title_placeholder": "Skriv in ämnets titel här...",
|
||||
"composer.handle_placeholder": "Namn",
|
||||
"composer.discard": "Avbryt",
|
||||
"composer.submit": "Skicka",
|
||||
"composer.submit": "Posta inlägg",
|
||||
"composer.replying_to": "Svarar till %1",
|
||||
"composer.new_topic": "Nytt ämne",
|
||||
"composer.uploading": "laddar upp...",
|
||||
@@ -92,21 +92,21 @@
|
||||
"composer.thumb_file_label": "Eller ladda upp en fil",
|
||||
"composer.thumb_remove": "Töm fält",
|
||||
"composer.drag_and_drop_images": "Dra och släpp bilder här",
|
||||
"more_users_and_guests": "%1 fler användare() och %2 gäst(er)",
|
||||
"more_users": "%1 fler användare()",
|
||||
"more_users_and_guests": "%1 fler användare och %2 gäst(er)",
|
||||
"more_users": "%1 fler användare",
|
||||
"more_guests": "1% fler gäst(er)",
|
||||
"users_and_others": "%1 och %2 andra",
|
||||
"sort_by": "Sortera på",
|
||||
"oldest_to_newest": "Äldst till nyaste",
|
||||
"newest_to_oldest": "Nyaste till äldst",
|
||||
"most_votes": "Mest röster",
|
||||
"most_posts": "Felst inlägg",
|
||||
"most_votes": "Flest röster",
|
||||
"most_posts": "Flest inlägg",
|
||||
"stale.title": "Skapa nytt ämne istället?",
|
||||
"stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?",
|
||||
"stale.warning": "Ämnet du svarar på är ganska gammalt. Vill du skapa ett nytt ämne istället och inkludera en referens till det här ämnet i ditt inlägg?",
|
||||
"stale.create": "Skapa nytt ämne",
|
||||
"stale.reply_anyway": "Svara på ämnet ändå",
|
||||
"link_back": "Re: [%1](%2)",
|
||||
"spam": "Spam",
|
||||
"offensive": "Offensive",
|
||||
"custom-flag-reason": "Enter a flagging reason"
|
||||
"offensive": "Kränkande",
|
||||
"custom-flag-reason": "Ange skälet för rapporteringen"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"uploading-file": "Laddar upp filen...",
|
||||
"select-file-to-upload": "Välj en fil att ladda upp!",
|
||||
"upload-success": "File uploaded successfully!",
|
||||
"maximum-file-size": "Maximum %1 kb"
|
||||
"upload-success": "Filen laddades upp!",
|
||||
"maximum-file-size": "Maximalt %1 kb"
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"banned": "Bannad",
|
||||
"banned": "Bannlyst",
|
||||
"offline": "Offline",
|
||||
"username": "Användarnamn",
|
||||
"joindate": "Gick med",
|
||||
"postcount": "Antal inlägg",
|
||||
"email": "Epost",
|
||||
"confirm_email": "Bekräfta epostadress ",
|
||||
"email": "E-post",
|
||||
"confirm_email": "Bekräfta e-postadress ",
|
||||
"ban_account": "Bannlys konto",
|
||||
"ban_account_confirm": "Vill du verkligen bannlysa den här användaren?",
|
||||
"unban_account": "Ta bort bannlysning",
|
||||
@@ -22,7 +22,7 @@
|
||||
"profile": "Profil",
|
||||
"profile_views": "Profil-visningar",
|
||||
"reputation": "Rykte",
|
||||
"favourites": "Bookmarks",
|
||||
"favourites": "Bokmärken",
|
||||
"watched": "Bevakad",
|
||||
"followers": "Följare",
|
||||
"following": "Följer",
|
||||
@@ -30,20 +30,20 @@
|
||||
"signature": "Signatur",
|
||||
"birthday": "Födelsedag",
|
||||
"chat": "Chatta",
|
||||
"chat_with": "Chat with %1",
|
||||
"chat_with": "Chatta med %1",
|
||||
"follow": "Följ",
|
||||
"unfollow": "Sluta följ",
|
||||
"more": "Mer",
|
||||
"profile_update_success": "Profilen uppdaterades.",
|
||||
"change_picture": "Ändra bild",
|
||||
"change_username": "Change Username",
|
||||
"change_email": "Change Email",
|
||||
"change_username": "Ändra användarnamn",
|
||||
"change_email": "Ändra e-postadress",
|
||||
"edit": "Ändra",
|
||||
"edit-profile": "Edit Profile",
|
||||
"edit-profile": "Redigera profil",
|
||||
"default_picture": "Standard-ikon",
|
||||
"uploaded_picture": "Uppladdad bild",
|
||||
"upload_new_picture": "Ladda upp ny bild",
|
||||
"upload_new_picture_from_url": "Ladda upp ny bild från länk",
|
||||
"upload_new_picture_from_url": "Ladda upp ny bild via länk",
|
||||
"current_password": "Nuvarande lösenord",
|
||||
"change_password": "Ändra lösenord",
|
||||
"change_password_error": "Ogiltigt lösenord.",
|
||||
@@ -56,56 +56,56 @@
|
||||
"password": "Lösenord",
|
||||
"username_taken_workaround": "Användarnamnet är redan upptaget, så vi förändrade det lite. Du kallas nu för <strong>%1</strong>",
|
||||
"password_same_as_username": "Ditt lösenord är samma som ditt användarnamn, välj ett annat lösenord.",
|
||||
"password_same_as_email": "Your password is the same as your email, please select another password.",
|
||||
"password_same_as_email": "Ditt lösenord är detsamma som din e-postadress. Var god välj ett annat lösenord.",
|
||||
"upload_picture": "Ladda upp bild",
|
||||
"upload_a_picture": "Ladda upp en bild",
|
||||
"remove_uploaded_picture": "Ta bort uppladdad bild",
|
||||
"upload_cover_picture": "Upload cover picture",
|
||||
"upload_cover_picture": "Ladda upp omslagsbild",
|
||||
"settings": "Inställningar",
|
||||
"show_email": "Visa min epost",
|
||||
"show_fullname": "Visa Fullständigt Namn",
|
||||
"show_email": "Visa min e-postadress",
|
||||
"show_fullname": "Visa fullständigt namn",
|
||||
"restrict_chats": "Tillåt endast chatt-meddelanden från användare som jag följer",
|
||||
"digest_label": "Prenumerera på sammanställt flöde",
|
||||
"digest_description": "Prenumerera på epostuppdateringar för det här forumet (notiser och ämnen) med en viss regelbundenhet",
|
||||
"digest_description": "Prenumerera på e-postuppdateringar för det här forumet (notiser och ämnen) med en viss regelbundenhet",
|
||||
"digest_off": "Avslagen",
|
||||
"digest_daily": "Daligen",
|
||||
"digest_daily": "Dagligen",
|
||||
"digest_weekly": "Veckovis",
|
||||
"digest_monthly": "Månadsvis",
|
||||
"send_chat_notifications": "Skicka ett epostmeddelande om nya chatt-meddelanden tas emot när jag inte är online.",
|
||||
"send_post_notifications": "Skicka ett epost när svar kommit på ämnen jag prenumererar på till",
|
||||
"send_chat_notifications": "Skicka ett e-postmeddelande om nya chatt-meddelanden tas emot när jag inte är online.",
|
||||
"send_post_notifications": "Skicka ett e-postmeddelande när svar tillkommit på ämnen jag prenumererar på",
|
||||
"settings-require-reload": "Vissa inställningar som ändrades kräver att sidan laddas om. Klicka här för att ladda om sidan.",
|
||||
"has_no_follower": "Denna användare har inga följare :(",
|
||||
"follows_no_one": "Denna användare följer ingen :(",
|
||||
"has_no_posts": "Användaren har inte skrivit några inlägg ännu",
|
||||
"has_no_topics": "Användaren har inte skrivit några ämnen ännu",
|
||||
"has_no_watched_topics": "Användaren har inte bevakat några ämnen ännu",
|
||||
"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",
|
||||
"email_hidden": "Epost dold",
|
||||
"has_no_posts": "Användaren har inte skrivit några inlägg ännu.",
|
||||
"has_no_topics": "Användaren har inte postat några ämnen ännu.",
|
||||
"has_no_watched_topics": "Användaren har inte bevakat några ämnen ännu.",
|
||||
"has_no_upvoted_posts": "Den här användaren har inte röstat upp några inlägg än.",
|
||||
"has_no_downvoted_posts": "Den här användaren har inte röstat ned några inlägg än.",
|
||||
"has_no_voted_posts": "Den här användaren har inga inlägg med röster",
|
||||
"email_hidden": "E-post dold",
|
||||
"hidden": "dold",
|
||||
"paginate_description": "Gör så att ämnen och inlägg visas som sidor istället för oändlig skroll",
|
||||
"topics_per_page": "Ämnen per sida",
|
||||
"posts_per_page": "Inlägg per sida",
|
||||
"notification_sounds": "Spela ett ljud när du får en notis",
|
||||
"browsing": "Inställning för bläddring",
|
||||
"open_links_in_new_tab": "Öppna utgående länkar på ny flik",
|
||||
"enable_topic_searching": "Aktivera Sökning Inom Ämne",
|
||||
"open_links_in_new_tab": "Öppna utgående länkar i ny flik",
|
||||
"enable_topic_searching": "Aktivera sökning inom ämne",
|
||||
"topic_search_help": "Om aktiverat kommer sökning inom ämne överskrida webbläsarens vanliga funktionen för sökning bland sidor och tillåta dig att söka genom hela ämnet istället för det som endast visas på skärmen.",
|
||||
"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": "Följ ämnen som du svarat på",
|
||||
"follow_topics_you_create": "Följ ämnen du skapat",
|
||||
"grouptitle": "Group Title",
|
||||
"delay_image_loading": "Fördröj inladdning av bilder",
|
||||
"image_load_delay_help": "Aktivera för att hindra bilder ifrån att ladda in, innan de skrollats fram på skärmen. ",
|
||||
"scroll_to_my_post": "Visa det nya inlägget när ett svar har postats",
|
||||
"follow_topics_you_reply_to": "Följ ämnen som du svarar på",
|
||||
"follow_topics_you_create": "Följ ämnen du skapar",
|
||||
"grouptitle": "Grupptitel",
|
||||
"no-group-title": "Ingen titel på gruppen",
|
||||
"select-skin": "Välj ett 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",
|
||||
"custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")",
|
||||
"select-homepage": "Välj en startsida",
|
||||
"homepage": "Startsida",
|
||||
"homepage_description": "Välj en sida som ska användas som forumets startsida eller 'Ingen' för att använda standardstartsidan.",
|
||||
"custom_route": "Sökväg till egen startsida",
|
||||
"custom_route_help": "Skriv in ett sökvägsnamn här, utan föregående slash. (tex \"recent\" eller \"popular\")",
|
||||
"sso.title": "Single Sign-on-tjänster",
|
||||
"sso.associated": "Associated with",
|
||||
"sso.not-associated": "Click here to associate with"
|
||||
"sso.associated": "Associerad med",
|
||||
"sso.not-associated": "Klicka här för att associera med"
|
||||
}
|
||||
@@ -15,6 +15,6 @@
|
||||
"popular_topics": "Populära ämnen",
|
||||
"unread_topics": "Olästa ämnen",
|
||||
"categories": "Kategorier",
|
||||
"tags": "Märkord",
|
||||
"no-users-found": "No users found!"
|
||||
"tags": "Taggar",
|
||||
"no-users-found": "Inga användare hittades!"
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"category": "Category",
|
||||
"subcategories": "Subcategories",
|
||||
"category": "類別",
|
||||
"subcategories": "子類別",
|
||||
"new_topic_button": "新主題",
|
||||
"guest-login-post": "登錄後才能發表",
|
||||
"guest-login-post": "登入後才能張貼",
|
||||
"no_topics": "<strong>這個類別還沒有任何主題。</strong><br />為何不來發點東西呢?",
|
||||
"browsing": "正在瀏覽",
|
||||
"no_replies": "還沒有回覆",
|
||||
"no_new_posts": "No new posts.",
|
||||
"no_new_posts": "沒有新的張貼",
|
||||
"share_this_category": "分享這類別",
|
||||
"watch": "觀看",
|
||||
"ignore": "忽略",
|
||||
"watch.message": "您正觀看著此類別的更新",
|
||||
"ignore.message": "您已忽略此類別的更新",
|
||||
"watched-categories": "Watched categories"
|
||||
"watched-categories": "看過的類別"
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"password-reset-requested": "已要求重設密碼 - %1!",
|
||||
"welcome-to": "歡迎來到%1",
|
||||
"invite": "Invitation from %1",
|
||||
"invite": "邀請來自 %1",
|
||||
"greeting_no_name": "您好",
|
||||
"greeting_with_name": "您好,%1",
|
||||
"welcome.text1": "多謝登記%1!",
|
||||
"welcome.text1": "感謝您註冊 %1!",
|
||||
"welcome.text2": "要啟用你的帳戶,我們先要驗證你用作登記的電郵地址",
|
||||
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.",
|
||||
"welcome.text3": "管理者已經批準您的註冊申請。你現在可以使用你的帳號/密碼進行登入。",
|
||||
"welcome.cta": "請點擊此處確認您的電郵地址",
|
||||
"invitation.text1": "%1 has invited you to join %2",
|
||||
"invitation.ctr": "Click here to create your account.",
|
||||
"invitation.text1": "%1 邀請您加入 %2",
|
||||
"invitation.ctr": "點擊這裡來建立你的帳號",
|
||||
"reset.text1": "我們收到一個重設密碼的請求,你忘掉了密碼嗎?如果不是,請忽略這封郵件。",
|
||||
"reset.text2": "要繼續重置密碼,請點擊以下鏈接:",
|
||||
"reset.cta": "點擊這裡重置密碼",
|
||||
@@ -21,10 +21,10 @@
|
||||
"digest.cta": "點擊這裡訪問%1",
|
||||
"digest.unsub.info": "本摘要按您的訂閱設置發送給您。",
|
||||
"digest.no_topics": "在過去%1沒有活躍的話題",
|
||||
"digest.day": "day",
|
||||
"digest.week": "week",
|
||||
"digest.month": "month",
|
||||
"digest.subject": "Digest for %1",
|
||||
"digest.day": "日",
|
||||
"digest.week": "週",
|
||||
"digest.month": "月",
|
||||
"digest.subject": "摘要於 %1",
|
||||
"notif.chat.subject": "收到來自$1的聊天消息",
|
||||
"notif.chat.cta": "點擊此處繼續對話",
|
||||
"notif.chat.unsub.info": "本聊天通知按您的訂閱設置發送給您。",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"invalid-data": "無效的資料",
|
||||
"not-logged-in": "您似乎還沒有登入喔!",
|
||||
"account-locked": "您的帳戶暫時被鎖定!",
|
||||
"search-requires-login": "Searching requires an account - please login or register.",
|
||||
"search-requires-login": "進行搜尋需要有帳號 - 請先註冊或登入",
|
||||
"invalid-cid": "無效的類別 ID",
|
||||
"invalid-tid": "無效的主題 ID",
|
||||
"invalid-pid": "無效的文章 ID",
|
||||
@@ -14,17 +14,17 @@
|
||||
"invalid-password": "無效的密碼",
|
||||
"invalid-username-or-password": "請指定用戶名和密碼",
|
||||
"invalid-search-term": "無效的搜索字詞",
|
||||
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
|
||||
"invalid-pagination-value": "無效的分頁數值, 必需是至少 %1 與最多 %2",
|
||||
"username-taken": "該使用者名稱已被使用",
|
||||
"email-taken": "該信箱已被使用",
|
||||
"email-not-confirmed": "您的電郵尚未得到確認,請點擊此處確認您的電子郵件。",
|
||||
"email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.",
|
||||
"no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email",
|
||||
"email-confirm-failed": "We could not confirm your email, please try again later.",
|
||||
"email-confirm-failed": "我們無法確認你的Email,請之後再重試。",
|
||||
"confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.",
|
||||
"username-too-short": "使用者名稱太簡短",
|
||||
"username-too-long": "使用者名稱太長",
|
||||
"password-too-long": "Password too long",
|
||||
"username-too-short": "帳號太短",
|
||||
"username-too-long": "帳號太長",
|
||||
"password-too-long": "密碼太長",
|
||||
"user-banned": "該使用者已被停用",
|
||||
"user-too-new": "抱歉,發表您第一篇文章須要等待 %1 秒",
|
||||
"blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.",
|
||||
@@ -50,9 +50,9 @@
|
||||
"too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)",
|
||||
"still-uploading": "請等待上傳完成。",
|
||||
"file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file",
|
||||
"guest-upload-disabled": "Guest uploading has been disabled",
|
||||
"already-favourited": "You have already bookmarked this post",
|
||||
"already-unfavourited": "You have already unbookmarked this post",
|
||||
"guest-upload-disabled": "訪客上傳是被禁止的",
|
||||
"already-favourited": "你已經將這篇張貼加入書籤",
|
||||
"already-unfavourited": "你已經將這篇張貼移除書籤",
|
||||
"cant-ban-other-admins": "您無法禁止其他的管理員!",
|
||||
"cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin",
|
||||
"invalid-image-type": "無效的圖像類型。允許的類型:%1",
|
||||
@@ -61,11 +61,11 @@
|
||||
"group-name-too-short": "群組名稱太短了",
|
||||
"group-already-exists": "群組名稱已存在",
|
||||
"group-name-change-not-allowed": "變更群組名稱不被允許",
|
||||
"group-already-member": "Already part of this group",
|
||||
"group-not-member": "Not a member of this group",
|
||||
"group-needs-owner": "This group requires at least one owner",
|
||||
"group-already-invited": "This user has already been invited",
|
||||
"group-already-requested": "Your membership request has already been submitted",
|
||||
"group-already-member": "已經加入這個群組",
|
||||
"group-not-member": "不是這個群組的一員",
|
||||
"group-needs-owner": "這個群組需要至少一個擁有者",
|
||||
"group-already-invited": "這個使用者已經被邀請",
|
||||
"group-already-requested": "你的會員申請已經被提交",
|
||||
"post-already-deleted": "此文章已經被刪除",
|
||||
"post-already-restored": "此文章已還原",
|
||||
"topic-already-deleted": "此主題已經被刪除",
|
||||
@@ -78,27 +78,27 @@
|
||||
"about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).",
|
||||
"cant-chat-with-yourself": "你不能與自己聊天!",
|
||||
"chat-restricted": "此用戶已限制了他的聊天功能。你要在他關注你之後,才能跟他聊天",
|
||||
"chat-disabled": "Chat system disabled",
|
||||
"chat-disabled": "聊天系統被禁止",
|
||||
"too-many-messages": "You have sent too many messages, please wait awhile.",
|
||||
"invalid-chat-message": "Invalid chat message",
|
||||
"chat-message-too-long": "Chat message is too long",
|
||||
"cant-edit-chat-message": "You are not allowed to edit this message",
|
||||
"cant-remove-last-user": "You can't remove the last user",
|
||||
"cant-delete-chat-message": "You are not allowed to delete this message",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"invalid-chat-message": "無效的聊天訊息",
|
||||
"chat-message-too-long": "聊天訊息太長",
|
||||
"cant-edit-chat-message": "你不被允許編輯這條訊息",
|
||||
"cant-remove-last-user": "你不能移除最後的使用者",
|
||||
"cant-delete-chat-message": "你不被允許刪除這條訊息",
|
||||
"already-voting-for-this-post": "你已經對這個張貼投過票了",
|
||||
"reputation-system-disabled": "信譽系統已停用。",
|
||||
"downvoting-disabled": "Downvoting已停用",
|
||||
"not-enough-reputation-to-downvote": "你沒有足夠的信譽downvote這個帖子",
|
||||
"not-enough-reputation-to-flag": "你沒有足夠的信譽來舉報這個帖子",
|
||||
"already-flagged": "You have already flagged this post",
|
||||
"already-flagged": "你已經對這個張貼標記過了",
|
||||
"reload-failed": "NodeBB重載\"%1\"時遇到了問題。 NodeBB將繼續提供現有的客戶端資源,但請你撤消重載前的動作。",
|
||||
"registration-error": "註冊錯誤",
|
||||
"parse-error": "Something went wrong while parsing server response",
|
||||
"parse-error": "當剖析伺服器回應時發生了某個錯誤",
|
||||
"wrong-login-type-email": "請使用您的電子郵件進行登錄",
|
||||
"wrong-login-type-username": "請使用您的使用者名稱進行登錄",
|
||||
"invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).",
|
||||
"no-session-found": "No login session found!",
|
||||
"not-in-room": "User not in room",
|
||||
"no-users-in-room": "No users in this room",
|
||||
"cant-kick-self": "You can't kick yourself from the group"
|
||||
"no-session-found": "沒有找到登入的連線階段!",
|
||||
"not-in-room": "使用者沒有在聊天室中",
|
||||
"no-users-in-room": "沒有使用者在聊天室中",
|
||||
"cant-kick-self": "你不能把自己從群組中踢出"
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
"pagination.out_of": "%1 out of %2",
|
||||
"pagination.enter_index": "輸入Index",
|
||||
"header.admin": "管理",
|
||||
"header.categories": "Categories",
|
||||
"header.categories": "類別",
|
||||
"header.recent": "最近",
|
||||
"header.unread": "未讀",
|
||||
"header.tags": "標籤",
|
||||
@@ -33,7 +33,7 @@
|
||||
"header.notifications": "通知",
|
||||
"header.search": "搜尋",
|
||||
"header.profile": "設置",
|
||||
"header.navigation": "Navigation",
|
||||
"header.navigation": "導覽",
|
||||
"notifications.loading": "消息載入中",
|
||||
"chats.loading": "聊天載入中···",
|
||||
"motd.welcome": "歡迎來到 NodeBB,一個未來的討論平台。",
|
||||
@@ -49,29 +49,29 @@
|
||||
"users": "使用者",
|
||||
"topics": "主題",
|
||||
"posts": "文章",
|
||||
"best": "Best",
|
||||
"upvoted": "Upvoted",
|
||||
"downvoted": "Downvoted",
|
||||
"best": "最棒",
|
||||
"upvoted": "讚",
|
||||
"downvoted": "爛",
|
||||
"views": "Views",
|
||||
"reputation": "聲譽",
|
||||
"read_more": "閱讀更多...",
|
||||
"more": "More",
|
||||
"more": "更多",
|
||||
"posted_ago_by_guest": "posted %1 by Guest",
|
||||
"posted_ago_by": "posted %1 by %2",
|
||||
"posted_ago": "posted %1",
|
||||
"posted_in": "posted in %1",
|
||||
"posted_in_by": "posted in %1 by %2",
|
||||
"posted_in": "張貼於 %1",
|
||||
"posted_in_by": "張貼於 %1 由 %2",
|
||||
"posted_in_ago": "posted in %1",
|
||||
"posted_in_ago_by": "posted in %1 %2 by %3",
|
||||
"user_posted_ago": "%1 posted %2",
|
||||
"guest_posted_ago": "Guest posted %1",
|
||||
"last_edited_by": "last edited by %1",
|
||||
"last_edited_by": "最後編輯由 %1",
|
||||
"norecentposts": "最近沒新文章",
|
||||
"norecenttopics": "最近沒新主題",
|
||||
"recentposts": "最近的文章",
|
||||
"recentips": "最近登錄的 IP 來源位址",
|
||||
"away": "離開",
|
||||
"dnd": "Do not disturb",
|
||||
"dnd": "不要打擾",
|
||||
"invisible": "隱藏",
|
||||
"offline": "離線",
|
||||
"email": "Email",
|
||||
@@ -84,11 +84,11 @@
|
||||
"follow": "追蹤",
|
||||
"unfollow": "取消追蹤",
|
||||
"delete_all": "全部刪除",
|
||||
"map": "Map",
|
||||
"sessions": "Login Sessions",
|
||||
"ip_address": "IP Address",
|
||||
"enter_page_number": "Enter page number",
|
||||
"upload_file": "Upload file",
|
||||
"upload": "Upload",
|
||||
"allowed-file-types": "Allowed file types are %1"
|
||||
"map": "地圖",
|
||||
"sessions": "登入連線階段",
|
||||
"ip_address": "IP地址",
|
||||
"enter_page_number": "輸入頁碼",
|
||||
"upload_file": "上傳檔案",
|
||||
"upload": "上傳",
|
||||
"allowed-file-types": "允許的檔案類型是 %1"
|
||||
}
|
||||
@@ -6,25 +6,25 @@
|
||||
"no_groups_found": "這裡看不到任何群組",
|
||||
"pending.accept": "接受",
|
||||
"pending.reject": "拒絕",
|
||||
"pending.accept_all": "Accept All",
|
||||
"pending.reject_all": "Reject All",
|
||||
"pending.none": "There are no pending members at this time",
|
||||
"invited.none": "There are no invited members at this time",
|
||||
"invited.uninvite": "Rescind Invitation",
|
||||
"invited.search": "Search for a user to invite to this group",
|
||||
"invited.notification_title": "You have been invited to join <strong>%1</strong>",
|
||||
"request.notification_title": "Group Membership Request from <strong>%1</strong>",
|
||||
"request.notification_text": "<strong>%1</strong> has requested to become a member of <strong>%2</strong>",
|
||||
"pending.accept_all": "同意所有",
|
||||
"pending.reject_all": "拒絕所有",
|
||||
"pending.none": "目前沒有等待中的會員",
|
||||
"invited.none": "目前沒有邀請的會員",
|
||||
"invited.uninvite": "撤銷邀請",
|
||||
"invited.search": "搜尋要邀請加入這個群組的用戶",
|
||||
"invited.notification_title": "你已被邀請加入<strong>%1</strong>",
|
||||
"request.notification_title": "群組會員要求,來自<strong>%1</strong>",
|
||||
"request.notification_text": "<strong>%1</strong>已經要求成為<strong>%2</strong>群組的會員",
|
||||
"cover-save": "儲存",
|
||||
"cover-saving": "儲存中",
|
||||
"details.title": "群組詳細信息",
|
||||
"details.members": "成員列表",
|
||||
"details.pending": "待審成員",
|
||||
"details.invited": "Invited Members",
|
||||
"details.invited": "邀請會員",
|
||||
"details.has_no_posts": "這個群組的成員還未發出任何帖子。",
|
||||
"details.latest_posts": "最新文章",
|
||||
"details.private": "私人",
|
||||
"details.disableJoinRequests": "Disable join requests",
|
||||
"details.disableJoinRequests": "禁止加入要求",
|
||||
"details.grant": "准許/撤銷 所有權",
|
||||
"details.kick": "剔除",
|
||||
"details.owner_options": "群組管理員",
|
||||
@@ -37,18 +37,18 @@
|
||||
"details.change_colour": "變更顏色",
|
||||
"details.badge_text": "徽章字串",
|
||||
"details.userTitleEnabled": "顯示徽章",
|
||||
"details.private_help": "If enabled, joining of groups requires approval from a group owner",
|
||||
"details.private_help": "如果開啟,加入群組需要經過群組擁有者批準",
|
||||
"details.hidden": "隱藏",
|
||||
"details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually",
|
||||
"details.delete_group": "Delete Group",
|
||||
"details.private_system_help": "Private groups is disabled at system level, this option does not do anything",
|
||||
"details.hidden_help": "如果開啟的話,群組將不會在群組列表中被看到,而且用戶將需要手動邀請",
|
||||
"details.delete_group": "刪除群組",
|
||||
"details.private_system_help": "私有群組在系統層級被禁用,這個選項沒有任何作用",
|
||||
"event.updated": "群組詳細訊息已被更新",
|
||||
"event.deleted": "此 \"%1\" 群組已被刪除了",
|
||||
"membership.accept-invitation": "Accept Invitation",
|
||||
"membership.invitation-pending": "Invitation Pending",
|
||||
"membership.join-group": "Join Group",
|
||||
"membership.leave-group": "Leave Group",
|
||||
"membership.reject": "Reject",
|
||||
"new-group.group_name": "Group Name:",
|
||||
"upload-group-cover": "Upload group cover"
|
||||
"membership.accept-invitation": "同意邀請",
|
||||
"membership.invitation-pending": "邀請等待中",
|
||||
"membership.join-group": "加入群組",
|
||||
"membership.leave-group": "離開群組",
|
||||
"membership.reject": "拒絕",
|
||||
"new-group.group_name": "群組名稱:",
|
||||
"upload-group-cover": "上傳群組封面圖"
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
"chat.no_active": "暫無聊天",
|
||||
"chat.user_typing": "%1 正在輸入中...",
|
||||
"chat.user_has_messaged_you": "%1 已傳送訊息給你了",
|
||||
"chat.see_all": "See all chats",
|
||||
"chat.mark_all_read": "Mark all chats read",
|
||||
"chat.see_all": "顯示全部聊天",
|
||||
"chat.mark_all_read": "所有訊息標為已讀",
|
||||
"chat.no-messages": "請選擇收件人來查看聊天記錄",
|
||||
"chat.no-users-in-room": "No users in this room",
|
||||
"chat.no-users-in-room": "沒有用戶在聊天室中",
|
||||
"chat.recent-chats": "最近的聊天記錄",
|
||||
"chat.contacts": "通訊錄",
|
||||
"chat.message-history": "消息記錄",
|
||||
@@ -17,22 +17,22 @@
|
||||
"chat.seven_days": "7日",
|
||||
"chat.thirty_days": "30日",
|
||||
"chat.three_months": "3個月",
|
||||
"chat.delete_message_confirm": "Are you sure you wish to delete this message?",
|
||||
"chat.roomname": "Chat Room %1",
|
||||
"chat.add-users-to-room": "Add users to room",
|
||||
"composer.compose": "Compose",
|
||||
"composer.show_preview": "Show Preview",
|
||||
"composer.hide_preview": "Hide Preview",
|
||||
"chat.delete_message_confirm": "你確定要刪除這個訊息?",
|
||||
"chat.roomname": "聊天室 %1",
|
||||
"chat.add-users-to-room": "將用戶加入聊天室中",
|
||||
"composer.compose": "撰寫",
|
||||
"composer.show_preview": "顯示預覽",
|
||||
"composer.hide_preview": "隱藏預覽",
|
||||
"composer.user_said_in": "%1在%2裡說:",
|
||||
"composer.user_said": "%1說:",
|
||||
"composer.discard": "你確定要放棄這帖子嗎?",
|
||||
"composer.submit_and_lock": "Submit and Lock",
|
||||
"composer.toggle_dropdown": "Toggle Dropdown",
|
||||
"composer.uploading": "Uploading %1",
|
||||
"bootbox.ok": "OK",
|
||||
"bootbox.cancel": "Cancel",
|
||||
"bootbox.confirm": "Confirm",
|
||||
"cover.dragging_title": "Cover Photo Positioning",
|
||||
"cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"",
|
||||
"cover.saved": "Cover photo image and position saved"
|
||||
"composer.submit_and_lock": "提交然後鎖定",
|
||||
"composer.toggle_dropdown": "切換下拉選單",
|
||||
"composer.uploading": "上傳中 %1",
|
||||
"bootbox.ok": "好",
|
||||
"bootbox.cancel": "取消",
|
||||
"bootbox.confirm": "確認",
|
||||
"cover.dragging_title": "封面照片位置",
|
||||
"cover.dragging_message": "拖拉封面照片到想要的位置,然後按下\"儲存\"",
|
||||
"cover.saved": "封面照片與位置已儲存"
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"home": "首頁",
|
||||
"unread": "未讀的主題",
|
||||
"popular-day": "Popular topics today",
|
||||
"popular-week": "Popular topics this week",
|
||||
"popular-month": "Popular topics this month",
|
||||
"popular-alltime": "All time popular topics",
|
||||
"popular-day": "今天受歡迎的主題",
|
||||
"popular-week": "本週受歡迎的主題",
|
||||
"popular-month": "本月受歡迎的主題",
|
||||
"popular-alltime": "所有時間受歡迎的主題",
|
||||
"recent": "近期的主題",
|
||||
"flagged-posts": "Flagged Posts",
|
||||
"users/online": "Online Users",
|
||||
"users/latest": "Latest Users",
|
||||
"flagged-posts": "標記的張貼",
|
||||
"users/online": "線上用戶",
|
||||
"users/latest": "最近用戶",
|
||||
"users/sort-posts": "Users with the most posts",
|
||||
"users/sort-reputation": "Users with the most reputation",
|
||||
"users/banned": "Banned Users",
|
||||
"users/search": "User Search",
|
||||
"users/banned": "已封鎖用戶",
|
||||
"users/search": "用戶搜尋",
|
||||
"notifications": "新訊息通知",
|
||||
"tags": "標籤",
|
||||
"tag": "Topics tagged under \"%1\"",
|
||||
"register": "Register an account",
|
||||
"login": "Login to your account",
|
||||
"register": "註冊帳號",
|
||||
"login": "登入帳號",
|
||||
"reset": "Reset your account password",
|
||||
"categories": "Categories",
|
||||
"groups": "Groups",
|
||||
"group": "%1 group",
|
||||
"chats": "Chats",
|
||||
"chat": "Chatting with %1",
|
||||
"account/edit": "Editing \"%1\"",
|
||||
"categories": "類別",
|
||||
"groups": "群組",
|
||||
"group": "%1 群組",
|
||||
"chats": "聊天",
|
||||
"chat": "與 %1 聊天",
|
||||
"account/edit": "編輯 \"%1\"",
|
||||
"account/edit/password": "Editing password of \"%1\"",
|
||||
"account/edit/username": "Editing username of \"%1\"",
|
||||
"account/edit/email": "Editing email of \"%1\"",
|
||||
@@ -32,14 +32,14 @@
|
||||
"account/followers": "People who follow %1",
|
||||
"account/posts": "Posts made by %1",
|
||||
"account/topics": "Topics created by %1",
|
||||
"account/groups": "%1's Groups",
|
||||
"account/groups": "%1 的群組",
|
||||
"account/favourites": "%1's Bookmarked Posts",
|
||||
"account/settings": "User Settings",
|
||||
"account/settings": "用戶設定",
|
||||
"account/watched": "Topics watched by %1",
|
||||
"account/upvoted": "Posts upvoted by %1",
|
||||
"account/downvoted": "Posts downvoted by %1",
|
||||
"account/best": "Best posts made by %1",
|
||||
"confirm": "Email Confirmed",
|
||||
"confirm": "已確認電子郵件",
|
||||
"maintenance.text": "目前 %1 正在進行維修。請稍後再來。",
|
||||
"maintenance.messageIntro": "此外,管理員有以下訊息:",
|
||||
"throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time."
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
"alltime": "所有時間",
|
||||
"no_recent_topics": "最近沒有新的主題。",
|
||||
"no_popular_topics": "最近沒有受歡迎的主題。",
|
||||
"there-is-a-new-topic": "There is a new topic.",
|
||||
"there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.",
|
||||
"there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.",
|
||||
"there-are-new-topics": "There are %1 new topics.",
|
||||
"there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.",
|
||||
"there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.",
|
||||
"there-is-a-new-post": "There is a new post.",
|
||||
"there-are-new-posts": "There are %1 new posts.",
|
||||
"there-is-a-new-topic": "有一篇新主題",
|
||||
"there-is-a-new-topic-and-a-new-post": "有一篇新主題與一篇新張貼",
|
||||
"there-is-a-new-topic-and-new-posts": "有一篇新主題與 %1 篇新張貼",
|
||||
"there-are-new-topics": "有 %1 篇新主題",
|
||||
"there-are-new-topics-and-a-new-post": "有 %1 篇新主題與一篇新張貼",
|
||||
"there-are-new-topics-and-new-posts": "有 %1 篇新主題與 %2 篇新張貼",
|
||||
"there-is-a-new-post": "有一篇新張貼",
|
||||
"there-are-new-posts": "有 %1 篇新張貼",
|
||||
"click-here-to-reload": "點擊這裡進行重整。"
|
||||
}
|
||||
@@ -15,5 +15,5 @@
|
||||
"alternative_registration": "其他註冊方式",
|
||||
"terms_of_use": "使用條款",
|
||||
"agree_to_terms_of_use": "同意遵守使用條款",
|
||||
"registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator."
|
||||
"registration-added-to-queue": "您的註冊已經被加入到審核序列中。您將會在管理者批準後收到一封電子郵件。"
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"enter_email_address": "輸入電子郵件地址",
|
||||
"password_reset_sent": "密碼重設郵件已發送。",
|
||||
"invalid_email": "無效的電子郵件 / 電子郵件不存在!",
|
||||
"password_too_short": "The password entered is too short, please pick a different password.",
|
||||
"passwords_do_not_match": "The two passwords you've entered do not match.",
|
||||
"password_expired": "Your password has expired, please choose a new password"
|
||||
"password_too_short": "輸入的密碼太短,請使用另一個不同的密碼",
|
||||
"passwords_do_not_match": "你已經輸入的兩個密碼不一樣",
|
||||
"password_expired": "你的密碼已過期,請選擇一組新密碼"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"results_matching": "有%1個跟\"%2\"相符的結果(%3秒)",
|
||||
"no-matches": "沒有找到相符的主題",
|
||||
"advanced-search": "Advanced Search",
|
||||
"advanced-search": "進階搜尋",
|
||||
"in": "在",
|
||||
"titles": "標題",
|
||||
"titles-posts": "標題與發布",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"no_tag_topics": "沒有此標籤的主題。",
|
||||
"tags": "標籤",
|
||||
"enter_tags_here": "Enter tags here, between %1 and %2 characters each.",
|
||||
"enter_tags_here": "在這裡輸入標籤,每個介於 %1 到 %2 字元。 ",
|
||||
"enter_tags_here_short": "輸入標籤...",
|
||||
"no_tags": "還沒有標籤呢。"
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"notify_me": "該主題有新回覆時通知我",
|
||||
"quote": "引用",
|
||||
"reply": "回覆",
|
||||
"reply-as-topic": "Reply as topic",
|
||||
"reply-as-topic": "回復為另一個新主題",
|
||||
"guest-login-reply": "登入以回覆",
|
||||
"edit": "編輯",
|
||||
"delete": "刪除",
|
||||
@@ -26,7 +26,7 @@
|
||||
"tools": "工具",
|
||||
"flag": "檢舉",
|
||||
"locked": "已鎖定",
|
||||
"bookmark_instructions": "Click here to return to the last read post in this thread.",
|
||||
"bookmark_instructions": "點擊這裡返回到這個討論串的最後一篇張貼文",
|
||||
"flag_title": "檢舉這篇文章, 交給仲裁者來審閱.",
|
||||
"flag_success": "這文章已經被檢舉要求仲裁.",
|
||||
"deleted_message": "此主題已被刪除。只有具有主題管理權限的用戶才能看到它。",
|
||||
@@ -34,8 +34,8 @@
|
||||
"not_following_topic.message": "有人貼文回覆主題時, 你將不會收到通知.",
|
||||
"login_to_subscribe": "請先註冊或登錄, 才可訂閱此主題.",
|
||||
"markAsUnreadForAll.success": "將全部的主題設為未讀.",
|
||||
"mark_unread": "Mark unread",
|
||||
"mark_unread.success": "Topic marked as unread.",
|
||||
"mark_unread": "標為未讀",
|
||||
"mark_unread.success": "標記主題為未讀",
|
||||
"watch": "關注",
|
||||
"unwatch": "取消關注",
|
||||
"watch.title": "當主題有新回覆時將收到通知",
|
||||
@@ -51,7 +51,7 @@
|
||||
"thread_tools.move_all": "移動全部",
|
||||
"thread_tools.fork": "Fork 主題",
|
||||
"thread_tools.delete": "刪除主題",
|
||||
"thread_tools.delete-posts": "Delete Posts",
|
||||
"thread_tools.delete-posts": "刪除張貼",
|
||||
"thread_tools.delete_confirm": "你確定要刪除這個主題?",
|
||||
"thread_tools.restore": "還原刪除的主題",
|
||||
"thread_tools.restore_confirm": "你確定你要恢復這個主題嗎?",
|
||||
@@ -65,9 +65,9 @@
|
||||
"disabled_categories_note": "停用的版面為灰色",
|
||||
"confirm_move": "移動",
|
||||
"confirm_fork": "作為主題",
|
||||
"favourite": "Bookmark",
|
||||
"favourites": "Bookmarks",
|
||||
"favourites.has_no_favourites": "You haven't bookmarked any posts yet.",
|
||||
"favourite": "書籤",
|
||||
"favourites": "書籤",
|
||||
"favourites.has_no_favourites": "你尚未將任何張貼加入書籤",
|
||||
"loading_more_posts": "載入更多文章",
|
||||
"move_topic": "移動主題",
|
||||
"move_topics": "移動主題",
|
||||
@@ -78,7 +78,7 @@
|
||||
"fork_topic_instruction": "點擊要作為主題的文章",
|
||||
"fork_no_pids": "尚未選擇文章!",
|
||||
"fork_success": "成功分叉成新的主題!點擊這裡進入新的主題。",
|
||||
"delete_posts_instruction": "Click the posts you want to delete/purge",
|
||||
"delete_posts_instruction": "點擊你想要刪除/清除的張貼",
|
||||
"composer.title_placeholder": "輸入標題...",
|
||||
"composer.handle_placeholder": "名字",
|
||||
"composer.discard": "放棄",
|
||||
@@ -101,12 +101,12 @@
|
||||
"newest_to_oldest": "從新到舊",
|
||||
"most_votes": "得票最多",
|
||||
"most_posts": "最多post",
|
||||
"stale.title": "Create new topic instead?",
|
||||
"stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?",
|
||||
"stale.create": "Create a new topic",
|
||||
"stale.reply_anyway": "Reply to this topic anyway",
|
||||
"link_back": "Re: [%1](%2)",
|
||||
"spam": "Spam",
|
||||
"offensive": "Offensive",
|
||||
"custom-flag-reason": "Enter a flagging reason"
|
||||
"stale.title": "改為建立新的主題?",
|
||||
"stale.warning": "你正回覆的主題是非常舊的一篇。你想要改為建立一個新主題,然後參考到這篇你回覆的?",
|
||||
"stale.create": "建立新主題",
|
||||
"stale.reply_anyway": "無論如何都回覆這個主題",
|
||||
"link_back": "回覆: [%1](%2)",
|
||||
"spam": "灌水",
|
||||
"offensive": "攻擊",
|
||||
"custom-flag-reason": "輸入標記的理由"
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
"mark_as_read": "標記成已讀",
|
||||
"selected": "已選擇",
|
||||
"all": "全部",
|
||||
"all_categories": "All categories",
|
||||
"all_categories": "所有類別",
|
||||
"topics_marked_as_read.success": "標記主題成已讀!",
|
||||
"all-topics": "All Topics",
|
||||
"new-topics": "New Topics",
|
||||
"watched-topics": "Watched Topics"
|
||||
"all-topics": "所有主題",
|
||||
"new-topics": "新主題",
|
||||
"watched-topics": "已觀看主題"
|
||||
}
|
||||
@@ -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": "檔案上傳中...",
|
||||
"select-file-to-upload": "選擇要上傳的檔案!",
|
||||
"upload-success": "檔案已成功上傳!",
|
||||
"maximum-file-size": "最大大小 %1 kb"
|
||||
}
|
||||
@@ -6,15 +6,15 @@
|
||||
"enter_username": "輸入想找的使用者帳號",
|
||||
"load_more": "載入更多",
|
||||
"users-found-search-took": "發現 %1 用戶!搜尋只用 %2 秒。",
|
||||
"filter-by": "Filter By",
|
||||
"filter-by": "過濾依照",
|
||||
"online-only": "線上僅有",
|
||||
"invite": "Invite",
|
||||
"invitation-email-sent": "An invitation email has been sent to %1",
|
||||
"user_list": "User List",
|
||||
"recent_topics": "Recent Topics",
|
||||
"popular_topics": "Popular Topics",
|
||||
"unread_topics": "Unread Topics",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags",
|
||||
"no-users-found": "No users found!"
|
||||
"invite": "邀請",
|
||||
"invitation-email-sent": "所有邀請Email已經被寄送到 %1",
|
||||
"user_list": "用戶列表",
|
||||
"recent_topics": "最新的主題",
|
||||
"popular_topics": "受歡迎的主題",
|
||||
"unread_topics": "未讀的主題",
|
||||
"categories": "類別",
|
||||
"tags": "標籤",
|
||||
"no-users-found": "沒有找到用戶!"
|
||||
}
|
||||
@@ -63,6 +63,10 @@ $(document).ready(function() {
|
||||
|
||||
url = ajaxify.start(url, quiet);
|
||||
|
||||
if (!window.location.pathname.match(/\/(403|404)$/g)) {
|
||||
app.previousUrl = window.location.href;
|
||||
}
|
||||
|
||||
$('body').removeClass(ajaxify.data.bodyClass);
|
||||
$('#footer, #content').removeClass('hide').addClass('ajaxifying');
|
||||
|
||||
@@ -85,9 +89,10 @@ $(document).ready(function() {
|
||||
|
||||
ajaxify.handleRedirects = function(url) {
|
||||
url = ajaxify.removeRelativePath(url.replace(/\/$/, '')).toLowerCase();
|
||||
var isAdminRoute = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0;
|
||||
var isClientToAdmin = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0;
|
||||
var isAdminToClient = !url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') === 0;
|
||||
var uploadsOrApi = url.startsWith('uploads') || url.startsWith('api');
|
||||
if (isAdminRoute || uploadsOrApi) {
|
||||
if (isClientToAdmin || isAdminToClient || uploadsOrApi) {
|
||||
window.open(RELATIVE_PATH + '/' + url, '_top');
|
||||
return true;
|
||||
}
|
||||
@@ -100,10 +105,6 @@ $(document).ready(function() {
|
||||
|
||||
$(window).trigger('action:ajaxify.start', {url: url});
|
||||
|
||||
if (!window.location.pathname.match(/\/(403|404)$/g)) {
|
||||
app.previousUrl = window.location.href;
|
||||
}
|
||||
|
||||
ajaxify.currentPage = url.split(/[?#]/)[0];
|
||||
if (window.history && window.history.pushState) {
|
||||
window.history[!quiet ? 'pushState' : 'replaceState']({
|
||||
@@ -136,7 +137,8 @@ $(document).ready(function() {
|
||||
} else if (status === 401) {
|
||||
app.alertError('[[global:please_log_in]]');
|
||||
app.previousUrl = url;
|
||||
return ajaxify.go('login');
|
||||
window.location.href = config.relative_path + '/login';
|
||||
return;
|
||||
} else if (status === 302 || status === 308) {
|
||||
if (data.responseJSON.external) {
|
||||
window.location.href = data.responseJSON.external;
|
||||
|
||||
@@ -31,9 +31,13 @@ define('forum/login', ['csrf', 'translator'], function(csrf, translator) {
|
||||
window.location.href = data + '?loggedin';
|
||||
},
|
||||
error: function(data, status) {
|
||||
errorEl.find('p').translateText(data.responseText);
|
||||
errorEl.show();
|
||||
submitEl.removeClass('disabled');
|
||||
if (data.status === 403 && data.statusText === 'Forbidden') {
|
||||
window.location.href = config.relative_path + '/login?error=csrf-invalid';
|
||||
} else {
|
||||
errorEl.find('p').translateText(data.responseText);
|
||||
errorEl.show();
|
||||
submitEl.removeClass('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -45,7 +49,12 @@ define('forum/login', ['csrf', 'translator'], function(csrf, translator) {
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#content #username').focus();
|
||||
if ($('#content #username').attr('readonly')) {
|
||||
$('#content #password').focus();
|
||||
} else {
|
||||
$('#content #username').focus();
|
||||
}
|
||||
|
||||
|
||||
// Add "returnTo" data if present
|
||||
if (app.previousUrl) {
|
||||
|
||||
@@ -99,9 +99,13 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) {
|
||||
},
|
||||
error: function(data) {
|
||||
translator.translate(data.responseText, config.defaultLang, function(translated) {
|
||||
errorEl.find('p').text(translated);
|
||||
errorEl.removeClass('hidden');
|
||||
registerBtn.removeClass('disabled');
|
||||
if (data.status === 403 && data.statusText === 'Forbidden') {
|
||||
window.location.href = config.relative_path + '/register?error=csrf-invalid';
|
||||
} else {
|
||||
errorEl.find('p').text(translated);
|
||||
errorEl.removeClass('hidden');
|
||||
registerBtn.removeClass('disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,7 +55,8 @@ define('forum/topic/fork', ['components', 'postSelect'], function(components, po
|
||||
forkCommit.attr('disabled', true);
|
||||
socket.emit('topics.createTopicFromPosts', {
|
||||
title: forkModal.find('#fork-title').val(),
|
||||
pids: postSelect.pids
|
||||
pids: postSelect.pids,
|
||||
fromTid: ajaxify.data.tid
|
||||
}, function(err, newTopic) {
|
||||
function fadeOutAndRemove(pid) {
|
||||
components.get('post', 'pid', pid).fadeOut(500, function() {
|
||||
|
||||
@@ -336,8 +336,9 @@ define('forum/topic/posts', [
|
||||
Posts.showBottomPostBar = function() {
|
||||
var mainPost = components.get('post', 'index', 0);
|
||||
var posts = $('[component="post"]');
|
||||
if (!!mainPost.length && posts.length > 1 && $('.post-bar').length < 2) {
|
||||
if (!!mainPost.length && posts.length > 1 && $('.post-bar').length < 2 && $('.post-bar-placeholder').length) {
|
||||
$('.post-bar').clone().appendTo(mainPost);
|
||||
$('.post-bar-placeholder').remove();
|
||||
} else if (mainPost.length && posts.length < 2) {
|
||||
mainPost.find('.post-bar').remove();
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ define('forum/topic/threadTools', [
|
||||
|
||||
components.get('topic/lock').toggleClass('hidden', data.isLocked);
|
||||
components.get('topic/unlock').toggleClass('hidden', !data.isLocked);
|
||||
components.get('topic/reply').toggleClass('hidden', isLocked);
|
||||
components.get('topic/reply/container').toggleClass('hidden', isLocked);
|
||||
components.get('topic/reply/locked').toggleClass('hidden', !isLocked);
|
||||
|
||||
threadEl.find('[component="post/reply"], [component="post/quote"], [component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked);
|
||||
|
||||
@@ -103,7 +103,9 @@
|
||||
helpers.generateChildrenCategories = function(category) {
|
||||
var html = '';
|
||||
var relative_path = (typeof config !== 'undefined' ? config.relative_path : require('nconf').get('relative_path'));
|
||||
|
||||
if (!category || !category.children) {
|
||||
return html;
|
||||
}
|
||||
category.children.forEach(function(child) {
|
||||
if (!child) {
|
||||
return;
|
||||
|
||||
@@ -84,14 +84,11 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound,
|
||||
payload.message = notifData.bodyShort;
|
||||
payload.type = 'info';
|
||||
payload.clickfn = function() {
|
||||
socket.emit('notifications.generatePath', notifData.nid, function(err, path) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
if (path) {
|
||||
ajaxify.go(path);
|
||||
}
|
||||
});
|
||||
if (notifData.path.startsWith('http') && notifData.path.startsWith('https')) {
|
||||
window.location.href = notifData.path;
|
||||
} else {
|
||||
window.location.href = window.location.protocol + '//' + window.location.host + config.relative_path + notifData.path;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
payload.message = '[[notifications:you_have_unread_notifications]]';
|
||||
@@ -104,13 +101,13 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound,
|
||||
if (ajaxify.currentPage === 'notifications') {
|
||||
ajaxify.refresh();
|
||||
}
|
||||
|
||||
|
||||
if (!unreadNotifs[notifData.nid]) {
|
||||
incrementNotifCount(1);
|
||||
|
||||
sound.play('notification');
|
||||
unreadNotifs[notifData.nid] = true;
|
||||
}
|
||||
unreadNotifs[notifData.nid] = true;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('event:notifications.updateCount', function(count) {
|
||||
|
||||
@@ -23,7 +23,7 @@ define('uploader', ['csrf', 'translator'], function(csrf, translator) {
|
||||
title: data.title || '[[global:upload_file]]',
|
||||
description: data.description || '',
|
||||
button: data.button || '[[global:upload]]',
|
||||
accept: data.accept ? data.accept.replace(/,/g, ',') : ''
|
||||
accept: data.accept ? data.accept.replace(/,/g, ', ') : ''
|
||||
}, function(uploadModal) {
|
||||
uploadModal = $(uploadModal);
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
$.get(RELATIVE_PATH + '/api/widgets/render' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), {
|
||||
locations: locations,
|
||||
template: template + '.tpl',
|
||||
url: url
|
||||
url: url,
|
||||
isMobile: utils.isMobile()
|
||||
}, function(renderedAreas) {
|
||||
for (var x=0; x<renderedAreas.length; ++x) {
|
||||
var renderedWidgets = renderedAreas[x].widgets,
|
||||
|
||||
@@ -54,7 +54,7 @@ profileController.get = function(req, res, callback) {
|
||||
},
|
||||
aboutme: function(next) {
|
||||
if (userData.aboutme) {
|
||||
plugins.fireHook('filter:parse.raw', userData.aboutme, next);
|
||||
plugins.fireHook('filter:parse.aboutme', userData.aboutme, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ var fs = require('fs'),
|
||||
image = require('../../image'),
|
||||
plugins = require('../../plugins');
|
||||
|
||||
var allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml'];
|
||||
|
||||
var uploadsController = {};
|
||||
|
||||
uploadsController.uploadCategoryPicture = function(req, res, next) {
|
||||
var uploadedFile = req.files.files[0];
|
||||
var allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/svg+xml'],
|
||||
params = null;
|
||||
var params = null;
|
||||
|
||||
try {
|
||||
params = JSON.parse(req.body.params);
|
||||
@@ -28,7 +28,7 @@ uploadsController.uploadCategoryPicture = function(req, res, next) {
|
||||
return next(e);
|
||||
}
|
||||
|
||||
if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
|
||||
if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) {
|
||||
var filename = 'category-' + params.cid + path.extname(uploadedFile.name);
|
||||
uploadImage(filename, 'category', uploadedFile, req, res, next);
|
||||
}
|
||||
@@ -126,8 +126,8 @@ uploadsController.uploadDefaultAvatar = function(req, res, next) {
|
||||
|
||||
function upload(name, req, res, next) {
|
||||
var uploadedFile = req.files.files[0];
|
||||
var allowedTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif'];
|
||||
if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
|
||||
|
||||
if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) {
|
||||
var filename = name + path.extname(uploadedFile.name);
|
||||
uploadImage(filename, 'system', uploadedFile, req, res, next);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ apiController.renderWidgets = function(req, res, next) {
|
||||
template: areas.template,
|
||||
url: areas.url,
|
||||
locations: areas.locations,
|
||||
isMobile: req.query.isMobile === 'true'
|
||||
},
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -208,6 +208,8 @@ authenticationController.onSuccessfulLogin = function(req, uid, callback) {
|
||||
var uuid = utils.generateUUID();
|
||||
req.session.meta = {};
|
||||
|
||||
delete req.session.forceLogin;
|
||||
|
||||
// Associate IP used during login with user account
|
||||
user.logIP(uid, req.ip);
|
||||
req.session.meta.ip = req.ip;
|
||||
@@ -308,8 +310,6 @@ authenticationController.logout = function(req, res, next) {
|
||||
|
||||
user.setUserField(uid, 'lastonline', Date.now() - 300000);
|
||||
|
||||
// action:user.loggedOut deprecated in > v0.9.3
|
||||
plugins.fireHook('action:user.loggedOut', {req: req, res: res, uid: uid});
|
||||
plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function() {
|
||||
res.status(200).send('');
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
var nconf = require('nconf'),
|
||||
async = require('async'),
|
||||
validator = require('validator'),
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var validator = require('validator');
|
||||
|
||||
translator = require('../../public/src/modules/translator'),
|
||||
categories = require('../categories'),
|
||||
plugins = require('../plugins'),
|
||||
meta = require('../meta');
|
||||
var translator = require('../../public/src/modules/translator');
|
||||
var categories = require('../categories');
|
||||
var plugins = require('../plugins');
|
||||
var meta = require('../meta');
|
||||
|
||||
var helpers = {};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ var helpers = require('./helpers');
|
||||
|
||||
var Controllers = {
|
||||
topics: require('./topics'),
|
||||
posts: require('./posts'),
|
||||
categories: require('./categories'),
|
||||
category: require('./category'),
|
||||
unread: require('./unread'),
|
||||
@@ -95,17 +96,24 @@ Controllers.reset = function(req, res, next) {
|
||||
};
|
||||
|
||||
Controllers.login = function(req, res, next) {
|
||||
var data = {},
|
||||
loginStrategies = require('../routes/authentication').getLoginStrategies(),
|
||||
registrationType = meta.config.registrationType || 'normal';
|
||||
var data = {};
|
||||
var loginStrategies = require('../routes/authentication').getLoginStrategies();
|
||||
var registrationType = meta.config.registrationType || 'normal';
|
||||
|
||||
var allowLoginWith = (meta.config.allowLoginWith || 'username-email');
|
||||
|
||||
var errorText;
|
||||
if (req.query.error === 'csrf-invalid') {
|
||||
errorText = '[[error:csrf-invalid]]';
|
||||
}
|
||||
|
||||
data.alternate_logins = loginStrategies.length > 0;
|
||||
data.authentication = loginStrategies;
|
||||
data.allowLocalLogin = parseInt(meta.config.allowLocalLogin, 10) === 1 || parseInt(req.query.local, 10) === 1;
|
||||
data.allowRegistration = registrationType === 'normal' || registrationType === 'admin-approval';
|
||||
data.allowLoginWith = '[[login:' + (meta.config.allowLoginWith || 'username-email') + ']]';
|
||||
data.allowLoginWith = '[[login:' + allowLoginWith + ']]';
|
||||
data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:login]]'}]);
|
||||
data.error = req.flash('error')[0];
|
||||
data.error = req.flash('error')[0] || errorText;
|
||||
data.title = '[[pages:login]]';
|
||||
|
||||
if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) {
|
||||
@@ -113,8 +121,18 @@ Controllers.login = function(req, res, next) {
|
||||
external: data.authentication[0].url
|
||||
});
|
||||
}
|
||||
if (req.uid) {
|
||||
user.getUserFields(req.uid, ['username', 'email'], function(err, user) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
data.username = allowLoginWith === 'email' ? user.email : user.username;
|
||||
res.render('login', data);
|
||||
});
|
||||
} else {
|
||||
res.render('login', data);
|
||||
}
|
||||
|
||||
res.render('login', data);
|
||||
};
|
||||
|
||||
Controllers.register = function(req, res, next) {
|
||||
@@ -124,6 +142,11 @@ Controllers.register = function(req, res, next) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var errorText;
|
||||
if (req.query.error === 'csrf-invalid') {
|
||||
errorText = '[[error:csrf-invalid]]';
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function(next) {
|
||||
if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') {
|
||||
@@ -153,7 +176,7 @@ Controllers.register = function(req, res, next) {
|
||||
data.termsOfUse = termsOfUse.postData.content;
|
||||
data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]);
|
||||
data.regFormEntry = [];
|
||||
data.error = req.flash('error')[0];
|
||||
data.error = req.flash('error')[0] || errorText;
|
||||
data.title = '[[pages:register]]';
|
||||
|
||||
res.render('register', data);
|
||||
|
||||
24
src/controllers/posts.js
Normal file
24
src/controllers/posts.js
Normal file
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
|
||||
var posts = require('../posts');
|
||||
var helpers = require('./helpers');
|
||||
|
||||
var postsController = {};
|
||||
|
||||
postsController.redirectToPost = function(req, res, callback) {
|
||||
var pid = parseInt(req.params.pid, 10);
|
||||
if (!pid) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
posts.generatePostPath(pid, req.uid, function(err, path) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
helpers.redirect(res, path);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports = postsController;
|
||||
@@ -83,7 +83,7 @@ function resizeImage(fileObj, callback) {
|
||||
function(next) {
|
||||
fullPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), '..', fileObj.url);
|
||||
|
||||
image.load(fullPath, next);
|
||||
image.size(fullPath, next);
|
||||
},
|
||||
function (imageData, next) {
|
||||
if (imageData.width < parseInt(meta.config.maximumImageWidth, 10) || 760) {
|
||||
@@ -185,6 +185,9 @@ function uploadFile(uid, uploadedFile, callback) {
|
||||
if (meta.config.hasOwnProperty('allowedFileExtensions')) {
|
||||
var allowed = file.allowedExtensions();
|
||||
var extension = path.extname(uploadedFile.name);
|
||||
if (!extension) {
|
||||
extension = '.' + mime.extension(uploadedFile.type);
|
||||
}
|
||||
if (allowed.length > 0 && allowed.indexOf(extension) === -1) {
|
||||
return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]'));
|
||||
}
|
||||
@@ -195,7 +198,7 @@ function uploadFile(uid, uploadedFile, callback) {
|
||||
|
||||
function saveFileToLocal(uploadedFile, callback) {
|
||||
var extension = path.extname(uploadedFile.name);
|
||||
if(!extension) {
|
||||
if (!extension) {
|
||||
extension = '.' + mime.extension(uploadedFile.type);
|
||||
}
|
||||
|
||||
|
||||
18
src/image.js
18
src/image.js
@@ -94,11 +94,19 @@ image.normalise = function(path, extension, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
image.load = function(path, callback) {
|
||||
new Jimp(path, function(err, data) {
|
||||
callback(err, data ? data.bitmap : null);
|
||||
});
|
||||
};
|
||||
image.size = function(path, callback) {
|
||||
if (plugins.hasListeners('filter:image.size')) {
|
||||
plugins.fireHook('filter:image.size', {
|
||||
path: path,
|
||||
}, function(err, image) {
|
||||
callback(err, image);
|
||||
});
|
||||
} else {
|
||||
new Jimp(path, function(err, data) {
|
||||
callback(err, data ? data.bitmap : null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
image.convertImageToBase64 = function(path, callback) {
|
||||
fs.readFile(path, function(err, data) {
|
||||
|
||||
@@ -55,7 +55,8 @@ module.exports = function(Messaging) {
|
||||
message = {
|
||||
content: content,
|
||||
timestamp: timestamp,
|
||||
fromuid: fromuid
|
||||
fromuid: fromuid,
|
||||
roomId: roomId
|
||||
};
|
||||
|
||||
plugins.fireHook('filter:messaging.save', message, next);
|
||||
|
||||
@@ -135,4 +135,4 @@ module.exports = function(Meta) {
|
||||
db.deleteObjectField('config', field);
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
38
src/middleware/cls.js
Normal file
38
src/middleware/cls.js
Normal file
@@ -0,0 +1,38 @@
|
||||
var path = require('path');
|
||||
var sockets = require('path');
|
||||
var websockets = require('../socket.io/');
|
||||
var continuationLocalStorage = require('continuation-local-storage');
|
||||
var APP_NAMESPACE = require(path.join(__dirname, '../../package.json')).name;
|
||||
var namespace = continuationLocalStorage.createNamespace(APP_NAMESPACE);
|
||||
|
||||
(function(cls) {
|
||||
cls.http = function (req, res, next) {
|
||||
namespace.run(function() {
|
||||
namespace.set('request', req);
|
||||
next && next();
|
||||
});
|
||||
};
|
||||
|
||||
cls.socket = function (socket, payload, event, next) {
|
||||
namespace.run(function() {
|
||||
namespace.set('request', websockets.reqFromSocket(socket, payload, event));
|
||||
next && next();
|
||||
});
|
||||
};
|
||||
|
||||
cls.get = function (key) {
|
||||
return namespace.get(key);
|
||||
};
|
||||
|
||||
cls.set = function (key, value) {
|
||||
return namespace.set(key, value);
|
||||
};
|
||||
|
||||
cls.setItem = cls.set;
|
||||
cls.getItem = cls.set;
|
||||
cls.namespace = namespace;
|
||||
cls.continuationLocalStorage = continuationLocalStorage;
|
||||
|
||||
})(exports);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ var meta = require('../meta'),
|
||||
compression = require('compression'),
|
||||
favicon = require('serve-favicon'),
|
||||
session = require('express-session'),
|
||||
cls = require('./cls'),
|
||||
useragent = require('express-useragent');
|
||||
|
||||
|
||||
@@ -73,6 +74,7 @@ module.exports = function(app) {
|
||||
|
||||
app.use(middleware.addHeaders);
|
||||
app.use(middleware.processRender);
|
||||
app.use(cls.http);
|
||||
auth.initialize(app, middleware);
|
||||
|
||||
return middleware;
|
||||
|
||||
@@ -87,7 +87,9 @@ middleware.addHeaders = function (req, res, next) {
|
||||
headers = _.pick(headers, Boolean); // Remove falsy headers
|
||||
|
||||
for(var key in headers) {
|
||||
res.setHeader(key, headers[key]);
|
||||
if (headers.hasOwnProperty(key)) {
|
||||
res.setHeader(key, headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
@@ -103,6 +105,10 @@ middleware.pluginHooks = function(req, res, next) {
|
||||
};
|
||||
|
||||
middleware.redirectToAccountIfLoggedIn = function(req, res, next) {
|
||||
if (req.session.forceLogin) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
return next();
|
||||
}
|
||||
@@ -159,16 +165,49 @@ middleware.checkAccountPermissions = function(req, res, next) {
|
||||
});
|
||||
};
|
||||
|
||||
middleware.redirectUidToUserslug = function(req, res, next) {
|
||||
var uid = parseInt(req.params.uid, 10);
|
||||
if (!uid) {
|
||||
return next();
|
||||
}
|
||||
user.getUserField(uid, 'userslug', function(err, userslug) {
|
||||
if (err || !userslug) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var path = req.path.replace(/^\/api/, '')
|
||||
.replace('uid', 'user')
|
||||
.replace(uid, function() { return userslug; });
|
||||
controllers.helpers.redirect(res, path);
|
||||
});
|
||||
};
|
||||
|
||||
middleware.isAdmin = function(req, res, next) {
|
||||
if (!req.uid) {
|
||||
return controllers.helpers.notAllowed(req, res);
|
||||
}
|
||||
|
||||
user.isAdministrator(req.uid, function (err, isAdmin) {
|
||||
if (err || isAdmin) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
var loginTime = req.session.meta ? req.session.meta.datetime : 0;
|
||||
if (loginTime && parseInt(loginTime, 10) > Date.now() - 3600000) {
|
||||
return next();
|
||||
}
|
||||
|
||||
req.session.returnTo = nconf.get('relative_path') + req.path.replace(/^\/api/, '');
|
||||
req.session.forceLogin = 1;
|
||||
if (res.locals.isAPI) {
|
||||
res.status(401).json({});
|
||||
} else {
|
||||
res.redirect('/login');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.locals.isAPI) {
|
||||
return controllers.helpers.notAllowed(req, res);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ module.exports = function(middleware) {
|
||||
};
|
||||
|
||||
function buildBodyClass(req) {
|
||||
var clean = req.path.replace(/^\/api/, '').replace(/^\//, '');
|
||||
var clean = req.path.replace(/^\/api/, '').replace(/^\/|\/$/g, '');
|
||||
var parts = clean.split('/').slice(0, 3);
|
||||
parts.forEach(function(p, index) {
|
||||
parts[index] = index ? parts[0] + '-' + p : 'page-' + (p || 'home');
|
||||
|
||||
@@ -12,6 +12,7 @@ var User = require('./user');
|
||||
var groups = require('./groups');
|
||||
var meta = require('./meta');
|
||||
var plugins = require('./plugins');
|
||||
var utils = require('../public/src/utils');
|
||||
|
||||
(function(Notifications) {
|
||||
|
||||
@@ -45,6 +46,8 @@ var plugins = require('./plugins');
|
||||
return next(null, null);
|
||||
}
|
||||
|
||||
notification.datetimeISO = utils.toISOString(notification.datetime);
|
||||
|
||||
if (notification.bodyLong) {
|
||||
notification.bodyLong = S(notification.bodyLong).escapeHTML().s;
|
||||
}
|
||||
@@ -85,7 +88,7 @@ var plugins = require('./plugins');
|
||||
// Removes nids that have been pruned
|
||||
db.isSortedSetMembers('notifications', nids, function(err, exists) {
|
||||
if (err) {
|
||||
return callbacK(err);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
nids = nids.filter(function(notifId, idx) {
|
||||
|
||||
@@ -5,9 +5,29 @@ var winston = require('winston'),
|
||||
|
||||
module.exports = function(Plugins) {
|
||||
Plugins.deprecatedHooks = {
|
||||
'filter:user.delete': 'static:user.delete',
|
||||
'filter:user.custom_fields': null,
|
||||
'action:user.loggedOut': 'static:user.loggedOut'
|
||||
'filter:user.custom_fields': null // remove in v1.1.0
|
||||
};
|
||||
|
||||
Plugins.deprecatedHooksParams = {
|
||||
'action:homepage.get': '{req, res}',
|
||||
'filter:register.check': '{req, res}',
|
||||
'action:user.loggedOut': '{req, res}',
|
||||
'static:user.loggedOut': '{req, res}',
|
||||
'filter:categories.build': '{req, res}',
|
||||
'filter:category.build': '{req, res}',
|
||||
'filter:group.build': '{req, res}',
|
||||
'filter:register.build': '{req, res}',
|
||||
'filter:composer.build': '{req, res}',
|
||||
'filter:popular.build': '{req, res}',
|
||||
'filter:recent.build': '{req, res}',
|
||||
'filter:topic.build': '{req, res}',
|
||||
'filter:users.build': '{req, res}',
|
||||
'filter:admin.category.get': '{req, res}',
|
||||
'filter:middleware.renderHeader': '{req, res}',
|
||||
'filter:widget.render': '{req, res}',
|
||||
'filter:middleware.buildHeader': '{req, locals}',
|
||||
'action:middleware.pageView': '{req}',
|
||||
'action:meta.override404': '{req}'
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -29,12 +49,23 @@ module.exports = function(Plugins) {
|
||||
var method;
|
||||
|
||||
if (Object.keys(Plugins.deprecatedHooks).indexOf(data.hook) !== -1) {
|
||||
winston.warn('[plugins/' + id + '] Hook `' + data.hook + '` is deprecated, ' +
|
||||
winston.warn('[plugins/' + id + '] Hook `' + data.hook + '` is deprecated, ' +
|
||||
(Plugins.deprecatedHooks[data.hook] ?
|
||||
'please use `' + Plugins.deprecatedHooks[data.hook] + '` instead.' :
|
||||
'there is no alternative.'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// handle hook's startsWith, i.e. action:homepage.get
|
||||
var _parts = data.hook.split(':');
|
||||
_parts.pop();
|
||||
var _hook = _parts.join(':');
|
||||
if (Plugins.deprecatedHooksParams[_hook]) {
|
||||
winston.warn('[plugins/' + id + '] Hook `' + _hook + '` parameters: `' + Plugins.deprecatedHooksParams[_hook] + '`, are being deprecated, '
|
||||
+ 'all plugins should now use the `middleware/cls` module instead of hook\'s arguments to get a reference to the `req`, `res` and/or `socket` object(s) (from which you can get the current `uid` if you need to.) '
|
||||
+ '- for more info, visit https://docs.nodebb.org/en/latest/plugins/create.html#getting-a-reference-to-req-res-socket-and-uid-within-any-plugin-hook');
|
||||
delete Plugins.deprecatedHooksParams[_hook];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.hook && data.method) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
topics = require('../topics');
|
||||
var async = require('async');
|
||||
|
||||
var topics = require('../topics');
|
||||
var utils = require('../../public/src/utils');
|
||||
|
||||
module.exports = function(Posts) {
|
||||
|
||||
@@ -42,4 +44,45 @@ module.exports = function(Posts) {
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.generatePostPath = function (pid, uid, callback) {
|
||||
Posts.generatePostPaths([pid], uid, function(err, paths) {
|
||||
callback(err, Array.isArray(paths) && paths.length ? paths[0] : null);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.generatePostPaths = function (pids, uid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Posts.getPostsFields(pids, ['pid', 'tid'], next);
|
||||
},
|
||||
function (postData, next) {
|
||||
async.parallel({
|
||||
indices: function(next) {
|
||||
Posts.getPostIndices(postData, uid, next);
|
||||
},
|
||||
topics: function(next) {
|
||||
var tids = postData.map(function(post) {
|
||||
return post ? post.tid : null;
|
||||
});
|
||||
|
||||
topics.getTopicsFields(tids, ['slug'], next);
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
var paths = pids.map(function(pid, index) {
|
||||
var slug = results.topics[index] ? results.topics[index].slug : null;
|
||||
var postIndex = utils.isNumber(results.indices[index]) ? parseInt(results.indices[index], 10) + 1 : null;
|
||||
|
||||
if (slug && postIndex) {
|
||||
return '/topic/' + slug + '/' + postIndex;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
next(null, paths);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
};
|
||||
@@ -7,6 +7,8 @@ module.exports = function (app, middleware, controllers) {
|
||||
var middlewares = [middleware.checkGlobalPrivacySettings];
|
||||
var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions];
|
||||
|
||||
setupPageRoute(app, '/uid/:uid/:section?', middleware, [], middleware.redirectUidToUserslug);
|
||||
|
||||
setupPageRoute(app, '/user/:userslug', middleware, middlewares, controllers.accounts.profile.get);
|
||||
setupPageRoute(app, '/user/:userslug/following', middleware, middlewares, controllers.accounts.follow.getFollowing);
|
||||
setupPageRoute(app, '/user/:userslug/followers', middleware, middlewares, controllers.accounts.follow.getFollowers);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
"use strict";
|
||||
|
||||
var nconf = require('nconf'),
|
||||
path = require('path'),
|
||||
async = require('async'),
|
||||
winston = require('winston'),
|
||||
controllers = require('../controllers'),
|
||||
plugins = require('../plugins'),
|
||||
express = require('express'),
|
||||
validator = require('validator'),
|
||||
var nconf = require('nconf');
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var controllers = require('../controllers');
|
||||
var plugins = require('../plugins');
|
||||
var express = require('express');
|
||||
var validator = require('validator');
|
||||
|
||||
accountRoutes = require('./accounts'),
|
||||
var accountRoutes = require('./accounts');
|
||||
|
||||
metaRoutes = require('./meta'),
|
||||
apiRoutes = require('./api'),
|
||||
adminRoutes = require('./admin'),
|
||||
feedRoutes = require('./feeds'),
|
||||
pluginRoutes = require('./plugins'),
|
||||
authRoutes = require('./authentication'),
|
||||
helpers = require('./helpers');
|
||||
var metaRoutes = require('./meta');
|
||||
var apiRoutes = require('./api');
|
||||
var adminRoutes = require('./admin');
|
||||
var feedRoutes = require('./feeds');
|
||||
var pluginRoutes = require('./plugins');
|
||||
var authRoutes = require('./authentication');
|
||||
var helpers = require('./helpers');
|
||||
|
||||
var setupPageRoute = helpers.setupPageRoute;
|
||||
|
||||
@@ -46,6 +46,10 @@ function topicRoutes(app, middleware, controllers) {
|
||||
setupPageRoute(app, '/topic/:topic_id/:slug?', middleware, [], controllers.topics.get);
|
||||
}
|
||||
|
||||
function postRoutes(app, middleware, controllers) {
|
||||
setupPageRoute(app, '/post/:pid', middleware, [], controllers.posts.redirectToPost);
|
||||
}
|
||||
|
||||
function tagRoutes(app, middleware, controllers) {
|
||||
setupPageRoute(app, '/tags/:tag', middleware, [middleware.privateTagListing], controllers.tags.getTag);
|
||||
setupPageRoute(app, '/tags', middleware, [middleware.privateTagListing], controllers.tags.getTags);
|
||||
@@ -71,7 +75,6 @@ function userRoutes(app, middleware, controllers) {
|
||||
setupPageRoute(app, '/users/banned', middleware, middlewares, controllers.users.getBannedUsers);
|
||||
}
|
||||
|
||||
|
||||
function groupRoutes(app, middleware, controllers) {
|
||||
var middlewares = [middleware.checkGlobalPrivacySettings, middleware.exposeGroupName];
|
||||
|
||||
@@ -124,6 +127,7 @@ module.exports = function(app, middleware, hotswapIds) {
|
||||
|
||||
mainRoutes(router, middleware, controllers);
|
||||
topicRoutes(router, middleware, controllers);
|
||||
postRoutes(router, middleware, controllers);
|
||||
globalModRoutes(router, middleware, controllers);
|
||||
tagRoutes(router, middleware, controllers);
|
||||
categoryRoutes(router, middleware, controllers);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var S = require('string');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var websockets = require('./index');
|
||||
var user = require('../user');
|
||||
@@ -53,24 +52,24 @@ SocketHelpers.sendNotificationToPostOwner = function(pid, fromuid, notification)
|
||||
if (!pid || !fromuid || !notification) {
|
||||
return;
|
||||
}
|
||||
posts.getPostFields(pid, ['tid', 'uid', 'content'], function(err, postData) {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!postData.uid || fromuid === parseInt(postData.uid, 10)) {
|
||||
return;
|
||||
}
|
||||
|
||||
async.parallel({
|
||||
username: async.apply(user.getUserField, fromuid, 'username'),
|
||||
topicTitle: async.apply(topics.getTopicField, postData.tid, 'title'),
|
||||
postObj: async.apply(posts.parsePost, postData)
|
||||
}, function(err, results) {
|
||||
if (err) {
|
||||
fromuid = parseInt(fromuid, 10);
|
||||
var postData;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
posts.getPostFields(pid, ['tid', 'uid', 'content'], next);
|
||||
},
|
||||
function (_postData, next) {
|
||||
postData = _postData;
|
||||
if (!postData.uid || fromuid === parseInt(postData.uid, 10)) {
|
||||
return;
|
||||
}
|
||||
|
||||
async.parallel({
|
||||
username: async.apply(user.getUserField, fromuid, 'username'),
|
||||
topicTitle: async.apply(topics.getTopicField, postData.tid, 'title'),
|
||||
postObj: async.apply(posts.parsePost, postData)
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
var title = S(results.topicTitle).decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
@@ -78,16 +77,20 @@ SocketHelpers.sendNotificationToPostOwner = function(pid, fromuid, notification)
|
||||
bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: results.postObj.content,
|
||||
pid: pid,
|
||||
path: '/post/' + pid,
|
||||
nid: 'post:' + pid + ':uid:' + fromuid,
|
||||
from: fromuid,
|
||||
mergeId: notification + '|' + pid,
|
||||
topicTitle: results.topicTitle
|
||||
}, function(err, notification) {
|
||||
if (!err && notification) {
|
||||
notifications.push(notification, [postData.uid]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, next);
|
||||
}
|
||||
], function(err, notification) {
|
||||
if (err) {
|
||||
return winston.error(err);
|
||||
}
|
||||
if (notification) {
|
||||
notifications.push(notification, [postData.uid]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,27 +100,38 @@ SocketHelpers.sendNotificationToTopicOwner = function(tid, fromuid, notification
|
||||
return;
|
||||
}
|
||||
|
||||
async.parallel({
|
||||
username: async.apply(user.getUserField, fromuid, 'username'),
|
||||
topicData: async.apply(topics.getTopicFields, tid, ['uid', 'slug', 'title']),
|
||||
}, function(err, results) {
|
||||
if (err || fromuid === parseInt(results.topicData.uid, 10)) {
|
||||
return;
|
||||
}
|
||||
fromuid = parseInt(fromuid, 10);
|
||||
|
||||
var title = S(results.topicData.title).decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]',
|
||||
path: nconf.get('relative_path') + '/topic/' + results.topicData.slug,
|
||||
nid: 'tid:' + tid + ':uid:' + fromuid,
|
||||
from: fromuid
|
||||
}, function(err, notification) {
|
||||
if (!err && notification) {
|
||||
notifications.push(notification, [results.topicData.uid]);
|
||||
var ownerUid;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
username: async.apply(user.getUserField, fromuid, 'username'),
|
||||
topicData: async.apply(topics.getTopicFields, tid, ['uid', 'slug', 'title']),
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
if (fromuid === parseInt(results.topicData.uid, 10)) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
ownerUid = results.topicData.uid;
|
||||
var title = S(results.topicData.title).decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]',
|
||||
path: '/topic/' + results.topicData.slug,
|
||||
nid: 'tid:' + tid + ':uid:' + fromuid,
|
||||
from: fromuid
|
||||
}, next);
|
||||
}
|
||||
], function(err, notification) {
|
||||
if (err) {
|
||||
return winston.error(err);
|
||||
}
|
||||
if (notification && parseInt(ownerUid, 10)) {
|
||||
notifications.push(notification, [ownerUid]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -10,209 +10,224 @@ var winston = require('winston');
|
||||
var db = require('../database');
|
||||
var logger = require('../logger');
|
||||
var ratelimit = require('../middleware/ratelimit');
|
||||
var cls = require('../middleware/cls');
|
||||
|
||||
var Sockets = {};
|
||||
var Namespaces = {};
|
||||
(function(Sockets) {
|
||||
var Namespaces = {};
|
||||
var io;
|
||||
|
||||
var io;
|
||||
Sockets.init = function (server) {
|
||||
requireModules();
|
||||
|
||||
Sockets.init = function(server) {
|
||||
requireModules();
|
||||
io = new SocketIO({
|
||||
path: nconf.get('relative_path') + '/socket.io'
|
||||
});
|
||||
|
||||
io = new SocketIO({
|
||||
path: nconf.get('relative_path') + '/socket.io'
|
||||
});
|
||||
addRedisAdapter(io);
|
||||
|
||||
addRedisAdapter(io);
|
||||
io.use(socketioWildcard);
|
||||
io.use(authorize);
|
||||
|
||||
io.use(socketioWildcard);
|
||||
io.use(authorize);
|
||||
io.on('connection', onConnection);
|
||||
io.on('disconnect', onDisconnect);
|
||||
|
||||
io.on('connection', onConnection);
|
||||
io.listen(server, {
|
||||
transports: nconf.get('socket.io:transports')
|
||||
});
|
||||
|
||||
io.listen(server, {
|
||||
transports: nconf.get('socket.io:transports')
|
||||
});
|
||||
|
||||
Sockets.server = io;
|
||||
};
|
||||
|
||||
function onConnection(socket) {
|
||||
socket.ip = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress;
|
||||
|
||||
logger.io_one(socket, socket.uid);
|
||||
|
||||
onConnect(socket);
|
||||
|
||||
socket.on('*', function(payload) {
|
||||
onMessage(socket, payload);
|
||||
});
|
||||
}
|
||||
|
||||
function onConnect(socket) {
|
||||
if (socket.uid) {
|
||||
socket.join('uid_' + socket.uid);
|
||||
socket.join('online_users');
|
||||
} else {
|
||||
socket.join('online_guests');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onMessage(socket, payload) {
|
||||
if (!payload.data.length) {
|
||||
return winston.warn('[socket.io] Empty payload');
|
||||
}
|
||||
|
||||
var eventName = payload.data[0];
|
||||
var params = payload.data[1];
|
||||
var callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function() {};
|
||||
|
||||
if (!eventName) {
|
||||
return winston.warn('[socket.io] Empty method name');
|
||||
}
|
||||
|
||||
var parts = eventName.toString().split('.');
|
||||
var namespace = parts[0];
|
||||
var methodToCall = parts.reduce(function(prev, cur) {
|
||||
if (prev !== null && prev[cur]) {
|
||||
return prev[cur];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, Namespaces);
|
||||
|
||||
if(!methodToCall) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
winston.warn('[socket.io] Unrecognized message: ' + eventName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
socket.previousEvents = socket.previousEvents || [];
|
||||
socket.previousEvents.push(eventName);
|
||||
if (socket.previousEvents.length > 20) {
|
||||
socket.previousEvents.shift();
|
||||
}
|
||||
|
||||
if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
|
||||
winston.warn('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents);
|
||||
return socket.disconnect();
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
validateSession(socket, next);
|
||||
},
|
||||
function (next) {
|
||||
if (Namespaces[namespace].before) {
|
||||
Namespaces[namespace].before(socket, eventName, params, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
methodToCall(socket, params, next);
|
||||
}
|
||||
], function(err, result) {
|
||||
callback(err ? {message: err.message} : null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function requireModules() {
|
||||
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
|
||||
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist'
|
||||
];
|
||||
|
||||
modules.forEach(function(module) {
|
||||
Namespaces[module] = require('./' + module);
|
||||
});
|
||||
}
|
||||
|
||||
function validateSession(socket, callback) {
|
||||
var req = socket.request;
|
||||
if (!req.signedCookies || !req.signedCookies['express.sid']) {
|
||||
return callback(new Error('[[error:invalid-session]]'));
|
||||
}
|
||||
db.sessionStore.get(req.signedCookies['express.sid'], function(err, sessionData) {
|
||||
if (err || !sessionData) {
|
||||
return callback(err || new Error('[[error:invalid-session]]'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function authorize(socket, callback) {
|
||||
var request = socket.request;
|
||||
|
||||
if (!request) {
|
||||
return callback(new Error('[[error:not-authorized]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function(next) {
|
||||
cookieParser(request, {}, next);
|
||||
},
|
||||
function(next) {
|
||||
db.sessionStore.get(request.signedCookies['express.sid'], function(err, sessionData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (sessionData && sessionData.passport && sessionData.passport.user) {
|
||||
request.session = sessionData;
|
||||
socket.uid = parseInt(sessionData.passport.user, 10);
|
||||
} else {
|
||||
socket.uid = 0;
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
function addRedisAdapter(io) {
|
||||
if (nconf.get('redis')) {
|
||||
var redisAdapter = require('socket.io-redis');
|
||||
var redis = require('../database/redis');
|
||||
var pub = redis.connect({return_buffers: true});
|
||||
var sub = redis.connect({return_buffers: true});
|
||||
|
||||
io.adapter(redisAdapter({pubClient: pub, subClient: sub}));
|
||||
} else if (nconf.get('isCluster') === 'true') {
|
||||
winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.');
|
||||
}
|
||||
}
|
||||
|
||||
Sockets.in = function(room) {
|
||||
return io.in(room);
|
||||
};
|
||||
|
||||
Sockets.getUserSocketCount = function(uid) {
|
||||
if (!io) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var room = io.sockets.adapter.rooms['uid_' + uid];
|
||||
return room ? room.length : 0;
|
||||
};
|
||||
|
||||
|
||||
Sockets.reqFromSocket = function(socket) {
|
||||
var headers = socket.request.headers;
|
||||
var host = headers.host;
|
||||
var referer = headers.referer || '';
|
||||
|
||||
return {
|
||||
ip: headers['x-forwarded-for'] || socket.ip,
|
||||
host: host,
|
||||
protocol: socket.request.connection.encrypted ? 'https' : 'http',
|
||||
secure: !!socket.request.connection.encrypted,
|
||||
url: referer,
|
||||
path: referer.substr(referer.indexOf(host) + host.length),
|
||||
headers: headers
|
||||
Sockets.server = io;
|
||||
};
|
||||
};
|
||||
|
||||
function onConnection(socket) {
|
||||
socket.ip = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress;
|
||||
|
||||
logger.io_one(socket, socket.uid);
|
||||
|
||||
cls.socket(socket, null, 'connection', function () {
|
||||
onConnect(socket);
|
||||
});
|
||||
|
||||
socket.on('*', function (payload) {
|
||||
cls.socket(socket, payload, null, function () {
|
||||
onMessage(socket, payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onConnect(socket) {
|
||||
if (socket.uid) {
|
||||
socket.join('uid_' + socket.uid);
|
||||
socket.join('online_users');
|
||||
} else {
|
||||
socket.join('online_guests');
|
||||
}
|
||||
}
|
||||
|
||||
function onDisconnect(socket) {
|
||||
cls.socket(socket, null, 'disconnect', function () {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = Sockets;
|
||||
function onMessage(socket, payload) {
|
||||
if (!payload.data.length) {
|
||||
return winston.warn('[socket.io] Empty payload');
|
||||
}
|
||||
|
||||
var eventName = payload.data[0];
|
||||
var params = payload.data[1];
|
||||
var callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () {
|
||||
};
|
||||
|
||||
if (!eventName) {
|
||||
return winston.warn('[socket.io] Empty method name');
|
||||
}
|
||||
|
||||
var parts = eventName.toString().split('.');
|
||||
var namespace = parts[0];
|
||||
var methodToCall = parts.reduce(function (prev, cur) {
|
||||
if (prev !== null && prev[cur]) {
|
||||
return prev[cur];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, Namespaces);
|
||||
|
||||
if (!methodToCall) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
winston.warn('[socket.io] Unrecognized message: ' + eventName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
socket.previousEvents = socket.previousEvents || [];
|
||||
socket.previousEvents.push(eventName);
|
||||
if (socket.previousEvents.length > 20) {
|
||||
socket.previousEvents.shift();
|
||||
}
|
||||
|
||||
if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
|
||||
winston.warn('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents);
|
||||
return socket.disconnect();
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
validateSession(socket, next);
|
||||
},
|
||||
function (next) {
|
||||
if (Namespaces[namespace].before) {
|
||||
Namespaces[namespace].before(socket, eventName, params, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
methodToCall(socket, params, next);
|
||||
}
|
||||
], function (err, result) {
|
||||
callback(err ? {message: err.message} : null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function requireModules() {
|
||||
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
|
||||
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist'
|
||||
];
|
||||
|
||||
modules.forEach(function (module) {
|
||||
Namespaces[module] = require('./' + module);
|
||||
});
|
||||
}
|
||||
|
||||
function validateSession(socket, callback) {
|
||||
var req = socket.request;
|
||||
if (!req.signedCookies || !req.signedCookies['express.sid']) {
|
||||
return callback(new Error('[[error:invalid-session]]'));
|
||||
}
|
||||
db.sessionStore.get(req.signedCookies['express.sid'], function (err, sessionData) {
|
||||
if (err || !sessionData) {
|
||||
return callback(err || new Error('[[error:invalid-session]]'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function authorize(socket, callback) {
|
||||
var request = socket.request;
|
||||
|
||||
if (!request) {
|
||||
return callback(new Error('[[error:not-authorized]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
cookieParser(request, {}, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sessionStore.get(request.signedCookies['express.sid'], function (err, sessionData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (sessionData && sessionData.passport && sessionData.passport.user) {
|
||||
request.session = sessionData;
|
||||
socket.uid = parseInt(sessionData.passport.user, 10);
|
||||
} else {
|
||||
socket.uid = 0;
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
function addRedisAdapter(io) {
|
||||
if (nconf.get('redis')) {
|
||||
var redisAdapter = require('socket.io-redis');
|
||||
var redis = require('../database/redis');
|
||||
var pub = redis.connect({return_buffers: true});
|
||||
var sub = redis.connect({return_buffers: true});
|
||||
|
||||
io.adapter(redisAdapter({pubClient: pub, subClient: sub}));
|
||||
} else if (nconf.get('isCluster') === 'true') {
|
||||
winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.');
|
||||
}
|
||||
}
|
||||
|
||||
Sockets.in = function (room) {
|
||||
return io.in(room);
|
||||
};
|
||||
|
||||
Sockets.getUserSocketCount = function (uid) {
|
||||
if (!io) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var room = io.sockets.adapter.rooms['uid_' + uid];
|
||||
return room ? room.length : 0;
|
||||
};
|
||||
|
||||
|
||||
Sockets.reqFromSocket = function (socket, payload, event) {
|
||||
var headers = socket.request.headers;
|
||||
var host = headers.host;
|
||||
var referer = headers.referer || '';
|
||||
var data = ((payload || {}).data || []);
|
||||
|
||||
return {
|
||||
uid: socket.uid,
|
||||
params: data[1],
|
||||
method: event || data[0],
|
||||
body: payload,
|
||||
ip: headers['x-forwarded-for'] || socket.ip,
|
||||
host: host,
|
||||
protocol: socket.request.connection.encrypted ? 'https' : 'http',
|
||||
secure: !!socket.request.connection.encrypted,
|
||||
url: referer,
|
||||
path: referer.substr(referer.indexOf(host) + host.length),
|
||||
headers: headers
|
||||
};
|
||||
};
|
||||
|
||||
})(exports);
|
||||
@@ -56,28 +56,4 @@ SocketNotifs.markAllRead = function(socket, data, callback) {
|
||||
notifications.markAllRead(socket.uid, callback);
|
||||
};
|
||||
|
||||
SocketNotifs.generatePath = function(socket, nid, callback) {
|
||||
if (!socket.uid) {
|
||||
return callback(new Error('[[error:no-privileges]]'));;
|
||||
}
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
notifications.get(nid, next);
|
||||
},
|
||||
function (notification, next) {
|
||||
if (!notification) {
|
||||
return next(null, '');
|
||||
}
|
||||
user.notifications.generateNotificationPaths([notification], socket.uid, next);
|
||||
},
|
||||
function (notificationsData, next) {
|
||||
if (notificationsData && notificationsData.length) {
|
||||
next(null, notificationsData[0].path);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
module.exports = SocketNotifs;
|
||||
|
||||
@@ -90,6 +90,7 @@ module.exports = function(SocketPosts) {
|
||||
bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: post.content,
|
||||
pid: data.pid,
|
||||
path: '/post/' + data.pid,
|
||||
nid: 'post_flag:' + data.pid + ':uid:' + socket.uid,
|
||||
from: socket.uid,
|
||||
mergeId: 'notifications:user_flagged_post_in|' + data.pid,
|
||||
|
||||
@@ -69,7 +69,7 @@ SocketTopics.createTopicFromPosts = function(socket, data, callback) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
topics.createTopicFromPosts(socket.uid, data.title, data.pids, callback);
|
||||
topics.createTopicFromPosts(socket.uid, data.title, data.pids, data.fromTid, callback);
|
||||
};
|
||||
|
||||
SocketTopics.toggleFollow = function(socket, tid, callback) {
|
||||
|
||||
@@ -155,7 +155,7 @@ SocketUser.follow = function(socket, data, callback) {
|
||||
bodyShort: '[[notifications:user_started_following_you, ' + userData.username + ']]',
|
||||
nid: 'follow:' + data.uid + ':uid:' + socket.uid,
|
||||
from: socket.uid,
|
||||
path: '/user/' + userData.userslug,
|
||||
path: '/uid/' + socket.uid,
|
||||
mergeId: 'notifications:user_started_following_you'
|
||||
}, next);
|
||||
},
|
||||
|
||||
@@ -328,4 +328,88 @@ var social = require('./social');
|
||||
}
|
||||
};
|
||||
|
||||
Topics.getTopicBookmarks = function( tid, callback ){
|
||||
db.getSortedSetRangeWithScores(['tid:' + tid + ':bookmarks'], 0, -1, callback );
|
||||
};
|
||||
|
||||
Topics.updateTopicBookmarks = function( tid, pids, callback ){
|
||||
var maxIndex;
|
||||
var Posts = posts;
|
||||
async.waterfall([
|
||||
function(next){
|
||||
Topics.getPostCount( tid, next );
|
||||
},
|
||||
function(postcount, next){
|
||||
maxIndex = postcount;
|
||||
Topics.getTopicBookmarks( tid, next );
|
||||
},
|
||||
function(bookmarks, next){
|
||||
var uids = bookmarks.map( function( bookmark ){return bookmark.value});
|
||||
var forkedPosts = pids.map( function( pid ){ return { pid: pid, tid: tid }; } );
|
||||
var uidBookmark = new Object();
|
||||
var uidData = bookmarks.map(
|
||||
function( bookmark ){
|
||||
var u = new Object();
|
||||
u.uid = bookmark.value;
|
||||
u.bookmark = bookmark.score;
|
||||
return u;
|
||||
} );
|
||||
async.map(
|
||||
uidData,
|
||||
function( data, mapCallback ){
|
||||
Posts.getPostIndices(
|
||||
forkedPosts,
|
||||
data.uid,
|
||||
function( err, indices ){
|
||||
if( err ){
|
||||
callback( err );
|
||||
}
|
||||
data.postIndices = indices;
|
||||
mapCallback( null, data );
|
||||
} )
|
||||
},
|
||||
function( err, results ){
|
||||
if( err ){
|
||||
return callback();
|
||||
}
|
||||
async.map(
|
||||
results,
|
||||
function( data, mapCallback ){
|
||||
var uid = data.uid;
|
||||
var bookmark = data.bookmark;
|
||||
bookmark = bookmark < maxIndex ? bookmark : maxIndex;
|
||||
var postIndices = data.postIndices;
|
||||
var i;
|
||||
for( i = 0; i < postIndices.length && postIndices[i] < data.bookmark; ++i ){
|
||||
--bookmark;
|
||||
}
|
||||
|
||||
if( bookmark != data.bookmark ){
|
||||
mapCallback( null, { uid: uid, bookmark: bookmark } );
|
||||
}
|
||||
else{
|
||||
mapCallback( null, null );
|
||||
}
|
||||
},
|
||||
function( err, results ){
|
||||
async.map( results,
|
||||
function(ui, cb ){
|
||||
if( ui && ui.bookmark){
|
||||
Topics.setUserBookmark( tid, ui.uid, ui.bookmark, cb );
|
||||
}
|
||||
else{
|
||||
return cb( null, null );
|
||||
}
|
||||
},
|
||||
function( err, results ){
|
||||
next();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}],
|
||||
function( err, result ){ callback();} );
|
||||
};
|
||||
}(exports));
|
||||
|
||||
@@ -12,6 +12,7 @@ var notifications = require('../notifications');
|
||||
var privileges = require('../privileges');
|
||||
var meta = require('../meta');
|
||||
var emailer = require('../emailer');
|
||||
var plugins = require('../plugins');
|
||||
|
||||
module.exports = function(Topics) {
|
||||
|
||||
@@ -57,6 +58,7 @@ module.exports = function(Topics) {
|
||||
}
|
||||
db.setAdd('tid:' + tid + ':followers', uid, next);
|
||||
},
|
||||
async.apply(plugins.fireHook, 'action:topic.follow', { uid: uid, tid: tid }),
|
||||
function(next) {
|
||||
db.sortedSetAdd('uid:' + uid + ':followed_tids', Date.now(), tid, next);
|
||||
}
|
||||
@@ -75,6 +77,7 @@ module.exports = function(Topics) {
|
||||
}
|
||||
db.setRemove('tid:' + tid + ':followers', uid, next);
|
||||
},
|
||||
async.apply(plugins.fireHook, 'action:topic.unfollow', { uid: uid, tid: tid }),
|
||||
function(next) {
|
||||
db.sortedSetRemove('uid:' + uid + ':followed_tids', tid, next);
|
||||
}
|
||||
@@ -138,6 +141,7 @@ module.exports = function(Topics) {
|
||||
bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: postData.content,
|
||||
pid: postData.pid,
|
||||
path: '/post/' + postData.pid,
|
||||
nid: 'new_post:tid:' + postData.topic.tid + ':pid:' + postData.pid + ':uid:' + exceptUid,
|
||||
tid: postData.topic.tid,
|
||||
from: exceptUid,
|
||||
|
||||
@@ -13,7 +13,7 @@ var meta = require('../meta');
|
||||
|
||||
module.exports = function(Topics) {
|
||||
|
||||
Topics.createTopicFromPosts = function(uid, title, pids, callback) {
|
||||
Topics.createTopicFromPosts = function(uid, title, pids, fromTid, callback) {
|
||||
if (title) {
|
||||
title = title.trim();
|
||||
}
|
||||
@@ -55,6 +55,9 @@ module.exports = function(Topics) {
|
||||
}
|
||||
Topics.create({uid: results.postData.uid, title: title, cid: cid}, next);
|
||||
},
|
||||
function( results, next) {
|
||||
Topics.updateTopicBookmarks(fromTid, pids, function(){ next( null, results );} );
|
||||
},
|
||||
function(_tid, next) {
|
||||
function move(pid, next) {
|
||||
privileges.posts.canEdit(pid, uid, function(err, canEdit) {
|
||||
|
||||
@@ -6,7 +6,7 @@ var db = require('./database'),
|
||||
|
||||
Upgrade = {},
|
||||
|
||||
minSchemaDate = Date.UTC(2015, 7, 18), // This value gets updated every new MINOR version
|
||||
minSchemaDate = Date.UTC(2015, 10, 6), // This value gets updated every new MAJOR version
|
||||
schemaDate, thisSchemaDate,
|
||||
|
||||
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
|
||||
@@ -62,88 +62,6 @@ Upgrade.upgrade = function(callback) {
|
||||
}
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
thisSchemaDate = Date.UTC(2015, 8, 30);
|
||||
if (schemaDate < thisSchemaDate) {
|
||||
updatesMade = true;
|
||||
winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar');
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.isObjectField, 'config', 'customGravatarDefaultImage'),
|
||||
function(keyExists, _next) {
|
||||
if (keyExists) {
|
||||
_next();
|
||||
} else {
|
||||
winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar skipped');
|
||||
Upgrade.update(thisSchemaDate, next);
|
||||
next();
|
||||
}
|
||||
},
|
||||
async.apply(db.getObjectField, 'config', 'customGravatarDefaultImage'),
|
||||
async.apply(db.setObjectField, 'config', 'defaultAvatar'),
|
||||
async.apply(db.deleteObjectField, 'config', 'customGravatarDefaultImage')
|
||||
], function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar done');
|
||||
Upgrade.update(thisSchemaDate, next);
|
||||
});
|
||||
} else {
|
||||
winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar skipped');
|
||||
next();
|
||||
}
|
||||
},
|
||||
function(next) {
|
||||
thisSchemaDate = Date.UTC(2015, 10, 6);
|
||||
if (schemaDate < thisSchemaDate) {
|
||||
updatesMade = true;
|
||||
winston.info('[2015/11/06] Removing gravatar');
|
||||
|
||||
db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
async.eachLimit(uids, 500, function(uid, next) {
|
||||
db.getObjectFields('user:' + uid, ['picture', 'gravatarpicture'], function(err, userData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!userData.picture || !userData.gravatarpicture) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (userData.gravatarpicture === userData.picture) {
|
||||
async.series([
|
||||
function (next) {
|
||||
db.setObjectField('user:' + uid, 'picture', '', next);
|
||||
},
|
||||
function (next) {
|
||||
db.deleteObjectField('user:' + uid, 'gravatarpicture', next);
|
||||
}
|
||||
], next);
|
||||
} else {
|
||||
db.deleteObjectField('user:' + uid, 'gravatarpicture', next);
|
||||
}
|
||||
});
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
winston.info('[2015/11/06] Gravatar pictures removed!');
|
||||
Upgrade.update(thisSchemaDate, next);
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
winston.info('[2015/11/06] Gravatar removal skipped');
|
||||
next();
|
||||
}
|
||||
},
|
||||
function(next) {
|
||||
thisSchemaDate = Date.UTC(2015, 11, 15);
|
||||
|
||||
|
||||
@@ -121,10 +121,6 @@ module.exports = function(User) {
|
||||
},
|
||||
function(next) {
|
||||
groups.leaveAllGroups(uid, next);
|
||||
},
|
||||
function(next) {
|
||||
// Deprecated as of v0.7.4, remove in v1.0.0
|
||||
plugins.fireHook('filter:user.delete', uid, next);
|
||||
}
|
||||
], next);
|
||||
},
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
nconf = require('nconf'),
|
||||
winston = require('winston'),
|
||||
S = require('string'),
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var S = require('string');
|
||||
|
||||
user = require('../user'),
|
||||
db = require('../database'),
|
||||
meta = require('../meta'),
|
||||
notifications = require('../notifications'),
|
||||
posts = require('../posts'),
|
||||
topics = require('../topics'),
|
||||
privileges = require('../privileges'),
|
||||
utils = require('../../public/src/utils');
|
||||
var db = require('../database');
|
||||
var meta = require('../meta');
|
||||
var notifications = require('../notifications');
|
||||
var privileges = require('../privileges');
|
||||
|
||||
(function(UserNotifications) {
|
||||
|
||||
@@ -103,89 +98,13 @@ var async = require('async'),
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
UserNotifications.generateNotificationPaths(notifications, uid, callback);
|
||||
});
|
||||
};
|
||||
|
||||
UserNotifications.generateNotificationPaths = function (notifications, uid, callback) {
|
||||
var pids = notifications.map(function(notification) {
|
||||
return notification ? notification.pid : null;
|
||||
});
|
||||
|
||||
generatePostPaths(pids, uid, function(err, pidToPaths) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
notifications = notifications.map(function(notification, index) {
|
||||
if (!notification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
notification.path = pidToPaths[notification.pid] || notification.path || null;
|
||||
|
||||
if (notification.nid.startsWith('follow')) {
|
||||
notification.path = '/user/' + notification.user.userslug;
|
||||
}
|
||||
|
||||
notification.datetimeISO = utils.toISOString(notification.datetime);
|
||||
return notification;
|
||||
}).filter(function(notification) {
|
||||
// Remove notifications that do not resolve to a path
|
||||
return notification && notification.path !== null;
|
||||
notifications = notifications.filter(function(notification) {
|
||||
return notification && notification.path;
|
||||
});
|
||||
|
||||
callback(null, notifications);
|
||||
});
|
||||
};
|
||||
|
||||
function generatePostPaths(pids, uid, callback) {
|
||||
pids = pids.filter(Boolean);
|
||||
var postKeys = pids.map(function(pid) {
|
||||
return 'post:' + pid;
|
||||
});
|
||||
|
||||
db.getObjectsFields(postKeys, ['pid', 'tid'], function(err, postData) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var topicKeys = postData.map(function(post) {
|
||||
return post ? 'topic:' + post.tid : null;
|
||||
});
|
||||
|
||||
async.parallel({
|
||||
indices: function(next) {
|
||||
posts.getPostIndices(postData, uid, next);
|
||||
},
|
||||
topics: function(next) {
|
||||
db.getObjectsFields(topicKeys, ['slug', 'deleted'], next);
|
||||
}
|
||||
}, function(err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var pidToPaths = {};
|
||||
pids.forEach(function(pid, index) {
|
||||
if (parseInt(results.topics[index].deleted, 10) === 1) {
|
||||
pidToPaths[pid] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var slug = results.topics[index] ? results.topics[index].slug : null;
|
||||
var postIndex = utils.isNumber(results.indices[index]) ? parseInt(results.indices[index], 10) + 1 : null;
|
||||
|
||||
if (slug && postIndex) {
|
||||
pidToPaths[pid] = '/topic/' + slug + '/' + postIndex;
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, pidToPaths);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
UserNotifications.getDailyUnread = function(uid, callback) {
|
||||
var yesterday = Date.now() - (1000 * 60 * 60 * 24); // Approximate, can be more or less depending on time changes, makes no difference really.
|
||||
@@ -281,13 +200,20 @@ var async = require('async'),
|
||||
};
|
||||
|
||||
UserNotifications.sendTopicNotificationToFollowers = function(uid, topicData, postData) {
|
||||
db.getSortedSetRange('followers:' + uid, 0, -1, function(err, followers) {
|
||||
if (err || !Array.isArray(followers) || !followers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
privileges.categories.filterUids('read', topicData.cid, followers, function(err, followers) {
|
||||
if (err || !followers.length) {
|
||||
var followers;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRange('followers:' + uid, 0, -1, next);
|
||||
},
|
||||
function (followers, next) {
|
||||
if (!Array.isArray(followers) || !followers.length) {
|
||||
return;
|
||||
}
|
||||
privileges.categories.filterUids('read', topicData.cid, followers, next);
|
||||
},
|
||||
function (_followers, next) {
|
||||
followers = _followers;
|
||||
if (!followers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,15 +226,20 @@ var async = require('async'),
|
||||
bodyShort: '[[notifications:user_posted_topic, ' + postData.user.username + ', ' + title + ']]',
|
||||
bodyLong: postData.content,
|
||||
pid: postData.pid,
|
||||
path: '/post/' + postData.pid,
|
||||
nid: 'tid:' + postData.tid + ':uid:' + uid,
|
||||
tid: postData.tid,
|
||||
from: uid
|
||||
}, function(err, notification) {
|
||||
if (!err && notification) {
|
||||
notifications.push(notification, followers);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, next);
|
||||
}
|
||||
], function(err, notification) {
|
||||
if (err) {
|
||||
return winston.error(err);
|
||||
}
|
||||
|
||||
if (notification) {
|
||||
notifications.push(notification, followers);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ module.exports = function(User) {
|
||||
async.series([
|
||||
async.apply(image.normalise, picture.path, extension),
|
||||
async.apply(fs.rename, picture.path + '.png', picture.path)
|
||||
], next);
|
||||
], function(err) {
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
User.getUserField(updateUid, 'uploadedpicture', next);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="sounds settings" class="row">
|
||||
<div class="col-xs-9">
|
||||
<div class="col-xs-12">
|
||||
<form role="form">
|
||||
<div class="row">
|
||||
<div class="col-sm-2 col-xs-12 settings-header">Notifications</div>
|
||||
@@ -15,7 +15,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="btn-group col-xs-3">
|
||||
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
|
||||
<button type="button" class="form-control btn btn-sm btn-default" data-action="play"><span class="hidden-xs">Play </span><i class="fa fa-play"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="btn-group col-xs-3">
|
||||
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
|
||||
<button type="button" class="form-control btn btn-sm btn-default" data-action="play"><span class="hidden-xs">Play </span><i class="fa fa-play"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,25 +50,20 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="btn-group col-xs-3">
|
||||
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
|
||||
<button type="button" class="form-control btn btn-sm btn-default" data-action="play"><span class="hidden-xs">Play </span><i class="fa fa-play"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<input data-action="upload" data-title="Upload Sound" data-route="{config.relative_path}/api/admin/upload/sound" type="button" class="btn btn-primary" value="Upload New Sound"></input>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-3">
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<input data-action="upload" data-title="Upload Sound" data-route="{config.relative_path}/api/admin/upload/sound" type="button" class="btn btn-primary btn-block" value="Upload New Sound"></input>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||
|
||||
18
src/views/admin/partials/widget-settings.tpl
Normal file
18
src/views/admin/partials/widget-settings.tpl
Normal file
@@ -0,0 +1,18 @@
|
||||
<br />
|
||||
<label>Title:</label>
|
||||
<input type="text" class="form-control" name="title" placeholder="Title (only shown on some containers)" /><br />
|
||||
|
||||
<label>Container:</label>
|
||||
<textarea rows="4" class="form-control container-html" name="container" placeholder="Drag and drop a container or enter HTML here."></textarea>
|
||||
|
||||
<div class="checkbox">
|
||||
<label><input name="hide-guests" type="checkbox"> Hide from anonymous users?</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label><input name="hide-registered" type="checkbox"> Hide from registered users?</input></label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label><input name="hide-mobile" type="checkbox"> Hide on mobile?</input></label>
|
||||
</div>
|
||||
@@ -1,8 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
var async = require('async'),
|
||||
plugins = require('../plugins');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
var plugins = require('../plugins');
|
||||
|
||||
var admin = {};
|
||||
|
||||
@@ -22,6 +23,9 @@ admin.get = function(callback) {
|
||||
},
|
||||
widgets: function(next) {
|
||||
plugins.fireHook('filter:widgets.getWidgets', [], next);
|
||||
},
|
||||
adminTemplate: function(next) {
|
||||
fs.readFile(path.resolve(__dirname, '../../public/templates/admin/partials/widget-settings.tpl'), 'utf8', next);
|
||||
}
|
||||
}, function(err, widgetData) {
|
||||
if (err) {
|
||||
@@ -34,17 +38,14 @@ admin.get = function(callback) {
|
||||
area.data = areaData;
|
||||
next(err);
|
||||
});
|
||||
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
for (var w in widgetData.widgets) {
|
||||
if (widgetData.widgets.hasOwnProperty(w)) {
|
||||
// if this gets anymore complicated, it needs to be a template
|
||||
widgetData.widgets[w].content += "<br /><label>Title:</label><input type=\"text\" class=\"form-control\" name=\"title\" placeholder=\"Title (only shown on some containers)\" /><br /><label>Container:</label><textarea rows=\"4\" class=\"form-control container-html\" name=\"container\" placeholder=\"Drag and drop a container or enter HTML here.\"></textarea><div class=\"checkbox\"><label><input name=\"hide-guests\" type=\"checkbox\"> Hide from anonymous users?</label></div><div class=\"checkbox\"><label><input name=\"hide-registered\" type=\"checkbox\"> Hide from registered users?</input></label></div>";
|
||||
}
|
||||
}
|
||||
|
||||
widgetData.widgets.forEach(function(w) {
|
||||
w.content += widgetData.adminTemplate;
|
||||
});
|
||||
|
||||
var templates = [],
|
||||
list = {}, index = 0;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
var async = require('async'),
|
||||
winston = require('winston'),
|
||||
templates = require('templates.js'),
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var templates = require('templates.js');
|
||||
|
||||
plugins = require('../plugins'),
|
||||
translator = require('../../public/src/modules/translator'),
|
||||
db = require('../database');
|
||||
var plugins = require('../plugins');
|
||||
var translator = require('../../public/src/modules/translator');
|
||||
var db = require('../database');
|
||||
|
||||
var widgets = {};
|
||||
|
||||
@@ -30,7 +30,10 @@ widgets.render = function(uid, area, req, res, callback) {
|
||||
}
|
||||
|
||||
async.map(widgetsByLocation[location], function(widget, next) {
|
||||
if (!widget || !widget.data || (!!widget.data['hide-registered'] && uid !== 0) || (!!widget.data['hide-guests'] && uid === 0)) {
|
||||
if (!widget || !widget.data ||
|
||||
(!!widget.data['hide-registered'] && uid !== 0) ||
|
||||
(!!widget.data['hide-guests'] && uid === 0) ||
|
||||
(!!widget.data['hide-mobile'] && area.isMobile)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ var db = require('./mocks/databasemock');
|
||||
var topics = require('../src/topics');
|
||||
var categories = require('../src/categories');
|
||||
var User = require('../src/user');
|
||||
var groups = require('../src/groups');
|
||||
var async = require('async');
|
||||
|
||||
describe('Topic\'s', function() {
|
||||
var topic,
|
||||
@@ -188,6 +190,100 @@ describe('Topic\'s', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('.fork', function(){
|
||||
var newTopic;
|
||||
var replies = new Array();
|
||||
var topicPids;
|
||||
var originalBookmark = 5;
|
||||
function postReply( next ){
|
||||
topics.reply({uid: topic.userId, content: 'test post ' + replies.length, tid: newTopic.tid},
|
||||
function(err, result) {
|
||||
assert.equal(err, null, 'was created with error');
|
||||
assert.ok(result);
|
||||
replies.push( result );
|
||||
next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
before( function(done) {
|
||||
async.waterfall(
|
||||
[
|
||||
function(next){
|
||||
groups.join('administrators', topic.userId, next);
|
||||
},
|
||||
function( next ){
|
||||
topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) {
|
||||
assert.ifError( err );
|
||||
newTopic = result.topicData;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){ postReply( next );},
|
||||
function( next ){
|
||||
topicPids = replies.map( function( reply ){ return reply.pid; } );
|
||||
topics.setUserBookmark( newTopic.tid, topic.userId, originalBookmark, next );
|
||||
}],
|
||||
done );
|
||||
});
|
||||
|
||||
it('should have 12 replies', function(done) {
|
||||
assert.equal( 12, replies.length );
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not update the user\'s bookmark', function(done){
|
||||
async.waterfall([
|
||||
function(next){
|
||||
topics.createTopicFromPosts(
|
||||
topic.userId,
|
||||
'Fork test, no bookmark update',
|
||||
topicPids.slice( -2 ),
|
||||
newTopic.tid,
|
||||
next );
|
||||
},
|
||||
function( forkedTopicData, next){
|
||||
topics.getUserBookmark( newTopic.tid, topic.userId, next );
|
||||
},
|
||||
function( bookmark, next ){
|
||||
assert.equal( originalBookmark, bookmark );
|
||||
next();
|
||||
}
|
||||
],done);
|
||||
});
|
||||
|
||||
it('should update the user\'s bookmark ', function(done){
|
||||
async.waterfall([
|
||||
function(next){
|
||||
topics.createTopicFromPosts(
|
||||
topic.userId,
|
||||
'Fork test, no bookmark update',
|
||||
topicPids.slice( 1, 3 ),
|
||||
newTopic.tid,
|
||||
next );
|
||||
},
|
||||
function( forkedTopicData, next){
|
||||
topics.getUserBookmark( newTopic.tid, topic.userId, next );
|
||||
},
|
||||
function( bookmark, next ){
|
||||
assert.equal( originalBookmark - 2, bookmark );
|
||||
next();
|
||||
}
|
||||
],done);
|
||||
});
|
||||
});
|
||||
|
||||
after(function() {
|
||||
db.flushdb();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user