Merge commit '6392cd31df3b311fbbfdf2ff09a1a7509ef62139' into weekly

This commit is contained in:
NodeBB Misty
2016-05-09 16:00:15 -04:00
86 changed files with 1275 additions and 973 deletions

5
.gitignore vendored
View File

@@ -47,4 +47,7 @@ pidfile
## Transifex
tx.exe
.transifexrc
.transifexrc
##Coverage output
coverage

View File

@@ -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"
}
]
}
}

View File

@@ -1,5 +1,5 @@
{
"invalid-data": "بيانات غير صالحة",
"invalid-data": "بيانات غير صحيحة",
"not-logged-in": "لم تقم بتسجيل الدخول",
"account-locked": "تم حظر حسابك مؤقتًا.",
"search-requires-login": "البحث في المنتدى يتطلب حساب - الرجاء تسجيل الدخول أو التسجيل",

View File

@@ -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",

View File

@@ -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?"
}

View File

@@ -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",

View File

@@ -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!"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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."
}

View File

@@ -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. "
}

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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."

View File

@@ -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",

View File

@@ -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."
}

View File

@@ -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": "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"
}

View File

@@ -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"
}

View File

@@ -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 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"
}

View File

@@ -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!"
}

View File

@@ -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": "看過的類別"
}

View File

@@ -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": "本聊天通知按您的訂閱設置發送給您。",

View File

@@ -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": "你不能把自己從群組中踢出"
}

View File

@@ -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"
}

View File

@@ -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": "上傳群組封面圖"
}

View File

@@ -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": "封面照片與位置已儲存"
}

View File

@@ -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."

View File

@@ -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": "點擊這裡進行重整。"
}

View File

@@ -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": "您的註冊已經被加入到審核序列中。您將會在管理者批準後收到一封電子郵件。"
}

View File

@@ -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": "你的密碼已過期,請選擇一組新密碼"
}

View File

@@ -1,7 +1,7 @@
{
"results_matching": "有%1個跟\"%2\"相符的結果(%3秒",
"no-matches": "沒有找到相符的主題",
"advanced-search": "Advanced Search",
"advanced-search": "進階搜尋",
"in": "在",
"titles": "標題",
"titles-posts": "標題與發布",

View File

@@ -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": "還沒有標籤呢。"
}

View File

@@ -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": "輸入標記的理由"
}

View File

@@ -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": "已觀看主題"
}

View File

@@ -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"
}

View File

@@ -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": "沒有找到用戶!"
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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');
}
});
}
});

View File

@@ -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() {

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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, '&#44;') : ''
accept: data.accept ? data.accept.replace(/,/g, '&#44; ') : ''
}, function(uploadModal) {
uploadModal = $(uploadModal);

View File

@@ -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,

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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('');
});

View File

@@ -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 = {};

View File

@@ -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
View 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;

View File

@@ -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('&#44; ') + ']]'));
}
@@ -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);
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -135,4 +135,4 @@ module.exports = function(Meta) {
db.deleteObjectField('config', field);
};
};
};

38
src/middleware/cls.js Normal file
View 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);

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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');

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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);
};
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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, '&#37;').replace(/,/g, '&#44;');
@@ -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, '&#37;').replace(/,/g, '&#44;');
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, '&#37;').replace(/,/g, '&#44;');
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]);
}
});
};

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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);
},

View File

@@ -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));

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);
},

View File

@@ -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);
}
});
};

View File

@@ -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);

View File

@@ -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">

View 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>

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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();
});