Compare commits

...

95 Commits

Author SHA1 Message Date
Barış Soner Uşaklı
a476d7261b fix: pin jquery version 2020-04-23 11:07:38 -04:00
Misty (Bot)
71f4607db4 chore: incrementing version number - v1.13.2 2020-02-05 21:03:51 +00:00
Misty (Bot)
d6ac2ba396 Merge commit 'a5ef6b53b8aab7b3e472eb3857c82ad3d72a6516' into v1.13.x 2020-02-05 21:03:51 +00:00
Barış Soner Uşaklı
a5ef6b53b8 fix: admin relogin 2020-02-03 11:04:20 -05:00
Barış Soner Uşaklı
c35a21d7f0 fix: #8135 2020-01-31 22:56:55 -05:00
Baris Usakli
1e50616c13 fix: handle mkdirp0.5->1.0x so it doesn't break upgrade 2020-01-31 15:05:50 -05:00
renovate[bot]
157832131d chore(deps): update dependency eslint to v6.8.0 (#8062)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-31 14:14:42 -05:00
renovate[bot]
d5b3d56296 Update dependency postcss to v7.0.26 (#8048)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-31 14:14:23 -05:00
renovate[bot]
976e26a958 chore(deps): update dependency nyc to v15 (#8094)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-31 14:13:54 -05:00
renovate[bot]
eb4a1a5772 chore(deps): update commitlint monorepo (#8100)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-31 14:13:41 -05:00
renovate[bot]
cdfbcbb9ce chore(deps): update dependency lint-staged to v10.0.7 (#8132)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-31 14:13:25 -05:00
Baris Usakli
87225a90c3 fix: #8134, upgrade mkdirp to 1.0.x 2020-01-31 14:10:00 -05:00
Misty (Bot)
5ed7fc0ffa Latest translations and fallbacks 2020-01-31 09:30:22 +00:00
renovate[bot]
16ab641dd1 fix(deps): update dependency connect-redis to v4.0.4 (#8143)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-30 11:20:48 -05:00
renovate[bot]
726ba71c57 fix(deps): update dependency rimraf to v3.0.1 (#8138)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-30 10:56:53 -05:00
renovate[bot]
f07b4bfa62 fix(deps): update dependency validator to v12.2.0 (#8136)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-30 09:10:14 -05:00
renovate[bot]
b370333c6a chore(deps): update dependency mocha to v7 (#8106)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-30 09:09:38 -05:00
Julian Lam
b959c24a2b Merge branch 'master' of github.com:NodeBB/NodeBB 2020-01-29 12:47:55 -05:00
Julian Lam
111ed802cf fix: onSuccessfulLogin not working
In scenarios where onSuccessfulLogin was not called in the SSO plugin,
core's calling of onSuccessfulLogin was prematurely returning, because
it was checking the wrong value.

This commit fixes the issue by checking a different value.
2020-01-29 12:47:48 -05:00
renovate[bot]
6d7131fbc5 fix(deps): update dependency nodebb-theme-persona to v10.1.34 (#8140)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-28 13:33:30 -05:00
Barış Soner Uşaklı
8c48f94b96 fix: #8139, dont allow restore if not deleted by self 2020-01-28 13:03:58 -05:00
Barış Soner Uşaklı
9969dd6335 fix: use view_deleted when filtering, closes #8137 2020-01-28 12:42:57 -05:00
Barış Soner Uşaklı
d927b763c1 fix: escape invalid rules 2020-01-26 22:18:07 -05:00
Barış Soner Uşaklı
66febb8071 feat: add test for isOnline 2020-01-26 21:51:05 -05:00
Barış Soner Uşaklı
3cca929a88 fix: add missing await 2020-01-26 21:35:04 -05:00
Barış Soner Uşaklı
df2c785127 feat: add test for change post owner 2020-01-24 15:24:01 -05:00
Barış Soner Uşaklı
0ae1eb4f6e fix: missing await in SocketPosts.changeOwner 2020-01-24 14:28:27 -05:00
Renovate Bot
dd440ce902 chore(deps): update dependency husky to v4 2020-01-24 13:34:09 -05:00
Andrew Rodrigues
027f3f2256 chore: bump themes 2020-01-24 06:58:11 -05:00
Barış Soner Uşaklı
23810cc64b fix: #8133, check if user is in room before removing 2020-01-23 22:40:14 -05:00
Barış Soner Uşaklı
30c503611c refactor: messaging 2020-01-23 22:19:15 -05:00
Barış Soner Uşaklı
cd1fa27a8b fix: add missing await 2020-01-23 22:19:00 -05:00
Barış Soner Uşaklı
ee4304b443 Merge branch 'master' of https://github.com/NodeBB/NodeBB 2020-01-23 22:13:07 -05:00
Barış Soner Uşaklı
f799f017ab fix: missing await 2020-01-23 22:04:49 -05:00
Baris Usakli
418c174d56 fix: dont return flag data to client 2020-01-23 12:48:21 -05:00
Barış Soner Uşaklı
51236df4ed fix: check if user has read priv before flagging 2020-01-22 12:14:50 -05:00
Barış Soner Uşaklı
1f13ab8a19 fix: restrict getUsersInRoom to members 2020-01-22 11:46:26 -05:00
Andrew Rodrigues
236a173009 chore: bump vanilla 2020-01-21 16:37:32 -05:00
Andrew Rodrigues
82ace391cb chore: bump persona 2020-01-21 15:58:13 -05:00
Barış Soner Uşaklı
3077eb9428 fix: remove unused conditional, dont add dupe messages 2020-01-21 15:17:31 -05:00
Julian Lam
ecc579a29c fix: tests for messaging 2020-01-21 14:35:50 -05:00
Julian Lam
594cd7e176 fix: #8127 user join system message duplicated
- Now showing user-join message for chat owner in addition to the newly
  added user
- If a modal already exists for a room when createModal is called, don't
  return null, return the modal
2020-01-21 13:57:16 -05:00
Julian Lam
106c141ff5 fix: background-size in taskbar images 2020-01-21 13:57:15 -05:00
renovate[bot]
c510a2c4f2 fix(deps): update dependency nodebb-theme-persona to v10.1.31 (#8129)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-21 13:49:55 -05:00
renovate[bot]
0e49cfb98d fix(deps): update dependency mongodb to v3.5.2 (#8092)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-21 13:49:36 -05:00
Renovate Bot
66992a556c chore(deps): update dependency lint-staged to v10.0.1 2020-01-20 15:18:18 -05:00
Barış Soner Uşaklı
1b08f37612 fix: tests, was using hardcoded message id 2020-01-20 11:15:26 -05:00
Barış Soner Uşaklı
bfaba89557 Merge branch 'master' of https://github.com/NodeBB/NodeBB 2020-01-20 10:59:22 -05:00
Julian Lam
8bb5e71ebe fix: typo in #8116 2020-01-20 10:21:27 -05:00
Julian Lam
3fac09b1ab fix: build step defaults to series instead of parallel
- The logic for the build step now defaults to series instead of
  parallel, unless more than 4 CPU cores are detected by the os
  library.
- The `--series` flag still exists, and will enforce build in
  series, as before.
2020-01-20 10:21:27 -05:00
Julian Lam
8e5a2276af feat: check flag values on save (assignee and state) (#8122)
* feat: add assignee checking when updating flag

Prior to this, it was possible to update the assignee to any value (or
any user. This commit adds checking to allow only admins, global
moderators, or in the case of flagged posts, moderators.

Also some prep work was added for value checking `state`.

* feat: value checking `state` on flag update

The state should be one of the constants defined earlier in the file.
2020-01-20 10:19:23 -05:00
Misty (Bot)
ddce77b343 Latest translations and fallbacks 2020-01-20 09:29:52 +00:00
Barış Soner Uşaklı
6a63c1a100 fix: escape system message, don't allow editing system messages 2020-01-19 22:20:43 -05:00
Barış Soner Uşaklı
c8fb7f9246 fix: escape register query param 2020-01-19 22:03:18 -05:00
Barış Soner Uşaklı
8c6a7954cf fix: delete upload 2020-01-19 14:57:06 -05:00
Renovate Bot
d74eecfbe8 chore(deps): update dependency lint-staged to v10 2020-01-19 14:23:07 -05:00
Barış Soner Uşaklı
153b1a0eaa fix: check uploadName 2020-01-19 11:56:13 -05:00
Misty (Bot)
1656738359 Latest translations and fallbacks 2020-01-19 09:29:07 +00:00
Barış Soner Uşaklı
01d1ae78c8 fix: #8120, bubble errors from static hooks 2020-01-17 13:30:57 -05:00
Barış Soner Uşaklı
b0f3e48ac2 fix: escape bootswatchSkin and homepageRoute 2020-01-17 11:48:00 -05:00
Barış Soner Uşaklı
3e52557689 fix: change owner missing await 2020-01-17 11:16:23 -05:00
yossizahn
09d55581d8 Fix: ACP > manage > group: save disableLeave (#8123)
* Fix: ACP > manage > group: save disableLeave

* Fix eslint comma-dangle
2020-01-17 09:38:23 -05:00
renovate[bot]
16e8f49655 fix(deps): update dependency sharp to v0.24.0 (#8121)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-16 15:59:37 -05:00
Barış Soner Uşaklı
e3952674ba fix: hsts always enabled 2020-01-15 19:32:14 -05:00
Barış Soner Uşaklı
b7a57996f7 fix: escape topic.thumb 2020-01-15 18:53:08 -05:00
renovate[bot]
ca10f8f073 fix(deps): update dependency nodebb-plugin-composer-default to v6.3.21 (#8119)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-15 12:07:25 -05:00
Baris Usakli
842916ea42 Merge branch 'master' of https://github.com/NodeBB/NodeBB 2020-01-15 11:36:39 -05:00
Baris Usakli
4eb9652a2a fix: #8112, don't crash hook returns no data 2020-01-15 11:36:27 -05:00
Opliko
740de034fa #8115 - log post owner changes (#8117)
* log post owner changes

* log each post separately

* use map instad of a loop
2020-01-15 11:05:57 -05:00
Opliko
879acc85ae Add disable/enable category to category edit page (#8118) 2020-01-14 17:30:56 -05:00
Barış Soner Uşaklı
e06c1bfcd2 fix: escape config.userLang/acpLang, don't allow invalid language codes 2020-01-13 12:27:50 -05:00
renovate[bot]
df5e3a7394 fix(deps): update dependency nodebb-widget-essentials to v4.0.18 (#8111)
Co-authored-by: WhiteSource Renovate <renovatebot@gmail.com>
2020-01-09 20:22:10 -05:00
Barış Soner Uşaklı
61da8c29ac fix: group create/join/update name validation 2020-01-07 15:40:54 -05:00
Barış Soner Uşaklı
48f086279c fix: don't crash if groupData is missing 2020-01-07 10:39:35 -05:00
Barış Soner Uşaklı
5a8217de01 fix: #8105, fix export json on page load 2020-01-04 10:34:20 -05:00
arite
527745310d fix: #8103, fix advanced menu not displaying in ACP 2020-01-04 00:27:20 -05:00
Renovate Bot
5e6233969e Update dependency nodebb-theme-vanilla to v11.1.13 2020-01-03 16:42:49 -05:00
Renovate Bot
81c5ca15eb Update dependency nodebb-plugin-mentions to v2.7.4 2020-01-03 14:26:59 -05:00
Misty (Bot)
ac567bc10a Latest translations and fallbacks 2020-01-02 09:49:08 +00:00
Misty (Bot)
5410b5d6da Latest translations and fallbacks 2020-01-01 09:47:53 +00:00
Misty (Bot)
223225378e Latest translations and fallbacks 2019-12-31 09:49:04 +00:00
Barış Soner Uşaklı
10989cccaa fix: meta description missing if url doesn't have post index 2019-12-30 22:19:00 -05:00
Misty (Bot)
0aae421417 Latest translations and fallbacks 2019-12-30 09:48:38 +00:00
Misty (Bot)
fd056b58fb Latest translations and fallbacks 2019-12-29 09:48:18 +00:00
Misty (Bot)
bb1515ce56 Latest translations and fallbacks 2019-12-28 09:48:24 +00:00
Barış Soner Uşaklı
c1b1ee61f3 fix: create user modal instantly closing 2019-12-27 09:51:41 -05:00
Barış Soner Uşaklı
9d074731f4 fix: login with weak password 2019-12-26 20:17:54 -05:00
Misty (Bot)
b9679df784 Latest translations and fallbacks 2019-12-26 09:49:04 +00:00
Barış Soner Uşaklı
f6d7a24a67 fix: dont check password strength on login 2019-12-24 09:07:17 -05:00
Misty (Bot)
9a4a48bc45 Latest translations and fallbacks 2019-12-20 09:49:13 +00:00
Julian Lam
2edc6960d0 docs: updated changelog 2019-12-19 15:21:11 -05:00
Misty (Bot)
cc6758a0f1 chore: incrementing version number - v1.13.1 2019-12-19 15:17:41 -05:00
Misty (Bot)
d1e0672fa6 chore: incrementing version number - v1.13.1 2019-12-19 20:16:59 +00:00
Misty (Bot)
bf7cab0e4f Merge commit '94e2c7e4bb0bc8f6db6bc25537b4a630e6a4f487' into v1.13.x 2019-12-19 20:16:59 +00:00
Misty (Bot)
c6ef1486de chore: incrementing version number - v1.13.0 2019-11-13 21:43:14 +00:00
114 changed files with 1060 additions and 541 deletions

View File

@@ -1,3 +1,74 @@
#### 1.13.1 (2019-12-19)
##### Chores
* incrementing version number - v1.13.1 (d1e0672f)
* incrementing version number - v1.13.0 (c38b2d23)
* **deps:**
* update dependency husky to v3.1.0 (#8046) (c3418c26)
* update dependency coveralls to v3.0.9 (#8067) (0aeee144)
* update dependency eslint to v6.7.0 (32cfe96f)
* update dependency coveralls to v3.0.8 (#8054) (8ba26104)
##### Documentation Changes
* updated changelog (94499da3)
##### New Features
* better output for cli plugins list, closes #8075 (4fc69443)
* #5272, allow changing user groups from manage users page (05c9fe27)
* merge social authentication into plugins menu in ACP (f9a8ebfc)
* convert middleware.isAdmin to async/await (efd1e88b)
##### Bug Fixes
* #8085, fix cookie name (dec157d6)
* #8058, fix incorrect digest setting display in ACP (1b992d82)
* remove select version (6a17e32d)
* travis config (3ae98300)
* travis :dog: (3731dc4e)
* #8078, dont mark notifications read without a mergeId (a8df6d62)
* #8077, show continue chat on all profile pages (7af1c873)
* profile showing posts from deleted topics (2679f37d)
* #8073, configurable necroThreshold (4d669783)
* allow members to search as well (b323df2f)
* #8069, dont show hidden groups in search (c2cd7de8)
* missing await (33fd4a1c)
* #8064, break-word on post-queue (1bda92e3)
* #6711 (7ed002a1)
* #8061, don't crash if there is a network problem (de404102)
* #8059, properly mark topic unread when using mark unread for all (a688aaae)
* #8042, dont show errors after clearing form (3811e0a3)
* unhandled promise rejection error on reset error (51073772)
* #8050, fix redirect after registration (366ad5cd)
* make _csrf a secure cookie if the website is using https (#8045) (0efe27b1)
* #8034 (0a96c923)
* serialize (a2545204)
* show login fields if user has local password (1eca5b3d)
* use the correct attribute name for widgets (6c404b81)
* **deps:**
* update dependency semver to v7 (483d7535)
* update dependency nodebb-theme-vanilla to v11.1.12 (610ecf35)
* update dependency sharp to v0.23.4 (#8076) (eb18c182)
* update dependency nodebb-theme-persona to v10.1.30 (0514383a)
* update dependency nodebb-plugin-markdown to v8.11.0 (702ca164)
* update dependency connect-mongo to v3.2.0 (2aef7a5b)
* update dependency mongodb to v3.3.5 (#8065) (68118e43)
* update dependency nodebb-theme-persona to v10.1.29 (#8057) (34933091)
* update dependency sharp to v0.23.3 (#8044) (6fa88823)
* update dependency validator to v12.1.0 (#8055) (488ea394)
* update dependency nodebb-theme-slick to v1.2.28 (#8041) (b3511f71)
* update dependency nodebb-theme-vanilla to v11.1.11 (#8040) (d567c4ae)
* update dependency nodebb-theme-persona to v10.1.28 (#8039) (6c87bed5)
* update dependency nodebb-plugin-dbsearch to v4.0.7 (#8038) (1e2e16b4)
##### Refactors
* async/await middleware (a227cbe3)
* change to const/let (3454a24b)
* shorter returns (cec00795)
### 1.13.0 (2019-11-13) ### 1.13.0 (2019-11-13)
##### Chores ##### Chores

View File

@@ -2,7 +2,7 @@
"name": "nodebb", "name": "nodebb",
"license": "GPL-3.0", "license": "GPL-3.0",
"description": "NodeBB Forum", "description": "NodeBB Forum",
"version": "1.13.0", "version": "1.13.2",
"homepage": "http://www.nodebb.org", "homepage": "http://www.nodebb.org",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -49,7 +49,7 @@
"connect-mongo": "3.2.0", "connect-mongo": "3.2.0",
"connect-multiparty": "^2.1.0", "connect-multiparty": "^2.1.0",
"connect-pg-simple": "^6.0.0", "connect-pg-simple": "^6.0.0",
"connect-redis": "4.0.3", "connect-redis": "4.0.4",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"cron": "^1.3.0", "cron": "^1.3.0",
"cropperjs": "^1.2.2", "cropperjs": "^1.2.2",
@@ -63,7 +63,7 @@
"helmet": "^3.11.0", "helmet": "^3.11.0",
"html-to-text": "^5.0.0", "html-to-text": "^5.0.0",
"ipaddr.js": "^1.5.4", "ipaddr.js": "^1.5.4",
"jquery": "^3.2.1", "jquery": "3.4.1",
"jsesc": "2.5.2", "jsesc": "2.5.2",
"json-2-csv": "^3.0.0", "json-2-csv": "^3.0.0",
"jsonwebtoken": "^8.4.0", "jsonwebtoken": "^8.4.0",
@@ -73,43 +73,43 @@
"lru-cache": "5.1.1", "lru-cache": "5.1.1",
"material-design-lite": "^1.3.0", "material-design-lite": "^1.3.0",
"mime": "^2.2.0", "mime": "^2.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^1.0.3",
"mongodb": "3.4.0", "mongodb": "3.5.2",
"morgan": "^1.9.1", "morgan": "^1.9.1",
"mousetrap": "^1.6.1", "mousetrap": "^1.6.1",
"mubsub-nbb": "^1.5.1", "mubsub-nbb": "^1.5.1",
"nconf": "^0.10.0", "nconf": "^0.10.0",
"nodebb-plugin-composer-default": "6.3.20", "nodebb-plugin-composer-default": "6.3.21",
"nodebb-plugin-dbsearch": "4.0.7", "nodebb-plugin-dbsearch": "4.0.7",
"nodebb-plugin-emoji": "^3.0.0", "nodebb-plugin-emoji": "^3.0.0",
"nodebb-plugin-emoji-android": "2.0.0", "nodebb-plugin-emoji-android": "2.0.0",
"nodebb-plugin-markdown": "8.11.0", "nodebb-plugin-markdown": "8.11.0",
"nodebb-plugin-mentions": "2.7.3", "nodebb-plugin-mentions": "2.7.4",
"nodebb-plugin-soundpack-default": "1.0.0", "nodebb-plugin-soundpack-default": "1.0.0",
"nodebb-plugin-spam-be-gone": "0.6.7", "nodebb-plugin-spam-be-gone": "0.6.7",
"nodebb-rewards-essentials": "0.1.2", "nodebb-rewards-essentials": "0.1.2",
"nodebb-theme-lavender": "5.0.11", "nodebb-theme-lavender": "5.0.11",
"nodebb-theme-persona": "10.1.30", "nodebb-theme-persona": "10.1.34",
"nodebb-theme-slick": "1.2.28", "nodebb-theme-slick": "1.2.28",
"nodebb-theme-vanilla": "11.1.12", "nodebb-theme-vanilla": "11.1.15",
"nodebb-widget-essentials": "4.0.17", "nodebb-widget-essentials": "4.0.18",
"nodemailer": "^6.0.0", "nodemailer": "^6.0.0",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-local": "1.0.0", "passport-local": "1.0.0",
"pg": "^7.4.0", "pg": "^7.4.0",
"pg-cursor": "^2.0.0", "pg-cursor": "^2.0.0",
"postcss": "7.0.21", "postcss": "7.0.26",
"postcss-clean": "1.1.0", "postcss-clean": "1.1.0",
"promise-polyfill": "^8.0.0", "promise-polyfill": "^8.0.0",
"prompt": "^1.0.0", "prompt": "^1.0.0",
"redis": "2.8.0", "redis": "2.8.0",
"request": "2.88.0", "request": "2.88.0",
"rimraf": "3.0.0", "rimraf": "3.0.1",
"rss": "^1.2.2", "rss": "^1.2.2",
"sanitize-html": "^1.16.3", "sanitize-html": "^1.16.3",
"semver": "^7.0.0", "semver": "^7.0.0",
"serve-favicon": "^2.4.5", "serve-favicon": "^2.4.5",
"sharp": "0.23.4", "sharp": "0.24.0",
"sitemap": "^5.0.0", "sitemap": "^5.0.0",
"socket.io": "2.3.0", "socket.io": "2.3.0",
"socket.io-adapter-cluster": "^1.0.1", "socket.io-adapter-cluster": "^1.0.1",
@@ -124,27 +124,27 @@
"textcomplete.contenteditable": "^0.1.1", "textcomplete.contenteditable": "^0.1.1",
"toobusy-js": "^0.5.1", "toobusy-js": "^0.5.1",
"uglify-es": "^3.3.9", "uglify-es": "^3.3.9",
"validator": "12.1.0", "validator": "12.2.0",
"winston": "3.2.1", "winston": "3.2.1",
"xml": "^1.0.1", "xml": "^1.0.1",
"xregexp": "^4.1.1", "xregexp": "^4.1.1",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "8.2.0", "@commitlint/cli": "8.3.5",
"@commitlint/config-angular": "8.2.0", "@commitlint/config-angular": "8.3.4",
"coveralls": "3.0.9", "coveralls": "3.0.9",
"eslint": "6.7.0", "eslint": "6.8.0",
"eslint-config-airbnb-base": "14.0.0", "eslint-config-airbnb-base": "14.0.0",
"eslint-plugin-import": "2.18.2", "eslint-plugin-import": "2.18.2",
"grunt": "1.0.4", "grunt": "1.0.4",
"grunt-contrib-watch": "1.1.0", "grunt-contrib-watch": "1.1.0",
"husky": "3.1.0", "husky": "4.2.1",
"jsdom": "15.2.1", "jsdom": "15.2.1",
"lint-staged": "9.4.2", "lint-staged": "10.0.7",
"mocha": "6.2.2", "mocha": "7.0.1",
"mocha-lcov-reporter": "1.3.0", "mocha-lcov-reporter": "1.3.0",
"nyc": "14.1.1", "nyc": "15.0.0",
"smtp-server": "3.5.0" "smtp-server": "3.5.0"
}, },
"bugs": { "bugs": {

View File

@@ -18,7 +18,7 @@
"manage/groups": "Gruppen", "manage/groups": "Gruppen",
"manage/ip-blacklist": "IP Blacklist", "manage/ip-blacklist": "IP Blacklist",
"manage/uploads": "Uploads", "manage/uploads": "Uploads",
"manage/digest": "Digests", "manage/digest": "Zusammenfassungen",
"section-settings": "Einstellungen", "section-settings": "Einstellungen",
"settings/general": "Allgemein", "settings/general": "Allgemein",

View File

@@ -33,8 +33,8 @@
"testing.select": "Wählen Sie die E-Mail Vorlage", "testing.select": "Wählen Sie die E-Mail Vorlage",
"testing.send": "Test-E-Mail versenden", "testing.send": "Test-E-Mail versenden",
"testing.send-help": "Die Test-E-Mail wird an die E-Mail Adresse des momentan eingeloggten Nutzers geschickt.", "testing.send-help": "Die Test-E-Mail wird an die E-Mail Adresse des momentan eingeloggten Nutzers geschickt.",
"subscriptions": "Email Digests", "subscriptions": "Email Zusammenfassungen",
"subscriptions.disable": "Disable email digests", "subscriptions.disable": "Deaktivierung der Email Zusammenfassungen",
"subscriptions.hour": "Sende Zeit", "subscriptions.hour": "Sende Zeit",
"subscriptions.hour-help": "Bitte geben Sie eine Nummer ein, welche die Stunde repräsentiert zu welcher geplante Emails versandt werden sollen (z.B. <code>0</code> für Mitternacht, <code>17</code> für 5 Uhr Nachmittags). Beachten Sie, dass die Zeit auf der Serverzeit basiert und daher nicht umbedingt mit ihrer Systemzeit übereinstimmen muss.<br>Die ungefähre Serverzeit ist: <span id=\"serverTime\"></span><br>Die nächste tägliche Sendung ist um <span id=\"nextDigestTime\"></span> geplant" "subscriptions.hour-help": "Bitte geben Sie eine Nummer ein, welche die Stunde repräsentiert zu welcher geplante Emails versandt werden sollen (z.B. <code>0</code> für Mitternacht, <code>17</code> für 5 Uhr Nachmittags). Beachten Sie, dass die Zeit auf der Serverzeit basiert und daher nicht umbedingt mit ihrer Systemzeit übereinstimmen muss.<br>Die ungefähre Serverzeit ist: <span id=\"serverTime\"></span><br>Die nächste tägliche Sendung ist um <span id=\"nextDigestTime\"></span> geplant"
} }

View File

@@ -27,9 +27,9 @@
"digest.week": "der letzten Woche", "digest.week": "der letzten Woche",
"digest.month": "des letzen Monats", "digest.month": "des letzen Monats",
"digest.subject": "Zusammenfassung für %1", "digest.subject": "Zusammenfassung für %1",
"digest.title.day": "Your Daily Digest", "digest.title.day": "Deine tägliche Zusammenfassung",
"digest.title.week": "Your Weekly Digest", "digest.title.week": "Deine wöchentliche Zusammenfassung",
"digest.title.month": "Your Monthly Digest", "digest.title.month": "Deine monatliche Zusammenfassung",
"notif.chat.subject": "Neue Chatnachricht von %1 erhalten", "notif.chat.subject": "Neue Chatnachricht von %1 erhalten",
"notif.chat.cta": "Klicke hier, um die Unterhaltung fortzusetzen", "notif.chat.cta": "Klicke hier, um die Unterhaltung fortzusetzen",
"notif.chat.unsub.info": "Diese Chat-Benachrichtigung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.", "notif.chat.unsub.info": "Diese Chat-Benachrichtigung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.",

View File

@@ -134,6 +134,6 @@
"diffs.no-revisions-description": "Dieser Beitrag ha <strong>%1</strong> Revisionen.", "diffs.no-revisions-description": "Dieser Beitrag ha <strong>%1</strong> Revisionen.",
"diffs.current-revision": "Aktuelle Revision", "diffs.current-revision": "Aktuelle Revision",
"diffs.original-revision": "Ursprüngliche Revision", "diffs.original-revision": "Ursprüngliche Revision",
"timeago_later": "%1 later", "timeago_later": "%1 später",
"timeago_earlier": "%1 earlier" "timeago_earlier": "%1 earlier"
} }

View File

@@ -1,11 +1,11 @@
{ {
"events": "ארועים", "events": "ארועים",
"no-events": "אין ארועים", "no-events": "אין ארועים",
"control-panel": "בקרת ארועים", "control-panel": "בקרת ארועים\n ",
"filters": "Filters", "filters": "מסננים",
"filters-apply": "Apply Filters", "filters-apply": "החל מסננים",
"filter-type": "Event Type", "filter-type": "סוג אירוע",
"filter-start": "Start Date", "filter-start": "מתאריך",
"filter-end": "End Date", "filter-end": "עד תאריך",
"filter-perPage": "Per Page" "filter-perPage": "פריטים בכל דף"
} }

View File

@@ -18,13 +18,13 @@
"last_reply_time": "תגובה אחרונה", "last_reply_time": "תגובה אחרונה",
"reply-as-topic": "הגב כנושא", "reply-as-topic": "הגב כנושא",
"guest-login-reply": "התחבר כדי לפרסם תגובה", "guest-login-reply": "התחבר כדי לפרסם תגובה",
"login-to-view": "🔒 Log in to view", "login-to-view": "🔒 התחבר כדי לצפות",
"edit": "עריכה", "edit": "עריכה",
"delete": "מחק", "delete": "מחק",
"purge": "מחק לצמיתות", "purge": "מחק לצמיתות",
"restore": "שחזר", "restore": "שחזר",
"move": "הזז", "move": "הזז",
"change-owner": "Change Owner", "change-owner": "שנה מחבר הודעה",
"fork": "פורק", "fork": "פורק",
"link": "לינק", "link": "לינק",
"share": "שתף", "share": "שתף",
@@ -56,7 +56,7 @@
"ignoring": "מתעלם", "ignoring": "מתעלם",
"watching.description": "הודע לי על תגובות חדשות. <br/>הצג נושא חדש ברשימת הלא נקראו.", "watching.description": "הודע לי על תגובות חדשות. <br/>הצג נושא חדש ברשימת הלא נקראו.",
"not-watching.description": "אל תיידע אותי על תגובות חדשות. <br/>הצג נושא חדש ברשימת הלא נקראו במידה ובחרתי לא להתעלם מקבוצת הדיון", "not-watching.description": "אל תיידע אותי על תגובות חדשות. <br/>הצג נושא חדש ברשימת הלא נקראו במידה ובחרתי לא להתעלם מקבוצת הדיון",
"ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "ignoring.description": "אל תתריע לי על תגובות חדשות. <br/>אל תראה את הנושא בנושאים שלא נקראו ",
"thread_tools.title": "כלי נושא", "thread_tools.title": "כלי נושא",
"thread_tools.markAsUnreadForAll": "סמן לא נקרא לכולם", "thread_tools.markAsUnreadForAll": "סמן לא נקרא לכולם",
"thread_tools.pin": "נעץ נושא", "thread_tools.pin": "נעץ נושא",
@@ -66,7 +66,7 @@
"thread_tools.move": "הזז נושא", "thread_tools.move": "הזז נושא",
"thread_tools.move-posts": "הזז פוסטים", "thread_tools.move-posts": "הזז פוסטים",
"thread_tools.move_all": "הזז הכל", "thread_tools.move_all": "הזז הכל",
"thread_tools.change_owner": "Change Owner", "thread_tools.change_owner": "שנה את כותב ההודעה",
"thread_tools.select_category": "בחר קטגוריה", "thread_tools.select_category": "בחר קטגוריה",
"thread_tools.fork": "שכפל נושא", "thread_tools.fork": "שכפל נושא",
"thread_tools.delete": "מחק נושא", "thread_tools.delete": "מחק נושא",
@@ -101,7 +101,7 @@
"delete_posts_instruction": "לחץ על הפוסטים שברצונך למחוק", "delete_posts_instruction": "לחץ על הפוסטים שברצונך למחוק",
"merge_topics_instruction": "לחץ על הנושאים שתרצה למזג", "merge_topics_instruction": "לחץ על הנושאים שתרצה למזג",
"move_posts_instruction": "לחץ על הפוסטים שאתה רוצה להזיז", "move_posts_instruction": "לחץ על הפוסטים שאתה רוצה להזיז",
"change_owner_instruction": "Click the posts you want to assign to another user", "change_owner_instruction": "לחץ על ההודעה שהנך רוצה לשנות את בעל ההודעה",
"composer.title_placeholder": "הכנס את כותרת הנושא כאן...", "composer.title_placeholder": "הכנס את כותרת הנושא כאן...",
"composer.handle_placeholder": "שם", "composer.handle_placeholder": "שם",
"composer.discard": "ביטול", "composer.discard": "ביטול",
@@ -130,10 +130,10 @@
"stale.reply_anyway": "הגב לנושא זה בכל מקרה", "stale.reply_anyway": "הגב לנושא זה בכל מקרה",
"link_back": "תגובה: [%1](%2)", "link_back": "תגובה: [%1](%2)",
"diffs.title": "היסטוריית עריכת הפוסט", "diffs.title": "היסטוריית עריכת הפוסט",
"diffs.description": "This post has <strong>%1</strong> revisions. Click one of the revisions below to see the post content at that point in time.", "diffs.description": "להודעה זו יש <strong>%1</strong> עריכות. לחץ על אחת מהעריכות להלן כדי לראות את תוכן ההודעה בנקודת זמן זו.",
"diffs.no-revisions-description": "לפוסט זה יש <strong>%1</strong>גרסאות", "diffs.no-revisions-description": "לפוסט זה יש <strong>%1</strong>גרסאות",
"diffs.current-revision": "current revision", "diffs.current-revision": "גירסה נוכחית",
"diffs.original-revision": "original revision", "diffs.original-revision": "גירסה מקורית",
"timeago_later": "%1 later", "timeago_later": "אחרי %1:",
"timeago_earlier": "%1 earlier" "timeago_earlier": "לפני %1 "
} }

View File

@@ -30,16 +30,16 @@
"select-category": "Seleziona Categoria", "select-category": "Seleziona Categoria",
"set-parent-category": "Imposta la Categoria Padre", "set-parent-category": "Imposta la Categoria Padre",
"privileges.description": "You can configure the access control privileges for portions of the site in this section. Privileges can be granted on a per-user or a per-group basis. Select the domain of effect from the dropdown below.", "privileges.description": "In questa sezione è possibile configurare i privilegi di controllo dell'accesso per parti del sito. I privilegi possono essere concessi per utente o per gruppo. Seleziona il dominio dell'effetto dal menu a discesa in basso.",
"privileges.category-selector": "Configura privilegi per", "privileges.category-selector": "Configura privilegi per",
"privileges.warning": "<strong>Note</strong>: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", "privileges.warning": "<strong>Nota</strong>: Le impostazioni dei privilegi hanno effetto immediato. Non è necessario salvare la categoria dopo aver regolato queste impostazioni.",
"privileges.section-viewing": "Visualizzazione dei Privilegi", "privileges.section-viewing": "Visualizzazione dei Privilegi",
"privileges.section-posting": "Posting Privileges", "privileges.section-posting": "Privilegi di pubblicazione",
"privileges.section-moderation": "Moderation Privileges", "privileges.section-moderation": "Privilegi di Moderazione",
"privileges.section-other": "Altro", "privileges.section-other": "Altro",
"privileges.section-user": "Utente", "privileges.section-user": "Utente",
"privileges.search-user": "Aggiungi Utente", "privileges.search-user": "Aggiungi Utente",
"privileges.no-users": "No user-specific privileges in this category.", "privileges.no-users": "Nessun privilegio specifico dell'utente in questa categoria.",
"privileges.section-group": "Gruppo", "privileges.section-group": "Gruppo",
"privileges.group-private": "Questo gruppo è privato", "privileges.group-private": "Questo gruppo è privato",
"privileges.search-group": "Aggiungi gruppo", "privileges.search-group": "Aggiungi gruppo",
@@ -49,27 +49,27 @@
"privileges.copy-group-privileges-to-children": "Copia i privilegi di questo gruppo dai figli di questa categoria.", "privileges.copy-group-privileges-to-children": "Copia i privilegi di questo gruppo dai figli di questa categoria.",
"privileges.copy-group-privileges-to-all-categories": "Copia i privilegi di questo gruppo in tutte le categorie.", "privileges.copy-group-privileges-to-all-categories": "Copia i privilegi di questo gruppo in tutte le categorie.",
"privileges.copy-group-privileges-from": "Copia questo gruppo di privilegi da un altra categoria.", "privileges.copy-group-privileges-from": "Copia questo gruppo di privilegi da un altra categoria.",
"privileges.inherit": "If the <code>registered-users</code> group is granted a specific privilege, all other groups receive an <strong>implicit privilege</strong>, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the <code>registered-users</code> user group, and so, privileges for additional groups need not be explicitly granted.", "privileges.inherit": "Se l' <code>utente registrato</code> al gruppo viene concesso un privilegio specifico, tutti gli altri gruppi ricevono un<strong>privilegio implicito</strong>,anche se non sono esplicitamente definiti / controllati. Questo privilegio implicito ti viene mostrato perché tutti gli utenti fanno parte di<code>gruppo di utenti</code> registrati e quindi i privilegi per gruppi aggiuntivi non devono essere esplicitamente concessi.",
"privileges.copy-success": "Privilegi copiati!", "privileges.copy-success": "Privilegi copiati!",
"analytics.back": "Back to Categories List", "analytics.back": "Torna all'Elenco delle Categorie",
"analytics.title": "Statistiche per la categoria \"%1\"", "analytics.title": "Statistiche per la categoria \"%1\"",
"analytics.pageviews-hourly": "<strong>Figura 1</strong> &ndash; Vista delle visualizzazioni orarie per questa categoria</small>", "analytics.pageviews-hourly": "<strong>Figura 1</strong> &ndash; Vista delle visualizzazioni orarie per questa categoria</small>",
"analytics.pageviews-daily": "<strong>Figura 2</strong> &ndash; Vista delle visualizzazioni giornaliere per questa categoria</small>", "analytics.pageviews-daily": "<strong>Figura 2</strong> &ndash; Vista delle visualizzazioni giornaliere per questa categoria</small>",
"analytics.topics-daily": "<strong>Figura 3</strong> &ndash; Argomenti giornalieri creati in questa categoria</small>", "analytics.topics-daily": "<strong>Figura 3</strong> &ndash; Argomenti giornalieri creati in questa categoria</small>",
"analytics.posts-daily": "<strong>Figure 4</strong> &ndash; Daily posts made in this category</small>", "analytics.posts-daily": "<strong>Figura 4</strong>dash; Post giornalieri pubblicati in questa categoria</small>",
"alert.created": "Creato", "alert.created": "Creato",
"alert.create-success": "Categoria creata con successo!", "alert.create-success": "Categoria creata con successo!",
"alert.none-active": "Hai una categoria non attiva.", "alert.none-active": "Hai una categoria non attiva.",
"alert.create": "Crea una Categoria", "alert.create": "Crea una Categoria",
"alert.confirm-moderate": "<strong>Are you sure you wish to grant the moderation privilege to this user group?</strong> This group is public, and any users can join at will.", "alert.confirm-moderate": "<strong>Sei sicuro di voler concedere il privilegio di moderazione a questo gruppo di utenti?</strong> Questo gruppo è pubblico e tutti gli utenti possono aderire a piacimento.",
"alert.confirm-purge": "<p class=\"lead\">Vuoi davvero eliminare definitivamente questa categoria \"%1\"?</p><h5><strong class=\"text-danger\">Attenzione!</strong>Tutte le discussioni e i posti in questa categoria saranno eliminati definitivamente!</h5> <p class=\"help-block\">Eliminare definitivamente una categoria rimuoverà tutte le discussioni e i post ed eliminerà la categoria dal database. Se vuoi rimuovere una categoria <em>temporaneamente</em>, puoi invece \"disabilitare\" la categoria.", "alert.confirm-purge": "<p class=\"lead\">Vuoi davvero eliminare definitivamente questa categoria \"%1\"?</p><h5><strong class=\"text-danger\">Attenzione!</strong>Tutte le discussioni e i posti in questa categoria saranno eliminati definitivamente!</h5> <p class=\"help-block\">Eliminare definitivamente una categoria rimuoverà tutte le discussioni e i post ed eliminerà la categoria dal database. Se vuoi rimuovere una categoria <em>temporaneamente</em>, puoi invece \"disabilitare\" la categoria.",
"alert.purge-success": "Categoria eliminata definitivamente!", "alert.purge-success": "Categoria eliminata definitivamente!",
"alert.copy-success": "Settings Copied!", "alert.copy-success": "Impostazioni copiate!",
"alert.set-parent-category": "Set Parent Category", "alert.set-parent-category": "Imposta la Categoria Genitore",
"alert.updated": "Updated Categories", "alert.updated": "Categorie aggiornate",
"alert.updated-success": "Category IDs %1 successfully updated.", "alert.updated-success": "ID categoria %1 aggiornati correttamente.",
"alert.upload-image": "Carica immagine categoria", "alert.upload-image": "Carica immagine categoria",
"alert.find-user": "Trova un Utente", "alert.find-user": "Trova un Utente",
"alert.user-search": "Cerca un utente qui...", "alert.user-search": "Cerca un utente qui...",

View File

@@ -1,21 +1,21 @@
{ {
"lead": "A listing of digest delivery stats and times is displayed below.", "lead": "Di seguito viene visualizzato un elenco di statistiche e tempi di consegna del digest.",
"disclaimer": "Please be advised that email delivery is not guaranteed, due to the nature of email technology. Many variables factor into whether an email sent to the recipient server is ultimately delivered into the user's inbox, including server reputation, blacklisted IP addresses, and whether DKIM/SPF/DMARC is configured.", "disclaimer": "Si informa che la consegna della posta non è garantita a causa della natura della tecnologia di posta elettronica. Molte variabili determinano se una e-mail inviata al server del destinatario viene infine recapitata nella posta in arrivo dell'utente, inclusa la reputazione del server, gli indirizzi IP nella lista nera e se DKIM/SPF/DMARC è configurato.",
"disclaimer-continued": "A successful delivery means the message was sent successfully by NodeBB and acknowledged by the recipient server. It does not mean the email landed in the inbox. For best results, we recommend using a third-party email delivery service such as <a href=\"https://sendgrid.com/why-sendgrid/\">SendGrid</a>.", "disclaimer-continued": "Una consegna corretta indica che il messaggio è stato inviato correttamente da NodeBB e riconosciuto dal server destinatario. Non significa che l'email è arrivata nella posta in arrivo. Per risultati ottimali, si consiglia di utilizzare un servizio di consegna e-mail di terze parti come <a href=\"https://sendgrid.com/why-sendgrid/\">SendGrid</a>.",
"user": "User", "user": "Utente",
"subscription": "Subscription Type", "subscription": "Tipo di Abbonamento",
"last-delivery": "Last successful delivery", "last-delivery": "Ultima consegna riuscita",
"default": "System default", "default": "Sistema predefinito",
"default-help": "<em>System default</em> means the user has not explicitly overridden the global forum setting for digests, which is currently: &quot;<strong>%1</strong>&quot;", "default-help": "<em>Sistema predefinito</em> significa che l'utente non ha esplicitamente sovrascritto l'impostazione del forum globale per i digest, che attualmente è: & quot;<strong>%1</strong>&quot;",
"resend": "Resend Digest", "resend": "Rinvia Digest",
"resend-all-confirm": "Are you sure you wish to mnually execute this digest run?", "resend-all-confirm": "Sei sicuro di voler eseguire manualmente questa corsa digest?",
"resent-single": "Manual digest resend completed", "resent-single": "Invio del digest manuale completato",
"resent-day": "Daily digest resent", "resent-day": "Rinvio digest giornaliero",
"resent-week": "Weekly digest resent", "resent-week": "Rinvio del digest settimanale",
"resent-month": "Monthly digest resent", "resent-month": "Rinvio del digest mensile",
"null": "<em>Never</em>", "null": "<em>Mai</em>",
"manual-run": "Manual digest run:", "manual-run": "Esecuzione digest manuale:",
"no-delivery-data": "No delivery data found" "no-delivery-data": "Nessun dato di consegna trovato"
} }

View File

@@ -15,8 +15,8 @@
"delete": "Rimuovi Utente(i)", "delete": "Rimuovi Utente(i)",
"purge": "Rimuovi Utente(i) e Contenuto", "purge": "Rimuovi Utente(i) e Contenuto",
"download-csv": "Scarica CSV", "download-csv": "Scarica CSV",
"manage-groups": "Manage Groups", "manage-groups": "Gestisci Gruppi",
"add-group": "Add Group", "add-group": "Aggiungi Gruppo",
"invite": "Invito", "invite": "Invito",
"new": "Nuovo utente", "new": "Nuovo utente",

View File

@@ -27,33 +27,33 @@
"restrictions.max-title-length": "Lunghezza Massima Titolo", "restrictions.max-title-length": "Lunghezza Massima Titolo",
"restrictions.min-post-length": "Lunghezza Minima Post", "restrictions.min-post-length": "Lunghezza Minima Post",
"restrictions.max-post-length": "Lunghezza Massima Post", "restrictions.max-post-length": "Lunghezza Massima Post",
"restrictions.days-until-stale": "Days until topic is considered stale", "restrictions.days-until-stale": "Giorni prima che l'argomento sia considerato vecchio",
"restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", "restrictions.stale-help": "Se un argomento è considerato \"non aggiornato\", verrà mostrato un avviso agli utenti che tentano di rispondere a tale argomento.",
"timestamp": "Timestamp", "timestamp": "Data e Ora",
"timestamp.cut-off": "Date cut-off (in days)", "timestamp.cut-off": "Data di interruzione (in giorni)",
"timestamp.cut-off-help": "Dates &amp; times will be shown in a relative manner (e.g. \"3 hours ago\" / \"5 days ago\"), and localised into various\n\t\t\t\t\tlanguages. After a certain point, this text can be switched to display the localised date itself\n\t\t\t\t\t(e.g. 5 Nov 2016 15:30).<br /><em>(Default: <code>30</code>, or one month). Set to 0 to always display dates, leave blank to always display relative times.</em>", "timestamp.cut-off-help": "I tempi delle date verranno visualizzati in modo relativo (ad es. \"3 ore fa\" / \"5 giorni fa\") e localizzati in varie\n\t\t\t\t\tlingue. Dopo un certo punto, questo testo può essere cambiato per visualizzare la data localizzata\n\t\t\t\t\t(es. 5 Nov 2016 15:30).<br /><em>(Predefinito: <code>30</code>, o un mese).Impostare su 0 per visualizzare sempre le date, lasciare vuoto per visualizzare sempre i tempi relativi.</em>",
"timestamp.necro-threshold": "Necro Threshold (in days)", "timestamp.necro-threshold": "Necro Threshold (in giorni)",
"timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: <code>7</code>, or one week). Set to 0 to disable.</em>", "timestamp.necro-threshold-help": "Un messaggio verrà mostrato tra i post se il tempo tra loro è più lungo della soglia necro. (Predefinito: <code>7</code>, o una settimana). Impostare su 0 per disabilitare.</em>",
"teaser": "Teaser Post", "teaser": "Post Inopportuno",
"teaser.last-post": "Last &ndash; Show the latest post, including the original post, if no replies", "teaser.last-post": "Ultimo &ndash; Mostra l'ultimo post, incluso il post originale, se non ci sono risposte",
"teaser.last-reply": "Last &ndash; Show the latest reply, or a \"No replies\" placeholder if no replies", "teaser.last-reply": "Ultimo &ndash; Mostra l'ultima risposta o un segnaposto \"Nessuna risposta\" se non risposto",
"teaser.first": "Primo", "teaser.first": "Primo",
"unread": "Unread Settings", "unread": "Impostazioni non Lette",
"unread.cutoff": "Unread cutoff days", "unread.cutoff": "Giorni di interruzione non letti",
"unread.min-track-last": "Minimum posts in topic before tracking last read", "unread.min-track-last": "Post minimi nell'argomento prima del monitoraggio dell'ultima lettura",
"recent": "Impostazioni Recenti", "recent": "Impostazioni Recenti",
"recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", "recent.categoryFilter.disable": "Disabilita il filtro degli argomenti nelle categorie ignorate nella pagina/recente",
"signature": "Signature Settings", "signature": "Impostazioni della Firma",
"signature.disable": "Disable signatures", "signature.disable": "Disabilita le firme",
"signature.no-links": "Disable links in signatures", "signature.no-links": "Disabilita i collegamenti nelle firme",
"signature.no-images": "Disable images in signatures", "signature.no-images": "Disabilita le immagini nelle firme",
"signature.max-length": "Maximum Signature Length", "signature.max-length": "Lunghezza massima della firma",
"composer": "Composer Settings", "composer": "Impostazioni del compositore",
"composer-help": "The following settings govern the functionality and/or appearance of the post composer shown\n\t\t\t\tto users when they create new topics, or reply to existing topics.", "composer-help": "Le seguenti impostazioni regolano la funzionalità e/o l'aspetto del post compositore mostrato\n\t\t\t\tagli utenti quando creano nuovi argomenti o rispondono ad argomenti esistenti.",
"composer.show-help": "Show \"Help\" tab", "composer.show-help": "Mostra la scheda \"Aiuto\"",
"composer.enable-plugin-help": "Allow plugins to add content to the help tab", "composer.enable-plugin-help": "Consenti ai plug-in di aggiungere contenuti alla scheda Guida",
"composer.custom-help": "Custom Help Text", "composer.custom-help": "Testo di aiuto personalizzato",
"ip-tracking": "IP Tracking", "ip-tracking": "Monitoraggio IP",
"ip-tracking.each-post": "Track IP Address for each post", "ip-tracking.each-post": "Traccia l'indirizzo IP per ogni post",
"enable-post-history": "Enable Post History" "enable-post-history": "Abilita Cronologia post"
} }

View File

@@ -41,11 +41,11 @@
"registration-type.invite-only": "Solo Invito", "registration-type.invite-only": "Solo Invito",
"registration-type.admin-invite-only": "Solo invito per Amministratori", "registration-type.admin-invite-only": "Solo invito per Amministratori",
"registration-type.disabled": "Niente registrazione", "registration-type.disabled": "Niente registrazione",
"registration-type.help": "Normal - Users can register from the /register page.<br/>\nInvite Only - Users can invite others from the <a href=\"%1/users\" target=\"_blank\">users</a> page.<br/>\nAdmin Invite Only - Only administrators can invite others from <a href=\"%1/users\" target=\"_blank\">users</a> and <a href=\"%1/admin/manage/users\">admin/manage/users</a> pages.<br/>\nNo registration - No user registration.<br/>", "registration-type.help": "Normale: gli utenti possono registrarsi dalla pagina/di registrazione.<br/>\nSolo invito: gli utenti possono invitare altri dalla <a href=\"%1/users\" target=\"_blank\">pagina</a> utenti.<br/>\nSolo su invito amministratore: solo gli amministratori possono invitare altri <a href=\"%1/users\" target=\"_blank\">utenti</a> e<a href=\"%1/admin/manage/users\">dalle pagine</a> amministratore/gestione/utenti.<br/>\nNessuna registrazione - Nessuna registrazione dell'utente.<br/>",
"registration-approval-type.help": "Normal - Users are registered immediately.<br/>\nAdmin Approval - User registrations are placed in an <a href=\"%1/admin/manage/registration\">approval queue</a> for administrators.<br/>\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.<br/>", "registration-approval-type.help": "Normale: gli utenti vengono registrati immediatamente.<br/>\nApprovazione amministratore - Le registrazioni degli utenti vengono inserite in una <a href=\"%1/admin/manage/registration\">coda di approvazione</a> per amministratori.<br/>\nApprovazione amministratore per IP - Normale per i nuovi utenti, Approvazione amministratore per indirizzi IP che dispongono già di un account.<br/>",
"registration.max-invites": "Numero massimo di inviti per Utente", "registration.max-invites": "Numero massimo di inviti per Utente",
"max-invites": "Numero massimo di inviti per Utente", "max-invites": "Numero massimo di inviti per Utente",
"max-invites-help": "0 for no restriction. Admins get infinite invitations<br>Only applicable for \"Invite Only\"", "max-invites-help": "0 per nessuna restrizione. Gli amministratori ricevono infiniti inviti<br>Applicabile solo per \"Solo invito\"",
"invite-expiration": "Invito scaduto", "invite-expiration": "Invito scaduto",
"invite-expiration-help": "Il tuo invito scadrà tra %1 giorni.", "invite-expiration-help": "Il tuo invito scadrà tra %1 giorni.",
"min-username-length": "Lunghezza Minima Username", "min-username-length": "Lunghezza Minima Username",
@@ -63,7 +63,7 @@
"outgoing-new-tab": "Apri link esterni in una nuova scheda", "outgoing-new-tab": "Apri link esterni in una nuova scheda",
"topic-search": "Abilita ricerca nella Discussione", "topic-search": "Abilita ricerca nella Discussione",
"digest-freq": "Iscriviti al Riepilogo", "digest-freq": "Iscriviti al Riepilogo",
"digest-freq.off": "Off", "digest-freq.off": "Spento",
"digest-freq.daily": "Quotidiano", "digest-freq.daily": "Quotidiano",
"digest-freq.weekly": "Settimanale", "digest-freq.weekly": "Settimanale",
"digest-freq.monthly": "Mensile", "digest-freq.monthly": "Mensile",
@@ -72,8 +72,8 @@
"follow-created-topics": "Segui le discussioni che tu crei", "follow-created-topics": "Segui le discussioni che tu crei",
"follow-replied-topics": "Segui discussioni a cui rispondi tu", "follow-replied-topics": "Segui discussioni a cui rispondi tu",
"default-notification-settings": "Impostazioni di notifica predefinite", "default-notification-settings": "Impostazioni di notifica predefinite",
"categoryWatchState": "Default category watch state", "categoryWatchState": "Stato predefinito della categoria di controllo",
"categoryWatchState.watching": "Watching", "categoryWatchState.watching": "Guardare",
"categoryWatchState.notwatching": "Not Watching", "categoryWatchState.notwatching": "Non Guardare",
"categoryWatchState.ignoring": "Ignorato" "categoryWatchState.ignoring": "Ignorato"
} }

View File

@@ -34,7 +34,7 @@
"notif.chat.cta": "Clicca qui per continuare la conversazione", "notif.chat.cta": "Clicca qui per continuare la conversazione",
"notif.chat.unsub.info": "Questa notifica di chat ti è stata inviata perché l'hai sottoscritta nelle impostazioni.", "notif.chat.unsub.info": "Questa notifica di chat ti è stata inviata perché l'hai sottoscritta nelle impostazioni.",
"notif.post.unsub.info": "Questa notifica di discussione ti è stata inviata perché l'hai sottoscritta nelle impostazioni.", "notif.post.unsub.info": "Questa notifica di discussione ti è stata inviata perché l'hai sottoscritta nelle impostazioni.",
"notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", "notif.post.unsub.one-click": "In alternativa, annulla l'iscrizione a future email come questa, facendo clic",
"notif.cta": "Al forum", "notif.cta": "Al forum",
"notif.cta-new-reply": "Visualizza Post", "notif.cta-new-reply": "Visualizza Post",
"notif.cta-new-chat": "Visualizza Chat", "notif.cta-new-chat": "Visualizza Chat",
@@ -42,8 +42,8 @@
"notif.test.long": "Questo è un test delle notifiche email. Invia aiuto!", "notif.test.long": "Questo è un test delle notifiche email. Invia aiuto!",
"test.text1": "Questa è una email di prova per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.", "test.text1": "Questa è una email di prova per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.",
"unsub.cta": "Clicca qui per modificare queste impostazioni", "unsub.cta": "Clicca qui per modificare queste impostazioni",
"unsubscribe": "unsubscribe", "unsubscribe": "Annulla l'iscrizione",
"unsub.success": "You will no longer receive emails from the <strong>%1</strong> mailing list", "unsub.success": "Non riceverai più email dalla <strong>%1</strong> mailing list",
"banned.subject": "Sei stato bannato da %1", "banned.subject": "Sei stato bannato da %1",
"banned.text1": "L'utente %1 è stato bannato da %2", "banned.text1": "L'utente %1 è stato bannato da %2",
"banned.text2": "Questo ban durerà fino a %1.", "banned.text2": "Questo ban durerà fino a %1.",

View File

@@ -26,14 +26,14 @@
"invalid-pagination-value": "Valore di impaginazione non valido, deve essere almeno %1 ed al massimo %2", "invalid-pagination-value": "Valore di impaginazione non valido, deve essere almeno %1 ed al massimo %2",
"username-taken": "Nome utente già esistente", "username-taken": "Nome utente già esistente",
"email-taken": "Email già esistente", "email-taken": "Email già esistente",
"email-not-confirmed": "You are unable to post until your email is confirmed, please click here to confirm your email.", "email-not-confirmed": "Non puoi pubblicare post finché la tua email non è confermata, fai clic qui per confermare la tua email",
"email-not-confirmed-chat": "Non puoi chattare finché non confermi la tua email, per favore clicca qui per confermare la tua email.", "email-not-confirmed-chat": "Non puoi chattare finché non confermi la tua email, per favore clicca qui per confermare la tua email.",
"email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You won't be able to post or chat until your email is confirmed.", "email-not-confirmed-email-sent": "La tua email non è stata ancora confermata, controlla la tua casella di posta per l'email di conferma. Non potrai pubblicare post o chattare fino a quando la tua email non sarà confermata.",
"no-email-to-confirm": "Questo forum richiede una conferma via email, clicca qui per inserire un'email", "no-email-to-confirm": "Questo forum richiede una conferma via email, clicca qui per inserire un'email",
"email-confirm-failed": "Non abbiamo potuto confermare la tua email, per favore riprovaci più tardi.", "email-confirm-failed": "Non abbiamo potuto confermare la tua email, per favore riprovaci più tardi.",
"confirm-email-already-sent": "Email di conferma già inviata, per favore attendere %1 minuto(i) per inviarne un'altra.", "confirm-email-already-sent": "Email di conferma già inviata, per favore attendere %1 minuto(i) per inviarne un'altra.",
"sendmail-not-found": "Impossibile trovare l'eseguibile di sendmail, per favore assicurati che sia installato ed eseguibile dall'utente che esegue NodeBB.", "sendmail-not-found": "Impossibile trovare l'eseguibile di sendmail, per favore assicurati che sia installato ed eseguibile dall'utente che esegue NodeBB.",
"digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", "digest-not-enabled": "Questo utente non ha digest attivi o l'impostazione predefinita del sistema non è configurata per l'invio di digest",
"username-too-short": "Nome utente troppo corto", "username-too-short": "Nome utente troppo corto",
"username-too-long": "Nome utente troppo lungo", "username-too-long": "Nome utente troppo lungo",
"password-too-long": "Password troppo lunga", "password-too-long": "Password troppo lunga",

View File

@@ -159,17 +159,17 @@
"consent.email_intro": "Occasionalmente, potremmo inviare email al tuo indirizzo email registrato per fornirti aggiornamenti e/o per informarti di nuove attività che ti riguardano. Puoi personalizzare la frequenza del riepilogo della comunità (compresa la disabilitazione definitiva), così come selezionare quali tipi di notifiche ricevere via email, tramite la pagina delle impostazioni utente.", "consent.email_intro": "Occasionalmente, potremmo inviare email al tuo indirizzo email registrato per fornirti aggiornamenti e/o per informarti di nuove attività che ti riguardano. Puoi personalizzare la frequenza del riepilogo della comunità (compresa la disabilitazione definitiva), così come selezionare quali tipi di notifiche ricevere via email, tramite la pagina delle impostazioni utente.",
"consent.digest_frequency": "A meno che non sia stato modificato esplicitamente nelle impostazioni utente, questa comunità fornisce email riepilogative ogni %1.", "consent.digest_frequency": "A meno che non sia stato modificato esplicitamente nelle impostazioni utente, questa comunità fornisce email riepilogative ogni %1.",
"consent.digest_off": "A meno che non sia stato modificato esplicitamente nelle impostazioni utente, questa comunità non invia email riepilogative", "consent.digest_off": "A meno che non sia stato modificato esplicitamente nelle impostazioni utente, questa comunità non invia email riepilogative",
"consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", "consent.received": "Hai fornito il consenso a questo sito Web per raccogliere ed elaborare le tue informazioni. Non è richiesta alcuna azione aggiuntiva.",
"consent.not_received": "You have not provided consent for data collection and processing. At any time this website&apos;s administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", "consent.not_received": "Non hai fornito il consenso per la raccolta e l'elaborazione dei dati. In qualsiasi momento l'amministrazione di questo sito Web può decidere di eliminare il tuo account per renderlo conforme al regolamento generale sulla protezione dei dati.",
"consent.give": "Consenti", "consent.give": "Consenti",
"consent.right_of_access": "Hai i privilegi di accesso", "consent.right_of_access": "Hai i privilegi di accesso",
"consent.right_of_access_description": "You have the right to access any data collected by this website upon request. You can retrieve a copy of this data by clicking the appropriate button below.", "consent.right_of_access_description": "Hai il diritto di accedere a tutti i dati raccolti da questo sito Web su richiesta. È possibile recuperare una copia di questi dati facendo clic sul pulsante appropriato di seguito.",
"consent.right_to_rectification": "Hai i privilegi alla rettifica", "consent.right_to_rectification": "Hai i privilegi alla rettifica",
"consent.right_to_rectification_description": "You have the right to change or update any inaccurate data provided to us. Your profile can be updated by editing your profile, and post content can always be edited. If this is not the case, please contact this site&apos;s administrative team.", "consent.right_to_rectification_description": "Hai il diritto di modificare o aggiornare i dati inesatti forniti a noi. Il tuo profilo può essere aggiornato modificando il tuo profilo e il contenuto dei post può sempre essere modificato. In caso contrario, contattare questo team amministrativo del sito.",
"consent.right_to_erasure": "Hai i privilegi per cancellare", "consent.right_to_erasure": "Hai i privilegi per cancellare",
"consent.right_to_erasure_description": "At any time, you are able to revoke your consent to data collection and/or processing by deleting your account. Your individual profile can be deleted, although your posted content will remain. If you wish to delete both your account <strong>and</strong> your content, please contact the administrative team for this website.", "consent.right_to_erasure_description": "In qualsiasi momento, puoi revocare il tuo consenso alla raccolta e / o al trattamento dei dati eliminando il tuo account. Il tuo profilo individuale può essere eliminato, anche se i contenuti pubblicati rimarranno. Se desideri eliminare entrambi i tuoi account <strong>e</strong> i tuoi contenuti, contatta il team amministrativo per questo sito Web.",
"consent.right_to_data_portability": "Hai i privilegi alla portabilità dei dati", "consent.right_to_data_portability": "Hai i privilegi alla portabilità dei dati",
"consent.right_to_data_portability_description": "You may request from us a machine-readable export of any collected data about you and your account. You can do so by clicking the appropriate button below.", "consent.right_to_data_portability_description": "Puoi richiedere da noi un'esportazione leggibile meccanicamente di tutti i dati raccolti su di te e sul tuo account. Puoi farlo facendo clic sul pulsante appropriato in basso.",
"consent.export_profile": "Esporta i profili (.csv)", "consent.export_profile": "Esporta i profili (.csv)",
"consent.export_uploads": "Esporta i contenuti caricati (.zip)", "consent.export_uploads": "Esporta i contenuti caricati (.zip)",
"consent.export_posts": "Esporta i post (.csv)" "consent.export_posts": "Esporta i post (.csv)"

View File

@@ -20,11 +20,11 @@
"error.select-clone": "Proszę wybrać stronę do sklonowania", "error.select-clone": "Proszę wybrać stronę do sklonowania",
"title": "Title", "title": "Tytuł",
"title.placeholder": "Title (only shown on some containers)", "title.placeholder": "Tytuł (wyświetlany tylko w niektórych kontenerach)",
"container": "Container", "container": "Kontener",
"container.placeholder": "Drag and drop a container or enter HTML here.", "container.placeholder": "Przeciągnij i upuść kontener lub wpisz tutaj HTML.",
"show-to-groups": "Show to groups", "show-to-groups": "Pokaż dla grup",
"hide-from-groups": "Hide from groups", "hide-from-groups": "Ukryj dla grup",
"hide-on-mobile": "Hide on mobile" "hide-on-mobile": "Ukraj na urządzeniach mobilnych"
} }

View File

@@ -3,19 +3,19 @@
"disclaimer": "Please be advised that email delivery is not guaranteed, due to the nature of email technology. Many variables factor into whether an email sent to the recipient server is ultimately delivered into the user's inbox, including server reputation, blacklisted IP addresses, and whether DKIM/SPF/DMARC is configured.", "disclaimer": "Please be advised that email delivery is not guaranteed, due to the nature of email technology. Many variables factor into whether an email sent to the recipient server is ultimately delivered into the user's inbox, including server reputation, blacklisted IP addresses, and whether DKIM/SPF/DMARC is configured.",
"disclaimer-continued": "A successful delivery means the message was sent successfully by NodeBB and acknowledged by the recipient server. It does not mean the email landed in the inbox. For best results, we recommend using a third-party email delivery service such as <a href=\"https://sendgrid.com/why-sendgrid/\">SendGrid</a>.", "disclaimer-continued": "A successful delivery means the message was sent successfully by NodeBB and acknowledged by the recipient server. It does not mean the email landed in the inbox. For best results, we recommend using a third-party email delivery service such as <a href=\"https://sendgrid.com/why-sendgrid/\">SendGrid</a>.",
"user": "User", "user": "Użytkownik",
"subscription": "Subscription Type", "subscription": "Typ subskrypcji",
"last-delivery": "Last successful delivery", "last-delivery": "Last successful delivery",
"default": "System default", "default": "Domyślne ustawienie systemowe",
"default-help": "<em>System default</em> means the user has not explicitly overridden the global forum setting for digests, which is currently: &quot;<strong>%1</strong>&quot;", "default-help": "<em>System default</em> means the user has not explicitly overridden the global forum setting for digests, which is currently: &quot;<strong>%1</strong>&quot;",
"resend": "Resend Digest", "resend": "Wyślij ponownie podsumowanie",
"resend-all-confirm": "Are you sure you wish to mnually execute this digest run?", "resend-all-confirm": "Czy na pewno chcesz ręcznie wykonać włącznie podsumowania?",
"resent-single": "Manual digest resend completed", "resent-single": "Ręczne wysyłanie podsumowania zakończone",
"resent-day": "Daily digest resent", "resent-day": "Codzienne podsumowanie",
"resent-week": "Weekly digest resent", "resent-week": "Tygodniowe podsumowanie",
"resent-month": "Monthly digest resent", "resent-month": "Miesięczne podsumowanie",
"null": "<em>Never</em>", "null": "<em>Nigdy</em>",
"manual-run": "Manual digest run:", "manual-run": "Włącz ręcznie podsumowania",
"no-delivery-data": "No delivery data found" "no-delivery-data": "Nie znaleziono danych"
} }

View File

@@ -1,8 +1,8 @@
{ {
"global": "Globalny", "global": "Globalny",
"global.no-users": "Brak globalnych uprawnień zdefiniowanych dla użytkownika", "global.no-users": "Brak globalnych uprawnień zdefiniowanych dla użytkownika",
"group-privileges": "Group Privileges", "group-privileges": "Uprawnienia grup",
"user-privileges": "User Privileges", "user-privileges": "Uprawnienia użytkownika",
"chat": "Dostęp do czatu", "chat": "Dostęp do czatu",
"upload-images": "Przesyłanie zdjęć", "upload-images": "Przesyłanie zdjęć",
"upload-files": "Przesyłanie plików", "upload-files": "Przesyłanie plików",
@@ -16,7 +16,7 @@
"view-groups": "Wyświetlanie grup", "view-groups": "Wyświetlanie grup",
"allow-local-login": "Logowanie lokalne", "allow-local-login": "Logowanie lokalne",
"allow-group-creation": "Tworzenie grup", "allow-group-creation": "Tworzenie grup",
"view-users-info": "View Users Info", "view-users-info": "Pokaż dane użytkownika",
"find-category": "Szukanie kategorii", "find-category": "Szukanie kategorii",
"access-category": "Dostęp do kategorii", "access-category": "Dostęp do kategorii",
"access-topics": "Dostęp do tematów", "access-topics": "Dostęp do tematów",

View File

@@ -15,8 +15,8 @@
"delete": "Usuń użytkownika(-ów)", "delete": "Usuń użytkownika(-ów)",
"purge": "Usuń użytkownika(-ów) oraz zawartość", "purge": "Usuń użytkownika(-ów) oraz zawartość",
"download-csv": "Pobierz CSV", "download-csv": "Pobierz CSV",
"manage-groups": "Manage Groups", "manage-groups": "Zarządzaj grupami",
"add-group": "Add Group", "add-group": "Dodaj grupę",
"invite": "Zaproś", "invite": "Zaproś",
"new": "Nowy użytkownik", "new": "Nowy użytkownik",

View File

@@ -18,7 +18,7 @@
"manage/groups": "Grupy", "manage/groups": "Grupy",
"manage/ip-blacklist": "Czarna lista IP", "manage/ip-blacklist": "Czarna lista IP",
"manage/uploads": "Przesłane pliki", "manage/uploads": "Przesłane pliki",
"manage/digest": "Digests", "manage/digest": "Podsumownia",
"section-settings": "Ustawienia", "section-settings": "Ustawienia",
"settings/general": "Ogólne", "settings/general": "Ogólne",

View File

@@ -33,8 +33,8 @@
"testing.select": "Wybierz szablon e-maila", "testing.select": "Wybierz szablon e-maila",
"testing.send": "Wyślij testowy e-mail", "testing.send": "Wyślij testowy e-mail",
"testing.send-help": "Testowy e-mail zostanie wysłany na adres aktualnie zalogowanego użytkownika.", "testing.send-help": "Testowy e-mail zostanie wysłany na adres aktualnie zalogowanego użytkownika.",
"subscriptions": "Email Digests", "subscriptions": "Podsumowania e-mail",
"subscriptions.disable": "Disable email digests", "subscriptions.disable": "Wyłącz podsumowania e-maili",
"subscriptions.hour": "Godzina podsumowania", "subscriptions.hour": "Godzina podsumowania",
"subscriptions.hour-help": "Wprowadź liczbę odpowiadającą godzinie, o której mają być wysyłane regularne e-maile z podsumowaniem (np. <code>0</code> dla północy lub <code>17</code> dla 17:00). Pamiętaj, że godzina jest godziną serwera i nie musi zgadzać się z czasem lokalnym administratora. Przybliżony czas serwera to: <span id=\"serverTime\"></span><br /> Wysłanie kolejnego e-maila z podsumowaniem zaplanowano na <span id=\"nextDigestTime\"></span>" "subscriptions.hour-help": "Wprowadź liczbę odpowiadającą godzinie, o której mają być wysyłane regularne e-maile z podsumowaniem (np. <code>0</code> dla północy lub <code>17</code> dla 17:00). Pamiętaj, że godzina jest godziną serwera i nie musi zgadzać się z czasem lokalnym administratora. Przybliżony czas serwera to: <span id=\"serverTime\"></span><br /> Wysłanie kolejnego e-maila z podsumowaniem zaplanowano na <span id=\"nextDigestTime\"></span>"
} }

View File

@@ -7,12 +7,12 @@
"sorting.most-posts": "Najwięcej postów", "sorting.most-posts": "Najwięcej postów",
"sorting.topic-default": "Domyślne sortowanie tematów", "sorting.topic-default": "Domyślne sortowanie tematów",
"length": "Długość postu", "length": "Długość postu",
"post-queue": "Post Queue", "post-queue": "Kolejka postów",
"restrictions": "Restrykcje postowania", "restrictions": "Restrykcje postowania",
"restrictions-new": "Restrykcje dla nowych użytkowników", "restrictions-new": "Restrykcje dla nowych użytkowników",
"restrictions.post-queue": "Włącz kolejkę postów", "restrictions.post-queue": "Włącz kolejkę postów",
"restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", "restrictions.post-queue-rep-threshold": "Reputacja wymagana do ominięcia kolejki postów",
"restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", "restrictions.groups-exempt-from-post-queue": "Wybierz grupy, które powinny być zwolnione z kolejki postów",
"restrictions-new.post-queue": "Włącz restrykcje dla nowych użytkowników", "restrictions-new.post-queue": "Włącz restrykcje dla nowych użytkowników",
"restrictions.post-queue-help": "Włączenie kolejki postów spowoduje umieszczenie postów nowych użytkowników w kolejce do zatwierdzenia", "restrictions.post-queue-help": "Włączenie kolejki postów spowoduje umieszczenie postów nowych użytkowników w kolejce do zatwierdzenia",
"restrictions-new.post-queue-help": "Włączenie restrykcji dla nowych użytkowników ustawi restrykcje na ich wpisy.", "restrictions-new.post-queue-help": "Włączenie restrykcji dla nowych użytkowników ustawi restrykcje na ich wpisy.",
@@ -32,8 +32,8 @@
"timestamp": "Znacznik czasowy", "timestamp": "Znacznik czasowy",
"timestamp.cut-off": "Termin odcięcia (w dniach)", "timestamp.cut-off": "Termin odcięcia (w dniach)",
"timestamp.cut-off-help": "Daty oraz godziny będą wyświetlane w sposób relatywny (np. \"3 godziny temu\" / \"5 dni temu\"), oraz przetłumaczone na różne\n\t\t\t\t\tjęzyki. Po określonym czasie, ten tekst może zostać zmieniony, aby wyświetlać sformatowane daty.\n\t\t\t\t\t(np. 4 Lut 2017 12:45).<br /><em>(domyślnie: <code>30</code>, lub jeden miesiąc). Ustaw 0, aby zawsze wyświetlać daty; pozostaw puste, aby korzystać z tylko z relatywnych opisów.</em>", "timestamp.cut-off-help": "Daty oraz godziny będą wyświetlane w sposób relatywny (np. \"3 godziny temu\" / \"5 dni temu\"), oraz przetłumaczone na różne\n\t\t\t\t\tjęzyki. Po określonym czasie, ten tekst może zostać zmieniony, aby wyświetlać sformatowane daty.\n\t\t\t\t\t(np. 4 Lut 2017 12:45).<br /><em>(domyślnie: <code>30</code>, lub jeden miesiąc). Ustaw 0, aby zawsze wyświetlać daty; pozostaw puste, aby korzystać z tylko z relatywnych opisów.</em>",
"timestamp.necro-threshold": "Necro Threshold (in days)", "timestamp.necro-threshold": "Próg nekro (w dniach)",
"timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: <code>7</code>, or one week). Set to 0 to disable.</em>", "timestamp.necro-threshold-help": "Komunikat zostanie wyświetlony między postami, jeśli czas między nimi jest dłuższy niż próg nekro. (Domyślnie: <code>7</code> lub jeden tydzień). Ustaw na 0, aby wyłączyć.</em>",
"teaser": "Zwiastun postu", "teaser": "Zwiastun postu",
"teaser.last-post": "Ostatni &ndash; Pokaż ostatni post, włączając pierwszy post, w razie braku odpowiedzi", "teaser.last-post": "Ostatni &ndash; Pokaż ostatni post, włączając pierwszy post, w razie braku odpowiedzi",
"teaser.last-reply": "Ostatni &ndash; Pokaż ostatnią odpowiedź lub komunikat „Brak odpowiedzi” w razie ich braku", "teaser.last-reply": "Ostatni &ndash; Pokaż ostatnią odpowiedź lub komunikat „Brak odpowiedzi” w razie ich braku",

View File

@@ -34,7 +34,7 @@
"notif.chat.cta": "Kliknij tutaj, aby kontynuować rozmowę", "notif.chat.cta": "Kliknij tutaj, aby kontynuować rozmowę",
"notif.chat.unsub.info": "To powiadomienie o czacie zostało wysłane zgodnie z Twoimi ustawieniami.", "notif.chat.unsub.info": "To powiadomienie o czacie zostało wysłane zgodnie z Twoimi ustawieniami.",
"notif.post.unsub.info": "To powiadomienie o poście zostało wysłane zgodnie z Twoimi ustawieniami.", "notif.post.unsub.info": "To powiadomienie o poście zostało wysłane zgodnie z Twoimi ustawieniami.",
"notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", "notif.post.unsub.one-click": "Możesz zrezygnować z otrzymywania takich e-maili w przyszłości, klikając",
"notif.cta": "Na forum", "notif.cta": "Na forum",
"notif.cta-new-reply": "Pokaż wpisy", "notif.cta-new-reply": "Pokaż wpisy",
"notif.cta-new-chat": "Pokaż czat", "notif.cta-new-chat": "Pokaż czat",
@@ -42,8 +42,8 @@
"notif.test.long": "To jest email testowy z powiadomieniami. Wyślij pomoc!", "notif.test.long": "To jest email testowy z powiadomieniami. Wyślij pomoc!",
"test.text1": "To jest e-mail testowy wysyłany w celu sprawdzenia konfiguracji e-mailera w NodeBB.", "test.text1": "To jest e-mail testowy wysyłany w celu sprawdzenia konfiguracji e-mailera w NodeBB.",
"unsub.cta": "Kliknij tutaj, aby zmienić te ustawienia", "unsub.cta": "Kliknij tutaj, aby zmienić te ustawienia",
"unsubscribe": "unsubscribe", "unsubscribe": "Wypisz się",
"unsub.success": "You will no longer receive emails from the <strong>%1</strong> mailing list", "unsub.success": "Nie będziesz już otrzymywać wiadomości e-mail z <strong>%1</strong>",
"banned.subject": "Zostałeś zbanowany na %1", "banned.subject": "Zostałeś zbanowany na %1",
"banned.text1": "Użytkownik %1 został zbanowany na %2.", "banned.text1": "Użytkownik %1 został zbanowany na %2.",
"banned.text2": "Ban potrwa do %1", "banned.text2": "Ban potrwa do %1",

View File

@@ -5,15 +5,15 @@
"account-locked": "Twoje konto zostało tymczasowo zablokowane", "account-locked": "Twoje konto zostało tymczasowo zablokowane",
"search-requires-login": "Wyszukiwanie wymaga konta - zaloguj się lub zarejestruj.", "search-requires-login": "Wyszukiwanie wymaga konta - zaloguj się lub zarejestruj.",
"goback": "Wciśnij wstecz, aby powrócić do poprzedniej strony", "goback": "Wciśnij wstecz, aby powrócić do poprzedniej strony",
"invalid-cid": "Błędne ID kategorii", "invalid-cid": "Nieprawidłowy ID kategorii",
"invalid-tid": "Błędne ID tematu", "invalid-tid": "Nieprawidłowy ID tematu",
"invalid-pid": "Błędne ID posta", "invalid-pid": "Nieprawidłowy ID posta",
"invalid-uid": "Błędne ID użytkownika", "invalid-uid": "Nieprawidłowy ID użytkownika",
"invalid-username": "Błędny login", "invalid-username": "Nieprawidłowy login",
"invalid-email": "Błędny e-mail", "invalid-email": "Nieprawidłowy adres e-mail",
"invalid-fullname": "Invalid Fullname", "invalid-fullname": "Nieprawidłowa nazwa",
"invalid-location": "Invalid Location", "invalid-location": "Nieprawidłowa lokalizacja",
"invalid-birthday": "Invalid Birthday", "invalid-birthday": "Nieprawidłowa data urodzenia",
"invalid-title": "Błędna nazwa", "invalid-title": "Błędna nazwa",
"invalid-user-data": "Błędne dane użytkownika", "invalid-user-data": "Błędne dane użytkownika",
"invalid-password": "Błędne hasło", "invalid-password": "Błędne hasło",
@@ -26,14 +26,14 @@
"invalid-pagination-value": "Błędna wartość paginacji, zakres od %1 do %2", "invalid-pagination-value": "Błędna wartość paginacji, zakres od %1 do %2",
"username-taken": "Login zajęty", "username-taken": "Login zajęty",
"email-taken": "Email zajęty", "email-taken": "Email zajęty",
"email-not-confirmed": "You are unable to post until your email is confirmed, please click here to confirm your email.", "email-not-confirmed": "Nie możesz opublikować, dopóki Twój adres e-mail nie zostanie potwierdzony, kliknij tutaj, aby potwierdzić swój adres e-mail.",
"email-not-confirmed-chat": "Nie możesz prowadzić rozmów, dopóki twój email nie zostanie potwierdzony. Kliknij tutaj, aby potwierdzić swój email.", "email-not-confirmed-chat": "Nie możesz prowadzić rozmów, dopóki twój email nie zostanie potwierdzony. Kliknij tutaj, aby potwierdzić swój email.",
"email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You won't be able to post or chat until your email is confirmed.", "email-not-confirmed-email-sent": "Twój e-mail nie został jeszcze potwierdzony, sprawdź swoją skrzynkę pocztową, aby znaleźć e-mail z potwierdzeniem. Nie będziesz mógł dodawać postów ani czatować, dopóki Twój adres e-mail nie zostanie potwierdzony.",
"no-email-to-confirm": "To forum wymaga weryfikacji przez email. Proszę kliknąć tutaj, aby wprowadzić adres.", "no-email-to-confirm": "To forum wymaga weryfikacji przez email. Proszę kliknąć tutaj, aby wprowadzić adres.",
"email-confirm-failed": "Nie byliśmy w stanie potwierdzić Twojego adresu e-mail. Spróbuj później.", "email-confirm-failed": "Nie byliśmy w stanie potwierdzić Twojego adresu e-mail. Spróbuj później.",
"confirm-email-already-sent": "Email potwierdzający został już wysłany, proszę odczekaj jeszcze %1 minut(y), aby wysłać kolejny.", "confirm-email-already-sent": "Email potwierdzający został już wysłany, proszę odczekaj jeszcze %1 minut(y), aby wysłać kolejny.",
"sendmail-not-found": "Program sendmail nie został znaleziony, proszę upewnij się, że jest zainstalowany i możliwy do uruchomienia przez użytkownika uruchamiającego NodeBB.", "sendmail-not-found": "Program sendmail nie został znaleziony, proszę upewnij się, że jest zainstalowany i możliwy do uruchomienia przez użytkownika uruchamiającego NodeBB.",
"digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", "digest-not-enabled": "Ten użytkownik nie ma włączonych skrótów lub system nie jest skonfigurowany do wysyłania skrótów",
"username-too-short": "Nazwa użytkownika za krótka", "username-too-short": "Nazwa użytkownika za krótka",
"username-too-long": "Zbyt długa nazwa użytkownika", "username-too-long": "Zbyt długa nazwa użytkownika",
"password-too-long": "Hasło jest za długie", "password-too-long": "Hasło jest za długie",
@@ -103,8 +103,8 @@
"group-needs-owner": "Ta grupa musi mieć przynajmniej jednego właściciela", "group-needs-owner": "Ta grupa musi mieć przynajmniej jednego właściciela",
"group-already-invited": "Ten użytkownik został już zaproszony", "group-already-invited": "Ten użytkownik został już zaproszony",
"group-already-requested": "Twoje podanie o członkostwo zostało już wysłane", "group-already-requested": "Twoje podanie o członkostwo zostało już wysłane",
"group-join-disabled": "You are not able to join this group at this time", "group-join-disabled": "Nie możesz teraz dołączyć do tej grupy",
"group-leave-disabled": "You are not able to leave this group at this time", "group-leave-disabled": "Obecnie nie możesz opuścić tej grupy",
"post-already-deleted": "Ten post został już skasowany", "post-already-deleted": "Ten post został już skasowany",
"post-already-restored": "Ten post został już przywrócony", "post-already-restored": "Ten post został już przywrócony",
"topic-already-deleted": "Ten temat został już skasowany", "topic-already-deleted": "Ten temat został już skasowany",

View File

@@ -25,7 +25,7 @@
"details.latest_posts": "Ostatnie posty", "details.latest_posts": "Ostatnie posty",
"details.private": "Prywatna", "details.private": "Prywatna",
"details.disableJoinRequests": "Wyłączono prośbę o dołączenie", "details.disableJoinRequests": "Wyłączono prośbę o dołączenie",
"details.disableLeave": "Disallow users from leaving the group", "details.disableLeave": "Wyłącz możliwość opuszczania użytkowników z grupy",
"details.grant": "Nadaj/Cofnij prawa Właściciela", "details.grant": "Nadaj/Cofnij prawa Właściciela",
"details.kick": "Wykop", "details.kick": "Wykop",
"details.kick_confirm": "Jesteś pewny, że chcesz wyrzucić tego użytkownika z grupy?", "details.kick_confirm": "Jesteś pewny, że chcesz wyrzucić tego użytkownika z grupy?",
@@ -49,11 +49,11 @@
"event.updated": "Dane grupy zostały zaktualizowane", "event.updated": "Dane grupy zostały zaktualizowane",
"event.deleted": "Grupa \"%1\" została usunięta", "event.deleted": "Grupa \"%1\" została usunięta",
"membership.accept-invitation": "Przyjmij zaproszenie", "membership.accept-invitation": "Przyjmij zaproszenie",
"membership.accept.notification_title": "You are now a member of <strong>%1</strong>", "membership.accept.notification_title": "Jesteś teraz członkiem <strong>%1</strong>",
"membership.invitation-pending": "Oczekujące zaproszenie", "membership.invitation-pending": "Oczekujące zaproszenie",
"membership.join-group": "Dołącz do grupy", "membership.join-group": "Dołącz do grupy",
"membership.leave-group": "Opuść grupę", "membership.leave-group": "Opuść grupę",
"membership.leave.notification_title": "<strong>%1</strong> has left group <strong>%2</strong>", "membership.leave.notification_title": "<strong>%1</strong> opuścił grupę <strong>%2</strong>",
"membership.reject": "Odrzuć", "membership.reject": "Odrzuć",
"new-group.group_name": "Nazwa grupy:", "new-group.group_name": "Nazwa grupy:",
"upload-group-cover": "Prześlij zdjęcie tła grupy", "upload-group-cover": "Prześlij zdjęcie tła grupy",

View File

@@ -1,6 +1,6 @@
{ {
"reset_password": "Zresetuj hasło", "reset_password": "Zresetuj hasło",
"update_password": "Zmień hasło", "update_password": "Zaktualizuj hasło",
"password_changed.title": "Hasło zmienione", "password_changed.title": "Hasło zmienione",
"password_changed.message": "<p>Hasło zostało zmienione. <a href=\"/login\">Zaloguj się ponownie</a>.", "password_changed.message": "<p>Hasło zostało zmienione. <a href=\"/login\">Zaloguj się ponownie</a>.",
"wrong_reset_code.title": "Nieprawidłowy kod resetujący", "wrong_reset_code.title": "Nieprawidłowy kod resetujący",
@@ -9,7 +9,7 @@
"repeat_password": "Powtórz hasło", "repeat_password": "Powtórz hasło",
"enter_email": "Podaj swój <strong>adres e-mail</strong>, by otrzymać wiadomość z instrukcjami, jak zresetować hasło.", "enter_email": "Podaj swój <strong>adres e-mail</strong>, by otrzymać wiadomość z instrukcjami, jak zresetować hasło.",
"enter_email_address": "Wpisz swój adres e-mail", "enter_email_address": "Wpisz swój adres e-mail",
"password_reset_sent": "If the specified address corresponds to an existing user account, a password reset email was sent. Please note that only one email will be sent per minute.", "password_reset_sent": "Jeśli podany adres odpowiada istniejącemu kontu użytkownika, to zostanie wysłana wiadomość e-mail dotyczącą resetowania hasła. Pamiętaj, że na minutę zostanie wysłany tylko jeden e-mail.",
"invalid_email": "Nieprawidłowy adres e-mail.", "invalid_email": "Nieprawidłowy adres e-mail.",
"password_too_short": "Wprowadzone hasło jest zbyt krótkie, wybierz inne hasło.", "password_too_short": "Wprowadzone hasło jest zbyt krótkie, wybierz inne hasło.",
"passwords_do_not_match": "Wprowadzone hasła nie pasują do siebie", "passwords_do_not_match": "Wprowadzone hasła nie pasują do siebie",

View File

@@ -34,7 +34,7 @@
"moved": "Przeniesiony", "moved": "Przeniesiony",
"copy-ip": "Kopiuj IP", "copy-ip": "Kopiuj IP",
"ban-ip": "Blokuj IP", "ban-ip": "Blokuj IP",
"view-history": "Edytuj historię", "view-history": "Historia edycji",
"bookmark_instructions": "Kliknij tutaj, by powrócić do ostatniego przeczytanego postu w tym temacie.", "bookmark_instructions": "Kliknij tutaj, by powrócić do ostatniego przeczytanego postu w tym temacie.",
"flag_title": "Zgłoś post do moderacji", "flag_title": "Zgłoś post do moderacji",
"merged_message": "Ten temat został połączony z <a href=\"/topic/%1\">%2</a>", "merged_message": "Ten temat został połączony z <a href=\"/topic/%1\">%2</a>",

View File

@@ -26,7 +26,7 @@
"reputation": "Reputacja", "reputation": "Reputacja",
"bookmarks": "Zakładki", "bookmarks": "Zakładki",
"watched_categories": "Obserwowane kategorie", "watched_categories": "Obserwowane kategorie",
"change_all": "Change All", "change_all": "Zmień wszystko",
"watched": "Obserwowane", "watched": "Obserwowane",
"ignored": "Zignorowane", "ignored": "Zignorowane",
"default-category-watch-state": "Domyślny stan oglądania kategorii", "default-category-watch-state": "Domyślny stan oglądania kategorii",
@@ -125,7 +125,7 @@
"follow_topics_you_reply_to": "Obserwuj tematy, w których uczestniczysz", "follow_topics_you_reply_to": "Obserwuj tematy, w których uczestniczysz",
"follow_topics_you_create": "Obserwuj tematy, które utworzyłeś", "follow_topics_you_create": "Obserwuj tematy, które utworzyłeś",
"grouptitle": "Nazwa grupy", "grouptitle": "Nazwa grupy",
"group-order-help": "Select a group and use the arrows to order titles", "group-order-help": "Wybierz grupę i użyj strzałek, aby zamówić tytuł",
"no-group-title": "Brak nazwy grupy", "no-group-title": "Brak nazwy grupy",
"select-skin": "Wybierz skórkę", "select-skin": "Wybierz skórkę",
"select-homepage": "Wybierz stronę startową", "select-homepage": "Wybierz stronę startową",

View File

@@ -24,8 +24,8 @@
"plugin-item.install": "Установить", "plugin-item.install": "Установить",
"plugin-item.uninstall": "Удалить", "plugin-item.uninstall": "Удалить",
"plugin-item.settings": "Настройки", "plugin-item.settings": "Настройки",
"plugin-item.installed": "Установленные", "plugin-item.installed": "Установленная версия",
"plugin-item.latest": "Недавние", "plugin-item.latest": "Последняя версия",
"plugin-item.upgrade": "Обновить", "plugin-item.upgrade": "Обновить",
"plugin-item.more-info": "Дополнительная информация:", "plugin-item.more-info": "Дополнительная информация:",
"plugin-item.unknown": "Неизвестно", "plugin-item.unknown": "Неизвестно",

View File

@@ -10,13 +10,13 @@
"watch": "Стежити", "watch": "Стежити",
"ignore": "Ігнорувати", "ignore": "Ігнорувати",
"watching": "Відстежується", "watching": "Відстежується",
"not-watching": "Not Watching", "not-watching": "Не спостерігається",
"ignoring": "Ігнорувати", "ignoring": "Ігнорувати",
"watching.description": "Show topics in unread and recent", "watching.description": "Показати теми в непрочитаних та останніх",
"not-watching.description": "Do not show topics in unread, show in recent", "not-watching.description": "Не показувати теми в непрочитаних, показувати в останніх",
"ignoring.description": "Do not show topics in unread and recent", "ignoring.description": "Не показувати теми в непрочитаних і останніх",
"watching.message": "You are now watching updates from this category and all subcategories", "watching.message": "Ви зараз спостерігаєте за оновленнями з цієї категорії та всіх її підкатегорій",
"notwatching.message": "You are not watching updates from this category and all subcategories", "notwatching.message": "Зараз ви не спостерігаєте за оновленнями з цієї категорії та всіх її підкатегорій",
"ignoring.message": "You are now ignoring updates from this category and all subcategories", "ignoring.message": "Зараз ви ігноруєте оновлення з цієї категорії та всіх її підкатегорій",
"watched-categories": "Переглянуті категорії" "watched-categories": "Переглянуті категорії"
} }

View File

@@ -1,19 +1,19 @@
{ {
"test-email.subject": "Test Email", "test-email.subject": "Тестове поштове повідомлення",
"password-reset-requested": "Password Reset Requested!", "password-reset-requested": "Отримано запит на скидання пароля!",
"welcome-to": "Ласкаво просимо до %1", "welcome-to": "Ласкаво просимо до %1",
"invite": "Запрошення від %1", "invite": "Запрошення від %1",
"greeting_no_name": "Привіт", "greeting_no_name": "Привіт",
"greeting_with_name": "Привіт %1", "greeting_with_name": "Привіт %1",
"email.verify-your-email.subject": "Please verify your email", "email.verify-your-email.subject": "Будь-ласка перевірте вашу електронну адресу",
"email.verify.text1": "Your email address has changed!", "email.verify.text1": "Ваша електронна адреса змінилась!",
"welcome.text1": "Дякуємо за реєстрацію з %1!", "welcome.text1": "Дякуємо за реєстрацію з %1!",
"welcome.text2": "Щоб повністю активувати ваш акаунт, нам потрібно перевірити, що вам належить електронна адреса, яку ви вказали при реєстрації ", "welcome.text2": "Щоб повністю активувати ваш акаунт, нам потрібно перевірити, що вам належить електронна адреса, яку ви вказали при реєстрації ",
"welcome.text3": "Адміністратор схвалив ваш запит на реєстрацію. Ви можете залогінитись, використовуючи свій пароль та назву акаунту", "welcome.text3": "Адміністратор схвалив ваш запит на реєстрацію. Ви можете залогінитись, використовуючи свій пароль та назву акаунту",
"welcome.cta": "Натисніть тут, щоб підтвердити вашу електронну адресу", "welcome.cta": "Натисніть тут, щоб підтвердити вашу електронну адресу",
"invitation.text1": "%1 запросив вас приєднатися до %2", "invitation.text1": "%1 запросив вас приєднатися до %2",
"invitation.text2": "Термін дії вашого запрошення закінчиться за %1 днів.", "invitation.text2": "Термін дії вашого запрошення закінчиться за %1 днів.",
"invitation.cta": "Click here to create your account.", "invitation.cta": "Натисніть тут щоб створити акаунт.",
"reset.text1": "Ми отримали запит на відновлення вашого паролю, можливо тому, что ви його забули. Якщо вам це не потрібно - проігноруйте цей лист", "reset.text1": "Ми отримали запит на відновлення вашого паролю, можливо тому, что ви його забули. Якщо вам це не потрібно - проігноруйте цей лист",
"reset.text2": "Щоб продовжити відновлення паролю, будь ласка, перейдіть за посиланням", "reset.text2": "Щоб продовжити відновлення паролю, будь ласка, перейдіть за посиланням",
"reset.cta": "Натисніть тут щоб скинути Ваш пароль", "reset.cta": "Натисніть тут щоб скинути Ваш пароль",
@@ -27,23 +27,23 @@
"digest.week": "тиждень", "digest.week": "тиждень",
"digest.month": "місяць", "digest.month": "місяць",
"digest.subject": "Дайджест для %1", "digest.subject": "Дайджест для %1",
"digest.title.day": "Your Daily Digest", "digest.title.day": "Ваш щоденний дайджест",
"digest.title.week": "Your Weekly Digest", "digest.title.week": "Ваш тижневий дайджест",
"digest.title.month": "Your Monthly Digest", "digest.title.month": "Ваш місячний дайджест",
"notif.chat.subject": "Отримане нове повідомлення чату від %1", "notif.chat.subject": "Отримане нове повідомлення чату від %1",
"notif.chat.cta": "Натисніть тут, щоб продовжити розмову", "notif.chat.cta": "Натисніть тут, щоб продовжити розмову",
"notif.chat.unsub.info": "Це повідомлення чату було вислано вам, згідно ваших налаштувань підписки", "notif.chat.unsub.info": "Це повідомлення чату було вислано вам, згідно ваших налаштувань підписки",
"notif.post.unsub.info": "Це поштове повідомлення було вислано вам, згідно ваших налаштувань підписки", "notif.post.unsub.info": "Це поштове повідомлення було вислано вам, згідно ваших налаштувань підписки",
"notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", "notif.post.unsub.one-click": "Ви також можете відписатись від схожих майбутніх повідомлень, натиснувши тут",
"notif.cta": "To the forum", "notif.cta": "На форум",
"notif.cta-new-reply": "View Post", "notif.cta-new-reply": "Переглянути допис",
"notif.cta-new-chat": "View Chat", "notif.cta-new-chat": "Переглянути чат",
"notif.test.short": "Testing Notifications", "notif.test.short": "Перевірка сповіщень",
"notif.test.long": "This is a test of the notifications email. Send help!", "notif.test.long": "Це перевірка повідомлення про сповіщення.",
"test.text1": "Це пробний лист для верифікації поштової служби. Всі налаштування вірні для NodeBB.", "test.text1": "Це пробний лист для верифікації поштової служби. Всі налаштування вірні для NodeBB.",
"unsub.cta": "Натисніть тут, щоб змінити ці налаштування", "unsub.cta": "Натисніть тут, щоб змінити ці налаштування",
"unsubscribe": "unsubscribe", "unsubscribe": "відписатись",
"unsub.success": "You will no longer receive emails from the <strong>%1</strong> mailing list", "unsub.success": "Ви більше не будете отримувати повідомлення з <strong>%1</strong> поштової розсилки",
"banned.subject": "Ви були забанені на %1", "banned.subject": "Ви були забанені на %1",
"banned.text1": "Користувач %1 був забанений на %2.", "banned.text1": "Користувач %1 був забанений на %2.",
"banned.text2": "Тривалість бану - до %1.", "banned.text2": "Тривалість бану - до %1.",

View File

@@ -11,9 +11,9 @@
"invalid-uid": "Невірний ID користувача", "invalid-uid": "Невірний ID користувача",
"invalid-username": "Невірне ім'я користувача", "invalid-username": "Невірне ім'я користувача",
"invalid-email": "Невірна електронна адреса", "invalid-email": "Невірна електронна адреса",
"invalid-fullname": "Invalid Fullname", "invalid-fullname": "Невірне повне ім'я",
"invalid-location": "Invalid Location", "invalid-location": "Невірне місцезнаходження",
"invalid-birthday": "Invalid Birthday", "invalid-birthday": "Невірна дата народження",
"invalid-title": "Невірний заголовок", "invalid-title": "Невірний заголовок",
"invalid-user-data": "Невірні користувацькі дані", "invalid-user-data": "Невірні користувацькі дані",
"invalid-password": "Невірний пароль", "invalid-password": "Невірний пароль",
@@ -26,18 +26,18 @@
"invalid-pagination-value": "Невірне значення сторінки, має бути щонайменше %1 та щонайбільше %2", "invalid-pagination-value": "Невірне значення сторінки, має бути щонайменше %1 та щонайбільше %2",
"username-taken": "Це ім'я зайняте", "username-taken": "Це ім'я зайняте",
"email-taken": "Ця електронна пошта зайнята", "email-taken": "Ця електронна пошта зайнята",
"email-not-confirmed": "You are unable to post until your email is confirmed, please click here to confirm your email.", "email-not-confirmed": "Ви не зможете постити до підтвердження вашої електронної адреси, будь-ласка натисніть тут щоб підтвердити свій емейл.",
"email-not-confirmed-chat": "Ви не можете користуватися чатом поки ваша електронна пошта не буде підтверджена, натисніть тут, щоб це зробити.", "email-not-confirmed-chat": "Ви не можете користуватися чатом поки ваша електронна пошта не буде підтверджена, натисніть тут, щоб це зробити.",
"email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You won't be able to post or chat until your email is confirmed.", "email-not-confirmed-email-sent": "Ваша електронна адреса ще не була підтверджена, будь-ласка перевірте свою поштову скриньку. Ви не зможете постити або чатитись до того як ваш емейл підтверджено.",
"no-email-to-confirm": "Цей форум вимагає підтвердження електронної пошти, будь-ласка, натисніть тут, щоб його ввести.", "no-email-to-confirm": "Цей форум вимагає підтвердження електронної пошти, будь-ласка, натисніть тут, щоб його ввести.",
"email-confirm-failed": "Ми не можемо підтвердити вашу електронну пошту, будь ласка, спробуйте пізніше.", "email-confirm-failed": "Ми не можемо підтвердити вашу електронну пошту, будь ласка, спробуйте пізніше.",
"confirm-email-already-sent": "Підтвердження по електронній пошті вже було надіслано, зачекайте, будь ласка, %1 хвилин(и), щоб відправити ще одне. ", "confirm-email-already-sent": "Підтвердження по електронній пошті вже було надіслано, зачекайте, будь ласка, %1 хвилин(и), щоб відправити ще одне. ",
"sendmail-not-found": "Виконуваний файл sendmail не знайдено, переконайтесь, будь ласка, що його встановлено та що він виконується власником процесу NodeBB.", "sendmail-not-found": "Виконуваний файл sendmail не знайдено, переконайтесь, будь ласка, що його встановлено та що він виконується власником процесу NodeBB.",
"digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", "digest-not-enabled": "Цей користувач не має активних дайджестів, або налаштування по замовчанню не включають надсилання дайджестів.",
"username-too-short": "Ім'я користувача закоротке", "username-too-short": "Ім'я користувача закоротке",
"username-too-long": "Ім'я користувача задовге", "username-too-long": "Ім'я користувача задовге",
"password-too-long": "Пароль задовгий", "password-too-long": "Пароль задовгий",
"reset-rate-limited": "Too many password reset requests (rate limited)", "reset-rate-limited": "Занадто багато запитів на скидання паролю (кількість за період часу обмежена)",
"user-banned": "Користувача забанено", "user-banned": "Користувача забанено",
"user-banned-reason": "Вибачте, але цей акаунт було забанено (Причина: %1)", "user-banned-reason": "Вибачте, але цей акаунт було забанено (Причина: %1)",
"user-banned-reason-until": "Вибачте, цей акаунт забанений до %1 (Причина: %2)", "user-banned-reason-until": "Вибачте, цей акаунт забанений до %1 (Причина: %2)",
@@ -83,7 +83,7 @@
"still-uploading": "Зачекайте, будь ласка, доки завантаження завершиться.", "still-uploading": "Зачекайте, будь ласка, доки завантаження завершиться.",
"file-too-big": "Максимальний розмір файлу %1 кБ — завантажте менший файл, будь ласка.", "file-too-big": "Максимальний розмір файлу %1 кБ — завантажте менший файл, будь ласка.",
"guest-upload-disabled": "Гостьове завантаження вимкнено.", "guest-upload-disabled": "Гостьове завантаження вимкнено.",
"cors-error": "Unable to upload image due to misconfigured CORS", "cors-error": "Неможливо завантажити зображення через неправильно налаштований CORS",
"already-bookmarked": "Ви вже додали цей пост собі в закладки", "already-bookmarked": "Ви вже додали цей пост собі в закладки",
"already-unbookmarked": "Ви вже видалили цей пост із закладок", "already-unbookmarked": "Ви вже видалили цей пост із закладок",
"cant-ban-other-admins": "Ви не можете банити інших адмінів!", "cant-ban-other-admins": "Ви не можете банити інших адмінів!",
@@ -93,7 +93,7 @@
"invalid-image-type": "Невірний тип зображення. Дозволені типи: %1", "invalid-image-type": "Невірний тип зображення. Дозволені типи: %1",
"invalid-image-extension": "Невірне розширення зображення", "invalid-image-extension": "Невірне розширення зображення",
"invalid-file-type": "Невірний тип файлу. Дозволені типи: %1", "invalid-file-type": "Невірний тип файлу. Дозволені типи: %1",
"invalid-image-dimensions": "Image dimensions are too big", "invalid-image-dimensions": "Зображення занадто велике",
"group-name-too-short": "Ім'я групи занадто коротке", "group-name-too-short": "Ім'я групи занадто коротке",
"group-name-too-long": "Ім'я групи занадто довге", "group-name-too-long": "Ім'я групи занадто довге",
"group-already-exists": "Група вже існує", "group-already-exists": "Група вже існує",
@@ -103,8 +103,8 @@
"group-needs-owner": "Ця група потребує щонайменше одного власника", "group-needs-owner": "Ця група потребує щонайменше одного власника",
"group-already-invited": "Користувача вже було запрошено", "group-already-invited": "Користувача вже було запрошено",
"group-already-requested": "Ваша заявка на вступ вже подана", "group-already-requested": "Ваша заявка на вступ вже подана",
"group-join-disabled": "You are not able to join this group at this time", "group-join-disabled": "Ви не можете приєднатись до цієї групи зараз",
"group-leave-disabled": "You are not able to leave this group at this time", "group-leave-disabled": "Ви не можете покинути цю групу зараз",
"post-already-deleted": "Цей пост вже видалено", "post-already-deleted": "Цей пост вже видалено",
"post-already-restored": "Цей пост вже відновлено", "post-already-restored": "Цей пост вже відновлено",
"topic-already-deleted": "Ця тема вже була видалена", "topic-already-deleted": "Ця тема вже була видалена",
@@ -127,7 +127,7 @@
"chat-edit-duration-expired": "Ви можете редагувати повідомлення чату лише через %1 секунд після публікації", "chat-edit-duration-expired": "Ви можете редагувати повідомлення чату лише через %1 секунд після публікації",
"chat-delete-duration-expired": "Ви можете видаляти повідомлення чату лише через %1 секунд після публікації", "chat-delete-duration-expired": "Ви можете видаляти повідомлення чату лише через %1 секунд після публікації",
"chat-deleted-already": "Це повідомлення чату вже було видалено.", "chat-deleted-already": "Це повідомлення чату вже було видалено.",
"chat-restored-already": "This chat message has already been restored.", "chat-restored-already": "Це чат повідомлення вже було відновлене",
"already-voting-for-this-post": "Ви вже проголосували за цей пост.", "already-voting-for-this-post": "Ви вже проголосували за цей пост.",
"reputation-system-disabled": "Система репутацій вимкнена.", "reputation-system-disabled": "Система репутацій вимкнена.",
"downvoting-disabled": "Голосування проти вимкнено", "downvoting-disabled": "Голосування проти вимкнено",
@@ -159,8 +159,8 @@
"cant-move-to-same-topic": "Ви не можете перемістити пост до тієї ж самої теми!", "cant-move-to-same-topic": "Ви не можете перемістити пост до тієї ж самої теми!",
"cannot-block-self": "Ви не можете заблокувати самого себе!", "cannot-block-self": "Ви не можете заблокувати самого себе!",
"cannot-block-privileged": "Ви не можете заблокувати адміністраторів або глобальних модераторів", "cannot-block-privileged": "Ви не можете заблокувати адміністраторів або глобальних модераторів",
"cannot-block-guest": "Guest are not able to block other users", "cannot-block-guest": "Гості не можуть блокувати інших користувачів",
"already-blocked": "This user is already blocked", "already-blocked": "Цей користувач вже заблокований",
"already-unblocked": "This user is already unblocked", "already-unblocked": "Цей користувач вже розблокований",
"no-connection": "Схоже, виникла проблема з вашим Інтернет-з'єднанням" "no-connection": "Схоже, виникла проблема з вашим Інтернет-з'єднанням"
} }

View File

@@ -59,8 +59,8 @@
"downvoted": "Проти", "downvoted": "Проти",
"views": "Перегляди", "views": "Перегляди",
"reputation": "Репутація", "reputation": "Репутація",
"lastpost": "Last post", "lastpost": "Останній допис",
"firstpost": "First post", "firstpost": "Перший допис",
"read_more": "читати далі", "read_more": "читати далі",
"more": "Більше", "more": "Більше",
"posted_ago_by_guest": "запостив Гість %1", "posted_ago_by_guest": "запостив Гість %1",
@@ -87,7 +87,7 @@
"language": "Мова", "language": "Мова",
"guest": "Гість", "guest": "Гість",
"guests": "Гості", "guests": "Гості",
"former_user": "A Former User", "former_user": "Колишній користувач",
"updated.title": "Форум оновлено", "updated.title": "Форум оновлено",
"updated.message": "Форум було щойно оновлено до останньої версії. Клікніть тут, щоб оновити сторінку.", "updated.message": "Форум було щойно оновлено до останньої версії. Клікніть тут, щоб оновити сторінку.",
"privacy": "Приватність", "privacy": "Приватність",

View File

@@ -25,11 +25,11 @@
"details.latest_posts": "Останні пости", "details.latest_posts": "Останні пости",
"details.private": "Приватна", "details.private": "Приватна",
"details.disableJoinRequests": "Вимкнути запити на приєднання", "details.disableJoinRequests": "Вимкнути запити на приєднання",
"details.disableLeave": "Disallow users from leaving the group", "details.disableLeave": "Забороніть користувачам покидати групу",
"details.grant": "Надати/забрати права адміністратора", "details.grant": "Надати/забрати права адміністратора",
"details.kick": "Вигнати", "details.kick": "Вигнати",
"details.kick_confirm": "Ви впевнені, що бажаєте видалити цього користувача з групи?", "details.kick_confirm": "Ви впевнені, що бажаєте видалити цього користувача з групи?",
"details.add-member": "Add Member", "details.add-member": "Додати члена групи",
"details.owner_options": "Адміністрація групи", "details.owner_options": "Адміністрація групи",
"details.group_name": "Назва групи", "details.group_name": "Назва групи",
"details.member_count": "Кількість учасників", "details.member_count": "Кількість учасників",
@@ -37,8 +37,8 @@
"details.description": "Опис", "details.description": "Опис",
"details.badge_preview": "Попередній перегляд бейджа", "details.badge_preview": "Попередній перегляд бейджа",
"details.change_icon": "Змінити іконку", "details.change_icon": "Змінити іконку",
"details.change_label_colour": "Change Label Colour", "details.change_label_colour": "Змінити колір позначки",
"details.change_text_colour": "Change Text Colour", "details.change_text_colour": "Змінити колір тексту",
"details.badge_text": "Текст бейджа", "details.badge_text": "Текст бейджа",
"details.userTitleEnabled": "Показати бейдж", "details.userTitleEnabled": "Показати бейдж",
"details.private_help": "Якщо увімкнено, приєднання до групи вимагає підтвердження власника.", "details.private_help": "Якщо увімкнено, приєднання до групи вимагає підтвердження власника.",
@@ -49,11 +49,11 @@
"event.updated": "Деталі групи оновлено", "event.updated": "Деталі групи оновлено",
"event.deleted": "Група \"%1\" видалена", "event.deleted": "Група \"%1\" видалена",
"membership.accept-invitation": "Прийняти запрошення", "membership.accept-invitation": "Прийняти запрошення",
"membership.accept.notification_title": "You are now a member of <strong>%1</strong>", "membership.accept.notification_title": "Тепер ви є членом <strong>%1</strong>",
"membership.invitation-pending": "Запрошення в черзі", "membership.invitation-pending": "Запрошення в черзі",
"membership.join-group": "Приєднатися до групи", "membership.join-group": "Приєднатися до групи",
"membership.leave-group": "Покинути групу", "membership.leave-group": "Покинути групу",
"membership.leave.notification_title": "<strong>%1</strong> has left group <strong>%2</strong>", "membership.leave.notification_title": "<strong>%1</strong> покинув групу <strong>%2</strong>",
"membership.reject": "Відхилити", "membership.reject": "Відхилити",
"new-group.group_name": "Назва групи:", "new-group.group_name": "Назва групи:",
"upload-group-cover": "Завантажити обкладинку групи", "upload-group-cover": "Завантажити обкладинку групи",

View File

@@ -22,7 +22,7 @@
"chat.delete_message_confirm": "Ви впевнені, що хочете видалити це повідомлення?", "chat.delete_message_confirm": "Ви впевнені, що хочете видалити це повідомлення?",
"chat.retrieving-users": "Отримання користувачів...", "chat.retrieving-users": "Отримання користувачів...",
"chat.manage-room": "Управління чат кімнатами", "chat.manage-room": "Управління чат кімнатами",
"chat.add-user-help": "Search for users here. When selected, the user will be added to the chat. The new user will not be able to see chat messages written before they were added to the conversation. Only room owners (<i class=\"fa fa-star text-warning\"></i>) may remove users from chat rooms.", "chat.add-user-help": "Шукайте користувачів тут. Користувача можна додати до чату, обравши його. Нові користувачі не можуть бачити повідомлення, написані до того, як їх додали до розмови. Тільки власники кімнат можуть видаляти користувачів з кімнат.",
"chat.confirm-chat-with-dnd-user": "Користувач змінив свій статус на DnD (Не турбувати). Ви дійсно бажаєте надіслати йому повідомлення в чат?", "chat.confirm-chat-with-dnd-user": "Користувач змінив свій статус на DnD (Не турбувати). Ви дійсно бажаєте надіслати йому повідомлення в чат?",
"chat.rename-room": "Перейменувати Кімнату", "chat.rename-room": "Перейменувати Кімнату",
"chat.rename-placeholder": "Введіть назву своєї кімнати тут", "chat.rename-placeholder": "Введіть назву своєї кімнати тут",
@@ -33,10 +33,10 @@
"chat.in-room": "У цій кімнаті", "chat.in-room": "У цій кімнаті",
"chat.kick": "Штурхнути", "chat.kick": "Штурхнути",
"chat.show-ip": "Показати IP", "chat.show-ip": "Показати IP",
"chat.owner": "Room Owner", "chat.owner": "Власник кімнати",
"chat.system.user-join": "%1 has joined the room", "chat.system.user-join": "%1 зайшов в кімнату",
"chat.system.user-leave": "%1 has left the room", "chat.system.user-leave": "%1 покинув кімнату",
"chat.system.room-rename": "%2 has renamed this room: %1", "chat.system.room-rename": "%2 перейменував кімнату на: %1",
"composer.compose": "Редактор повідомлень", "composer.compose": "Редактор повідомлень",
"composer.show_preview": "Показати попередній перегляд", "composer.show_preview": "Показати попередній перегляд",
"composer.hide_preview": "Сховати попередній перегляд", "composer.hide_preview": "Сховати попередній перегляд",
@@ -50,7 +50,7 @@
"composer.formatting.italic": "Курсив", "composer.formatting.italic": "Курсив",
"composer.formatting.list": "Список", "composer.formatting.list": "Список",
"composer.formatting.strikethrough": "Закреслений", "composer.formatting.strikethrough": "Закреслений",
"composer.formatting.code": "Code", "composer.formatting.code": "Код",
"composer.formatting.link": "Посилання", "composer.formatting.link": "Посилання",
"composer.formatting.picture": "Зображення", "composer.formatting.picture": "Зображення",
"composer.upload-picture": "Завантажити зображення", "composer.upload-picture": "Завантажити зображення",

View File

@@ -8,7 +8,7 @@
"outgoing_link_message": "Ви залишаєте %1", "outgoing_link_message": "Ви залишаєте %1",
"continue_to": "Перейти до %1", "continue_to": "Перейти до %1",
"return_to": "Повернутись до %1", "return_to": "Повернутись до %1",
"new_notification": "You have a new notification", "new_notification": "У вас нове сповіщення",
"you_have_unread_notifications": "У вас немає непрочитаних сповіщень", "you_have_unread_notifications": "У вас немає непрочитаних сповіщень",
"all": "Всі", "all": "Всі",
"topics": "Теми", "topics": "Теми",
@@ -56,7 +56,7 @@
"notificationType_follow": "Коли хтось починає слідкувати за вами", "notificationType_follow": "Коли хтось починає слідкувати за вами",
"notificationType_new-chat": "Коли ви отримуєте повідомлення чату", "notificationType_new-chat": "Коли ви отримуєте повідомлення чату",
"notificationType_group-invite": "Коли ви отримуєте запрошення до групи", "notificationType_group-invite": "Коли ви отримуєте запрошення до групи",
"notificationType_group-request-membership": "When someone requests to join a group you own", "notificationType_group-request-membership": "Коли хтось подає запит на приєднання до групи, якою ви володієте",
"notificationType_new-register": "Коли когось додано до черги на реєстрацію", "notificationType_new-register": "Коли когось додано до черги на реєстрацію",
"notificationType_post-queue": "Коли новий пост знаходиться в черзі", "notificationType_post-queue": "Коли новий пост знаходиться в черзі",
"notificationType_new-post-flag": "Коли повідомлення позначено", "notificationType_new-post-flag": "Коли повідомлення позначено",

View File

@@ -6,10 +6,10 @@
"popular-month": "Популярні теми цього місяця", "popular-month": "Популярні теми цього місяця",
"popular-alltime": "Популярні теми за весь час", "popular-alltime": "Популярні теми за весь час",
"recent": "Свіжі теми", "recent": "Свіжі теми",
"top-day": "Top voted topics today", "top-day": "Найрейтинговіші теми сьогодні",
"top-week": "Top voted topics this week", "top-week": "Найрейтинговіші теми цього тижня",
"top-month": "Top voted topics this month", "top-month": "Найрейтинговіші теми цього місяця",
"top-alltime": "Top Voted Topics", "top-alltime": "Найрейтинговіші теми",
"moderator-tools": "Інструменти Модератора", "moderator-tools": "Інструменти Модератора",
"flagged-content": "Оскаржений вміст", "flagged-content": "Оскаржений вміст",
"ip-blacklist": "Чорний список IP адрес", "ip-blacklist": "Чорний список IP адрес",
@@ -43,10 +43,10 @@
"account/following": "Люди за котрими стежить %1", "account/following": "Люди за котрими стежить %1",
"account/followers": "Люди котрі стежать за %1", "account/followers": "Люди котрі стежать за %1",
"account/posts": "Пости написані %1", "account/posts": "Пости написані %1",
"account/latest-posts": "Latest posts made by %1", "account/latest-posts": "Останні дописи від %1",
"account/topics": "Теми створені %1", "account/topics": "Теми створені %1",
"account/groups": "Групи %1", "account/groups": "Групи %1",
"account/watched_categories": "%1's Watched Categories", "account/watched_categories": "Категорії, за якими спостерігає %1",
"account/bookmarks": "Закладки %1", "account/bookmarks": "Закладки %1",
"account/settings": "Налаштування користувача", "account/settings": "Налаштування користувача",
"account/watched": "Теми за якими стежить %1", "account/watched": "Теми за якими стежить %1",
@@ -56,7 +56,7 @@
"account/best": "Найкращі пости %1", "account/best": "Найкращі пости %1",
"account/blocks": "Заблоковані користувачі для %1", "account/blocks": "Заблоковані користувачі для %1",
"account/uploads": "Завантаження від %1", "account/uploads": "Завантаження від %1",
"account/sessions": "Login Sessions", "account/sessions": "Логін-сесії",
"confirm": "Електронну пошту підтверджено", "confirm": "Електронну пошту підтверджено",
"maintenance.text": "%1 в данний час на технічному обслуговувані. Завітайте, будь ласка, пізніше.", "maintenance.text": "%1 в данний час на технічному обслуговувані. Завітайте, будь ласка, пізніше.",
"maintenance.messageIntro": "Крім того, адміністратор залишив це повідомлення:", "maintenance.messageIntro": "Крім того, адміністратор залишив це повідомлення:",

View File

@@ -9,7 +9,7 @@
"repeat_password": "Підтвердіть пароль", "repeat_password": "Підтвердіть пароль",
"enter_email": "Будь ласка, введіть свою <strong>електронну пошту</strong> і ми надішлемо вам листа с інструкцією як скинути ваш обліковий запис.", "enter_email": "Будь ласка, введіть свою <strong>електронну пошту</strong> і ми надішлемо вам листа с інструкцією як скинути ваш обліковий запис.",
"enter_email_address": "Введіть електронну пошту", "enter_email_address": "Введіть електронну пошту",
"password_reset_sent": "If the specified address corresponds to an existing user account, a password reset email was sent. Please note that only one email will be sent per minute.", "password_reset_sent": "Якщо зазначена електронна адреса належить існуючому користувачеві, повідомлення для скидання паролю було надіслане на цю адресу. Майте на увазі, що тільки одне повідомлення може бути надіслане за хвилину.",
"invalid_email": "Невірна або неіснуюча електронна пошта!", "invalid_email": "Невірна або неіснуюча електронна пошта!",
"password_too_short": "Уведений пароль закороткий, оберіть, будь ласка, інший.", "password_too_short": "Уведений пароль закороткий, оберіть, будь ласка, інший.",
"passwords_do_not_match": "Паролі що ви ввели не співпадають.", "passwords_do_not_match": "Паролі що ви ввели не співпадають.",

View File

@@ -17,7 +17,7 @@
"at-most": "Щонайбільше", "at-most": "Щонайбільше",
"relevance": "Релевантність", "relevance": "Релевантність",
"post-time": "Час посту", "post-time": "Час посту",
"votes": "Votes", "votes": "Голоси",
"newer-than": "Новіші за", "newer-than": "Новіші за",
"older-than": "Старіші за", "older-than": "Старіші за",
"any-date": "Будь-яка дата", "any-date": "Будь-яка дата",
@@ -31,7 +31,7 @@
"sort-by": "Сортувати за", "sort-by": "Сортувати за",
"last-reply-time": "Час останньої відповіді", "last-reply-time": "Час останньої відповіді",
"topic-title": "Заголовок теми", "topic-title": "Заголовок теми",
"topic-votes": "Topic votes", "topic-votes": "Голоси за тему",
"number-of-replies": "Кількість відповідей", "number-of-replies": "Кількість відповідей",
"number-of-views": "Кількість переглядів", "number-of-views": "Кількість переглядів",
"topic-start-date": "Час початку теми", "topic-start-date": "Час початку теми",
@@ -44,5 +44,5 @@
"search-preferences-saved": "Налаштування пошуку збережено", "search-preferences-saved": "Налаштування пошуку збережено",
"search-preferences-cleared": "Налаштування пошуку очищені", "search-preferences-cleared": "Налаштування пошуку очищені",
"show-results-as": "Показати результати як", "show-results-as": "Показати результати як",
"see-more-results": "See more results (%1)" "see-more-results": "Дивитись більше результатів (%1)"
} }

View File

@@ -18,13 +18,13 @@
"last_reply_time": "Остання відповідь", "last_reply_time": "Остання відповідь",
"reply-as-topic": "Відповісти темою", "reply-as-topic": "Відповісти темою",
"guest-login-reply": "Увійти для відповіді", "guest-login-reply": "Увійти для відповіді",
"login-to-view": "🔒 Log in to view", "login-to-view": "🔒 Увійдіть щоб переглянути",
"edit": "Редагувати", "edit": "Редагувати",
"delete": "Видалити", "delete": "Видалити",
"purge": "Стерти", "purge": "Стерти",
"restore": "Відновити", "restore": "Відновити",
"move": "Перемістити", "move": "Перемістити",
"change-owner": "Change Owner", "change-owner": "Змінити Власника",
"fork": "Відгалужити", "fork": "Відгалужити",
"link": "Зв'язати", "link": "Зв'язати",
"share": "Поширити", "share": "Поширити",
@@ -66,7 +66,7 @@
"thread_tools.move": "Перемістити тему", "thread_tools.move": "Перемістити тему",
"thread_tools.move-posts": "Перемістити Пости", "thread_tools.move-posts": "Перемістити Пости",
"thread_tools.move_all": "Перемістити всі", "thread_tools.move_all": "Перемістити всі",
"thread_tools.change_owner": "Change Owner", "thread_tools.change_owner": "Змінити Власника",
"thread_tools.select_category": "Обрати Категорію", "thread_tools.select_category": "Обрати Категорію",
"thread_tools.fork": "Відгалужити тему", "thread_tools.fork": "Відгалужити тему",
"thread_tools.delete": "Видалити тему", "thread_tools.delete": "Видалити тему",
@@ -101,7 +101,7 @@
"delete_posts_instruction": "Тисніть пости які ви бажаєте видалити/стерти", "delete_posts_instruction": "Тисніть пости які ви бажаєте видалити/стерти",
"merge_topics_instruction": "Натисніть на теми, які потрібно об'єднати", "merge_topics_instruction": "Натисніть на теми, які потрібно об'єднати",
"move_posts_instruction": "Натисніть на пости, які ви хочете перемістити", "move_posts_instruction": "Натисніть на пости, які ви хочете перемістити",
"change_owner_instruction": "Click the posts you want to assign to another user", "change_owner_instruction": "Клікніть на дописи які ви хочете призначити іншому користувачу",
"composer.title_placeholder": "Уведіть заголовок теми...", "composer.title_placeholder": "Уведіть заголовок теми...",
"composer.handle_placeholder": "Ім'я", "composer.handle_placeholder": "Ім'я",
"composer.discard": "Скасувати", "composer.discard": "Скасувати",
@@ -134,6 +134,6 @@
"diffs.no-revisions-description": "Цей пост має <strong>%1</strong> версій.", "diffs.no-revisions-description": "Цей пост має <strong>%1</strong> версій.",
"diffs.current-revision": "поточна ревізія", "diffs.current-revision": "поточна ревізія",
"diffs.original-revision": "початкова ревізія", "diffs.original-revision": "початкова ревізія",
"timeago_later": "%1 later", "timeago_later": "%1 пізніше",
"timeago_earlier": "%1 earlier" "timeago_earlier": "%1 раніше"
} }

View File

@@ -25,17 +25,17 @@
"profile_views": "Переглядів профілю", "profile_views": "Переглядів профілю",
"reputation": "Репутація", "reputation": "Репутація",
"bookmarks": "Закладки", "bookmarks": "Закладки",
"watched_categories": "Watched categories", "watched_categories": "Категорії, за якими ви спостерігаєте",
"change_all": "Change All", "change_all": "Змінити Всі",
"watched": "Переглянуті", "watched": "Переглянуті",
"ignored": "Ігнорується", "ignored": "Ігнорується",
"default-category-watch-state": "Default category watch state", "default-category-watch-state": "Спостереження за категоріями за замовчанням",
"followers": "Відстежувачі", "followers": "Відстежувачі",
"following": "Відстежувані", "following": "Відстежувані",
"blocks": "Блокування", "blocks": "Блокування",
"block_toggle": "Увімкнути Блокування", "block_toggle": "Увімкнути Блокування",
"block_user": "Block User", "block_user": "Заблокувати Користувача",
"unblock_user": "Unblock User", "unblock_user": "Розблокувати Користувача",
"aboutme": "Про мене", "aboutme": "Про мене",
"signature": "Підпис", "signature": "Підпис",
"birthday": "День народження", "birthday": "День народження",
@@ -50,7 +50,7 @@
"change_picture": "Змінити зображення", "change_picture": "Змінити зображення",
"change_username": "Змінити ім'я користувача", "change_username": "Змінити ім'я користувача",
"change_email": "Змінити електронну пошту", "change_email": "Змінити електронну пошту",
"email_same_as_password": "Please enter your current password to continue &ndash; you've entered your new email again", "email_same_as_password": "Будь-ласка введіть ваш поточний пароль щоб продовжити &ndash; ви ввели ваш новий емейл знову",
"edit": "Редагувати", "edit": "Редагувати",
"edit-profile": "Редагувати профіль", "edit-profile": "Редагувати профіль",
"default_picture": "Стандартна іконка", "default_picture": "Стандартна іконка",
@@ -112,9 +112,9 @@
"no-sound": "Без звуку", "no-sound": "Без звуку",
"upvote-notif-freq": "Частота сповіщень позитивних відгуків", "upvote-notif-freq": "Частота сповіщень позитивних відгуків",
"upvote-notif-freq.all": "Всі позитивні відгуки", "upvote-notif-freq.all": "Всі позитивні відгуки",
"upvote-notif-freq.first": "First Per Post", "upvote-notif-freq.first": "Перше в дописі",
"upvote-notif-freq.everyTen": "Кожні 10 позитивних відгуків", "upvote-notif-freq.everyTen": "Кожні 10 позитивних відгуків",
"upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", "upvote-notif-freq.threshold": "На 1, 5, 10, 25, 50, 100, 150, 200...",
"upvote-notif-freq.logarithmic": "На 10, 100, 1000...", "upvote-notif-freq.logarithmic": "На 10, 100, 1000...",
"upvote-notif-freq.disabled": "Вимкнено", "upvote-notif-freq.disabled": "Вимкнено",
"browsing": "Налаштування перегляду", "browsing": "Налаштування перегляду",
@@ -125,7 +125,7 @@
"follow_topics_you_reply_to": "Підписуватися на теми в котрих ви відповідаєте", "follow_topics_you_reply_to": "Підписуватися на теми в котрих ви відповідаєте",
"follow_topics_you_create": "Підписуватися на теми які ви створюєте", "follow_topics_you_create": "Підписуватися на теми які ви створюєте",
"grouptitle": "Заголовок групи", "grouptitle": "Заголовок групи",
"group-order-help": "Select a group and use the arrows to order titles", "group-order-help": "Оберіть групу і використовуйте стрілки для зміни порядку заголовків",
"no-group-title": "Немає заголовка групи", "no-group-title": "Немає заголовка групи",
"select-skin": "Обрати стиль сайту", "select-skin": "Обрати стиль сайту",
"select-homepage": "Обрати домашню сторінку", "select-homepage": "Обрати домашню сторінку",
@@ -152,7 +152,7 @@
"info.moderation-note": "Коментар модератора", "info.moderation-note": "Коментар модератора",
"info.moderation-note.success": "Коментар модератора збережено", "info.moderation-note.success": "Коментар модератора збережено",
"info.moderation-note.add": "Додати коментар", "info.moderation-note.add": "Додати коментар",
"sessions.description": "This page allows you to view any active sessions on this forum and revoke them if necessary. You can revoke your own session by logging out of your account.", "sessions.description": "Ця сторінка дозволяє вам переглядати будь-які активні сесії на цьому форумі та видаляти їх якщо потрібно. Ви можете видалити вашу власну сесію, якщо вийдете зі свого акаунта.",
"consent.title": "Ваші Права &amp; Згода", "consent.title": "Ваші Права &amp; Згода",
"consent.lead": "Цей форум збирає та обробляє вашу особисту інформацію.", "consent.lead": "Цей форум збирає та обробляє вашу особисту інформацію.",
"consent.intro": "Ми використовуємо цю інформацію виключно з метою персоналізації вашої активності у цій спільноті, а також для з'єднання ваших постів з вашим особистим акаунтом. На етапі реєстрації ми просили вас надати ім'я користувача та електронну пошту, також ви можете (необов'язково) надати нам додаткову інформацію, щоб завершити створення свого користувацького профілю на цьому сайті.<br /><br />Ми зберігаємо цю інформацію протягом всього періоду життя вашого акаунту, і ви можете відкликати свою згоду у будь-який час, якщо видалите акаунт. У будь-який час ви можете отримати копію ваших особистих даних та внеску на цьому сайті через свою сторінку Права &amp; Згода.<br /><br />Якщо у вас виникли будь-які питання або зауваження, ми заохочуємо вас звернутись до команди Адміністраторів цього форуму.", "consent.intro": "Ми використовуємо цю інформацію виключно з метою персоналізації вашої активності у цій спільноті, а також для з'єднання ваших постів з вашим особистим акаунтом. На етапі реєстрації ми просили вас надати ім'я користувача та електронну пошту, також ви можете (необов'язково) надати нам додаткову інформацію, щоб завершити створення свого користувацького профілю на цьому сайті.<br /><br />Ми зберігаємо цю інформацію протягом всього періоду життя вашого акаунту, і ви можете відкликати свою згоду у будь-який час, якщо видалите акаунт. У будь-який час ви можете отримати копію ваших особистих даних та внеску на цьому сайті через свою сторінку Права &amp; Згода.<br /><br />Якщо у вас виникли будь-які питання або зауваження, ми заохочуємо вас звернутись до команди Адміністраторів цього форуму.",

View File

@@ -10,7 +10,7 @@
"filter-by": "Фільтрувати за", "filter-by": "Фільтрувати за",
"online-only": "Лише в мережі", "online-only": "Лише в мережі",
"invite": "Запросити", "invite": "Запросити",
"prompt-email": "Emails:", "prompt-email": "Емейли:",
"invitation-email-sent": "Лист із запрошенням відправлено %1", "invitation-email-sent": "Лист із запрошенням відправлено %1",
"user_list": "Список користувачів", "user_list": "Список користувачів",
"recent_topics": "Нещодавні теми", "recent_topics": "Нещодавні теми",

View File

@@ -1,32 +1,32 @@
{ {
"forum-traffic": "Forum Traffic", "forum-traffic": "Lưu lượng truy cập",
"page-views": "Lượt xem trang", "page-views": "Lượt xem trang",
"unique-visitors": "Khách truy cập duy nhất", "unique-visitors": "Khách truy cập duy nhất",
"new-users": "New Users", "new-users": "Người dùng mới",
"posts": "Bài viết", "posts": "Bài viết",
"topics": "Chủ đề", "topics": "Chủ đề",
"page-views-seven": "7 ngày trước", "page-views-seven": "7 ngày trước",
"page-views-thirty": "30 ngày trước", "page-views-thirty": "30 ngày trước",
"page-views-last-day": "24 giờ trước", "page-views-last-day": "24 giờ trước",
"page-views-custom": "Custom Date Range", "page-views-custom": "Tùy chỉnh phạm vi ngày",
"page-views-custom-start": "Range Start", "page-views-custom-start": "Phạm vi bắt đầu",
"page-views-custom-end": "Range End", "page-views-custom-end": "Phạm vi kết thúc",
"page-views-custom-help": "Enter a date range of page views you would like to view. If no date picker is available, the accepted format is <code>YYYY-MM-DD</code>", "page-views-custom-help": "Nhập phạm vi ngày của lượt xem trang bạn muốn xem. Nếu không có bộ chọn ngày, định dạng được chấp nhận là <code>YYYY-MM-DD</code>",
"page-views-custom-error": "Please enter a valid date range in the format <code>YYYY-MM-DD</code>", "page-views-custom-error": "Vui lòng nhập một phạm vi ngày hợp lệ trong định dạng <code>YYYY-MM-DD</code>",
"stats.yesterday": "Yesterday", "stats.yesterday": "Hôm qua",
"stats.today": "Today", "stats.today": "Hôm nay",
"stats.last-week": "Last Week", "stats.last-week": "Tuần trước",
"stats.this-week": "This Week", "stats.this-week": "Tuần này",
"stats.last-month": "Last Month", "stats.last-month": "Tháng trước",
"stats.this-month": "This Month", "stats.this-month": "Tháng này",
"stats.all": "All Time", "stats.all": "Mọi lúc",
"updates": "Updates", "updates": "Cập nhật",
"running-version": "You are running <strong>NodeBB v<span id=\"version\">%1</span></strong>.", "running-version": "Bạn đang chạy <strong>NodeBB v<span id=\"version\">%1</span></strong>.",
"keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", "keep-updated": "Luôn đảm bảo rằng NodeBB của bạn được cập nhật cho các bản vá bảo mật và sửa lỗi mới nhất.",
"up-to-date": "<p>You are <strong>up-to-date</strong> <i class=\"fa fa-check\"></i></p>", "up-to-date": "<p>Bạn đang <strong>bản mới nhất</strong> <i class=\"fa fa-check\"></i></p>",
"upgrade-available": "<p>A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.</p>", "upgrade-available": "<p>Phiên bản mới (v%1) đã được phát hành. Xem xét <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">nâng cấp NodeBB của bạn</a>.</p>",
"prerelease-upgrade-available": "<p>This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.</p>", "prerelease-upgrade-available": "<p>This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.</p>",
"prerelease-warning": "<p>This is a <strong>pre-release</strong> version of NodeBB. Unintended bugs may occur. <i class=\"fa fa-exclamation-triangle\"></i></p>", "prerelease-warning": "<p>This is a <strong>pre-release</strong> version of NodeBB. Unintended bugs may occur. <i class=\"fa fa-exclamation-triangle\"></i></p>",
"running-in-development": "<span>Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.</span>", "running-in-development": "<span>Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.</span>",
@@ -39,7 +39,7 @@
"search-plugin-not-installed": "Search Plugin not installed", "search-plugin-not-installed": "Search Plugin not installed",
"search-plugin-tooltip": "Install a search plugin from the plugin page in order to activate search functionality", "search-plugin-tooltip": "Install a search plugin from the plugin page in order to activate search functionality",
"control-panel": "System Control", "control-panel": "Điều khiển hệ thống",
"rebuild-and-restart": "Rebuild &amp; Restart", "rebuild-and-restart": "Rebuild &amp; Restart",
"restart": "Restart", "restart": "Restart",
"restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.",

View File

@@ -13,7 +13,7 @@
"reorder-plugins": "重新排序插件", "reorder-plugins": "重新排序插件",
"order-active": "排序生效插件", "order-active": "排序生效插件",
"dev-interested": "有兴趣为NodeBB开发插件", "dev-interested": "有兴趣为NodeBB开发插件",
"docs-info": "有关插件创作的完整文档可以在 <a target=\"_blank\" href=\"https://docs.nodebb.org/development/plugins/\">NodeBB 文档</a>中找到。", "docs-info": "有关插件创作的完整文档可以在 <a target=\"_blank\" href=\"https://docs.nodebb-cn.org/development\">NodeBB 文档</a>中找到。",
"order.description": "部分插件需要在其它插件启用之后才能完美运作。", "order.description": "部分插件需要在其它插件启用之后才能完美运作。",
"order.explanation": "插件将按照以下顺序载入,从上至下。", "order.explanation": "插件将按照以下顺序载入,从上至下。",

View File

@@ -15,8 +15,8 @@
"delete": "删除用户", "delete": "删除用户",
"purge": "删除用户和内容", "purge": "删除用户和内容",
"download-csv": "下载CSV", "download-csv": "下载CSV",
"manage-groups": "Manage Groups", "manage-groups": "管理用户组",
"add-group": "Add Group", "add-group": "添加至群组",
"invite": "邀请", "invite": "邀请",
"new": "新建用户", "new": "新建用户",

View File

@@ -32,8 +32,8 @@
"timestamp": "时间戳", "timestamp": "时间戳",
"timestamp.cut-off": "截止日期(天)", "timestamp.cut-off": "截止日期(天)",
"timestamp.cut-off-help": "日期&amp;时间将以相对方式 (例如“3小时前” / “5天前”) 显示,并且会依照访客语言时区转换。在某一时刻之后,可以切换该文本以显示本地化日期本身 (例如2016年11月5日15:30) 。<br /> <em> (默认值:<code> 30 </code>或一个月) 。 设置为0可始终显示日期留空以始终显示相对时间。</em>", "timestamp.cut-off-help": "日期&amp;时间将以相对方式 (例如“3小时前” / “5天前”) 显示,并且会依照访客语言时区转换。在某一时刻之后,可以切换该文本以显示本地化日期本身 (例如2016年11月5日15:30) 。<br /> <em> (默认值:<code> 30 </code>或一个月) 。 设置为0可始终显示日期留空以始终显示相对时间。</em>",
"timestamp.necro-threshold": "Necro Threshold (in days)", "timestamp.necro-threshold": "挖坟警告(单位:天)",
"timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: <code>7</code>, or one week). Set to 0 to disable.</em>", "timestamp.necro-threshold-help": "若进行回复的帖子最后回复的时间早于挖坟警告设定的天数,则在尝试回复前显示挖坟警告(默认:<code>7</code>天)。可以设置为 0 来禁用。</em>",
"teaser": "预览帖子", "teaser": "预览帖子",
"teaser.last-post": "最后&ndash; 显示最新的帖子,包括原帖,如果没有回复", "teaser.last-post": "最后&ndash; 显示最新的帖子,包括原帖,如果没有回复",
"teaser.last-reply": "最后&ndash; 显示最新回复,如果没有回复,则显示“无回复”占位符", "teaser.last-reply": "最后&ndash; 显示最新回复,如果没有回复,则显示“无回复”占位符",

View File

@@ -1,7 +1,7 @@
{ {
"category": "版块", "category": "版块",
"subcategories": "子版块", "subcategories": "子版块",
"new_topic_button": "主题", "new_topic_button": "发表主题",
"guest-login-post": "登录以发表", "guest-login-post": "登录以发表",
"no_topics": "<strong>此版块还没有任何内容。</strong><br />赶紧来发帖吧!", "no_topics": "<strong>此版块还没有任何内容。</strong><br />赶紧来发帖吧!",
"browsing": "正在浏览", "browsing": "正在浏览",

View File

@@ -6,15 +6,15 @@
"greeting_no_name": "您好", "greeting_no_name": "您好",
"greeting_with_name": "%1您好", "greeting_with_name": "%1您好",
"email.verify-your-email.subject": "请验证你的电子邮箱", "email.verify-your-email.subject": "请验证你的电子邮箱",
"email.verify.text1": "你的电子邮箱地址已成功更改!", "email.verify.text1": "你的电子邮箱地址已成功更改!",
"welcome.text1": "感谢您注册 %1 帐户!", "welcome.text1": "感谢您注册 %1 帐户!",
"welcome.text2": "我们需要在校验您注册时填写的电子邮箱地址后,才能激活您的帐户。", "welcome.text2": "在您验证您绑定的邮箱地址后,您的账户才能激活。",
"welcome.text3": "管理员接受了您的注册请求,请用您的用户名和密码登陆。", "welcome.text3": "管理员批准了您的注册申请,现在您可以登录您的账户了。",
"welcome.cta": "点击这里确认您的电子邮箱地址", "welcome.cta": "点击这里确认您的电子邮箱地址",
"invitation.text1": "%1 邀请您加入 %2", "invitation.text1": "%1 邀请您加入 %2",
"invitation.text2": "您的邀请将在 %1 天后过期。", "invitation.text2": "您的邀请将在 %1 天后过期。",
"invitation.cta": "点击这里新建账号", "invitation.cta": "点击这里新建账号",
"reset.text1": "可能由于您忘记了密码,我们收到了重置您帐户密码的申请。 如果您没有提交密码重置的请求,请忽略这封邮件。", "reset.text1": "可能您忘记了密码,我们收到了重置您帐户密码的申请。 如果您没有申请密码重置,请忽略这封邮件。",
"reset.text2": "如需继续重置密码,请点击下面的链接:", "reset.text2": "如需继续重置密码,请点击下面的链接:",
"reset.cta": "点击这里重置您的密码", "reset.cta": "点击这里重置您的密码",
"reset.notify.subject": "更改密码成功", "reset.notify.subject": "更改密码成功",
@@ -43,10 +43,10 @@
"test.text1": "这是一封测试邮件,用来验证 NodeBB 的邮件配置是否设置正确。", "test.text1": "这是一封测试邮件,用来验证 NodeBB 的邮件配置是否设置正确。",
"unsub.cta": "点击这里修改这些设置", "unsub.cta": "点击这里修改这些设置",
"unsubscribe": "退订", "unsubscribe": "退订",
"unsub.success": "将不再<strong>%1</strong>邮寄名单接受邮件", "unsub.success": "将不再收到来自<strong>%1</strong>邮寄名单邮件",
"banned.subject": "您已被封禁从 %1", "banned.subject": "您在 %1 的账户已被封禁",
"banned.text1": "户 %1 已被封禁从 %2.", "banned.text1": "您在 %2 的账户 %1 已被封禁",
"banned.text2": "封禁将持续到 %1.", "banned.text2": "本次封禁将在 %1 结束。",
"banned.text3": "这是您被封禁的原因:", "banned.text3": "这是您被封禁的原因:",
"closing": "谢谢!" "closing": "谢谢!"
} }

View File

@@ -2,11 +2,11 @@
"username-email": "用户名 / 邮箱", "username-email": "用户名 / 邮箱",
"username": "用户名", "username": "用户名",
"email": "邮件", "email": "邮件",
"remember_me": "记住我", "remember_me": "保持登录信息",
"forgot_password": "忘记密码?", "forgot_password": "忘记密码?",
"alternative_logins": "使用合作网站帐号登录", "alternative_logins": "使用合作网站帐号登录",
"failed_login_attempt": "登录失败", "failed_login_attempt": "登录失败",
"login_successful": "您已成功登录!", "login_successful": "您已成功登录!",
"dont_have_account": "没有帐号?", "dont_have_account": "没有帐号?",
"logged-out-due-to-inactivity": "由于长时间不活动,您的账号已被管理员从控制面板中注销" "logged-out-due-to-inactivity": "由于长时间不活动,您的账号已被管理员从控制面板中注销"
} }

View File

@@ -465,7 +465,7 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator', 'benchpress'
// Update the View as JSON button url // Update the View as JSON button url
var apiEl = $('#view-as-json'); var apiEl = $('#view-as-json');
var newHref = $.param({ var newHref = $.param({
units: units, units: units || 'hours',
until: until, until: until,
count: amount, count: amount,
}); });

View File

@@ -227,6 +227,22 @@ define('admin/manage/category', [
$('button[data-action="setParent"]').removeClass('hide'); $('button[data-action="setParent"]').removeClass('hide');
}); });
}); });
$('button[data-action="toggle"]').on('click', function () {
var payload = {};
var $this = $(this);
var disabled = $this.attr('data-disabled') === '1';
payload[ajaxify.data.category.cid] = {
disabled: disabled ? 0 : 1,
};
socket.emit('admin.categories.update', payload, function (err) {
if (err) {
return app.alertError(err.message);
}
$this.translateText(!disabled ? '[[admin/manage/categories:enable]]' : '[[admin/manage/categories:disable]]');
$this.toggleClass('btn-primary', !disabled).toggleClass('btn-danger', disabled);
$this.attr('data-disabled', disabled ? 0 : 1);
});
});
}; };
function modified(el) { function modified(el) {

View File

@@ -110,6 +110,7 @@ define('admin/manage/group', [
private: $('#group-private').is(':checked'), private: $('#group-private').is(':checked'),
hidden: $('#group-hidden').is(':checked'), hidden: $('#group-hidden').is(':checked'),
disableJoinRequests: $('#group-disableJoinRequests').is(':checked'), disableJoinRequests: $('#group-disableJoinRequests').is(':checked'),
disableLeave: $('#group-disableLeave').is(':checked'),
}, },
}, function (err) { }, function (err) {
if (err) { if (err) {

View File

@@ -308,6 +308,7 @@ define('admin/manage/users', ['translator', 'benchpress', 'autocomplete'], funct
}, },
}); });
}); });
return false;
}); });
} }

View File

@@ -398,7 +398,7 @@ app.cacheBuster = null;
} }
if (registerMessage) { if (registerMessage) {
$(document).ready(function () { $(document).ready(function () {
showAlert('register', decodeURIComponent(registerMessage)); showAlert('register', utils.escapeHTML(decodeURIComponent(registerMessage)));
registerMessage = false; registerMessage = false;
}); });
} }

View File

@@ -70,13 +70,10 @@ define('chat', [
roomData.silent = true; roomData.silent = true;
roomData.uid = app.user.uid; roomData.uid = app.user.uid;
roomData.isSelf = isSelf; roomData.isSelf = isSelf;
module.createModal(roomData, function (modal) { module.createModal(roomData, function () {
if (!isSelf) { if (!isSelf) {
updateTitleAndPlaySound(data.message.mid, username); updateTitleAndPlaySound(data.message.mid, username);
} }
if (!modal) {
addMessageToModal(data);
}
}); });
}); });
} }
@@ -87,7 +84,10 @@ define('chat', [
var username = data.message.fromUser.username; var username = data.message.fromUser.username;
var isSelf = data.self === 1; var isSelf = data.self === 1;
require(['forum/chats/messages'], function (ChatsMessages) { require(['forum/chats/messages'], function (ChatsMessages) {
// don't add if already added
if (!modal.find('[data-mid="' + data.message.messageId + '"]').length) {
ChatsMessages.appendChatMessage(modal.find('.chat-content'), data.message); ChatsMessages.appendChatMessage(modal.find('.chat-content'), data.message);
}
if (modal.is(':visible')) { if (modal.is(':visible')) {
taskbar.updateActive(modal.attr('data-uuid')); taskbar.updateActive(modal.attr('data-uuid'));
@@ -145,7 +145,7 @@ define('chat', [
require(['scrollStop', 'forum/chats', 'forum/chats/messages'], function (scrollStop, Chats, ChatsMessages) { require(['scrollStop', 'forum/chats', 'forum/chats/messages'], function (scrollStop, Chats, ChatsMessages) {
app.parseAndTranslate('chat', data, function (chatModal) { app.parseAndTranslate('chat', data, function (chatModal) {
if (module.modalExists(data.roomId)) { if (module.modalExists(data.roomId)) {
return callback(null); return callback(module.getModal(data.roomId));
} }
var uuid = utils.generateUUID(); var uuid = utils.generateUUID();
var dragged = false; var dragged = false;

View File

@@ -155,7 +155,7 @@ define('taskbar', ['benchpress', 'translator'], function (Benchpress, translator
var taskbarEl = $('<li />') var taskbarEl = $('<li />')
.addClass(data.options.className) .addClass(data.options.className)
.html('<a href="#"' + (data.options.image ? ' style="background-image: url(\'' + data.options.image + '\');"' : '') + '>' + .html('<a href="#"' + (data.options.image ? ' style="background-image: url(\'' + data.options.image + '\'); background-size: cover;"' : '') + '>' +
(data.options.icon ? '<i class="fa ' + data.options.icon + '"></i> ' : '') + (data.options.icon ? '<i class="fa ' + data.options.icon + '"></i> ' : '') +
'<span component="taskbar/title">' + title + '</span>' + '<span component="taskbar/title">' + title + '</span>' +
'</a>') '</a>')

View File

@@ -361,7 +361,7 @@
var nodes = descendantTextNodes(element); var nodes = descendantTextNodes(element);
var text = nodes.map(function (node) { var text = nodes.map(function (node) {
return node.nodeValue; return utils.escapeHTML(node.nodeValue);
}).join(' || '); }).join(' || ');
var attrNodes = attributes.reduce(function (prev, attr) { var attrNodes = attributes.reduce(function (prev, attr) {

View File

@@ -123,15 +123,17 @@ if (typeof window !== 'undefined') {
$.timeago.settings.allowFuture = true; $.timeago.settings.allowFuture = true;
var userLang = config.userLang.replace('_', '-'); var userLang = config.userLang.replace('_', '-');
var options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; var options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' };
var formatFn; var formatFn = function (date) {
if (typeof Intl === 'undefined') {
formatFn = function (date) {
return date.toLocaleString(userLang, options); return date.toLocaleString(userLang, options);
}; };
} else { try {
if (typeof Intl !== 'undefined') {
var dtFormat = new Intl.DateTimeFormat(userLang, options); var dtFormat = new Intl.DateTimeFormat(userLang, options);
formatFn = dtFormat.format; formatFn = dtFormat.format;
} }
} catch (err) {
console.error(err);
}
var iso; var iso;
var date; var date;

View File

@@ -8,7 +8,7 @@ const _ = require('lodash');
const versions = require('../../admin/versions'); const versions = require('../../admin/versions');
const db = require('../../database'); const db = require('../../database');
const meta = require('../../meta'); const meta = require('../../meta');
const analytics = require('../../analytics').async; const analytics = require('../../analytics');
const plugins = require('../../plugins'); const plugins = require('../../plugins');
const user = require('../../user'); const user = require('../../user');
const utils = require('../../utils'); const utils = require('../../utils');
@@ -93,7 +93,7 @@ dashboardController.getAnalytics = async (req, res, next) => {
} }
const method = req.query.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet; const method = req.query.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet;
let payload = await Promise.all(sets.map(async set => method('analytics:' + set, until, count))); let payload = await Promise.all(sets.map(set => method('analytics:' + set, until, count)));
payload = _.zipObject(sets, payload); payload = _.zipObject(sets, payload);
res.json({ res.json({

View File

@@ -86,8 +86,8 @@ apiController.loadConfig = async function (req) {
config.usePagination = settings.usePagination; config.usePagination = settings.usePagination;
config.topicsPerPage = settings.topicsPerPage; config.topicsPerPage = settings.topicsPerPage;
config.postsPerPage = settings.postsPerPage; config.postsPerPage = settings.postsPerPage;
config.userLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.userLang || config.defaultLang; config.userLang = validator.escape(String((req.query.lang ? req.query.lang : null) || settings.userLang || config.defaultLang));
config.acpLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.acpLang; config.acpLang = validator.escape(String((req.query.lang ? req.query.lang : null) || settings.acpLang));
config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab;
config.topicPostSort = settings.topicPostSort || config.topicPostSort; config.topicPostSort = settings.topicPostSort || config.topicPostSort;
config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort;

View File

@@ -316,8 +316,12 @@ authenticationController.doLogin = async function (req, uid) {
}; };
authenticationController.onSuccessfulLogin = async function (req, uid) { authenticationController.onSuccessfulLogin = async function (req, uid) {
// If already called once, return prematurely /*
if (req.res.locals.user) { * Older code required that this method be called from within the SSO plugin.
* That behaviour is no longer required, onSuccessfulLogin is now automatically
* called in NodeBB core. However, if already called, return prematurely
*/
if (req.loggedIn && !req.session.forceLogin) {
return true; return true;
} }

View File

@@ -22,6 +22,10 @@ exports.get = async function (req, res, callback) {
templateData: {}, templateData: {},
}); });
if (!data || !data.templateData) {
return callback(new Error('[[error:invalid-data]]'));
}
if (data.templateData.disabled) { if (data.templateData.disabled) {
res.render('', { res.render('', {
title: '[[modules:composer.compose]]', title: '[[modules:composer.compose]]',

View File

@@ -166,9 +166,9 @@ async function buildBreadcrumbs(topicData) {
} }
async function addTags(topicData, req, res) { async function addTags(topicData, req, res) {
var postAtIndex = topicData.posts.find(p => parseInt(p.index, 10) === parseInt(Math.max(0, req.params.post_index - 1), 10)); const postIndex = parseInt(req.params.post_index, 10) || 0;
const postAtIndex = topicData.posts.find(p => parseInt(p.index, 10) === parseInt(Math.max(0, postIndex - 1), 10));
var description = ''; let description = '';
if (postAtIndex && postAtIndex.content) { if (postAtIndex && postAtIndex.content) {
description = utils.stripHTMLTags(utils.decodeHTMLEntities(postAtIndex.content)); description = utils.stripHTMLTags(utils.decodeHTMLEntities(postAtIndex.content));
} }
@@ -329,10 +329,10 @@ topicsController.pagination = async function (req, res, callback) {
return helpers.notAllowed(req, res); return helpers.notAllowed(req, res);
} }
var postCount = topic.postcount; const postCount = topic.postcount;
var pageCount = Math.max(1, Math.ceil(postCount / settings.postsPerPage)); const pageCount = Math.max(1, Math.ceil(postCount / settings.postsPerPage));
var paginationData = pagination.create(currentPage, pageCount); const paginationData = pagination.create(currentPage, pageCount);
paginationData.rel.forEach(function (rel) { paginationData.rel.forEach(function (rel) {
rel.href = nconf.get('url') + '/topic/' + topic.slug + rel.href; rel.href = nconf.get('url') + '/topic/' + topic.slug + rel.href;
}); });

View File

@@ -25,6 +25,7 @@ events.types = [
'post-delete', 'post-delete',
'post-restore', 'post-restore',
'post-purge', 'post-purge',
'post-change-owner',
'topic-delete', 'topic-delete',
'topic-restore', 'topic-restore',
'topic-purge', 'topic-purge',

View File

@@ -10,7 +10,6 @@ const graceful = require('graceful-fs');
const util = require('util'); const util = require('util');
const readdirAsync = util.promisify(fs.readdir); const readdirAsync = util.promisify(fs.readdir);
const mkdirpAsync = util.promisify(mkdirp);
const copyFileAsync = util.promisify(fs.copyFile); const copyFileAsync = util.promisify(fs.copyFile);
const writeFleAsync = util.promisify(fs.writeFile); const writeFleAsync = util.promisify(fs.writeFile);
const statAsync = util.promisify(fs.stat); const statAsync = util.promisify(fs.stat);
@@ -33,7 +32,7 @@ file.saveFileToLocal = async function (filename, folder, tempPath) {
const uploadPath = path.join(nconf.get('upload_path'), folder, filename); const uploadPath = path.join(nconf.get('upload_path'), folder, filename);
winston.verbose('Saving file ' + filename + ' to : ' + uploadPath); winston.verbose('Saving file ' + filename + ' to : ' + uploadPath);
await mkdirpAsync(path.dirname(uploadPath)); await mkdirp(path.dirname(uploadPath));
await copyFileAsync(tempPath, uploadPath); await copyFileAsync(tempPath, uploadPath);
return { return {
url: '/assets/uploads/' + (folder ? folder + '/' : '') + filename, url: '/assets/uploads/' + (folder ? folder + '/' : '') + filename,

View File

@@ -19,6 +19,16 @@ const utils = require('../public/src/utils');
const Flags = module.exports; const Flags = module.exports;
Flags._constants = {
states: ['open', 'wip', 'resolved', 'rejected'],
state_class: {
open: 'info',
wip: 'warning',
resolved: 'success',
rejected: 'danger',
},
};
Flags.init = async function () { Flags.init = async function () {
// Query plugins for custom filter strategies and merge into core filter strategies // Query plugins for custom filter strategies and merge into core filter strategies
function prepareSets(sets, orSets, prefix, value) { function prepareSets(sets, orSets, prefix, value) {
@@ -162,13 +172,7 @@ Flags.list = async function (filters, uid) {
'icon:text': userObj['icon:text'], 'icon:text': userObj['icon:text'],
}, },
}; };
const stateToLabel = { flagObj.labelClass = Flags._constants.state_class[flagObj.state];
open: 'info',
wip: 'warning',
resolved: 'success',
rejected: 'danger',
};
flagObj.labelClass = stateToLabel[flagObj.state];
return Object.assign(flagObj, { return Object.assign(flagObj, {
description: validator.escape(String(flagObj.description)), description: validator.escape(String(flagObj.description)),
@@ -247,18 +251,21 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
timestamp = Date.now(); timestamp = Date.now();
doHistoryAppend = true; doHistoryAppend = true;
} }
const [exists, targetExists, targetUid, targetCid] = await Promise.all([ const [flagExists, targetExists, canFlag, targetUid, targetCid] = await Promise.all([
// Sanity checks // Sanity checks
Flags.exists(type, id, uid), Flags.exists(type, id, uid),
Flags.targetExists(type, id), Flags.targetExists(type, id),
Flags.canFlag(type, id, uid),
// Extra data for zset insertion // Extra data for zset insertion
Flags.getTargetUid(type, id), Flags.getTargetUid(type, id),
Flags.getTargetCid(type, id), Flags.getTargetCid(type, id),
]); ]);
if (exists) { if (flagExists) {
throw new Error('[[error:already-flagged]]'); throw new Error('[[error:already-flagged]]');
} else if (!targetExists) { } else if (!targetExists) {
throw new Error('[[error:invalid-data]]'); throw new Error('[[error:invalid-data]]');
} else if (!canFlag) {
throw new Error('[[error:no-privileges]]');
} }
const flagId = await db.incrObjectField('global', 'nextFlagId'); const flagId = await db.incrObjectField('global', 'nextFlagId');
@@ -303,6 +310,16 @@ Flags.exists = async function (type, id, uid) {
return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':')); return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':'));
}; };
Flags.canFlag = async function (type, id, uid) {
if (type === 'user') {
return true;
}
if (type === 'post') {
return await privileges.posts.can('topics:read', id, uid);
}
throw new Error('[[error:invalid-data]]');
};
Flags.getTarget = async function (type, id, uid) { Flags.getTarget = async function (type, id, uid) {
if (type === 'user') { if (type === 'user') {
const userData = await user.getUserData(id); const userData = await user.getUserData(id);
@@ -344,6 +361,7 @@ Flags.getTargetCid = async function (type, id) {
}; };
Flags.update = async function (flagId, uid, changeset) { Flags.update = async function (flagId, uid, changeset) {
const current = await db.getObjectFields('flag:' + flagId, ['state', 'assignee', 'type', 'targetId']);
const now = changeset.datetime || Date.now(); const now = changeset.datetime || Date.now();
const notifyAssignee = async function (assigneeId) { const notifyAssignee = async function (assigneeId) {
if (assigneeId === '' || parseInt(uid, 10) === parseInt(assigneeId, 10)) { if (assigneeId === '' || parseInt(uid, 10) === parseInt(assigneeId, 10)) {
@@ -359,23 +377,43 @@ Flags.update = async function (flagId, uid, changeset) {
}); });
await notifications.push(notifObj, [assigneeId]); await notifications.push(notifObj, [assigneeId]);
}; };
const isAssignable = async function (assigneeId) {
let allowed = false;
allowed = await user.isAdminOrGlobalMod(assigneeId);
// Retrieve existing flag data to compare for history-saving purposes // Mods are also allowed to be assigned, if flag target is post in uid's moderated cid
const current = await db.getObjectFields('flag:' + flagId, ['state', 'assignee']); if (!allowed && current.type === 'post') {
const cid = await posts.getCidByPid(current.targetId);
allowed = await user.isModerator(assigneeId, cid);
}
return allowed;
};
// Retrieve existing flag data to compare for history-saving/reference purposes
const tasks = []; const tasks = [];
for (var prop in changeset) { for (var prop in changeset) {
if (changeset.hasOwnProperty(prop)) { if (changeset.hasOwnProperty(prop)) {
if (current[prop] === changeset[prop]) { if (current[prop] === changeset[prop]) {
delete changeset[prop]; delete changeset[prop];
} else if (prop === 'state') { } else if (prop === 'state') {
if (!Flags._constants.states.includes(changeset[prop])) {
delete changeset[prop];
} else {
tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId)); tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId));
tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId)); tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId));
}
} else if (prop === 'assignee') { } else if (prop === 'assignee') {
/* eslint-disable-next-line */
if (!await isAssignable(parseInt(changeset[prop], 10))) {
delete changeset[prop];
} else {
tasks.push(db.sortedSetAdd('flags:byAssignee:' + changeset[prop], now, flagId)); tasks.push(db.sortedSetAdd('flags:byAssignee:' + changeset[prop], now, flagId));
tasks.push(notifyAssignee(changeset[prop])); tasks.push(notifyAssignee(changeset[prop]));
} }
} }
} }
}
if (!Object.keys(changeset).length) { if (!Object.keys(changeset).length) {
return; return;

View File

@@ -16,7 +16,7 @@ module.exports = function (Groups) {
const disableLeave = parseInt(data.disableLeave, 10) === 1 ? 1 : 0; const disableLeave = parseInt(data.disableLeave, 10) === 1 ? 1 : 0;
const isHidden = parseInt(data.hidden, 10) === 1; const isHidden = parseInt(data.hidden, 10) === 1;
validateGroupName(data.name); Groups.validateGroupName(data.name);
const exists = await meta.userOrGroupExists(data.name); const exists = await meta.userOrGroupExists(data.name);
if (exists) { if (exists) {
@@ -72,11 +72,15 @@ module.exports = function (Groups) {
Groups.isPrivilegeGroup(data.name); Groups.isPrivilegeGroup(data.name);
} }
function validateGroupName(name) { Groups.validateGroupName = function (name) {
if (!name) { if (!name) {
throw new Error('[[error:group-name-too-short]]'); throw new Error('[[error:group-name-too-short]]');
} }
if (typeof name !== 'string') {
throw new Error('[[error:invalid-group-name]]');
}
if (!Groups.isPrivilegeGroup(name) && name.length > meta.config.maximumGroupNameLength) { if (!Groups.isPrivilegeGroup(name) && name.length > meta.config.maximumGroupNameLength) {
throw new Error('[[error:group-name-too-long]]'); throw new Error('[[error:group-name-too-long]]');
} }
@@ -88,5 +92,5 @@ module.exports = function (Groups) {
if (name.includes('/') || !utils.slugify(name)) { if (name.includes('/') || !utils.slugify(name)) {
throw new Error('[[error:invalid-group-name]]'); throw new Error('[[error:invalid-group-name]]');
} }
} };
}; };

View File

@@ -53,6 +53,11 @@ module.exports = function (Groups) {
return Array.isArray(groupsData) && groupsData[0] ? groupsData[0] : null; return Array.isArray(groupsData) && groupsData[0] ? groupsData[0] : null;
}; };
Groups.getGroupField = async function (groupName, field) {
const groupData = await Groups.getGroupFields(groupName, [field]);
return groupData ? groupData[field] : null;
};
Groups.getGroupFields = async function (groupName, fields) { Groups.getGroupFields = async function (groupName, fields) {
const groups = await Groups.getGroupsFields([groupName], fields); const groups = await Groups.getGroupsFields([groupName], fields);
return groups ? groups[0] : null; return groups ? groups[0] : null;

View File

@@ -78,7 +78,8 @@ Groups.getGroupsBySort = async function (sort, start, stop) {
Groups.getNonPrivilegeGroups = async function (set, start, stop) { Groups.getNonPrivilegeGroups = async function (set, start, stop) {
let groupNames = await db.getSortedSetRevRange(set, start, stop); let groupNames = await db.getSortedSetRevRange(set, start, stop);
groupNames = groupNames.concat(Groups.ephemeralGroups).filter(groupName => !Groups.isPrivilegeGroup(groupName)); groupNames = groupNames.concat(Groups.ephemeralGroups).filter(groupName => !Groups.isPrivilegeGroup(groupName));
return await Groups.getGroupsData(groupNames); const groupsData = await Groups.getGroupsData(groupNames);
return groupsData.filter(Boolean);
}; };
Groups.getGroups = async function (set, start, stop) { Groups.getGroups = async function (set, start, stop) {

View File

@@ -54,7 +54,10 @@ module.exports = function (Groups) {
payload.disableLeave = values.disableLeave ? '1' : '0'; payload.disableLeave = values.disableLeave ? '1' : '0';
} }
if (values.hasOwnProperty('name')) {
await checkNameChange(groupName, values.name); await checkNameChange(groupName, values.name);
}
if (values.hasOwnProperty('private')) { if (values.hasOwnProperty('private')) {
await updatePrivacy(groupName, values.private); await updatePrivacy(groupName, values.private);
} }
@@ -125,6 +128,10 @@ module.exports = function (Groups) {
} }
async function checkNameChange(currentName, newName) { async function checkNameChange(currentName, newName) {
Groups.validateGroupName(newName);
if (Groups.isPrivilegeGroup(newName)) {
throw new Error('[[error:invalid-group-name]]');
}
const currentSlug = utils.slugify(currentName); const currentSlug = utils.slugify(currentName);
const newSlug = utils.slugify(newName); const newSlug = utils.slugify(newName);
if (currentName === newName || currentSlug === newSlug) { if (currentName === newName || currentSlug === newSlug) {

View File

@@ -1,9 +1,9 @@
'use strict'; 'use strict';
var meta = require('../meta'); const meta = require('../meta');
var plugins = require('../plugins'); const plugins = require('../plugins');
var db = require('../database'); const db = require('../database');
var user = require('../user'); const user = require('../user');
module.exports = function (Messaging) { module.exports = function (Messaging) {
Messaging.sendMessage = async (data) => { Messaging.sendMessage = async (data) => {
@@ -21,7 +21,7 @@ module.exports = function (Messaging) {
throw new Error('[[error:invalid-chat-message]]'); throw new Error('[[error:invalid-chat-message]]');
} }
const maximumChatMessageLength = (meta.config.maximumChatMessageLength || 1000); const maximumChatMessageLength = meta.config.maximumChatMessageLength || 1000;
const data = await plugins.fireHook('filter:messaging.checkContent', { content: content }); const data = await plugins.fireHook('filter:messaging.checkContent', { content: content });
content = String(data.content).trim(); content = String(data.content).trim();
if (!content) { if (!content) {

View File

@@ -1,9 +1,11 @@
'use strict'; 'use strict';
var db = require('../database'); const validator = require('validator');
var user = require('../user');
var utils = require('../utils'); const db = require('../database');
var plugins = require('../plugins'); const user = require('../user');
const utils = require('../utils');
const plugins = require('../plugins');
const intFields = ['timestamp', 'edited', 'fromuid', 'roomId', 'deleted', 'system']; const intFields = ['timestamp', 'edited', 'fromuid', 'roomId', 'deleted', 'system'];
@@ -79,6 +81,7 @@ module.exports = function (Messaging) {
messages = await Promise.all(messages.map(async (message) => { messages = await Promise.all(messages.map(async (message) => {
if (message.system) { if (message.system) {
message.content = validator.escape(String(message.content));
return message; return message;
} }

View File

@@ -11,6 +11,6 @@ module.exports = function (Messaging) {
throw new Error('[[error:chat-' + field + '-already]]'); throw new Error('[[error:chat-' + field + '-already]]');
} }
return await Messaging.setMessageField(mid, 'deleted', state); await Messaging.setMessageField(mid, 'deleted', state);
} }
}; };

View File

@@ -1,9 +1,9 @@
'use strict'; 'use strict';
var meta = require('../meta'); const meta = require('../meta');
var user = require('../user'); const user = require('../user');
var sockets = require('../socket.io'); const sockets = require('../socket.io');
module.exports = function (Messaging) { module.exports = function (Messaging) {
@@ -57,18 +57,18 @@ module.exports = function (Messaging) {
const [isAdmin, messageData] = await Promise.all([ const [isAdmin, messageData] = await Promise.all([
user.isAdministrator(uid), user.isAdministrator(uid),
Messaging.getMessageFields(messageId, ['fromuid', 'timestamp']), Messaging.getMessageFields(messageId, ['fromuid', 'timestamp', 'system']),
]); ]);
if (isAdmin) { if (isAdmin && !messageData.system) {
return; return;
} }
var chatConfigDuration = meta.config[durationConfig]; const chatConfigDuration = meta.config[durationConfig];
if (chatConfigDuration && Date.now() - messageData.timestamp > chatConfigDuration * 1000) { if (chatConfigDuration && Date.now() - messageData.timestamp > chatConfigDuration * 1000) {
throw new Error('[[error:chat-' + type + '-duration-expired, ' + meta.config[durationConfig] + ']]'); throw new Error('[[error:chat-' + type + '-duration-expired, ' + meta.config[durationConfig] + ']]');
} }
if (messageData.fromuid === parseInt(uid, 10)) { if (messageData.fromuid === parseInt(uid, 10) && !messageData.system) {
return; return;
} }

View File

@@ -139,14 +139,8 @@ Messaging.getRecentChats = async (callerUid, uid, start, stop) => {
}); });
}; };
Messaging.generateUsernames = (users, excludeUid) => { Messaging.generateUsernames = (users, excludeUid) => users.filter(user => user && parseInt(user.uid, 10) !== excludeUid)
users = users.filter(function (user) { .map(user => user.username).join(', ');
return user && parseInt(user.uid, 10) !== excludeUid;
});
return users.map(function (user) {
return user.username;
}).join(', ');
};
Messaging.getTeaser = async (uid, roomId) => { Messaging.getTeaser = async (uid, roomId) => {
const mid = await Messaging.getLatestUndeletedMessage(uid, roomId); const mid = await Messaging.getLatestUndeletedMessage(uid, roomId);

View File

@@ -20,10 +20,7 @@ module.exports = function (Messaging) {
}; };
Messaging.getRoomsData = async (roomIds) => { Messaging.getRoomsData = async (roomIds) => {
const roomData = await db.getObjects(roomIds.map(function (roomId) { const roomData = await db.getObjects(roomIds.map(roomId => 'chat:room:' + roomId));
return 'chat:room:' + roomId;
}));
modifyRoomData(roomData); modifyRoomData(roomData);
return roomData; return roomData;
}; };
@@ -53,6 +50,7 @@ module.exports = function (Messaging) {
db.sortedSetAdd('chat:room:' + roomId + ':uids', now, uid), db.sortedSetAdd('chat:room:' + roomId + ':uids', now, uid),
]); ]);
await Promise.all([ await Promise.all([
Messaging.addSystemMessage('user-join', uid, roomId), // chat owner should also get the user-join system message
Messaging.addUsersToRoom(uid, toUids, roomId), Messaging.addUsersToRoom(uid, toUids, roomId),
Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now), Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now),
]); ]);
@@ -61,7 +59,7 @@ module.exports = function (Messaging) {
}; };
Messaging.isUserInRoom = async (uid, roomId) => { Messaging.isUserInRoom = async (uid, roomId) => {
const inRoom = db.isSortedSetMember('chat:room:' + roomId + ':uids', uid); const inRoom = await db.isSortedSetMember('chat:room:' + roomId + ':uids', uid);
const data = await plugins.fireHook('filter:messaging.isUserInRoom', { uid: uid, roomId: roomId, inRoom: inRoom }); const data = await plugins.fireHook('filter:messaging.isUserInRoom', { uid: uid, roomId: roomId, inRoom: inRoom });
return data.inRoom; return data.inRoom;
}; };
@@ -113,6 +111,9 @@ module.exports = function (Messaging) {
}; };
Messaging.leaveRoom = async (uids, roomId) => { Messaging.leaveRoom = async (uids, roomId) => {
const isInRoom = await Promise.all(uids.map(uid => Messaging.isUserInRoom(uid, roomId)));
uids = uids.filter((uid, index) => isInRoom[index]);
const keys = uids const keys = uids
.map(uid => 'uid:' + uid + ':chat:rooms') .map(uid => 'uid:' + uid + ':chat:rooms')
.concat(uids.map(uid => 'uid:' + uid + ':chat:rooms:unread')); .concat(uids.map(uid => 'uid:' + uid + ':chat:rooms:unread'));
@@ -127,6 +128,9 @@ module.exports = function (Messaging) {
}; };
Messaging.leaveRooms = async (uid, roomIds) => { Messaging.leaveRooms = async (uid, roomIds) => {
const isInRoom = await Promise.all(roomIds.map(roomId => Messaging.isUserInRoom(uid, roomId)));
roomIds = roomIds.filter((roomId, index) => isInRoom[index]);
const roomKeys = roomIds.map(roomId => 'chat:room:' + roomId + ':uids'); const roomKeys = roomIds.map(roomId => 'chat:room:' + roomId + ':uids');
await Promise.all([ await Promise.all([
db.sortedSetsRemove(roomKeys, uid), db.sortedSetsRemove(roomKeys, uid),
@@ -192,7 +196,7 @@ module.exports = function (Messaging) {
}; };
Messaging.canReply = async (roomId, uid) => { Messaging.canReply = async (roomId, uid) => {
const inRoom = db.isSortedSetMember('chat:room:' + roomId + ':uids', uid); const inRoom = await db.isSortedSetMember('chat:room:' + roomId + ':uids', uid);
const data = await plugins.fireHook('filter:messaging.canReply', { uid: uid, roomId: roomId, inRoom: inRoom, canReply: inRoom }); const data = await plugins.fireHook('filter:messaging.canReply', { uid: uid, roomId: roomId, inRoom: inRoom, canReply: inRoom });
return data.canReply; return data.canReply;
}; };

View File

@@ -3,6 +3,7 @@
const ipaddr = require('ipaddr.js'); const ipaddr = require('ipaddr.js');
const winston = require('winston'); const winston = require('winston');
const _ = require('lodash'); const _ = require('lodash');
const validator = require('validator');
const db = require('../database'); const db = require('../database');
const pubsub = require('../pubsub'); const pubsub = require('../pubsub');
@@ -128,7 +129,7 @@ Blacklist.validate = function (rules) {
} }
if (!addr || whitelist.includes(rule)) { if (!addr || whitelist.includes(rule)) {
invalid.push(rule); invalid.push(validator.escape(rule));
return false; return false;
} }

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const os = require('os');
const async = require('async'); const async = require('async');
const winston = require('winston'); const winston = require('winston');
const nconf = require('nconf'); const nconf = require('nconf');
@@ -150,7 +151,14 @@ exports.build = function (targets, options, callback) {
targets = targets.split(','); targets = targets.split(',');
} }
var parallel = !nconf.get('series') && !options.series; let series = nconf.get('series') || options.series;
if (series === undefined) {
// Detect # of CPUs and select strategy as appropriate
winston.verbose('[build] Querying CPU core count for build strategy');
const cpus = os.cpus();
series = cpus.length < 4;
winston.verbose('[build] System returned ' + cpus.length + ' cores, opting for ' + (series ? 'series' : 'parallel') + ' build strategy');
}
targets = targets targets = targets
// get full target name // get full target name
@@ -195,14 +203,14 @@ exports.build = function (targets, options, callback) {
require('./minifier').maxThreads = threads - 1; require('./minifier').maxThreads = threads - 1;
} }
if (parallel) { if (!series) {
winston.info('[build] Building in parallel mode'); winston.info('[build] Building in parallel mode');
} else { } else {
winston.info('[build] Building in series mode'); winston.info('[build] Building in series mode');
} }
startTime = Date.now(); startTime = Date.now();
buildTargets(targets, parallel, next); buildTargets(targets, !series, next);
}, },
function (next) { function (next) {
totalTime = (Date.now() - startTime) / 1000; totalTime = (Date.now() - startTime) / 1000;

View File

@@ -5,7 +5,6 @@ const path = require('path');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
const winston = require('winston'); const winston = require('winston');
const util = require('util'); const util = require('util');
const mkdirpAsync = util.promisify(mkdirp);
const writeFileAsync = util.promisify(fs.writeFile); const writeFileAsync = util.promisify(fs.writeFile);
const readFileAsync = util.promisify(fs.readFile); const readFileAsync = util.promisify(fs.readFile);
@@ -19,7 +18,7 @@ function generate() {
} }
exports.write = async function write() { exports.write = async function write() {
await mkdirpAsync(path.dirname(filePath)); await mkdirp(path.dirname(filePath));
await writeFileAsync(filePath, generate()); await writeFileAsync(filePath, generate());
}; };

View File

@@ -3,7 +3,16 @@
var path = require('path'); var path = require('path');
var async = require('async'); var async = require('async');
var fs = require('fs'); var fs = require('fs');
const util = require('util');
var mkdirp = require('mkdirp'); var mkdirp = require('mkdirp');
var mkdirpCallback;
if (mkdirp.hasOwnProperty('native')) {
mkdirpCallback = util.callbackify(mkdirp);
} else {
mkdirpCallback = mkdirp;
mkdirp = util.promisify(mkdirp);
}
var rimraf = require('rimraf'); var rimraf = require('rimraf');
var file = require('../file'); var file = require('../file');
@@ -119,7 +128,7 @@ function minifyModules(modules, fork, callback) {
return prev; return prev;
}, []); }, []);
async.each(moduleDirs, mkdirp, function (err) { async.each(moduleDirs, mkdirpCallback, function (err) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@@ -156,7 +165,7 @@ function linkModules(callback) {
async.parallel({ async.parallel({
dir: function (cb) { dir: function (cb) {
mkdirp(path.dirname(destPath), function (err) { mkdirpCallback(path.dirname(destPath), function (err) {
cb(err); cb(err);
}); });
}, },
@@ -272,7 +281,7 @@ JS.linkStatics = function (callback) {
var sourceDir = plugins.staticDirs[mappedPath]; var sourceDir = plugins.staticDirs[mappedPath];
var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath); var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);
mkdirp(path.dirname(destDir), function (err) { mkdirpCallback(path.dirname(destDir), function (err) {
if (err) { if (err) {
return next(err); return next(err);
} }
@@ -343,7 +352,7 @@ JS.buildBundle = function (target, fork, callback) {
getBundleScriptList(target, next); getBundleScriptList(target, next);
}, },
function (files, next) { function (files, next) {
mkdirp(path.join(__dirname, '../../build/public'), function (err) { mkdirpCallback(path.join(__dirname, '../../build/public'), function (err) {
next(err, files); next(err, files);
}); });
}, },

View File

@@ -2,12 +2,12 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const mkdirp = require('mkdirp'); const util = require('util');
let mkdirp = require('mkdirp');
mkdirp = mkdirp.hasOwnProperty('native') ? mkdirp : util.promisify(mkdirp);
const rimraf = require('rimraf'); const rimraf = require('rimraf');
const _ = require('lodash'); const _ = require('lodash');
const util = require('util');
const mkdirpAsync = util.promisify(mkdirp);
const rimrafAsync = util.promisify(rimraf); const rimrafAsync = util.promisify(rimraf);
const writeFileAsync = util.promisify(fs.writeFile); const writeFileAsync = util.promisify(fs.writeFile);
const readFileAsync = util.promisify(fs.readFile); const readFileAsync = util.promisify(fs.readFile);
@@ -46,7 +46,7 @@ async function getTranslationMetadata() {
// save a list of languages to `${buildLanguagesPath}/metadata.json` // save a list of languages to `${buildLanguagesPath}/metadata.json`
// avoids readdirs later on // avoids readdirs later on
await mkdirpAsync(buildLanguagesPath); await mkdirp(buildLanguagesPath);
const result = { const result = {
languages: languages, languages: languages,
namespaces: namespaces, namespaces: namespaces,
@@ -59,7 +59,7 @@ async function writeLanguageFile(language, namespace, translations) {
const dev = global.env === 'development'; const dev = global.env === 'development';
const filePath = path.join(buildLanguagesPath, language, namespace + '.json'); const filePath = path.join(buildLanguagesPath, language, namespace + '.json');
await mkdirpAsync(path.dirname(filePath)); await mkdirp(path.dirname(filePath));
await writeFileAsync(filePath, JSON.stringify(translations, null, dev ? 2 : 0)); await writeFileAsync(filePath, JSON.stringify(translations, null, dev ? 2 : 0));
} }

View File

@@ -2,14 +2,14 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const rimraf = require('rimraf');
const mkdirp = require('mkdirp');
const util = require('util'); const util = require('util');
const rimraf = require('rimraf');
let mkdirp = require('mkdirp');
mkdirp = mkdirp.hasOwnProperty('native') ? mkdirp : util.promisify(mkdirp);
const readdirAsync = util.promisify(fs.readdir); const readdirAsync = util.promisify(fs.readdir);
const rimrafAsync = util.promisify(rimraf); const rimrafAsync = util.promisify(rimraf);
const mkdirpAsync = util.promisify(mkdirp);
const writeFileAsync = util.promisify(fs.writeFile); const writeFileAsync = util.promisify(fs.writeFile);
const file = require('../file'); const file = require('../file');
@@ -70,7 +70,7 @@ Sounds.build = async function build() {
map.unshift({}); map.unshift({});
map = Object.assign.apply(null, map); map = Object.assign.apply(null, map);
await rimrafAsync(soundsPath); await rimrafAsync(soundsPath);
await mkdirpAsync(soundsPath); await mkdirp(soundsPath);
await writeFileAsync(path.join(soundsPath, 'fileMap.json'), JSON.stringify(map)); await writeFileAsync(path.join(soundsPath, 'fileMap.json'), JSON.stringify(map));

View File

@@ -1,11 +1,11 @@
'use strict'; 'use strict';
const mkdirp = require('mkdirp'); const util = require('util');
let mkdirp = require('mkdirp');
mkdirp = mkdirp.hasOwnProperty('native') ? mkdirp : util.promisify(mkdirp);
const rimraf = require('rimraf'); const rimraf = require('rimraf');
const winston = require('winston'); const winston = require('winston');
const path = require('path'); const path = require('path');
const util = require('util');
const fs = require('fs'); const fs = require('fs');
const fsReadFile = util.promisify(fs.readFile); const fsReadFile = util.promisify(fs.readFile);
const fsWriteFile = util.promisify(fs.writeFile); const fsWriteFile = util.promisify(fs.writeFile);
@@ -123,10 +123,9 @@ Templates.compileTemplate = compileTemplate;
async function compile() { async function compile() {
const _rimraf = util.promisify(rimraf); const _rimraf = util.promisify(rimraf);
const _mkdirp = util.promisify(mkdirp);
await _rimraf(viewsPath); await _rimraf(viewsPath);
await _mkdirp(viewsPath); await mkdirp(viewsPath);
let files = await db.getSortedSetRange('plugins:active', 0, -1); let files = await db.getSortedSetRange('plugins:active', 0, -1);
files = await getTemplateDirs(files); files = await getTemplateDirs(files);
@@ -137,7 +136,7 @@ async function compile() {
let imported = await fsReadFile(filePath, 'utf8'); let imported = await fsReadFile(filePath, 'utf8');
imported = await processImports(files, name, imported); imported = await processImports(files, name, imported);
await _mkdirp(path.join(viewsPath, path.dirname(name))); await mkdirp(path.join(viewsPath, path.dirname(name)));
await fsWriteFile(path.join(viewsPath, name), imported); await fsWriteFile(path.join(viewsPath, name), imported);
const compiled = await Benchpress.precompile(imported, { minify: global.env !== 'development' }); const compiled = await Benchpress.precompile(imported, { minify: global.env !== 'development' });

View File

@@ -148,8 +148,13 @@ module.exports = function (Plugins) {
if (!Array.isArray(hookList) || !hookList.length) { if (!Array.isArray(hookList) || !hookList.length) {
return; return;
} }
// don't bubble errors from these hooks, so bad plugins don't stop startup
const noErrorHooks = ['static:app.load', 'static:assets.prepare', 'static:app.preload'];
await async.each(hookList, function (hookObj, next) { await async.each(hookList, function (hookObj, next) {
if (typeof hookObj.method === 'function') { if (typeof hookObj.method !== 'function') {
return next();
}
let timedOut = false; let timedOut = false;
const timeoutId = setTimeout(function () { const timeoutId = setTimeout(function () {
winston.warn('[plugins] Callback timed out, hook \'' + hook + '\' in plugin \'' + hookObj.id + '\''); winston.warn('[plugins] Callback timed out, hook \'' + hook + '\' in plugin \'' + hookObj.id + '\'');
@@ -157,16 +162,14 @@ module.exports = function (Plugins) {
next(); next();
}, 5000); }, 5000);
const onError = (err) => { const callback = (err) => {
clearTimeout(timeoutId);
if (err) {
winston.error('[plugins] Error executing \'' + hook + '\' in plugin \'' + hookObj.id + '\''); winston.error('[plugins] Error executing \'' + hook + '\' in plugin \'' + hookObj.id + '\'');
winston.error(err); winston.error(err.stack);
clearTimeout(timeoutId); }
next();
};
const callback = (...args) => {
clearTimeout(timeoutId);
if (!timedOut) { if (!timedOut) {
next(...args); next(noErrorHooks.includes(hook) ? null : err);
} }
}; };
try { try {
@@ -174,14 +177,11 @@ module.exports = function (Plugins) {
if (utils.isPromise(returned)) { if (utils.isPromise(returned)) {
returned.then( returned.then(
payload => setImmediate(callback, null, payload), payload => setImmediate(callback, null, payload),
err => setImmediate(onError, err) err => setImmediate(callback, err)
); );
} }
} catch (err) { } catch (err) {
onError(err); callback(err);
}
} else {
next();
} }
}); });
} }

View File

@@ -123,7 +123,7 @@ module.exports = function (Posts) {
}; };
Posts.changeOwner = async function (pids, toUid) { Posts.changeOwner = async function (pids, toUid) {
const exists = user.exists(toUid); const exists = await user.exists(toUid);
if (!exists) { if (!exists) {
throw new Error('[[error:no-user]]'); throw new Error('[[error:no-user]]');
} }
@@ -163,6 +163,7 @@ module.exports = function (Posts) {
reduceCounters(postsByUser), reduceCounters(postsByUser),
updateTopicPosters(postData, toUid), updateTopicPosters(postData, toUid),
]); ]);
return postData;
}; };
async function reduceCounters(postsByUser) { async function reduceCounters(postsByUser) {

View File

@@ -110,6 +110,7 @@ module.exports = function (privileges) {
return await utils.promiseParallel({ return await utils.promiseParallel({
categories: categories.getCategoriesFields(cids, ['disabled']), categories: categories.getCategoriesFields(cids, ['disabled']),
allowedTo: helpers.isUserAllowedTo(privilege, uid, cids), allowedTo: helpers.isUserAllowedTo(privilege, uid, cids),
view_deleted: helpers.isUserAllowedTo('posts:view_deleted', uid, cids),
isAdmin: user.isAdministrator(uid), isAdmin: user.isAdministrator(uid),
}); });
}; };

View File

@@ -88,16 +88,17 @@ module.exports = function (privileges) {
cids = _.uniq(cids); cids = _.uniq(cids);
const results = await privileges.categories.getBase(privilege, cids, uid); const results = await privileges.categories.getBase(privilege, cids, uid);
cids = cids.filter(function (cid, index) { const allowedCids = cids.filter(function (cid, index) {
return !results.categories[index].disabled && return !results.categories[index].disabled &&
(results.allowedTo[index] || results.isAdmin); (results.allowedTo[index] || results.isAdmin);
}); });
const cidsSet = new Set(cids); const cidsSet = new Set(allowedCids);
const canViewDeleted = _.zipObject(cids, results.view_deleted);
pids = postData.filter(function (post) { pids = postData.filter(function (post) {
return post.topic && cidsSet.has(post.topic.cid) && return post.topic && cidsSet.has(post.topic.cid) &&
((!post.topic.deleted && !post.deleted) || results.isAdmin); ((!post.topic.deleted && !post.deleted) || canViewDeleted[post.topic.cid] || results.isAdmin);
}).map(post => post.pid); }).map(post => post.pid);
const data = await plugins.fireHook('filter:privileges.posts.filter', { const data = await plugins.fireHook('filter:privileges.posts.filter', {

View File

@@ -68,14 +68,15 @@ module.exports = function (privileges) {
} }
const topicsData = await topics.getTopicsFields(tids, ['tid', 'cid', 'deleted']); const topicsData = await topics.getTopicsFields(tids, ['tid', 'cid', 'deleted']);
let cids = _.uniq(topicsData.map(topic => topic.cid)); const cids = _.uniq(topicsData.map(topic => topic.cid));
const results = await privileges.categories.getBase(privilege, cids, uid); const results = await privileges.categories.getBase(privilege, cids, uid);
cids = cids.filter((cid, index) => !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin)); const allowedCids = cids.filter((cid, index) => !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin));
const cidsSet = new Set(cids); const cidsSet = new Set(allowedCids);
const canViewDeleted = _.zipObject(cids, results.view_deleted);
tids = topicsData.filter(t => cidsSet.has(t.cid) && (!t.deleted || results.isAdmin)).map(t => t.tid); tids = topicsData.filter(t => cidsSet.has(t.cid) && (!t.deleted || canViewDeleted[t.cid] || results.isAdmin)).map(t => t.tid);
const data = await plugins.fireHook('filter:privileges.topics.filter', { const data = await plugins.fireHook('filter:privileges.topics.filter', {
privilege: privilege, privilege: privilege,
@@ -115,7 +116,7 @@ module.exports = function (privileges) {
}; };
privileges.topics.canDelete = async function (tid, uid) { privileges.topics.canDelete = async function (tid, uid) {
const topicData = await topics.getTopicFields(tid, ['cid', 'postcount']); const topicData = await topics.getTopicFields(tid, ['uid', 'cid', 'postcount', 'deleterUid']);
const [isModerator, isAdministrator, isOwner, allowedTo] = await Promise.all([ const [isModerator, isAdministrator, isOwner, allowedTo] = await Promise.all([
user.isModerator(uid, topicData.cid), user.isModerator(uid, topicData.cid),
user.isAdministrator(uid), user.isAdministrator(uid),
@@ -135,7 +136,8 @@ module.exports = function (privileges) {
throw new Error(langKey); throw new Error(langKey);
} }
return allowedTo[0] && (isOwner || isModerator); const deleterUid = topicData.deleterUid;
return allowedTo[0] && ((isOwner && (deleterUid === 0 || deleterUid === topicData.uid)) || isModerator);
}; };
privileges.topics.canEdit = async function (tid, uid) { privileges.topics.canEdit = async function (tid, uid) {

View File

@@ -21,7 +21,7 @@ SocketFlags.create = async function (socket, data) {
const flagObj = await flags.create(data.type, data.id, socket.uid, data.reason); const flagObj = await flags.create(data.type, data.id, socket.uid, data.reason);
await flags.notify(flagObj, socket.uid); await flags.notify(flagObj, socket.uid);
return flagObj; return flagObj.flagId;
}; };
SocketFlags.update = async function (socket, data) { SocketFlags.update = async function (socket, data) {

View File

@@ -22,6 +22,10 @@ SocketGroups.join = async (socket, data) => {
throw new Error('[[error:invalid-uid]]'); throw new Error('[[error:invalid-uid]]');
} }
if (typeof data.groupName !== 'string') {
throw new Error('[[error:invalid-group-name]]');
}
if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) { if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) {
throw new Error('[[error:not-allowed]]'); throw new Error('[[error:not-allowed]]');
} }
@@ -66,6 +70,10 @@ SocketGroups.leave = async (socket, data) => {
throw new Error('[[error:invalid-uid]]'); throw new Error('[[error:invalid-uid]]');
} }
if (typeof data.groupName !== 'string') {
throw new Error('[[error:invalid-group-name]]');
}
if (data.groupName === 'administrators') { if (data.groupName === 'administrators') {
throw new Error('[[error:cant-remove-self-as-admin]]'); throw new Error('[[error:cant-remove-self-as-admin]]');
} }
@@ -104,6 +112,9 @@ SocketGroups.addMember = async (socket, data) => {
}; };
async function isOwner(socket, data) { async function isOwner(socket, data) {
if (typeof data.groupName !== 'string') {
throw new Error('[[error:invalid-group-name]]');
}
const results = await utils.promiseParallel({ const results = await utils.promiseParallel({
isAdmin: await user.isAdministrator(socket.uid), isAdmin: await user.isAdministrator(socket.uid),
isGlobalModerator: await user.isGlobalModerator(socket.uid), isGlobalModerator: await user.isGlobalModerator(socket.uid),
@@ -118,6 +129,9 @@ async function isOwner(socket, data) {
} }
async function isInvited(socket, data) { async function isInvited(socket, data) {
if (typeof data.groupName !== 'string') {
throw new Error('[[error:invalid-group-name]]');
}
const invited = await groups.isInvited(socket.uid, data.groupName); const invited = await groups.isInvited(socket.uid, data.groupName);
if (!invited) { if (!invited) {
throw new Error('[[error:not-invited]]'); throw new Error('[[error:not-invited]]');
@@ -171,6 +185,9 @@ SocketGroups.rejectAll = async (socket, data) => {
}; };
async function acceptRejectAll(method, socket, data) { async function acceptRejectAll(method, socket, data) {
if (typeof data.groupName !== 'string') {
throw new Error('[[error:invalid-group-name]]');
}
const uids = await groups.getPending(data.groupName); const uids = await groups.getPending(data.groupName);
await Promise.all(uids.map(async (uid) => { await Promise.all(uids.map(async (uid) => {
await method(socket, { groupName: data.groupName, toUid: uid }); await method(socket, { groupName: data.groupName, toUid: uid });
@@ -251,7 +268,7 @@ SocketGroups.kick = async (socket, data) => {
SocketGroups.create = async (socket, data) => { SocketGroups.create = async (socket, data) => {
if (!socket.uid) { if (!socket.uid) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} else if (groups.isPrivilegeGroup(data.name)) { } else if (typeof data.name !== 'string' || groups.isPrivilegeGroup(data.name)) {
throw new Error('[[error:invalid-group-name]]'); throw new Error('[[error:invalid-group-name]]');
} }
@@ -260,6 +277,7 @@ SocketGroups.create = async (socket, data) => {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
data.ownerUid = socket.uid; data.ownerUid = socket.uid;
data.system = false;
const groupData = await groups.create(data); const groupData = await groups.create(data);
logGroupEvent(socket, 'group-create', { logGroupEvent(socket, 'group-create', {
groupName: data.name, groupName: data.name,
@@ -338,7 +356,6 @@ SocketGroups.cover.update = async (socket, data) => {
if (!socket.uid) { if (!socket.uid) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
await canModifyGroup(socket.uid, data.groupName); await canModifyGroup(socket.uid, data.groupName);
return await groups.updateCover(socket.uid, data); return await groups.updateCover(socket.uid, data);
}; };
@@ -353,12 +370,17 @@ SocketGroups.cover.remove = async (socket, data) => {
}; };
async function canModifyGroup(uid, groupName) { async function canModifyGroup(uid, groupName) {
if (typeof groupName !== 'string') {
throw new Error('[[error:invalid-group-name]]');
}
const results = await utils.promiseParallel({ const results = await utils.promiseParallel({
isOwner: groups.ownership.isOwner(uid, groupName), isOwner: groups.ownership.isOwner(uid, groupName),
isAdminOrGlobalMod: user.isAdminOrGlobalMod(uid), system: groups.getGroupField(groupName, 'system'),
isAdmin: user.isAdministrator(uid),
isGlobalMod: user.isGlobalModerator(uid),
}); });
if (!results.isOwner && !results.isAdminOrGlobalMod) { if (!(results.isOwner || results.isAdmin || (results.isGlobalMod && !results.system))) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
} }

View File

@@ -113,11 +113,14 @@ SocketModules.chats.getUsersInRoom = async function (socket, data) {
if (!data || !data.roomId) { if (!data || !data.roomId) {
throw new Error('[[error:invalid-data]]'); throw new Error('[[error:invalid-data]]');
} }
const [userData, isOwner] = await Promise.all([ const [isUserInRoom, isOwner, userData] = await Promise.all([
Messaging.getUsersInRoom(data.roomId, 0, -1), Messaging.isUserInRoom(socket.uid, data.roomId),
Messaging.isRoomOwner(socket.uid, data.roomId), Messaging.isRoomOwner(socket.uid, data.roomId),
Messaging.getUsersInRoom(data.roomId, 0, -1),
]); ]);
if (!isUserInRoom) {
throw new Error('[[error:no-privileges]]');
}
userData.forEach((user) => { userData.forEach((user) => {
user.canKick = (parseInt(user.uid, 10) !== parseInt(socket.uid, 10)) && isOwner; user.canKick = (parseInt(user.uid, 10) !== parseInt(socket.uid, 10)) && isOwner;
}); });

View File

@@ -167,11 +167,22 @@ module.exports = function (SocketPosts) {
if (!data || !Array.isArray(data.pids) || !data.toUid) { if (!data || !Array.isArray(data.pids) || !data.toUid) {
throw new Error('[[error:invalid-data]]'); throw new Error('[[error:invalid-data]]');
} }
const isAdminOrGlobalMod = user.isAdminOrGlobalMod(socket.uid); const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid);
if (!isAdminOrGlobalMod) { if (!isAdminOrGlobalMod) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
await posts.changeOwner(data.pids, data.toUid); var postData = await posts.changeOwner(data.pids, data.toUid);
var logs = postData.map(({ pid, uid, cid }) => (events.log({
type: 'post-change-owner',
uid: socket.uid,
ip: socket.ip,
targetUid: data.toUid,
pid: pid,
originalUid: uid,
cid: cid,
})));
await Promise.all(logs);
}; };
}; };

View File

@@ -11,6 +11,7 @@ const intFields = [
'tid', 'cid', 'uid', 'mainPid', 'postcount', 'tid', 'cid', 'uid', 'mainPid', 'postcount',
'viewcount', 'deleted', 'locked', 'pinned', 'viewcount', 'deleted', 'locked', 'pinned',
'timestamp', 'upvotes', 'downvotes', 'lastposttime', 'timestamp', 'upvotes', 'downvotes', 'lastposttime',
'deleterUid',
]; ];
module.exports = function (Topics) { module.exports = function (Topics) {
@@ -90,6 +91,10 @@ function modifyTopic(topic, fields) {
escapeTitle(topic); escapeTitle(topic);
if (topic.hasOwnProperty('thumb')) {
topic.thumb = validator.escape(String(topic.thumb));
}
if (topic.hasOwnProperty('timestamp')) { if (topic.hasOwnProperty('timestamp')) {
topic.timestampISO = utils.toISOString(topic.timestamp); topic.timestampISO = utils.toISOString(topic.timestamp);
} }

View File

@@ -50,8 +50,7 @@ Topics.getTopics = async function (tids, options) {
} }
tids = await privileges.topics.filterTids('topics:read', tids, uid); tids = await privileges.topics.filterTids('topics:read', tids, uid);
const topics = await Topics.getTopicsByTids(tids, options); return await Topics.getTopicsByTids(tids, options);
return topics;
}; };
Topics.getTopicsByTids = async function (tids, options) { Topics.getTopicsByTids = async function (tids, options) {

View File

@@ -127,7 +127,7 @@ module.exports = function (User) {
}; };
User.isPasswordValid = function (password, minStrength) { User.isPasswordValid = function (password, minStrength) {
minStrength = minStrength || meta.config.minimumPasswordStrength; minStrength = (minStrength || minStrength === 0) ? minStrength : meta.config.minimumPasswordStrength;
// Sanity checks: Checks if defined and is string // Sanity checks: Checks if defined and is string
if (!password || !utils.isPasswordValid(password)) { if (!password || !utils.isPasswordValid(password)) {

View File

@@ -30,11 +30,8 @@ module.exports = function (User) {
const now = Date.now(); const now = Date.now();
const isArray = Array.isArray(uid); const isArray = Array.isArray(uid);
uid = isArray ? uid : [uid]; uid = isArray ? uid : [uid];
const lastonline = db.sortedSetScores('users:online', uid); const lastonline = await db.sortedSetScores('users:online', uid);
const isOnline = uid.map(function (uid, index) { const isOnline = uid.map((uid, index) => (now - lastonline[index]) < (meta.config.onlineCutoff * 60000));
return (now - lastonline[index]) < (meta.config.onlineCutoff * 60000);
});
return isArray ? isOnline : isOnline[0]; return isArray ? isOnline : isOnline[0];
}; };
}; };

View File

@@ -23,7 +23,7 @@ module.exports = function (User) {
hashedPassword = ''; hashedPassword = '';
} }
User.isPasswordValid(password); User.isPasswordValid(password, 0);
await User.auth.logAttempt(uid, ip); await User.auth.logAttempt(uid, ip);
const ok = await Password.compare(password, hashedPassword); const ok = await Password.compare(password, hashedPassword);
if (ok) { if (ok) {

View File

@@ -1,10 +1,13 @@
'use strict'; 'use strict';
const validator = require('validator');
const meta = require('../meta'); const meta = require('../meta');
const db = require('../database'); const db = require('../database');
const plugins = require('../plugins'); const plugins = require('../plugins');
const notifications = require('../notifications'); const notifications = require('../notifications');
const languages = require('../languages');
module.exports = function (User) { module.exports = function (User) {
User.getSettings = async function (uid) { User.getSettings = async function (uid) {
@@ -55,7 +58,8 @@ module.exports = function (User) {
settings.upvoteNotifFreq = getSetting(settings, 'upvoteNotifFreq', 'all'); settings.upvoteNotifFreq = getSetting(settings, 'upvoteNotifFreq', 'all');
settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1; settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1;
settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1;
settings.bootswatchSkin = settings.bootswatchSkin || ''; settings.bootswatchSkin = validator.escape(String(settings.bootswatchSkin || ''));
settings.homePageRoute = validator.escape(String(settings.homePageRoute || ''));
settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1;
settings.categoryWatchState = getSetting(settings, 'categoryWatchState', 'notwatching'); settings.categoryWatchState = getSetting(settings, 'categoryWatchState', 'notwatching');
@@ -87,6 +91,13 @@ module.exports = function (User) {
throw new Error('[[error:invalid-pagination-value, 2, ' + maxTopicsPerPage + ']]'); throw new Error('[[error:invalid-pagination-value, 2, ' + maxTopicsPerPage + ']]');
} }
const languageCodes = await languages.listCodes();
if (data.userLang && !languageCodes.includes(data.userLang)) {
throw new Error('[[error:invalid-language]]');
}
if (data.acpLang && !languageCodes.includes(data.acpLang)) {
throw new Error('[[error:invalid-language]]');
}
data.userLang = data.userLang || meta.config.defaultLang; data.userLang = data.userLang || meta.config.defaultLang;
plugins.fireHook('action:user.saveSettings', { uid: uid, settings: data }); plugins.fireHook('action:user.saveSettings', { uid: uid, settings: data });

View File

@@ -18,10 +18,14 @@ module.exports = function (User) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
const finalPath = path.join(nconf.get('upload_path'), uploadName);
if (!finalPath.startsWith(nconf.get('upload_path'))) {
throw new Error('[[error:invalid-path]]');
}
winston.verbose('[user/deleteUpload] Deleting ' + uploadName); winston.verbose('[user/deleteUpload] Deleting ' + uploadName);
await Promise.all([ await Promise.all([
file.delete(path.join(nconf.get('upload_path'), uploadName)), file.delete(finalPath),
file.delete(path.join(nconf.get('upload_path'), path.dirname(uploadName), path.basename(uploadName, path.extname(uploadName)) + '-resized' + path.extname(uploadName))), file.delete(file.appendToFileName(finalPath, '-resized')),
]); ]);
await db.sortedSetRemove('uid:' + uid + ':uploads', uploadName); await db.sortedSetRemove('uid:' + uid + ':uploads', uploadName);
}; };

Some files were not shown because too many files have changed in this diff Show More