diff --git a/README.md b/README.md index 3667684b36..43f2b7cb29 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ NodeBB requires the following software to be installed: ## Installation -[Please refer to platform-specific installation documentation](http://docs.nodebb.org/en/latest/installing/os.html) +[Please refer to platform-specific installation documentation](https://docs.nodebb.org/installing/os) ## Securing NodeBB diff --git a/app.js b/app.js index 90f5ef046e..7a0b7bc2a5 100644 --- a/app.js +++ b/app.js @@ -26,7 +26,10 @@ if (require.main !== module) { } var nconf = require('nconf'); -nconf.argv().env('__'); +nconf.argv().env({ + separator: '__', + lowerCase: true, +}); var url = require('url'); var async = require('async'); diff --git a/loader.js b/loader.js index 214f785eb9..f897fd79ce 100644 --- a/loader.js +++ b/loader.js @@ -142,7 +142,7 @@ function getPorts() { process.exit(); } var urlObject = url.parse(_url); - var port = nconf.get('port') || nconf.get('PORT') || urlObject.port || 4567; + var port = nconf.get('port') || urlObject.port || 4567; if (!Array.isArray(port)) { port = [port]; } diff --git a/package.json b/package.json index 6f03e3c465..0a0b28dd14 100644 --- a/package.json +++ b/package.json @@ -55,12 +55,12 @@ "morgan": "^1.3.2", "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "4.4.14", + "nodebb-plugin-composer-default": "4.4.15", "nodebb-plugin-dbsearch": "2.0.4", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.2.1", "nodebb-plugin-markdown": "7.1.1", - "nodebb-plugin-mentions": "2.0.3", + "nodebb-plugin-mentions": "2.1.1", "nodebb-plugin-soundpack-default": "1.0.0", "nodebb-plugin-spam-be-gone": "0.5.0", "nodebb-rewards-essentials": "0.0.9", diff --git a/public/language/ja/admin/general/dashboard.json b/public/language/ja/admin/general/dashboard.json index ed544afb51..124d26c394 100644 --- a/public/language/ja/admin/general/dashboard.json +++ b/public/language/ja/admin/general/dashboard.json @@ -20,12 +20,12 @@ "stats.all": "全て", "updates": "更新", - "running-version": "NodeBB v %1 を実行しています。", + "running-version": "NodeBB v%1 を実行しています。", "keep-updated": "常に最新のセキュリティパッチとバグ修正のためにNodeBBが最新であることを確認してください。", - "up-to-date": "

あなたは最新の状態です。 ", + "up-to-date": "

あなたは最新の状態です。

", "upgrade-available": "

新しいバージョン (v%1) がリリースされました。NodeBBのアップグレードを検討してください。

", "prerelease-upgrade-available": "

これはNodeBBの旧リリースのバージョンです。新しいバージョン(v%1)がリリースされました。 NodeBBのアップグレードを検討してください。", - "prerelease-warning": "

これはNodeBBのプレリリース版です。意図しないバグが発生することがあります。

", + "prerelease-warning": "

これはNodeBBのプレリリース版です。意図しないバグが発生することがあります。

", "running-in-development": "フォーラムが開発モードで動作しています。フォーラムの動作が脆弱かもしれませんので、管理者に問い合わせてください。", "notices": "通知", diff --git a/public/language/ko/error.json b/public/language/ko/error.json index 2c8cb8d882..40715f2083 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -4,21 +4,21 @@ "not-logged-in": "로그인하지 않았습니다.", "account-locked": "임시로 잠긴 계정입니다.", "search-requires-login": "검색을 하기 위해서는 계정이 필요합니다. 로그인하거나 가입해 주십시오.", - "invalid-cid": "올바르지 않은 카테고리 ID입니다.", - "invalid-tid": "올바르지 않은 주제 ID입니다.", - "invalid-pid": "올바르지 않은 게시물 ID입니다.", + "invalid-cid": "올바르지 않은 게시판 ID입니다.", + "invalid-tid": "올바르지 않은 게시물 ID입니다.", + "invalid-pid": "올바르지 않은 포스트 ID입니다.", "invalid-uid": "올바르지 않은 사용자 ID입니다.", - "invalid-username": "올바르지 않은 사용자 이름입니다.", + "invalid-username": "올바르지 않은 사용자명 입니다.", "invalid-email": "올바르지 않은 이메일입니다.", "invalid-title": "올바르지 않은 제목입니다.", "invalid-user-data": "올바르지 않은 사용자 정보입니다.", "invalid-password": "올바르지 않은 비밀번호입니다.", - "invalid-login-credentials": "잘못된 로그인 정보입니다.", - "invalid-username-or-password": "사용자 이름과 패스워드를 모두 설정해주세요.", + "invalid-login-credentials": "올바르지 않은 로그인 정보입니다.", + "invalid-username-or-password": "사용자명과 패스워드를 모두 설정해주세요.", "invalid-search-term": "올바르지 않은 검색어입니다.", "csrf-invalid": "세션이 만료되어 로그인에 실패하였습니다. 다시 시도해 주세요.", - "invalid-pagination-value": "올바르지 않은 값입니다. 최소 1%에서 최대 2%까지 설정해야 합니다.", - "username-taken": "이미 사용 중인 사용자 이름입니다.", + "invalid-pagination-value": "올바르지 않은 페이지 값입니다. 최소 1% 에서 최대 2% 사이로 설정해야 합니다.", + "username-taken": "이미 사용 중인 사용자명 입니다.", "email-taken": "이미 사용 중인 이메일입니다.", "email-not-confirmed": "아직 이메일이 인증되지 않았습니다. 여기를 누르면 인증 메일을 발송할 수 있습니다.", "email-not-confirmed-chat": "아직 이메일이 인증되지 않았습니다. 대화기능은 인증 후에 사용이 가능합니다.", @@ -36,49 +36,49 @@ "user-too-new": "죄송합니다, 첫 번째 게시물은 %1 초 후에 작성할 수 있습니다.", "blacklisted-ip": "죄송하지만, 당신의 IP는 이 커뮤니티로부터 차단되었습니다. 만약 에러라는 생각이 드신다면 관리자에게 연락해주세요.", "ban-expiry-missing": "해당 차단의 만료일을 설정 해주세요.", - "no-category": "존재하지 않는 카테고리입니다.", - "no-topic": "존재하지 않는 주제입니다.", - "no-post": "존재하지 않는 게시물입니다.", - "no-group": "존재하지 않는 그룹입니다.", - "no-user": "존재하지 않는 사용자입니다.", - "no-teaser": "존재하지 않는 미리보기입니다.", + "no-category": "존재하지 않는 게시판 입니다.", + "no-topic": "존재하지 않는 게시물 입니다.", + "no-post": "존재하지 않는 포스트 입니다.", + "no-group": "존재하지 않는 그룹 입니다.", + "no-user": "존재하지 않는 사용자 입니다.", + "no-teaser": "존재하지 않는 미리보기 입니다.", "no-privileges": "이 작업을 할 수 있는 권한이 없습니다.", - "category-disabled": "비활성화된 카테고리입니다.", - "topic-locked": "잠긴 주제입니다.", - "post-edit-duration-expired": "게시물의 수정은 작성한 시간으로부터 %1초 후에 가능합니다.", - "post-edit-duration-expired-minutes": "게시물의 수정은 작성한 시간으로부터 %1분 후에 가능합니다.", - "post-edit-duration-expired-minutes-seconds": "게시물의 수정은 작성한 시간으로부터 %1분 %2초 후에 가능합니다.", - "post-edit-duration-expired-hours": "게시물의 수정은 작성한 시간으로부터 %1시간 후에 가능합니다.", - "post-edit-duration-expired-hours-minutes": "게시물의 수정은 작성한 시간으로부터 %1시간 %2분 후에 가능합니다.", - "post-edit-duration-expired-days": "게시물의 수정은 작성한 시간으로부터 %1일 후에 가능합니다.", - "post-edit-duration-expired-days-hours": "게시물의 수정은 작성한 시간으로부터 %1일 %2시간 후에 가능합니다.", - "post-delete-duration-expired": "게시물의 삭제는 작성한 시간으로부터 %1초 후에 가능합니다.", - "post-delete-duration-expired-minutes": "게시물의 삭제는 작성한 시간으로부터 %1분 후에 가능합니다.", - "post-delete-duration-expired-minutes-seconds": "게시물의 삭제는 작성한 시간으로부터 %1분 %2초 후에 가능합니다.", - "post-delete-duration-expired-hours": "게시물의 삭제는 작성한 시간으로부터 %1시간 후에 가능합니다.", - "post-delete-duration-expired-hours-minutes": "게시물의 삭제는 작성한 시간으로부터 %1시간 %2분 후에 가능합니다.", - "post-delete-duration-expired-days": "게시물의 삭제는 작성한 시간으로부터 %1일 후에 가능합니다.", - "post-delete-duration-expired-days-hours": "게시물의 삭제는 작성한 시간으로부터 %1일 %2시간 후에 가능합니다.", - "cant-delete-topic-has-reply": "답글이 달린 토픽은 삭제하실 수 없습니다.", - "cant-delete-topic-has-replies": "답글이 %1개 이상 달린 토픽은 삭제하실 수 없습니다.", - "content-too-short": "게시물의 내용이 너무 짧습니다. 내용은 최소 %1자 이상이어야 합니다.", - "content-too-long": "게시물의 내용이 너무 깁니다. 내용은 최대 %1자 이내로 작성할 수 있습니다.", + "category-disabled": "게시판이 비활성화 되었습니다.", + "topic-locked": "게시물이 잠겼습니다.", + "post-edit-duration-expired": "포스트의 수정은 작성한 시간으로부터 %1 초 후에 가능합니다.", + "post-edit-duration-expired-minutes": "포스트의 수정은 작성한 시간으로부터 %1분 후에 가능합니다.", + "post-edit-duration-expired-minutes-seconds": "포스트의 수정은 작성한 시간으로부터 %1분 %2초 후에 가능합니다.", + "post-edit-duration-expired-hours": "포스트의 수정은 작성한 시간으로부터 %1시간 후에 가능합니다.", + "post-edit-duration-expired-hours-minutes": "포스트의 수정은 작성한 시간으로부터 %1시간 %2분 후에 가능합니다.", + "post-edit-duration-expired-days": "포스트의 수정은 작성한 시간으로부터 %1일 후에 가능합니다.", + "post-edit-duration-expired-days-hours": "포스트의 수정은 작성한 시간으로부터 %1일 %2시간 후에 가능합니다.", + "post-delete-duration-expired": "포스트의 삭제는 작성한 시간으로부터 %1초 후에 가능합니다.", + "post-delete-duration-expired-minutes": "포스트의 삭제는 작성한 시간으로부터 %1분 후에 가능합니다.", + "post-delete-duration-expired-minutes-seconds": "포스트의 삭제는 작성한 시간으로부터 %1분 %2초 후에 가능합니다.", + "post-delete-duration-expired-hours": "포스트의 삭제는 작성한 시간으로부터 %1시간 후에 가능합니다.", + "post-delete-duration-expired-hours-minutes": "포스트의 삭제는 작성한 시간으로부터 %1시간 %2분 후에 가능합니다.", + "post-delete-duration-expired-days": "포스트의 삭제는 작성한 시간으로부터 %1일 후에 가능합니다.", + "post-delete-duration-expired-days-hours": "포스트의 삭제는 작성한 시간으로부터 %1일 %2시간 후에 가능합니다.", + "cant-delete-topic-has-reply": "답글이 달린 게시물은 삭제하실 수 없습니다.", + "cant-delete-topic-has-replies": "답글이 %1개 이상 달린 게시물은 삭제하실 수 없습니다.", + "content-too-short": "포스트의 내용이 너무 짧습니다. 내용은 최소 %1자 이상이어야 합니다.", + "content-too-long": "포스트의 내용이 너무 깁니다. 내용은 최대 %1자 이내로 작성할 수 있습니다.", "title-too-short": "제목이 너무 짧습니다. 제목은 최소 %1자 이상이어야 합니다.", "title-too-long": "제목이 너무 깁니다. 제목은 최대 %1자 이내로 작성할 수 있습니다.", "category-not-selected": "선택된 게시판이 없습니다.", "too-many-posts": "새 게시물 작성은 %1초마다 가능합니다 - 조금 천천히 작성해주세요.", "too-many-posts-newbie": "신규 사용자는 %2 만큼의 인지도를 얻기 전까지 %1초마다 게시물을 작성할 수 있습니다. 조금 천천히 작성해주세요.", - "tag-too-short": "꼬리표가 너무 짧습니다. 꼬리표는 최소 %1자 이상이어야 합니다.", - "tag-too-long": "꼬리표가 너무 깁니다. 꼬리표는 최대 %1자 이내로 사용가능합니다.", - "not-enough-tags": "꼬리표가 없거나 부족합니다. 게시물은 %1개 이상의 꼬리표를 사용해야 합니다.", - "too-many-tags": "꼬리표가 너무 많습니다. 게시물은 %1개 이하의 꼬리표를 사용할 수 있습니다.", + "tag-too-short": "태그가 너무 짧습니다. 태그는 최소 %1자 이상이어야 합니다.", + "tag-too-long": "태그가 너무 깁니다. 태그는 최대 %1자 이내로 사용가능합니다.", + "not-enough-tags": "태그가 없거나 부족합니다. 게시물은 %1개 이상의 태그를 사용해야 합니다.", + "too-many-tags": "태그가 너무 많습니다. 게시물은 %1개 이하의 태그를 사용할 수 있습니다.", "still-uploading": "업로드가 끝날 때까지 기다려주세요.", "file-too-big": "업로드 가능한 파일크기는 최대 %1 KB 입니다 - 파일의 용량을 줄이거나 압축을 활용하세요.", - "guest-upload-disabled": "손님의 파일 업로드는 제한되어 있습니다.", - "already-bookmarked": "이미 즐겨찾기에 추가된 글입니다.", - "already-unbookmarked": "이미 즐겨찾기를 해제한 글입니다.", + "guest-upload-disabled": "미가입 사용자의 파일 업로드는 제한되어 있습니다.", + "already-bookmarked": "이미 즐겨찾기에 추가한 포스트 입니다.", + "already-unbookmarked": "이미 즐겨찾기를 해제한 포스트 입니다.", "cant-ban-other-admins": "다른 관리자를 차단할 수 없습니다.", - "cant-remove-last-admin": "귀하는 유일한 관리자입니다. 관리자를 그만두시기 전에 다른 사용자를 관리자로 선임하세요.", + "cant-remove-last-admin": "귀하는 유일한 관리자입니다. 관리자를 그만두시기 전에 다른 사용자를 관리자로 임명하세요.", "cant-delete-admin": "해당 계정을 삭제하기 전에 관리자 권한을 해제 해주십시오.", "invalid-image-type": "올바르지 않은 이미지입니다. 사용가능한 유형: %1", "invalid-image-extension": "올바르지 않은 이미지 확장자입니다.", @@ -86,43 +86,43 @@ "group-name-too-short": "그룹 이름이 너무 짧습니다.", "group-name-too-long": "그룹 이름이 너무 깁니다.", "group-already-exists": "이미 존재하는 그룹입니다.", - "group-name-change-not-allowed": "그룹 이름의 변경은 불가합니다.", + "group-name-change-not-allowed": "그룹 이름의 변경이 불가능 합니다.", "group-already-member": "이미 이 그룹에 속해있습니다.", "group-not-member": "이 그룹의 구성원이 아닙니다.", "group-needs-owner": "이 그룹은 적어도 한 명의 소유자가 필요합니다.", "group-already-invited": "이 사용자는 이미 초대됐습니다.", "group-already-requested": "회원 요청이 이미 제출되었습니다.", - "post-already-deleted": "이미 삭제된 게시물입니다.", - "post-already-restored": "이미 복원된 게시물입니다.", - "topic-already-deleted": "이미 삭제된 주제입니다.", - "topic-already-restored": "이미 복원된 주제입니다.", - "cant-purge-main-post": "주요 게시물은 삭제할 수 없습니다. 대신 주제를 삭제하세요.", - "topic-thumbnails-are-disabled": "주제 섬네일이 이미 해제되었습니다.", + "post-already-deleted": "이미 삭제된 포스트 입니다.", + "post-already-restored": "이미 복원된 포스트 입니다.", + "topic-already-deleted": "이미 삭제된 게시물 입니다.", + "topic-already-restored": "이미 복원된 게시물 입니다.", + "cant-purge-main-post": "메인 포스트는 삭제할 수 없습니다. 대신 게시물을 삭제하세요.", + "topic-thumbnails-are-disabled": "게시물 섬네일이 비활성화 되었습니다.", "invalid-file": "올바르지 않은 파일입니다.", - "uploads-are-disabled": "업로드는 비활성화되어 있습니다.", - "signature-too-long": "서명은 %1자 이내로 작성할 수 있습니다.", - "about-me-too-long": "자기소개는 %1자 이내로 작성할 수 있습니다.", + "uploads-are-disabled": "업로드가 비활성화 되었습니다.", + "signature-too-long": "서명은 %1 자를 넘길 수 없습니다.", + "about-me-too-long": "자기소개는 %1 자를 넘길 수 없습니다.", "cant-chat-with-yourself": "자신과는 대화할 수 없습니다.", - "chat-restricted": "이 사용자는 대화를 제한하고 있습니다. 대화하려면 이 사용자가 귀하를 따라야합니다.", - "chat-disabled": "대화 기능을 사용하지 않습니다.", + "chat-restricted": "이 사용자는 대화를 제한하고 있습니다. 대화하려면 이 사용자가 귀하를 팔로우 해야합니다.", + "chat-disabled": "채팅 시스템이 비활성화 되었습니다.", "too-many-messages": "짧은 시간동안 너무 많은 메시지를 전송하였습니다. 잠시 후에 다시 시도하세요.", "invalid-chat-message": "올바르지 않은 메시지입니다.", - "chat-message-too-long": "대화 메세지는 최대 %1자로 제한됩니다.", - "cant-edit-chat-message": "편집 할 수 있는 권한이 없습니다.", + "chat-message-too-long": "채팅 메세지는 최대 %1자로 제한됩니다.", + "cant-edit-chat-message": "이 메세지를 수정 할 권한이 없습니다.", "cant-remove-last-user": "마지막 사용자를 삭제할 수 없습니다.", - "cant-delete-chat-message": "메세지를 지울 권한이 없습니다.", - "already-voting-for-this-post": "이미 이 게시글에 투표하셨습니다.", - "reputation-system-disabled": "인지도 기능이 비활성 상태입니다.", + "cant-delete-chat-message": "이 메세지를 삭제할 권한이 없습니다.", + "already-voting-for-this-post": "이미 이 포스트에 투표하셨습니다.", + "reputation-system-disabled": "인지도 시스템이 비활성 상태입니다.", "downvoting-disabled": "비추천 기능이 비활성 상태입니다.", - "not-enough-reputation-to-downvote": "인지도가 낮아 이 게시물에 반대할 수 없습니다.", - "not-enough-reputation-to-flag": "인지도가 낮아 이 게시물을 신고할 수 없습니다.", + "not-enough-reputation-to-downvote": "인지도가 낮아 이 포스트를 비추천할 수 없습니다.", + "not-enough-reputation-to-flag": "인지도가 낮아 이 포스트를 신고할 수 없습니다.", "already-flagged": "이미 이 게시물을 신고했습니다.", "reload-failed": "NodeBB 서버를 다시 읽어들이는 중 다음과 같은 문제가 발생했으나 사용자측은 지속적으로 자원을 제공받습니다. 오류 문구: \"%1\" 문제를 해결하시려면 다시 읽어들이기 전의 수정사항을 원래대로 되돌려주세요. ", "registration-error": "등록 오류", - "parse-error": "서버 응답을 파싱하는 동안 문제가 발생했습니다.", + "parse-error": "서버로 부터의 응답을 읽는 동안 문제가 발생했습니다.", "wrong-login-type-email": "이메일 주소를 통해 로그인하세요.", "wrong-login-type-username": "사용자명을 통해 로그인하세요.", - "invite-maximum-met": "초대가능한 사용자를 모두 초대했습니다. (%2명 중 %1을 초대)", + "invite-maximum-met": "초대 한도 만큼의 사용자를 초대했습니다. (%2명 중 %1을 초대)", "no-session-found": "로그인 세션을 찾을 수 없습니다.", "not-in-room": "없는 사용자입니다.", "no-users-in-room": "사용자가 없습니다.", diff --git a/public/language/ko/flags.json b/public/language/ko/flags.json index db6117d5e2..8693130936 100644 --- a/public/language/ko/flags.json +++ b/public/language/ko/flags.json @@ -7,21 +7,21 @@ "assignee": "담당자", "update": "업데이트", "updated": "업데이트 되었습니다.", - "target-purged": "해당 신고된 게시물은 완전 삭제 되었으며, 더이상 존재하지 않습니다.", + "target-purged": "해당 신고된 컨텐츠는 완전 삭제 되었으며, 더이상 존재하지 않습니다.", "quick-filters": "간편 필터", "filter-active": "적용된 하나 이상의 필터가 있습니다.", "filter-reset": "필터 제거", - "filters": "필터 항목", + "filters": "필터 옵션", "filter-reporterId": "신고자 ID", "filter-targetUid": "신고된 게시물 ID", "filter-type": "신고 유형", "filter-type-all": "모든 컨텐츠", - "filter-type-post": "글", + "filter-type-post": "포스트", "filter-state": "처리 상태", "filter-assignee": "담당자 ID", "filter-cid": "게시판", - "filter-quick-mine": "나에게 배정", + "filter-quick-mine": "나에게 배정된 신고", "filter-cid-all": "모든 게시판", "apply-filters": "필터 적용", @@ -53,7 +53,7 @@ "modal-title": "부적절한 컨텐츠 신고", "modal-body": "%1 %2 에 대한 신고 사유를 적어주시거나, 빠른 신고 버튼 중 하나를 사용해 주세요.", "modal-reason-spam": "스팸", - "modal-reason-offensive": "공격적인", + "modal-reason-offensive": "부적절한 글", "modal-reason-custom": "신고 사유", "modal-submit": "리포트 제출", "modal-submit-success": "이 컨텐츠는 신고되었습니다." diff --git a/public/language/ko/global.json b/public/language/ko/global.json index 60b031ab0c..67c23301ef 100644 --- a/public/language/ko/global.json +++ b/public/language/ko/global.json @@ -3,7 +3,7 @@ "search": "검색", "buttons.close": "닫기", "403.title": "접근이 거부되었습니다.", - "403.message": "접속할 수 없는 페이지에 접근하였습니다.", + "403.message": "권한이 없는 페이지에 접속을 시도하였습니다.", "403.login": "로그인되어 있는지 확인해 주세요.", "404.title": "페이지를 찾을 수 없습니다.", "404.message": "존재하지 않는 페이지에 접근하였습니다. 홈 페이지로 이동합니다.", @@ -15,21 +15,21 @@ "login": "로그인", "please_log_in": "로그인해 주세요.", "logout": "로그아웃", - "posting_restriction_info": "게시물 작성은 현재 회원에게만 제한되고 있습니다. 여기를 누르면 로그인 페이지로 이동합니다.", + "posting_restriction_info": "현재 회원들만 게시물을 작성 할 수 있습니다.여기를 누르면 로그인 페이지로 이동합니다.", "welcome_back": "환영합니다.", "you_have_successfully_logged_in": "성공적으로 로그인했습니다.", "save_changes": "저장", "save": "저장", "close": "닫기", "pagination": "페이지", - "pagination.out_of": "%2개 중 %1개", + "pagination.out_of": "%2 개 중 %1 개", "pagination.enter_index": "이동할 게시물 번호를 입력하세요.", "header.admin": "관리자", - "header.categories": "카테고리", - "header.recent": "최근 주제", - "header.unread": "읽지 않은 주제", - "header.tags": "꼬리표", - "header.popular": "인기 주제", + "header.categories": "게시판", + "header.recent": "최근 게시물", + "header.unread": "읽지 않은 게시물", + "header.tags": "태그", + "header.popular": "인기 게시물", "header.users": "사용자", "header.groups": "그룹", "header.chats": "채팅", @@ -46,35 +46,35 @@ "alert.error": "오류", "alert.banned": "차단됨", "alert.banned.message": "이 계정은 차단되었습니다. 지금 로그아웃됩니다.", - "alert.unfollow": "더 이상 %1님을 팔로우하지 않습니다.", - "alert.follow": "%1님을 팔로우합니다.", + "alert.unfollow": "더 이상 %1 님을 팔로우 하지않습니다.", + "alert.follow": "%1 님을 팔로우 합니다.", "online": "온라인", "users": "사용자", - "topics": "주제", - "posts": "게시물", + "topics": "게시물", + "posts": "포스트", "best": "베스트", - "upvoters": "추천한 유저", - "upvoted": "Upvoted", - "downvoters": "비추천한 유저", - "downvoted": "비추됨", + "upvoters": "추천한 사용자", + "upvoted": "추천된 게시물", + "downvoters": "비추천한 사용자", + "downvoted": "비추천된 게시물", "views": "조회 수", - "reputation": "인기도", - "read_more": "전체 보기", + "reputation": "등급", + "read_more": "더 보기", "more": "더 보기", - "posted_ago_by_guest": "익명 사용자가 %1에 작성했습니다.", - "posted_ago_by": "%2님이 %1에 작성했습니다.", - "posted_ago": "%1에 작성되었습니다.", - "posted_in": "%1에 작성되었습니다.", - "posted_in_by": "%2님이 %1에 작성했습니다.", - "posted_in_ago": "%2 %1에 작성되었습니다. ", - "posted_in_ago_by": "%3님이 %2 %1에 작성했습니다.", - "user_posted_ago": "%1님이 %2에 작성했습니다.", - "guest_posted_ago": "익명 사용자가 %1에 작성했습니다.", - "last_edited_by": "%1님이 마지막으로 수정했습니다.", - "norecentposts": "최근 작성된 게시물이 없습니다.", - "norecenttopics": "최근 생성된 생성된 주제가 없습니다.", - "recentposts": "최근 게시물", - "recentips": "최근 로그인 IP", + "posted_ago_by_guest": "미가입 사용자가 %1 에 작성했습니다.", + "posted_ago_by": "%2 님이 %1 에 작성했습니다.", + "posted_ago": "%1 에 작성되었습니다.", + "posted_in": "%1 에 작성되었습니다.", + "posted_in_by": "%2 님이 %1 에 작성했습니다.", + "posted_in_ago": "%2 %1 에 작성되었습니다. ", + "posted_in_ago_by": "%3 님이 %2 %1 에 작성했습니다.", + "user_posted_ago": "%1 님이 %2 에 작성했습니다.", + "guest_posted_ago": "미가입 사용자가 %1 에 작성했습니다.", + "last_edited_by": "%1 님이 마지막으로 수정했습니다.", + "norecentposts": "최근 작성된 포스트가 없습니다.", + "norecenttopics": "최근 작성된 게시물이 없습니다.", + "recentposts": "최근 포스트", + "recentips": "최근에 로그인한 IP", "moderator_tools": "(준)관리자 도구모음", "away": "자리 비움", "dnd": "방해 금지", @@ -85,7 +85,7 @@ "guest": "익명 사용자", "guests": "익명 사용자", "updated.title": "포럼이 업데이트 되었습니다.", - "updated.message": "이 포럼은 지금 최신 버전으로 업데이트 되었습니다. 여기를 누르면 페이지를 새로고침합니다.", + "updated.message": "이 포럼은 지금 최신 버전으로 업데이트 되었습니다. 페이지를 새로고침 하시려면 여기를 클릭해주세요.", "privacy": "개인정보", "follow": "팔로우", "unfollow": "언팔로우", diff --git a/public/language/ko/groups.json b/public/language/ko/groups.json index 2d4bd2f4b0..6b7c393ee2 100644 --- a/public/language/ko/groups.json +++ b/public/language/ko/groups.json @@ -1,28 +1,28 @@ { "groups": "그룹", "view_group": "그룹 보기", - "owner": "그룹관리자", - "new_group": "그룹 생성", + "owner": "그룹 관리자", + "new_group": "새로운 그룹 만들기", "no_groups_found": "그룹이 없습니다.", "pending.accept": "수락", "pending.reject": "거절", "pending.accept_all": "전체 수락", "pending.reject_all": "전체 거절", - "pending.none": "지금은 승인대기 회원이 없습니다.", + "pending.none": "지금은 승인 대기중인 회원이 없습니다.", "invited.none": "지금은 초대된 회원이 없습니다.", - "invited.uninvite": "초대를 철회", - "invited.search": "그룹에 초대할 사용자를 검색하세요.", + "invited.uninvite": "초대 취소", + "invited.search": "그룹에 초대할 사용자 검색", "invited.notification_title": "%1 그룹에 초대되었습니다.", - "request.notification_title": "%1 님으로부터 그룹 구성원 요청이 들어왔습니다.", - "request.notification_text": "%1 님이 %2 의 멤버 가입을 신청했습니다.", + "request.notification_title": "%1 님으로부터 그룹 가입 요청이 들어왔습니다.", + "request.notification_text": "%1 님이 %2 에 가입을 신청했습니다.", "cover-save": "저장", "cover-saving": "저장 중", "details.title": "그룹 상세정보", "details.members": "구성원 목록", - "details.pending": "대기 구성원", + "details.pending": "승인 대기중인 구성원", "details.invited": "초대된 구성원", "details.has_no_posts": "이 그룹의 구성원이 작성한 게시물이 없습니다.", - "details.latest_posts": "최근 게시물", + "details.latest_posts": "최근 포스트", "details.private": "비공개", "details.disableJoinRequests": "가입 신청 비활성화하기", "details.grant": "소유권 이전/포기하기", @@ -38,7 +38,7 @@ "details.change_colour": "컬러 변경", "details.badge_text": "배지 문구", "details.userTitleEnabled": "배지 보이기", - "details.private_help": "활성 후 구성원 가입시 그룹 관리자의 승인이 필요합니다.", + "details.private_help": "활성 시, 구성원 가입시 그룹 관리자의 승인이 필요합니다.", "details.hidden": "숨김", "details.hidden_help": "활성 시 그룹 목록에 노출되지 않습니다. 또한 구성원은 초대를 통해서만 가입이 가능합니다.", "details.delete_group": "그룹 삭제", @@ -51,7 +51,7 @@ "membership.leave-group": "그룹 나가기", "membership.reject": "거절", "new-group.group_name": "그룹명:", - "upload-group-cover": "그룹 커버 업로드", + "upload-group-cover": "그룹 커버 사진 업로드", "bulk-invite-instructions": "초대하고자 하는 사용자 목록을 콤마(,) 로 구분하여 기입해 주십시오.\n예: bravominski, stjohndlee, yoojkim", "bulk-invite": "다수의 사용자 초대", "remove_group_cover_confirm": "해당 커버 사진을 제거 하시겠습니까?" diff --git a/public/language/ko/login.json b/public/language/ko/login.json index 527fe9aa3a..a1691ee955 100644 --- a/public/language/ko/login.json +++ b/public/language/ko/login.json @@ -1,9 +1,9 @@ { - "username-email": "사용자 이름 / 이메일", + "username-email": "사용자명 / 이메일", "username": "사용자명", "email": "이메일", "remember_me": "로그인 유지", - "forgot_password": "비밀번호 초기화", + "forgot_password": "비밀번호 재설정", "alternative_logins": "다른 방법으로 로그인", "failed_login_attempt": "로그인 실패", "login_successful": "성공적으로 로그인했습니다.", diff --git a/public/language/ko/modules.json b/public/language/ko/modules.json index 0325e49b4a..521e73bed6 100644 --- a/public/language/ko/modules.json +++ b/public/language/ko/modules.json @@ -1,33 +1,33 @@ { - "chat.chatting_with": "", + "chat.chatting_with": " 님과의 채팅", "chat.placeholder": "메시지를 여기에 입력한 후 엔터를 눌러 전송하세요.", "chat.send": "전송", "chat.no_active": "활성화된 채팅이 없습니다.", - "chat.user_typing": "%1님이 입력 중입니다.", - "chat.user_has_messaged_you": "%1님이 메시지를 보냈습니다.", - "chat.see_all": "모든 대화 보기", - "chat.mark_all_read": "읽은 채팅 읽음으로 표시", - "chat.no-messages": "대화 기록을 보려면 대화 상대를 선택하세요.", + "chat.user_typing": "%1 님이 입력 중입니다.", + "chat.user_has_messaged_you": "%1 님이 메시지를 보냈습니다.", + "chat.see_all": "모든 채팅 보기", + "chat.mark_all_read": "모든 채팅 읽음으로 표시", + "chat.no-messages": "채팅 기록을 보려면 채팅 상대를 선택하세요.", "chat.no-users-in-room": "사용자가 없습니다.", - "chat.recent-chats": "최근 대화 내용", + "chat.recent-chats": "최근 채팅", "chat.contacts": "연락처", "chat.message-history": "대화 기록", - "chat.pop-out": "분리된 창에서 대화", + "chat.pop-out": "분리된 창에서 채팅", "chat.minimize": "최소화", "chat.maximize": "최대화", "chat.seven_days": "7일", "chat.thirty_days": "30일", "chat.three_months": "3개월", - "chat.delete_message_confirm": "이 대화를 삭제하시겠습니까?", - "chat.add-users-to-room": "유저 추가하기", + "chat.delete_message_confirm": "이 메세지를 삭제 하시겠습니까?", + "chat.add-users-to-room": "사용자 추가하기", "chat.confirm-chat-with-dnd-user": "이 사용자의 상태는 \"방해금지\" 입니다. 그래도 대화를 요청 하시겠습니까?", "composer.compose": "작성", "composer.show_preview": "미리보기", "composer.hide_preview": "미리보기 숨김", - "composer.user_said_in": "%1님이 %2에서 한 말:", - "composer.user_said": "%1님의 말:", - "composer.discard": "이 게시물을 지우겠습니까?", - "composer.submit_and_lock": "잠금 상태로 작성 완료", + "composer.user_said_in": "%1 님이 %2 에서 보낸 메세지:", + "composer.user_said": "%1 님의 메세지:", + "composer.discard": "이 포스트를 삭제 하시겠습니까?", + "composer.submit_and_lock": "글 작성 후 잠금", "composer.toggle_dropdown": "내려서 확인하기", "composer.uploading": "%1 업로드 중", "composer.formatting.bold": "굵은글씨", diff --git a/public/language/ko/notifications.json b/public/language/ko/notifications.json index f13cde61bc..cb3b29e88d 100644 --- a/public/language/ko/notifications.json +++ b/public/language/ko/notifications.json @@ -3,46 +3,46 @@ "no_notifs": "새 알림이 없습니다.", "see_all": "모든 알림 보기", "mark_all_read": "모든 알림을 읽음 상태로 변경", - "back_to_home": "%1로 돌아가기", + "back_to_home": "%1 로 돌아가기", "outgoing_link": "외부 링크", - "outgoing_link_message": "%1 에서 나오셨습니다.", + "outgoing_link_message": "%1 를 떠납니다.", "continue_to": "%1 사이트로 이동", "return_to": "%1 사이트로 돌아가기", "new_notification": "새 알림", "you_have_unread_notifications": "읽지 않은 알림이 있습니다.", "all": "모든 알림", - "topics": "토픽", + "topics": "게시물", "replies": "답글", - "chat": "대화", + "chat": "채팅", "follows": "팔로우", "upvote": "추천", "new-flags": "새로 들어온 신고", "my-flags": "내게 배정된 신고", - "bans": "접근 금지", + "bans": "차단", "new_message_from": "%1님이 메시지를 보냈습니다.", - "upvoted_your_post_in": "%1님이 %2의 내 게시물을 추천했습니다.", - "upvoted_your_post_in_dual": "%1님과 %2님이 %3의 내 게시물을 추천했습니다.", - "upvoted_your_post_in_multiple": "%1 님과 다른 %2 명이 %3의 내 게시물을 추천했습니다.", - "moved_your_post": "%1님이 귀하의 게시물을 %2로 옮겼습니다.", - "moved_your_topic": "%1%2 로 옮겨졌습니다.", - "user_flagged_post_in": "%1님이 %2의 게시물을 신고했습니다.", - "user_flagged_post_in_dual": "%1 님과 %2 님이 %3 안의 게시물에 플래그를 세웠습니다.", - "user_flagged_post_in_multiple": "%1 님과 %2 명의 다른 유저들이 %3 안의 게시물에 플래그를 세웠습니다.", + "upvoted_your_post_in": "%1님이 %2의 내 포스트를 추천했습니다.", + "upvoted_your_post_in_dual": "%1님과 %2님이 %3의 내 포스트를 추천했습니다.", + "upvoted_your_post_in_multiple": "%1 님과 다른 %2 명이 %3의 내 포스트를 추천했습니다.", + "moved_your_post": "%1님이 귀하의 포스트를 %2로 옮겼습니다.", + "moved_your_topic": "%1%2 를 옮겼습니다.", + "user_flagged_post_in": "%1님이 %2에 속한 포스트를 신고했습니다.", + "user_flagged_post_in_dual": "%1 님과 %2 님이 %3 에 속한 포스트를 신고했습니다.", + "user_flagged_post_in_multiple": "%1 님과 %2 명의 다른 유저들이 %3 에 속한 포스트를 신고했습니다.", "user_flagged_user": "%1님이 %2의 프로필을 신고했습니다.", "user_flagged_user_dual": "%1님과 %2님이 %3의 프로필을 신고했습니다.", "user_flagged_user_multiple": "%1 님과 다른 %2 명이 %3의 프로필을 신고했습니다.", - "user_posted_to": "%1님이 %2에 답글을 작성했습니다.", + "user_posted_to": "%1님이 %2 에 답글을 달았습니다.", "user_posted_to_dual": "%1 님과 %2 님이 %3 에 답글을 달았습니다.", "user_posted_to_multiple": "%1 님과 %2 명의 다른 유저들이 %3 에 답글을 달았습니다.", - "user_posted_topic": "%1님이 새 주제를 작성했습니다: %2", - "user_started_following_you": "%1님이 나를 팔로우합니다.", - "user_started_following_you_dual": "%1님과 %2님이 당신을 팔로우 시작했습니다.", - "user_started_following_you_multiple": "%1님외 %2명이 당신을 팔로우 시작했습니다.", + "user_posted_topic": "%1님이 새 게시물을 작성했습니다: %2", + "user_started_following_you": "%1님이 나를 팔로우 합니다.", + "user_started_following_you_dual": "%1님과 %2님이 나를 팔로우 합니다.", + "user_started_following_you_multiple": "%1님외 %2명이 나를 팔로우 합니다.", "new_register": "%1님이 가입요청을 했습니다.", - "new_register_multiple": "%1 개의 회원가입 요청이 승인 대기중입니다.", - "flag_assigned_to_you": "신고 ID %1 이 배정되었습니다.", - "email-confirmed": "확인된 이메일", - "email-confirmed-message": "이메일을 확인해주셔서 감사합니다. 계정이 완전히 활성화되었습니다.", - "email-confirm-error-message": "이메일 주소를 검증하지 못했습니다. 코드가 올바르지 않거나 만료되었을 수 있습니다.", + "new_register_multiple": "%1 개의 회원가입 요청이 승인 대기중입니다.", + "flag_assigned_to_you": "신고 ID %1 이 나에게 배정되었습니다.", + "email-confirmed": "이메일 인증 되었습니다", + "email-confirmed-message": "이메일을 인증해주셔서 감사합니다. 계정이 완전히 활성화되었습니다.", + "email-confirm-error-message": "이메일 주소를 인증하지 못했습니다. 코드가 올바르지 않거나 만료 되었을 수 있습니다.", "email-confirm-sent": "확인 이메일이 발송되었습니다." } \ No newline at end of file diff --git a/public/language/ko/pages.json b/public/language/ko/pages.json index a5566ccf84..5bcd9ba573 100644 --- a/public/language/ko/pages.json +++ b/public/language/ko/pages.json @@ -1,51 +1,51 @@ { "home": "홈", - "unread": "읽지 않은 주제", - "popular-day": "인기있는 주제 (오늘)", - "popular-week": "인기있는 주제 (주간)", - "popular-month": "인기있는 주제 (월간)", - "popular-alltime": "인기있는 주제", - "recent": "최근 주제", - "flagged-content": "플래그된 컨텐츠", + "unread": "읽지 않은 게시물", + "popular-day": "인기있는 게시물 (오늘)", + "popular-week": "인기있는 게시물 (주간)", + "popular-month": "인기있는 게시물 (월간)", + "popular-alltime": "인기있는 게시물", + "recent": "최근 게시물", + "flagged-content": "신고된 컨텐츠", "ip-blacklist": "IP 블랙리스트", - "users/online": "온라인 사용자", + "users/online": "접속중인 사용자", "users/latest": "최근 사용자", "users/sort-posts": "가장 많은 게시물을 작성한 사용자", - "users/sort-reputation": "가장 높은 인지도를 가진 사용자", - "users/banned": "차단된 유저", - "users/most-flags": "가장 많이 플래그가 달린 사용자", + "users/sort-reputation": "가장 높은 등급을 가진 사용자", + "users/banned": "차단된 사용자", + "users/most-flags": "가장 많이 신고된 사용자", "users/search": "사용자 검색", "notifications": "알림", "tags": "태그", - "tag": "\"%1\" 꼬리표가 달린 주제", + "tag": "\"%1\" 태그가 달린 게시물", "register": "회원가입", "registration-complete": "회원가입 완료", "login": "로그인", - "reset": "패스워드 초기화", - "categories": "카테고리", + "reset": "패스워드 재설정", + "categories": "게시판", "groups": "그룹", "group": "%1 그룹", - "chats": "대화", - "chat": "%1 님과 대화", + "chats": "채팅", + "chat": "%1 님과 채팅", "flags": "신고 목록", "flag-details": "신고 ID %1 의 세부내용", - "account/edit": "\"%1\" 편집", - "account/edit/password": "\"%1\" 의 패스워드 변경", - "account/edit/username": "\"%1\" 의 사용자명 변경", - "account/edit/email": "\"%1\" 의 이메일 변경", + "account/edit": "\"%1\" 수정", + "account/edit/password": "\"%1\" 의 패스워드 수정", + "account/edit/username": "\"%1\" 의 사용자명 수정", + "account/edit/email": "\"%1\" 의 이메일 수정", "account/info": "계정 정보", - "account/following": "%1 명이 팔로우", - "account/followers": "%1 명을 팔로우", - "account/posts": "%1 님이 작성한 게시물", - "account/topics": "%1 님이 생성한 주제", - "account/groups": "%1님의 그룹", - "account/bookmarks": "%1 님이 즐겨찾기 한 게시물", + "account/following": "%1 님이 팔로우 하는 사용자", + "account/followers": "%1 님을 팔로우 하는 사용자", + "account/posts": "%1 님이 작성한 포스트", + "account/topics": "%1 님이 생성한 게시물", + "account/groups": "%1 님의 그룹", + "account/bookmarks": "%1 님이 즐겨찾기 한 포스트", "account/settings": "사용자 설정", - "account/watched": "%1님이 지켜보는 주제", - "account/upvoted": "%1 님이 upvote한 게시물", - "account/downvoted": "%1 님에 의해 Downvote된 게시물", - "account/best": "%1 님 최고의 게시물", - "confirm": "확인된 이메일", + "account/watched": "%1 님이 관심 가진 게시물", + "account/upvoted": "%1 님이 추천한 포스트", + "account/downvoted": "%1 님에 비추천한 포스트", + "account/best": "%1 님 최고의 포스트", + "confirm": "이메일 인증 되었습니다", "maintenance.text": "%1 사이트는 현재 점검 중입니다. 나중에 다시 방문해주세요.", "maintenance.messageIntro": "다음은 관리자가 전하는 메시지입니다.", "throttled.text": "과도한 부하로 %1 를 로드할 수 없습니다. 잠시후에 다시 시도해주세요." diff --git a/public/language/ko/recent.json b/public/language/ko/recent.json index 2c795f1970..5773b12a48 100644 --- a/public/language/ko/recent.json +++ b/public/language/ko/recent.json @@ -1,19 +1,19 @@ { - "title": "최근 주제", + "title": "최근 게시글", "day": "일간", "week": "주간", "month": "월간", "year": "연간", "alltime": "전체", - "no_recent_topics": "최근 생성된 주제가 없습니다.", - "no_popular_topics": "인기 주제가 없습니다.", - "there-is-a-new-topic": "새로운 주제가 있습니다.", - "there-is-a-new-topic-and-a-new-post": "새로운 주제와 게시물이 있습니다.", - "there-is-a-new-topic-and-new-posts": "새로운 주제와 %1 개의 게시물이 있습니다.", - "there-are-new-topics": "새로운 %1 개의 주제가 있습니다.", - "there-are-new-topics-and-a-new-post": "새로운 %1 개의 주제와 게시물이 있습니다.", - "there-are-new-topics-and-new-posts": "새로운 %1 개의 주제와 %2 개의 게시물이 있습니다.", - "there-is-a-new-post": "새로운 게시물이 있습니다.", - "there-are-new-posts": "새로운 %1 개의 게시물이 있습니다.", + "no_recent_topics": "최근 생성된 게시물이 없습니다.", + "no_popular_topics": "인기 게시물이 없습니다.", + "there-is-a-new-topic": "새로운 게시물이 있습니다.", + "there-is-a-new-topic-and-a-new-post": "새로운 게시물과 포스트가 있습니다.", + "there-is-a-new-topic-and-new-posts": "새로운 게시물과 %1 개의 포스트가 있습니다.", + "there-are-new-topics": "새로운 %1 개의 게시물이 있습니다.", + "there-are-new-topics-and-a-new-post": "새로운 %1 개의 게시물과 포스트가 있습니다.", + "there-are-new-topics-and-new-posts": "새로운 %1 개의 게시물과 %2 개의 포스트가 있습니다.", + "there-is-a-new-post": "새로운 포스트가 있습니다.", + "there-are-new-posts": "새로운 %1 개의 포스트가 있습니다.", "click-here-to-reload": "이 곳을 클릭하여 새로고침 하세요." } \ No newline at end of file diff --git a/public/language/ko/register.json b/public/language/ko/register.json index 6db2503ed4..6f2d19f5b2 100644 --- a/public/language/ko/register.json +++ b/public/language/ko/register.json @@ -2,17 +2,17 @@ "register": "회원가입", "cancel_registration": "회원가입 취소", "help.email": "입력하신 이메일 주소는 공개되지 않으며, 설정을 통해 공개하실 수 있습니다.", - "help.username_restrictions": "%1자 이상 %2자 이하의 고유한 이름을 입력하세요. @username 같은 방식으로 다른 사람들을 언급할 수 있습니다.", + "help.username_restrictions": "%1자 이상 %2자 이하의 고유한 사용자명을 입력하세요. @username 같은 방식으로 다른 사람들을 언급할 수 있습니다.", "help.minimum_password_length": "비밀번호는 최소 %1자로 제한됩니다.", "email_address": "이메일", "email_address_placeholder": "여기에 이메일 주소를 입력하세요.", - "username": "사용자 이름", - "username_placeholder": "여기에 사용자 이름을 입력하세요.", + "username": "사용자명", + "username_placeholder": "여기에 사용자명을 입력하세요.", "password": "비밀번호", "password_placeholder": "여기에 비밀번호를 입력하세요.", - "confirm_password": "비밀번호 재입력", - "confirm_password_placeholder": "여기에 비밀번호를 재입력하세요.", - "register_now_button": "회원가입", + "confirm_password": "비밀번호 확인", + "confirm_password_placeholder": "여기에 비밀번호 확인을 입력하세요.", + "register_now_button": "가입하기", "alternative_registration": "다른 방법으로 회원가입", "terms_of_use": "이용약관", "agree_to_terms_of_use": "이용약관에 동의합니다.", diff --git a/public/language/ko/reset_password.json b/public/language/ko/reset_password.json index 6701e2fa7d..98e774c668 100644 --- a/public/language/ko/reset_password.json +++ b/public/language/ko/reset_password.json @@ -1,17 +1,17 @@ { - "reset_password": "비밀번호 초기화", - "update_password": "비밀번호 변경", - "password_changed.title": "비밀번호가 변경되었습니다.", - "password_changed.message": "

성공적으로 비밀번호가 초기화되었습니다. 다시 로그인해 주세요.", + "reset_password": "패스워드 재설정", + "update_password": "패스워드 변경", + "password_changed.title": "패스워드가 변경되었습니다.", + "password_changed.message": "

성공적으로 패스워드가 재설정 되었습니다. 다시 로그인해 주세요.", "wrong_reset_code.title": "올바르지 않은 초기화 코드입니다.", - "wrong_reset_code.message": "올바르지 않은 초기화 코드입니다. 다시 시도하거나 새로운 초기화 코드를 요청하세요.", - "new_password": "새 비밀번호", - "repeat_password": "비밀번호 재입력", - "enter_email": "이메일 주소를 입력하면 비밀번호를 초기화하는 방법을 메일로 알려드립니다.", - "enter_email_address": "여기에 이메일 주소를 입력하세요.", - "password_reset_sent": "이메일이 발송되었습니다.", + "wrong_reset_code.message": "올바르지 않은 재설정 코드입니다. 다시 시도하거나 새로운 재설정 코드를 요청하세요.", + "new_password": "새 패스워드", + "repeat_password": "패스워드 확인", + "enter_email": "이메일 주소를 입력하면 패스워드를 재설정하는 방법을 메일로 알려드립니다.", + "enter_email_address": "이메일 주소를 입력하세요.", + "password_reset_sent": "패스워드 재설정 이메일이 발송되었습니다.", "invalid_email": "올바르지 않거나 가입되지 않은 이메일입니다.", - "password_too_short": "입력한 비밀번호가 너무 짧습니다, 다시 입력하세요.", - "passwords_do_not_match": "비밀번호와 재입력한 비밀번호가 일치하지 않습니다.", - "password_expired": "비밀번호가 만료되었습니다, 새로운 비밀번호를 입력하세요." + "password_too_short": "입력한 패스워드가 너무 짧습니다, 다시 입력하세요.", + "passwords_do_not_match": "패스워드와 패스워드 확인이 일치하지 않습니다.", + "password_expired": "패스워드가 만료되었습니다, 새로운 패스워드를 입력하세요." } \ No newline at end of file diff --git a/public/language/pl/admin/advanced/database.json b/public/language/pl/admin/advanced/database.json index dd365e5b0b..b35c3abe2e 100644 --- a/public/language/pl/admin/advanced/database.json +++ b/public/language/pl/admin/advanced/database.json @@ -7,7 +7,7 @@ "mongo": "Mongo", "mongo.version": "Wersja MongoDB", - "mongo.storage-engine": "Storage Engine", + "mongo.storage-engine": "Silnik Magazynowania", "mongo.collections": "Kolekcje", "mongo.objects": "Obiekty", "mongo.avg-object-size": "Przybliżony rozmiar obiektu", @@ -15,7 +15,7 @@ "mongo.storage-size": "Rozmiar pamięci", "mongo.index-size": "Rozmiar indeksu", "mongo.file-size": "Rozmiar pliku", - "mongo.resident-memory": "Resident Memory", + "mongo.resident-memory": "Przynależna Pamięć", "mongo.virtual-memory": "Pamięc wirtualna", "mongo.mapped-memory": "Pamięc zmapowana", "mongo.raw-info": "Surowe informacje MongoDB", @@ -29,8 +29,8 @@ "redis.memory-frag-ratio": "Proporcja fragmentacji pamięci", "redis.total-connections-recieved": "Otrzymanych połączeń", "redis.total-commands-processed": "Przetworzonych połączeń", - "redis.iops": "Instantaneous Ops. Per Second", - "redis.keyspace-hits": "Keyspace Hits", - "redis.keyspace-misses": "Keyspace Misses", + "redis.iops": "Natychmiastowe Operacje Na Sekundę", + "redis.keyspace-hits": "Trafienia Kluczy", + "redis.keyspace-misses": "Nietrafione Klucze", "redis.raw-info": "Surowe informacje Redis" } \ No newline at end of file diff --git a/public/language/pl/admin/appearance/customise.json b/public/language/pl/admin/appearance/customise.json index 0e56eb3d69..99dc3569f7 100644 --- a/public/language/pl/admin/appearance/customise.json +++ b/public/language/pl/admin/appearance/customise.json @@ -7,6 +7,6 @@ "custom-header.description": "Wpisz tutaj kod HTML (JavaScript, tagi <meta>) który ma być dołączony do sekcji <head> w szablonie forum.", "custom-header.enable": "Włącz własny nagłówek", - "custom-css.livereload": "Enable Live Reload", - "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" + "custom-css.livereload": "Włącz żywe przeładowanie", + "custom-css.livereload.description": "Włącz to, aby zmusić wszystkie sesje do odświeżenia na każdym urządzeniu na twoim koncie gdziekolwiek klikniesz zapisz." } \ No newline at end of file diff --git a/public/language/pl/admin/development/info.json b/public/language/pl/admin/development/info.json index 2213ad9909..c018cfdf8c 100644 --- a/public/language/pl/admin/development/info.json +++ b/public/language/pl/admin/development/info.json @@ -1,12 +1,12 @@ { "you-are-on": "Informacja - Jesteś na %1:%2", - "nodes-responded": "%1 nodes responded within %2ms!", + "nodes-responded": "%1 nodes odpowiedziały w ciągu %2ms!", "host": "Host", "pid": "pid", "nodejs": "nodejs", "online": "online", "git": "git", - "memory": "memory", + "memory": "pamięć", "load": "obciążenie", "uptime": "uptime", diff --git a/public/language/pl/admin/extend/plugins.json b/public/language/pl/admin/extend/plugins.json index 6ac7776536..ee9d5675b7 100644 --- a/public/language/pl/admin/extend/plugins.json +++ b/public/language/pl/admin/extend/plugins.json @@ -14,8 +14,8 @@ "dev-interested": "Zainteresowany pisanie pluginów do NodeBB??", "docs-info": "Pełna dokumentacje dotycząca pisania pluginów znajduje się tutaj NodeBB Docs Portal.", - "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", - "order.explanation": "Plugins load in the order specified here, from top to bottom", + "order.description": "Pewne pluginy działają idealnie kiedy są zainicjalizowane przed/po innymi pluginami.", + "order.explanation": "Pluginy ładują się tutaj w określonej kolejności, od góry do dołu.", "plugin-item.themes": "Style", "plugin-item.deactivate": "Dezaktywować", @@ -28,7 +28,7 @@ "plugin-item.upgrade": "Zaktualizuj", "plugin-item.more-info": "Po więcej informacji:", "plugin-item.unknown": "Nieznane", - "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.unknown-explanation": "Stan tego pluginu nie może być ustalony, prawdopodobnie z powodu błędu konfiguracji błędów.", "alert.enabled": "Plugin Włączony", "alert.disabled": "Plugin Wyłączony", @@ -40,8 +40,8 @@ "alert.upgrade-success": "Proszę przeładować NodeBB, aby poprawnie działał plugin", "alert.install-success": "Plugin pomyślnie zainstalowany, proszę aktywować go.", "alert.uninstall-success": "Plugin został pomyślnie zdezaktywowany oraz odinstalowany.", - "alert.suggest-error": "

NodeBB could not reach the package manager, proceed with installation of latest version?

Server returned (%1): %2
", - "alert.package-manager-unreachable": "

NodeBB could not reach the package manager, an upgrade is not suggested at this time.

", - "alert.incompatible": "

Your version of NodeBB (v%1) is only cleared to upgrade to v%2 of this plugin. Please update your NodeBB if you wish to install a newer version of this plugin.

", + "alert.suggest-error": "

NodeBB nie może dostać się do menedżera pakietów, kontynuować instalacji ostatniej wersji?

Serwer zwrócił (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB nie może dostać się do menedżera pakietów, aktualizacja nie jest sugerowana w tym momencie.

", + "alert.incompatible": "

Twoja wersja NodeBB (v%1) jest usuwana w celu uaktualnienia do v%2 tego pluginu. Proszę zaktualizuj twoje NodeBB jeżeli chcesz zainstalować nowszą wersję tego pluginu.

", "alert.possibly-incompatible": "

No Compatibility Information Found

This plugin did not specify a specific version for installation given your NodeBB version. Full compatibility cannot be guaranteed, and may cause your NodeBB to no longer start properly.

In the event that NodeBB cannot boot properly:

$ ./nodebb reset plugin=\"%1\"

Continue installation of latest version of this plugin?

" } diff --git a/public/language/pl/groups.json b/public/language/pl/groups.json index a51f362e60..59b297af7c 100644 --- a/public/language/pl/groups.json +++ b/public/language/pl/groups.json @@ -27,7 +27,7 @@ "details.disableJoinRequests": "Wyłączono prośbę o dołączenie", "details.grant": "Nadaj/Cofnij prawa Właściciela", "details.kick": "Wykop", - "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.kick_confirm": "Jesteś pewny, że chcesz wyrzucić tego użytkownika z grupy?", "details.owner_options": "Administracja grupy", "details.group_name": "Nazwa grupy", "details.member_count": "Liczba Członków", diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json index 1afc9a68da..222a9ef591 100644 --- a/public/language/pl/modules.json +++ b/public/language/pl/modules.json @@ -20,7 +20,7 @@ "chat.three_months": "3 miesiące", "chat.delete_message_confirm": "Jesteś pewny, że chcesz usunąć tą wiadomość?", "chat.add-users-to-room": "Dodaj użytkownika do pokoju czatu", - "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.confirm-chat-with-dnd-user": "Ten użytkownik ustawił swój status na \"nie przeszkadzać\". Czy wciąż chcesz z nim rozmawiać?", "composer.compose": "Twórz", "composer.show_preview": "Pokaż Podgląd", "composer.hide_preview": "Ukryj Podgląd", diff --git a/public/src/client/category.js b/public/src/client/category.js index c712c9438b..6bf2ea0ffc 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -15,8 +15,8 @@ define('forum/category', [ ], function (infinitescroll, share, navigator, categoryTools, sort, components, translator, topicSelect, pagination, storage) { var Category = {}; - $(window).on('action:ajaxify.end', function (ev, data) { - if (data.tpl_url !== 'category') { + $(window).on('action:ajaxify.start', function (ev, data) { + if (data.url && !data.url.startsWith('category/')) { navigator.disable(); removeListeners(); diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 3651c8be2b..08a707f029 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -23,16 +23,14 @@ define('forum/topic', [ Topic.replaceURLTimeout = 0; } - if (ajaxify.currentPage !== data.url) { + if (data.url && !data.url.startsWith('topic/')) { navigator.disable(); components.get('navbar/title').find('span').text('').hide(); app.removeAlert('bookmark'); events.removeListeners(); $(window).off('keydown', onKeyDown); - } - if (data.url && !data.url.startsWith('topic/')) { require(['search'], function (search) { if (search.topicDOM.active) { search.topicDOM.end(); diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 707a9a5b0f..90b845538e 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -199,7 +199,7 @@ define('forum/topic/postTools', [ var selectedNode = getSelectedNode(); showStaleWarning(function () { - var username = getUserName(button); + var username = getUserSlug(button); if (getData(button, 'data-uid') === '0' || !getData(button, 'data-userslug')) { username = ''; } @@ -231,7 +231,7 @@ define('forum/topic/postTools', [ var selectedNode = getSelectedNode(); showStaleWarning(function () { - var username = getUserName(button); + var username = getUserSlug(button); var toPid = getData(button, 'data-pid'); function quote(text) { @@ -284,7 +284,7 @@ define('forum/topic/postTools', [ selectedText = range.toString(); var postEl = $(content).parents('[component="post"]'); selectedPid = postEl.attr('data-pid'); - username = getUserName($(content)); + username = getUserSlug($(content)); range.detach(); } return { text: selectedText, pid: selectedPid, username: username }; @@ -309,22 +309,22 @@ define('forum/topic/postTools', [ return button.parents('[data-pid]').attr(data); } - function getUserName(button) { - var username = ''; + function getUserSlug(button) { + var slug = ''; var post = button.parents('[data-pid]'); if (button.attr('component') === 'topic/reply') { - return username; + return slug; } if (post.length) { - username = post.attr('data-username').replace(/\s/g, '-'); + slug = post.attr('data-userslug'); } if (post.length && post.attr('data-uid') !== '0') { - username = '@' + username; + slug = '@' + slug; } - return username; + return slug; } function togglePostDelete(button, tid) { diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index 93583d6ed9..5dfa5def2e 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -470,6 +470,9 @@ define('settings', function () { } } + // Save loaded settings into ajaxify.data for use client-side + ajaxify.data.settings = values; + $(formEl).deserialize(values); $(formEl).find('input[type="checkbox"]').each(function () { $(this).parents('.mdl-switch').toggleClass('is-checked', $(this).is(':checked')); @@ -510,6 +513,9 @@ define('settings', function () { // Remove unsaved flag to re-enable ajaxify app.flags._unsaved = false; + // Also save to local ajaxify.data + ajaxify.data.settings = values; + if (typeof callback === 'function') { callback(err); } else if (err) { diff --git a/src/controllers/category.js b/src/controllers/category.js index a2e3ef3141..8edc68d076 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -22,6 +22,7 @@ categoryController.get = function (req, res, callback) { var pageCount = 1; var userPrivileges; var settings; + var rssToken; if ((req.params.topic_index && !utils.isNumber(req.params.topic_index)) || !utils.isNumber(cid)) { return callback(); @@ -41,10 +42,14 @@ categoryController.get = function (req, res, callback) { userSettings: function (next) { user.getSettings(req.uid, next); }, + rssToken: function (next) { + user.auth.getFeedToken(req.uid, next); + }, }, next); }, function (results, next) { userPrivileges = results.privileges; + rssToken = results.rssToken; if (!results.categoryData.slug || (results.categoryData && parseInt(results.categoryData.disabled, 10) === 1)) { return callback(); @@ -121,15 +126,15 @@ categoryController.get = function (req, res, callback) { categoryData.description = translator.escape(categoryData.description); categoryData.privileges = userPrivileges; categoryData.showSelect = categoryData.privileges.editable; - - addTags(categoryData, res); - + categoryData.rssFeedUrl = nconf.get('url') + '/category/' + categoryData.cid + '.rss'; if (parseInt(req.uid, 10)) { categories.markAsRead([cid], req.uid); + categoryData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; } + addTags(categoryData, res); + categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; - categoryData.rssFeedUrl = nconf.get('relative_path') + '/category/' + categoryData.cid + '.rss'; categoryData.title = translator.escape(categoryData.name); pageCount = Math.max(1, Math.ceil(categoryData.topic_count / settings.topicsPerPage)); categoryData.pagination = pagination.create(currentPage, pageCount, req.query); @@ -192,7 +197,7 @@ function addTags(categoryData, res) { { rel: 'alternate', type: 'application/rss+xml', - href: nconf.get('url') + '/category/' + categoryData.cid + '.rss', + href: categoryData.rssFeedUrl, }, { rel: 'up', diff --git a/src/controllers/topics.js b/src/controllers/topics.js index c702ba90fc..a794d40c8a 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -15,7 +15,7 @@ var helpers = require('./helpers'); var pagination = require('../pagination'); var utils = require('../utils'); -var topicsController = {}; +var topicsController = module.exports; topicsController.get = function (req, res, callback) { var tid = req.params.topic_id; @@ -23,6 +23,7 @@ topicsController.get = function (req, res, callback) { var pageCount = 1; var userPrivileges; var settings; + var rssToken; if ((req.params.post_index && !utils.isNumber(req.params.post_index)) || !utils.isNumber(tid)) { return callback(); @@ -40,6 +41,9 @@ topicsController.get = function (req, res, callback) { topic: function (next) { topics.getTopicData(tid, next); }, + rssToken: function (next) { + user.auth.getFeedToken(req.uid, next); + }, }, next); }, function (results, next) { @@ -48,6 +52,7 @@ topicsController.get = function (req, res, callback) { } userPrivileges = results.privileges; + rssToken = results.rssToken; if (!userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) { return helpers.notAllowed(req, res); @@ -129,167 +134,173 @@ topicsController.get = function (req, res, callback) { plugins.fireHook('filter:controllers.topic.get', { topicData: topicData, uid: req.uid }, next); }, function (data, next) { - var breadcrumbs = [ - { - text: data.topicData.category.name, - url: nconf.get('relative_path') + '/category/' + data.topicData.category.slug, - }, - { - text: data.topicData.title, - }, - ]; - - helpers.buildCategoryBreadcrumbs(data.topicData.category.parentCid, function (err, crumbs) { - if (err) { - return next(err); - } - data.topicData.breadcrumbs = crumbs.concat(breadcrumbs); - next(null, data.topicData); - }); + buildBreadcrumbs(data.topicData, next); }, - function (topicData, next) { - function findPost(index) { - for (var i = 0; i < topicData.posts.length; i += 1) { - if (parseInt(topicData.posts[i].index, 10) === parseInt(index, 10)) { - return topicData.posts[i]; - } - } + function (topicData) { + topicData.privileges = userPrivileges; + topicData.topicStaleDays = parseInt(meta.config.topicStaleDays, 10) || 60; + topicData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; + topicData['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; + topicData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + topicData.bookmarkThreshold = parseInt(meta.config.bookmarkThreshold, 10) || 5; + topicData.postEditDuration = parseInt(meta.config.postEditDuration, 10) || 0; + topicData.postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10) || 0; + topicData.scrollToMyPost = settings.scrollToMyPost; + topicData.rssFeedUrl = nconf.get('relative_path') + '/topic/' + topicData.tid + '.rss'; + if (req.uid) { + topicData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; } - var description = ''; - var postAtIndex = findPost(Math.max(0, req.params.post_index - 1)); + topicData.postIndex = req.params.post_index; + topicData.pagination = pagination.create(currentPage, pageCount, req.query); + topicData.pagination.rel.forEach(function (rel) { + rel.href = nconf.get('url') + '/topic/' + topicData.slug + rel.href; + res.locals.linkTags.push(rel); + }); - if (postAtIndex && postAtIndex.content) { - description = S(postAtIndex.content).decodeHTMLEntities().stripTags().s; + req.session.tids_viewed = req.session.tids_viewed || {}; + if (!req.session.tids_viewed[tid] || req.session.tids_viewed[tid] < Date.now() - 3600000) { + topics.increaseViewCount(tid); + req.session.tids_viewed[tid] = Date.now(); } - if (description.length > 255) { - description = description.substr(0, 255) + '...'; - } + addTags(topicData, req, res); - var ogImageUrl = ''; - if (topicData.thumb) { - ogImageUrl = topicData.thumb; - } else if (postAtIndex && postAtIndex.user && postAtIndex.user.picture) { - ogImageUrl = postAtIndex.user.picture; - } else if (meta.config['og:image']) { - ogImageUrl = meta.config['og:image']; - } else if (meta.config['brand:logo']) { - ogImageUrl = meta.config['brand:logo']; - } else { - ogImageUrl = '/logo.png'; - } - - if (typeof ogImageUrl === 'string' && ogImageUrl.indexOf('http') === -1) { - ogImageUrl = nconf.get('url') + ogImageUrl; + if (req.uid) { + topics.markAsRead([tid], req.uid, function (err, markedRead) { + if (err) { + return callback(err); + } + if (markedRead) { + topics.pushUnreadCount(req.uid); + topics.markTopicNotificationsRead([tid], req.uid); + } + }); } - description = description.replace(/\n/g, ' '); - - res.locals.metaTags = [ - { - name: 'title', - content: topicData.titleRaw, - }, - { - name: 'description', - content: description, - }, - { - property: 'og:title', - content: topicData.titleRaw, - }, - { - property: 'og:description', - content: description, - }, - { - property: 'og:type', - content: 'article', - }, - { - property: 'og:image', - content: ogImageUrl, - noEscape: true, - }, - { - property: 'og:image:url', - content: ogImageUrl, - noEscape: true, - }, - { - property: 'article:published_time', - content: utils.toISOString(topicData.timestamp), - }, - { - property: 'article:modified_time', - content: utils.toISOString(topicData.lastposttime), - }, - { - property: 'article:section', - content: topicData.category ? topicData.category.name : '', - }, - ]; - - res.locals.linkTags = [ - { - rel: 'alternate', - type: 'application/rss+xml', - href: nconf.get('url') + '/topic/' + tid + '.rss', - }, - ]; + res.render('topic', topicData); + }, + ], callback); +}; - if (topicData.category) { - res.locals.linkTags.push({ - rel: 'up', - href: nconf.get('url') + '/category/' + topicData.category.slug, - }); - } +function buildBreadcrumbs(topicData, callback) { + var breadcrumbs = [ + { + text: topicData.category.name, + url: nconf.get('relative_path') + '/category/' + topicData.category.slug, + }, + { + text: topicData.title, + }, + ]; + async.waterfall([ + function (next) { + helpers.buildCategoryBreadcrumbs(topicData.category.parentCid, next); + }, + function (crumbs, next) { + topicData.breadcrumbs = crumbs.concat(breadcrumbs); next(null, topicData); }, - ], function (err, data) { - if (err) { - return callback(err); + ], callback); +} + +function addTags(topicData, req, res) { + function findPost(index) { + for (var i = 0; i < topicData.posts.length; i += 1) { + if (parseInt(topicData.posts[i].index, 10) === parseInt(index, 10)) { + return topicData.posts[i]; + } } + } + var description = ''; + var postAtIndex = findPost(Math.max(0, req.params.post_index - 1)); - data.privileges = userPrivileges; - data.topicStaleDays = parseInt(meta.config.topicStaleDays, 10) || 60; - data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; - data['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; - data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; - data.bookmarkThreshold = parseInt(meta.config.bookmarkThreshold, 10) || 5; - data.postEditDuration = parseInt(meta.config.postEditDuration, 10) || 0; - data.postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10) || 0; - data.scrollToMyPost = settings.scrollToMyPost; - data.rssFeedUrl = nconf.get('relative_path') + '/topic/' + data.tid + '.rss'; - data.postIndex = req.params.post_index; - data.pagination = pagination.create(currentPage, pageCount, req.query); - data.pagination.rel.forEach(function (rel) { - rel.href = nconf.get('url') + '/topic/' + data.slug + rel.href; - res.locals.linkTags.push(rel); - }); + if (postAtIndex && postAtIndex.content) { + description = S(postAtIndex.content).decodeHTMLEntities().stripTags().s; + } - req.session.tids_viewed = req.session.tids_viewed || {}; - if (!req.session.tids_viewed[tid] || req.session.tids_viewed[tid] < Date.now() - 3600000) { - topics.increaseViewCount(tid); - req.session.tids_viewed[tid] = Date.now(); - } + if (description.length > 255) { + description = description.substr(0, 255) + '...'; + } - if (req.uid) { - topics.markAsRead([tid], req.uid, function (err, markedRead) { - if (err) { - return callback(err); - } - if (markedRead) { - topics.pushUnreadCount(req.uid); - topics.markTopicNotificationsRead([tid], req.uid); - } - }); - } + var ogImageUrl = ''; + if (topicData.thumb) { + ogImageUrl = topicData.thumb; + } else if (postAtIndex && postAtIndex.user && postAtIndex.user.picture) { + ogImageUrl = postAtIndex.user.picture; + } else if (meta.config['og:image']) { + ogImageUrl = meta.config['og:image']; + } else if (meta.config['brand:logo']) { + ogImageUrl = meta.config['brand:logo']; + } else { + ogImageUrl = '/logo.png'; + } - res.render('topic', data); - }); -}; + if (typeof ogImageUrl === 'string' && ogImageUrl.indexOf('http') === -1) { + ogImageUrl = nconf.get('url') + ogImageUrl; + } + + description = description.replace(/\n/g, ' '); + res.locals.metaTags = [ + { + name: 'title', + content: topicData.titleRaw, + }, + { + name: 'description', + content: description, + }, + { + property: 'og:title', + content: topicData.titleRaw, + }, + { + property: 'og:description', + content: description, + }, + { + property: 'og:type', + content: 'article', + }, + { + property: 'og:image', + content: ogImageUrl, + noEscape: true, + }, + { + property: 'og:image:url', + content: ogImageUrl, + noEscape: true, + }, + { + property: 'article:published_time', + content: utils.toISOString(topicData.timestamp), + }, + { + property: 'article:modified_time', + content: utils.toISOString(topicData.lastposttime), + }, + { + property: 'article:section', + content: topicData.category ? topicData.category.name : '', + }, + ]; + + res.locals.linkTags = [ + { + rel: 'alternate', + type: 'application/rss+xml', + href: topicData.rssFeedUrl, + }, + ]; + + if (topicData.category) { + res.locals.linkTags.push({ + rel: 'up', + href: nconf.get('url') + '/category/' + topicData.category.slug, + }); + } +} topicsController.teaser = function (req, res, next) { var tid = req.params.topic_id; @@ -355,5 +366,3 @@ topicsController.pagination = function (req, res, callback) { res.json(paginationData); }); }; - -module.exports = topicsController; diff --git a/src/database/mongo.js b/src/database/mongo.js index 14dcd330ac..c79ee81bdc 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -206,18 +206,18 @@ mongoModule.info = function (db, callback) { stats.mem = results.serverStatus.mem; stats.mem = results.serverStatus.mem; - stats.mem.resident = (stats.mem.resident / 1024).toFixed(2); - stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2); - stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2); + stats.mem.resident = (stats.mem.resident / 1024).toFixed(3); + stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(3); + stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(3); stats.collectionData = results.listCollections; stats.network = results.serverStatus.network; stats.raw = JSON.stringify(stats, null, 4); stats.avgObjSize = stats.avgObjSize.toFixed(2); - stats.dataSize = (stats.dataSize / scale).toFixed(2); - stats.storageSize = (stats.storageSize / scale).toFixed(2); - stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0; - stats.indexSize = (stats.indexSize / scale).toFixed(2); + stats.dataSize = (stats.dataSize / scale).toFixed(3); + stats.storageSize = (stats.storageSize / scale).toFixed(3); + stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(3) : 0; + stats.indexSize = (stats.indexSize / scale).toFixed(3); stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1'; stats.host = results.serverStatus.host; stats.version = results.serverStatus.version; diff --git a/src/database/redis.js b/src/database/redis.js index 75129b6c8b..13d87c27bd 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -152,7 +152,7 @@ redisModule.info = function (cxn, callback) { redisData[parts[0]] = parts[1]; } }); - redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2); + redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(3); redisData.raw = JSON.stringify(redisData, null, 4); redisData.redis = true; diff --git a/src/meta/settings.js b/src/meta/settings.js index 18eae100c3..d157a4cd95 100644 --- a/src/meta/settings.js +++ b/src/meta/settings.js @@ -36,7 +36,9 @@ Settings.set = function (hash, values, callback) { }; Settings.setOne = function (hash, field, value, callback) { - db.setObjectField('settings:' + hash, field, value, callback); + var data = {}; + data[field] = value; + Settings.set(hash, data, callback); }; Settings.setOnEmpty = function (hash, values, callback) { @@ -54,7 +56,7 @@ Settings.setOnEmpty = function (hash, values, callback) { }); if (Object.keys(empty).length) { - db.setObject('settings:' + hash, empty, next); + Settings.set(hash, empty, next); } else { next(); } diff --git a/src/routes/feeds.js b/src/routes/feeds.js index e1fd92cd59..62b8e6f650 100644 --- a/src/routes/feeds.js +++ b/src/routes/feeds.js @@ -26,6 +26,45 @@ module.exports = function (app, middleware) { app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag); }; +function validateTokenIfRequiresLogin(requiresLogin, cid, req, res, callback) { + var uid = req.query.uid; + var token = req.query.token; + + if (!requiresLogin) { + return callback(); + } + + if (!uid || !token) { + return helpers.notAllowed(req, res); + } + + async.waterfall([ + function (next) { + user.getUserField(uid, 'rss_token', next); + }, + function (_token, next) { + if (token === _token) { + async.waterfall([ + function (next) { + privileges.categories.get(cid, uid, next); + }, + function (privileges, next) { + if (!privileges.read) { + return helpers.notAllowed(req, res); + } + next(); + }, + ], callback); + return; + } + user.auth.logAttempt(uid, req.ip, next); + }, + function () { + helpers.notAllowed(req, res); + }, + ], callback); +} + function generateForTopic(req, res, callback) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { return controllers404.send404(req, res); @@ -33,6 +72,7 @@ function generateForTopic(req, res, callback) { var tid = req.params.topic_id; var userPrivileges; + var topic; async.waterfall([ function (next) { async.parallel({ @@ -48,11 +88,12 @@ function generateForTopic(req, res, callback) { if (!results.topic || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { return controllers404.send404(req, res); } - if (!results.privileges['topics:read']) { - return helpers.notAllowed(req, res); - } userPrivileges = results.privileges; - topics.getTopicWithPosts(results.topic, 'tid:' + tid + ':posts', req.uid, 0, 25, false, next); + topic = results.topic; + validateTokenIfRequiresLogin(!results.privileges['topics:read'], results.topic.cid, req, res, next); + }, + function (next) { + topics.getTopicWithPosts(topic, 'tid:' + tid + ':posts', req.uid || req.query.uid || 0, 0, 25, false, next); }, function (topicData) { topics.modifyPostsByPrivilege(topicData, userPrivileges); @@ -95,40 +136,12 @@ function generateForTopic(req, res, callback) { ], callback); } -function generateForUserTopics(req, res, callback) { - if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return controllers404.send404(req, res); - } - - var userslug = req.params.userslug; - - async.waterfall([ - function (next) { - user.getUidByUserslug(userslug, next); - }, - function (uid, next) { - if (!uid) { - return callback(); - } - user.getUserFields(uid, ['uid', 'username'], next); - }, - function (userData, next) { - generateForTopics({ - uid: req.uid, - title: 'Topics by ' + userData.username, - description: 'A list of topics that are posted by ' + userData.username, - feed_url: '/user/' + userslug + '/topics.rss', - site_url: '/user/' + userslug + '/topics', - }, 'uid:' + userData.uid + ':topics', req, res, next); - }, - ], callback); -} - function generateForCategory(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { return controllers404.send404(req, res); } var cid = req.params.category_id; + var category; async.waterfall([ function (next) { @@ -143,22 +156,23 @@ function generateForCategory(req, res, next) { reverse: true, start: 0, stop: 25, - uid: req.uid, + uid: req.uid || req.query.uid || 0, }, next); }, }, next); }, function (results, next) { - if (!results.privileges.read) { - return helpers.notAllowed(req, res); - } + category = results.category; + validateTokenIfRequiresLogin(!results.privileges.read, cid, req, res, next); + }, + function (next) { generateTopicsFeed({ - uid: req.uid, - title: results.category.name, - description: results.category.description, + uid: req.uid || req.query.uid || 0, + title: category.name, + description: category.description, feed_url: '/category/' + cid + '.rss', - site_url: '/category/' + results.category.cid, - }, results.category.topics, next); + site_url: '/category/' + category.cid, + }, category.topics, next); }, function (feed) { sendFeed(feed, res); @@ -292,12 +306,13 @@ function generateForRecentPosts(req, res, next) { ], next); } -function generateForCategoryRecentPosts(req, res, next) { +function generateForCategoryRecentPosts(req, res, callback) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { return controllers404.send404(req, res); } var cid = req.params.category_id; - + var category; + var posts; async.waterfall([ function (next) { async.parallel({ @@ -308,29 +323,29 @@ function generateForCategoryRecentPosts(req, res, next) { categories.getCategoryData(cid, next); }, posts: function (next) { - categories.getRecentReplies(cid, req.uid, 20, next); + categories.getRecentReplies(cid, req.uid || req.query.uid || 0, 20, next); }, }, next); }, function (results, next) { if (!results.category) { - return next(); - } - - if (!results.privileges.read) { - return helpers.notAllowed(req, res); + return controllers404.send404(req, res); } - + category = results.category; + posts = results.posts; + validateTokenIfRequiresLogin(!results.privileges.read, cid, req, res, next); + }, + function () { var feed = generateForPostsFeed({ - title: results.category.name + ' Recent Posts', - description: 'A list of recent posts from ' + results.category.name, + title: category.name + ' Recent Posts', + description: 'A list of recent posts from ' + category.name, feed_url: '/category/' + cid + '/recentposts.rss', site_url: '/category/' + cid + '/recentposts', - }, results.posts); + }, posts); sendFeed(feed, res); }, - ], next); + ], callback); } function generateForPostsFeed(feedOptions, posts) { @@ -357,6 +372,35 @@ function generateForPostsFeed(feedOptions, posts) { return feed; } +function generateForUserTopics(req, res, callback) { + if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { + return controllers404.send404(req, res); + } + + var userslug = req.params.userslug; + + async.waterfall([ + function (next) { + user.getUidByUserslug(userslug, next); + }, + function (uid, next) { + if (!uid) { + return callback(); + } + user.getUserFields(uid, ['uid', 'username'], next); + }, + function (userData, next) { + generateForTopics({ + uid: req.uid, + title: 'Topics by ' + userData.username, + description: 'A list of topics that are posted by ' + userData.username, + feed_url: '/user/' + userslug + '/topics.rss', + site_url: '/user/' + userslug + '/topics', + }, 'uid:' + userData.uid + ':topics', req, res, next); + }, + ], callback); +} + function generateForTag(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { return controllers404.send404(req, res); @@ -381,4 +425,3 @@ function sendFeed(feed, res) { var xml = feed.xml(); res.type('xml').set('Content-Length', Buffer.byteLength(xml)).send(xml); } - diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 43216575bc..df169aba0e 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -187,8 +187,10 @@ SocketAdmin.config.setMultiple = function (socket, data, callback) { logger.monitorConfig({ io: index.server }, setting); } } - plugins.fireHook('action:config.set', { settings: data }); - setImmediate(next); + data.type = 'config-change'; + data.uid = socket.uid; + data.ip = socket.ip; + events.log(data, next); }, ], callback); }; @@ -202,7 +204,19 @@ SocketAdmin.settings.get = function (socket, data, callback) { }; SocketAdmin.settings.set = function (socket, data, callback) { - meta.settings.set(data.hash, data.values, callback); + async.waterfall([ + function (next) { + meta.settings.set(data.hash, data.values, next); + }, + function (next) { + var eventData = data.values; + eventData.type = 'settings-change'; + eventData.uid = socket.uid; + eventData.ip = socket.ip; + eventData.hash = data.hash; + events.log(eventData, next); + }, + ], callback); }; SocketAdmin.settings.clearSitemapCache = function (socket, data, callback) { diff --git a/src/start.js b/src/start.js index 8d4e465a55..2195a0ec29 100644 --- a/src/start.js +++ b/src/start.js @@ -98,7 +98,7 @@ function setupConfigs() { nconf.set('secure', urlObject.protocol === 'https:'); nconf.set('use_port', !!urlObject.port); nconf.set('relative_path', relativePath); - nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); + nconf.set('port', urlObject.port || nconf.get('port') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); nconf.set('upload_url', '/assets/uploads'); } diff --git a/src/user/auth.js b/src/user/auth.js index 0a5c06fcb6..4347c860d1 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -6,6 +6,7 @@ var db = require('../database'); var meta = require('../meta'); var events = require('../events'); var batch = require('../batch'); +var utils = require('../utils'); module.exports = function (User) { User.auth = {}; @@ -47,6 +48,29 @@ module.exports = function (User) { ], callback); }; + User.auth.getFeedToken = function (uid, callback) { + if (!uid) { + return callback(); + } + var token; + async.waterfall([ + function (next) { + User.getUserField(uid, 'rss_token', next); + }, + function (_token, next) { + token = _token || utils.generateUUID(); + if (!_token) { + User.setUserField(uid, 'rss_token', token, next); + } else { + next(); + } + }, + function (next) { + next(null, token); + }, + ], callback); + }; + User.auth.clearLoginAttempts = function (uid) { db.delete('loginAttempts:' + uid); }; diff --git a/src/user/profile.js b/src/user/profile.js index d6f7ac5a4b..d3067b6945 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -283,7 +283,10 @@ module.exports = function (User) { }, function (hashedPassword, next) { async.parallel([ - async.apply(User.setUserField, data.uid, 'password', hashedPassword), + async.apply(User.setUserFields, data.uid, { + password: hashedPassword, + rss_token: utils.generateUUID(), + }), async.apply(User.reset.updateExpiry, data.uid), async.apply(User.auth.revokeAllSessions, data.uid), ], function (err) { diff --git a/src/webserver.js b/src/webserver.js index 2d26045b8b..828aa7b3da 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -133,8 +133,8 @@ function setupExpressApp(app, callback) { app.use(compression()); - app.get('/ping', ping); - app.get('/sping', ping); + app.get(relativePath + '/ping', ping); + app.get(relativePath + '/sping', ping); setupFavicon(app); @@ -231,6 +231,7 @@ function setupAutoLocale(app, callback) { function listen(callback) { callback = callback || function () { }; + console.log('derp', nconf.get('port')); var port = parseInt(nconf.get('port'), 10); var isSocket = isNaN(port); var socketPath = isSocket ? nconf.get('port') : ''; diff --git a/test/feeds.js b/test/feeds.js index e8e3f8dfa4..4ed9fe0937 100644 --- a/test/feeds.js +++ b/test/feeds.js @@ -12,6 +12,7 @@ var groups = require('../src/groups'); var user = require('../src/user'); var meta = require('../src/meta'); var privileges = require('../src/privileges'); +var helpers = require('./helpers'); describe('feeds', function () { var tid; @@ -113,4 +114,81 @@ describe('feeds', function () { }); }); }); + + describe('private feeds and tokens', function () { + var jar; + var rssToken; + before(function (done) { + helpers.loginUser('foo', 'barbar', function (err, _jar) { + assert.ifError(err); + jar = _jar; + done(); + }); + }); + + it('should load feed if its not private', function (done) { + request(nconf.get('url') + '/category/' + cid + '.rss', { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + + it('should not allow access if uid or token is missing', function (done) { + privileges.categories.rescind(['read'], cid, 'guests', function (err) { + assert.ifError(err); + async.parallel({ + test1: function (next) { + request(nconf.get('url') + '/category/' + cid + '.rss?uid=' + fooUid, { }, next); + }, + test2: function (next) { + request(nconf.get('url') + '/category/' + cid + '.rss?token=sometoken', { }, next); + }, + }, function (err, results) { + assert.ifError(err); + assert.equal(results.test1[0].statusCode, 200); + assert.equal(results.test2[0].statusCode, 200); + assert(results.test1[0].body.indexOf('Login to your account') !== -1); + assert(results.test2[0].body.indexOf('Login to your account') !== -1); + done(); + }); + }); + }); + + it('should not allow access if token is wrong', function (done) { + request(nconf.get('url') + '/category/' + cid + '.rss?uid=' + fooUid + '&token=sometoken', { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body.indexOf('Login to your account') !== -1); + done(); + }); + }); + + it('should allow access if token is correct', function (done) { + request(nconf.get('url') + '/api/category/' + cid, { jar: jar, json: true }, function (err, res, body) { + assert.ifError(err); + rssToken = body.rssFeedUrl.split('token')[1].slice(1); + request(nconf.get('url') + '/category/' + cid + '.rss?uid=' + fooUid + '&token=' + rssToken, { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + }); + + it('should not allow access if token is correct but has no privilege', function (done) { + privileges.categories.rescind(['read'], cid, 'registered-users', function (err) { + assert.ifError(err); + request(nconf.get('url') + '/category/' + cid + '.rss?uid=' + fooUid + '&token=' + rssToken, { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body.indexOf('Login to your account') !== -1); + done(); + }); + }); + }); + }); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 27dab2b647..c43b01a900 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -110,7 +110,7 @@ before(function (done) { nconf.set('secure', urlObject.protocol === 'https:'); nconf.set('use_port', !!urlObject.port); nconf.set('relative_path', relativePath); - nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); + nconf.set('port', urlObject.port || nconf.get('port') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path'))); nconf.set('core_templates_path', path.join(__dirname, '../../src/views'));